diff --git a/lib/push/notif-creators.js b/lib/push/notif-creators.js --- a/lib/push/notif-creators.js +++ b/lib/push/notif-creators.js @@ -864,7 +864,7 @@ +notifTexts: ResolvedNotifTexts, +threadID: string, +senderDeviceID: SenderDeviceID, - +unreadCount: number, + +unreadCount: ?number, +platformDetails: PlatformDetails, }; @@ -874,7 +874,7 @@ notifTexts: resolvedNotifTextsValidator, threadID: tID, senderDeviceID: senderDeviceIDValidator, - unreadCount: t.Number, + unreadCount: t.maybe(t.Number), platformDetails: tPlatformDetails, }); @@ -913,7 +913,7 @@ +notifTexts: ResolvedNotifTexts, +threadID: string, +senderDeviceID: SenderDeviceID, - +unreadCount: number, + +unreadCount: ?number, +platformDetails: PlatformDetails, }; @@ -922,7 +922,7 @@ notifTexts: resolvedNotifTextsValidator, threadID: tID, senderDeviceID: senderDeviceIDValidator, - unreadCount: t.Number, + unreadCount: t.maybe(t.Number), platformDetails: tPlatformDetails, }); diff --git a/lib/push/send-hooks.react.js b/lib/push/send-hooks.react.js --- a/lib/push/send-hooks.react.js +++ b/lib/push/send-hooks.react.js @@ -5,17 +5,20 @@ import { preparePushNotifs } from './send-utils.js'; import { ENSCacheContext } from '../components/ens-cache-provider.react.js'; import { NeynarClientContext } from '../components/neynar-client-provider.react.js'; -import type { MessageData, RawMessageInfo } from '../types/message-types.js'; -import type { ResolvedNotifTexts } from '../types/notif-types.js'; +import type { MessageData } from '../types/message-types.js'; +import type { + EncryptedNotifUtilsAPI, + SenderDeviceID, + TargetedNotificationWithPlatform, +} from '../types/notif-types.js'; import { useSelector } from '../utils/redux-utils.js'; function usePreparePushNotifs(): ( + encryptedNotifsUtilsAPI: EncryptedNotifUtilsAPI, + senderDeviceID: SenderDeviceID, messageDatas: $ReadOnlyArray, ) => Promise, - }>, + +[userID: string]: $ReadOnlyArray, }> { const rawMessageInfos = useSelector(state => state.messageStore.messages); const rawThreadInfos = useSelector(state => state.threadStore.threadInfos); @@ -26,8 +29,14 @@ const getFCNames = React.useContext(NeynarClientContext)?.getFCNames; return React.useCallback( - (messageDatas: $ReadOnlyArray) => { + ( + encryptedNotifsUtilsAPI: EncryptedNotifUtilsAPI, + senderDeviceID: SenderDeviceID, + messageDatas: $ReadOnlyArray, + ) => { return preparePushNotifs( + encryptedNotifsUtilsAPI, + senderDeviceID, rawMessageInfos, rawThreadInfos, auxUserInfos, diff --git a/lib/push/send-utils.js b/lib/push/send-utils.js --- a/lib/push/send-utils.js +++ b/lib/push/send-utils.js @@ -3,11 +3,22 @@ import _pickBy from 'lodash/fp/pickBy.js'; import uuidv4 from 'uuid/v4.js'; -import { generateNotifUserInfoPromise } from './utils.js'; +import { + createAPNsVisualNotification, + createAndroidVisualNotification, + createWNSNotification, + createWebNotification, +} from './notif-creators.js'; +import { + stringToVersionKey, + getDevicesByPlatform, + generateNotifUserInfoPromise, +} from './utils.js'; import { hasPermission } from '../permissions/minimally-encoded-thread-permissions.js'; import { rawMessageInfoFromMessageData, createMessageInfo, + shimUnsupportedRawMessageInfos, } from '../shared/message-utils.js'; import { pushTypes } from '../shared/messages/message-spec.js'; import { messageSpecs } from '../shared/messages/message-specs.js'; @@ -17,7 +28,7 @@ threadInfoFromRawThreadInfo, } from '../shared/thread-utils.js'; import type { AuxUserInfos } from '../types/aux-user-types.js'; -import type { PlatformDetails } from '../types/device-types.js'; +import type { PlatformDetails, Platform } from '../types/device-types.js'; import { identityDeviceTypeToPlatform, type IdentityPlatformDetails, @@ -28,7 +39,13 @@ messageDataLocalID, } from '../types/message-types.js'; import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; -import type { ResolvedNotifTexts } from '../types/notif-types.js'; +import type { + ResolvedNotifTexts, + NotificationTargetDevice, + TargetedNotificationWithPlatform, + SenderDeviceID, + EncryptedNotifUtilsAPI, +} from '../types/notif-types.js'; import type { RawThreadInfos } from '../types/thread-types.js'; import type { UserInfos } from '../types/user-types.js'; import { type GetENSNames } from '../utils/ens-helpers.js'; @@ -184,19 +201,17 @@ } async function buildNotifText( - inputData: { - +rawMessageInfos: $ReadOnlyArray, - +userID: string, - +threadInfos: { +[id: string]: ThreadInfo }, - +userInfos: UserInfos, - }, + rawMessageInfos: $ReadOnlyArray, + userID: string, + threadInfos: { +[id: string]: ThreadInfo }, + userInfos: UserInfos, getENSNames: ?GetENSNames, getFCNames: ?GetFCNames, ): Promise, + +threadID: string, }> { - const { rawMessageInfos, userID, threadInfos, userInfos } = inputData; const hydrateMessageInfo = (rawMessageInfo: RawMessageInfo) => createMessageInfo(rawMessageInfo, userID, userInfos, threadInfos); @@ -237,20 +252,249 @@ return null; } - return { notifTexts, newRawMessageInfos }; + return { notifTexts, newRawMessageInfos, threadID }; +} + +async function buildNotifsForUserDevices( + encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, + senderDeviceID: SenderDeviceID, + rawMessageInfos: $ReadOnlyArray, + userID: string, + threadInfos: { +[id: string]: ThreadInfo }, + userInfos: UserInfos, + getENSNames: ?GetENSNames, + getFCNames: ?GetFCNames, + devicesByPlatform: $ReadOnlyMap< + Platform, + $ReadOnlyMap>, + >, +): Promise> { + const notifTextWithNewRawMessageInfos = await buildNotifText( + rawMessageInfos, + userID, + threadInfos, + userInfos, + getENSNames, + getFCNames, + ); + + if (!notifTextWithNewRawMessageInfos) { + return null; + } + + const { notifTexts, newRawMessageInfos, threadID } = + notifTextWithNewRawMessageInfos; + + const promises: Array< + Promise<$ReadOnlyArray>, + > = []; + + const iosVersionToDevices = devicesByPlatform.get('ios'); + if (iosVersionToDevices) { + for (const [versionKey, devices] of iosVersionToDevices) { + const { codeVersion, stateVersion } = stringToVersionKey(versionKey); + const platformDetails = { + platform: 'ios', + codeVersion, + stateVersion, + }; + const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( + newRawMessageInfos, + platformDetails, + ); + + promises.push( + (async () => { + return ( + await createAPNsVisualNotification( + encryptedNotifUtilsAPI, + { + senderDeviceID, + notifTexts, + newRawMessageInfos: shimmedNewRawMessageInfos, + threadID, + collapseKey: undefined, + badgeOnly: false, + unreadCount: undefined, + platformDetails, + uniqueID: uuidv4(), + }, + devices, + ) + ).map(targetedNotification => ({ + platform: 'ios', + targetedNotification, + })); + })(), + ); + } + } + + const androidVersionToDevices = devicesByPlatform.get('android'); + if (androidVersionToDevices) { + for (const [versionKey, devices] of androidVersionToDevices) { + const { codeVersion, stateVersion } = stringToVersionKey(versionKey); + const platformDetails = { + platform: 'android', + codeVersion, + stateVersion, + }; + const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( + newRawMessageInfos, + platformDetails, + ); + + promises.push( + (async () => { + return ( + await createAndroidVisualNotification( + encryptedNotifUtilsAPI, + { + senderDeviceID, + notifTexts, + newRawMessageInfos: shimmedNewRawMessageInfos, + threadID, + collapseKey: undefined, + unreadCount: undefined, + platformDetails, + notifID: uuidv4(), + }, + devices, + ) + ).map(targetedNotification => ({ + platform: 'android', + targetedNotification, + })); + })(), + ); + } + } + + const macosVersionToDevices = devicesByPlatform.get('macos'); + if (macosVersionToDevices) { + for (const [versionKey, devices] of macosVersionToDevices) { + const { codeVersion, stateVersion, majorDesktopVersion } = + stringToVersionKey(versionKey); + const platformDetails = { + platform: 'macos', + codeVersion, + stateVersion, + majorDesktopVersion, + }; + const shimmedNewRawMessageInfos = shimUnsupportedRawMessageInfos( + newRawMessageInfos, + platformDetails, + ); + + promises.push( + (async () => { + return ( + await createAPNsVisualNotification( + encryptedNotifUtilsAPI, + { + senderDeviceID, + notifTexts, + newRawMessageInfos: shimmedNewRawMessageInfos, + threadID, + collapseKey: undefined, + badgeOnly: false, + unreadCount: undefined, + platformDetails, + uniqueID: uuidv4(), + }, + devices, + ) + ).map(targetedNotification => ({ + platform: 'macos', + targetedNotification, + })); + })(), + ); + } + } + + const windowsVersionToDevices = devicesByPlatform.get('windows'); + if (windowsVersionToDevices) { + for (const [versionKey, devices] of windowsVersionToDevices) { + const { codeVersion, stateVersion, majorDesktopVersion } = + stringToVersionKey(versionKey); + const platformDetails = { + platform: 'windows', + codeVersion, + stateVersion, + majorDesktopVersion, + }; + + promises.push( + (async () => { + return ( + await createWNSNotification( + encryptedNotifUtilsAPI, + { + notifTexts, + threadID, + senderDeviceID, + unreadCount: undefined, + platformDetails, + }, + devices, + ) + ).map(targetedNotification => ({ + platform: 'windows', + targetedNotification, + })); + })(), + ); + } + } + + const webVersionToDevices = devicesByPlatform.get('web'); + if (webVersionToDevices) { + for (const [versionKey, devices] of webVersionToDevices) { + const { codeVersion, stateVersion } = stringToVersionKey(versionKey); + const platformDetails = { + platform: 'web', + codeVersion, + stateVersion, + }; + + promises.push( + (async () => { + return ( + await createWebNotification( + encryptedNotifUtilsAPI, + { + notifTexts, + threadID, + senderDeviceID, + unreadCount: undefined, + platformDetails, + id: uuidv4(), + }, + devices, + ) + ).map(targetedNotification => ({ + platform: 'web', + targetedNotification, + })); + })(), + ); + } + } + + return (await Promise.all(promises)).flat(); } -async function buildNotifsTexts( +async function buildNotifsFromPushInfo( + encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, + senderDeviceID: SenderDeviceID, pushInfo: PushInfo, rawThreadInfos: RawThreadInfos, userInfos: UserInfos, getENSNames: ?GetENSNames, getFCNames: ?GetFCNames, ): Promise<{ - +[userID: string]: $ReadOnlyArray<{ - +notifTexts: ResolvedNotifTexts, - +newRawMessageInfos: $ReadOnlyArray, - }>, + +[userID: string]: $ReadOnlyArray, }> { const threadIDs = new Set(); @@ -268,13 +512,8 @@ } } - const perUserNotifTextsPromises: { - [userID: string]: Promise< - Array, - }>, - >, + const perUserBuildNotifsResultPromises: { + [userID: string]: Promise<$ReadOnlyArray>, } = {}; for (const userID in pushInfo) { @@ -288,45 +527,40 @@ ), ]), ); - - const userNotifTextsPromises = []; + const devicesByPlatform = getDevicesByPlatform(pushInfo[userID].devices); + const singleNotificationPromises = []; for (const rawMessageInfos of pushInfo[userID].messageInfos) { - userNotifTextsPromises.push( - buildNotifText( - { - // This is because coalescing is not supported - // for notifications generated on the client - rawMessageInfos: [rawMessageInfos], - threadInfos, + singleNotificationPromises.push( + (async () => { + return await buildNotifsForUserDevices( + encryptedNotifUtilsAPI, + senderDeviceID, + [rawMessageInfos], userID, + threadInfos, userInfos, - }, - getENSNames, - getFCNames, - ), + getENSNames, + getFCNames, + devicesByPlatform, + ); + })(), ); } - perUserNotifTextsPromises[userID] = Promise.all(userNotifTextsPromises); + perUserBuildNotifsResultPromises[userID] = (async () => { + return (await Promise.all(singleNotificationPromises)) + .filter(Boolean) + .flat(); + })(); } - const perUserNotifTexts = await promiseAll(perUserNotifTextsPromises); - const filteredPerUserNotifTexts: { - [userID: string]: $ReadOnlyArray<{ - +notifTexts: ResolvedNotifTexts, - +newRawMessageInfos: $ReadOnlyArray, - }>, - } = {}; - - for (const userID in perUserNotifTexts) { - filteredPerUserNotifTexts[userID] = - perUserNotifTexts[userID].filter(Boolean); - } - return filteredPerUserNotifTexts; + return promiseAll(perUserBuildNotifsResultPromises); } async function preparePushNotifs( + encryptedUtilsAPI: EncryptedNotifUtilsAPI, + senderDeviceID: SenderDeviceID, messageInfos: { +[id: string]: RawMessageInfo }, rawThreadInfos: RawThreadInfos, auxUserInfos: AuxUserInfos, @@ -335,10 +569,7 @@ getENSNames: ?GetENSNames, getFCNames: ?GetFCNames, ): Promise, - }>, + +[userID: string]: $ReadOnlyArray, }> { const { pushInfos } = await getPushUserInfo( messageInfos, @@ -351,7 +582,9 @@ return null; } - return await buildNotifsTexts( + return await buildNotifsFromPushInfo( + encryptedUtilsAPI, + senderDeviceID, pushInfos, rawThreadInfos, userInfos, diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js --- a/lib/types/notif-types.js +++ b/lib/types/notif-types.js @@ -2,6 +2,7 @@ import type { EncryptResult } from '@commapp/olm'; import t, { type TInterface, type TUnion } from 'tcomb'; +import type { Platform } from './device-types.js'; import type { EntityText, ThreadEntity } from '../utils/entity-text.js'; import { tShape } from '../utils/validation-utils.js'; @@ -40,7 +41,7 @@ +body: string, +prefix?: string, +title: string, - +unreadCount: number, + +unreadCount: ?number, +threadID: string, +encryptionFailed?: '1', }; @@ -65,7 +66,7 @@ +body: string, +prefix?: string, +title: string, - +unreadCount: number, + +unreadCount: ?number, +threadID: string, +encryptionFailed?: '1', }; @@ -352,6 +353,15 @@ +blobHolder?: string, }; +export type TargetedNotificationWithPlatform = { + +platform: Platform, + +targetedNotification: + | TargetedAPNsNotification + | TargetedWNSNotification + | TargetedWebNotification + | TargetedAndroidNotification, +}; + export type EncryptedNotifUtilsAPI = { +encryptSerializedNotifPayload: ( cryptoID: string,