diff --git a/web/push-notif/badge-handler.react.js b/web/push-notif/badge-handler.react.js --- a/web/push-notif/badge-handler.react.js +++ b/web/push-notif/badge-handler.react.js @@ -2,35 +2,59 @@ import * as React from 'react'; -import { connectionSelector } from 'lib/selectors/keyserver-selectors.js'; -import { unreadCount } from 'lib/selectors/thread-selectors.js'; -import type { ConnectionInfo } from 'lib/types/socket-types.js'; +import { allConnectionInfosSelector } from 'lib/selectors/keyserver-selectors.js'; +import { allUnreadCounts } from 'lib/selectors/thread-selectors.js'; -import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; +import { + updateNotifsUnreadCountStorage, + queryNotifsUnreadCountStorage, +} from './notif-crypto-utils.js'; import electron from '../electron.js'; import { useSelector } from '../redux/redux-utils.js'; import getTitle from '../title/get-title.js'; function useBadgeHandler() { - const connection = useSelector(connectionSelector(authoritativeKeyserverID)); - const prevConnection = React.useRef(); - - const boundUnreadCount = useSelector(unreadCount); - const prevUnreadCount = React.useRef(boundUnreadCount); + const connection = useSelector(allConnectionInfosSelector); + const unreadCount = useSelector(allUnreadCounts); React.useEffect(() => { - if ( - connection?.status === 'connected' && - (prevConnection.current?.status !== 'connected' || - boundUnreadCount !== prevUnreadCount.current) - ) { - document.title = getTitle(boundUnreadCount); - electron?.setBadge(boundUnreadCount === 0 ? null : boundUnreadCount); - } - - prevConnection.current = connection; - prevUnreadCount.current = boundUnreadCount; - }, [boundUnreadCount, connection]); + void (async () => { + const unreadCountUpdates: { + [keyserverID: string]: number, + } = {}; + const unreadCountQueries: Array = []; + + for (const keyserverID in unreadCount) { + if (connection[keyserverID]?.status !== 'connected') { + unreadCountQueries.push(keyserverID); + continue; + } + unreadCountUpdates[keyserverID] = unreadCount[keyserverID]; + } + + let queriedUnreadCounts: { +[keyserverID: string]: ?number } = {}; + [queriedUnreadCounts] = await Promise.all([ + queryNotifsUnreadCountStorage(unreadCountQueries), + updateNotifsUnreadCountStorage(unreadCountUpdates), + ]); + + let totalUnreadCount = 0; + for (const keyserverID in unreadCountUpdates) { + totalUnreadCount += unreadCountUpdates[keyserverID]; + } + + for (const keyserverID in queriedUnreadCounts) { + if (!queriedUnreadCounts[keyserverID]) { + totalUnreadCount += unreadCount[keyserverID]; + continue; + } + totalUnreadCount += queriedUnreadCounts[keyserverID]; + } + + document.title = getTitle(totalUnreadCount); + electron?.setBadge(totalUnreadCount === 0 ? null : totalUnreadCount); + })(); + }, [unreadCount, connection]); } export default useBadgeHandler; 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 @@ -58,6 +58,8 @@ const ASHOAT_KEYSERVER_ID_USED_ONLY_FOR_MIGRATION_FROM_LEGACY_NOTIF_STORAGE = '256'; +const INDEXED_DB_UNREAD_COUNT_SUFFIX = 'unreadCount'; + async function decryptWebNotification( encryptedNotification: EncryptedWebNotification, ): Promise { @@ -105,6 +107,11 @@ encryptedPayload, ); + const { unreadCount } = decryptedNotification; + await updateNotifsUnreadCountStorage({ + [keyserverID]: unreadCount, + }); + return { id, ...decryptedNotification }; } catch (e) { return { @@ -146,8 +153,9 @@ }; } + let decryptedNotification; try { - return await commonDecrypt( + decryptedNotification = await commonDecrypt<{ +[string]: mixed }>( encryptedOlmData, olmDataContentKey, encryptionKey, @@ -159,6 +167,27 @@ staffCanSee, }; } + + if (!keyserverID) { + return decryptedNotification; + } + + // iOS notifications require that unread count is set under + // `badge` key. Since MacOS notifications are created by the + // same function the unread count is also set under `badge` key + const { badge } = decryptedNotification; + if (typeof badge === 'number') { + await updateNotifsUnreadCountStorage({ [(keyserverID: string)]: badge }); + return decryptedNotification; + } + + const { unreadCount } = decryptedNotification; + if (typeof unreadCount === 'number') { + await updateNotifsUnreadCountStorage({ + [(keyserverID: string)]: unreadCount, + }); + } + return decryptedNotification; } async function commonDecrypt( @@ -461,10 +490,53 @@ await Promise.all([...insertionPromises, ...deletionPromises]); } +// Multiple keyserver unread count utilities +function getKeyserverUnreadCountKey(keyserverID: string) { + return [ + INDEXED_DB_KEYSERVER_PREFIX, + keyserverID, + INDEXED_DB_UNREAD_COUNT_SUFFIX, + ].join(INDEXED_DB_KEY_SEPARATOR); +} + +async function updateNotifsUnreadCountStorage(perKeyserverUnreadCount: { + +[keyserverID: string]: number, +}) { + const unreadCountUpdatePromises: Array> = Object.entries( + perKeyserverUnreadCount, + ).map(([keyserverID, unreadCount]) => { + const keyserverUnreadCountKey = getKeyserverUnreadCountKey(keyserverID); + return localforage.setItem(keyserverUnreadCountKey, unreadCount); + }); + + await Promise.all(unreadCountUpdatePromises); +} + +async function queryNotifsUnreadCountStorage( + keyserverIDs: $ReadOnlyArray, +): Promise<{ + +[keyserverID: string]: ?number, +}> { + const queryUnreadCountPromises: Array> = keyserverIDs.map( + keyserverID => { + const keyserverUnreadCountKey = getKeyserverUnreadCountKey(keyserverID); + return localforage.getItem(keyserverUnreadCountKey); + }, + ); + const unreadCounts: $ReadOnlyArray = await Promise.all( + queryUnreadCountPromises, + ); + return Object.fromEntries( + keyserverIDs.map((keyserverID, idx) => [keyserverID, unreadCounts[idx]]), + ); +} + export { decryptWebNotification, decryptDesktopNotification, getOlmDataContentKeyForCookie, getOlmEncryptionKeyDBLabelForCookie, migrateLegacyOlmNotificationsSessions, + updateNotifsUnreadCountStorage, + queryNotifsUnreadCountStorage, };