diff --git a/lib/push/send-hooks.react.js b/lib/push/send-hooks.react.js new file mode 100644 --- /dev/null +++ b/lib/push/send-hooks.react.js @@ -0,0 +1,48 @@ +// @flow + +import * as React from 'react'; + +import { + preparePushNotifs, + type PerUserNotifBuildResult, +} 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 { useSelector } from '../utils/redux-utils.js'; + +function usePreparePushNotifs(): ( + messageDatas: $ReadOnlyArray, +) => Promise { + const rawMessageInfos = useSelector(state => state.messageStore.messages); + const rawThreadInfos = useSelector(state => state.threadStore.threadInfos); + const auxUserInfos = useSelector(state => state.auxUserStore.auxUserInfos); + const userInfos = useSelector(state => state.userStore.userInfos); + + const { getENSNames } = React.useContext(ENSCacheContext); + const getFCNames = React.useContext(NeynarClientContext)?.getFCNames; + + return React.useCallback( + (messageDatas: $ReadOnlyArray) => { + return preparePushNotifs({ + messageInfos: rawMessageInfos, + rawThreadInfos, + auxUserInfos, + messageDatas, + userInfos, + getENSNames, + getFCNames, + }); + }, + [ + rawMessageInfos, + rawThreadInfos, + auxUserInfos, + userInfos, + getENSNames, + getFCNames, + ], + ); +} + +export { usePreparePushNotifs }; 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 @@ -5,10 +5,17 @@ import uuidv4 from 'uuid/v4.js'; import { hasPermission } from '../permissions/minimally-encoded-thread-permissions.js'; -import { rawMessageInfoFromMessageData } from '../shared/message-utils.js'; +import { + rawMessageInfoFromMessageData, + createMessageInfo, +} from '../shared/message-utils.js'; import { type PushType, pushTypes } from '../shared/messages/message-spec.js'; import { messageSpecs } from '../shared/messages/message-specs.js'; -import { isMemberActive } from '../shared/thread-utils.js'; +import { notifTextsForMessageInfo } from '../shared/notif-utils.js'; +import { + isMemberActive, + threadInfoFromRawThreadInfo, +} from '../shared/thread-utils.js'; import type { AuxUserInfos } from '../types/aux-user-types.js'; import type { PlatformDetails } from '../types/device-types.js'; import { @@ -20,7 +27,12 @@ type RawMessageInfo, 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 { RawThreadInfos } from '../types/thread-types.js'; +import type { UserInfos } from '../types/user-types.js'; +import { type GetENSNames } from '../utils/ens-helpers.js'; +import { type GetFCNames } from '../utils/farcaster-helpers.js'; import { promiseAll } from '../utils/promises.js'; export type Device = { @@ -266,4 +278,188 @@ }; } -export { getPushUserInfo, generateNotifUserInfoPromise }; +async function buildNotifText( + inputData: { + +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); + + const newMessageInfos = []; + const newRawMessageInfos = []; + + for (const rawMessageInfo of rawMessageInfos) { + const newMessageInfo = hydrateMessageInfo(rawMessageInfo); + if (newMessageInfo) { + newMessageInfos.push(newMessageInfo); + newRawMessageInfos.push(rawMessageInfo); + } + } + + if (newMessageInfos.length === 0) { + return null; + } + + const [{ threadID }] = newMessageInfos; + const threadInfo = threadInfos[threadID]; + const parentThreadInfo = threadInfo.parentThreadID + ? threadInfos[threadInfo.parentThreadID] + : null; + + // TODO: Using types from @Ashoat check ThreadSubscription and mentioning + + const username = userInfos[userID] && userInfos[userID].username; + const notifTargetUserInfo = { id: userID, username }; + const notifTexts = await notifTextsForMessageInfo( + newMessageInfos, + threadInfo, + parentThreadInfo, + notifTargetUserInfo, + getENSNames, + getFCNames, + ); + if (!notifTexts) { + return null; + } + + return { notifTexts, newRawMessageInfos }; +} + +export type PerUserNotifBuildResult = { + +[userID: string]: $ReadOnlyArray<{ + +notifTexts: ResolvedNotifTexts, + +newRawMessageInfos: $ReadOnlyArray, + }>, +}; + +async function buildNotifsTexts( + pushInfo: PushInfo, + rawThreadInfos: RawThreadInfos, + userInfos: UserInfos, + getENSNames: ?GetENSNames, + getFCNames: ?GetFCNames, +): Promise { + const threadIDs = new Set(); + + for (const userID in pushInfo) { + for (const rawMessageInfo of pushInfo[userID].messageInfos) { + const threadID = rawMessageInfo.threadID; + threadIDs.add(threadID); + + const messageSpec = messageSpecs[rawMessageInfo.type]; + if (messageSpec.threadIDs) { + for (const id of messageSpec.threadIDs(rawMessageInfo)) { + threadIDs.add(id); + } + } + } + } + + const perUserNotifTextsPromises: { + [userID: string]: Promise< + Array, + }>, + >, + } = {}; + + for (const userID in pushInfo) { + const threadInfos = Object.fromEntries( + [...threadIDs].map(threadID => [ + threadID, + threadInfoFromRawThreadInfo( + rawThreadInfos[threadID], + userID, + userInfos, + ), + ]), + ); + + const userNotifTextsPromises = []; + + 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, + }, + getENSNames, + getFCNames, + ), + ); + } + + perUserNotifTextsPromises[userID] = Promise.all(userNotifTextsPromises); + } + + const perUserNotifTexts = await promiseAll(perUserNotifTextsPromises); + const filteredPerUserNotifTexts: { ...PerUserNotifBuildResult } = {}; + + for (const userID in perUserNotifTexts) { + filteredPerUserNotifTexts[userID] = + perUserNotifTexts[userID].filter(Boolean); + } + return filteredPerUserNotifTexts; +} + +type PreparePushNotifsInputData = { + +messageInfos: { +[id: string]: RawMessageInfo }, + +rawThreadInfos: RawThreadInfos, + +auxUserInfos: AuxUserInfos, + +messageDatas: $ReadOnlyArray, + +userInfos: UserInfos, + +getENSNames: ?GetENSNames, + +getFCNames: ?GetFCNames, +}; + +async function preparePushNotifs( + inputData: PreparePushNotifsInputData, +): Promise { + const { + messageDatas, + messageInfos, + auxUserInfos, + rawThreadInfos, + userInfos, + getENSNames, + getFCNames, + } = inputData; + + const { pushInfos } = await getPushUserInfo( + messageInfos, + rawThreadInfos, + auxUserInfos, + messageDatas, + ); + + if (!pushInfos) { + return null; + } + + return await buildNotifsTexts( + pushInfos, + rawThreadInfos, + userInfos, + getENSNames, + getFCNames, + ); +} + +export { preparePushNotifs, generateNotifUserInfoPromise };