fa93173b1f
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>
61 lines
2.2 KiB
TypeScript
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>
|
|
);
|
|
}
|