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>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user