diff --git a/lib/shared/messages/add-members-message-spec.js b/lib/shared/messages/add-members-message-spec.js index c98cb799b..4608c9be8 100644 --- a/lib/shared/messages/add-members-message-spec.js +++ b/lib/shared/messages/add-members-message-spec.js @@ -1,90 +1,99 @@ // @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'; +import { joinResult } from './utils'; 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, }; }, + + notificationCollapseKey(rawMessageInfo) { + return joinResult( + rawMessageInfo.type, + rawMessageInfo.threadID, + rawMessageInfo.creatorID, + ); + }, }); diff --git a/lib/shared/messages/change-role-message-spec.js b/lib/shared/messages/change-role-message-spec.js index b19b61a6d..77e924896 100644 --- a/lib/shared/messages/change-role-message-spec.js +++ b/lib/shared/messages/change-role-message-spec.js @@ -1,98 +1,108 @@ // @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'; +import { joinResult } from './utils'; 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, }; }, + + notificationCollapseKey(rawMessageInfo) { + return joinResult( + rawMessageInfo.type, + rawMessageInfo.threadID, + rawMessageInfo.creatorID, + rawMessageInfo.newRole, + ); + }, }); diff --git a/lib/shared/messages/change-settings-message-spec.js b/lib/shared/messages/change-settings-message-spec.js index ed49c5dab..afbd61a38 100644 --- a/lib/shared/messages/change-settings-message-spec.js +++ b/lib/shared/messages/change-settings-message-spec.js @@ -1,84 +1,94 @@ // @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'; +import { joinResult } from './utils'; 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, }; }, + + notificationCollapseKey(rawMessageInfo) { + return joinResult( + rawMessageInfo.type, + rawMessageInfo.threadID, + rawMessageInfo.creatorID, + rawMessageInfo.field, + ); + }, }); diff --git a/lib/shared/messages/create-entry-message-spec.js b/lib/shared/messages/create-entry-message-spec.js index 6c6565cd9..614aa1987 100644 --- a/lib/shared/messages/create-entry-message-spec.js +++ b/lib/shared/messages/create-entry-message-spec.js @@ -1,107 +1,112 @@ // @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'; +import { joinResult } from './utils'; 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, }; }, + + notificationCollapseKey(rawMessageInfo) { + return joinResult(rawMessageInfo.creatorID, rawMessageInfo.entryID); + }, }); diff --git a/lib/shared/messages/edit-entry-message-spec.js b/lib/shared/messages/edit-entry-message-spec.js index 122570965..c613171c9 100644 --- a/lib/shared/messages/edit-entry-message-spec.js +++ b/lib/shared/messages/edit-entry-message-spec.js @@ -1,107 +1,112 @@ // @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'; +import { joinResult } from './utils'; 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, }; }, + + notificationCollapseKey(rawMessageInfo) { + return joinResult(rawMessageInfo.creatorID, rawMessageInfo.entryID); + }, }); diff --git a/lib/shared/messages/join-thread-message-spec.js b/lib/shared/messages/join-thread-message-spec.js index e3052b301..7695b7f42 100644 --- a/lib/shared/messages/join-thread-message-spec.js +++ b/lib/shared/messages/join-thread-message-spec.js @@ -1,72 +1,77 @@ // @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'; +import { joinResult } from './utils'; 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, }; }, + + notificationCollapseKey(rawMessageInfo) { + return joinResult(rawMessageInfo.type, rawMessageInfo.threadID); + }, }); diff --git a/lib/shared/messages/leave-thread-message-spec.js b/lib/shared/messages/leave-thread-message-spec.js index 9350fddb2..1646107e0 100644 --- a/lib/shared/messages/leave-thread-message-spec.js +++ b/lib/shared/messages/leave-thread-message-spec.js @@ -1,72 +1,77 @@ // @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'; +import { joinResult } from './utils'; 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, }; }, + + notificationCollapseKey(rawMessageInfo) { + return joinResult(rawMessageInfo.type, rawMessageInfo.threadID); + }, }); diff --git a/lib/shared/messages/message-spec.js b/lib/shared/messages/message-spec.js index 023c58947..bb4986afb 100644 --- a/lib/shared/messages/message-spec.js +++ b/lib/shared/messages/message-spec.js @@ -1,78 +1,79 @@ // @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 { 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, + +notificationCollapseKey?: (rawMessageInfo: RawInfo) => ?string, |}; diff --git a/lib/shared/messages/multimedia-message-spec.js b/lib/shared/messages/multimedia-message-spec.js index 18cbf0a9f..a972d4532 100644 --- a/lib/shared/messages/multimedia-message-spec.js +++ b/lib/shared/messages/multimedia-message-spec.js @@ -1,219 +1,229 @@ // @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'; +import { joinResult } from './utils'; 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, }; }, + + notificationCollapseKey(rawMessageInfo) { + // We use the legacy constant here to collapse both types into one + return joinResult( + messageTypes.IMAGES, + rawMessageInfo.threadID, + rawMessageInfo.creatorID, + ); + }, }); 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 38411ed77..4089de12d 100644 --- a/lib/shared/messages/remove-members-message-spec.js +++ b/lib/shared/messages/remove-members-message-spec.js @@ -1,90 +1,99 @@ // @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'; +import { joinResult } from './utils'; 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, }; }, + + notificationCollapseKey(rawMessageInfo) { + return joinResult( + rawMessageInfo.type, + rawMessageInfo.threadID, + rawMessageInfo.creatorID, + ); + }, }); diff --git a/lib/shared/messages/utils.js b/lib/shared/messages/utils.js index ef417bf87..fc646957e 100644 --- a/lib/shared/messages/utils.js +++ b/lib/shared/messages/utils.js @@ -1,18 +1,21 @@ // @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]; } + +export const joinResult = (...keys: $ReadOnlyArray) => + keys.join('|'); diff --git a/lib/shared/notif-utils.js b/lib/shared/notif-utils.js index 726b3d879..2e1e23f9f 100644 --- a/lib/shared/notif-utils.js +++ b/lib/shared/notif-utils.js @@ -1,186 +1,140 @@ // @flow import invariant from 'invariant'; 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 { robotextForMessageInfo, robotextToRawString } from './message-utils'; import { messageSpecs } from './messages/message-specs'; import { threadNoun } from './thread-utils'; import { stringForUser } from './user-utils'; function notifTextsForMessageInfo( messageInfos: MessageInfo[], threadInfo: ThreadInfo, ): NotifTexts { const fullNotifTexts = fullNotifTextsForMessageInfo(messageInfos, threadInfo); return { merged: trimNotifText(fullNotifTexts.merged, 300), body: trimNotifText(fullNotifTexts.body, 300), title: trimNotifText(fullNotifTexts.title, 100), ...(fullNotifTexts.prefix && { prefix: trimNotifText(fullNotifTexts.prefix, 50), }), }; } 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 mostRecentMessageInfoType( messageInfos: $ReadOnlyArray, ): MessageType { if (messageInfos.length === 0) { throw new Error('expected MessageInfo, but none present!'); } return messageInfos[0].type; } function fullNotifTextsForMessageInfo( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): NotifTexts { const mostRecentType = mostRecentMessageInfoType(messageInfos); 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; - } + 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 { notifTextsForMessageInfo, notifCollapseKeyForRawMessageInfo, mergePrefixIntoBody, };