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, @@ -258,15 +258,19 @@ ).length, ); -const allUnreadCounts: (state: BaseAppState<>) => { +const thinThreadsUnreadCountSelector: (state: BaseAppState<>) => { +[keyserverID: string]: number, } = 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), ), ); @@ -540,7 +544,7 @@ childThreadInfos, containedThreadInfos, unreadCount, - allUnreadCounts, + thinThreadsUnreadCountSelector, unreadBackgroundCount, unreadCountSelectorForCommunity, otherUsersButNoOtherAdmins, diff --git a/native/push/push-handler.react.js b/native/push/push-handler.react.js --- a/native/push/push-handler.react.js +++ b/native/push/push-handler.react.js @@ -27,7 +27,7 @@ } from 'lib/selectors/keyserver-selectors.js'; import { threadInfoSelector, - allUnreadCounts, + thinThreadsUnreadCountSelector, unreadThickThreadIDsSelector, } from 'lib/selectors/thread-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; @@ -108,7 +108,7 @@ // Navigation state +activeThread: ?string, // Redux state - +unreadCount: { +[keyserverID: string]: number }, + +thinThreadsUnreadCount: { +[keyserverID: string]: number }, +unreadThickThreadIDs: $ReadOnlyArray, +connection: { +[keyserverID: string]: ?ConnectionInfo }, +deviceTokens: { @@ -326,7 +326,7 @@ } async updateBadgeCount() { - const curUnreadCounts = this.props.unreadCount; + const curThinUnreadCounts = this.props.thinThreadsUnreadCount; const curConnections = this.props.connection; const currentUnreadThickThreads = this.props.unreadThickThreadIDs; @@ -339,7 +339,7 @@ }> = []; const notifsStorageQueries: Array = []; - for (const keyserverID in curUnreadCounts) { + for (const keyserverID in curThinUnreadCounts) { if (curConnections[keyserverID]?.status !== 'connected') { notifsStorageQueries.push(keyserverID); continue; @@ -347,7 +347,7 @@ notifStorageUpdates.push({ id: keyserverID, - unreadCount: curUnreadCounts[keyserverID], + unreadCount: curThinUnreadCounts[keyserverID], }); } @@ -401,7 +401,9 @@ } async resetBadgeCount() { - const keyserversDataToRemove = Object.keys(this.props.unreadCount); + const keyserversDataToRemove = Object.keys( + this.props.thinThreadsUnreadCount, + ); try { await commCoreModule.removeKeyserverDataFromNotifStorage( keyserversDataToRemove, @@ -827,7 +829,7 @@ React.memo(function ConnectedPushHandler(props: BaseProps) { const navContext = React.useContext(NavContext); const activeThread = activeMessageListSelector(navContext); - const unreadCount = useSelector(allUnreadCounts); + const thinThreadsUnreadCount = useSelector(thinThreadsUnreadCountSelector); const unreadThickThreadIDs = useSelector(unreadThickThreadIDsSelector); const connection = useSelector(allConnectionInfosSelector); const deviceTokens = useSelector(deviceTokensSelector); @@ -852,7 +854,7 @@ + { void (async () => { @@ -24,17 +33,32 @@ } = {}; const unreadCountQueries: Array = []; - for (const keyserverID in unreadCount) { + for (const keyserverID in thinThreadsUnreadCount) { if (connection[keyserverID]?.status !== 'connected') { unreadCountQueries.push(keyserverID); continue; } - unreadCountUpdates[keyserverID] = unreadCount[keyserverID]; + unreadCountUpdates[keyserverID] = thinThreadsUnreadCount[keyserverID]; } 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), ]); @@ -45,16 +69,27 @@ for (const keyserverID in queriedUnreadCounts) { if (!queriedUnreadCounts[keyserverID]) { - totalUnreadCount += unreadCount[keyserverID]; + totalUnreadCount += thinThreadsUnreadCount[keyserverID]; continue; } totalUnreadCount += queriedUnreadCounts[keyserverID]; } + totalUnreadCount += unreadThickThreadIDs.length; document.title = getTitle(totalUnreadCount); electron?.setBadge(totalUnreadCount === 0 ? null : totalUnreadCount); })(); - }, [unreadCount, connection]); + }, [ + tunnelbrokerSocketState, + currentUnreadThickThreadIDs, + thinThreadsUnreadCount, + 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, };