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 @@ -4,16 +4,22 @@ import { preparePushNotifs, - type PerUserNotifBuildResult, + type PerUserTargetedNotifications, } 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 } from '../types/message-types.js'; +import type { + EncryptedNotifUtilsAPI, + SenderDeviceDescriptor, +} from '../types/notif-types.js'; import { useSelector } from '../utils/redux-utils.js'; function usePreparePushNotifs(): ( + encryptedNotifsUtilsAPI: EncryptedNotifUtilsAPI, + senderDeviceDescriptor: SenderDeviceDescriptor, messageDatas: $ReadOnlyArray, -) => Promise { +) => Promise { const rawMessageInfos = useSelector(state => state.messageStore.messages); const rawThreadInfos = useSelector(state => state.threadStore.threadInfos); const auxUserInfos = useSelector(state => state.auxUserStore.auxUserInfos); @@ -23,8 +29,14 @@ const getFCNames = React.useContext(NeynarClientContext)?.getFCNames; return React.useCallback( - (messageDatas: $ReadOnlyArray) => { + ( + encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, + senderDeviceDescriptor: SenderDeviceDescriptor, + messageDatas: $ReadOnlyArray, + ) => { return preparePushNotifs({ + encryptedNotifUtilsAPI, + senderDeviceDescriptor, messageInfos: 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,20 @@ import _pickBy from 'lodash/fp/pickBy.js'; import uuidv4 from 'uuid/v4.js'; -import { generateNotifUserInfoPromise } from './utils.js'; +import { createAndroidVisualNotification } from './android-notif-creators.js'; +import { createAPNsVisualNotification } from './apns-notif-creators.js'; +import { + stringToVersionKey, + getDevicesByPlatform, + generateNotifUserInfoPromise, +} from './utils.js'; +import { createWebNotification } from './web-notif-creators.js'; +import { createWNSNotification } from './wns-notif-creators.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 +26,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 +37,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, + SenderDeviceDescriptor, + 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'; @@ -187,19 +202,16 @@ } 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, }> { - const { rawMessageInfos, userID, threadInfos, userInfos } = inputData; const hydrateMessageInfo = (rawMessageInfo: RawMessageInfo) => createMessageInfo(rawMessageInfo, userID, userInfos, threadInfos); @@ -243,20 +255,277 @@ return { notifTexts, newRawMessageInfos }; } -export type PerUserNotifBuildResult = { - +[userID: string]: $ReadOnlyArray<{ - +notifTexts: ResolvedNotifTexts, - +newRawMessageInfos: $ReadOnlyArray, - }>, +type BuildNotifsForUserDevicesInputData = { + +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, + +senderDeviceDescriptor: SenderDeviceDescriptor, + +rawMessageInfos: $ReadOnlyArray, + +userID: string, + +threadInfos: { +[id: string]: ThreadInfo }, + +userInfos: UserInfos, + +getENSNames: ?GetENSNames, + +getFCNames: ?GetFCNames, + +devicesByPlatform: $ReadOnlyMap< + Platform, + $ReadOnlyMap>, + >, }; -async function buildNotifsTexts( - pushInfo: PushInfo, - rawThreadInfos: RawThreadInfos, - userInfos: UserInfos, - getENSNames: ?GetENSNames, - getFCNames: ?GetFCNames, -): Promise { +async function buildNotifsForUserDevices( + inputData: BuildNotifsForUserDevicesInputData, +): Promise> { + const { + encryptedNotifUtilsAPI, + senderDeviceDescriptor, + rawMessageInfos, + userID, + threadInfos, + userInfos, + getENSNames, + getFCNames, + devicesByPlatform, + } = inputData; + + const notifTextWithNewRawMessageInfos = await buildNotifText( + rawMessageInfos, + userID, + threadInfos, + userInfos, + getENSNames, + getFCNames, + ); + + if (!notifTextWithNewRawMessageInfos) { + return null; + } + + const { notifTexts, newRawMessageInfos } = notifTextWithNewRawMessageInfos; + const [{ threadID }] = newRawMessageInfos; + + 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, + { + senderDeviceDescriptor, + 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, + { + senderDeviceDescriptor, + notifTexts, + newRawMessageInfos: shimmedNewRawMessageInfos, + threadID, + collapseKey: undefined, + badgeOnly: false, + 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, + { + senderDeviceDescriptor, + 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, + senderDeviceDescriptor, + 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, + senderDeviceDescriptor, + platformDetails, + id: uuidv4(), + }, + devices, + ) + ).map(targetedNotification => ({ + platform: 'web', + targetedNotification, + })); + })(), + ); + } + } + + return (await Promise.all(promises)).flat(); +} + +export type PerUserTargetedNotifications = { + +[userID: string]: $ReadOnlyArray, +}; + +type BuildNotifsFromPushInfoInputData = { + +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, + +senderDeviceDescriptor: SenderDeviceDescriptor, + +pushInfo: PushInfo, + +rawThreadInfos: RawThreadInfos, + +userInfos: UserInfos, + +getENSNames: ?GetENSNames, + +getFCNames: ?GetFCNames, +}; + +async function buildNotifsFromPushInfo( + inputData: BuildNotifsFromPushInfoInputData, +): Promise { + const { + encryptedNotifUtilsAPI, + senderDeviceDescriptor, + pushInfo, + rawThreadInfos, + userInfos, + getENSNames, + getFCNames, + } = inputData; const threadIDs = new Set(); for (const userID in pushInfo) { @@ -273,13 +542,8 @@ } } - const perUserNotifTextsPromises: { - [userID: string]: Promise< - Array, - }>, - >, + const perUserBuildNotifsResultPromises: { + [userID: string]: Promise<$ReadOnlyArray>, } = {}; for (const userID in pushInfo) { @@ -293,41 +557,42 @@ ), ]), ); - - const userNotifTextsPromises = []; + const devicesByPlatform = getDevicesByPlatform(pushInfo[userID].devices); + const singleNotificationPromises = []; for (const rawMessageInfos of pushInfo[userID].messageInfos) { - userNotifTextsPromises.push( - buildNotifText( - { - // We always pass one element array here - // because coalescing is not supported for - // notifications generated on the client - rawMessageInfos: [rawMessageInfos], - threadInfos, - userID, - userInfos, - }, + singleNotificationPromises.push( + // We always pass one element array here + // because coalescing is not supported for + // notifications generated on the client + buildNotifsForUserDevices({ + encryptedNotifUtilsAPI, + senderDeviceDescriptor, + rawMessageInfos: [rawMessageInfos], + userID, + threadInfos, + userInfos, getENSNames, getFCNames, - ), + devicesByPlatform, + }), ); } - perUserNotifTextsPromises[userID] = Promise.all(userNotifTextsPromises); + perUserBuildNotifsResultPromises[userID] = (async () => { + const singleNotificationResults = await Promise.all( + singleNotificationPromises, + ); + return singleNotificationResults.filter(Boolean).flat(); + })(); } - const perUserNotifTexts = await promiseAll(perUserNotifTextsPromises); - const filteredPerUserNotifTexts: { ...PerUserNotifBuildResult } = {}; - - for (const userID in perUserNotifTexts) { - filteredPerUserNotifTexts[userID] = - perUserNotifTexts[userID].filter(Boolean); - } - return filteredPerUserNotifTexts; + return promiseAll(perUserBuildNotifsResultPromises); } type PreparePushNotifsInputData = { + +encryptedNotifUtilsAPI: EncryptedNotifUtilsAPI, + +senderDeviceDescriptor: SenderDeviceDescriptor, +messageInfos: { +[id: string]: RawMessageInfo }, +rawThreadInfos: RawThreadInfos, +auxUserInfos: AuxUserInfos, @@ -339,8 +604,10 @@ async function preparePushNotifs( inputData: PreparePushNotifsInputData, -): Promise { +): Promise { const { + encryptedNotifUtilsAPI, + senderDeviceDescriptor, messageDatas, messageInfos, auxUserInfos, @@ -361,13 +628,15 @@ return null; } - return await buildNotifsTexts( - pushInfos, + return await buildNotifsFromPushInfo({ + encryptedNotifUtilsAPI, + senderDeviceDescriptor, + pushInfo: pushInfos, rawThreadInfos, userInfos, getENSNames, getFCNames, - ); + }); } export { preparePushNotifs, generateNotifUserInfoPromise }; diff --git a/lib/push/web-notif-creators.js b/lib/push/web-notif-creators.js --- a/lib/push/web-notif-creators.js +++ b/lib/push/web-notif-creators.js @@ -21,7 +21,7 @@ +notifTexts: ResolvedNotifTexts, +threadID: string, +senderDeviceDescriptor: SenderDeviceDescriptor, - +unreadCount: number, + +unreadCount?: number, +platformDetails: PlatformDetails, }; @@ -31,7 +31,7 @@ notifTexts: resolvedNotifTextsValidator, threadID: tID, senderDeviceDescriptor: senderDeviceDescriptorValidator, - unreadCount: t.Number, + unreadCount: t.maybe(t.Number), platformDetails: tPlatformDetails, }); diff --git a/lib/push/wns-notif-creators.js b/lib/push/wns-notif-creators.js --- a/lib/push/wns-notif-creators.js +++ b/lib/push/wns-notif-creators.js @@ -22,7 +22,7 @@ +notifTexts: ResolvedNotifTexts, +threadID: string, +senderDeviceDescriptor: SenderDeviceDescriptor, - +unreadCount: number, + +unreadCount?: number, +platformDetails: PlatformDetails, }; @@ -31,7 +31,7 @@ notifTexts: resolvedNotifTextsValidator, threadID: tID, senderDeviceDescriptor: senderDeviceDescriptorValidator, - unreadCount: t.Number, + unreadCount: t.maybe(t.Number), platformDetails: tPlatformDetails, }); 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 @@ -3,6 +3,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'; @@ -42,7 +43,7 @@ +body: string, +prefix?: string, +title: string, - +unreadCount: number, + +unreadCount?: number, +threadID: string, +encryptionFailed?: '1', }; @@ -68,7 +69,7 @@ +body: string, +prefix?: string, +title: string, - +unreadCount: number, + +unreadCount?: number, +threadID: string, +encryptionFailed?: '1', }; @@ -361,6 +362,15 @@ +blobHolder?: string, }; +export type TargetedNotificationWithPlatform = { + +platform: Platform, + +targetedNotification: + | TargetedAPNsNotification + | TargetedWNSNotification + | TargetedWebNotification + | TargetedAndroidNotification, +}; + export type EncryptedNotifUtilsAPI = { +encryptSerializedNotifPayload: ( cryptoID: string,