Files
iios/packages/iios-messaging-ui/src/components/thread-pane.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

61 lines
2.2 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 Slack-style thread side panel: the root message + its replies + a composer that posts back
* into the thread (parentInteractionId = root). Uses its own useMessages on the same thread; the
* shared adapter subscription keeps it and the main view in sync.
*/
export function ThreadPane({
threadId,
rootId,
onClose,
}: {
threadId: string;
rootId: string;
onClose: () => void;
}) {
const { messages, send, react, upload, seenIds, sendTyping, canReact, canUpload } = useMessages(threadId);
const members = useMembers(threadId);
const memberNames = useMemo(() => members.map((m) => m.name), [members]);
const root = useMemo(() => messages.find((m) => m.id === rootId), [messages, rootId]);
const replies = useMemo(() => messages.filter((m) => m.parentInteractionId === rootId), [messages, rootId]);
return (
<aside className="miu-pane">
<header className="miu-pane-head">
<span>Thread</span>
<button type="button" className="miu-pane-close" onClick={onClose} aria-label="Close thread">
</button>
</header>
<div className="miu-messages miu-pane-messages">
{root ? (
<MessageItem message={root} memberNames={memberNames} canReact={canReact} onReact={(id, e) => void react(id, e)} seen={seenIds.has(root.id)} />
) : (
<div className="miu-empty">Message not found.</div>
)}
<div className="miu-pane-divider">{replies.length} {replies.length === 1 ? 'reply' : 'replies'}</div>
{replies.map((m) => (
<MessageItem key={m.id} message={m} memberNames={memberNames} canReact={canReact} onReact={(id, e) => void react(id, e)} seen={seenIds.has(m.id)} />
))}
</div>
<Composer
members={members}
canUpload={canUpload}
upload={upload}
onSend={send}
onTyping={sendTyping}
parentInteractionId={rootId}
placeholder="Reply…"
/>
</aside>
);
}