diff --git a/lib/selectors/thread-selectors.js b/lib/selectors/thread-selectors.js --- a/lib/selectors/thread-selectors.js +++ b/lib/selectors/thread-selectors.js @@ -18,7 +18,7 @@ } from './calendar-filter-selectors.js'; import { relativeMemberInfoSelectorForMembersOfThread } from './user-selectors.js'; import genesis from '../facts/genesis.js'; -import { extractKeyserverIDFromIDOptional } from '../keyserver-conn/keyserver-call-utils.js'; +import { extractKeyserverIDFromID } from '../keyserver-conn/keyserver-call-utils.js'; import { getAvatarForThread, getRandomDefaultEmojiAvatar, @@ -263,10 +263,14 @@ } = createSelector( (state: BaseAppState<>) => state.threadStore.threadInfos, (threadInfos: RawThreadInfos): { +[keyserverID: string]: number } => { + const thinThreadInfosList = values(threadInfos).filter( + threadInfo => !threadInfo.thick, + ); + const keyserverToThreads = _groupBy(threadInfo => - extractKeyserverIDFromIDOptional(threadInfo.id), + extractKeyserverIDFromID(threadInfo.id), )( - values(threadInfos).filter(threadInfo => + thinThreadInfosList.filter(threadInfo => threadInHomeChatList(threadInfo), ), ); diff --git a/web/app.react.js b/web/app.react.js --- a/web/app.react.js +++ b/web/app.react.js @@ -69,7 +69,7 @@ import UpdateModalHandler from './modals/update-modal.react.js'; import SettingsSwitcher from './navigation-panels/settings-switcher.react.js'; import Topbar from './navigation-panels/topbar.react.js'; -import useBadgeHandler from './push-notif/badge-handler.react.js'; +import BadgeHandler from './push-notif/badge-handler.react.js'; import encryptedNotifUtilsAPI from './push-notif/encrypted-notif-utils-api.js'; import { PushNotificationsHandler } from './push-notif/push-notifs-handler.js'; import { updateNavInfoActionType } from './redux/action-types.js'; @@ -532,8 +532,6 @@ !!state.threadStore.threadInfos[activeChatThreadID]?.currentUser.unread, ); - useBadgeHandler(); - const dispatch = useDispatch(); const modalContext = useModalContext(); const modals = React.useMemo( @@ -561,6 +559,7 @@ onClose={releaseLockOrAbortRequest} secondaryTunnelbrokerConnection={secondaryTunnelbrokerConnection} > + { void (async () => { const unreadCountUpdates: { @@ -33,8 +42,23 @@ } let queriedUnreadCounts: { +[keyserverID: string]: ?number } = {}; - [queriedUnreadCounts] = await Promise.all([ + let unreadThickThreadIDs: $ReadOnlyArray = []; + + const handleUnreadThickThreadIDsInNotifsStoragePromise = (async () => { + if (tunnelbrokerSocketState.connected) { + await updateNotifsUnreadThickThreadIDsStorage({ + type: 'set', + threadIDs: currentUnreadThickThreadIDs, + forceWrite: true, + }); + return currentUnreadThickThreadIDs; + } + return getNotifsUnreadThickThreadIDs(); + })(); + + [queriedUnreadCounts, unreadThickThreadIDs] = await Promise.all([ queryNotifsUnreadCountStorage(unreadCountQueries), + handleUnreadThickThreadIDsInNotifsStoragePromise, updateNotifsUnreadCountStorage(unreadCountUpdates), ]); @@ -51,10 +75,21 @@ totalUnreadCount += queriedUnreadCounts[keyserverID]; } + totalUnreadCount += unreadThickThreadIDs.length; document.title = getTitle(totalUnreadCount); electron?.setBadge(totalUnreadCount === 0 ? null : totalUnreadCount); })(); - }, [unreadCount, connection]); + }, [ + tunnelbrokerSocketState, + currentUnreadThickThreadIDs, + unreadCount, + connection, + ]); +} + +function BadgeHandler(): React.Node { + useBadgeHandler(); + return null; } -export default useBadgeHandler; +export default BadgeHandler; diff --git a/web/push-notif/notif-crypto-utils.js b/web/push-notif/notif-crypto-utils.js --- a/web/push-notif/notif-crypto-utils.js +++ b/web/push-notif/notif-crypto-utils.js @@ -88,6 +88,12 @@ const INDEXED_DB_NOTIFS_ACCOUNT_ENCRYPTION_KEY_DB_LABEL = 'notificationAccountEncryptionKey'; +// thick threads unread count +const INDEXED_DB_UNREAD_THICK_THREAD_IDS = 'unreadThickThreadIDs'; +const INDEXED_DB_UNREAD_THICK_THREAD_IDS_ENCRYPTION_KEY_DB_LABEL = + 'unreadThickThreadIDsEncryptionKey'; +const INDEXED_DB_UNREAD_THICK_THREADS_SYNC_KEY = 'unreadThickThreadIDsSyncKey'; + async function deserializeEncryptedData( encryptedData: EncryptedData, encryptionKey: CryptoKey, @@ -456,16 +462,25 @@ updatedOlmData = resultUpdatedOlmData; updatedNotifsAccount = resultUpdatedNotifsAccount; - await persistNotifsAccountWithOlmData({ - accountWithPicklingKey: updatedNotifsAccount, - accountEncryptionKey, - encryptionKey, - olmData: updatedOlmData, - olmDataKey, - olmEncryptionKeyDBLabel, - synchronizationValue, - forceWrite: false, - }); + const { threadID } = decryptedNotification; + + await Promise.all([ + persistNotifsAccountWithOlmData({ + accountWithPicklingKey: updatedNotifsAccount, + accountEncryptionKey, + encryptionKey, + olmData: updatedOlmData, + olmDataKey, + olmEncryptionKeyDBLabel, + synchronizationValue, + forceWrite: false, + }), + updateNotifsUnreadThickThreadIDsStorage({ + type: 'add', + threadIDs: [threadID], + forceWrite: false, + }), + ]); return { id, ...decryptedNotification }; } @@ -585,16 +600,26 @@ encryptedPayload, ); - await persistNotifsAccountWithOlmData({ - accountWithPicklingKey: updatedNotifsAccount, - accountEncryptionKey, - encryptionKey, - olmData: updatedOlmData, - olmDataKey, - olmEncryptionKeyDBLabel, - synchronizationValue, - forceWrite: false, - }); + const { threadID } = decryptedNotification; + invariant(typeof threadID === 'string', 'threadID should be string'); + + await Promise.all([ + persistNotifsAccountWithOlmData({ + accountWithPicklingKey: updatedNotifsAccount, + accountEncryptionKey, + encryptionKey, + olmData: updatedOlmData, + olmDataKey, + olmEncryptionKeyDBLabel, + synchronizationValue, + forceWrite: false, + }), + updateNotifsUnreadThickThreadIDsStorage({ + type: 'add', + threadIDs: [threadID], + forceWrite: false, + }), + ]); return decryptedNotification; } @@ -1253,6 +1278,118 @@ return Object.fromEntries(queriedUnreadCounts); } +async function updateNotifsUnreadThickThreadIDsStorage(input: { + +type: 'add' | 'remove' | 'set', + +threadIDs: $ReadOnlyArray, + +forceWrite: boolean, +}): Promise { + const { type, threadIDs, forceWrite } = input; + + const { + values: { + [INDEXED_DB_UNREAD_THICK_THREAD_IDS]: encryptedData, + [INDEXED_DB_UNREAD_THICK_THREAD_IDS_ENCRYPTION_KEY_DB_LABEL]: + encryptionKey, + }, + synchronizationValue, + } = await localforage.getMultipleItems<{ + unreadThickThreadIDs: ?EncryptedData, + unreadThickThreadIDsEncryptionKey: ?(CryptoKey | SubtleCrypto$JsonWebKey), + }>( + [ + INDEXED_DB_UNREAD_THICK_THREAD_IDS, + INDEXED_DB_UNREAD_THICK_THREAD_IDS_ENCRYPTION_KEY_DB_LABEL, + ], + INDEXED_DB_UNREAD_THICK_THREADS_SYNC_KEY, + ); + + let unreadThickThreadIDs; + let unreadThickThreadIDsEncryptionKey; + + if (encryptedData && encryptionKey) { + unreadThickThreadIDsEncryptionKey = await validateCryptoKey(encryptionKey); + unreadThickThreadIDs = new Set( + await deserializeEncryptedData>( + encryptedData, + unreadThickThreadIDsEncryptionKey, + ), + ); + } else { + unreadThickThreadIDs = new Set(); + unreadThickThreadIDsEncryptionKey = await generateCryptoKey({ + extractable: isDesktopSafari, + }); + } + + if (type === 'add') { + for (const threadID of threadIDs) { + unreadThickThreadIDs.add(threadID); + } + } else if (type === 'remove') { + for (const threadID of threadIDs) { + unreadThickThreadIDs.delete(threadID); + } + } else { + unreadThickThreadIDs = new Set(threadIDs); + } + + const [encryptionKeyPersistentForm, updatedEncryptedData] = await Promise.all( + [ + getCryptoKeyPersistentForm(unreadThickThreadIDsEncryptionKey), + serializeUnencryptedData( + [...unreadThickThreadIDs], + unreadThickThreadIDsEncryptionKey, + ), + ], + ); + + const newSynchronizationValue = uuid.v4(); + await localforage.setMultipleItems( + { + [INDEXED_DB_UNREAD_THICK_THREAD_IDS]: updatedEncryptedData, + [INDEXED_DB_UNREAD_THICK_THREAD_IDS_ENCRYPTION_KEY_DB_LABEL]: + encryptionKeyPersistentForm, + }, + INDEXED_DB_UNREAD_THICK_THREADS_SYNC_KEY, + synchronizationValue, + newSynchronizationValue, + forceWrite, + ); +} + +async function getNotifsUnreadThickThreadIDs(): Promise< + $ReadOnlyArray, +> { + const { + values: { + [INDEXED_DB_UNREAD_THICK_THREAD_IDS]: encryptedData, + [INDEXED_DB_UNREAD_THICK_THREAD_IDS_ENCRYPTION_KEY_DB_LABEL]: + encryptionKey, + }, + } = await localforage.getMultipleItems<{ + unreadThickThreadIDs: ?EncryptedData, + unreadThickThreadIDsEncryptionKey: ?(CryptoKey | SubtleCrypto$JsonWebKey), + }>( + [ + INDEXED_DB_UNREAD_THICK_THREAD_IDS, + INDEXED_DB_UNREAD_THICK_THREAD_IDS_ENCRYPTION_KEY_DB_LABEL, + ], + INDEXED_DB_UNREAD_THICK_THREADS_SYNC_KEY, + ); + + if (!encryptionKey || !encryptedData) { + return []; + } + + const unreadThickThreadIDsEncryptionKey = + await validateCryptoKey(encryptionKey); + + return await deserializeEncryptedData>( + encryptedData, + unreadThickThreadIDsEncryptionKey, + ); +} + export { decryptWebNotification, decryptDesktopNotification, @@ -1268,4 +1405,6 @@ persistEncryptionKey, retrieveEncryptionKey, persistNotifsAccountWithOlmData, + updateNotifsUnreadThickThreadIDsStorage, + getNotifsUnreadThickThreadIDs, };