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 (
-