diff --git a/src/components/ChatViewer.tsx b/src/components/ChatViewer.tsx index b164539..3751140 100644 --- a/src/components/ChatViewer.tsx +++ b/src/components/ChatViewer.tsx @@ -68,6 +68,16 @@ interface ChatViewerProps { headerPrefix?: React.ReactNode; } +/** + * Message group for consecutive messages from same author + */ +interface MessageGroup { + author: string; + messages: Message[]; + firstTimestamp: number; + lastTimestamp: number; +} + /** * Helper: Format timestamp as a readable day marker */ @@ -423,6 +433,143 @@ const MessageItem = memo(function MessageItem({ return messageContent; }); +/** + * MessageGroupItem - Renders a group of consecutive messages from the same author + * Shows author name and timestamp only on the first message (Slack-style) + */ +const MessageGroupItem = memo( + function MessageGroupItem({ + group, + adapter, + conversation, + onReply, + canReply, + onScrollToMessage, + }: { + group: MessageGroup; + adapter: ChatProtocolAdapter; + conversation: Conversation; + onReply?: (messageId: string) => void; + canReply: boolean; + onScrollToMessage?: (messageId: string) => void; + }) { + const relays = useMemo( + () => getConversationRelays(conversation), + [conversation], + ); + + return ( +
+ {/* First message: Show author name and timestamp */} +
+
+ {/* Header: Author + Timestamp */} +
+ + + + + {/* Reactions for first message */} + + {canReply && onReply && ( + + )} +
+ + {/* First message content */} +
+ {group.messages[0].event ? ( + + {group.messages[0].replyTo && ( + + )} + + ) : ( + + {group.messages[0].content} + + )} +
+ + {/* Subsequent messages: No author name, just content */} + {group.messages.slice(1).map((message) => ( +
+
+
+ {/* Small timestamp on hover */} + + + + + {canReply && onReply && ( + + )} +
+
+ {message.event ? ( + + {message.replyTo && ( + + )} + + ) : ( + + {message.content} + + )} +
+
+
+ ))} +
+
+
+ ); + }, + (prev, next) => { + // Compare group by first message ID and length + // If same author and same number of messages, assume same group + return ( + prev.group.messages[0].id === next.group.messages[0].id && + prev.group.messages.length === next.group.messages.length + ); + }, +); + /** * ChatViewer - Main chat interface component * @@ -538,15 +685,18 @@ export function ChatViewer({ [adapter, conversation], ); - // Process messages to include day markers + // Process messages to include day markers and group consecutive messages const messagesWithMarkers = useMemo(() => { if (!messages || messages.length === 0) return []; const items: Array< - | { type: "message"; data: Message } + | { type: "message-group"; data: MessageGroup } | { type: "day-marker"; data: string; timestamp: number } > = []; + const GROUP_TIME_WINDOW = 300; // 5 minutes in seconds + let currentGroup: MessageGroup | null = null; + messages.forEach((message, index) => { // Add day marker if this is the first message or if day changed if (index === 0) { @@ -558,6 +708,12 @@ export function ChatViewer({ } else { const prevMessage = messages[index - 1]; if (isDifferentDay(prevMessage.timestamp, message.timestamp)) { + // Flush current group before day marker + if (currentGroup) { + items.push({ type: "message-group", data: currentGroup }); + currentGroup = null; + } + items.push({ type: "day-marker", data: formatDayMarker(message.timestamp), @@ -566,10 +722,38 @@ export function ChatViewer({ } } - // Add the message itself - items.push({ type: "message", data: message }); + // Determine if this message should be grouped with the previous one + const shouldGroup = + currentGroup && + currentGroup.author === message.author && + message.type === "user" && // Only group regular user messages + message.timestamp - currentGroup.lastTimestamp <= GROUP_TIME_WINDOW; + + if (shouldGroup && currentGroup) { + // Add to existing group + currentGroup.messages.push(message); + currentGroup.lastTimestamp = message.timestamp; + } else { + // Flush previous group if exists + if (currentGroup) { + items.push({ type: "message-group", data: currentGroup }); + } + + // Start new group + currentGroup = { + author: message.author, + messages: [message], + firstTimestamp: message.timestamp, + lastTimestamp: message.timestamp, + }; + } }); + // Flush final group + if (currentGroup) { + items.push({ type: "message-group", data: currentGroup }); + } + return items; }, [messages]); @@ -688,9 +872,11 @@ export function ChatViewer({ const handleScrollToMessage = useCallback( (messageId: string) => { if (!messagesWithMarkers) return; - // Find index in the rendered array (which includes day markers) + // Find index in the rendered array (which includes day markers and message groups) const index = messagesWithMarkers.findIndex( - (item) => item.type === "message" && item.data.id === messageId, + (item) => + item.type === "message-group" && + item.data.messages.some((msg) => msg.id === messageId), ); if (index !== -1 && virtuosoRef.current) { virtuosoRef.current.scrollToIndex({ @@ -981,9 +1167,9 @@ export function ChatViewer({ ); } return ( -