Files
iios/packages/iios-messaging-ui/src/components/thread.tsx
T
maaz519 fa93173b1f feat(messaging-ui): Slack-style threaded-replies side panel
Replies (messages with parentInteractionId) now open in a right-hand ThreadPane
instead of inline quoting:
- extracted a shared Composer (draft + @mention autocomplete + attach) and
  MessageItem (bubble + mentions + attachment + reactions + reply affordance)
- Thread shows top-level messages only; a message with replies gets a
  '💬 N replies' link, and hovering shows 💬 'Reply in thread'
- ThreadPane renders the root + its replies + a composer that posts back with
  parentInteractionId; Messenger becomes 3-column (list | thread | pane),
  pane closes on conversation switch
- no contract/adapter/backend change (parentInteractionId was already wired)
- thread-pane render test (open → reply → parent shows the count); 58 green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 00:15:21 +05:30

62 lines
2.4 KiB
TypeScript

import { useMemo } from 'react';
import { useMessages } from '../hooks/use-messages';
import { useMembers } from '../hooks/use-members';
import { Composer } from './composer';
import { MessageItem } from './message-item';
/**
* The main conversation view: top-level messages + composer. Replies (messages with a
* parentInteractionId) are hidden here and live in the ThreadPane — a message with replies shows a
* "N replies" link that opens it. @mentions, reactions, and attachments all work.
*/
export function Thread({
threadId,
activeRootId,
onOpenThread,
}: {
threadId: string | null;
activeRootId?: string | null;
onOpenThread?: (rootId: string) => void;
}) {
const { messages, loading, error, send, react, upload, typingUserIds, seenIds, sendTyping, canReact, canUpload } = useMessages(threadId);
const members = useMembers(threadId);
const memberNames = useMemo(() => members.map((m) => m.name), [members]);
const topLevel = useMemo(() => messages.filter((m) => !m.parentInteractionId), [messages]);
const replyCount = useMemo(() => {
const counts = new Map<string, number>();
for (const m of messages) if (m.parentInteractionId) counts.set(m.parentInteractionId, (counts.get(m.parentInteractionId) ?? 0) + 1);
return counts;
}, [messages]);
if (!threadId) {
return <div className="miu-empty miu-thread-empty">Select a conversation.</div>;
}
return (
<div className={`miu-thread${activeRootId ? ' has-pane' : ''}`}>
<div className="miu-messages">
{loading && messages.length === 0 ? <div className="miu-empty">Loading</div> : null}
{error ? <div className="miu-empty miu-error">{error}</div> : null}
{topLevel.map((m) => (
<MessageItem
key={m.id}
message={m}
memberNames={memberNames}
canReact={canReact}
onReact={(id, emoji) => void react(id, emoji)}
seen={seenIds.has(m.id)}
replyCount={replyCount.get(m.id) ?? 0}
{...(onOpenThread ? { onOpenThread } : {})}
/>
))}
{typingUserIds.length > 0 ? (
<div className="miu-typing">{typingUserIds.length === 1 ? 'typing…' : 'several people are typing…'}</div>
) : null}
</div>
<Composer members={members} canUpload={canUpload} upload={upload} onSend={send} onTyping={sendTyping} />
</div>
);
}