From 198d5a632985fb4b57208170d2ed2d4df834660b Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Wed, 3 Jun 2026 20:34:58 +0700 Subject: [PATCH] feat(messages): group message feed by channel and thread sections --- .../messages/components/MessageFeed.tsx | 91 ++++++++++++++++--- 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/services/frontend/src/features/messages/components/MessageFeed.tsx b/services/frontend/src/features/messages/components/MessageFeed.tsx index d24cf39..9794bcf 100644 --- a/services/frontend/src/features/messages/components/MessageFeed.tsx +++ b/services/frontend/src/features/messages/components/MessageFeed.tsx @@ -1,5 +1,7 @@ import { motion } from "framer-motion"; +import { Hash } from "lucide-react"; import { useEffect, useMemo, useRef } from "react"; +import { parseMetadata } from "../../../entities/message/types"; import type { MessageRecord } from "../../../shared/api/client"; import { cardItem, cardStagger } from "../../../shared/hooks/useFramerStagger"; import { ScrollArea, EmptyStateMascot } from "../../../shared/ui"; @@ -18,12 +20,12 @@ export interface MessageFeedProps { /** Messages from the same user within 5 minutes are visually grouped. */ const GROUP_WINDOW_MS = 5 * 60 * 1000; -interface MessageGroup { +interface UserMessageGroup { messages: MessageRecord[]; } -function groupMessages(messages: MessageRecord[]): MessageGroup[] { - const groups: MessageGroup[] = []; +function groupByUser(messages: MessageRecord[]): UserMessageGroup[] { + const groups: UserMessageGroup[] = []; for (const msg of messages) { const lastGroup = groups[groups.length - 1]; if ( @@ -41,6 +43,57 @@ function groupMessages(messages: MessageRecord[]): MessageGroup[] { return groups; } +interface ChannelSection { + key: string; + channelId: string; + threadId: string | null; + label: string; + groups: UserMessageGroup[]; +} + +function computeChannelSections(messages: MessageRecord[]): ChannelSection[] { + if (messages.length === 0) return []; + + // Group by (channel_id, thread_id) + const map = new Map(); + for (const msg of messages) { + const key = msg.thread_id + ? `${msg.channel_id}:t:${msg.thread_id}` + : msg.channel_id; + const list = map.get(key); + if (list) list.push(msg); + else map.set(key, [msg]); + } + + const sections: ChannelSection[] = []; + for (const [key, channelMessages] of map) { + const first = channelMessages[0]; + const meta = parseMetadata(first.metadata); + const ch = meta?.channel; + + let label: string; + if (ch?.threadName && ch?.channelName) { + label = `# ${ch.channelName} › ${ch.threadName}`; + } else if (ch?.channelName) { + label = `# ${ch.channelName}`; + } else if (first.thread_id) { + label = `thread:${first.thread_id.slice(0, 8)}`; + } else { + label = `# ${first.channel_id.slice(0, 8)}`; + } + + sections.push({ + key, + channelId: first.channel_id, + threadId: first.thread_id, + label, + groups: groupByUser(channelMessages), + }); + } + + return sections; +} + export function MessageFeed({ messages, onReanalyze, @@ -62,13 +115,13 @@ export function MessageFeed({ (entries) => { if (entries[0]?.isIntersecting) onLoadMore(); }, - { rootMargin: "400px" }, // preload before user reaches bottom + { rootMargin: "400px" }, ); observer.observe(el); return () => observer.disconnect(); }, [onLoadMore, hasMore]); - const groupedMessages = useMemo(() => groupMessages(messages), [messages]); + const sections = useMemo(() => computeChannelSections(messages), [messages]); if (loading) { return ( @@ -94,13 +147,27 @@ export function MessageFeed({ initial="initial" animate="animate" > - {groupedMessages.map((group) => ( - - - + {sections.map((section) => ( +
+ {/* Channel/Thread section header */} +
+ + + {section.label} + +
+ +
+ {section.groups.map((group) => ( + + + + ))} +
+
))} {/* Infinite-scroll sentinel */}