show unread badge in nav items and improve logic
This commit is contained in:
parent
ce54cf4317
commit
188dce0e30
12 changed files with 89 additions and 50 deletions
|
|
@ -59,6 +59,8 @@ import { useGetRoomName } from '../../../hooks/storage/useGetRoomName';
|
|||
import { useGetRoomAvatar } from '../../../hooks/storage/useGetRoomAvatar';
|
||||
import { useObservableValue } from '../../../hooks/useObservableValue';
|
||||
import { useClient } from '../../../hooks/useClient';
|
||||
import { unreadRoomsAtom } from '../../../state/room/roomToUnread';
|
||||
import { useRoomsUnread } from '../../../state/hooks/unread';
|
||||
|
||||
type DirectMenuProps = {
|
||||
requestClose: () => void;
|
||||
|
|
@ -187,6 +189,7 @@ export function Direct() {
|
|||
const getRoomAvatar = useGetRoomAvatar(getRoomHeroes, roomToHeroMembersResult.value);
|
||||
const roomToJoinRule = useObservableValue(ct.roomToJoinRule);
|
||||
const roomToNotifications = useRoomsNotifications(directs, roomToNotificationsAtom);
|
||||
const unreadRooms = useRoomsUnread(directs, unreadRoomsAtom);
|
||||
|
||||
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -288,6 +291,7 @@ export function Direct() {
|
|||
linkPath={getDirectRoomPath(ct.roomToAlias.read.get(roomId) ?? roomId)}
|
||||
notificationMode={getRoomNotificationMode(notificationPreferences, roomId)}
|
||||
notifications={notifications}
|
||||
unread={unreadRooms.has(roomId)}
|
||||
/>
|
||||
</VirtualTile>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ import {
|
|||
import { roomToNotificationsAtom } from '../../../state/room/roomToNotifications';
|
||||
import { useGetRoomAvatar } from '../../../hooks/storage/useGetRoomAvatar';
|
||||
import { unreadRoomsAtom } from '../../../state/room/roomToUnread';
|
||||
import { useRoomsUnread } from '../../../state/hooks/unread';
|
||||
|
||||
type HomeMenuProps = {
|
||||
requestClose: () => void;
|
||||
|
|
@ -212,7 +213,7 @@ export function Home() {
|
|||
const getRoomAvatar = useGetRoomAvatar(getRoomHeroes, roomToHeroMembersResult.value);
|
||||
const roomToJoinRule = useObservableValue(ct.roomToJoinRule);
|
||||
const roomToNotifications = useRoomsNotifications(rooms, roomToNotificationsAtom);
|
||||
const unreadRooms = useAtomValue(unreadRoomsAtom);
|
||||
const unreadRooms = useRoomsUnread(rooms, unreadRoomsAtom);
|
||||
|
||||
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
||||
const navigate = useNavigate();
|
||||
|
|
|
|||
|
|
@ -71,8 +71,6 @@ import { GetContentCallback, MessageEvent, StateEvent } from '../../../../types/
|
|||
import { useMatrixEventRenderer } from '../../../hooks/useMatrixEventRenderer';
|
||||
import * as customHtmlCss from '../../../styles/CustomHtml.css';
|
||||
import { useRoomNavigate } from '../../../hooks/useRoomNavigate';
|
||||
import { useRoomUnread } from '../../../state/hooks/unread';
|
||||
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
|
||||
import { markAsRead } from '../../../utils/notifications';
|
||||
import { ContainerColor } from '../../../styles/ContainerColor.css';
|
||||
import { VirtualTile } from '../../../components/virtualizer';
|
||||
|
|
@ -224,7 +222,7 @@ function RoomNotificationsGroupComp({
|
|||
}: RoomNotificationsGroupProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||
// const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
|
||||
|
||||
const powerLevels = usePowerLevels(room);
|
||||
const creators = useRoomCreators(room);
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ import {
|
|||
} from '../../../state/hooks/roomNotifications';
|
||||
import { getJoinRoomStateId } from '../../../../client/storage/collections';
|
||||
import { useAllJoinedRoomsSet } from '../../../hooks/useGetRoom';
|
||||
import { unreadRoomsAtom } from '../../../state/room/roomToUnread';
|
||||
import { useRoomsUnread } from '../../../state/hooks/unread';
|
||||
|
||||
type SpaceMenuProps = {
|
||||
requestClose: () => void;
|
||||
|
|
@ -405,6 +407,7 @@ export function Space() {
|
|||
const getRoomAvatar = useGetRoomAvatar(getRoomHeroes, roomToHeroMembersResult.value);
|
||||
const roomToJoinRule = useObservableValue(ct.roomToJoinRule);
|
||||
const roomToNotifications = useRoomsNotifications(rooms, roomToNotificationsAtom);
|
||||
const unreadRooms = useRoomsUnread(rooms, unreadRoomsAtom);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: hierarchy.length,
|
||||
|
|
@ -512,6 +515,7 @@ export function Space() {
|
|||
linkPath={getToLink(roomId)}
|
||||
notificationMode={getRoomNotificationMode(notificationPreferences, roomId)}
|
||||
notifications={notifications}
|
||||
unread={unreadRooms.has(roomId)}
|
||||
/>
|
||||
</VirtualTile>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,22 @@
|
|||
import { Unread } from '../../../types/matrix/room';
|
||||
import { roomToUnreadAtom } from '../room/roomToUnread';
|
||||
import { useCallback } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { selectAtom } from 'jotai/utils';
|
||||
import { unreadRoomsAtom } from '../room/roomToUnread';
|
||||
|
||||
export const useRoomsUnread = (
|
||||
rooms: string[],
|
||||
roomToUnreadAtm: typeof roomToUnreadAtom
|
||||
): Unread | undefined => undefined;
|
||||
unreadRooms: typeof unreadRoomsAtom
|
||||
): Set<string> => {
|
||||
const selector = useCallback(
|
||||
(unreads: Set<string>) => new Set<string>(rooms.filter((roomId) => unreads.has(roomId))),
|
||||
[rooms]
|
||||
);
|
||||
|
||||
export const useRoomUnread = (
|
||||
roomId: string,
|
||||
roomToUnreadAtm: typeof roomToUnreadAtom
|
||||
): Unread | undefined => undefined;
|
||||
return useAtomValue(selectAtom(unreadRooms, selector));
|
||||
};
|
||||
|
||||
export const useRoomUnread = (roomId: string, unreadRooms: typeof unreadRoomsAtom): boolean => {
|
||||
const selector = useCallback((unreads: Set<string>) => unreads.has(roomId), [roomId]);
|
||||
|
||||
return useAtomValue(selectAtom(unreadRooms, selector));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ export const useBindRoomToNotificationsAtom = (
|
|||
const unread = unreadState.status === AsyncStatus.Success ? unreadState.data : undefined;
|
||||
|
||||
const processNotifications = useCallback(() => {
|
||||
console.log('roomToNotifications');
|
||||
const notifications = unread?.notifications.read ?? [];
|
||||
const threadNotifications = unread?.threadNotifications.read ?? [];
|
||||
const { mute } = roomsNotificationPreferences;
|
||||
|
|
|
|||
|
|
@ -1,42 +1,28 @@
|
|||
import { atom, useSetAtom } from 'jotai';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { ThreadId } from '../../../types/matrix/room';
|
||||
import { useObservableValue, useObservableValuePromise } from '../../hooks/useObservableValue';
|
||||
import { useObservableValue } from '../../hooks/useObservableValue';
|
||||
import { getReadReceiptId } from '../../../client/storage/collections';
|
||||
import { ClientContext } from '../../../client/context';
|
||||
|
||||
export const roomToUnreadAtom = atom(new Map());
|
||||
|
||||
export const unreadRoomsAtom = atom<Set<string>>(new Set<string>());
|
||||
|
||||
export const useBindUnreadRoomsAtom = (ct: ClientContext, unreadRooms: typeof unreadRoomsAtom) => {
|
||||
const setUnreadRooms = useSetAtom(unreadRooms);
|
||||
const roomToLatestEvent = useObservableValue(ct.roomToLatestEvent);
|
||||
|
||||
const receiptKeys: string[] = useMemo(() => {
|
||||
const keys: string[] = [];
|
||||
roomToLatestEvent.forEach((_, roomId) => {
|
||||
keys.push(getReadReceiptId(roomId, ct.session.userId, ThreadId.Main));
|
||||
});
|
||||
|
||||
return keys;
|
||||
}, [roomToLatestEvent, ct]);
|
||||
|
||||
const receiptsResult = useObservableValuePromise(
|
||||
useMemo(() => ct.storage.readReceipt.observableRead.getMultiple(receiptKeys), [ct, receiptKeys])
|
||||
);
|
||||
const userReadReceipts = useObservableValue(ct.userReadReceipt);
|
||||
|
||||
useEffect(() => {
|
||||
const receipts = receiptsResult.value;
|
||||
if (receipts) {
|
||||
const rooms: string[] = [];
|
||||
Array.from(roomToLatestEvent.entries()).forEach(([roomId, event], index) => {
|
||||
const receipt = receipts[index];
|
||||
if (!receipt || receipt.ts > event.origin_server_ts) return;
|
||||
rooms.push(roomId);
|
||||
});
|
||||
const unreads = new Set<string>();
|
||||
|
||||
setUnreadRooms(new Set(rooms));
|
||||
}
|
||||
}, [setUnreadRooms, receiptsResult, roomToLatestEvent]);
|
||||
Array.from(roomToLatestEvent.entries()).forEach(([roomId, event]) => {
|
||||
const receipt = userReadReceipts.find(
|
||||
(r) => getReadReceiptId(roomId, ct.session.userId, ThreadId.Main) === r.id
|
||||
);
|
||||
if (!receipt || receipt.ts > event.origin_server_ts) return;
|
||||
unreads.add(roomId);
|
||||
});
|
||||
|
||||
setUnreadRooms(unreads);
|
||||
}, [ct, setUnreadRooms, userReadReceipts, roomToLatestEvent]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
JoinRoomStateObject,
|
||||
KnockRoomStateObject,
|
||||
LeaveRoomStateObject,
|
||||
ReadReceiptObject,
|
||||
sessionValue,
|
||||
} from './storage/collections';
|
||||
import { Storage } from './storage';
|
||||
|
|
@ -117,6 +118,7 @@ export type ClientContext = {
|
|||
roomToCreators: ObservableValue<Map<string, Set<string>>>;
|
||||
roomToVersion: ObservableValue<Map<string, string>>;
|
||||
roomToLatestEvent: ObservableValue<Map<string, MessageEventWithoutRoomId>>;
|
||||
userReadReceipt: ObservableValue<ReadReceiptObject[]>;
|
||||
roomToTimeline: ObservableValue<Map<string, Timeline>>;
|
||||
|
||||
pendingMemberRooms: ObservableValue<string[]>;
|
||||
|
|
@ -836,6 +838,21 @@ export const createRoomToLatestEventObservable = async (
|
|||
return roomToLatestEventObservable;
|
||||
};
|
||||
|
||||
export const createUserReceiptObservable = async (
|
||||
session: Session,
|
||||
storage: Storage,
|
||||
disposer: Disposer
|
||||
): Promise<ObservableValue<ReadReceiptObject[]>> => {
|
||||
const receiptObservable = await storage.readReceipt.observableRead.queryIndex(
|
||||
'userId',
|
||||
session.userId
|
||||
);
|
||||
|
||||
disposer.collect(receiptObservable);
|
||||
|
||||
return receiptObservable;
|
||||
};
|
||||
|
||||
export const createLogoutObservable = (
|
||||
syncObservable: ObservableValue<SyncValue>,
|
||||
disposer: Disposer
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
createSyncTokenObservable,
|
||||
createVerificationRequestObservable,
|
||||
createBackupInfoObservable,
|
||||
createUserReceiptObservable,
|
||||
} from './context';
|
||||
import { createStorage } from './storage';
|
||||
import { getAuthJSONRequest } from './request';
|
||||
|
|
@ -69,6 +70,7 @@ export const initializeClient = async (session: Session): Promise<ClientContext>
|
|||
const roomToType = createRoomToTypeObservable(state, disposer);
|
||||
const roomToReplacement = createRoomToReplacementObservable(state, disposer);
|
||||
const roomToLatestEvent = await createRoomToLatestEventObservable(storage, disposer);
|
||||
const userReadReceipt = await createUserReceiptObservable(session, storage, disposer);
|
||||
|
||||
const syncTokenObservable = await createSyncTokenObservable(storage, disposer);
|
||||
const pendingMemberRooms = await createPendingMemberRoomObservable(storage, disposer);
|
||||
|
|
@ -129,6 +131,7 @@ export const initializeClient = async (session: Session): Promise<ClientContext>
|
|||
roomToType,
|
||||
roomToReplacement,
|
||||
roomToLatestEvent,
|
||||
userReadReceipt,
|
||||
roomToCreators,
|
||||
roomToVersion,
|
||||
roomToTimeline,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { IDBPDatabase } from 'idb';
|
||||
import { IDBPDatabase, IDBPTransaction } from 'idb';
|
||||
import { StorageDriver } from 'neurone/lib/storage';
|
||||
import { Collection, Entry } from 'neurone/lib/storage/collection';
|
||||
import { ReceiptType } from 'neurone/lib/api/types';
|
||||
|
|
@ -13,7 +13,11 @@ export const getReadReceiptId = (
|
|||
threadId: string | ThreadId
|
||||
): string => encodeKey(roomId, userId, threadId);
|
||||
|
||||
export const prepareReadReceiptStore = (idb: IDBPDatabase, oldVersion: number) => {
|
||||
export const prepareReadReceiptStore = (
|
||||
idb: IDBPDatabase,
|
||||
oldVersion: number,
|
||||
transaction: IDBPTransaction<any, any, 'versionchange'>
|
||||
) => {
|
||||
if (oldVersion < 1) {
|
||||
const store = idb.createObjectStore(READ_RECEIPT_STORE_NAME, {
|
||||
keyPath: 'id',
|
||||
|
|
@ -22,11 +26,17 @@ export const prepareReadReceiptStore = (idb: IDBPDatabase, oldVersion: number) =
|
|||
store.createIndex('roomId', 'roomId', { unique: false });
|
||||
store.createIndex('eventId', 'eventId', { unique: false });
|
||||
}
|
||||
|
||||
if (oldVersion < 2) {
|
||||
const store = transaction.objectStore(READ_RECEIPT_STORE_NAME);
|
||||
|
||||
store.createIndex('userId', 'userId', { unique: false });
|
||||
}
|
||||
};
|
||||
|
||||
export type ReadReceiptObject = Entry<
|
||||
'id',
|
||||
'eventId' | 'roomId',
|
||||
'userId' | 'eventId' | 'roomId',
|
||||
{
|
||||
id: string;
|
||||
roomId: string;
|
||||
|
|
@ -38,7 +48,11 @@ export type ReadReceiptObject = Entry<
|
|||
}
|
||||
>;
|
||||
|
||||
export type ReadReceiptCollection = Collection<'id', 'eventId' | 'roomId', ReadReceiptObject>;
|
||||
export type ReadReceiptCollection = Collection<
|
||||
'id',
|
||||
'eventId' | 'roomId' | 'userId',
|
||||
ReadReceiptObject
|
||||
>;
|
||||
|
||||
export const createReadReceiptCollection = (driver: StorageDriver): ReadReceiptCollection =>
|
||||
new Collection(READ_RECEIPT_STORE_NAME, 'id', driver);
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ import {
|
|||
|
||||
export * from './utils';
|
||||
|
||||
const DATABASE_VERSION = 1;
|
||||
const DATABASE_VERSION = 2;
|
||||
|
||||
export interface Storage {
|
||||
sessionValue: SessionValueCollection;
|
||||
|
|
@ -95,7 +95,7 @@ export const createStorage = async (
|
|||
|
||||
// TODO: handle all openDB callbacks.
|
||||
const idb = await openDB(dbName, DATABASE_VERSION, {
|
||||
upgrade(db, oldVersion) {
|
||||
upgrade(db, oldVersion, newVersion, transaction) {
|
||||
prepareSessionValueStore(db, oldVersion);
|
||||
|
||||
prepareAccountDataStore(db, oldVersion);
|
||||
|
|
@ -109,7 +109,7 @@ export const createStorage = async (
|
|||
prepareUnreadNotificationsStore(db, oldVersion);
|
||||
prepareUnreadThreadNotificationsStore(db, oldVersion);
|
||||
|
||||
prepareReadReceiptStore(db, oldVersion);
|
||||
prepareReadReceiptStore(db, oldVersion, transaction);
|
||||
|
||||
prepareInviteRoomStateStore(db, oldVersion);
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import {
|
|||
UnreadThreadNotificationsObject,
|
||||
} from '../storage/collections';
|
||||
import { ThreadId } from '../../types/matrix/room';
|
||||
import { getLatestEventSyncFilter } from '../filter';
|
||||
|
||||
export const writeAccountData = async (
|
||||
ct: ClientContext,
|
||||
|
|
@ -167,15 +168,17 @@ export const writeJoinRoomLatestEvent = async (
|
|||
): Promise<void> => {
|
||||
const roomLatestEventObjects: JoinRoomLatestEventObject[] = [];
|
||||
|
||||
const latestEventFilter = getLatestEventSyncFilter();
|
||||
const supportedTypes = latestEventFilter.room?.timeline?.types ?? [];
|
||||
|
||||
joinRooms.forEach((roomId) => {
|
||||
const joinRoom = join[roomId];
|
||||
const { events } = joinRoom.timeline ?? {};
|
||||
if (!events) return;
|
||||
|
||||
// TODO: filter events that are supported by timeline.
|
||||
for (let i = events.length - 1; i >= 0; i -= 1) {
|
||||
const event = events[i];
|
||||
if (!('state_key' in event)) {
|
||||
if (!('state_key' in event) && supportedTypes.includes(event.type)) {
|
||||
roomLatestEventObjects.push({
|
||||
id: roomId,
|
||||
event,
|
||||
|
|
|
|||
Loading…
Reference in a new issue