import type { MessageResource } from '../types/resources/message' import dayjs from './dayjs' import { isSystemMessage } from './system_messages' const FIVE_MINUTES_MS = 5 * 60 * 1000 export type DateSeparator = { type: 'DateSeparator'; id: string; date: string } export type ReplyShadowMessage = { type: 'ReplyShadowMessage' id: string messageId: string replyRoot: MessageResource | null isReplyShadowMessage: boolean nextRendersAuthor: boolean } export type EnrichedMessage = MessageResource | DateSeparator | ReplyShadowMessage interface GroupMessagesProps { ms: MessageResource[] inReplyScreen?: boolean } export function groupMessages({ ms, inReplyScreen }: GroupMessagesProps): EnrichedMessage[] { const items: EnrichedMessage[] = [] const messagesById = new Map(ms.map(message => [message.id, message])) let myLatestSeen = false let nextNeighborEnriched: MessageResource | undefined ms.forEach((message, i) => { const { prev } = neighborsOf(ms, i) const next = nextNeighborEnriched if (isSystemMessage(message)) { const enriched = enrichSystemMessage(message, next) items.push(enriched) if (datesDifferBetween(message, prev)) items.push(dateSeparator(message)) nextNeighborEnriched = enriched return } const isMyLatest = !myLatestSeen && message.mine if (isMyLatest) myLatestSeen = true const enriched = enrichRegularMessage( { ...message, replyRoot: resolveReplyRoot(message, messagesById) }, prev, next, isMyLatest, !!inReplyScreen ) items.push(enriched) const shadow = replyShadowFor(enriched, prev) if (shadow) items.push(shadow) if (!prev || datesDifferBetween(message, prev)) { items.push(dateSeparator(message)) } nextNeighborEnriched = enriched }) return items } function neighborsOf(arr: T[], i: number): { prev: T | undefined; next: T | undefined } { return { prev: arr[i + 1], next: arr[i - 1] } } function resolveReplyRoot( message: MessageResource, messagesById: Map ): MessageResource | null { if (!message.replyRootId || message.replyRootId === message.id) return null const loadedRoot = messagesById.get(message.replyRootId) const loadedRootIsSaved = !!loadedRoot && !loadedRoot.pending && !loadedRoot.error if (loadedRootIsSaved) return loadedRoot return message.replyRoot ?? loadedRoot ?? null } function enrichSystemMessage( message: MessageResource, next: MessageResource | undefined ): MessageResource { return { ...message, myLatestInConversation: false, lastInGroup: true, renderAuthor: false, nextRendersAuthor: next?.renderAuthor, isReplyShadowMessage: false, nextIsReplyShadowMessage: false, threadPosition: null, } } function enrichRegularMessage( message: MessageResource, prev: MessageResource | undefined, next: MessageResource | undefined, isMyLatest: boolean, inReplyScreen: boolean ): MessageResource { const inThread = message.replyRootId !== null const showThreadDetails = !inReplyScreen && inThread return { ...message, myLatestInConversation: isMyLatest, lastInGroup: !next || startsNewGroup(next, message), renderAuthor: !message.mine && (!prev || startsNewGroup(message, prev)), nextRendersAuthor: next?.renderAuthor, isReplyShadowMessage: false, nextIsReplyShadowMessage: nextIntroducesReplyShadow(next, message), threadPosition: showThreadDetails ? threadPositionFor(message, next) : null, prevIsMyReply: showThreadDetails ? prev?.mine : undefined, nextIsMyReply: showThreadDetails ? next?.mine : undefined, } } function startsNewGroup(a: MessageResource, b: MessageResource): boolean { return ( a.author?.id !== b.author?.id || differsByMoreThan5Min(a, b) || a.replyRootId !== b.replyRootId || datesDifferBetween(a, b) ) } function differsByMoreThan5Min(a: MessageResource, b: MessageResource): boolean { return Math.abs(toMillis(a.createdAt) - toMillis(b.createdAt)) > FIVE_MINUTES_MS } function toMillis(iso: string): number { return new Date(iso).getTime() } function datesDifferBetween(a: MessageResource, b: MessageResource | undefined): boolean { if (!b) return false return dateKey(a) !== dateKey(b) } function dateKey(message: MessageResource): string { return dayjs(message.createdAt).format('YYYY-MM-DD') } function dateSeparator(message: MessageResource): DateSeparator { return { type: 'DateSeparator', id: `day-divider-${message.id}`, date: dateKey(message) } } function threadPositionFor( message: MessageResource, next: MessageResource | undefined ): 'first' | 'center' | 'last' | null { const isThreadRoot = message.replyRootId === message.id const isLast = !next || next.replyRootId !== message.replyRootId || datesDifferBetween(next, message) if (isThreadRoot && isLast) return null if (isThreadRoot) return 'first' if (isLast) return 'last' return 'center' } function nextIntroducesReplyShadow( next: MessageResource | undefined, current: MessageResource ): boolean { if (!next) return false const nextInThread = next.replyRootId !== null const nextIsThreadRoot = next.replyRootId === next.id const differentThread = next.replyRootId !== current.replyRootId return nextInThread && !nextIsThreadRoot && (differentThread || datesDifferBetween(next, current)) } function replyShadowFor( message: MessageResource, prev: MessageResource | undefined ): ReplyShadowMessage | undefined { if (!message.replyRootId) return undefined if (message.replyRootId === message.id) return undefined const enteringNewThread = !prev || prev.replyRootId !== message.replyRootId || datesDifferBetween(prev, message) if (!enteringNewThread) return undefined return { type: 'ReplyShadowMessage', id: `${message.id}-${message.replyRootId}`, messageId: message.replyRootId, replyRoot: message.replyRoot ?? null, isReplyShadowMessage: true, nextRendersAuthor: message.renderAuthor ?? false, } }