diff --git a/lib/shared/messages/add-members-message-spec.js b/lib/shared/messages/add-members-message-spec.js index b8e2e431c..c98cb799b 100644 --- a/lib/shared/messages/add-members-message-spec.js +++ b/lib/shared/messages/add-members-message-spec.js @@ -1,57 +1,90 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; import type { AddMembersMessageData, AddMembersMessageInfo, RawAddMembersMessageInfo, } from '../../types/message/add-members'; +import { values } from '../../utils/objects'; import type { MessageSpec } from './message-spec'; export const addMembersMessageSpec: MessageSpec< AddMembersMessageData, RawAddMembersMessageInfo, AddMembersMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify(data.addedUserIDs); }, rawMessageInfoFromRow(row) { return { type: messageTypes.ADD_MEMBERS, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), addedUserIDs: JSON.parse(row.content), }; }, createMessageInfo(rawMessageInfo, creator, params) { const addedMembers = params.createRelativeUserInfos( rawMessageInfo.addedUserIDs, ); return { type: messageTypes.ADD_MEMBERS, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, addedMembers, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, robotext(messageInfo, creator, params) { const users = messageInfo.addedMembers; invariant(users.length !== 0, 'added who??'); const addedUsersString = params.robotextForUsers(users); return `${creator} added ${addedUsersString}`; }, + + notificationTexts(messageInfos, threadInfo, params) { + const addedMembersObject = {}; + for (const messageInfo of messageInfos) { + invariant( + messageInfo.type === messageTypes.ADD_MEMBERS, + 'messageInfo should be messageTypes.ADD_MEMBERS!', + ); + for (const member of messageInfo.addedMembers) { + addedMembersObject[member.id] = member; + } + } + const addedMembers = values(addedMembersObject); + + const mostRecentMessageInfo = messageInfos[0]; + invariant( + mostRecentMessageInfo.type === messageTypes.ADD_MEMBERS, + 'messageInfo should be messageTypes.ADD_MEMBERS!', + ); + const mergedMessageInfo = { ...mostRecentMessageInfo, addedMembers }; + + const robotext = params.strippedRobotextForMessageInfo( + mergedMessageInfo, + threadInfo, + ); + const merged = `${robotext} to ${params.notifThreadName(threadInfo)}`; + return { + merged, + title: threadInfo.uiName, + body: robotext, + }; + }, }); diff --git a/lib/shared/messages/change-role-message-spec.js b/lib/shared/messages/change-role-message-spec.js index b993a153c..b19b61a6d 100644 --- a/lib/shared/messages/change-role-message-spec.js +++ b/lib/shared/messages/change-role-message-spec.js @@ -1,65 +1,98 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; import type { ChangeRoleMessageData, ChangeRoleMessageInfo, RawChangeRoleMessageInfo, } from '../../types/message/change-role'; +import { values } from '../../utils/objects'; import type { MessageSpec } from './message-spec'; export const changeRoleMessageSpec: MessageSpec< ChangeRoleMessageData, RawChangeRoleMessageInfo, ChangeRoleMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ userIDs: data.userIDs, newRole: data.newRole, }); }, rawMessageInfoFromRow(row) { const content = JSON.parse(row.content); return { type: messageTypes.CHANGE_ROLE, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), userIDs: content.userIDs, newRole: content.newRole, }; }, createMessageInfo(rawMessageInfo, creator, params) { const members = params.createRelativeUserInfos(rawMessageInfo.userIDs); return { type: messageTypes.CHANGE_ROLE, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, members, newRole: rawMessageInfo.newRole, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, robotext(messageInfo, creator, params) { const users = messageInfo.members; invariant(users.length !== 0, 'changed whose role??'); const usersString = params.robotextForUsers(users); const verb = params.threadInfo.roles[messageInfo.newRole].isDefault ? 'removed' : 'added'; const noun = users.length === 1 ? 'an admin' : 'admins'; return `${creator} ${verb} ${usersString} as ${noun}`; }, + + notificationTexts(messageInfos, threadInfo, params) { + const membersObject = {}; + for (const messageInfo of messageInfos) { + invariant( + messageInfo.type === messageTypes.CHANGE_ROLE, + 'messageInfo should be messageTypes.CHANGE_ROLE!', + ); + for (const member of messageInfo.members) { + membersObject[member.id] = member; + } + } + const members = values(membersObject); + + const mostRecentMessageInfo = messageInfos[0]; + invariant( + mostRecentMessageInfo.type === messageTypes.CHANGE_ROLE, + 'messageInfo should be messageTypes.CHANGE_ROLE!', + ); + const mergedMessageInfo = { ...mostRecentMessageInfo, members }; + + const robotext = params.strippedRobotextForMessageInfo( + mergedMessageInfo, + threadInfo, + ); + const merged = `${robotext} from ${params.notifThreadName(threadInfo)}`; + return { + merged, + title: threadInfo.uiName, + body: robotext, + }; + }, }); diff --git a/lib/shared/messages/change-settings-message-spec.js b/lib/shared/messages/change-settings-message-spec.js index ee0129562..ed49c5dab 100644 --- a/lib/shared/messages/change-settings-message-spec.js +++ b/lib/shared/messages/change-settings-message-spec.js @@ -1,65 +1,84 @@ // @flow +import invariant from 'invariant'; + import { messageTypes } from '../../types/message-types'; import type { ChangeSettingsMessageData, ChangeSettingsMessageInfo, RawChangeSettingsMessageInfo, } from '../../types/message/change-settings'; import type { MessageSpec } from './message-spec'; export const changeSettingsMessageSpec: MessageSpec< ChangeSettingsMessageData, RawChangeSettingsMessageInfo, ChangeSettingsMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ [data.field]: data.value, }); }, rawMessageInfoFromRow(row) { const content = JSON.parse(row.content); const field = Object.keys(content)[0]; return { type: messageTypes.CHANGE_SETTINGS, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), field, value: content[field], }; }, createMessageInfo(rawMessageInfo, creator) { return { type: messageTypes.CHANGE_SETTINGS, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, field: rawMessageInfo.field, value: rawMessageInfo.value, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, robotext(messageInfo, creator, params) { let value; if (messageInfo.field === 'color') { value = `<#${messageInfo.value}|c${messageInfo.threadID}>`; } else { value = messageInfo.value; } return ( `${creator} updated ` + `${params.encodedThreadEntity(messageInfo.threadID, 'the thread')}'s ` + `${messageInfo.field} to "${value}"` ); }, + + notificationTexts(messageInfos, threadInfo, params) { + const mostRecentMessageInfo = messageInfos[0]; + invariant( + mostRecentMessageInfo.type === messageTypes.CHANGE_SETTINGS, + 'messageInfo should be messageTypes.CHANGE_SETTINGS!', + ); + const body = params.strippedRobotextForMessageInfo( + mostRecentMessageInfo, + threadInfo, + ); + return { + merged: body, + title: threadInfo.uiName, + body, + }; + }, }); diff --git a/lib/shared/messages/create-entry-message-spec.js b/lib/shared/messages/create-entry-message-spec.js index 23e3611d2..6c6565cd9 100644 --- a/lib/shared/messages/create-entry-message-spec.js +++ b/lib/shared/messages/create-entry-message-spec.js @@ -1,63 +1,107 @@ // @flow +import invariant from 'invariant'; + import { messageTypes } from '../../types/message-types'; import type { CreateEntryMessageData, CreateEntryMessageInfo, RawCreateEntryMessageInfo, } from '../../types/message/create-entry'; import { prettyDate } from '../../utils/date-utils'; +import { stringForUser } from '../user-utils'; import type { MessageSpec } from './message-spec'; export const createEntryMessageSpec: MessageSpec< CreateEntryMessageData, RawCreateEntryMessageInfo, CreateEntryMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, rawMessageInfoFromRow(row) { 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, }; }, createMessageInfo(rawMessageInfo, creator) { 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, id) { return { ...messageData, id }; }, robotext(messageInfo, creator) { const date = prettyDate(messageInfo.date); return ( `${creator} created an event scheduled for ${date}: ` + `"${messageInfo.text}"` ); }, + + notificationTexts(messageInfos, threadInfo, params) { + 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, + }; + }, }); diff --git a/lib/shared/messages/create-sidebar-message-spec.js b/lib/shared/messages/create-sidebar-message-spec.js index 216148e35..a3359fee0 100644 --- a/lib/shared/messages/create-sidebar-message-spec.js +++ b/lib/shared/messages/create-sidebar-message-spec.js @@ -1,109 +1,135 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; import type { CreateSidebarMessageData, CreateSidebarMessageInfo, RawCreateSidebarMessageInfo, } from '../../types/message/create-sidebar'; +import { stringForUser } from '../user-utils'; import { hasMinCodeVersion } from '../version-utils'; import type { MessageSpec } from './message-spec'; +import { assertSingleMessageInfo } from './utils'; export const createSidebarMessageSpec: MessageSpec< CreateSidebarMessageData, RawCreateSidebarMessageInfo, CreateSidebarMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ ...data.initialThreadState, sourceMessageAuthorID: data.sourceMessageAuthorID, }); }, rawMessageInfoFromRow(row) { const { sourceMessageAuthorID, ...initialThreadState } = JSON.parse( row.content, ); return { type: messageTypes.CREATE_SIDEBAR, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), sourceMessageAuthorID, initialThreadState, }; }, createMessageInfo(rawMessageInfo, creator, params) { const { threadInfos } = params; const parentThreadInfo = threadInfos[rawMessageInfo.initialThreadState.parentThreadID]; const sourceMessageAuthor = params.createRelativeUserInfos([ rawMessageInfo.sourceMessageAuthorID, ])[0]; if (!sourceMessageAuthor) { return null; } return { type: messageTypes.CREATE_SIDEBAR, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, sourceMessageAuthor, initialThreadState: { name: rawMessageInfo.initialThreadState.name, parentThreadInfo, color: rawMessageInfo.initialThreadState.color, otherMembers: params.createRelativeUserInfos( rawMessageInfo.initialThreadState.memberIDs.filter( (userID: string) => userID !== rawMessageInfo.creatorID, ), ), }, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, robotext(messageInfo, creator, params) { let text = `started ${params.encodedThreadEntity( messageInfo.threadID, `this sidebar`, )}`; const users = messageInfo.initialThreadState.otherMembers.filter( (member) => member.id !== messageInfo.sourceMessageAuthor.id, ); if (users.length !== 0) { const initialUsersString = params.robotextForUsers(users); text += ` and added ${initialUsersString}`; } return `${creator} ${text}`; }, shimUnsupportedMessageInfo(rawMessageInfo, platformDetails) { // TODO determine min code version if (hasMinCodeVersion(platformDetails, 75)) { return rawMessageInfo; } const { id } = rawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: 'created a sidebar', unsupportedMessageInfo: rawMessageInfo, }; }, + + notificationTexts(messageInfos, threadInfo) { + const messageInfo = assertSingleMessageInfo(messageInfos); + invariant( + messageInfo.type === messageTypes.CREATE_SIDEBAR, + 'messageInfo should be messageTypes.CREATE_SIDEBAR!', + ); + const prefix = stringForUser(messageInfo.creator); + const title = threadInfo.uiName; + const sourceMessageAuthorPossessive = messageInfo.sourceMessageAuthor + .isViewer + ? 'your' + : `${stringForUser(messageInfo.sourceMessageAuthor)}'s`; + const body = + `started a sidebar in response to ${sourceMessageAuthorPossessive} ` + + `message "${messageInfo.initialThreadState.name ?? ''}"`; + const merged = `${prefix} ${body}`; + return { + merged, + body, + title, + prefix, + }; + }, }); diff --git a/lib/shared/messages/create-sub-thread-message-spec.js b/lib/shared/messages/create-sub-thread-message-spec.js index 833e17a31..841fa5237 100644 --- a/lib/shared/messages/create-sub-thread-message-spec.js +++ b/lib/shared/messages/create-sub-thread-message-spec.js @@ -1,75 +1,93 @@ // @flow +import invariant from 'invariant'; + import { permissionLookup } from '../../permissions/thread-permissions'; import { messageTypes } from '../../types/message-types'; import type { CreateSubthreadMessageData, CreateSubthreadMessageInfo, RawCreateSubthreadMessageInfo, } from '../../types/message/create-subthread'; import { threadPermissions, threadTypes } from '../../types/thread-types'; import type { MessageSpec } from './message-spec'; +import { assertSingleMessageInfo } from './utils'; export const createSubThreadMessageSpec: MessageSpec< CreateSubthreadMessageData, RawCreateSubthreadMessageInfo, CreateSubthreadMessageInfo, > = Object.freeze({ messageContent(data) { return data.childThreadID; }, rawMessageInfoFromRow(row) { const subthreadPermissions = row.subthread_permissions; if (!permissionLookup(subthreadPermissions, threadPermissions.KNOW_OF)) { return null; } return { type: messageTypes.CREATE_SUB_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), childThreadID: row.content, }; }, createMessageInfo(rawMessageInfo, creator, params) { const { threadInfos } = params; const childThreadInfo = threadInfos[rawMessageInfo.childThreadID]; if (!childThreadInfo) { return null; } return { type: messageTypes.CREATE_SUB_THREAD, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, childThreadInfo, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, robotext(messageInfo, creator) { const childName = messageInfo.childThreadInfo.name; const childNoun = messageInfo.childThreadInfo.type === threadTypes.SIDEBAR ? 'sidebar' : 'child thread'; if (childName) { return ( `${creator} created a ${childNoun}` + ` named "<${encodeURI(childName)}|t${messageInfo.childThreadInfo.id}>"` ); } else { return ( `${creator} created a ` + `<${childNoun}|t${messageInfo.childThreadInfo.id}>` ); } }, + + notificationTexts(messageInfos, threadInfo, params) { + const messageInfo = assertSingleMessageInfo(messageInfos); + invariant( + messageInfo.type === messageTypes.CREATE_SUB_THREAD, + 'messageInfo should be messageTypes.CREATE_SUB_THREAD!', + ); + return params.notifTextForSubthreadCreation( + messageInfo.creator, + messageInfo.childThreadInfo.type, + threadInfo, + messageInfo.childThreadInfo.name, + messageInfo.childThreadInfo.uiName, + ); + }, }); diff --git a/lib/shared/messages/create-thread-message-spec.js b/lib/shared/messages/create-thread-message-spec.js index c9d1cc288..2056241a4 100644 --- a/lib/shared/messages/create-thread-message-spec.js +++ b/lib/shared/messages/create-thread-message-spec.js @@ -1,85 +1,119 @@ // @flow +import invariant from 'invariant'; + import { messageTypes } from '../../types/message-types'; import type { CreateThreadMessageData, CreateThreadMessageInfo, RawCreateThreadMessageInfo, } from '../../types/message/create-thread'; +import { stringForUser } from '../user-utils'; import type { MessageSpec } from './message-spec'; +import { assertSingleMessageInfo } from './utils'; export const createThreadMessageSpec: MessageSpec< CreateThreadMessageData, RawCreateThreadMessageInfo, CreateThreadMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify(data.initialThreadState); }, rawMessageInfoFromRow(row) { return { type: messageTypes.CREATE_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), initialThreadState: JSON.parse(row.content), }; }, createMessageInfo(rawMessageInfo, creator, params) { const initialParentThreadID = rawMessageInfo.initialThreadState.parentThreadID; const parentThreadInfo = initialParentThreadID ? params.threadInfos[initialParentThreadID] : null; return { type: messageTypes.CREATE_THREAD, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, initialThreadState: { name: rawMessageInfo.initialThreadState.name, parentThreadInfo, type: rawMessageInfo.initialThreadState.type, color: rawMessageInfo.initialThreadState.color, otherMembers: params.createRelativeUserInfos( rawMessageInfo.initialThreadState.memberIDs.filter( (userID: string) => userID !== rawMessageInfo.creatorID, ), ), }, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, robotext(messageInfo, creator, params) { let text = `created ${params.encodedThreadEntity( messageInfo.threadID, `this thread`, )}`; const parentThread = messageInfo.initialThreadState.parentThreadInfo; if (parentThread) { text += ' as a child of ' + `<${encodeURI(parentThread.uiName)}|t${parentThread.id}>`; } if (messageInfo.initialThreadState.name) { text += ` with the name "${encodeURI( messageInfo.initialThreadState.name, )}"`; } const users = messageInfo.initialThreadState.otherMembers; if (users.length !== 0) { const initialUsersString = params.robotextForUsers(users); text += ` and added ${initialUsersString}`; } return `${creator} ${text}`; }, + + notificationTexts(messageInfos, threadInfo, params) { + const messageInfo = assertSingleMessageInfo(messageInfos); + invariant( + messageInfo.type === messageTypes.CREATE_THREAD, + 'messageInfo should be messageTypes.CREATE_THREAD!', + ); + const parentThreadInfo = messageInfo.initialThreadState.parentThreadInfo; + if (parentThreadInfo) { + return params.notifTextForSubthreadCreation( + messageInfo.creator, + messageInfo.initialThreadState.type, + parentThreadInfo, + messageInfo.initialThreadState.name, + threadInfo.uiName, + ); + } + const prefix = stringForUser(messageInfo.creator); + const body = 'created a new thread'; + let merged = `${prefix} ${body}`; + if (messageInfo.initialThreadState.name) { + merged += ` called "${messageInfo.initialThreadState.name}"`; + } + return { + merged, + body, + title: threadInfo.uiName, + prefix, + }; + }, }); diff --git a/lib/shared/messages/delete-entry-message-spec.js b/lib/shared/messages/delete-entry-message-spec.js index 7d849b042..3be9efb53 100644 --- a/lib/shared/messages/delete-entry-message-spec.js +++ b/lib/shared/messages/delete-entry-message-spec.js @@ -1,63 +1,86 @@ // @flow +import invariant from 'invariant'; + import { messageTypes } from '../../types/message-types'; import type { DeleteEntryMessageData, DeleteEntryMessageInfo, RawDeleteEntryMessageInfo, } from '../../types/message/delete-entry'; import { prettyDate } from '../../utils/date-utils'; +import { stringForUser } from '../user-utils'; import type { MessageSpec } from './message-spec'; +import { assertSingleMessageInfo } from './utils'; export const deleteEntryMessageSpec: MessageSpec< DeleteEntryMessageData, RawDeleteEntryMessageInfo, DeleteEntryMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, rawMessageInfoFromRow(row) { const content = JSON.parse(row.content); return { type: messageTypes.DELETE_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, }; }, createMessageInfo(rawMessageInfo, creator) { return { type: messageTypes.DELETE_ENTRY, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, entryID: rawMessageInfo.entryID, date: rawMessageInfo.date, text: rawMessageInfo.text, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, robotext(messageInfo, creator) { const date = prettyDate(messageInfo.date); return ( `${creator} deleted an event scheduled for ${date}: ` + `"${messageInfo.text}"` ); }, + + notificationTexts(messageInfos, threadInfo, params) { + const messageInfo = assertSingleMessageInfo(messageInfos); + invariant( + messageInfo.type === messageTypes.DELETE_ENTRY, + 'messageInfo should be messageTypes.DELETE_ENTRY!', + ); + const prefix = stringForUser(messageInfo.creator); + const body = + `deleted an event in ${params.notifThreadName(threadInfo)} ` + + `scheduled for ${prettyDate(messageInfo.date)}: "${messageInfo.text}"`; + const merged = `${prefix} ${body}`; + return { + merged, + title: threadInfo.uiName, + body, + prefix, + }; + }, }); diff --git a/lib/shared/messages/edit-entry-message-spec.js b/lib/shared/messages/edit-entry-message-spec.js index 7bb24b4cf..122570965 100644 --- a/lib/shared/messages/edit-entry-message-spec.js +++ b/lib/shared/messages/edit-entry-message-spec.js @@ -1,63 +1,107 @@ // @flow +import invariant from 'invariant'; + import { messageTypes } from '../../types/message-types'; import type { EditEntryMessageData, EditEntryMessageInfo, RawEditEntryMessageInfo, } from '../../types/message/edit-entry'; import { prettyDate } from '../../utils/date-utils'; +import { stringForUser } from '../user-utils'; import type { MessageSpec } from './message-spec'; export const editEntryMessageSpec: MessageSpec< EditEntryMessageData, RawEditEntryMessageInfo, EditEntryMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, rawMessageInfoFromRow(row) { 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, }; }, createMessageInfo(rawMessageInfo, creator) { 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, id) { return { ...messageData, id }; }, robotext(messageInfo, creator) { const date = prettyDate(messageInfo.date); return ( `${creator} updated the text of an event scheduled for ` + `${date}: "${messageInfo.text}"` ); }, + + notificationTexts(messageInfos, threadInfo, params) { + 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, + }; + }, }); diff --git a/lib/shared/messages/join-thread-message-spec.js b/lib/shared/messages/join-thread-message-spec.js index 181ba086c..e3052b301 100644 --- a/lib/shared/messages/join-thread-message-spec.js +++ b/lib/shared/messages/join-thread-message-spec.js @@ -1,46 +1,72 @@ // @flow +import invariant from 'invariant'; + import { messageTypes } from '../../types/message-types'; import type { JoinThreadMessageData, JoinThreadMessageInfo, RawJoinThreadMessageInfo, } from '../../types/message/join-thread'; +import { values } from '../../utils/objects'; +import { pluralize } from '../../utils/text-utils'; +import { stringForUser } from '../user-utils'; import type { MessageSpec } from './message-spec'; export const joinThreadMessageSpec: MessageSpec< JoinThreadMessageData, RawJoinThreadMessageInfo, JoinThreadMessageInfo, > = Object.freeze({ rawMessageInfoFromRow(row) { return { type: messageTypes.JOIN_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), }; }, createMessageInfo(rawMessageInfo, creator) { return { type: messageTypes.JOIN_THREAD, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, robotext(messageInfo, creator, params) { return ( `${creator} joined ` + params.encodedThreadEntity(messageInfo.threadID, 'this thread') ); }, + + notificationTexts(messageInfos, threadInfo, params) { + const joinerArray = {}; + for (const messageInfo of messageInfos) { + invariant( + messageInfo.type === messageTypes.JOIN_THREAD, + 'messageInfo should be messageTypes.JOIN_THREAD!', + ); + joinerArray[messageInfo.creator.id] = messageInfo.creator; + } + const joiners = values(joinerArray); + const joinersString = pluralize(joiners.map(stringForUser)); + + const body = `${joinersString} joined`; + const merged = `${body} ${params.notifThreadName(threadInfo)}`; + return { + merged, + title: threadInfo.uiName, + body, + }; + }, }); diff --git a/lib/shared/messages/leave-thread-message-spec.js b/lib/shared/messages/leave-thread-message-spec.js index 8cc239674..9350fddb2 100644 --- a/lib/shared/messages/leave-thread-message-spec.js +++ b/lib/shared/messages/leave-thread-message-spec.js @@ -1,46 +1,72 @@ // @flow +import invariant from 'invariant'; + import { messageTypes } from '../../types/message-types'; import type { LeaveThreadMessageData, LeaveThreadMessageInfo, RawLeaveThreadMessageInfo, } from '../../types/message/leave-thread'; +import { values } from '../../utils/objects'; +import { pluralize } from '../../utils/text-utils'; +import { stringForUser } from '../user-utils'; import type { MessageSpec } from './message-spec'; export const leaveThreadMessageSpec: MessageSpec< LeaveThreadMessageData, RawLeaveThreadMessageInfo, LeaveThreadMessageInfo, > = Object.freeze({ rawMessageInfoFromRow(row) { return { type: messageTypes.LEAVE_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), }; }, createMessageInfo(rawMessageInfo, creator) { return { type: messageTypes.LEAVE_THREAD, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, robotext(messageInfo, creator, params) { return ( `${creator} left ` + params.encodedThreadEntity(messageInfo.threadID, 'this thread') ); }, + + notificationTexts(messageInfos, threadInfo, params) { + const leaverBeavers = {}; + for (const messageInfo of messageInfos) { + invariant( + messageInfo.type === messageTypes.LEAVE_THREAD, + 'messageInfo should be messageTypes.LEAVE_THREAD!', + ); + leaverBeavers[messageInfo.creator.id] = messageInfo.creator; + } + const leavers = values(leaverBeavers); + const leaversString = pluralize(leavers.map(stringForUser)); + + const body = `${leaversString} left`; + const merged = `${body} ${params.notifThreadName(threadInfo)}`; + return { + merged, + title: threadInfo.uiName, + body, + }; + }, }); diff --git a/lib/shared/messages/message-spec.js b/lib/shared/messages/message-spec.js index 5d30eed37..023c58947 100644 --- a/lib/shared/messages/message-spec.js +++ b/lib/shared/messages/message-spec.js @@ -1,54 +1,78 @@ // @flow import type { PlatformDetails } from '../../types/device-types'; import type { Media } from '../../types/media-types'; import type { MessageInfo, RawComposableMessageInfo, RawMessageInfo, RawRobotextMessageInfo, + RobotextMessageInfo, } from '../../types/message-types'; import type { RawUnsupportedMessageInfo } from '../../types/message/unsupported'; -import type { ThreadInfo } from '../../types/thread-types'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo, ThreadType } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; export type MessageSpec = {| +messageContent?: (data: Data) => string | null, +rawMessageInfoFromRow?: ( row: Object, params: {| +localID: string, +media?: $ReadOnlyArray, +derivedMessages: $ReadOnlyMap< string, RawComposableMessageInfo | RawRobotextMessageInfo, >, |}, ) => ?RawInfo, +createMessageInfo: ( rawMessageInfo: RawInfo, creator: RelativeUserInfo, params: {| +threadInfos: {| [id: string]: ThreadInfo |}, +createMessageInfoFromRaw: (rawInfo: RawMessageInfo) => MessageInfo, +createRelativeUserInfos: ( userIDs: $ReadOnlyArray, ) => RelativeUserInfo[], |}, ) => ?Info, +rawMessageInfoFromMessageData?: (messageData: Data, id: string) => RawInfo, +robotext?: ( messageInfo: Info, creator: string, params: {| +encodedThreadEntity: (threadID: string, text: string) => string, +robotextForUsers: (users: RelativeUserInfo[]) => string, +robotextForUser: (user: RelativeUserInfo) => string, +threadInfo: ThreadInfo, |}, ) => string, +shimUnsupportedMessageInfo?: ( rawMessageInfo: RawInfo, platformDetails: ?PlatformDetails, ) => RawInfo | RawUnsupportedMessageInfo, + +notificationTexts?: ( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: {| + +notifThreadName: (threadInfo: ThreadInfo) => string, + +notifTextForSubthreadCreation: ( + creator: RelativeUserInfo, + threadType: ThreadType, + parentThreadInfo: ThreadInfo, + childThreadName: ?string, + childThreadUIName: string, + ) => NotifTexts, + +strippedRobotextForMessageInfo: ( + messageInfo: RobotextMessageInfo, + threadInfo: ThreadInfo, + ) => string, + +notificationTexts: ( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + ) => NotifTexts, + |}, + ) => NotifTexts, |}; diff --git a/lib/shared/messages/multimedia-message-spec.js b/lib/shared/messages/multimedia-message-spec.js index 0392e3a45..18cbf0a9f 100644 --- a/lib/shared/messages/multimedia-message-spec.js +++ b/lib/shared/messages/multimedia-message-spec.js @@ -1,182 +1,219 @@ // @flow import invariant from 'invariant'; import { + contentStringForMediaArray, multimediaMessagePreview, shimUploadURI, } from '../../media/media-utils'; import type { PlatformDetails } from '../../types/device-types'; import type { Media, Video, Image } from '../../types/media-types'; import type { RawMultimediaMessageInfo } from '../../types/message-types'; import { messageTypes } from '../../types/message-types'; import type { ImagesMessageData, RawImagesMessageInfo, ImagesMessageInfo, } from '../../types/message/images'; import type { MediaMessageData, MediaMessageInfo, RawMediaMessageInfo, } from '../../types/message/media'; import { createMediaMessageInfo } from '../message-utils'; +import { threadIsGroupChat } from '../thread-utils'; +import { stringForUser } from '../user-utils'; import { hasMinCodeVersion } from '../version-utils'; import type { MessageSpec } from './message-spec'; export const multimediaMessageSpec: MessageSpec< MediaMessageData | ImagesMessageData, RawMediaMessageInfo | RawImagesMessageInfo, MediaMessageInfo | ImagesMessageInfo, > = Object.freeze({ messageContent(data) { const mediaIDs = data.media.map((media) => parseInt(media.id, 10)); return JSON.stringify(mediaIDs); }, rawMessageInfoFromRow(row, params) { const { localID, media } = params; invariant(media, 'Media should be provided'); return createMediaMessageInfo({ threadID: row.threadID.toString(), creatorID: row.creatorID.toString(), media, id: row.id.toString(), localID, time: row.time, }); }, createMessageInfo(rawMessageInfo, creator) { if (rawMessageInfo.type === messageTypes.IMAGES) { const messageInfo: ImagesMessageInfo = { type: messageTypes.IMAGES, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, media: rawMessageInfo.media, }; if (rawMessageInfo.id) { messageInfo.id = rawMessageInfo.id; } if (rawMessageInfo.localID) { messageInfo.localID = rawMessageInfo.localID; } return messageInfo; } else if (rawMessageInfo.type === messageTypes.MULTIMEDIA) { const messageInfo: MediaMessageInfo = { type: messageTypes.MULTIMEDIA, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, media: rawMessageInfo.media, }; if (rawMessageInfo.id) { messageInfo.id = rawMessageInfo.id; } if (rawMessageInfo.localID) { messageInfo.localID = rawMessageInfo.localID; } return messageInfo; } }, rawMessageInfoFromMessageData(messageData, id) { if (messageData.type === messageTypes.IMAGES) { return ({ ...messageData, id }: RawImagesMessageInfo); } else { return ({ ...messageData, id }: RawMediaMessageInfo); } }, shimUnsupportedMessageInfo(rawMessageInfo, platformDetails) { if (rawMessageInfo.type === messageTypes.IMAGES) { const shimmedRawMessageInfo = shimMediaMessageInfo( rawMessageInfo, platformDetails, ); if (hasMinCodeVersion(platformDetails, 30)) { return shimmedRawMessageInfo; } const { id } = shimmedRawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: shimmedRawMessageInfo.threadID, creatorID: shimmedRawMessageInfo.creatorID, time: shimmedRawMessageInfo.time, robotext: multimediaMessagePreview(shimmedRawMessageInfo), unsupportedMessageInfo: shimmedRawMessageInfo, }; } else { const shimmedRawMessageInfo = shimMediaMessageInfo( rawMessageInfo, platformDetails, ); // TODO figure out first native codeVersion supporting video playback if (hasMinCodeVersion(platformDetails, 62)) { return shimmedRawMessageInfo; } const { id } = shimmedRawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: shimmedRawMessageInfo.threadID, creatorID: shimmedRawMessageInfo.creatorID, time: shimmedRawMessageInfo.time, robotext: multimediaMessagePreview(shimmedRawMessageInfo), unsupportedMessageInfo: shimmedRawMessageInfo, }; } }, + + notificationTexts(messageInfos, threadInfo, params) { + const media = []; + for (const messageInfo of messageInfos) { + invariant( + messageInfo.type === messageTypes.IMAGES || + messageInfo.type === messageTypes.MULTIMEDIA, + 'messageInfo should be multimedia type!', + ); + for (const singleMedia of messageInfo.media) { + media.push(singleMedia); + } + } + const contentString = contentStringForMediaArray(media); + const userString = stringForUser(messageInfos[0].creator); + + let body, merged; + if (!threadInfo.name && !threadIsGroupChat(threadInfo)) { + body = `sent you ${contentString}`; + merged = body; + } else { + body = `sent ${contentString}`; + const threadName = params.notifThreadName(threadInfo); + merged = `${body} to ${threadName}`; + } + merged = `${userString} ${merged}`; + + return { + merged, + body, + title: threadInfo.uiName, + prefix: userString, + }; + }, }); function shimMediaMessageInfo( rawMessageInfo: RawMultimediaMessageInfo, platformDetails: ?PlatformDetails, ): RawMultimediaMessageInfo { if (rawMessageInfo.type === messageTypes.IMAGES) { let uriChanged = false; const newMedia: Image[] = []; for (const singleMedia of rawMessageInfo.media) { const shimmedURI = shimUploadURI(singleMedia.uri, platformDetails); if (shimmedURI === singleMedia.uri) { newMedia.push(singleMedia); } else { newMedia.push(({ ...singleMedia, uri: shimmedURI }: Image)); uriChanged = true; } } if (!uriChanged) { return rawMessageInfo; } return ({ ...rawMessageInfo, media: newMedia, }: RawImagesMessageInfo); } else { let uriChanged = false; const newMedia: Media[] = []; for (const singleMedia of rawMessageInfo.media) { const shimmedURI = shimUploadURI(singleMedia.uri, platformDetails); if (shimmedURI === singleMedia.uri) { newMedia.push(singleMedia); } else if (singleMedia.type === 'photo') { newMedia.push(({ ...singleMedia, uri: shimmedURI }: Image)); uriChanged = true; } else { newMedia.push(({ ...singleMedia, uri: shimmedURI }: Video)); uriChanged = true; } } if (!uriChanged) { return rawMessageInfo; } return ({ ...rawMessageInfo, media: newMedia, }: RawMediaMessageInfo); } } diff --git a/lib/shared/messages/remove-members-message-spec.js b/lib/shared/messages/remove-members-message-spec.js index fedb46267..38411ed77 100644 --- a/lib/shared/messages/remove-members-message-spec.js +++ b/lib/shared/messages/remove-members-message-spec.js @@ -1,57 +1,90 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; import type { RawRemoveMembersMessageInfo, RemoveMembersMessageData, RemoveMembersMessageInfo, } from '../../types/message/remove-members'; +import { values } from '../../utils/objects'; import type { MessageSpec } from './message-spec'; export const removeMembersMessageSpec: MessageSpec< RemoveMembersMessageData, RawRemoveMembersMessageInfo, RemoveMembersMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify(data.removedUserIDs); }, rawMessageInfoFromRow(row) { return { type: messageTypes.REMOVE_MEMBERS, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), removedUserIDs: JSON.parse(row.content), }; }, createMessageInfo(rawMessageInfo, creator, params) { const removedMembers = params.createRelativeUserInfos( rawMessageInfo.removedUserIDs, ); return { type: messageTypes.REMOVE_MEMBERS, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, removedMembers, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, robotext(messageInfo, creator, params) { const users = messageInfo.removedMembers; invariant(users.length !== 0, 'removed who??'); const removedUsersString = params.robotextForUsers(users); return `${creator} removed ${removedUsersString}`; }, + + notificationTexts(messageInfos, threadInfo, params) { + const removedMembersObject = {}; + for (const messageInfo of messageInfos) { + invariant( + messageInfo.type === messageTypes.REMOVE_MEMBERS, + 'messageInfo should be messageTypes.REMOVE_MEMBERS!', + ); + for (const member of messageInfo.removedMembers) { + removedMembersObject[member.id] = member; + } + } + const removedMembers = values(removedMembersObject); + + const mostRecentMessageInfo = messageInfos[0]; + invariant( + mostRecentMessageInfo.type === messageTypes.REMOVE_MEMBERS, + 'messageInfo should be messageTypes.REMOVE_MEMBERS!', + ); + const mergedMessageInfo = { ...mostRecentMessageInfo, removedMembers }; + + const robotext = params.strippedRobotextForMessageInfo( + mergedMessageInfo, + threadInfo, + ); + const merged = `${robotext} from ${params.notifThreadName(threadInfo)}`; + return { + merged, + title: threadInfo.uiName, + body: robotext, + }; + }, }); diff --git a/lib/shared/messages/restore-entry-message-spec.js b/lib/shared/messages/restore-entry-message-spec.js index 922da496b..3332cbad6 100644 --- a/lib/shared/messages/restore-entry-message-spec.js +++ b/lib/shared/messages/restore-entry-message-spec.js @@ -1,63 +1,86 @@ // @flow +import invariant from 'invariant'; + import { messageTypes } from '../../types/message-types'; import type { RawRestoreEntryMessageInfo, RestoreEntryMessageData, RestoreEntryMessageInfo, } from '../../types/message/restore-entry'; import { prettyDate } from '../../utils/date-utils'; +import { stringForUser } from '../user-utils'; import type { MessageSpec } from './message-spec'; +import { assertSingleMessageInfo } from './utils'; export const restoreEntryMessageSpec: MessageSpec< RestoreEntryMessageData, RawRestoreEntryMessageInfo, RestoreEntryMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, rawMessageInfoFromRow(row) { const content = JSON.parse(row.content); return { type: messageTypes.RESTORE_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, }; }, createMessageInfo(rawMessageInfo, creator) { return { type: messageTypes.RESTORE_ENTRY, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, entryID: rawMessageInfo.entryID, date: rawMessageInfo.date, text: rawMessageInfo.text, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, robotext(messageInfo, creator) { const date = prettyDate(messageInfo.date); return ( `${creator} restored an event scheduled for ${date}: ` + `"${messageInfo.text}"` ); }, + + notificationTexts(messageInfos, threadInfo, params) { + const messageInfo = assertSingleMessageInfo(messageInfos); + invariant( + messageInfo.type === messageTypes.RESTORE_ENTRY, + 'messageInfo should be messageTypes.RESTORE_ENTRY!', + ); + const prefix = stringForUser(messageInfo.creator); + const body = + `restored an event in ${params.notifThreadName(threadInfo)} ` + + `scheduled for ${prettyDate(messageInfo.date)}: "${messageInfo.text}"`; + const merged = `${prefix} ${body}`; + return { + merged, + title: threadInfo.uiName, + body, + prefix, + }; + }, }); diff --git a/lib/shared/messages/sidebar-source-message-spec.js b/lib/shared/messages/sidebar-source-message-spec.js index 105c91423..dba75f99c 100644 --- a/lib/shared/messages/sidebar-source-message-spec.js +++ b/lib/shared/messages/sidebar-source-message-spec.js @@ -1,95 +1,106 @@ // @flow import invariant from 'invariant'; import type { RawSidebarSourceMessageInfo, SidebarSourceMessageData, SidebarSourceMessageInfo, } from '../../types/message-types'; import { messageTypes } from '../../types/message-types'; import { hasMinCodeVersion } from '../version-utils'; import type { MessageSpec } from './message-spec'; +import { assertSingleMessageInfo } from './utils'; export const sidebarSourceMessageSpec: MessageSpec< SidebarSourceMessageData, RawSidebarSourceMessageInfo, SidebarSourceMessageInfo, > = Object.freeze({ messageContent(data) { const sourceMessageID = data.sourceMessage?.id; invariant(sourceMessageID, 'Source message id should be set'); return JSON.stringify({ sourceMessageID, }); }, rawMessageInfoFromRow(row, params) { const { derivedMessages } = params; invariant(derivedMessages, 'Derived messages should be provided'); const content = JSON.parse(row.content); const sourceMessage = derivedMessages.get(content.sourceMessageID); if (!sourceMessage) { console.warn( `Message with id ${row.id} has a derived message ` + `${content.sourceMessageID} which is not present in the database`, ); } return { type: messageTypes.SIDEBAR_SOURCE, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), sourceMessage, }; }, createMessageInfo(rawMessageInfo, creator, params) { if (!rawMessageInfo.sourceMessage) { return null; } const sourceMessage = params.createMessageInfoFromRaw( rawMessageInfo.sourceMessage, ); invariant( sourceMessage && sourceMessage.type !== messageTypes.SIDEBAR_SOURCE, 'Sidebars can not be created from SIDEBAR SOURCE', ); return { type: messageTypes.SIDEBAR_SOURCE, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, sourceMessage, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, shimUnsupportedMessageInfo(rawMessageInfo, platformDetails) { // TODO determine min code version if ( hasMinCodeVersion(platformDetails, 75) && rawMessageInfo.sourceMessage ) { return rawMessageInfo; } const { id } = rawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: 'first message in sidebar', unsupportedMessageInfo: rawMessageInfo, }; }, + + notificationTexts(messageInfos, threadInfo, params) { + const messageInfo = assertSingleMessageInfo(messageInfos); + invariant( + messageInfo.type === messageTypes.SIDEBAR_SOURCE, + 'messageInfo should be messageTypes.SIDEBAR_SOURCE!', + ); + const sourceMessageInfo = messageInfo.sourceMessage; + return params.notificationTexts([sourceMessageInfo], threadInfo); + }, }); diff --git a/lib/shared/messages/text-message-spec.js b/lib/shared/messages/text-message-spec.js index 20b9deead..5814b060e 100644 --- a/lib/shared/messages/text-message-spec.js +++ b/lib/shared/messages/text-message-spec.js @@ -1,55 +1,84 @@ // @flow +import invariant from 'invariant'; + import { messageTypes } from '../../types/message-types'; import type { RawTextMessageInfo, TextMessageData, TextMessageInfo, } from '../../types/message/text'; +import { threadIsGroupChat } from '../thread-utils'; +import { stringForUser } from '../user-utils'; import type { MessageSpec } from './message-spec'; +import { assertSingleMessageInfo } from './utils'; export const textMessageSpec: MessageSpec< TextMessageData, RawTextMessageInfo, TextMessageInfo, > = Object.freeze({ messageContent(data) { return data.text; }, rawMessageInfoFromRow(row, params) { const rawTextMessageInfo: RawTextMessageInfo = { type: messageTypes.TEXT, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), text: row.content, }; if (params.localID) { rawTextMessageInfo.localID = params.localID; } return rawTextMessageInfo; }, createMessageInfo(rawMessageInfo, creator) { const messageInfo: TextMessageInfo = { type: messageTypes.TEXT, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, text: rawMessageInfo.text, }; if (rawMessageInfo.id) { messageInfo.id = rawMessageInfo.id; } if (rawMessageInfo.localID) { messageInfo.localID = rawMessageInfo.localID; } return messageInfo; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, + + notificationTexts(messageInfos, threadInfo, params) { + const messageInfo = assertSingleMessageInfo(messageInfos); + invariant( + messageInfo.type === messageTypes.TEXT, + 'messageInfo should be messageTypes.TEXT!', + ); + if (!threadInfo.name && !threadIsGroupChat(threadInfo)) { + return { + merged: `${threadInfo.uiName}: ${messageInfo.text}`, + body: messageInfo.text, + title: threadInfo.uiName, + }; + } else { + const userString = stringForUser(messageInfo.creator); + const threadName = params.notifThreadName(threadInfo); + return { + merged: `${userString} to ${threadName}: ${messageInfo.text}`, + body: messageInfo.text, + title: threadInfo.uiName, + prefix: `${userString}:`, + }; + } + }, }); diff --git a/lib/shared/messages/update-relationship-message-spec.js b/lib/shared/messages/update-relationship-message-spec.js index 642df52a6..ae0d02e57 100644 --- a/lib/shared/messages/update-relationship-message-spec.js +++ b/lib/shared/messages/update-relationship-message-spec.js @@ -1,92 +1,111 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; import type { RawUpdateRelationshipMessageInfo, UpdateRelationshipMessageData, UpdateRelationshipMessageInfo, } from '../../types/message/update-relationship'; +import { stringForUser } from '../user-utils'; import { hasMinCodeVersion } from '../version-utils'; import type { MessageSpec } from './message-spec'; +import { assertSingleMessageInfo } from './utils'; export const updateRelationshipMessageSpec: MessageSpec< UpdateRelationshipMessageData, RawUpdateRelationshipMessageInfo, UpdateRelationshipMessageInfo, > = Object.freeze({ messageContent(data) { return JSON.stringify({ operation: data.operation, targetID: data.targetID, }); }, rawMessageInfoFromRow(row) { const content = JSON.parse(row.content); return { type: messageTypes.UPDATE_RELATIONSHIP, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), targetID: content.targetID, operation: content.operation, }; }, createMessageInfo(rawMessageInfo, creator, params) { const target = params.createRelativeUserInfos([rawMessageInfo.targetID])[0]; if (!target) { return null; } return { type: messageTypes.UPDATE_RELATIONSHIP, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, target, time: rawMessageInfo.time, operation: rawMessageInfo.operation, }; }, rawMessageInfoFromMessageData(messageData, id) { return { ...messageData, id }; }, robotext(messageInfo, creator, params) { const target = params.robotextForUser(messageInfo.target); if (messageInfo.operation === 'request_sent') { return `${creator} sent ${target} a friend request`; } else if (messageInfo.operation === 'request_accepted') { const targetPossessive = messageInfo.target.isViewer ? 'your' : `${target}'s`; return `${creator} accepted ${targetPossessive} friend request`; } invariant( false, `Invalid operation ${messageInfo.operation} ` + `of message with type ${messageInfo.type}`, ); }, shimUnsupportedMessageInfo(rawMessageInfo, platformDetails) { if (hasMinCodeVersion(platformDetails, 71)) { return rawMessageInfo; } const { id } = rawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: 'performed a relationship action', unsupportedMessageInfo: rawMessageInfo, }; }, + + notificationTexts(messageInfos, threadInfo) { + const messageInfo = assertSingleMessageInfo(messageInfos); + const prefix = stringForUser(messageInfo.creator); + const title = threadInfo.uiName; + const body = + messageInfo.operation === 'request_sent' + ? 'sent you a friend request' + : 'accepted your friend request'; + const merged = `${prefix} ${body}`; + return { + merged, + body, + title, + prefix, + }; + }, }); diff --git a/lib/shared/messages/utils.js b/lib/shared/messages/utils.js new file mode 100644 index 000000000..ef417bf87 --- /dev/null +++ b/lib/shared/messages/utils.js @@ -0,0 +1,18 @@ +// @flow + +import type { MessageInfo } from '../../types/message-types'; + +export function assertSingleMessageInfo( + messageInfos: $ReadOnlyArray, +): MessageInfo { + if (messageInfos.length === 0) { + throw new Error('expected single MessageInfo, but none present!'); + } else if (messageInfos.length !== 1) { + const messageIDs = messageInfos.map((messageInfo) => messageInfo.id); + console.log( + 'expected single MessageInfo, but there are multiple! ' + + messageIDs.join(', '), + ); + } + return messageInfos[0]; +} diff --git a/lib/shared/notif-utils.js b/lib/shared/notif-utils.js index f08016e3a..726b3d879 100644 --- a/lib/shared/notif-utils.js +++ b/lib/shared/notif-utils.js @@ -1,564 +1,186 @@ // @flow import invariant from 'invariant'; -import { contentStringForMediaArray } from '../media/media-utils'; import { type MessageInfo, type RawMessageInfo, type RobotextMessageInfo, type MessageType, messageTypes, } from '../types/message-types'; +import type { NotifTexts } 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 { values } from '../utils/objects'; -import { pluralize } from '../utils/text-utils'; import { robotextForMessageInfo, robotextToRawString } from './message-utils'; -import { threadIsGroupChat, threadNoun } from './thread-utils'; +import { messageSpecs } from './messages/message-specs'; +import { threadNoun } from './thread-utils'; import { stringForUser } from './user-utils'; -type NotifTexts = {| - merged: string, - body: string, - title: string, - prefix?: string, -|}; function notifTextsForMessageInfo( messageInfos: MessageInfo[], threadInfo: ThreadInfo, ): NotifTexts { const fullNotifTexts = fullNotifTextsForMessageInfo(messageInfos, threadInfo); - const result: NotifTexts = { + return { merged: trimNotifText(fullNotifTexts.merged, 300), body: trimNotifText(fullNotifTexts.body, 300), title: trimNotifText(fullNotifTexts.title, 100), + ...(fullNotifTexts.prefix && { + prefix: trimNotifText(fullNotifTexts.prefix, 50), + }), }; - if (fullNotifTexts.prefix) { - result.prefix = trimNotifText(fullNotifTexts.prefix, 50); - } - return result; } function trimNotifText(text: string, maxLength: number): string { if (text.length <= maxLength) { return text; } return text.substr(0, maxLength - 3) + '...'; } 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 thread'; } } -function assertSingleMessageInfo( - messageInfos: $ReadOnlyArray, -): MessageInfo { - if (messageInfos.length === 0) { - throw new Error('expected single MessageInfo, but none present!'); - } else if (messageInfos.length !== 1) { - const messageIDs = messageInfos.map((messageInfo) => messageInfo.id); - console.log( - 'expected single MessageInfo, but there are multiple! ' + - messageIDs.join(', '), - ); - } - return messageInfos[0]; -} - function mostRecentMessageInfoType( messageInfos: $ReadOnlyArray, ): MessageType { if (messageInfos.length === 0) { throw new Error('expected MessageInfo, but none present!'); } return messageInfos[0].type; } function fullNotifTextsForMessageInfo( - messageInfos: MessageInfo[], + messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): NotifTexts { const mostRecentType = mostRecentMessageInfoType(messageInfos); - if (mostRecentType === messageTypes.TEXT) { - const messageInfo = assertSingleMessageInfo(messageInfos); - invariant( - messageInfo.type === messageTypes.TEXT, - 'messageInfo should be messageTypes.TEXT!', - ); - if (!threadInfo.name && !threadIsGroupChat(threadInfo)) { - return { - merged: `${threadInfo.uiName}: ${messageInfo.text}`, - body: messageInfo.text, - title: threadInfo.uiName, - }; - } else { - const userString = stringForUser(messageInfo.creator); - const threadName = notifThreadName(threadInfo); - return { - merged: `${userString} to ${threadName}: ${messageInfo.text}`, - body: messageInfo.text, - title: threadInfo.uiName, - prefix: `${userString}:`, - }; - } - } else if (mostRecentType === messageTypes.CREATE_THREAD) { - const messageInfo = assertSingleMessageInfo(messageInfos); - invariant( - messageInfo.type === messageTypes.CREATE_THREAD, - 'messageInfo should be messageTypes.CREATE_THREAD!', - ); - const parentThreadInfo = messageInfo.initialThreadState.parentThreadInfo; - if (parentThreadInfo) { - return notifTextForSubthreadCreation( - messageInfo.creator, - messageInfo.initialThreadState.type, - parentThreadInfo, - messageInfo.initialThreadState.name, - threadInfo.uiName, - ); - } - const prefix = stringForUser(messageInfo.creator); - const body = 'created a new thread'; - let merged = `${prefix} ${body}`; - if (messageInfo.initialThreadState.name) { - merged += ` called "${messageInfo.initialThreadState.name}"`; - } - return { - merged, - body, - title: threadInfo.uiName, - prefix, - }; - } else if (mostRecentType === messageTypes.ADD_MEMBERS) { - const addedMembersObject = {}; - for (let messageInfo of messageInfos) { - invariant( - messageInfo.type === messageTypes.ADD_MEMBERS, - 'messageInfo should be messageTypes.ADD_MEMBERS!', - ); - for (let member of messageInfo.addedMembers) { - addedMembersObject[member.id] = member; - } - } - const addedMembers = values(addedMembersObject); - - const mostRecentMessageInfo = messageInfos[0]; - invariant( - mostRecentMessageInfo.type === messageTypes.ADD_MEMBERS, - 'messageInfo should be messageTypes.ADD_MEMBERS!', - ); - const mergedMessageInfo = { ...mostRecentMessageInfo, addedMembers }; - - const robotext = strippedRobotextForMessageInfo( - mergedMessageInfo, - threadInfo, - ); - const merged = `${robotext} to ${notifThreadName(threadInfo)}`; - return { - merged, - title: threadInfo.uiName, - body: robotext, - }; - } else if (mostRecentType === messageTypes.CREATE_SUB_THREAD) { - const messageInfo = assertSingleMessageInfo(messageInfos); - invariant( - messageInfo.type === messageTypes.CREATE_SUB_THREAD, - 'messageInfo should be messageTypes.CREATE_SUB_THREAD!', - ); - return notifTextForSubthreadCreation( - messageInfo.creator, - messageInfo.childThreadInfo.type, - threadInfo, - messageInfo.childThreadInfo.name, - messageInfo.childThreadInfo.uiName, - ); - } else if (mostRecentType === messageTypes.REMOVE_MEMBERS) { - const removedMembersObject = {}; - for (let messageInfo of messageInfos) { - invariant( - messageInfo.type === messageTypes.REMOVE_MEMBERS, - 'messageInfo should be messageTypes.REMOVE_MEMBERS!', - ); - for (let member of messageInfo.removedMembers) { - removedMembersObject[member.id] = member; - } - } - const removedMembers = values(removedMembersObject); - - const mostRecentMessageInfo = messageInfos[0]; - invariant( - mostRecentMessageInfo.type === messageTypes.REMOVE_MEMBERS, - 'messageInfo should be messageTypes.REMOVE_MEMBERS!', - ); - const mergedMessageInfo = { ...mostRecentMessageInfo, removedMembers }; - - const robotext = strippedRobotextForMessageInfo( - mergedMessageInfo, - threadInfo, - ); - const merged = `${robotext} from ${notifThreadName(threadInfo)}`; - return { - merged, - title: threadInfo.uiName, - body: robotext, - }; - } else if (mostRecentType === messageTypes.CHANGE_ROLE) { - const membersObject = {}; - for (let messageInfo of messageInfos) { - invariant( - messageInfo.type === messageTypes.CHANGE_ROLE, - 'messageInfo should be messageTypes.CHANGE_ROLE!', - ); - for (let member of messageInfo.members) { - membersObject[member.id] = member; - } - } - const members = values(membersObject); - - const mostRecentMessageInfo = messageInfos[0]; - invariant( - mostRecentMessageInfo.type === messageTypes.CHANGE_ROLE, - 'messageInfo should be messageTypes.CHANGE_ROLE!', - ); - const mergedMessageInfo = { ...mostRecentMessageInfo, members }; - - const robotext = strippedRobotextForMessageInfo( - mergedMessageInfo, - threadInfo, - ); - const merged = `${robotext} from ${notifThreadName(threadInfo)}`; - return { - merged, - title: threadInfo.uiName, - body: robotext, - }; - } else if (mostRecentType === messageTypes.LEAVE_THREAD) { - const leaverBeavers = {}; - for (let messageInfo of messageInfos) { - invariant( - messageInfo.type === messageTypes.LEAVE_THREAD, - 'messageInfo should be messageTypes.LEAVE_THREAD!', - ); - leaverBeavers[messageInfo.creator.id] = messageInfo.creator; - } - const leavers = values(leaverBeavers); - const leaversString = pluralize(leavers.map(stringForUser)); - - const body = `${leaversString} left`; - const merged = `${body} ${notifThreadName(threadInfo)}`; - return { - merged, - title: threadInfo.uiName, - body, - }; - } else if (mostRecentType === messageTypes.JOIN_THREAD) { - const joinerArray = {}; - for (let messageInfo of messageInfos) { - invariant( - messageInfo.type === messageTypes.JOIN_THREAD, - 'messageInfo should be messageTypes.JOIN_THREAD!', - ); - joinerArray[messageInfo.creator.id] = messageInfo.creator; - } - const joiners = values(joinerArray); - const joinersString = pluralize(joiners.map(stringForUser)); - - const body = `${joinersString} joined`; - const merged = `${body} ${notifThreadName(threadInfo)}`; - return { - merged, - title: threadInfo.uiName, - body, - }; - } else if ( - mostRecentType === messageTypes.CREATE_ENTRY || - mostRecentType === messageTypes.EDIT_ENTRY - ) { - 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 ` + - `${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 ${notifThreadName(threadInfo)} ` + - `scheduled for ${prettyDate(messageInfo.date)}: "${messageInfo.text}"`; - const merged = `${prefix} ${body}`; - return { - merged, - title: threadInfo.uiName, - body, - prefix, - }; - } else if (mostRecentType === messageTypes.DELETE_ENTRY) { - const messageInfo = assertSingleMessageInfo(messageInfos); - invariant( - messageInfo.type === messageTypes.DELETE_ENTRY, - 'messageInfo should be messageTypes.DELETE_ENTRY!', - ); - const prefix = stringForUser(messageInfo.creator); - const body = - `deleted an event in ${notifThreadName(threadInfo)} ` + - `scheduled for ${prettyDate(messageInfo.date)}: "${messageInfo.text}"`; - const merged = `${prefix} ${body}`; - return { - merged, - title: threadInfo.uiName, - body, - prefix, - }; - } else if (mostRecentType === messageTypes.RESTORE_ENTRY) { - const messageInfo = assertSingleMessageInfo(messageInfos); - invariant( - messageInfo.type === messageTypes.RESTORE_ENTRY, - 'messageInfo should be messageTypes.RESTORE_ENTRY!', - ); - const prefix = stringForUser(messageInfo.creator); - const body = - `restored an event in ${notifThreadName(threadInfo)} ` + - `scheduled for ${prettyDate(messageInfo.date)}: "${messageInfo.text}"`; - const merged = `${prefix} ${body}`; - return { - merged, - title: threadInfo.uiName, - body, - prefix, - }; - } else if (mostRecentType === messageTypes.CHANGE_SETTINGS) { - const mostRecentMessageInfo = messageInfos[0]; - invariant( - mostRecentMessageInfo.type === messageTypes.CHANGE_SETTINGS, - 'messageInfo should be messageTypes.CHANGE_SETTINGS!', - ); - const body = strippedRobotextForMessageInfo( - mostRecentMessageInfo, - threadInfo, - ); - return { - merged: body, - title: threadInfo.uiName, - body, - }; - } else if ( - mostRecentType === messageTypes.IMAGES || - mostRecentType === messageTypes.MULTIMEDIA - ) { - const media = []; - for (let messageInfo of messageInfos) { - invariant( - messageInfo.type === messageTypes.IMAGES || - messageInfo.type === messageTypes.MULTIMEDIA, - 'messageInfo should be multimedia type!', - ); - for (let singleMedia of messageInfo.media) { - media.push(singleMedia); - } - } - const contentString = contentStringForMediaArray(media); - const userString = stringForUser(messageInfos[0].creator); - - let body, merged; - if (!threadInfo.name && !threadIsGroupChat(threadInfo)) { - body = `sent you ${contentString}`; - merged = body; - } else { - body = `sent ${contentString}`; - const threadName = notifThreadName(threadInfo); - merged = `${body} to ${threadName}`; - } - merged = `${userString} ${merged}`; - - return { - merged, - body, - title: threadInfo.uiName, - prefix: userString, - }; - } else if (mostRecentType === messageTypes.UPDATE_RELATIONSHIP) { - const messageInfo = assertSingleMessageInfo(messageInfos); - const prefix = stringForUser(messageInfo.creator); - const title = threadInfo.uiName; - const body = - messageInfo.operation === 'request_sent' - ? 'sent you a friend request' - : 'accepted your friend request'; - const merged = `${prefix} ${body}`; - return { - merged, - body, - title, - prefix, - }; - } else if (mostRecentType === messageTypes.SIDEBAR_SOURCE) { - const messageInfo = assertSingleMessageInfo(messageInfos); - invariant( - messageInfo.type === messageTypes.SIDEBAR_SOURCE, - 'messageInfo should be messageTypes.SIDEBAR_SOURCE!', - ); - const sourceMessageInfo = messageInfo.sourceMessage; - return fullNotifTextsForMessageInfo([sourceMessageInfo], threadInfo); - } else if (mostRecentType === messageTypes.CREATE_SIDEBAR) { - const messageInfo = assertSingleMessageInfo(messageInfos); - invariant( - messageInfo.type === messageTypes.CREATE_SIDEBAR, - 'messageInfo should be messageTypes.CREATE_SIDEBAR!', - ); - const prefix = stringForUser(messageInfo.creator); - const title = threadInfo.uiName; - const sourceMessageAuthorPossessive = messageInfo.sourceMessageAuthor - .isViewer - ? 'your' - : `${stringForUser(messageInfo.sourceMessageAuthor)}'s`; - const body = - `started a sidebar in response to ${sourceMessageAuthorPossessive} ` + - `message "${messageInfo.initialThreadState.name}"`; - const merged = `${prefix} ${body}`; - return { - merged, - body, - title, - prefix, - }; - } else { - invariant(false, `we're not aware of messageType ${mostRecentType}`); - } + const messageSpec = messageSpecs[mostRecentType]; + invariant( + messageSpec.notificationTexts, + `we're not aware of messageType ${mostRecentType}`, + ); + return messageSpec.notificationTexts(messageInfos, threadInfo, { + notifThreadName, + notifTextForSubthreadCreation, + strippedRobotextForMessageInfo, + notificationTexts: fullNotifTextsForMessageInfo, + }); } function strippedRobotextForMessageInfo( messageInfo: RobotextMessageInfo, threadInfo: ThreadInfo, ): string { const robotext = robotextForMessageInfo(messageInfo, threadInfo); const threadEntityRegex = new RegExp(`<[^<>\\|]+\\|t${threadInfo.id}>`); const threadMadeExplicit = robotext.replace( threadEntityRegex, notifThreadName(threadInfo), ); return robotextToRawString(threadMadeExplicit); } const joinResult = (...keys: (string | number)[]) => keys.join('|'); function notifCollapseKeyForRawMessageInfo( rawMessageInfo: RawMessageInfo, ): ?string { if ( rawMessageInfo.type === messageTypes.ADD_MEMBERS || rawMessageInfo.type === messageTypes.REMOVE_MEMBERS ) { return joinResult( rawMessageInfo.type, rawMessageInfo.threadID, rawMessageInfo.creatorID, ); } else if ( rawMessageInfo.type === messageTypes.IMAGES || rawMessageInfo.type === messageTypes.MULTIMEDIA ) { // We use the legacy constant here to collapse both types into one return joinResult( messageTypes.IMAGES, rawMessageInfo.threadID, rawMessageInfo.creatorID, ); } else if (rawMessageInfo.type === messageTypes.CHANGE_SETTINGS) { return joinResult( rawMessageInfo.type, rawMessageInfo.threadID, rawMessageInfo.creatorID, rawMessageInfo.field, ); } else if (rawMessageInfo.type === messageTypes.CHANGE_ROLE) { return joinResult( rawMessageInfo.type, rawMessageInfo.threadID, rawMessageInfo.creatorID, rawMessageInfo.newRole, ); } else if ( rawMessageInfo.type === messageTypes.JOIN_THREAD || rawMessageInfo.type === messageTypes.LEAVE_THREAD ) { return joinResult(rawMessageInfo.type, rawMessageInfo.threadID); } else if ( rawMessageInfo.type === messageTypes.CREATE_ENTRY || rawMessageInfo.type === messageTypes.EDIT_ENTRY ) { return joinResult(rawMessageInfo.creatorID, rawMessageInfo.entryID); } else { return 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 { notifTextsForMessageInfo, notifCollapseKeyForRawMessageInfo, mergePrefixIntoBody, }; diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js new file mode 100644 index 000000000..0eda9e536 --- /dev/null +++ b/lib/types/notif-types.js @@ -0,0 +1,8 @@ +// @flow + +export type NotifTexts = {| + +merged: string, + +body: string, + +title: string, + +prefix?: string, +|};