diff --git a/lib/shared/messages/add-members-message-spec.js b/lib/shared/messages/add-members-message-spec.js index e2fe4d43f..c7f81f9d4 100644 --- a/lib/shared/messages/add-members-message-spec.js +++ b/lib/shared/messages/add-members-message-spec.js @@ -1,174 +1,183 @@ // @flow import invariant from 'invariant'; import type { CreateMessageInfoParams, MessageSpec, NotificationTextsParams, } from './message-spec.js'; import { joinResult } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import { type AddMembersMessageData, type AddMembersMessageInfo, type RawAddMembersMessageInfo, rawAddMembersMessageInfoValidator, } from '../../types/messages/add-members.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText, pluralizeEntityText, } from '../../utils/entity-text.js'; import { values } from '../../utils/objects.js'; import { notifRobotextForMessageInfo } from '../notif-utils.js'; -export const addMembersMessageSpec: MessageSpec< +type AddMembersMessageSpec = MessageSpec< AddMembersMessageData, RawAddMembersMessageInfo, AddMembersMessageInfo, -> = Object.freeze({ +> & { + // We need to explicitly type this as non-optional so that + // it can be referenced from messageContentForClientDB below + +messageContentForServerDB: ( + data: AddMembersMessageData | RawAddMembersMessageInfo, + ) => string, + ... +}; + +export const addMembersMessageSpec: AddMembersMessageSpec = Object.freeze({ messageContentForServerDB( data: AddMembersMessageData | RawAddMembersMessageInfo, ): string { return JSON.stringify(data.addedUserIDs); }, messageContentForClientDB(data: RawAddMembersMessageInfo): string { - return this.messageContentForServerDB(data); + return addMembersMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(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), }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawAddMembersMessageInfo { const content = clientDBMessageInfo.content; invariant( content !== undefined && content !== null, 'content must be defined for AddMembers', ); const rawAddMembersMessageInfo: RawAddMembersMessageInfo = { type: messageTypes.ADD_MEMBERS, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, addedUserIDs: JSON.parse(content), }; return rawAddMembersMessageInfo; }, 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 { invariant(id, 'RawAddMembersMessageInfo needs id'); return { ...messageData, id }; }, robotext(messageInfo: AddMembersMessageInfo): EntityText { const users = messageInfo.addedMembers; invariant(users.length !== 0, 'added who??'); const creator = ET.user({ userInfo: messageInfo.creator }); const addedUsers = pluralizeEntityText( users.map(user => ET`${ET.user({ userInfo: user })}`), ); return ET`${creator} added ${addedUsers}`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, params: NotificationTextsParams, ): Promise { const addedMembersObject: { [string]: RelativeUserInfo } = {}; 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 { parentThreadInfo } = params; const robotext = notifRobotextForMessageInfo( mergedMessageInfo, threadInfo, parentThreadInfo, ); const merged = ET`${robotext} to ${ET.thread({ display: 'shortName', threadInfo, })}`; return { merged, title: threadInfo.uiName, body: robotext, }; }, notificationCollapseKey(rawMessageInfo: RawAddMembersMessageInfo): string { return joinResult( rawMessageInfo.type, rawMessageInfo.threadID, rawMessageInfo.creatorID, ); }, userIDs(rawMessageInfo: RawAddMembersMessageInfo): $ReadOnlyArray { return rawMessageInfo.addedUserIDs; }, canBeSidebarSource: true, canBePinned: false, validator: rawAddMembersMessageInfoValidator, }); diff --git a/lib/shared/messages/change-role-message-spec.js b/lib/shared/messages/change-role-message-spec.js index f46cb00b3..00b491034 100644 --- a/lib/shared/messages/change-role-message-spec.js +++ b/lib/shared/messages/change-role-message-spec.js @@ -1,244 +1,253 @@ // @flow import invariant from 'invariant'; import { pushTypes, type CreateMessageInfoParams, type MessageSpec, type RobotextParams, type NotificationTextsParams, } from './message-spec.js'; import { joinResult } from './utils.js'; import type { PlatformDetails } from '../../types/device-types.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import { type ChangeRoleMessageData, type ChangeRoleMessageInfo, type RawChangeRoleMessageInfo, rawChangeRoleMessageInfoValidator, } from '../../types/messages/change-role.js'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText, pluralizeEntityText, } from '../../utils/entity-text.js'; import { entityTextToRawString } from '../../utils/entity-text.js'; import { values } from '../../utils/objects.js'; import { constructChangeRoleEntityText } from '../message-utils.js'; import { notifRobotextForMessageInfo } from '../notif-utils.js'; import { hasMinCodeVersion } from '../version-utils.js'; -export const changeRoleMessageSpec: MessageSpec< +type ChangeRoleMessageSpec = MessageSpec< ChangeRoleMessageData, RawChangeRoleMessageInfo, ChangeRoleMessageInfo, -> = Object.freeze({ +> & { + // We need to explicitly type this as non-optional so that + // it can be referenced from messageContentForClientDB below + +messageContentForServerDB: ( + data: ChangeRoleMessageData | RawChangeRoleMessageInfo, + ) => string, + ... +}; + +export const changeRoleMessageSpec: ChangeRoleMessageSpec = Object.freeze({ messageContentForServerDB( data: ChangeRoleMessageData | RawChangeRoleMessageInfo, ): string { return JSON.stringify({ userIDs: data.userIDs, newRole: data.newRole, roleName: data.roleName, }); }, messageContentForClientDB(data: RawChangeRoleMessageInfo): string { - return this.messageContentForServerDB(data); + return changeRoleMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(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, roleName: content.roleName, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawChangeRoleMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for ChangeRole', ); const content = JSON.parse(clientDBMessageInfo.content); const rawChangeRoleMessageInfo: RawChangeRoleMessageInfo = { type: messageTypes.CHANGE_ROLE, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, userIDs: content.userIDs, newRole: content.newRole, roleName: content.roleName, }; return rawChangeRoleMessageInfo; }, 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, roleName: rawMessageInfo.roleName, }; }, rawMessageInfoFromMessageData( messageData: ChangeRoleMessageData, id: ?string, ): RawChangeRoleMessageInfo { invariant(id, 'RawChangeRoleMessageInfo needs id'); return { ...messageData, id }; }, robotext( messageInfo: ChangeRoleMessageInfo, params: RobotextParams, ): EntityText { const users = messageInfo.members; invariant(users.length !== 0, 'changed whose role??'); const creator = ET.user({ userInfo: messageInfo.creator }); const affectedUsers = pluralizeEntityText( users.map(user => ET`${ET.user({ userInfo: user })}`), ); const { threadInfo, parentThreadInfo } = params; const threadRoleName = threadInfo?.roles[messageInfo.newRole]?.name ?? parentThreadInfo?.roles[messageInfo.newRole]?.name; const messageInfoRoleName = messageInfo.roleName; const roleName = threadRoleName ?? messageInfoRoleName; const constructedEntityText = constructChangeRoleEntityText( affectedUsers, roleName, ); return ET`${creator} ${constructedEntityText}`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, params: NotificationTextsParams, ): Promise { const membersObject: { [string]: RelativeUserInfo } = {}; 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 { parentThreadInfo } = params; const robotext = notifRobotextForMessageInfo( mergedMessageInfo, threadInfo, parentThreadInfo, ); const merged = ET`${robotext} of ${ET.thread({ display: 'shortName', threadInfo, })}`; return { merged, title: threadInfo.uiName, body: robotext, }; }, shimUnsupportedMessageInfo( rawMessageInfo: RawChangeRoleMessageInfo, platformDetails: ?PlatformDetails, ): RawChangeRoleMessageInfo | RawUnsupportedMessageInfo { if (hasMinCodeVersion(platformDetails, { native: 251 })) { return rawMessageInfo; } const { id, userIDs } = rawMessageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); const affectedUsers = userIDs.length === 1 ? 'a member' : 'some members'; const roleName = rawMessageInfo.roleName; const constructedEntityText = constructChangeRoleEntityText( affectedUsers, roleName, ); const stringifiedEntityText = entityTextToRawString(constructedEntityText); return { type: messageTypes.UNSUPPORTED, id, threadID: rawMessageInfo.threadID, creatorID: rawMessageInfo.creatorID, time: rawMessageInfo.time, robotext: stringifiedEntityText, unsupportedMessageInfo: rawMessageInfo, }; }, unshimMessageInfo( unwrapped: RawChangeRoleMessageInfo, ): RawChangeRoleMessageInfo { return unwrapped; }, notificationCollapseKey(rawMessageInfo: RawChangeRoleMessageInfo): string { return joinResult( rawMessageInfo.type, rawMessageInfo.threadID, rawMessageInfo.creatorID, rawMessageInfo.newRole, ); }, generatesNotifs: async () => pushTypes.NOTIF, canBeSidebarSource: true, canBePinned: false, validator: rawChangeRoleMessageInfoValidator, }); diff --git a/lib/shared/messages/change-settings-message-spec.js b/lib/shared/messages/change-settings-message-spec.js index 54b74c8db..4b9a3b9ff 100644 --- a/lib/shared/messages/change-settings-message-spec.js +++ b/lib/shared/messages/change-settings-message-spec.js @@ -1,190 +1,200 @@ // @flow import invariant from 'invariant'; import { pushTypes, type MessageSpec, type RobotextParams, type NotificationTextsParams, } from './message-spec.js'; import { joinResult } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import { type ChangeSettingsMessageData, type ChangeSettingsMessageInfo, type RawChangeSettingsMessageInfo, rawChangeSettingsMessageInfoValidator, } from '../../types/messages/change-settings.js'; import type { NotifTexts } from '../../types/notif-types.js'; import { assertThreadType } from '../../types/thread-types-enum.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; import { validHexColorRegex } from '../account-utils.js'; import { notifRobotextForMessageInfo } from '../notif-utils.js'; import { threadLabel } from '../thread-utils.js'; -export const changeSettingsMessageSpec: MessageSpec< +type ChangeSettingsMessageSpec = MessageSpec< ChangeSettingsMessageData, RawChangeSettingsMessageInfo, ChangeSettingsMessageInfo, -> = Object.freeze({ - messageContentForServerDB( +> & { + // We need to explicitly type this as non-optional so that + // it can be referenced from messageContentForClientDB below + +messageContentForServerDB: ( data: ChangeSettingsMessageData | RawChangeSettingsMessageInfo, - ): string { - return JSON.stringify({ - [data.field]: data.value, - }); - }, - - messageContentForClientDB(data: RawChangeSettingsMessageInfo): string { - return this.messageContentForServerDB(data); - }, - - rawMessageInfoFromServerDBRow(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], - }; - }, - - rawMessageInfoFromClientDB( - clientDBMessageInfo: ClientDBMessageInfo, - ): RawChangeSettingsMessageInfo { - invariant( - clientDBMessageInfo.content !== undefined && - clientDBMessageInfo.content !== null, - 'content must be defined for ChangeSettings', - ); - const content = JSON.parse(clientDBMessageInfo.content); - const field = Object.keys(content)[0]; - const rawChangeSettingsMessageInfo: RawChangeSettingsMessageInfo = { - type: messageTypes.CHANGE_SETTINGS, - id: clientDBMessageInfo.id, - threadID: clientDBMessageInfo.thread, - time: parseInt(clientDBMessageInfo.time), - creatorID: clientDBMessageInfo.user, - field, - value: content[field], - }; - return rawChangeSettingsMessageInfo; - }, - - 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 { - invariant(id, 'RawChangeSettingsMessageInfo needs id'); - return { ...messageData, id }; - }, - - robotext( - messageInfo: ChangeSettingsMessageInfo, - params: RobotextParams, - ): EntityText { - const creator = ET.user({ userInfo: messageInfo.creator }); - const thread = ET.thread({ - display: 'alwaysDisplayShortName', - threadID: messageInfo.threadID, - threadType: params.threadInfo?.type, - parentThreadID: params.threadInfo?.parentThreadID, - possessive: true, - }); - if ( - (messageInfo.field === 'name' || messageInfo.field === 'description') && - messageInfo.value.toString() === '' - ) { - return ET`${creator} cleared ${thread} ${messageInfo.field}`; - } - if (messageInfo.field === 'avatar') { - return ET`${creator} updated ${thread} ${messageInfo.field}`; - } - - let value; - if ( - messageInfo.field === 'color' && - messageInfo.value.toString().match(validHexColorRegex) - ) { - value = ET.color({ hex: `#${messageInfo.value}` }); - } else if (messageInfo.field === 'type') { + ) => string, + ... +}; + +export const changeSettingsMessageSpec: ChangeSettingsMessageSpec = + Object.freeze({ + messageContentForServerDB( + data: ChangeSettingsMessageData | RawChangeSettingsMessageInfo, + ): string { + return JSON.stringify({ + [data.field]: data.value, + }); + }, + + messageContentForClientDB(data: RawChangeSettingsMessageInfo): string { + return changeSettingsMessageSpec.messageContentForServerDB(data); + }, + + rawMessageInfoFromServerDBRow(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], + }; + }, + + rawMessageInfoFromClientDB( + clientDBMessageInfo: ClientDBMessageInfo, + ): RawChangeSettingsMessageInfo { invariant( - typeof messageInfo.value === 'number', - 'messageInfo.value should be number for thread type change ', + clientDBMessageInfo.content !== undefined && + clientDBMessageInfo.content !== null, + 'content must be defined for ChangeSettings', ); - const newThreadType = assertThreadType(messageInfo.value); - value = threadLabel(newThreadType); - } else { - value = messageInfo.value.toString(); - } - return ET`${creator} updated ${thread} ${messageInfo.field} to "${value}"`; - }, - - async notificationTexts( - messageInfos: $ReadOnlyArray, - threadInfo: ThreadInfo, - params: NotificationTextsParams, - ): Promise { - const mostRecentMessageInfo = messageInfos[0]; - invariant( - mostRecentMessageInfo.type === messageTypes.CHANGE_SETTINGS, - 'messageInfo should be messageTypes.CHANGE_SETTINGS!', - ); - const { parentThreadInfo } = params; - const body = notifRobotextForMessageInfo( - mostRecentMessageInfo, - threadInfo, - parentThreadInfo, - ); - return { - merged: body, - title: threadInfo.uiName, - body, - }; - }, - - notificationCollapseKey( - rawMessageInfo: RawChangeSettingsMessageInfo, - ): string { - return joinResult( - rawMessageInfo.type, - rawMessageInfo.threadID, - rawMessageInfo.creatorID, - rawMessageInfo.field, - ); - }, - - generatesNotifs: async () => pushTypes.NOTIF, - - canBeSidebarSource: true, - - canBePinned: false, - - validator: rawChangeSettingsMessageInfoValidator, -}); + const content = JSON.parse(clientDBMessageInfo.content); + const field = Object.keys(content)[0]; + const rawChangeSettingsMessageInfo: RawChangeSettingsMessageInfo = { + type: messageTypes.CHANGE_SETTINGS, + id: clientDBMessageInfo.id, + threadID: clientDBMessageInfo.thread, + time: parseInt(clientDBMessageInfo.time), + creatorID: clientDBMessageInfo.user, + field, + value: content[field], + }; + return rawChangeSettingsMessageInfo; + }, + + 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 { + invariant(id, 'RawChangeSettingsMessageInfo needs id'); + return { ...messageData, id }; + }, + + robotext( + messageInfo: ChangeSettingsMessageInfo, + params: RobotextParams, + ): EntityText { + const creator = ET.user({ userInfo: messageInfo.creator }); + const thread = ET.thread({ + display: 'alwaysDisplayShortName', + threadID: messageInfo.threadID, + threadType: params.threadInfo?.type, + parentThreadID: params.threadInfo?.parentThreadID, + possessive: true, + }); + if ( + (messageInfo.field === 'name' || messageInfo.field === 'description') && + messageInfo.value.toString() === '' + ) { + return ET`${creator} cleared ${thread} ${messageInfo.field}`; + } + if (messageInfo.field === 'avatar') { + return ET`${creator} updated ${thread} ${messageInfo.field}`; + } + + let value; + if ( + messageInfo.field === 'color' && + messageInfo.value.toString().match(validHexColorRegex) + ) { + value = ET.color({ hex: `#${messageInfo.value}` }); + } 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.toString(); + } + return ET`${creator} updated ${thread} ${messageInfo.field} to "${value}"`; + }, + + async notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): Promise { + const mostRecentMessageInfo = messageInfos[0]; + invariant( + mostRecentMessageInfo.type === messageTypes.CHANGE_SETTINGS, + 'messageInfo should be messageTypes.CHANGE_SETTINGS!', + ); + const { parentThreadInfo } = params; + const body = notifRobotextForMessageInfo( + mostRecentMessageInfo, + threadInfo, + parentThreadInfo, + ); + return { + merged: body, + title: threadInfo.uiName, + body, + }; + }, + + notificationCollapseKey( + rawMessageInfo: RawChangeSettingsMessageInfo, + ): string { + return joinResult( + rawMessageInfo.type, + rawMessageInfo.threadID, + rawMessageInfo.creatorID, + rawMessageInfo.field, + ); + }, + + generatesNotifs: async () => pushTypes.NOTIF, + + canBeSidebarSource: true, + + canBePinned: false, + + validator: rawChangeSettingsMessageInfoValidator, + }); diff --git a/lib/shared/messages/create-entry-message-spec.js b/lib/shared/messages/create-entry-message-spec.js index a06cdb815..67330dc7a 100644 --- a/lib/shared/messages/create-entry-message-spec.js +++ b/lib/shared/messages/create-entry-message-spec.js @@ -1,129 +1,138 @@ // @flow import invariant from 'invariant'; import { pushTypes, type MessageSpec } from './message-spec.js'; import { joinResult } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import { type CreateEntryMessageData, type CreateEntryMessageInfo, type RawCreateEntryMessageInfo, rawCreateEntryMessageInfoValidator, } from '../../types/messages/create-entry.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { prettyDate } from '../../utils/date-utils.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; import { notifTextsForEntryCreationOrEdit } from '../notif-utils.js'; -export const createEntryMessageSpec: MessageSpec< +type CreateEntryMessageSpec = MessageSpec< CreateEntryMessageData, RawCreateEntryMessageInfo, CreateEntryMessageInfo, -> = Object.freeze({ +> & { + // We need to explicitly type this as non-optional so that + // it can be referenced from messageContentForClientDB below + +messageContentForServerDB: ( + data: CreateEntryMessageData | RawCreateEntryMessageInfo, + ) => string, + ... +}; + +export const createEntryMessageSpec: CreateEntryMessageSpec = Object.freeze({ messageContentForServerDB( data: CreateEntryMessageData | RawCreateEntryMessageInfo, ): string { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, messageContentForClientDB(data: RawCreateEntryMessageInfo): string { - return this.messageContentForServerDB(data); + return createEntryMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawCreateEntryMessageInfo { const content = JSON.parse(row.content); return { type: messageTypes.CREATE_ENTRY, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), entryID: content.entryID, date: content.date, text: content.text, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawCreateEntryMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for CreateEntry', ); const content = JSON.parse(clientDBMessageInfo.content); const rawCreateEntryMessageInfo: RawCreateEntryMessageInfo = { type: messageTypes.CREATE_ENTRY, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, entryID: content.entryID, date: content.date, text: content.text, }; return rawCreateEntryMessageInfo; }, createMessageInfo( rawMessageInfo: RawCreateEntryMessageInfo, creator: RelativeUserInfo, ): CreateEntryMessageInfo { return { type: messageTypes.CREATE_ENTRY, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, entryID: rawMessageInfo.entryID, date: rawMessageInfo.date, text: rawMessageInfo.text, }; }, rawMessageInfoFromMessageData( messageData: CreateEntryMessageData, id: ?string, ): RawCreateEntryMessageInfo { invariant(id, 'RawCreateEntryMessageInfo needs id'); return { ...messageData, id }; }, robotext(messageInfo: CreateEntryMessageInfo): EntityText { const date = prettyDate(messageInfo.date); const creator = ET.user({ userInfo: messageInfo.creator }); const { text } = messageInfo; return ET`${creator} created an event scheduled for ${date}: "${text}"`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { return notifTextsForEntryCreationOrEdit(messageInfos, threadInfo); }, notificationCollapseKey(rawMessageInfo: RawCreateEntryMessageInfo): string { return joinResult(rawMessageInfo.creatorID, rawMessageInfo.entryID); }, generatesNotifs: async () => pushTypes.NOTIF, canBeSidebarSource: true, canBePinned: false, validator: rawCreateEntryMessageInfoValidator, }); diff --git a/lib/shared/messages/create-sidebar-message-spec.js b/lib/shared/messages/create-sidebar-message-spec.js index 05c28b886..d982883f5 100644 --- a/lib/shared/messages/create-sidebar-message-spec.js +++ b/lib/shared/messages/create-sidebar-message-spec.js @@ -1,214 +1,229 @@ // @flow import invariant from 'invariant'; import { pushTypes, type CreateMessageInfoParams, type MessageSpec, type NotificationTextsParams, type RobotextParams, } from './message-spec.js'; import { joinResult } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import { type CreateSidebarMessageData, type CreateSidebarMessageInfo, type RawCreateSidebarMessageInfo, rawCreateSidebarMessageInfoValidator, } from '../../types/messages/create-sidebar.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText, pluralizeEntityText, } from '../../utils/entity-text.js'; import { notifTextsForSidebarCreation } from '../notif-utils.js'; -export const createSidebarMessageSpec: MessageSpec< +type CreateSidebarMessageSpec = MessageSpec< CreateSidebarMessageData, RawCreateSidebarMessageInfo, CreateSidebarMessageInfo, -> = Object.freeze({ - messageContentForServerDB( +> & { + // We need to explicitly type this as non-optional so that + // it can be referenced from messageContentForClientDB below + +messageContentForServerDB: ( data: CreateSidebarMessageData | RawCreateSidebarMessageInfo, - ): string { - return JSON.stringify({ - ...data.initialThreadState, - sourceMessageAuthorID: data.sourceMessageAuthorID, - }); - }, - - messageContentForClientDB(data: RawCreateSidebarMessageInfo): string { - return this.messageContentForServerDB(data); - }, - - rawMessageInfoFromServerDBRow(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, - }; - }, - - rawMessageInfoFromClientDB( - clientDBMessageInfo: ClientDBMessageInfo, - ): RawCreateSidebarMessageInfo { - invariant( - clientDBMessageInfo.content !== undefined && - clientDBMessageInfo.content !== null, - 'content must be defined for CreateSidebar', - ); - - const { sourceMessageAuthorID, ...initialThreadState } = JSON.parse( - clientDBMessageInfo.content, - ); - const rawCreateSidebarMessageInfo: RawCreateSidebarMessageInfo = { - type: messageTypes.CREATE_SIDEBAR, - id: clientDBMessageInfo.id, - threadID: clientDBMessageInfo.thread, - time: parseInt(clientDBMessageInfo.time), - creatorID: clientDBMessageInfo.user, - sourceMessageAuthorID: sourceMessageAuthorID, - initialThreadState: initialThreadState, - }; - return rawCreateSidebarMessageInfo; - }, + ) => string, + ... +}; + +export const createSidebarMessageSpec: CreateSidebarMessageSpec = Object.freeze( + { + messageContentForServerDB( + data: CreateSidebarMessageData | RawCreateSidebarMessageInfo, + ): string { + return JSON.stringify({ + ...data.initialThreadState, + sourceMessageAuthorID: data.sourceMessageAuthorID, + }); + }, + + messageContentForClientDB(data: RawCreateSidebarMessageInfo): string { + return createSidebarMessageSpec.messageContentForServerDB(data); + }, + + rawMessageInfoFromServerDBRow(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, + }; + }, + + rawMessageInfoFromClientDB( + clientDBMessageInfo: ClientDBMessageInfo, + ): RawCreateSidebarMessageInfo { + invariant( + clientDBMessageInfo.content !== undefined && + clientDBMessageInfo.content !== null, + 'content must be defined for CreateSidebar', + ); - 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, + const { sourceMessageAuthorID, ...initialThreadState } = JSON.parse( + clientDBMessageInfo.content, + ); + const rawCreateSidebarMessageInfo: RawCreateSidebarMessageInfo = { + type: messageTypes.CREATE_SIDEBAR, + id: clientDBMessageInfo.id, + threadID: clientDBMessageInfo.thread, + time: parseInt(clientDBMessageInfo.time), + creatorID: clientDBMessageInfo.user, + sourceMessageAuthorID: sourceMessageAuthorID, + initialThreadState: initialThreadState, + }; + return rawCreateSidebarMessageInfo; + }, + + 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 { - invariant(id, 'RawCreateSidebarMessageInfo needs id'); - return { ...messageData, id }; - }, - - robotext( - messageInfo: CreateSidebarMessageInfo, - params: RobotextParams, - ): EntityText { - let text = ET`started ${ET.thread({ - display: 'alwaysDisplayShortName', - threadID: messageInfo.threadID, - threadType: params.threadInfo?.type, - parentThreadID: params.threadInfo?.parentThreadID, - })}`; - const users = messageInfo.initialThreadState.otherMembers.filter( - member => member.id !== messageInfo.sourceMessageAuthor.id, - ); - if (users.length !== 0) { - const initialUsers = pluralizeEntityText( - users.map(user => ET`${ET.user({ userInfo: user })}`), + }, + }; + }, + + rawMessageInfoFromMessageData( + messageData: CreateSidebarMessageData, + id: ?string, + ): RawCreateSidebarMessageInfo { + invariant(id, 'RawCreateSidebarMessageInfo needs id'); + return { ...messageData, id }; + }, + + robotext( + messageInfo: CreateSidebarMessageInfo, + params: RobotextParams, + ): EntityText { + let text = ET`started ${ET.thread({ + display: 'alwaysDisplayShortName', + threadID: messageInfo.threadID, + threadType: params.threadInfo?.type, + parentThreadID: params.threadInfo?.parentThreadID, + })}`; + const users = messageInfo.initialThreadState.otherMembers.filter( + member => member.id !== messageInfo.sourceMessageAuthor.id, + ); + if (users.length !== 0) { + const initialUsers = pluralizeEntityText( + users.map(user => ET`${ET.user({ userInfo: user })}`), + ); + text = ET`${text} and added ${initialUsers}`; + } + const creator = ET.user({ userInfo: messageInfo.creator }); + return ET`${creator} ${text}`; + }, + + unshimMessageInfo( + unwrapped: RawCreateSidebarMessageInfo, + ): RawCreateSidebarMessageInfo { + return unwrapped; + }, + + async notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): Promise { + const createSidebarMessageInfo = messageInfos[0]; + invariant( + createSidebarMessageInfo.type === messageTypes.CREATE_SIDEBAR, + 'first MessageInfo should be messageTypes.CREATE_SIDEBAR!', ); - text = ET`${text} and added ${initialUsers}`; - } - const creator = ET.user({ userInfo: messageInfo.creator }); - return ET`${creator} ${text}`; - }, - - unshimMessageInfo( - unwrapped: RawCreateSidebarMessageInfo, - ): RawCreateSidebarMessageInfo { - return unwrapped; - }, - - async notificationTexts( - messageInfos: $ReadOnlyArray, - threadInfo: ThreadInfo, - params: NotificationTextsParams, - ): Promise { - const createSidebarMessageInfo = messageInfos[0]; - invariant( - createSidebarMessageInfo.type === messageTypes.CREATE_SIDEBAR, - 'first MessageInfo should be messageTypes.CREATE_SIDEBAR!', - ); - - let sidebarSourceMessageInfo; - const secondMessageInfo = messageInfos[1]; - if ( - secondMessageInfo && - secondMessageInfo.type === messageTypes.SIDEBAR_SOURCE - ) { - sidebarSourceMessageInfo = secondMessageInfo; - } - - return notifTextsForSidebarCreation({ - createSidebarMessageInfo, - sidebarSourceMessageInfo, - threadInfo, - params, - }); - }, - - notificationCollapseKey(rawMessageInfo: RawCreateSidebarMessageInfo): string { - return joinResult(messageTypes.CREATE_SIDEBAR, rawMessageInfo.threadID); - }, - - generatesNotifs: async () => pushTypes.NOTIF, - - userIDs(rawMessageInfo: RawCreateSidebarMessageInfo): $ReadOnlyArray { - return rawMessageInfo.initialThreadState.memberIDs; - }, - threadIDs( - rawMessageInfo: RawCreateSidebarMessageInfo, - ): $ReadOnlyArray { - const { parentThreadID } = rawMessageInfo.initialThreadState; - return [parentThreadID]; + let sidebarSourceMessageInfo; + const secondMessageInfo = messageInfos[1]; + if ( + secondMessageInfo && + secondMessageInfo.type === messageTypes.SIDEBAR_SOURCE + ) { + sidebarSourceMessageInfo = secondMessageInfo; + } + + return notifTextsForSidebarCreation({ + createSidebarMessageInfo, + sidebarSourceMessageInfo, + threadInfo, + params, + }); + }, + + notificationCollapseKey( + rawMessageInfo: RawCreateSidebarMessageInfo, + ): string { + return joinResult(messageTypes.CREATE_SIDEBAR, rawMessageInfo.threadID); + }, + + generatesNotifs: async () => pushTypes.NOTIF, + + userIDs( + rawMessageInfo: RawCreateSidebarMessageInfo, + ): $ReadOnlyArray { + return rawMessageInfo.initialThreadState.memberIDs; + }, + + threadIDs( + rawMessageInfo: RawCreateSidebarMessageInfo, + ): $ReadOnlyArray { + const { parentThreadID } = rawMessageInfo.initialThreadState; + return [parentThreadID]; + }, + + canBeSidebarSource: true, + + canBePinned: false, + + validator: rawCreateSidebarMessageInfoValidator, }, - - canBeSidebarSource: true, - - canBePinned: false, - - validator: rawCreateSidebarMessageInfoValidator, -}); +); diff --git a/lib/shared/messages/create-sub-thread-message-spec.js b/lib/shared/messages/create-sub-thread-message-spec.js index 42abb846f..063eb35d7 100644 --- a/lib/shared/messages/create-sub-thread-message-spec.js +++ b/lib/shared/messages/create-sub-thread-message-spec.js @@ -1,171 +1,181 @@ // @flow import invariant from 'invariant'; import { pushTypes, type CreateMessageInfoParams, type MessageSpec, type GeneratesNotifsParams, } from './message-spec.js'; import { assertSingleMessageInfo } from './utils.js'; import { permissionLookup } from '../../permissions/thread-permissions.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import { type CreateSubthreadMessageData, type CreateSubthreadMessageInfo, type RawCreateSubthreadMessageInfo, rawCreateSubthreadMessageInfoValidator, } from '../../types/messages/create-subthread.js'; import type { NotifTexts } from '../../types/notif-types.js'; import { threadPermissions } from '../../types/thread-permission-types.js'; import { threadTypes } from '../../types/thread-types-enum.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; import { notifTextsForSubthreadCreation } from '../notif-utils.js'; -export const createSubThreadMessageSpec: MessageSpec< +type CreateSubThreadMessageSpec = MessageSpec< CreateSubthreadMessageData, RawCreateSubthreadMessageInfo, CreateSubthreadMessageInfo, -> = Object.freeze({ - messageContentForServerDB( +> & { + // We need to explicitly type this as non-optional so that + // it can be referenced from messageContentForClientDB below + +messageContentForServerDB: ( data: CreateSubthreadMessageData | RawCreateSubthreadMessageInfo, - ): string { - return data.childThreadID; - }, - - messageContentForClientDB(data: RawCreateSubthreadMessageInfo): string { - return this.messageContentForServerDB(data); - }, - - rawMessageInfoFromServerDBRow(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, - }; - }, - - rawMessageInfoFromClientDB( - clientDBMessageInfo: ClientDBMessageInfo, - ): RawCreateSubthreadMessageInfo { - const content = clientDBMessageInfo.content; - invariant( - content !== undefined && content !== null, - 'content must be defined for CreateSubThread', - ); - const rawCreateSubthreadMessageInfo: RawCreateSubthreadMessageInfo = { - type: messageTypes.CREATE_SUB_THREAD, - id: clientDBMessageInfo.id, - threadID: clientDBMessageInfo.thread, - time: parseInt(clientDBMessageInfo.time), - creatorID: clientDBMessageInfo.user, - childThreadID: content, - }; - return rawCreateSubthreadMessageInfo; - }, - - 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 { - invariant(id, 'RawCreateSubthreadMessageInfo needs id'); - return { ...messageData, id }; - }, - - robotext(messageInfo: CreateSubthreadMessageInfo): EntityText { - const threadEntity = ET.thread({ - display: 'shortName', - threadInfo: messageInfo.childThreadInfo, - subchannel: true, - }); - - let text; - if (messageInfo.childThreadInfo.name) { - const childNoun = - messageInfo.childThreadInfo.type === threadTypes.SIDEBAR - ? 'thread' - : 'subchannel'; - text = ET`created a ${childNoun} named "${threadEntity}"`; - } else { - text = ET`created a ${threadEntity}`; - } - - const creator = ET.user({ userInfo: messageInfo.creator }); - return ET`${creator} ${text}`; - }, - - async notificationTexts( - messageInfos: $ReadOnlyArray, - threadInfo: ThreadInfo, - ): Promise { - const messageInfo = assertSingleMessageInfo(messageInfos); - invariant( - messageInfo.type === messageTypes.CREATE_SUB_THREAD, - 'messageInfo should be messageTypes.CREATE_SUB_THREAD!', - ); - return notifTextsForSubthreadCreation({ - creator: messageInfo.creator, - threadType: messageInfo.childThreadInfo.type, - parentThreadInfo: threadInfo, - childThreadName: messageInfo.childThreadInfo.name, - childThreadUIName: messageInfo.childThreadInfo.uiName, - }); - }, - - generatesNotifs: async ( - rawMessageInfo: RawCreateSubthreadMessageInfo, - messageData: CreateSubthreadMessageData, - params: GeneratesNotifsParams, - ) => { - const { userNotMemberOfSubthreads } = params; - return userNotMemberOfSubthreads.has(rawMessageInfo.childThreadID) - ? pushTypes.NOTIF - : undefined; - }, - - threadIDs( - rawMessageInfo: RawCreateSubthreadMessageInfo, - ): $ReadOnlyArray { - return [rawMessageInfo.childThreadID]; - }, - - canBeSidebarSource: true, - - canBePinned: false, - - validator: rawCreateSubthreadMessageInfoValidator, -}); + ) => string, + ... +}; + +export const createSubThreadMessageSpec: CreateSubThreadMessageSpec = + Object.freeze({ + messageContentForServerDB( + data: CreateSubthreadMessageData | RawCreateSubthreadMessageInfo, + ): string { + return data.childThreadID; + }, + + messageContentForClientDB(data: RawCreateSubthreadMessageInfo): string { + return createSubThreadMessageSpec.messageContentForServerDB(data); + }, + + rawMessageInfoFromServerDBRow(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, + }; + }, + + rawMessageInfoFromClientDB( + clientDBMessageInfo: ClientDBMessageInfo, + ): RawCreateSubthreadMessageInfo { + const content = clientDBMessageInfo.content; + invariant( + content !== undefined && content !== null, + 'content must be defined for CreateSubThread', + ); + const rawCreateSubthreadMessageInfo: RawCreateSubthreadMessageInfo = { + type: messageTypes.CREATE_SUB_THREAD, + id: clientDBMessageInfo.id, + threadID: clientDBMessageInfo.thread, + time: parseInt(clientDBMessageInfo.time), + creatorID: clientDBMessageInfo.user, + childThreadID: content, + }; + return rawCreateSubthreadMessageInfo; + }, + + 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 { + invariant(id, 'RawCreateSubthreadMessageInfo needs id'); + return { ...messageData, id }; + }, + + robotext(messageInfo: CreateSubthreadMessageInfo): EntityText { + const threadEntity = ET.thread({ + display: 'shortName', + threadInfo: messageInfo.childThreadInfo, + subchannel: true, + }); + + let text; + if (messageInfo.childThreadInfo.name) { + const childNoun = + messageInfo.childThreadInfo.type === threadTypes.SIDEBAR + ? 'thread' + : 'subchannel'; + text = ET`created a ${childNoun} named "${threadEntity}"`; + } else { + text = ET`created a ${threadEntity}`; + } + + const creator = ET.user({ userInfo: messageInfo.creator }); + return ET`${creator} ${text}`; + }, + + async notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + ): Promise { + const messageInfo = assertSingleMessageInfo(messageInfos); + invariant( + messageInfo.type === messageTypes.CREATE_SUB_THREAD, + 'messageInfo should be messageTypes.CREATE_SUB_THREAD!', + ); + return notifTextsForSubthreadCreation({ + creator: messageInfo.creator, + threadType: messageInfo.childThreadInfo.type, + parentThreadInfo: threadInfo, + childThreadName: messageInfo.childThreadInfo.name, + childThreadUIName: messageInfo.childThreadInfo.uiName, + }); + }, + + generatesNotifs: async ( + rawMessageInfo: RawCreateSubthreadMessageInfo, + messageData: CreateSubthreadMessageData, + params: GeneratesNotifsParams, + ) => { + const { userNotMemberOfSubthreads } = params; + return userNotMemberOfSubthreads.has(rawMessageInfo.childThreadID) + ? pushTypes.NOTIF + : undefined; + }, + + threadIDs( + rawMessageInfo: RawCreateSubthreadMessageInfo, + ): $ReadOnlyArray { + return [rawMessageInfo.childThreadID]; + }, + + canBeSidebarSource: true, + + canBePinned: false, + + validator: rawCreateSubthreadMessageInfoValidator, + }); diff --git a/lib/shared/messages/create-thread-message-spec.js b/lib/shared/messages/create-thread-message-spec.js index 9f7a04759..15df1c822 100644 --- a/lib/shared/messages/create-thread-message-spec.js +++ b/lib/shared/messages/create-thread-message-spec.js @@ -1,210 +1,219 @@ // @flow import invariant from 'invariant'; import { pushTypes, type CreateMessageInfoParams, type MessageSpec, type RobotextParams, } from './message-spec.js'; import { assertSingleMessageInfo } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import { type CreateThreadMessageData, type CreateThreadMessageInfo, type RawCreateThreadMessageInfo, rawCreateThreadMessageInfoValidator, } from '../../types/messages/create-thread.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText, pluralizeEntityText, } from '../../utils/entity-text.js'; import { notifTextsForSubthreadCreation } from '../notif-utils.js'; import { threadNoun } from '../thread-utils.js'; -export const createThreadMessageSpec: MessageSpec< +type CreateThreadMessageSpec = MessageSpec< CreateThreadMessageData, RawCreateThreadMessageInfo, CreateThreadMessageInfo, -> = Object.freeze({ +> & { + // We need to explicitly type this as non-optional so that + // it can be referenced from messageContentForClientDB below + +messageContentForServerDB: ( + data: CreateThreadMessageData | RawCreateThreadMessageInfo, + ) => string, + ... +}; + +export const createThreadMessageSpec: CreateThreadMessageSpec = Object.freeze({ messageContentForServerDB( data: CreateThreadMessageData | RawCreateThreadMessageInfo, ): string { return JSON.stringify(data.initialThreadState); }, messageContentForClientDB(data: RawCreateThreadMessageInfo): string { - return this.messageContentForServerDB(data); + return createThreadMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(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), }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawCreateThreadMessageInfo { const content = clientDBMessageInfo.content; invariant( content !== undefined && content !== null, 'content must be defined for CreateThread', ); const rawCreateThreadMessageInfo: RawCreateThreadMessageInfo = { type: messageTypes.CREATE_THREAD, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, initialThreadState: JSON.parse(content), }; return rawCreateThreadMessageInfo; }, 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 { invariant(id, 'RawCreateThreadMessageInfo needs id'); return { ...messageData, id }; }, robotext( messageInfo: CreateThreadMessageInfo, params: RobotextParams, ): EntityText { let text = ET`created ${ET.thread({ display: 'alwaysDisplayShortName', threadID: messageInfo.threadID, threadType: params.threadInfo?.type, parentThreadID: params.threadInfo?.parentThreadID, })}`; const parentThread = messageInfo.initialThreadState.parentThreadInfo; if (parentThread) { text = ET`${text} as a child of ${ET.thread({ display: 'uiName', threadInfo: parentThread, })}`; } if (messageInfo.initialThreadState.name) { text = ET`${text} with the name "${messageInfo.initialThreadState.name}"`; } const users = messageInfo.initialThreadState.otherMembers; if (users.length !== 0) { const initialUsers = pluralizeEntityText( users.map(user => ET`${ET.user({ userInfo: user })}`), ); text = ET`${text} and added ${initialUsers}`; } const creator = ET.user({ userInfo: messageInfo.creator }); return ET`${creator} ${text}`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const messageInfo = assertSingleMessageInfo(messageInfos); invariant( messageInfo.type === messageTypes.CREATE_THREAD, 'messageInfo should be messageTypes.CREATE_THREAD!', ); const threadType = messageInfo.initialThreadState.type; const parentThreadInfo = messageInfo.initialThreadState.parentThreadInfo; const threadName = messageInfo.initialThreadState.name; if (parentThreadInfo) { return notifTextsForSubthreadCreation({ creator: messageInfo.creator, threadType, parentThreadInfo, childThreadName: threadName, childThreadUIName: threadInfo.uiName, }); } const creator = ET.user({ userInfo: messageInfo.creator }); const prefix = ET`${creator}`; const body = `created a new ${threadNoun(threadType)}`; let merged = ET`${prefix} ${body}`; if (threadName) { merged = ET`${merged} called "${threadName}"`; } return { merged, body, title: threadInfo.uiName, prefix, }; }, generatesNotifs: async () => pushTypes.NOTIF, userIDs(rawMessageInfo: RawCreateThreadMessageInfo): $ReadOnlyArray { return rawMessageInfo.initialThreadState.memberIDs; }, startsThread: true, threadIDs( rawMessageInfo: RawCreateThreadMessageInfo, ): $ReadOnlyArray { const { parentThreadID } = rawMessageInfo.initialThreadState; return parentThreadID ? [parentThreadID] : []; }, canBeSidebarSource: true, canBePinned: false, validator: rawCreateThreadMessageInfoValidator, }); diff --git a/lib/shared/messages/delete-entry-message-spec.js b/lib/shared/messages/delete-entry-message-spec.js index 3169537a9..68c796a20 100644 --- a/lib/shared/messages/delete-entry-message-spec.js +++ b/lib/shared/messages/delete-entry-message-spec.js @@ -1,144 +1,153 @@ // @flow import invariant from 'invariant'; import { pushTypes, type MessageSpec } from './message-spec.js'; import { assertSingleMessageInfo } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import { type DeleteEntryMessageData, type DeleteEntryMessageInfo, type RawDeleteEntryMessageInfo, rawDeleteEntryMessageInfoValidator, } from '../../types/messages/delete-entry.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { prettyDate } from '../../utils/date-utils.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; -export const deleteEntryMessageSpec: MessageSpec< +type DeleteEntryMessageSpec = MessageSpec< DeleteEntryMessageData, RawDeleteEntryMessageInfo, DeleteEntryMessageInfo, -> = Object.freeze({ +> & { + // We need to explicitly type this as non-optional so that + // it can be referenced from messageContentForClientDB below + +messageContentForServerDB: ( + data: DeleteEntryMessageData | RawDeleteEntryMessageInfo, + ) => string, + ... +}; + +export const deleteEntryMessageSpec: DeleteEntryMessageSpec = Object.freeze({ messageContentForServerDB( data: DeleteEntryMessageData | RawDeleteEntryMessageInfo, ): string { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, messageContentForClientDB(data: RawDeleteEntryMessageInfo): string { - return this.messageContentForServerDB(data); + return deleteEntryMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(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, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawDeleteEntryMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for DeleteEntry', ); const content = JSON.parse(clientDBMessageInfo.content); const rawDeleteEntryMessageInfo: RawDeleteEntryMessageInfo = { type: messageTypes.DELETE_ENTRY, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, entryID: content.entryID, date: content.date, text: content.text, }; return rawDeleteEntryMessageInfo; }, 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 { invariant(id, 'RawDeleteEntryMessageInfo needs id'); return { ...messageData, id }; }, robotext(messageInfo: DeleteEntryMessageInfo): EntityText { const date = prettyDate(messageInfo.date); const creator = ET.user({ userInfo: messageInfo.creator }); const { text } = messageInfo; return ET`${creator} deleted an event scheduled for ${date}: "${text}"`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const messageInfo = assertSingleMessageInfo(messageInfos); invariant( messageInfo.type === messageTypes.DELETE_ENTRY, 'messageInfo should be messageTypes.DELETE_ENTRY!', ); const thread = ET.thread({ display: 'shortName', threadInfo }); const creator = ET.user({ userInfo: messageInfo.creator }); const date = prettyDate(messageInfo.date); const prefix = ET`${creator}`; let body = ET`deleted an event in ${thread}`; body = ET`${body} scheduled for ${date}: "${messageInfo.text}"`; const merged = ET`${prefix} ${body}`; return { merged, title: threadInfo.uiName, body, prefix, }; }, generatesNotifs: async () => pushTypes.NOTIF, canBeSidebarSource: true, canBePinned: false, validator: rawDeleteEntryMessageInfoValidator, }); diff --git a/lib/shared/messages/edit-entry-message-spec.js b/lib/shared/messages/edit-entry-message-spec.js index b17e14d50..f0afd05f9 100644 --- a/lib/shared/messages/edit-entry-message-spec.js +++ b/lib/shared/messages/edit-entry-message-spec.js @@ -1,129 +1,138 @@ // @flow import invariant from 'invariant'; import { pushTypes, type MessageSpec } from './message-spec.js'; import { joinResult } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import { type EditEntryMessageData, type EditEntryMessageInfo, type RawEditEntryMessageInfo, rawEditEntryMessageInfoValidator, } from '../../types/messages/edit-entry.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { prettyDate } from '../../utils/date-utils.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; import { notifTextsForEntryCreationOrEdit } from '../notif-utils.js'; -export const editEntryMessageSpec: MessageSpec< +type EditEntryMessageSpec = MessageSpec< EditEntryMessageData, RawEditEntryMessageInfo, EditEntryMessageInfo, -> = Object.freeze({ +> & { + // We need to explicitly type this as non-optional so that + // it can be referenced from messageContentForClientDB below + +messageContentForServerDB: ( + data: EditEntryMessageData | RawEditEntryMessageInfo, + ) => string, + ... +}; + +export const editEntryMessageSpec: EditEntryMessageSpec = Object.freeze({ messageContentForServerDB( data: EditEntryMessageData | RawEditEntryMessageInfo, ): string { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, messageContentForClientDB(data: RawEditEntryMessageInfo): string { - return this.messageContentForServerDB(data); + return editEntryMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawEditEntryMessageInfo { const content = JSON.parse(row.content); return { type: messageTypes.EDIT_ENTRY, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), entryID: content.entryID, date: content.date, text: content.text, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawEditEntryMessageInfo { invariant( clientDBMessageInfo.content !== null && clientDBMessageInfo.content !== undefined, 'content must be defined for EditEntry', ); const content = JSON.parse(clientDBMessageInfo.content); const rawEditEntryMessageInfo: RawEditEntryMessageInfo = { type: messageTypes.EDIT_ENTRY, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, entryID: content.entryID, date: content.date, text: content.text, }; return rawEditEntryMessageInfo; }, createMessageInfo( rawMessageInfo: RawEditEntryMessageInfo, creator: RelativeUserInfo, ): EditEntryMessageInfo { return { type: messageTypes.EDIT_ENTRY, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, entryID: rawMessageInfo.entryID, date: rawMessageInfo.date, text: rawMessageInfo.text, }; }, rawMessageInfoFromMessageData( messageData: EditEntryMessageData, id: ?string, ): RawEditEntryMessageInfo { invariant(id, 'RawEditEntryMessageInfo needs id'); return { ...messageData, id }; }, robotext(messageInfo: EditEntryMessageInfo): EntityText { const date = prettyDate(messageInfo.date); const creator = ET.user({ userInfo: messageInfo.creator }); const { text } = messageInfo; return ET`${creator} updated the text of an event scheduled for ${date}: "${text}"`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { return notifTextsForEntryCreationOrEdit(messageInfos, threadInfo); }, notificationCollapseKey(rawMessageInfo: RawEditEntryMessageInfo): string { return joinResult(rawMessageInfo.creatorID, rawMessageInfo.entryID); }, generatesNotifs: async () => pushTypes.NOTIF, canBeSidebarSource: true, canBePinned: false, validator: rawEditEntryMessageInfoValidator, }); diff --git a/lib/shared/messages/multimedia-message-spec.js b/lib/shared/messages/multimedia-message-spec.js index 322281b21..cb3d07cbe 100644 --- a/lib/shared/messages/multimedia-message-spec.js +++ b/lib/shared/messages/multimedia-message-spec.js @@ -1,368 +1,381 @@ // @flow import invariant from 'invariant'; import { pushTypes, type MessageSpec, type MessageTitleParam, type RawMessageInfoFromServerDBRowParams, } from './message-spec.js'; import { joinResult } from './utils.js'; import { versionSpecificMediaMessageFormat, isMediaBlobServiceHosted, contentStringForMediaArray, multimediaMessagePreview, } from '../../media/media-utils.js'; import type { PlatformDetails } from '../../types/device-types.js'; import { assertMessageType, messageTypes, } from '../../types/message-types-enum.js'; import { isMediaMessageType, rawMultimediaMessageInfoValidator, } from '../../types/message-types.js'; import type { MessageInfo, RawMessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import type { ImagesMessageData, RawImagesMessageInfo, ImagesMessageInfo, } from '../../types/messages/images.js'; import type { MediaMessageData, MediaMessageInfo, RawMediaMessageInfo, } from '../../types/messages/media.js'; import { getMediaMessageServerDBContentsFromMedia } from '../../types/messages/media.js'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET } from '../../utils/entity-text.js'; import { translateClientDBMediaInfosToMedia, translateClientDBMediaInfoToImage, } from '../../utils/message-ops-utils.js'; import { createMediaMessageInfo } from '../message-utils.js'; import { threadIsGroupChat } from '../thread-utils.js'; import { FUTURE_CODE_VERSION, hasMinCodeVersion } from '../version-utils.js'; -export const multimediaMessageSpec: MessageSpec< +type MultimediaMessageSpec = MessageSpec< MediaMessageData | ImagesMessageData, RawMediaMessageInfo | RawImagesMessageInfo, MediaMessageInfo | ImagesMessageInfo, -> = Object.freeze({ +> & { + // We need to explicitly type this as non-optional so that + // it can be referenced from messageContentForClientDB below + +messageContentForServerDB: ( + data: + | MediaMessageData + | ImagesMessageData + | RawMediaMessageInfo + | RawImagesMessageInfo, + ) => string, + ... +}; + +export const multimediaMessageSpec: MultimediaMessageSpec = Object.freeze({ messageContentForServerDB( data: | MediaMessageData | ImagesMessageData | RawMediaMessageInfo | RawImagesMessageInfo, ): string { if (data.type === messageTypes.MULTIMEDIA) { return JSON.stringify( getMediaMessageServerDBContentsFromMedia(data.media), ); } const mediaIDs = data.media.map(media => parseInt(media.id, 10)); return JSON.stringify(mediaIDs); }, messageContentForClientDB( data: RawMediaMessageInfo | RawImagesMessageInfo, ): string { - return this.messageContentForServerDB(data); + return multimediaMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawImagesMessageInfo | RawMediaMessageInfo { const messageType = assertMessageType(parseInt(clientDBMessageInfo.type)); invariant( isMediaMessageType(messageType), 'message must be of type IMAGES or MULTIMEDIA', ); invariant( clientDBMessageInfo.media_infos !== null && clientDBMessageInfo.media_infos !== undefined, `media_infos must be defined`, ); let rawMessageInfo: RawImagesMessageInfo | RawMediaMessageInfo = messageType === messageTypes.IMAGES ? { type: messageTypes.IMAGES, threadID: clientDBMessageInfo.thread, creatorID: clientDBMessageInfo.user, time: parseInt(clientDBMessageInfo.time), media: clientDBMessageInfo.media_infos?.map( translateClientDBMediaInfoToImage, ) ?? [], } : { type: messageTypes.MULTIMEDIA, threadID: clientDBMessageInfo.thread, creatorID: clientDBMessageInfo.user, time: parseInt(clientDBMessageInfo.time), media: translateClientDBMediaInfosToMedia(clientDBMessageInfo), }; if (clientDBMessageInfo.local_id) { rawMessageInfo = { ...rawMessageInfo, localID: clientDBMessageInfo.local_id, }; } if (clientDBMessageInfo.id !== clientDBMessageInfo.local_id) { rawMessageInfo = { ...rawMessageInfo, id: clientDBMessageInfo.id, }; } return rawMessageInfo; }, messageTitle({ messageInfo, }: MessageTitleParam) { const creator = ET.user({ userInfo: messageInfo.creator }); const preview = multimediaMessagePreview(messageInfo); return ET`${creator} ${preview}`; }, rawMessageInfoFromServerDBRow( row: Object, params: RawMessageInfoFromServerDBRowParams, ): 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) { let messageInfo: ImagesMessageInfo = { type: messageTypes.IMAGES, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, media: rawMessageInfo.media, }; if (rawMessageInfo.id) { messageInfo = { ...messageInfo, id: rawMessageInfo.id }; } if (rawMessageInfo.localID) { messageInfo = { ...messageInfo, localID: rawMessageInfo.localID }; } return messageInfo; } else if (rawMessageInfo.type === messageTypes.MULTIMEDIA) { let messageInfo: MediaMessageInfo = { type: messageTypes.MULTIMEDIA, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, media: rawMessageInfo.media, }; if (rawMessageInfo.id) { messageInfo = { ...messageInfo, id: rawMessageInfo.id }; } if (rawMessageInfo.localID) { messageInfo = { ...messageInfo, localID: rawMessageInfo.localID }; } return messageInfo; } return undefined; }, rawMessageInfoFromMessageData( messageData: MediaMessageData | ImagesMessageData, id: ?string, ): RawMediaMessageInfo | RawImagesMessageInfo { const { sidebarCreation, ...rest } = messageData; if (rest.type === messageTypes.IMAGES && id) { return ({ ...rest, id }: RawImagesMessageInfo); } else if (rest.type === messageTypes.IMAGES) { return ({ ...rest }: RawImagesMessageInfo); } else if (id) { return ({ ...rest, id }: RawMediaMessageInfo); } else { return ({ ...rest }: RawMediaMessageInfo); } }, shimUnsupportedMessageInfo( rawMessageInfo: RawMediaMessageInfo | RawImagesMessageInfo, platformDetails: ?PlatformDetails, ): RawMediaMessageInfo | RawImagesMessageInfo | RawUnsupportedMessageInfo { if (rawMessageInfo.type === messageTypes.IMAGES) { return rawMessageInfo; } const messageInfo = versionSpecificMediaMessageFormat( rawMessageInfo, platformDetails, ); const containsBlobServiceMedia = messageInfo.media.some( isMediaBlobServiceHosted, ); const containsEncryptedMedia = messageInfo.media.some( media => media.type === 'encrypted_photo' || media.type === 'encrypted_video', ); if ( !containsBlobServiceMedia && !containsEncryptedMedia && hasMinCodeVersion(platformDetails, { native: 158 }) ) { return messageInfo; } if ( !containsBlobServiceMedia && hasMinCodeVersion(platformDetails, { native: 205 }) ) { return messageInfo; } if (hasMinCodeVersion(platformDetails, { native: FUTURE_CODE_VERSION })) { return messageInfo; } const { id } = messageInfo; invariant(id !== null && id !== undefined, 'id should be set on server'); return { type: messageTypes.UNSUPPORTED, id, threadID: messageInfo.threadID, creatorID: messageInfo.creatorID, time: messageInfo.time, robotext: multimediaMessagePreview(messageInfo), unsupportedMessageInfo: messageInfo, }; }, 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 media of unwrapped.media) { if (isMediaBlobServiceHosted(media)) { return messageInfo; } const { type } = media; if ( type !== 'photo' && type !== 'video' && type !== 'encrypted_photo' && type !== 'encrypted_video' ) { return messageInfo; } } } return undefined; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { 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 creator = ET.user({ userInfo: messageInfos[0].creator }); let body, merged; if (!threadInfo.name && !threadIsGroupChat(threadInfo)) { body = `sent you ${contentString}`; merged = body; } else { body = `sent ${contentString}`; const thread = ET.thread({ display: 'shortName', threadInfo }); merged = ET`${body} to ${thread}`; } merged = ET`${creator} ${merged}`; return { merged, body, title: threadInfo.uiName, prefix: ET`${creator}`, }; }, notificationCollapseKey( rawMessageInfo: RawMediaMessageInfo | RawImagesMessageInfo, ): string { // We use the legacy constant here to collapse both types into one return joinResult( messageTypes.IMAGES, rawMessageInfo.threadID, rawMessageInfo.creatorID, ); }, generatesNotifs: async ( rawMessageInfo: RawMediaMessageInfo | RawImagesMessageInfo, messageData: MediaMessageData | ImagesMessageData, ) => (messageData.sidebarCreation ? undefined : pushTypes.NOTIF), includedInRepliesCount: true, canBeSidebarSource: true, canBePinned: true, validator: rawMultimediaMessageInfoValidator, }); // 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 ad7899499..f9a5ae8f5 100644 --- a/lib/shared/messages/remove-members-message-spec.js +++ b/lib/shared/messages/remove-members-message-spec.js @@ -1,174 +1,189 @@ // @flow import invariant from 'invariant'; import type { CreateMessageInfoParams, MessageSpec, NotificationTextsParams, } from './message-spec.js'; import { joinResult } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import { type RawRemoveMembersMessageInfo, type RemoveMembersMessageData, type RemoveMembersMessageInfo, rawRemoveMembersMessageInfoValidator, } from '../../types/messages/remove-members.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText, pluralizeEntityText, } from '../../utils/entity-text.js'; import { values } from '../../utils/objects.js'; import { notifRobotextForMessageInfo } from '../notif-utils.js'; -export const removeMembersMessageSpec: MessageSpec< +type RemoveMembersMessageSpec = MessageSpec< RemoveMembersMessageData, RawRemoveMembersMessageInfo, RemoveMembersMessageInfo, -> = Object.freeze({ - messageContentForServerDB( +> & { + // We need to explicitly type this as non-optional so that + // it can be referenced from messageContentForClientDB below + +messageContentForServerDB: ( data: RemoveMembersMessageData | RawRemoveMembersMessageInfo, - ): string { - return JSON.stringify(data.removedUserIDs); - }, - - messageContentForClientDB(data: RawRemoveMembersMessageInfo): string { - return this.messageContentForServerDB(data); - }, - - rawMessageInfoFromServerDBRow(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), - }; - }, - - rawMessageInfoFromClientDB( - clientDBMessageInfo: ClientDBMessageInfo, - ): RawRemoveMembersMessageInfo { - const content = clientDBMessageInfo.content; - invariant( - content !== undefined && content !== null, - 'content must be defined for RemoveMembers', - ); - const rawRemoveMembersMessageInfo: RawRemoveMembersMessageInfo = { - type: messageTypes.REMOVE_MEMBERS, - id: clientDBMessageInfo.id, - threadID: clientDBMessageInfo.thread, - time: parseInt(clientDBMessageInfo.time), - creatorID: clientDBMessageInfo.user, - removedUserIDs: JSON.parse(content), - }; - return rawRemoveMembersMessageInfo; - }, - - 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 { - invariant(id, 'RawRemoveMembersMessageInfo needs id'); - return { ...messageData, id }; - }, - - robotext(messageInfo: RemoveMembersMessageInfo): EntityText { - const users = messageInfo.removedMembers; - invariant(users.length !== 0, 'added who??'); - - const creator = ET.user({ userInfo: messageInfo.creator }); - const removedUsers = pluralizeEntityText( - users.map(user => ET`${ET.user({ userInfo: user })}`), - ); + ) => string, + ... +}; + +export const removeMembersMessageSpec: RemoveMembersMessageSpec = Object.freeze( + { + messageContentForServerDB( + data: RemoveMembersMessageData | RawRemoveMembersMessageInfo, + ): string { + return JSON.stringify(data.removedUserIDs); + }, + + messageContentForClientDB(data: RawRemoveMembersMessageInfo): string { + return removeMembersMessageSpec.messageContentForServerDB(data); + }, + + rawMessageInfoFromServerDBRow(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), + }; + }, + + rawMessageInfoFromClientDB( + clientDBMessageInfo: ClientDBMessageInfo, + ): RawRemoveMembersMessageInfo { + const content = clientDBMessageInfo.content; + invariant( + content !== undefined && content !== null, + 'content must be defined for RemoveMembers', + ); + const rawRemoveMembersMessageInfo: RawRemoveMembersMessageInfo = { + type: messageTypes.REMOVE_MEMBERS, + id: clientDBMessageInfo.id, + threadID: clientDBMessageInfo.thread, + time: parseInt(clientDBMessageInfo.time), + creatorID: clientDBMessageInfo.user, + removedUserIDs: JSON.parse(content), + }; + return rawRemoveMembersMessageInfo; + }, + + 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 { + invariant(id, 'RawRemoveMembersMessageInfo needs id'); + return { ...messageData, id }; + }, + + robotext(messageInfo: RemoveMembersMessageInfo): EntityText { + const users = messageInfo.removedMembers; + invariant(users.length !== 0, 'added who??'); + + const creator = ET.user({ userInfo: messageInfo.creator }); + const removedUsers = pluralizeEntityText( + users.map(user => ET`${ET.user({ userInfo: user })}`), + ); - return ET`${creator} removed ${removedUsers}`; - }, + return ET`${creator} removed ${removedUsers}`; + }, + + async notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + params: NotificationTextsParams, + ): Promise { + const removedMembersObject: { [string]: RelativeUserInfo } = {}; + 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); - async notificationTexts( - messageInfos: $ReadOnlyArray, - threadInfo: ThreadInfo, - params: NotificationTextsParams, - ): Promise { - const removedMembersObject: { [string]: RelativeUserInfo } = {}; - for (const messageInfo of messageInfos) { + const mostRecentMessageInfo = messageInfos[0]; invariant( - messageInfo.type === messageTypes.REMOVE_MEMBERS, + mostRecentMessageInfo.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 { parentThreadInfo } = params; - const robotext = notifRobotextForMessageInfo( - mergedMessageInfo, - threadInfo, - parentThreadInfo, - ); - const merged = ET`${robotext} from ${ET.thread({ - display: 'shortName', - threadInfo, - })}`; - return { - merged, - title: threadInfo.uiName, - body: robotext, - }; - }, + const mergedMessageInfo = { ...mostRecentMessageInfo, removedMembers }; - notificationCollapseKey(rawMessageInfo: RawRemoveMembersMessageInfo): string { - return joinResult( - rawMessageInfo.type, - rawMessageInfo.threadID, - rawMessageInfo.creatorID, - ); - }, + const { parentThreadInfo } = params; + const robotext = notifRobotextForMessageInfo( + mergedMessageInfo, + threadInfo, + parentThreadInfo, + ); + const merged = ET`${robotext} from ${ET.thread({ + display: 'shortName', + threadInfo, + })}`; + return { + merged, + title: threadInfo.uiName, + body: robotext, + }; + }, + + notificationCollapseKey( + rawMessageInfo: RawRemoveMembersMessageInfo, + ): string { + return joinResult( + rawMessageInfo.type, + rawMessageInfo.threadID, + rawMessageInfo.creatorID, + ); + }, - userIDs(rawMessageInfo: RawRemoveMembersMessageInfo): $ReadOnlyArray { - return rawMessageInfo.removedUserIDs; - }, + userIDs( + rawMessageInfo: RawRemoveMembersMessageInfo, + ): $ReadOnlyArray { + return rawMessageInfo.removedUserIDs; + }, - canBeSidebarSource: true, + canBeSidebarSource: true, - canBePinned: false, + canBePinned: false, - validator: rawRemoveMembersMessageInfoValidator, -}); + validator: rawRemoveMembersMessageInfoValidator, + }, +); diff --git a/lib/shared/messages/restore-entry-message-spec.js b/lib/shared/messages/restore-entry-message-spec.js index 3227b8876..94cd84bf5 100644 --- a/lib/shared/messages/restore-entry-message-spec.js +++ b/lib/shared/messages/restore-entry-message-spec.js @@ -1,144 +1,153 @@ // @flow import invariant from 'invariant'; import { pushTypes, type MessageSpec } from './message-spec.js'; import { assertSingleMessageInfo } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import { type RawRestoreEntryMessageInfo, type RestoreEntryMessageData, type RestoreEntryMessageInfo, rawRestoreEntryMessageInfoValidator, } from '../../types/messages/restore-entry.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { prettyDate } from '../../utils/date-utils.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; -export const restoreEntryMessageSpec: MessageSpec< +type RestoreEntryMessageSpec = MessageSpec< RestoreEntryMessageData, RawRestoreEntryMessageInfo, RestoreEntryMessageInfo, -> = Object.freeze({ +> & { + // We need to explicitly type this as non-optional so that + // it can be referenced from messageContentForClientDB below + +messageContentForServerDB: ( + data: RestoreEntryMessageData | RawRestoreEntryMessageInfo, + ) => string, + ... +}; + +export const restoreEntryMessageSpec: RestoreEntryMessageSpec = Object.freeze({ messageContentForServerDB( data: RestoreEntryMessageData | RawRestoreEntryMessageInfo, ): string { return JSON.stringify({ entryID: data.entryID, date: data.date, text: data.text, }); }, messageContentForClientDB(data: RawRestoreEntryMessageInfo): string { - return this.messageContentForServerDB(data); + return restoreEntryMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(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, }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawRestoreEntryMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for RestoreEntry', ); const content = JSON.parse(clientDBMessageInfo.content); const rawRestoreEntryMessageInfo: RawRestoreEntryMessageInfo = { type: messageTypes.RESTORE_ENTRY, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, entryID: content.entryID, date: content.date, text: content.text, }; return rawRestoreEntryMessageInfo; }, 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 { invariant(id, 'RawRestoreEntryMessageInfo needs id'); return { ...messageData, id }; }, robotext(messageInfo: RestoreEntryMessageInfo): EntityText { const date = prettyDate(messageInfo.date); const creator = ET.user({ userInfo: messageInfo.creator }); const { text } = messageInfo; return ET`${creator} restored an event scheduled for ${date}: "${text}"`; }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const messageInfo = assertSingleMessageInfo(messageInfos); invariant( messageInfo.type === messageTypes.RESTORE_ENTRY, 'messageInfo should be messageTypes.RESTORE_ENTRY!', ); const thread = ET.thread({ display: 'shortName', threadInfo }); const creator = ET.user({ userInfo: messageInfo.creator }); const date = prettyDate(messageInfo.date); const prefix = ET`${creator}`; let body = ET`restored an event in ${thread}`; body = ET`${body} scheduled for ${date}: "${messageInfo.text}"`; const merged = ET`${prefix} ${body}`; return { merged, title: threadInfo.uiName, body, prefix, }; }, generatesNotifs: async () => pushTypes.NOTIF, canBeSidebarSource: true, canBePinned: false, validator: rawRestoreEntryMessageInfoValidator, }); diff --git a/lib/shared/messages/text-message-spec.js b/lib/shared/messages/text-message-spec.js index 20318af85..b37d29374 100644 --- a/lib/shared/messages/text-message-spec.js +++ b/lib/shared/messages/text-message-spec.js @@ -1,323 +1,332 @@ // @flow import invariant from 'invariant'; import * as SimpleMarkdown from 'simple-markdown'; import { pushTypes, type MessageSpec, type RawMessageInfoFromServerDBRowParams, type NotificationTextsParams, } from './message-spec.js'; import { assertSingleMessageInfo, joinResult } from './utils.js'; import { changeThreadSettingsActionTypes, useChangeThreadSettings, } from '../../actions/thread-actions.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import { type RawTextMessageInfo, type TextMessageData, type TextMessageInfo, rawTextMessageInfoValidator, } from '../../types/messages/text.js'; import type { MinimallyEncodedThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { NotifTexts } from '../../types/notif-types.js'; import { threadTypes } from '../../types/thread-types-enum.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { useDispatchActionPromise } from '../../utils/action-utils.js'; import { ET } from '../../utils/entity-text.js'; import { type ASTNode, type SingleASTNode, stripSpoilersFromNotifications, stripSpoilersFromMarkdownAST, } from '../markdown.js'; import { isUserMentioned, renderChatMentionsWithAltText, } from '../mention-utils.js'; import { notifTextsForSidebarCreation } from '../notif-utils.js'; import { threadIsGroupChat, extractNewMentionedParentMembers, } from '../thread-utils.js'; /** * 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: ASTNode): string => { if (Array.isArray(node)) { return node.map(rawTextFromMarkdownAST).join(''); } const { content, items } = node; if (content && typeof content === 'string') { return content; } else if (items) { return rawTextFromMarkdownAST(items); } else if (content) { return rawTextFromMarkdownAST(content); } 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< +type TextMessageSpec = MessageSpec< TextMessageData, RawTextMessageInfo, TextMessageInfo, -> = Object.freeze({ +> & { + // We need to explicitly type this as non-optional so that + // it can be referenced from messageContentForClientDB below + +messageContentForServerDB: ( + data: TextMessageData | RawTextMessageInfo, + ) => string, + ... +}; + +export const textMessageSpec: TextMessageSpec = Object.freeze({ messageContentForServerDB( data: TextMessageData | RawTextMessageInfo, ): string { return data.text; }, messageContentForClientDB(data: RawTextMessageInfo): string { - return this.messageContentForServerDB(data); + return textMessageSpec.messageContentForServerDB(data); }, messageTitle({ messageInfo, markdownRules }) { const { text } = messageInfo; const parser = SimpleMarkdown.parserFor(markdownRules); const ast = stripSpoilersFromMarkdownAST( parser(text, { disableAutoBlockNewlines: true }), ); return ET`${getFirstNonQuotedRawLine(ast).trim()}`; }, rawMessageInfoFromServerDBRow( row: Object, params: RawMessageInfoFromServerDBRowParams, ): RawTextMessageInfo { let 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 = { ...rawTextMessageInfo, localID: params.localID }; } return rawTextMessageInfo; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawTextMessageInfo { let rawTextMessageInfo: RawTextMessageInfo = { type: messageTypes.TEXT, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, text: clientDBMessageInfo.content ?? '', }; if (clientDBMessageInfo.local_id) { rawTextMessageInfo = { ...rawTextMessageInfo, localID: clientDBMessageInfo.local_id, }; } if (clientDBMessageInfo.id !== clientDBMessageInfo.local_id) { rawTextMessageInfo = { ...rawTextMessageInfo, id: clientDBMessageInfo.id, }; } return rawTextMessageInfo; }, createMessageInfo( rawMessageInfo: RawTextMessageInfo, creator: RelativeUserInfo, ): TextMessageInfo { let messageInfo: TextMessageInfo = { type: messageTypes.TEXT, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, text: rawMessageInfo.text, }; if (rawMessageInfo.id) { messageInfo = { ...messageInfo, id: rawMessageInfo.id }; } if (rawMessageInfo.localID) { messageInfo = { ...messageInfo, localID: rawMessageInfo.localID }; } return messageInfo; }, rawMessageInfoFromMessageData( messageData: TextMessageData, id: ?string, ): RawTextMessageInfo { const { sidebarCreation, ...rest } = messageData; if (id) { return { ...rest, id }; } else { return { ...rest }; } }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, params: NotificationTextsParams, ): Promise { // We special-case sidebarCreations. Usually we don't send any notifs in // that case to avoid a double-notif, but we need to update the original // notif if somebody was @-tagged in this message if ( messageInfos.length === 3 && messageInfos[2].type === messageTypes.SIDEBAR_SOURCE && messageInfos[1].type === messageTypes.CREATE_SIDEBAR ) { const sidebarSourceMessageInfo = messageInfos[2]; const createSidebarMessageInfo = messageInfos[1]; const sourceMessage = messageInfos[2].sourceMessage; const { username } = params.notifTargetUserInfo; if (!username) { // If we couldn't fetch the username for some reason, we won't be able // to extract @-mentions anyways, so we'll give up on updating the notif return null; } if ( sourceMessage.type === messageTypes.TEXT && isUserMentioned(username, sourceMessage.text) ) { // If the notif target was already mentioned in the source message, // there's no need to update the notif return null; } const messageInfo = messageInfos[0]; invariant( messageInfo.type === messageTypes.TEXT, 'messageInfo should be messageTypes.TEXT!', ); if (!isUserMentioned(username, messageInfo.text)) { // We only need to update the notif if the notif target is mentioned return null; } return notifTextsForSidebarCreation({ createSidebarMessageInfo, sidebarSourceMessageInfo, firstSidebarMessageInfo: messageInfo, threadInfo, params, }); } const messageInfo = assertSingleMessageInfo(messageInfos); invariant( messageInfo.type === messageTypes.TEXT, 'messageInfo should be messageTypes.TEXT!', ); const transformedNotificationText = stripSpoilersFromNotifications( renderChatMentionsWithAltText(messageInfo.text), ); if (!threadInfo.name && !threadIsGroupChat(threadInfo)) { const thread = ET.thread({ display: 'uiName', threadInfo }); return { merged: ET`${thread}: ${transformedNotificationText}`, body: transformedNotificationText, title: threadInfo.uiName, }; } else { const creator = ET.user({ userInfo: messageInfo.creator }); const thread = ET.thread({ display: 'shortName', threadInfo }); return { merged: ET`${creator} to ${thread}: ${transformedNotificationText}`, body: transformedNotificationText, title: threadInfo.uiName, prefix: ET`${creator}:`, }; } }, notificationCollapseKey( rawMessageInfo: RawTextMessageInfo, messageData: TextMessageData, ): ?string { if (!messageData.sidebarCreation) { return null; } return joinResult(messageTypes.CREATE_SIDEBAR, rawMessageInfo.threadID); }, generatesNotifs: async () => pushTypes.NOTIF, includedInRepliesCount: true, useCreationSideEffectsFunc: () => { const dispatchActionPromise = useDispatchActionPromise(); const callChangeThreadSettings = useChangeThreadSettings(); return async ( messageInfo: RawTextMessageInfo, threadInfo: ThreadInfo | MinimallyEncodedThreadInfo, parentThreadInfo: ?ThreadInfo | ?MinimallyEncodedThreadInfo, ) => { if (threadInfo.type !== threadTypes.SIDEBAR) { return; } invariant(parentThreadInfo, 'all sidebars should have a parent thread'); const mentionedNewMembers = extractNewMentionedParentMembers( messageInfo.text, threadInfo, parentThreadInfo, ); if (mentionedNewMembers.length === 0) { return; } const newMemberIDs = mentionedNewMembers.map(({ id }) => id); const addMembersPromise = callChangeThreadSettings({ threadID: threadInfo.id, changes: { newMemberIDs }, }); dispatchActionPromise(changeThreadSettingsActionTypes, addMembersPromise); await addMembersPromise; }; }, canBeSidebarSource: true, canBePinned: true, validator: rawTextMessageInfoValidator, }); diff --git a/lib/shared/messages/toggle-pin-message-spec.js b/lib/shared/messages/toggle-pin-message-spec.js index 54c3633d4..0d977f205 100644 --- a/lib/shared/messages/toggle-pin-message-spec.js +++ b/lib/shared/messages/toggle-pin-message-spec.js @@ -1,177 +1,186 @@ // @flow import invariant from 'invariant'; import type { MessageSpec, RobotextParams, RawMessageInfoFromServerDBRowParams, } from './message-spec.js'; import type { PlatformDetails } from '../../types/device-types'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ClientDBMessageInfo } from '../../types/message-types.js'; import { type TogglePinMessageData, type TogglePinMessageInfo, type RawTogglePinMessageInfo, rawTogglePinMessageInfoValidator, } from '../../types/messages/toggle-pin.js'; import type { RawUnsupportedMessageInfo } from '../../types/messages/unsupported'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; import { getPinnedContentFromClientDBMessageInfo } from '../../utils/message-ops-utils.js'; import { getPinnedContentFromMessage } from '../message-utils.js'; import { hasMinCodeVersion } from '../version-utils.js'; -export const togglePinMessageSpec: MessageSpec< +type TogglePinMessageSpec = MessageSpec< TogglePinMessageData, RawTogglePinMessageInfo, TogglePinMessageInfo, -> = Object.freeze({ +> & { + // We need to explicitly type this as non-optional so that + // it can be referenced from messageContentForClientDB below + +messageContentForServerDB: ( + data: TogglePinMessageData | RawTogglePinMessageInfo, + ) => string, + ... +}; + +export const togglePinMessageSpec: TogglePinMessageSpec = Object.freeze({ messageContentForServerDB( data: TogglePinMessageData | RawTogglePinMessageInfo, ): string { return JSON.stringify({ action: data.action, threadID: data.threadID, targetMessageID: data.targetMessageID, }); }, messageContentForClientDB(data: RawTogglePinMessageInfo): string { - return this.messageContentForServerDB(data); + return togglePinMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow( row: Object, params: RawMessageInfoFromServerDBRowParams, ): RawTogglePinMessageInfo { const content = JSON.parse(row.content); const { derivedMessages } = params; invariant(derivedMessages, 'Derived messages should be provided'); const targetMessage = derivedMessages.get(content.targetMessageID); invariant(targetMessage, 'targetMessage should be defined'); return { type: messageTypes.TOGGLE_PIN, id: row.id.toString(), threadID: row.threadID.toString(), targetMessageID: content.targetMessageID.toString(), action: content.action, pinnedContent: getPinnedContentFromMessage(targetMessage), time: row.time, creatorID: row.creatorID.toString(), }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawTogglePinMessageInfo { invariant( clientDBMessageInfo.content !== undefined && clientDBMessageInfo.content !== null, 'content must be defined for TogglePin', ); const content = JSON.parse(clientDBMessageInfo.content); const pinnedContent = getPinnedContentFromClientDBMessageInfo(clientDBMessageInfo); const rawTogglePinMessageInfo: RawTogglePinMessageInfo = { type: messageTypes.TOGGLE_PIN, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, targetMessageID: content.targetMessageID, action: content.action, pinnedContent, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, }; return rawTogglePinMessageInfo; }, createMessageInfo( rawMessageInfo: RawTogglePinMessageInfo, creator: RelativeUserInfo, ): TogglePinMessageInfo { return { type: messageTypes.TOGGLE_PIN, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, targetMessageID: rawMessageInfo.targetMessageID, action: rawMessageInfo.action, pinnedContent: rawMessageInfo.pinnedContent, creator, time: rawMessageInfo.time, }; }, rawMessageInfoFromMessageData( messageData: TogglePinMessageData, id: ?string, ): RawTogglePinMessageInfo { invariant(id, 'RawTogglePinMessageInfo needs id'); return { ...messageData, id }; }, robotext( messageInfo: TogglePinMessageInfo, params: RobotextParams, ): EntityText { const creator = ET.user({ userInfo: messageInfo.creator }); const action = messageInfo.action === 'pin' ? 'pinned' : 'unpinned'; const pinnedContent = messageInfo.pinnedContent; const preposition = messageInfo.action === 'pin' ? 'in' : 'from'; return ET`${creator} ${action} ${pinnedContent} ${preposition} ${ET.thread({ display: 'alwaysDisplayShortName', threadID: messageInfo.threadID, threadType: params.threadInfo?.type, parentThreadID: params.threadInfo?.parentThreadID, })}`; }, shimUnsupportedMessageInfo( rawMessageInfo: RawTogglePinMessageInfo, platformDetails: ?PlatformDetails, ): RawTogglePinMessageInfo | RawUnsupportedMessageInfo { if (hasMinCodeVersion(platformDetails, { native: 209 })) { 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: 'toggled a message pin', unsupportedMessageInfo: rawMessageInfo, }; }, unshimMessageInfo( unwrapped: RawTogglePinMessageInfo, ): RawTogglePinMessageInfo { return unwrapped; }, canBeSidebarSource: false, canBePinned: false, parseDerivedMessages(row: Object, requiredIDs: Set): void { try { const content = JSON.parse(row.content); requiredIDs.add(content.targetMessageID); } catch (e) { console.error( `Error parsing content of message with id ${row.id}: ${e.message}`, ); } }, validator: rawTogglePinMessageInfoValidator, }); diff --git a/lib/shared/messages/update-relationship-message-spec.js b/lib/shared/messages/update-relationship-message-spec.js index 41ad0d280..db7be299e 100644 --- a/lib/shared/messages/update-relationship-message-spec.js +++ b/lib/shared/messages/update-relationship-message-spec.js @@ -1,162 +1,177 @@ // @flow import invariant from 'invariant'; import { pushTypes, type CreateMessageInfoParams, type MessageSpec, } from './message-spec.js'; import { assertSingleMessageInfo } from './utils.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { MessageInfo, ClientDBMessageInfo, } from '../../types/message-types.js'; import { type RawUpdateRelationshipMessageInfo, type UpdateRelationshipMessageData, type UpdateRelationshipMessageInfo, rawUpdateRelationshipMessageInfoValidator, } from '../../types/messages/update-relationship.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { ThreadInfo } from '../../types/thread-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { ET, type EntityText } from '../../utils/entity-text.js'; -export const updateRelationshipMessageSpec: MessageSpec< +type UpdateRelationshipMessageSpec = MessageSpec< UpdateRelationshipMessageData, RawUpdateRelationshipMessageInfo, UpdateRelationshipMessageInfo, -> = Object.freeze({ - messageContentForServerDB( +> & { + // We need to explicitly type this as non-optional so that + // it can be referenced from messageContentForClientDB below + +messageContentForServerDB: ( data: UpdateRelationshipMessageData | RawUpdateRelationshipMessageInfo, - ): string { - return JSON.stringify({ - operation: data.operation, - targetID: data.targetID, - }); - }, - - messageContentForClientDB(data: RawUpdateRelationshipMessageInfo): string { - return this.messageContentForServerDB(data); - }, - - rawMessageInfoFromServerDBRow(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, - }; - }, - - rawMessageInfoFromClientDB( - clientDBMessageInfo: ClientDBMessageInfo, - ): RawUpdateRelationshipMessageInfo { - invariant( - clientDBMessageInfo.content !== undefined && - clientDBMessageInfo.content !== null, - 'content must be defined for UpdateRelationship', - ); - const content = JSON.parse(clientDBMessageInfo.content); - const rawUpdateRelationshipMessageInfo: RawUpdateRelationshipMessageInfo = { - type: messageTypes.UPDATE_RELATIONSHIP, - id: clientDBMessageInfo.id, - threadID: clientDBMessageInfo.thread, - time: parseInt(clientDBMessageInfo.time), - creatorID: clientDBMessageInfo.user, - targetID: content.targetID, - operation: content.operation, - }; - return rawUpdateRelationshipMessageInfo; - }, - - 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 { - invariant(id, 'RawUpdateRelationshipMessageInfo needs id'); - return { ...messageData, id }; - }, - - // ESLint doesn't recognize that invariant always throws - // eslint-disable-next-line consistent-return - robotext(messageInfo: UpdateRelationshipMessageInfo): EntityText { - const creator = ET.user({ userInfo: messageInfo.creator }); - if (messageInfo.operation === 'request_sent') { - const target = ET.user({ userInfo: messageInfo.target }); - return ET`${creator} sent ${target} a friend request`; - } else if (messageInfo.operation === 'request_accepted') { - const targetPossessive = ET.user({ - userInfo: messageInfo.target, - possessive: true, + ) => string, + ... +}; + +export const updateRelationshipMessageSpec: UpdateRelationshipMessageSpec = + Object.freeze({ + messageContentForServerDB( + data: UpdateRelationshipMessageData | RawUpdateRelationshipMessageInfo, + ): string { + return JSON.stringify({ + operation: data.operation, + targetID: data.targetID, }); - return ET`${creator} accepted ${targetPossessive} friend request`; - } - invariant( - false, - `Invalid operation ${messageInfo.operation} ` + - `of message with type ${messageInfo.type}`, - ); - }, - - unshimMessageInfo( - unwrapped: RawUpdateRelationshipMessageInfo, - ): RawUpdateRelationshipMessageInfo { - return unwrapped; - }, - - async notificationTexts( - messageInfos: $ReadOnlyArray, - threadInfo: ThreadInfo, - ): Promise { - const messageInfo = assertSingleMessageInfo(messageInfos); - const creator = ET.user({ userInfo: messageInfo.creator }); - const prefix = ET`${creator}`; - const title = threadInfo.uiName; - const body = - messageInfo.operation === 'request_sent' - ? 'sent you a friend request' - : 'accepted your friend request'; - const merged = ET`${prefix} ${body}`; - return { - merged, - body, - title, - prefix, - }; - }, - - generatesNotifs: async () => pushTypes.NOTIF, - - canBeSidebarSource: true, - - canBePinned: false, - - validator: rawUpdateRelationshipMessageInfoValidator, -}); + }, + + messageContentForClientDB(data: RawUpdateRelationshipMessageInfo): string { + return updateRelationshipMessageSpec.messageContentForServerDB(data); + }, + + rawMessageInfoFromServerDBRow( + 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, + }; + }, + + rawMessageInfoFromClientDB( + clientDBMessageInfo: ClientDBMessageInfo, + ): RawUpdateRelationshipMessageInfo { + invariant( + clientDBMessageInfo.content !== undefined && + clientDBMessageInfo.content !== null, + 'content must be defined for UpdateRelationship', + ); + const content = JSON.parse(clientDBMessageInfo.content); + const rawUpdateRelationshipMessageInfo: RawUpdateRelationshipMessageInfo = + { + type: messageTypes.UPDATE_RELATIONSHIP, + id: clientDBMessageInfo.id, + threadID: clientDBMessageInfo.thread, + time: parseInt(clientDBMessageInfo.time), + creatorID: clientDBMessageInfo.user, + targetID: content.targetID, + operation: content.operation, + }; + return rawUpdateRelationshipMessageInfo; + }, + + 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 { + invariant(id, 'RawUpdateRelationshipMessageInfo needs id'); + return { ...messageData, id }; + }, + + // ESLint doesn't recognize that invariant always throws + // eslint-disable-next-line consistent-return + robotext(messageInfo: UpdateRelationshipMessageInfo): EntityText { + const creator = ET.user({ userInfo: messageInfo.creator }); + if (messageInfo.operation === 'request_sent') { + const target = ET.user({ userInfo: messageInfo.target }); + return ET`${creator} sent ${target} a friend request`; + } else if (messageInfo.operation === 'request_accepted') { + const targetPossessive = ET.user({ + userInfo: messageInfo.target, + possessive: true, + }); + return ET`${creator} accepted ${targetPossessive} friend request`; + } + invariant( + false, + `Invalid operation ${messageInfo.operation} ` + + `of message with type ${messageInfo.type}`, + ); + }, + + unshimMessageInfo( + unwrapped: RawUpdateRelationshipMessageInfo, + ): RawUpdateRelationshipMessageInfo { + return unwrapped; + }, + + async notificationTexts( + messageInfos: $ReadOnlyArray, + threadInfo: ThreadInfo, + ): Promise { + const messageInfo = assertSingleMessageInfo(messageInfos); + const creator = ET.user({ userInfo: messageInfo.creator }); + const prefix = ET`${creator}`; + const title = threadInfo.uiName; + const body = + messageInfo.operation === 'request_sent' + ? 'sent you a friend request' + : 'accepted your friend request'; + const merged = ET`${prefix} ${body}`; + return { + merged, + body, + title, + prefix, + }; + }, + + generatesNotifs: async () => pushTypes.NOTIF, + + canBeSidebarSource: true, + + canBePinned: false, + + validator: rawUpdateRelationshipMessageInfoValidator, + });