diff --git a/lib/shared/messages/message-spec.js b/lib/shared/messages/message-spec.js index a46a65ee2..a2910db42 100644 --- a/lib/shared/messages/message-spec.js +++ b/lib/shared/messages/message-spec.js @@ -1,106 +1,105 @@ // @flow import type { PlatformDetails } from '../../types/device-types'; import type { Media } from '../../types/media-types'; import type { MessageInfo, ClientDBMessageInfo, RawComposableMessageInfo, RawMessageInfo, RawRobotextMessageInfo, } from '../../types/message-types'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported'; import type { NotifTexts } from '../../types/notif-types'; import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import type { EntityText } from '../../utils/entity-text'; import { type ParserRules } from '../markdown'; export type MessageTitleParam = { +messageInfo: Info, +threadInfo: ThreadInfo, +markdownRules: ParserRules, }; export type RawMessageInfoFromServerDBRowParams = { +localID: ?string, +media?: $ReadOnlyArray, +derivedMessages: $ReadOnlyMap< string, RawComposableMessageInfo | RawRobotextMessageInfo, >, }; export type CreateMessageInfoParams = { +threadInfos: { +[id: string]: ThreadInfo }, +createMessageInfoFromRaw: (rawInfo: RawMessageInfo) => ?MessageInfo, +createRelativeUserInfos: ( userIDs: $ReadOnlyArray, ) => RelativeUserInfo[], }; export type RobotextParams = { +threadInfo: ?ThreadInfo, }; export type NotificationTextsParams = { - +notifThreadName: (threadInfo: ThreadInfo) => string, +notificationTexts: ( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ) => Promise, }; export type GeneratesNotifsParams = { +notifTargetUserID: string, +userNotMemberOfSubthreads: Set, +fetchMessageInfoByID: (messageID: string) => Promise, }; export const pushTypes = Object.freeze({ NOTIF: 'notif', RESCIND: 'rescind', }); export type PushType = $Values; export type MessageSpec = { +messageContentForServerDB?: (data: Data | RawInfo) => string, +messageContentForClientDB?: (data: RawInfo) => string, +messageTitle?: (param: MessageTitleParam) => EntityText, +rawMessageInfoFromServerDBRow?: ( row: Object, params: RawMessageInfoFromServerDBRowParams, ) => ?RawInfo, +rawMessageInfoFromClientDB: ( clientDBMessageInfo: ClientDBMessageInfo, ) => RawInfo, +createMessageInfo: ( rawMessageInfo: RawInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ) => ?Info, +rawMessageInfoFromMessageData?: (messageData: Data, id: ?string) => RawInfo, +robotext?: (messageInfo: Info, params: RobotextParams) => EntityText, +shimUnsupportedMessageInfo?: ( rawMessageInfo: RawInfo, platformDetails: ?PlatformDetails, ) => RawInfo | RawUnsupportedMessageInfo, +unshimMessageInfo?: ( unwrapped: RawInfo, messageInfo: RawMessageInfo, ) => ?RawMessageInfo, +notificationTexts?: ( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, params: NotificationTextsParams, ) => Promise, +notificationCollapseKey?: (rawMessageInfo: RawInfo) => string, +generatesNotifs: ( rawMessageInfo: RawInfo, params: GeneratesNotifsParams, ) => Promise, +userIDs?: (rawMessageInfo: RawInfo) => $ReadOnlyArray, +startsThread?: boolean, +threadIDs?: (rawMessageInfo: RawInfo) => $ReadOnlyArray, +includedInRepliesCount?: boolean, }; diff --git a/lib/shared/notif-utils.js b/lib/shared/notif-utils.js index 69f353dfa..8ad34b2a8 100644 --- a/lib/shared/notif-utils.js +++ b/lib/shared/notif-utils.js @@ -1,258 +1,247 @@ // @flow import invariant from 'invariant'; import { type MessageInfo, type RawMessageInfo, type RobotextMessageInfo, type MessageType, messageTypes, } from '../types/message-types'; import type { NotifTexts, ResolvedNotifTexts } from '../types/notif-types'; import type { ThreadInfo, ThreadType } from '../types/thread-types'; import type { RelativeUserInfo } from '../types/user-types'; import { prettyDate } from '../utils/date-utils'; import type { GetENSNames } from '../utils/ens-helpers'; import { ET, getEntityTextAsString, type EntityText, } from '../utils/entity-text'; import { promiseAll } from '../utils/promises'; import { trimText } from '../utils/text-utils'; import { robotextForMessageInfo } from './message-utils'; import { messageSpecs } from './messages/message-specs'; import { threadNoun } from './thread-utils'; async function notifTextsForMessageInfo( messageInfos: MessageInfo[], threadInfo: ThreadInfo, getENSNames: ?GetENSNames, ): Promise { const fullNotifTexts = await fullNotifTextsForMessageInfo( messageInfos, threadInfo, getENSNames, ); const merged = trimText(fullNotifTexts.merged, 300); const body = trimText(fullNotifTexts.body, 300); const title = trimText(fullNotifTexts.title, 100); if (!fullNotifTexts.prefix) { return { merged, body, title }; } const prefix = trimText(fullNotifTexts.prefix, 50); return { merged, body, title, prefix }; } function notifTextsForEntryCreationOrEdit( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): NotifTexts { const hasCreateEntry = messageInfos.some( messageInfo => messageInfo.type === messageTypes.CREATE_ENTRY, ); const messageInfo = messageInfos[0]; const thread = ET.thread({ display: 'shortName', threadInfo }); const creator = ET.user({ userInfo: messageInfo.creator }); const prefix = ET`${creator}`; if (!hasCreateEntry) { invariant( messageInfo.type === messageTypes.EDIT_ENTRY, 'messageInfo should be messageTypes.EDIT_ENTRY!', ); const date = prettyDate(messageInfo.date); let body = ET`updated the text of an event in ${thread}`; body = ET`${body} scheduled for ${date}: "${messageInfo.text}"`; const merged = ET`${prefix} ${body}`; return { merged, title: threadInfo.uiName, body, prefix, }; } invariant( messageInfo.type === messageTypes.CREATE_ENTRY || messageInfo.type === messageTypes.EDIT_ENTRY, 'messageInfo should be messageTypes.CREATE_ENTRY/EDIT_ENTRY!', ); const date = prettyDate(messageInfo.date); let body = ET`created an event in ${thread}`; body = ET`${body} scheduled for ${date}: "${messageInfo.text}"`; const merged = ET`${prefix} ${body}`; return { merged, title: threadInfo.uiName, body, prefix, }; } type NotifTextsForSubthreadCreationInput = { +creator: RelativeUserInfo, +threadType: ThreadType, +parentThreadInfo: ThreadInfo, +childThreadName: ?string, +childThreadUIName: EntityText | string, }; function notifTextsForSubthreadCreation( input: NotifTextsForSubthreadCreationInput, ): NotifTexts { const { creator, threadType, parentThreadInfo, childThreadName, childThreadUIName, } = input; const prefix = ET`${ET.user({ userInfo: creator })}`; let body = `created a new ${threadNoun(threadType)}`; if (parentThreadInfo.name) { body = ET`${body} in ${parentThreadInfo.name}`; } let merged = ET`${prefix} ${body}`; if (childThreadName) { merged = ET`${merged} called "${childThreadName}"`; } return { merged, body, title: childThreadUIName, prefix, }; } -function notifThreadName(threadInfo: ThreadInfo): string { - if (threadInfo.name) { - return threadInfo.name; - } else { - return 'your chat'; - } -} - function mostRecentMessageInfoType( messageInfos: $ReadOnlyArray, ): MessageType { if (messageInfos.length === 0) { throw new Error('expected MessageInfo, but none present!'); } return messageInfos[0].type; } async function fullNotifTextsForMessageInfo( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, getENSNames: ?GetENSNames, ): Promise { const mostRecentType = mostRecentMessageInfoType(messageInfos); const messageSpec = messageSpecs[mostRecentType]; invariant( messageSpec.notificationTexts, `we're not aware of messageType ${mostRecentType}`, ); const innerNotificationTexts = ( innerMessageInfos: $ReadOnlyArray, innerThreadInfo: ThreadInfo, ) => fullNotifTextsForMessageInfo( innerMessageInfos, innerThreadInfo, getENSNames, ); const unresolvedNotifTexts = await messageSpec.notificationTexts( messageInfos, threadInfo, - { - notifThreadName, - notificationTexts: innerNotificationTexts, - }, + { notificationTexts: innerNotificationTexts }, ); const resolveToString = async ( entityText: string | EntityText, ): Promise => { if (typeof entityText === 'string') { return entityText; } const notifString = await getEntityTextAsString(entityText, getENSNames, { prefixThisThreadNounWith: 'your', }); invariant( notifString !== null && notifString !== undefined, 'getEntityTextAsString only returns falsey when passed falsey', ); return notifString; }; let promises = { merged: resolveToString(unresolvedNotifTexts.merged), body: resolveToString(unresolvedNotifTexts.body), title: resolveToString(unresolvedNotifTexts.title), }; if (unresolvedNotifTexts.prefix) { promises = { ...promises, prefix: resolveToString(unresolvedNotifTexts.prefix), }; } return await promiseAll(promises); } function notifRobotextForMessageInfo( messageInfo: RobotextMessageInfo, threadInfo: ThreadInfo, ): EntityText { const robotext = robotextForMessageInfo(messageInfo, threadInfo); return robotext.map(entity => { if ( typeof entity !== 'string' && entity.type === 'thread' && entity.id === threadInfo.id ) { return ET.thread({ display: 'shortName', threadInfo, possessive: entity.possessive, }); } return entity; }); } function notifCollapseKeyForRawMessageInfo( rawMessageInfo: RawMessageInfo, ): ?string { const messageSpec = messageSpecs[rawMessageInfo.type]; return messageSpec.notificationCollapseKey?.(rawMessageInfo) ?? null; } type Unmerged = $ReadOnly<{ body: string, title: string, prefix?: string, ... }>; type Merged = { body: string, title: string, }; function mergePrefixIntoBody(unmerged: Unmerged): Merged { const { body, title, prefix } = unmerged; const merged = prefix ? `${prefix} ${body}` : body; return { body: merged, title }; } export { notifRobotextForMessageInfo, notifTextsForMessageInfo, notifTextsForEntryCreationOrEdit, notifTextsForSubthreadCreation, notifCollapseKeyForRawMessageInfo, mergePrefixIntoBody, };