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,58 @@ 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]) { + 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 @@ -57,6 +57,8 @@ // Do not introduce new usages of this constant in the code!!! const AUTHORITATIVE_KEYSERVER_ID = '256'; +const INDEXED_DB_UNREAD_COUNT_SUFFIX = 'unreadCount'; + async function decryptWebNotification( encryptedNotification: EncryptedWebNotification, ): Promise { @@ -104,6 +106,11 @@ encryptedPayload, ); + const { unreadCount } = decryptedNotification; + await updateNotifsUnreadCountStorage({ + [keyserverID]: unreadCount, + }); + return { id, ...decryptedNotification }; } catch (e) { return { @@ -145,8 +152,9 @@ }; } + let decryptedNotification; try { - return await commonDecrypt( + decryptedNotification = await commonDecrypt<{ +[string]: mixed }>( encryptedOlmData, olmDataContentKey, encryptionKey, @@ -158,6 +166,21 @@ staffCanSee, }; } + + // 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]: badge }); + return decryptedNotification; + } + + const { unreadCount } = decryptedNotification; + if (typeof unreadCount === 'number') { + await updateNotifsUnreadCountStorage({ [keyserverID]: unreadCount }); + } + return decryptedNotification; } async function commonDecrypt( @@ -454,10 +477,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, };