diff --git a/lib/shared/messages/add-members-message-spec.js b/lib/shared/messages/add-members-message-spec.js index 8ed62fda9..dc114c53a 100644 --- a/lib/shared/messages/add-members-message-spec.js +++ b/lib/shared/messages/add-members-message-spec.js @@ -1,183 +1,227 @@ // @flow import invariant from 'invariant'; import type { CreateMessageInfoParams, MessageSpec, NotificationTextsParams, + MergeRobotextMessageItemResult, } from './message-spec.js'; import { joinResult } from './utils.js'; +import type { RobotextChatMessageInfoItem } from '../../selectors/chat-selectors.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ClientDBMessageInfo, MessageInfo, } from '../../types/message-types.js'; import { type AddMembersMessageData, type AddMembersMessageInfo, type RawAddMembersMessageInfo, rawAddMembersMessageInfoValidator, } from '../../types/messages/add-members.js'; import type { ThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { type EntityText, ET, pluralizeEntityText, } from '../../utils/entity-text.js'; import { values } from '../../utils/objects.js'; import { notifRobotextForMessageInfo } from '../notif-utils.js'; +function getAddMembersRobotext(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}`; +} + type AddMembersMessageSpec = MessageSpec< AddMembersMessageData, RawAddMembersMessageInfo, AddMembersMessageInfo, > & { // 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 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}`; + return getAddMembersRobotext(messageInfo); }, 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, + + mergeIntoPrecedingRobotextMessageItem( + messageInfo: AddMembersMessageInfo, + precedingMessageInfoItem: RobotextChatMessageInfoItem, + ): MergeRobotextMessageItemResult { + if (precedingMessageInfoItem.messageInfos.length === 0) { + return { shouldMerge: false }; + } + + const addedMembers = []; + const creatorID = messageInfo.creator.id; + for (const precedingMessageInfo of precedingMessageInfoItem.messageInfos) { + if ( + precedingMessageInfo.type !== messageTypes.ADD_MEMBERS || + precedingMessageInfo.creator.id !== creatorID + ) { + return { shouldMerge: false }; + } + for (const addedMember of precedingMessageInfo.addedMembers) { + addedMembers.push(addedMember); + } + } + + const messageInfos = [ + messageInfo, + ...precedingMessageInfoItem.messageInfos, + ]; + const newRobotext = getAddMembersRobotext({ + ...messageInfo, + addedMembers, + }); + const mergedItem = { + ...precedingMessageInfoItem, + messageInfos, + robotext: newRobotext, + }; + return { shouldMerge: true, item: mergedItem }; + }, }); diff --git a/lib/shared/messages/join-thread-message-spec.js b/lib/shared/messages/join-thread-message-spec.js index cca5ee26c..0535e44d2 100644 --- a/lib/shared/messages/join-thread-message-spec.js +++ b/lib/shared/messages/join-thread-message-spec.js @@ -1,127 +1,173 @@ // @flow import invariant from 'invariant'; -import type { MessageSpec, RobotextParams } from './message-spec.js'; +import type { + MessageSpec, + RobotextParams, + MergeRobotextMessageItemResult, +} from './message-spec.js'; import { joinResult } from './utils.js'; +import type { RobotextChatMessageInfoItem } from '../../selectors/chat-selectors.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ClientDBMessageInfo, MessageInfo, } from '../../types/message-types.js'; import { type JoinThreadMessageData, type JoinThreadMessageInfo, type RawJoinThreadMessageInfo, rawJoinThreadMessageInfoValidator, } from '../../types/messages/join-thread.js'; import type { ThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { type EntityText, ET, pluralizeEntityText, } from '../../utils/entity-text.js'; import { values } from '../../utils/objects.js'; +function getJoinThreadRobotext( + joinerString: EntityText, + threadID: string, + params: RobotextParams, +): EntityText { + return ET`${joinerString} joined ${ET.thread({ + display: 'alwaysDisplayShortName', + threadID, + threadType: params.threadInfo?.type, + parentThreadID: params.threadInfo?.parentThreadID, + })}`; +} + export const joinThreadMessageSpec: MessageSpec< JoinThreadMessageData, RawJoinThreadMessageInfo, JoinThreadMessageInfo, > = Object.freeze({ rawMessageInfoFromServerDBRow(row: Object): RawJoinThreadMessageInfo { return { type: messageTypes.JOIN_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawJoinThreadMessageInfo { const rawJoinThreadMessageInfo: RawJoinThreadMessageInfo = { type: messageTypes.JOIN_THREAD, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, }; return rawJoinThreadMessageInfo; }, createMessageInfo( rawMessageInfo: RawJoinThreadMessageInfo, creator: RelativeUserInfo, ): JoinThreadMessageInfo { return { type: messageTypes.JOIN_THREAD, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, }; }, rawMessageInfoFromMessageData( messageData: JoinThreadMessageData, id: ?string, ): RawJoinThreadMessageInfo { invariant(id, 'RawJoinThreadMessageInfo needs id'); return { ...messageData, id }; }, robotext( messageInfo: JoinThreadMessageInfo, params: RobotextParams, ): EntityText { const creator = ET.user({ userInfo: messageInfo.creator }); - return ET`${creator} joined ${ET.thread({ - display: 'alwaysDisplayShortName', - threadID: messageInfo.threadID, - threadType: params.threadInfo?.type, - parentThreadID: params.threadInfo?.parentThreadID, - })}`; + return getJoinThreadRobotext(ET`${creator}`, messageInfo.threadID, params); }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const joinerArray: { [string]: RelativeUserInfo } = {}; for (const messageInfo of messageInfos) { invariant( messageInfo.type === messageTypes.JOIN_THREAD, 'messageInfo should be messageTypes.JOIN_THREAD!', ); joinerArray[messageInfo.creator.id] = messageInfo.creator; } const joiners = values(joinerArray); const joiningUsers = pluralizeEntityText( joiners.map(user => ET`${ET.user({ userInfo: user })}`), ); const body = ET`${joiningUsers} joined`; const thread = ET.thread({ display: 'shortName', threadInfo }); const merged = ET`${body} ${thread}`; return { merged, title: threadInfo.uiName, body, }; }, notificationCollapseKey(rawMessageInfo: RawJoinThreadMessageInfo): string { return joinResult(rawMessageInfo.type, rawMessageInfo.threadID); }, canBeSidebarSource: true, canBePinned: false, validator: rawJoinThreadMessageInfoValidator, + + mergeIntoPrecedingRobotextMessageItem( + messageInfo: JoinThreadMessageInfo, + precedingMessageInfoItem: RobotextChatMessageInfoItem, + params: RobotextParams, + ): MergeRobotextMessageItemResult { + if (precedingMessageInfoItem.messageInfos.length === 0) { + return { shouldMerge: false }; + } + for (const precedingMessageInfo of precedingMessageInfoItem.messageInfos) { + if (precedingMessageInfo.type !== messageTypes.JOIN_THREAD) { + return { shouldMerge: false }; + } + } + const messageInfos = [ + messageInfo, + ...precedingMessageInfoItem.messageInfos, + ]; + const joiningUsers = pluralizeEntityText( + messageInfos.map(info => ET`${ET.user({ userInfo: info.creator })}`), + ); + const newRobotext = getJoinThreadRobotext( + joiningUsers, + messageInfo.threadID, + params, + ); + const mergedItem = { + ...precedingMessageInfoItem, + messageInfos, + robotext: newRobotext, + }; + return { shouldMerge: true, item: mergedItem }; + }, }); diff --git a/lib/shared/messages/leave-thread-message-spec.js b/lib/shared/messages/leave-thread-message-spec.js index 8fe5248ff..f877e6333 100644 --- a/lib/shared/messages/leave-thread-message-spec.js +++ b/lib/shared/messages/leave-thread-message-spec.js @@ -1,127 +1,173 @@ // @flow import invariant from 'invariant'; -import type { MessageSpec, RobotextParams } from './message-spec.js'; +import type { + MessageSpec, + RobotextParams, + MergeRobotextMessageItemResult, +} from './message-spec.js'; import { joinResult } from './utils.js'; +import type { RobotextChatMessageInfoItem } from '../../selectors/chat-selectors.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ClientDBMessageInfo, MessageInfo, } from '../../types/message-types.js'; import { type LeaveThreadMessageData, type LeaveThreadMessageInfo, type RawLeaveThreadMessageInfo, rawLeaveThreadMessageInfoValidator, } from '../../types/messages/leave-thread.js'; import type { ThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { type EntityText, ET, pluralizeEntityText, } from '../../utils/entity-text.js'; import { values } from '../../utils/objects.js'; +function getLeaveThreadRobotext( + leaverString: EntityText, + threadID: string, + params: RobotextParams, +): EntityText { + return ET`${leaverString} left ${ET.thread({ + display: 'alwaysDisplayShortName', + threadID, + threadType: params.threadInfo?.type, + parentThreadID: params.threadInfo?.parentThreadID, + })}`; +} + export const leaveThreadMessageSpec: MessageSpec< LeaveThreadMessageData, RawLeaveThreadMessageInfo, LeaveThreadMessageInfo, > = Object.freeze({ rawMessageInfoFromServerDBRow(row: Object): RawLeaveThreadMessageInfo { return { type: messageTypes.LEAVE_THREAD, id: row.id.toString(), threadID: row.threadID.toString(), time: row.time, creatorID: row.creatorID.toString(), }; }, rawMessageInfoFromClientDB( clientDBMessageInfo: ClientDBMessageInfo, ): RawLeaveThreadMessageInfo { const rawLeaveThreadMessageInfo: RawLeaveThreadMessageInfo = { type: messageTypes.LEAVE_THREAD, id: clientDBMessageInfo.id, threadID: clientDBMessageInfo.thread, time: parseInt(clientDBMessageInfo.time), creatorID: clientDBMessageInfo.user, }; return rawLeaveThreadMessageInfo; }, createMessageInfo( rawMessageInfo: RawLeaveThreadMessageInfo, creator: RelativeUserInfo, ): LeaveThreadMessageInfo { return { type: messageTypes.LEAVE_THREAD, id: rawMessageInfo.id, threadID: rawMessageInfo.threadID, creator, time: rawMessageInfo.time, }; }, rawMessageInfoFromMessageData( messageData: LeaveThreadMessageData, id: ?string, ): RawLeaveThreadMessageInfo { invariant(id, 'RawLeaveThreadMessageInfo needs id'); return { ...messageData, id }; }, robotext( messageInfo: LeaveThreadMessageInfo, params: RobotextParams, ): EntityText { const creator = ET.user({ userInfo: messageInfo.creator }); - return ET`${creator} left ${ET.thread({ - display: 'alwaysDisplayShortName', - threadID: messageInfo.threadID, - threadType: params.threadInfo?.type, - parentThreadID: params.threadInfo?.parentThreadID, - })}`; + return getLeaveThreadRobotext(ET`${creator}`, messageInfo.threadID, params); }, async notificationTexts( messageInfos: $ReadOnlyArray, threadInfo: ThreadInfo, ): Promise { const leaverBeavers: { [string]: RelativeUserInfo } = {}; for (const messageInfo of messageInfos) { invariant( messageInfo.type === messageTypes.LEAVE_THREAD, 'messageInfo should be messageTypes.LEAVE_THREAD!', ); leaverBeavers[messageInfo.creator.id] = messageInfo.creator; } const leavers = values(leaverBeavers); const leavingUsers = pluralizeEntityText( leavers.map(user => ET`${ET.user({ userInfo: user })}`), ); const body = ET`${leavingUsers} left`; const thread = ET.thread({ display: 'shortName', threadInfo }); const merged = ET`${body} ${thread}`; return { merged, title: threadInfo.uiName, body, }; }, notificationCollapseKey(rawMessageInfo: RawLeaveThreadMessageInfo): string { return joinResult(rawMessageInfo.type, rawMessageInfo.threadID); }, canBeSidebarSource: true, canBePinned: false, validator: rawLeaveThreadMessageInfoValidator, + + mergeIntoPrecedingRobotextMessageItem( + messageInfo: LeaveThreadMessageInfo, + precedingMessageInfoItem: RobotextChatMessageInfoItem, + params: RobotextParams, + ): MergeRobotextMessageItemResult { + if (precedingMessageInfoItem.messageInfos.length === 0) { + return { shouldMerge: false }; + } + for (const precedingMessageInfo of precedingMessageInfoItem.messageInfos) { + if (precedingMessageInfo.type !== messageTypes.LEAVE_THREAD) { + return { shouldMerge: false }; + } + } + const messageInfos = [ + messageInfo, + ...precedingMessageInfoItem.messageInfos, + ]; + const leavingUsers = pluralizeEntityText( + messageInfos.map(info => ET`${ET.user({ userInfo: info.creator })}`), + ); + const newRobotext = getLeaveThreadRobotext( + leavingUsers, + messageInfo.threadID, + params, + ); + const mergedItem = { + ...precedingMessageInfoItem, + messageInfos, + robotext: newRobotext, + }; + return { shouldMerge: true, item: mergedItem }; + }, }); diff --git a/lib/shared/messages/remove-members-message-spec.js b/lib/shared/messages/remove-members-message-spec.js index b3fce8d2a..806ff0661 100644 --- a/lib/shared/messages/remove-members-message-spec.js +++ b/lib/shared/messages/remove-members-message-spec.js @@ -1,189 +1,234 @@ // @flow import invariant from 'invariant'; import type { CreateMessageInfoParams, MessageSpec, NotificationTextsParams, + MergeRobotextMessageItemResult, } from './message-spec.js'; import { joinResult } from './utils.js'; +import type { RobotextChatMessageInfoItem } from '../../selectors/chat-selectors.js'; import { messageTypes } from '../../types/message-types-enum.js'; import type { ClientDBMessageInfo, MessageInfo, } from '../../types/message-types.js'; import { type RawRemoveMembersMessageInfo, rawRemoveMembersMessageInfoValidator, type RemoveMembersMessageData, type RemoveMembersMessageInfo, } from '../../types/messages/remove-members.js'; import type { ThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; import type { NotifTexts } from '../../types/notif-types.js'; import type { RelativeUserInfo } from '../../types/user-types.js'; import { type EntityText, ET, pluralizeEntityText, } from '../../utils/entity-text.js'; import { values } from '../../utils/objects.js'; import { notifRobotextForMessageInfo } from '../notif-utils.js'; +function getRemoveMembersRobotext( + messageInfo: RemoveMembersMessageInfo, +): EntityText { + const users = messageInfo.removedMembers; + invariant(users.length !== 0, 'removed who??'); + + const creator = ET.user({ userInfo: messageInfo.creator }); + const removedUsers = pluralizeEntityText( + users.map(user => ET`${ET.user({ userInfo: user })}`), + ); + + return ET`${creator} removed ${removedUsers}`; +} + type RemoveMembersMessageSpec = MessageSpec< RemoveMembersMessageData, RawRemoveMembersMessageInfo, RemoveMembersMessageInfo, > & { // We need to explicitly type this as non-optional so that // it can be referenced from messageContentForClientDB below +messageContentForServerDB: ( data: RemoveMembersMessageData | RawRemoveMembersMessageInfo, ) => 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 getRemoveMembersRobotext(messageInfo); }, 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); 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, }; }, notificationCollapseKey( rawMessageInfo: RawRemoveMembersMessageInfo, ): string { return joinResult( rawMessageInfo.type, rawMessageInfo.threadID, rawMessageInfo.creatorID, ); }, userIDs( rawMessageInfo: RawRemoveMembersMessageInfo, ): $ReadOnlyArray { return rawMessageInfo.removedUserIDs; }, canBeSidebarSource: true, canBePinned: false, validator: rawRemoveMembersMessageInfoValidator, + + mergeIntoPrecedingRobotextMessageItem( + messageInfo: RemoveMembersMessageInfo, + precedingMessageInfoItem: RobotextChatMessageInfoItem, + ): MergeRobotextMessageItemResult { + if (precedingMessageInfoItem.messageInfos.length === 0) { + return { shouldMerge: false }; + } + const removedMembers = []; + const creatorID = messageInfo.creator.id; + for (const precedingMessageInfo of precedingMessageInfoItem.messageInfos) { + if ( + precedingMessageInfo.type !== messageTypes.REMOVE_MEMBERS || + precedingMessageInfo.creator.id !== creatorID + ) { + return { shouldMerge: false }; + } + for (const removedMember of precedingMessageInfo.removedMembers) { + removedMembers.push(removedMember); + } + } + + const messageInfos = [ + messageInfo, + ...precedingMessageInfoItem.messageInfos, + ]; + const newRobotext = getRemoveMembersRobotext({ + ...messageInfo, + removedMembers, + }); + const mergedItem = { + ...precedingMessageInfoItem, + messageInfos, + robotext: newRobotext, + }; + return { shouldMerge: true, item: mergedItem }; + }, }, );