diff --git a/lib/shared/notif-utils.js b/lib/shared/notif-utils.js index 8ad34b2a8..db8f96ff8 100644 --- a/lib/shared/notif-utils.js +++ b/lib/shared/notif-utils.js @@ -1,247 +1,254 @@ // @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, + pluralizeEntityText, type EntityText, + type UserEntity, } 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, + +childThreadUIName: string | $ReadOnlyArray, }; 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 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, { 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; }; + const unresolvedTitle = unresolvedNotifTexts.title; + const pluralizedTitle = + typeof unresolvedTitle === 'string' + ? unresolvedTitle + : pluralizeEntityText(unresolvedTitle.map(entity => [entity])); let promises = { merged: resolveToString(unresolvedNotifTexts.merged), body: resolveToString(unresolvedNotifTexts.body), - title: resolveToString(unresolvedNotifTexts.title), + title: resolveToString(pluralizedTitle), }; 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, }; diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js index b1043ace3..eb526ebea 100644 --- a/lib/types/notif-types.js +++ b/lib/types/notif-types.js @@ -1,17 +1,17 @@ // @flow -import type { EntityText } from '../utils/entity-text'; +import type { EntityText, UserEntity } from '../utils/entity-text'; export type NotifTexts = { +merged: string | EntityText, +body: string | EntityText, - +title: string | EntityText, + +title: string | $ReadOnlyArray, +prefix?: string | EntityText, }; export type ResolvedNotifTexts = { +merged: string, +body: string, +title: string, +prefix?: string, };