diff --git a/lib/shared/messages/create-entry-message-spec.js b/lib/shared/messages/create-entry-message-spec.js index a02ef41d9..7bd8fddee 100644 --- a/lib/shared/messages/create-entry-message-spec.js +++ b/lib/shared/messages/create-entry-message-spec.js @@ -1,164 +1,122 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types'; import type { CreateEntryMessageData, CreateEntryMessageInfo, RawCreateEntryMessageInfo, } from '../../types/messages/create-entry'; import type { NotifTexts } from '../../types/notif-types'; import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { prettyDate } from '../../utils/date-utils'; import { ET, type EntityText } from '../../utils/entity-text'; -import { stringForUser } from '../user-utils'; -import { - pushTypes, - type MessageSpec, - type NotificationTextsParams, -} from './message-spec'; +import { notifTextsForEntryCreationOrEdit } from '../notif-utils'; +import { pushTypes, type MessageSpec } from './message-spec'; import { joinResult } from './utils'; export const createEntryMessageSpec: MessageSpec< CreateEntryMessageData, RawCreateEntryMessageInfo, CreateEntryMessageInfo, > = Object.freeze({ messageContentForServerDB( data: CreateEntryMessageData | RawCreateEntryMessageInfo, ): string { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, messageContentForClientDB(data: RawCreateEntryMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawCreateEntryMessageInfo { const content = JSON.parse(row.content); return { type: messageTypes.CREATE_ENTRY, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), entryID: content.entryID, date: content.date, text: content.text, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawCreateEntryMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined', ); const content = JSON.parse(clientDBMessageInfo.content); const rawCreateEntryMessageInfo: RawCreateEntryMessageInfo = { type: messageTypes.CREATE_ENTRY, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, entryID: content.entryID, date: content.date, text: content.text, }; return rawCreateEntryMessageInfo; }, createMessageInfo( rawMessageInfo: RawCreateEntryMessageInfo, creator: RelativeUserInfo, ): CreateEntryMessageInfo { return { type: messageTypes.CREATE_ENTRY, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, entryID: rawMessageInfo.entryID, date: rawMessageInfo.date, text: rawMessageInfo.text, }; }, rawMessageInfoFromMessageData( messageData: CreateEntryMessageData, id: ?string, ): RawCreateEntryMessageInfo { invariant(id, 'RawCreateEntryMessageInfo needs id'); return { ...messageData, id }; }, robotext(messageInfo: CreateEntryMessageInfo): EntityText { const date = prettyDate(messageInfo.date); const creator = ET.user({ userInfo: messageInfo.creator }); const { text } = messageInfo; return ET`${creator} created an event scheduled for ${date}: "${text}"`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, - params: NotificationTextsParams, ): Promise { - const hasCreateEntry = messageInfos.some( - messageInfo => messageInfo.type === messageTypes.CREATE_ENTRY, - ); - const messageInfo = messageInfos[0]; - if (!hasCreateEntry) { - invariant( - messageInfo.type === messageTypes.EDIT_ENTRY, - 'messageInfo should be messageTypes.EDIT_ENTRY!', - ); - const body = - `updated the text of an event in ` + - `${params.notifThreadName(threadInfo)} scheduled for ` + - `${prettyDate(messageInfo.date)}: "${messageInfo.text}"`; - const prefix = stringForUser(messageInfo.creator); - const merged = `${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 prefix = stringForUser(messageInfo.creator); - const body = - `created an event in ${params.notifThreadName(threadInfo)} ` + - `scheduled for ${prettyDate(messageInfo.date)}: "${messageInfo.text}"`; - const merged = `${prefix} ${body}`; - return { - merged, - title: threadInfo.uiName, - body, - prefix, - }; + return notifTextsForEntryCreationOrEdit(messageInfos, threadInfo); }, notificationCollapseKey(rawMessageInfo: RawCreateEntryMessageInfo): string { return joinResult(rawMessageInfo.creatorID, rawMessageInfo.entryID); }, generatesNotifs: async () => pushTypes.NOTIF, }); diff --git a/lib/shared/messages/edit-entry-message-spec.js b/lib/shared/messages/edit-entry-message-spec.js index 9e57fce02..015419579 100644 --- a/lib/shared/messages/edit-entry-message-spec.js +++ b/lib/shared/messages/edit-entry-message-spec.js @@ -1,164 +1,122 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types'; import type { EditEntryMessageData, EditEntryMessageInfo, RawEditEntryMessageInfo, } from '../../types/messages/edit-entry'; import type { NotifTexts } from '../../types/notif-types'; import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { prettyDate } from '../../utils/date-utils'; import { ET, type EntityText } from '../../utils/entity-text'; -import { stringForUser } from '../user-utils'; -import { - pushTypes, - type MessageSpec, - type NotificationTextsParams, -} from './message-spec'; +import { notifTextsForEntryCreationOrEdit } from '../notif-utils'; +import { pushTypes, type MessageSpec } from './message-spec'; import { joinResult } from './utils'; export const editEntryMessageSpec: MessageSpec< EditEntryMessageData, RawEditEntryMessageInfo, EditEntryMessageInfo, > = Object.freeze({ messageContentForServerDB( data: EditEntryMessageData | RawEditEntryMessageInfo, ): string { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, messageContentForClientDB(data: RawEditEntryMessageInfo): string { return this.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawEditEntryMessageInfo { const content = JSON.parse(row.content); return { type: messageTypes.EDIT_ENTRY, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), entryID: content.entryID, date: content.date, text: content.text, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawEditEntryMessageInfo { invariant( clientDBMessageInfo.content !== null && clientDBMessageInfo.content !== undefined, 'content must be defined', ); const content = JSON.parse(clientDBMessageInfo.content); const rawEditEntryMessageInfo: RawEditEntryMessageInfo = { type: messageTypes.EDIT_ENTRY, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, entryID: content.entryID, date: content.date, text: content.text, }; return rawEditEntryMessageInfo; }, createMessageInfo( rawMessageInfo: RawEditEntryMessageInfo, creator: RelativeUserInfo, ): EditEntryMessageInfo { return { type: messageTypes.EDIT_ENTRY, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, entryID: rawMessageInfo.entryID, date: rawMessageInfo.date, text: rawMessageInfo.text, }; }, rawMessageInfoFromMessageData( messageData: EditEntryMessageData, id: ?string, ): RawEditEntryMessageInfo { invariant(id, 'RawEditEntryMessageInfo needs id'); return { ...messageData, id }; }, robotext(messageInfo: EditEntryMessageInfo): EntityText { const date = prettyDate(messageInfo.date); const creator = ET.user({ userInfo: messageInfo.creator }); const { text } = messageInfo; return ET`${creator} updated the text of an event scheduled for ${date}: "${text}"`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, - params: NotificationTextsParams, ): Promise { - const hasCreateEntry = messageInfos.some( - messageInfo => messageInfo.type === messageTypes.CREATE_ENTRY, - ); - const messageInfo = messageInfos[0]; - if (!hasCreateEntry) { - invariant( - messageInfo.type === messageTypes.EDIT_ENTRY, - 'messageInfo should be messageTypes.EDIT_ENTRY!', - ); - const body = - `updated the text of an event in ` + - `${params.notifThreadName(threadInfo)} scheduled for ` + - `${prettyDate(messageInfo.date)}: "${messageInfo.text}"`; - const prefix = stringForUser(messageInfo.creator); - const merged = `${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 prefix = stringForUser(messageInfo.creator); - const body = - `created an event in ${params.notifThreadName(threadInfo)} ` + - `scheduled for ${prettyDate(messageInfo.date)}: "${messageInfo.text}"`; - const merged = `${prefix} ${body}`; - return { - merged, - title: threadInfo.uiName, - body, - prefix, - }; + return notifTextsForEntryCreationOrEdit(messageInfos, threadInfo); }, notificationCollapseKey(rawMessageInfo: RawEditEntryMessageInfo): string { return joinResult(rawMessageInfo.creatorID, rawMessageInfo.entryID); }, generatesNotifs: async () => pushTypes.NOTIF, }); diff --git a/lib/shared/notif-utils.js b/lib/shared/notif-utils.js index a25371cb4..2443837ca 100644 --- a/lib/shared/notif-utils.js +++ b/lib/shared/notif-utils.js @@ -1,196 +1,245 @@ // @flow import invariant from 'invariant'; import { type MessageInfo, type RawMessageInfo, type RobotextMessageInfo, type MessageType, + messageTypes, } from '../types/message-types'; -import type { ResolvedNotifTexts } from '../types/notif-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'; import { stringForUser } from './user-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, + }; +} + const notifTextForSubthreadCreation = ( creator: RelativeUserInfo, threadType: ThreadType, parentThreadInfo: ThreadInfo, childThreadName: ?string, childThreadUIName: string, ) => { const prefix = stringForUser(creator); let body = `created a new ${threadNoun(threadType)}`; if (parentThreadInfo.name) { body += ` in ${parentThreadInfo.name}`; } let merged = `${prefix} ${body}`; if (childThreadName) { 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, notifTextForSubthreadCreation, 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, notifCollapseKeyForRawMessageInfo, mergePrefixIntoBody, };