diff --git a/lib/shared/messages/add-members-message-spec.js b/lib/shared/messages/add-members-message-spec.js --- a/lib/shared/messages/add-members-message-spec.js +++ b/lib/shared/messages/add-members-message-spec.js @@ -30,11 +30,20 @@ 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 { @@ -42,7 +51,7 @@ }, messageContentForClientDB(data: RawAddMembersMessageInfo): string { - return this.messageContentForServerDB(data); + return addMembersMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawAddMembersMessageInfo { diff --git a/lib/shared/messages/change-role-message-spec.js b/lib/shared/messages/change-role-message-spec.js --- a/lib/shared/messages/change-role-message-spec.js +++ b/lib/shared/messages/change-role-message-spec.js @@ -37,11 +37,20 @@ 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 { @@ -53,7 +62,7 @@ }, messageContentForClientDB(data: RawChangeRoleMessageInfo): string { - return this.messageContentForServerDB(data); + return changeRoleMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawChangeRoleMessageInfo { diff --git a/lib/shared/messages/change-settings-message-spec.js b/lib/shared/messages/change-settings-message-spec.js --- a/lib/shared/messages/change-settings-message-spec.js +++ b/lib/shared/messages/change-settings-message-spec.js @@ -29,162 +29,172 @@ 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 --- a/lib/shared/messages/create-entry-message-spec.js +++ b/lib/shared/messages/create-entry-message-spec.js @@ -22,11 +22,20 @@ 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 { @@ -38,7 +47,7 @@ }, messageContentForClientDB(data: RawCreateEntryMessageInfo): string { - return this.messageContentForServerDB(data); + return createEntryMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawCreateEntryMessageInfo { diff --git a/lib/shared/messages/create-sidebar-message-spec.js b/lib/shared/messages/create-sidebar-message-spec.js --- a/lib/shared/messages/create-sidebar-message-spec.js +++ b/lib/shared/messages/create-sidebar-message-spec.js @@ -31,184 +31,199 @@ } 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 --- a/lib/shared/messages/create-sub-thread-message-spec.js +++ b/lib/shared/messages/create-sub-thread-message-spec.js @@ -29,143 +29,153 @@ 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 --- a/lib/shared/messages/create-thread-message-spec.js +++ b/lib/shared/messages/create-thread-message-spec.js @@ -31,11 +31,20 @@ 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 { @@ -43,7 +52,7 @@ }, messageContentForClientDB(data: RawCreateThreadMessageInfo): string { - return this.messageContentForServerDB(data); + return createThreadMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawCreateThreadMessageInfo { diff --git a/lib/shared/messages/delete-entry-message-spec.js b/lib/shared/messages/delete-entry-message-spec.js --- a/lib/shared/messages/delete-entry-message-spec.js +++ b/lib/shared/messages/delete-entry-message-spec.js @@ -21,11 +21,20 @@ 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 { @@ -37,7 +46,7 @@ }, messageContentForClientDB(data: RawDeleteEntryMessageInfo): string { - return this.messageContentForServerDB(data); + return deleteEntryMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawDeleteEntryMessageInfo { diff --git a/lib/shared/messages/edit-entry-message-spec.js b/lib/shared/messages/edit-entry-message-spec.js --- a/lib/shared/messages/edit-entry-message-spec.js +++ b/lib/shared/messages/edit-entry-message-spec.js @@ -22,11 +22,20 @@ 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 { @@ -38,7 +47,7 @@ }, messageContentForClientDB(data: RawEditEntryMessageInfo): string { - return this.messageContentForServerDB(data); + return editEntryMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawEditEntryMessageInfo { diff --git a/lib/shared/messages/multimedia-message-spec.js b/lib/shared/messages/multimedia-message-spec.js --- a/lib/shared/messages/multimedia-message-spec.js +++ b/lib/shared/messages/multimedia-message-spec.js @@ -53,11 +53,24 @@ 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 @@ -77,7 +90,7 @@ messageContentForClientDB( data: RawMediaMessageInfo | RawImagesMessageInfo, ): string { - return this.messageContentForServerDB(data); + return multimediaMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromClientDB( diff --git a/lib/shared/messages/remove-members-message-spec.js b/lib/shared/messages/remove-members-message-spec.js --- a/lib/shared/messages/remove-members-message-spec.js +++ b/lib/shared/messages/remove-members-message-spec.js @@ -30,145 +30,160 @@ 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 --- a/lib/shared/messages/restore-entry-message-spec.js +++ b/lib/shared/messages/restore-entry-message-spec.js @@ -21,11 +21,20 @@ 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 { @@ -37,7 +46,7 @@ }, messageContentForClientDB(data: RawRestoreEntryMessageInfo): string { - return this.messageContentForServerDB(data); + return restoreEntryMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow(row: Object): RawRestoreEntryMessageInfo { diff --git a/lib/shared/messages/text-message-spec.js b/lib/shared/messages/text-message-spec.js --- a/lib/shared/messages/text-message-spec.js +++ b/lib/shared/messages/text-message-spec.js @@ -89,11 +89,20 @@ 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 { @@ -101,7 +110,7 @@ }, messageContentForClientDB(data: RawTextMessageInfo): string { - return this.messageContentForServerDB(data); + return textMessageSpec.messageContentForServerDB(data); }, messageTitle({ messageInfo, markdownRules }) { diff --git a/lib/shared/messages/toggle-pin-message-spec.js b/lib/shared/messages/toggle-pin-message-spec.js --- a/lib/shared/messages/toggle-pin-message-spec.js +++ b/lib/shared/messages/toggle-pin-message-spec.js @@ -23,11 +23,20 @@ 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 { @@ -39,7 +48,7 @@ }, messageContentForClientDB(data: RawTogglePinMessageInfo): string { - return this.messageContentForServerDB(data); + return togglePinMessageSpec.messageContentForServerDB(data); }, rawMessageInfoFromServerDBRow( diff --git a/lib/shared/messages/update-relationship-message-spec.js b/lib/shared/messages/update-relationship-message-spec.js --- a/lib/shared/messages/update-relationship-message-spec.js +++ b/lib/shared/messages/update-relationship-message-spec.js @@ -24,139 +24,154 @@ 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, + });