diff --git a/lib/shared/messages/add-members-message-spec.js b/lib/shared/messages/add-members-message-spec.js index 7b13eec0a..b69b0ecfd 100644 --- a/lib/shared/messages/add-members-message-spec.js +++ b/lib/shared/messages/add-members-message-spec.js @@ -1,148 +1,156 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; +import type { MessageInfo } from '../../types/message-types'; import type { AddMembersMessageData, AddMembersMessageInfo, RawAddMembersMessageInfo, } from '../../types/messages/add-members'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { values } from '../../utils/objects'; import { robotextForMessageInfo, robotextToRawString, removeCreatorAsViewer, } from '../message-utils'; import type { CreateMessageInfoParams, MessageSpec, MessageTitleParam, + NotificationTextsParams, RobotextParams, } from './message-spec'; import { joinResult } from './utils'; export const addMembersMessageSpec: MessageSpec< AddMembersMessageData, RawAddMembersMessageInfo, AddMembersMessageInfo, > = Object.freeze({ messageContent(data: AddMembersMessageData): string { return JSON.stringify(data.addedUserIDs); }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: AddMembersMessageInfo = (messageInfo: AddMembersMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); validMessageInfo = { ...validMessageInfo, addedMembers: validMessageInfo.addedMembers.map((item) => ({ ...item, isViewer: false, })), }; } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, rawMessageInfoFromRow(row: Object): RawAddMembersMessageInfo { 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: RawAddMembersMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): AddMembersMessageInfo { const addedMembers = params.createRelativeUserInfos( rawMessageInfo.addedUserIDs, ); return { type: messageTypes.ADD_MEMBERS, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, addedMembers, }; }, rawMessageInfoFromMessageData( messageData: AddMembersMessageData, id: string, ): RawAddMembersMessageInfo { return { ...messageData, id }; }, robotext( messageInfo: AddMembersMessageInfo, creator: string, params: RobotextParams, ): string { const users = messageInfo.addedMembers; invariant(users.length !== 0, 'added who??'); const addedUsersString = params.robotextForUsers(users); return `${creator} added ${addedUsersString}`; }, - notificationTexts(messageInfos, threadInfo, params) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): NotifTexts { 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, ); }, generatesNotifs: false, userIDs(rawMessageInfo) { return rawMessageInfo.addedUserIDs; }, }); diff --git a/lib/shared/messages/change-role-message-spec.js b/lib/shared/messages/change-role-message-spec.js index b9f313e09..ef0da4c1a 100644 --- a/lib/shared/messages/change-role-message-spec.js +++ b/lib/shared/messages/change-role-message-spec.js @@ -1,153 +1,161 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; +import type { MessageInfo } from '../../types/message-types'; import type { ChangeRoleMessageData, ChangeRoleMessageInfo, RawChangeRoleMessageInfo, } from '../../types/messages/change-role'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { values } from '../../utils/objects'; import { robotextToRawString, robotextForMessageInfo, removeCreatorAsViewer, } from '../message-utils'; import type { CreateMessageInfoParams, MessageSpec, MessageTitleParam, + NotificationTextsParams, RobotextParams, } from './message-spec'; import { joinResult } from './utils'; export const changeRoleMessageSpec: MessageSpec< ChangeRoleMessageData, RawChangeRoleMessageInfo, ChangeRoleMessageInfo, > = Object.freeze({ messageContent(data: ChangeRoleMessageData): string { return JSON.stringify({ userIDs: data.userIDs, newRole: data.newRole, }); }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: ChangeRoleMessageInfo = (messageInfo: ChangeRoleMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); validMessageInfo = { ...validMessageInfo, members: validMessageInfo.members.map((item) => ({ ...item, isViewer: false, })), }; } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, rawMessageInfoFromRow(row: Object): RawChangeRoleMessageInfo { 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: RawChangeRoleMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): ChangeRoleMessageInfo { 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: ChangeRoleMessageData, id: string, ): RawChangeRoleMessageInfo { return { ...messageData, id }; }, robotext( messageInfo: ChangeRoleMessageInfo, creator: string, params: RobotextParams, ): string { 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) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): NotifTexts { 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, ); }, generatesNotifs: true, }); diff --git a/lib/shared/messages/change-settings-message-spec.js b/lib/shared/messages/change-settings-message-spec.js index d21e71333..bc300fc66 100644 --- a/lib/shared/messages/change-settings-message-spec.js +++ b/lib/shared/messages/change-settings-message-spec.js @@ -1,139 +1,147 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; +import type { MessageInfo } from '../../types/message-types'; import type { ChangeSettingsMessageData, ChangeSettingsMessageInfo, RawChangeSettingsMessageInfo, } from '../../types/messages/change-settings'; +import type { NotifTexts } from '../../types/notif-types'; import { assertThreadType } from '../../types/thread-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { robotextToRawString, robotextForMessageInfo, removeCreatorAsViewer, } from '../message-utils'; import { threadLabel } from '../thread-utils'; import type { MessageSpec, MessageTitleParam, + NotificationTextsParams, RobotextParams, } from './message-spec'; import { joinResult } from './utils'; export const changeSettingsMessageSpec: MessageSpec< ChangeSettingsMessageData, RawChangeSettingsMessageInfo, ChangeSettingsMessageInfo, > = Object.freeze({ messageContent(data: ChangeSettingsMessageData): string { return JSON.stringify({ [data.field]: data.value, }); }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: ChangeSettingsMessageInfo = (messageInfo: ChangeSettingsMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, rawMessageInfoFromRow(row: Object): RawChangeSettingsMessageInfo { 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: RawChangeSettingsMessageInfo, creator: RelativeUserInfo, ): ChangeSettingsMessageInfo { return { type: messageTypes.CHANGE_SETTINGS, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, field: rawMessageInfo.field, value: rawMessageInfo.value, }; }, rawMessageInfoFromMessageData( messageData: ChangeSettingsMessageData, id: string, ): RawChangeSettingsMessageInfo { return { ...messageData, id }; }, robotext( messageInfo: ChangeSettingsMessageInfo, creator: string, params: RobotextParams, ): string { let value; if (messageInfo.field === 'color') { value = `<#${messageInfo.value}|c${messageInfo.threadID}>`; } else if (messageInfo.field === 'type') { invariant( typeof messageInfo.value === 'number', 'messageInfo.value should be number for thread type change ', ); const newThreadType = assertThreadType(messageInfo.value); value = threadLabel(newThreadType); } else { value = messageInfo.value; } return ( `${creator} updated ` + `${params.encodedThreadEntity(messageInfo.threadID, 'the thread')}'s ` + `${messageInfo.field} to "${value}"` ); }, - notificationTexts(messageInfos, threadInfo, params) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): NotifTexts { 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, ); }, generatesNotifs: true, }); diff --git a/lib/shared/messages/create-entry-message-spec.js b/lib/shared/messages/create-entry-message-spec.js index ea3be47ef..e0529e435 100644 --- a/lib/shared/messages/create-entry-message-spec.js +++ b/lib/shared/messages/create-entry-message-spec.js @@ -1,140 +1,151 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; +import type { MessageInfo } from '../../types/message-types'; import type { CreateEntryMessageData, CreateEntryMessageInfo, RawCreateEntryMessageInfo, } from '../../types/messages/create-entry'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { prettyDate } from '../../utils/date-utils'; import { robotextToRawString, robotextForMessageInfo, removeCreatorAsViewer, } from '../message-utils'; import { stringForUser } from '../user-utils'; -import type { MessageSpec, MessageTitleParam } from './message-spec'; +import type { + MessageSpec, + MessageTitleParam, + NotificationTextsParams, +} from './message-spec'; import { joinResult } from './utils'; export const createEntryMessageSpec: MessageSpec< CreateEntryMessageData, RawCreateEntryMessageInfo, CreateEntryMessageInfo, > = Object.freeze({ messageContent(data: CreateEntryMessageData): string { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: CreateEntryMessageInfo = (messageInfo: CreateEntryMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, rawMessageInfoFromRow(row: Object): RawCreateEntryMessageInfo { const content = JSON.parse(row.content); return { type: messageTypes.CREATE_ENTRY, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), entryID: content.entryID, date: content.date, text: content.text, }; }, createMessageInfo( rawMessageInfo: RawCreateEntryMessageInfo, creator: RelativeUserInfo, ): CreateEntryMessageInfo { return { type: messageTypes.CREATE_ENTRY, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, entryID: rawMessageInfo.entryID, date: rawMessageInfo.date, text: rawMessageInfo.text, }; }, rawMessageInfoFromMessageData( messageData: CreateEntryMessageData, id: string, ): RawCreateEntryMessageInfo { return { ...messageData, id }; }, robotext(messageInfo: CreateEntryMessageInfo, creator: string): string { const date = prettyDate(messageInfo.date); return ( `${creator} created an event scheduled for ${date}: ` + `"${messageInfo.text}"` ); }, - notificationTexts(messageInfos, threadInfo, params) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): NotifTexts { 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); }, generatesNotifs: true, }); diff --git a/lib/shared/messages/create-sidebar-message-spec.js b/lib/shared/messages/create-sidebar-message-spec.js index 737b1b802..a910203a6 100644 --- a/lib/shared/messages/create-sidebar-message-spec.js +++ b/lib/shared/messages/create-sidebar-message-spec.js @@ -1,209 +1,215 @@ // @flow import invariant from 'invariant'; import type { PlatformDetails } from '../../types/device-types'; import { messageTypes } from '../../types/message-types'; +import type { MessageInfo } from '../../types/message-types'; import type { CreateSidebarMessageData, CreateSidebarMessageInfo, RawCreateSidebarMessageInfo, } from '../../types/messages/create-sidebar'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { robotextToRawString, robotextForMessageInfo, removeCreatorAsViewer, } from '../message-utils'; import { stringForUser } from '../user-utils'; import { hasMinCodeVersion } from '../version-utils'; import type { CreateMessageInfoParams, MessageSpec, MessageTitleParam, RobotextParams, } from './message-spec'; import { assertSingleMessageInfo } from './utils'; export const createSidebarMessageSpec: MessageSpec< CreateSidebarMessageData, RawCreateSidebarMessageInfo, CreateSidebarMessageInfo, > = Object.freeze({ messageContent(data: CreateSidebarMessageData): string { return JSON.stringify({ ...data.initialThreadState, sourceMessageAuthorID: data.sourceMessageAuthorID, }); }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: CreateSidebarMessageInfo = (messageInfo: CreateSidebarMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); validMessageInfo = { ...validMessageInfo, sourceMessageAuthor: { ...validMessageInfo.sourceMessageAuthor, isViewer: false, }, initialThreadState: { ...validMessageInfo.initialThreadState, otherMembers: validMessageInfo.initialThreadState.otherMembers.map( (item) => ({ ...item, isViewer: false, }), ), }, }; } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, rawMessageInfoFromRow(row: Object): RawCreateSidebarMessageInfo { 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: RawCreateSidebarMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): ?CreateSidebarMessageInfo { 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: CreateSidebarMessageData, id: string, ): RawCreateSidebarMessageInfo { return { ...messageData, id }; }, robotext( messageInfo: CreateSidebarMessageInfo, creator: string, params: RobotextParams, ): string { 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: RawCreateSidebarMessageInfo, platformDetails: ?PlatformDetails, ): RawCreateSidebarMessageInfo | RawUnsupportedMessageInfo { // 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, }; }, unshimMessageInfo( unwrapped: RawCreateSidebarMessageInfo, ): RawCreateSidebarMessageInfo { return unwrapped; }, - notificationTexts(messageInfos, threadInfo) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + ): NotifTexts { 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, }; }, generatesNotifs: true, userIDs(rawMessageInfo) { return rawMessageInfo.initialThreadState.memberIDs; }, threadIDs(rawMessageInfo) { const { parentThreadID } = rawMessageInfo.initialThreadState; return [parentThreadID]; }, }); diff --git a/lib/shared/messages/create-sub-thread-message-spec.js b/lib/shared/messages/create-sub-thread-message-spec.js index 7c3ef5cc1..5e68ca40e 100644 --- a/lib/shared/messages/create-sub-thread-message-spec.js +++ b/lib/shared/messages/create-sub-thread-message-spec.js @@ -1,130 +1,138 @@ // @flow import invariant from 'invariant'; import { permissionLookup } from '../../permissions/thread-permissions'; import { messageTypes } from '../../types/message-types'; +import type { MessageInfo } from '../../types/message-types'; import type { CreateSubthreadMessageData, CreateSubthreadMessageInfo, RawCreateSubthreadMessageInfo, } from '../../types/messages/create-subthread'; +import type { NotifTexts } from '../../types/notif-types'; import { threadPermissions, threadTypes } from '../../types/thread-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { robotextToRawString, robotextForMessageInfo, removeCreatorAsViewer, } from '../message-utils'; import type { CreateMessageInfoParams, MessageSpec, MessageTitleParam, + NotificationTextsParams, } from './message-spec'; import { assertSingleMessageInfo } from './utils'; export const createSubThreadMessageSpec: MessageSpec< CreateSubthreadMessageData, RawCreateSubthreadMessageInfo, CreateSubthreadMessageInfo, > = Object.freeze({ messageContent(data: CreateSubthreadMessageData): string { return data.childThreadID; }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: CreateSubthreadMessageInfo = (messageInfo: CreateSubthreadMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, rawMessageInfoFromRow(row: Object): ?RawCreateSubthreadMessageInfo { 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: RawCreateSubthreadMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): ?CreateSubthreadMessageInfo { 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: CreateSubthreadMessageData, id: string, ): RawCreateSubthreadMessageInfo { return { ...messageData, id }; }, robotext(messageInfo: CreateSubthreadMessageInfo, creator: string): string { 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) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): NotifTexts { 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, ); }, generatesNotifs: true, threadIDs(rawMessageInfo) { return [rawMessageInfo.childThreadID]; }, }); diff --git a/lib/shared/messages/create-thread-message-spec.js b/lib/shared/messages/create-thread-message-spec.js index 5c4bf249a..db9a20c88 100644 --- a/lib/shared/messages/create-thread-message-spec.js +++ b/lib/shared/messages/create-thread-message-spec.js @@ -1,180 +1,188 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; +import type { MessageInfo } from '../../types/message-types'; import type { CreateThreadMessageData, CreateThreadMessageInfo, RawCreateThreadMessageInfo, } from '../../types/messages/create-thread'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { robotextToRawString, robotextForMessageInfo, removeCreatorAsViewer, } from '../message-utils'; import { stringForUser } from '../user-utils'; import type { CreateMessageInfoParams, MessageSpec, MessageTitleParam, + NotificationTextsParams, RobotextParams, } from './message-spec'; import { assertSingleMessageInfo } from './utils'; export const createThreadMessageSpec: MessageSpec< CreateThreadMessageData, RawCreateThreadMessageInfo, CreateThreadMessageInfo, > = Object.freeze({ messageContent(data: CreateThreadMessageData): string { return JSON.stringify(data.initialThreadState); }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: CreateThreadMessageInfo = (messageInfo: CreateThreadMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); validMessageInfo = { ...validMessageInfo, initialThreadState: { ...validMessageInfo.initialThreadState, otherMembers: validMessageInfo.initialThreadState.otherMembers.map( (item) => ({ ...item, isViewer: false, }), ), }, }; } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, rawMessageInfoFromRow(row: Object): RawCreateThreadMessageInfo { 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: RawCreateThreadMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): CreateThreadMessageInfo { 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: CreateThreadMessageData, id: string, ): RawCreateThreadMessageInfo { return { ...messageData, id }; }, robotext( messageInfo: CreateThreadMessageInfo, creator: string, params: RobotextParams, ): string { 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) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): NotifTexts { 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, }; }, generatesNotifs: true, userIDs(rawMessageInfo) { return rawMessageInfo.initialThreadState.memberIDs; }, startsThread: true, threadIDs(rawMessageInfo) { const { parentThreadID } = rawMessageInfo.initialThreadState; return parentThreadID ? [parentThreadID] : []; }, }); diff --git a/lib/shared/messages/delete-entry-message-spec.js b/lib/shared/messages/delete-entry-message-spec.js index 1e27e4aad..f11b3d779 100644 --- a/lib/shared/messages/delete-entry-message-spec.js +++ b/lib/shared/messages/delete-entry-message-spec.js @@ -1,114 +1,125 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; +import type { MessageInfo } from '../../types/message-types'; import type { DeleteEntryMessageData, DeleteEntryMessageInfo, RawDeleteEntryMessageInfo, } from '../../types/messages/delete-entry'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { prettyDate } from '../../utils/date-utils'; import { robotextToRawString, robotextForMessageInfo, removeCreatorAsViewer, } from '../message-utils'; import { stringForUser } from '../user-utils'; -import type { MessageSpec, MessageTitleParam } from './message-spec'; +import type { + MessageSpec, + MessageTitleParam, + NotificationTextsParams, +} from './message-spec'; import { assertSingleMessageInfo } from './utils'; export const deleteEntryMessageSpec: MessageSpec< DeleteEntryMessageData, RawDeleteEntryMessageInfo, DeleteEntryMessageInfo, > = Object.freeze({ messageContent(data: DeleteEntryMessageData): string { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: DeleteEntryMessageInfo = (messageInfo: DeleteEntryMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, rawMessageInfoFromRow(row: Object): RawDeleteEntryMessageInfo { 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: RawDeleteEntryMessageInfo, creator: RelativeUserInfo, ): DeleteEntryMessageInfo { 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: DeleteEntryMessageData, id: string, ): RawDeleteEntryMessageInfo { return { ...messageData, id }; }, robotext(messageInfo: DeleteEntryMessageInfo, creator: string): string { const date = prettyDate(messageInfo.date); return ( `${creator} deleted an event scheduled for ${date}: ` + `"${messageInfo.text}"` ); }, - notificationTexts(messageInfos, threadInfo, params) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): NotifTexts { 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, }; }, generatesNotifs: true, }); diff --git a/lib/shared/messages/edit-entry-message-spec.js b/lib/shared/messages/edit-entry-message-spec.js index fdf6aca3e..565040478 100644 --- a/lib/shared/messages/edit-entry-message-spec.js +++ b/lib/shared/messages/edit-entry-message-spec.js @@ -1,140 +1,151 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; +import type { MessageInfo } from '../../types/message-types'; import type { EditEntryMessageData, EditEntryMessageInfo, RawEditEntryMessageInfo, } from '../../types/messages/edit-entry'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { prettyDate } from '../../utils/date-utils'; import { robotextToRawString, robotextForMessageInfo, removeCreatorAsViewer, } from '../message-utils'; import { stringForUser } from '../user-utils'; -import type { MessageSpec, MessageTitleParam } from './message-spec'; +import type { + MessageSpec, + MessageTitleParam, + NotificationTextsParams, +} from './message-spec'; import { joinResult } from './utils'; export const editEntryMessageSpec: MessageSpec< EditEntryMessageData, RawEditEntryMessageInfo, EditEntryMessageInfo, > = Object.freeze({ messageContent(data: EditEntryMessageData): string { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: EditEntryMessageInfo = (messageInfo: EditEntryMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, rawMessageInfoFromRow(row: Object): RawEditEntryMessageInfo { const content = JSON.parse(row.content); return { type: messageTypes.EDIT_ENTRY, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), entryID: content.entryID, date: content.date, text: content.text, }; }, createMessageInfo( rawMessageInfo: RawEditEntryMessageInfo, creator: RelativeUserInfo, ): EditEntryMessageInfo { return { type: messageTypes.EDIT_ENTRY, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, entryID: rawMessageInfo.entryID, date: rawMessageInfo.date, text: rawMessageInfo.text, }; }, rawMessageInfoFromMessageData( messageData: EditEntryMessageData, id: string, ): RawEditEntryMessageInfo { return { ...messageData, id }; }, robotext(messageInfo: EditEntryMessageInfo, creator: string): string { const date = prettyDate(messageInfo.date); return ( `${creator} updated the text of an event scheduled for ` + `${date}: "${messageInfo.text}"` ); }, - notificationTexts(messageInfos, threadInfo, params) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): NotifTexts { 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); }, generatesNotifs: true, }); diff --git a/lib/shared/messages/join-thread-message-spec.js b/lib/shared/messages/join-thread-message-spec.js index 8886da8fa..b2d5ef1d0 100644 --- a/lib/shared/messages/join-thread-message-spec.js +++ b/lib/shared/messages/join-thread-message-spec.js @@ -1,113 +1,121 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; +import type { MessageInfo } from '../../types/message-types'; import type { JoinThreadMessageData, JoinThreadMessageInfo, RawJoinThreadMessageInfo, } from '../../types/messages/join-thread'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { values } from '../../utils/objects'; import { pluralize } from '../../utils/text-utils'; import { robotextToRawString, robotextForMessageInfo, removeCreatorAsViewer, } from '../message-utils'; import { stringForUser } from '../user-utils'; import type { MessageSpec, MessageTitleParam, + NotificationTextsParams, RobotextParams, } from './message-spec'; import { joinResult } from './utils'; export const joinThreadMessageSpec: MessageSpec< JoinThreadMessageData, RawJoinThreadMessageInfo, JoinThreadMessageInfo, > = Object.freeze({ rawMessageInfoFromRow(row: Object): RawJoinThreadMessageInfo { return { type: messageTypes.JOIN_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), }; }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: JoinThreadMessageInfo = (messageInfo: JoinThreadMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, createMessageInfo( rawMessageInfo: RawJoinThreadMessageInfo, creator: RelativeUserInfo, ): JoinThreadMessageInfo { return { type: messageTypes.JOIN_THREAD, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, }; }, rawMessageInfoFromMessageData( messageData: JoinThreadMessageData, id: string, ): RawJoinThreadMessageInfo { return { ...messageData, id }; }, robotext( messageInfo: JoinThreadMessageInfo, creator: string, params: RobotextParams, ): string { return ( `${creator} joined ` + params.encodedThreadEntity(messageInfo.threadID, 'this thread') ); }, - notificationTexts(messageInfos, threadInfo, params) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): NotifTexts { 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); }, generatesNotifs: false, }); diff --git a/lib/shared/messages/leave-thread-message-spec.js b/lib/shared/messages/leave-thread-message-spec.js index 5adbe2b7b..0d2223305 100644 --- a/lib/shared/messages/leave-thread-message-spec.js +++ b/lib/shared/messages/leave-thread-message-spec.js @@ -1,113 +1,121 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; +import type { MessageInfo } from '../../types/message-types'; import type { LeaveThreadMessageData, LeaveThreadMessageInfo, RawLeaveThreadMessageInfo, } from '../../types/messages/leave-thread'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { values } from '../../utils/objects'; import { pluralize } from '../../utils/text-utils'; import { robotextToRawString, robotextForMessageInfo, removeCreatorAsViewer, } from '../message-utils'; import { stringForUser } from '../user-utils'; import type { MessageSpec, MessageTitleParam, + NotificationTextsParams, RobotextParams, } from './message-spec'; import { joinResult } from './utils'; export const leaveThreadMessageSpec: MessageSpec< LeaveThreadMessageData, RawLeaveThreadMessageInfo, LeaveThreadMessageInfo, > = Object.freeze({ rawMessageInfoFromRow(row: Object): RawLeaveThreadMessageInfo { return { type: messageTypes.LEAVE_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), }; }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: LeaveThreadMessageInfo = (messageInfo: LeaveThreadMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, createMessageInfo( rawMessageInfo: RawLeaveThreadMessageInfo, creator: RelativeUserInfo, ): LeaveThreadMessageInfo { return { type: messageTypes.LEAVE_THREAD, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, }; }, rawMessageInfoFromMessageData( messageData: LeaveThreadMessageData, id: string, ): RawLeaveThreadMessageInfo { return { ...messageData, id }; }, robotext( messageInfo: LeaveThreadMessageInfo, creator: string, params: RobotextParams, ): string { return ( `${creator} left ` + params.encodedThreadEntity(messageInfo.threadID, 'this thread') ); }, - notificationTexts(messageInfos, threadInfo, params) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): NotifTexts { 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); }, generatesNotifs: false, }); diff --git a/lib/shared/messages/message-spec.js b/lib/shared/messages/message-spec.js index a0ea7e6b8..d5c499f4e 100644 --- a/lib/shared/messages/message-spec.js +++ b/lib/shared/messages/message-spec.js @@ -1,105 +1,107 @@ // @flow import { type ParserRules } from 'simple-markdown'; 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/messages/unsupported'; import type { NotifTexts } from '../../types/notif-types'; import type { ThreadInfo, ThreadType } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import type { GetMessageTitleViewerContext } from '../message-utils'; export type MessageTitleParam = {| +messageInfo: Info, +threadInfo: ThreadInfo, +markdownRules: ParserRules, +viewerContext?: GetMessageTitleViewerContext, |}; export type RawMessageInfoFromRowParams = {| +localID: string, +media?: $ReadOnlyArray, +derivedMessages: $ReadOnlyMap< string, RawComposableMessageInfo | RawRobotextMessageInfo, >, |}; export type CreateMessageInfoParams = {| +threadInfos: {| [id: string]: ThreadInfo |}, +createMessageInfoFromRaw: (rawInfo: RawMessageInfo) => MessageInfo, +createRelativeUserInfos: ( userIDs: $ReadOnlyArray, ) => RelativeUserInfo[], |}; export type RobotextParams = {| +encodedThreadEntity: (threadID: string, text: string) => string, +robotextForUsers: (users: RelativeUserInfo[]) => string, +robotextForUser: (user: RelativeUserInfo) => string, +threadInfo: ThreadInfo, |}; +export type NotificationTextsParams = {| + +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, +|}; + export type MessageSpec = {| +messageContent?: (data: Data) => string, +messageTitle: (param: MessageTitleParam) => string, +rawMessageInfoFromRow?: ( row: Object, params: RawMessageInfoFromRowParams, ) => ?RawInfo, +createMessageInfo: ( rawMessageInfo: RawInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ) => ?Info, +rawMessageInfoFromMessageData?: (messageData: Data, id: string) => RawInfo, +robotext?: ( messageInfo: Info, creator: string, params: RobotextParams, ) => string, +shimUnsupportedMessageInfo?: ( rawMessageInfo: RawInfo, platformDetails: ?PlatformDetails, ) => RawInfo | RawUnsupportedMessageInfo, +unshimMessageInfo?: ( unwrapped: RawInfo, messageInfo: RawMessageInfo, ) => ?RawMessageInfo, +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, - |}, + params: NotificationTextsParams, ) => NotifTexts, +notificationCollapseKey?: (rawMessageInfo: RawInfo) => ?string, +generatesNotifs: boolean, +userIDs?: (rawMessageInfo: RawInfo) => $ReadOnlyArray, +startsThread?: boolean, +threadIDs?: (rawMessageInfo: RawInfo) => $ReadOnlyArray, +includedInRepliesCount?: boolean, |}; diff --git a/lib/shared/messages/multimedia-message-spec.js b/lib/shared/messages/multimedia-message-spec.js index c0fb00f31..25e158871 100644 --- a/lib/shared/messages/multimedia-message-spec.js +++ b/lib/shared/messages/multimedia-message-spec.js @@ -1,311 +1,319 @@ // @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 { + MessageInfo, RawMessageInfo, RawMultimediaMessageInfo, } from '../../types/message-types'; import { messageTypes, type MultimediaMessageInfo, } from '../../types/message-types'; import type { ImagesMessageData, RawImagesMessageInfo, ImagesMessageInfo, } from '../../types/messages/images'; import type { MediaMessageData, MediaMessageInfo, RawMediaMessageInfo, } from '../../types/messages/media'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { createMediaMessageInfo, messagePreviewText, removeCreatorAsViewer, } from '../message-utils'; import { threadIsGroupChat } from '../thread-utils'; import { stringForUser } from '../user-utils'; import { hasMinCodeVersion } from '../version-utils'; import type { MessageSpec, MessageTitleParam, + NotificationTextsParams, RawMessageInfoFromRowParams, } from './message-spec'; import { joinResult } from './utils'; export const multimediaMessageSpec: MessageSpec< MediaMessageData | ImagesMessageData, RawMediaMessageInfo | RawImagesMessageInfo, MediaMessageInfo | ImagesMessageInfo, > = Object.freeze({ messageContent(data: MediaMessageData | ImagesMessageData): string { const mediaIDs = data.media.map((media) => parseInt(media.id, 10)); return JSON.stringify(mediaIDs); }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: MultimediaMessageInfo = (messageInfo: MultimediaMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); } return messagePreviewText(validMessageInfo, threadInfo); }, rawMessageInfoFromRow( row: Object, params: RawMessageInfoFromRowParams, ): RawMediaMessageInfo | RawImagesMessageInfo { 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: RawMediaMessageInfo | RawImagesMessageInfo, creator: RelativeUserInfo, ): ?(MediaMessageInfo | ImagesMessageInfo) { 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: MediaMessageData | ImagesMessageData, id: string, ): RawMediaMessageInfo | RawImagesMessageInfo { if (messageData.type === messageTypes.IMAGES) { return ({ ...messageData, id }: RawImagesMessageInfo); } else { return ({ ...messageData, id }: RawMediaMessageInfo); } }, shimUnsupportedMessageInfo( rawMessageInfo: RawMediaMessageInfo | RawImagesMessageInfo, platformDetails: ?PlatformDetails, ): RawMediaMessageInfo | RawImagesMessageInfo | RawUnsupportedMessageInfo { 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, }; } }, unshimMessageInfo( unwrapped: RawMediaMessageInfo | RawImagesMessageInfo, messageInfo: RawMessageInfo, ): ?RawMessageInfo { if (unwrapped.type === messageTypes.IMAGES) { return { ...unwrapped, media: unwrapped.media.map((media) => { if (media.dimensions) { return media; } const dimensions = preDimensionUploads[media.id]; invariant( dimensions, 'only four photos were uploaded before dimensions were calculated, ' + `and ${media.id} was not one of them`, ); return { ...media, dimensions }; }), }; } else if (unwrapped.type === messageTypes.MULTIMEDIA) { for (const { type } of unwrapped.media) { if (type !== 'photo' && type !== 'video') { return messageInfo; } } } return undefined; }, - notificationTexts(messageInfos, threadInfo, params) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): NotifTexts { 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, ); }, generatesNotifs: true, includedInRepliesCount: true, }); 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); } } // Four photos were uploaded before dimensions were calculated server-side, // and delivered to clients without dimensions in the MultimediaMessageInfo. const preDimensionUploads = { '156642': { width: 1440, height: 1080 }, '156649': { width: 720, height: 803 }, '156794': { width: 720, height: 803 }, '156877': { width: 574, height: 454 }, }; diff --git a/lib/shared/messages/remove-members-message-spec.js b/lib/shared/messages/remove-members-message-spec.js index e31ece194..1f215d7df 100644 --- a/lib/shared/messages/remove-members-message-spec.js +++ b/lib/shared/messages/remove-members-message-spec.js @@ -1,148 +1,156 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; +import type { MessageInfo } from '../../types/message-types'; import type { RawRemoveMembersMessageInfo, RemoveMembersMessageData, RemoveMembersMessageInfo, } from '../../types/messages/remove-members'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { values } from '../../utils/objects'; import { robotextForMessageInfo, robotextToRawString, removeCreatorAsViewer, } from '../message-utils'; import type { CreateMessageInfoParams, MessageSpec, MessageTitleParam, + NotificationTextsParams, RobotextParams, } from './message-spec'; import { joinResult } from './utils'; export const removeMembersMessageSpec: MessageSpec< RemoveMembersMessageData, RawRemoveMembersMessageInfo, RemoveMembersMessageInfo, > = Object.freeze({ messageContent(data: RemoveMembersMessageData): string { return JSON.stringify(data.removedUserIDs); }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: RemoveMembersMessageInfo = (messageInfo: RemoveMembersMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); validMessageInfo = { ...validMessageInfo, removedMembers: validMessageInfo.removedMembers.map((item) => ({ ...item, isViewer: false, })), }; } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, rawMessageInfoFromRow(row: Object): RawRemoveMembersMessageInfo { 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: RawRemoveMembersMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): RemoveMembersMessageInfo { const removedMembers = params.createRelativeUserInfos( rawMessageInfo.removedUserIDs, ); return { type: messageTypes.REMOVE_MEMBERS, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, removedMembers, }; }, rawMessageInfoFromMessageData( messageData: RemoveMembersMessageData, id: string, ): RawRemoveMembersMessageInfo { return { ...messageData, id }; }, robotext( messageInfo: RemoveMembersMessageInfo, creator: string, params: RobotextParams, ): string { const users = messageInfo.removedMembers; invariant(users.length !== 0, 'removed who??'); const removedUsersString = params.robotextForUsers(users); return `${creator} removed ${removedUsersString}`; }, - notificationTexts(messageInfos, threadInfo, params) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): NotifTexts { 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, ); }, generatesNotifs: false, userIDs(rawMessageInfo) { return rawMessageInfo.removedUserIDs; }, }); diff --git a/lib/shared/messages/restore-entry-message-spec.js b/lib/shared/messages/restore-entry-message-spec.js index 8d3c54f57..bc70863af 100644 --- a/lib/shared/messages/restore-entry-message-spec.js +++ b/lib/shared/messages/restore-entry-message-spec.js @@ -1,114 +1,125 @@ // @flow import invariant from 'invariant'; import { messageTypes } from '../../types/message-types'; +import type { MessageInfo } from '../../types/message-types'; import type { RawRestoreEntryMessageInfo, RestoreEntryMessageData, RestoreEntryMessageInfo, } from '../../types/messages/restore-entry'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { prettyDate } from '../../utils/date-utils'; import { robotextToRawString, robotextForMessageInfo, removeCreatorAsViewer, } from '../message-utils'; import { stringForUser } from '../user-utils'; -import type { MessageSpec, MessageTitleParam } from './message-spec'; +import type { + MessageSpec, + MessageTitleParam, + NotificationTextsParams, +} from './message-spec'; import { assertSingleMessageInfo } from './utils'; export const restoreEntryMessageSpec: MessageSpec< RestoreEntryMessageData, RawRestoreEntryMessageInfo, RestoreEntryMessageInfo, > = Object.freeze({ messageContent(data: RestoreEntryMessageData): string { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: RestoreEntryMessageInfo = (messageInfo: RestoreEntryMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, rawMessageInfoFromRow(row: Object): RawRestoreEntryMessageInfo { 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: RawRestoreEntryMessageInfo, creator: RelativeUserInfo, ): RestoreEntryMessageInfo { 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: RestoreEntryMessageData, id: string, ): RawRestoreEntryMessageInfo { return { ...messageData, id }; }, robotext(messageInfo: RestoreEntryMessageInfo, creator: string): string { const date = prettyDate(messageInfo.date); return ( `${creator} restored an event scheduled for ${date}: ` + `"${messageInfo.text}"` ); }, - notificationTexts(messageInfos, threadInfo, params) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): NotifTexts { 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, }; }, generatesNotifs: true, }); diff --git a/lib/shared/messages/sidebar-source-message-spec.js b/lib/shared/messages/sidebar-source-message-spec.js index 1224ed80b..714f92881 100644 --- a/lib/shared/messages/sidebar-source-message-spec.js +++ b/lib/shared/messages/sidebar-source-message-spec.js @@ -1,141 +1,149 @@ // @flow import invariant from 'invariant'; import type { PlatformDetails } from '../../types/device-types'; import type { + MessageInfo, RawSidebarSourceMessageInfo, SidebarSourceMessageData, SidebarSourceMessageInfo, } from '../../types/message-types'; import { messageTypes } from '../../types/message-types'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { hasMinCodeVersion } from '../version-utils'; import type { CreateMessageInfoParams, MessageSpec, + NotificationTextsParams, RawMessageInfoFromRowParams, } from './message-spec'; import { assertSingleMessageInfo } from './utils'; export const sidebarSourceMessageSpec: MessageSpec< SidebarSourceMessageData, RawSidebarSourceMessageInfo, SidebarSourceMessageInfo, > = Object.freeze({ messageContent(data: SidebarSourceMessageData): string { const sourceMessageID = data.sourceMessage?.id; invariant(sourceMessageID, 'Source message id should be set'); return JSON.stringify({ sourceMessageID, }); }, messageTitle() { invariant(false, 'Cannot call messageTitle on sidebarSourceMessageSpec'); }, rawMessageInfoFromRow( row: Object, params: RawMessageInfoFromRowParams, ): RawSidebarSourceMessageInfo { 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: RawSidebarSourceMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): ?SidebarSourceMessageInfo { 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: SidebarSourceMessageData, id: string, ): RawSidebarSourceMessageInfo { return { ...messageData, id }; }, shimUnsupportedMessageInfo( rawMessageInfo: RawSidebarSourceMessageInfo, platformDetails: ?PlatformDetails, ): RawSidebarSourceMessageInfo | RawUnsupportedMessageInfo { // 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', dontPrefixCreator: true, unsupportedMessageInfo: rawMessageInfo, }; }, unshimMessageInfo( unwrapped: RawSidebarSourceMessageInfo, ): RawSidebarSourceMessageInfo { return unwrapped; }, - notificationTexts(messageInfos, threadInfo, params) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): NotifTexts { 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); }, generatesNotifs: false, startsThread: true, }); diff --git a/lib/shared/messages/text-message-spec.js b/lib/shared/messages/text-message-spec.js index 52df55f47..508f6f197 100644 --- a/lib/shared/messages/text-message-spec.js +++ b/lib/shared/messages/text-message-spec.js @@ -1,144 +1,155 @@ // @flow import invariant from 'invariant'; import * as SimpleMarkdown from 'simple-markdown'; import { messageTypes } from '../../types/message-types'; +import type { MessageInfo } from '../../types/message-types'; import type { RawTextMessageInfo, TextMessageData, TextMessageInfo, } from '../../types/messages/text'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { threadIsGroupChat } from '../thread-utils'; import { stringForUser } from '../user-utils'; -import type { MessageSpec, RawMessageInfoFromRowParams } from './message-spec'; +import type { + MessageSpec, + NotificationTextsParams, + RawMessageInfoFromRowParams, +} from './message-spec'; import { assertSingleMessageInfo } from './utils'; /** * most of the markdown leaves contain `content` field (it is an array or a string) * apart from lists, which have `items` field (that holds an array) */ const rawTextFromMarkdownAST = (node: SimpleMarkdown.ASTNode): string => { if (node.content && typeof node.content === 'string') { return node.content; } else if (node.items) { return rawTextFromMarkdownAST(node.items); } else if (node.content) { return rawTextFromMarkdownAST(node.content); } else if (Array.isArray(node)) { return node.map(rawTextFromMarkdownAST).join(''); } return ''; }; const getFirstNonQuotedRawLine = ( nodes: $ReadOnlyArray, ): string => { let result = 'message'; for (const node of nodes) { if (node.type === 'blockQuote') { result = 'quoted message'; } else { const rawText = rawTextFromMarkdownAST(node); if (!rawText || !rawText.replace(/\s/g, '')) { // handles the case of an empty(or containing only white spaces) // new line that usually occurs between a quote and the rest // of the message(we don't want it as a title, thus continue) continue; } return rawText; } } return result; }; export const textMessageSpec: MessageSpec< TextMessageData, RawTextMessageInfo, TextMessageInfo, > = Object.freeze({ messageContent(data: TextMessageData): string { return data.text; }, messageTitle({ messageInfo, markdownRules }) { const { text } = messageInfo; const parser = SimpleMarkdown.parserFor(markdownRules); const ast = parser(text, { disableAutoBlockNewlines: true }); return getFirstNonQuotedRawLine(ast); }, rawMessageInfoFromRow( row: Object, params: RawMessageInfoFromRowParams, ): RawTextMessageInfo { 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: RawTextMessageInfo, creator: RelativeUserInfo, ): TextMessageInfo { 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: TextMessageData, id: string, ): RawTextMessageInfo { return { ...messageData, id }; }, - notificationTexts(messageInfos, threadInfo, params) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): NotifTexts { 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}:`, }; } }, generatesNotifs: true, includedInRepliesCount: true, }); diff --git a/lib/shared/messages/update-relationship-message-spec.js b/lib/shared/messages/update-relationship-message-spec.js index ca3f8be02..6e8047ede 100644 --- a/lib/shared/messages/update-relationship-message-spec.js +++ b/lib/shared/messages/update-relationship-message-spec.js @@ -1,164 +1,170 @@ // @flow import invariant from 'invariant'; import type { PlatformDetails } from '../../types/device-types'; import { messageTypes } from '../../types/message-types'; +import type { MessageInfo } from '../../types/message-types'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported'; import type { RawUpdateRelationshipMessageInfo, UpdateRelationshipMessageData, UpdateRelationshipMessageInfo, } from '../../types/messages/update-relationship'; +import type { NotifTexts } from '../../types/notif-types'; +import type { ThreadInfo } from '../../types/thread-types'; import type { RelativeUserInfo } from '../../types/user-types'; import { robotextToRawString, robotextForMessageInfo, removeCreatorAsViewer, } from '../message-utils'; import { stringForUser } from '../user-utils'; import { hasMinCodeVersion } from '../version-utils'; import type { CreateMessageInfoParams, MessageSpec, MessageTitleParam, RobotextParams, } from './message-spec'; import { assertSingleMessageInfo } from './utils'; export const updateRelationshipMessageSpec: MessageSpec< UpdateRelationshipMessageData, RawUpdateRelationshipMessageInfo, UpdateRelationshipMessageInfo, > = Object.freeze({ messageContent(data: UpdateRelationshipMessageData): string { return JSON.stringify({ operation: data.operation, targetID: data.targetID, }); }, messageTitle({ messageInfo, threadInfo, viewerContext, }: MessageTitleParam) { let validMessageInfo: UpdateRelationshipMessageInfo = (messageInfo: UpdateRelationshipMessageInfo); if (viewerContext === 'global_viewer') { validMessageInfo = removeCreatorAsViewer(validMessageInfo); validMessageInfo = { ...validMessageInfo, target: { ...validMessageInfo.target, isViewer: false }, }; } return robotextToRawString( robotextForMessageInfo(validMessageInfo, threadInfo), ); }, rawMessageInfoFromRow(row: Object): RawUpdateRelationshipMessageInfo { 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: RawUpdateRelationshipMessageInfo, creator: RelativeUserInfo, params: CreateMessageInfoParams, ): ?UpdateRelationshipMessageInfo { 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: UpdateRelationshipMessageData, id: string, ): RawUpdateRelationshipMessageInfo { return { ...messageData, id }; }, robotext( messageInfo: UpdateRelationshipMessageInfo, creator: string, params: RobotextParams, ): string { 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: RawUpdateRelationshipMessageInfo, platformDetails: ?PlatformDetails, ): RawUpdateRelationshipMessageInfo | RawUnsupportedMessageInfo { 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, }; }, unshimMessageInfo( unwrapped: RawUpdateRelationshipMessageInfo, ): RawUpdateRelationshipMessageInfo { return unwrapped; }, - notificationTexts(messageInfos, threadInfo) { + notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + ): NotifTexts { 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, }; }, generatesNotifs: true, });