feat(messaging-ui): start direct messages + groups (Slack-style people picker)
Add a 'New message' + on the Direct messages section that opens a people picker
(NewConversation): pick one person -> DM, two or more -> group with an optional
name, via the adapter's existing openThread({participantIds, membership, subject}).
Expose directory() on MessagingAdapter (org people list); MockAdapter implements
it + dedupes 1:1 DMs. 72 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { useConversations } from '../hooks/use-conversations';
|
||||
import { useAdapter } from '../provider';
|
||||
import { ConversationList } from './conversation-list';
|
||||
import { ChannelBrowser } from './channel-browser';
|
||||
import { NewConversation } from './new-conversation';
|
||||
import { Thread } from './thread';
|
||||
import { ThreadPane } from './thread-pane';
|
||||
|
||||
@@ -15,28 +16,41 @@ export function Messenger() {
|
||||
const { conversations, loading, error, refetch } = useConversations();
|
||||
const adapter = useAdapter();
|
||||
const channelsSupported = typeof adapter.browseChannels === 'function';
|
||||
const directorySupported = typeof adapter.directory === 'function';
|
||||
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
const [browsing, setBrowsing] = useState(false);
|
||||
const [composing, setComposing] = useState(false); // "New message" people picker open
|
||||
const [activeRoot, setActiveRoot] = useState<string | null>(null); // open thread pane's root message
|
||||
|
||||
useEffect(() => {
|
||||
if (browsing) return;
|
||||
if (browsing || composing) return;
|
||||
if (selected && conversations.some((c) => c.threadId === selected)) return;
|
||||
setSelected(conversations[0]?.threadId ?? null);
|
||||
}, [conversations, selected, browsing]);
|
||||
}, [conversations, selected, browsing, composing]);
|
||||
|
||||
// Switching conversations (or into browse) closes any open thread pane.
|
||||
useEffect(() => setActiveRoot(null), [selected, browsing]);
|
||||
// Switching conversations (or into browse/compose) closes any open thread pane.
|
||||
useEffect(() => setActiveRoot(null), [selected, browsing, composing]);
|
||||
|
||||
const channels = useMemo(() => conversations.filter((c) => c.membership === 'channel'), [conversations]);
|
||||
const dms = useMemo(() => conversations.filter((c) => c.membership !== 'channel'), [conversations]);
|
||||
|
||||
function pick(threadId: string): void {
|
||||
setBrowsing(false);
|
||||
setComposing(false);
|
||||
setSelected(threadId);
|
||||
}
|
||||
|
||||
function openBrowse(): void {
|
||||
setComposing(false);
|
||||
setBrowsing(true);
|
||||
}
|
||||
|
||||
function openCompose(): void {
|
||||
setBrowsing(false);
|
||||
setComposing(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="miu-messenger">
|
||||
<aside className="miu-sidebar">
|
||||
@@ -47,12 +61,12 @@ export function Messenger() {
|
||||
<div className="miu-section">
|
||||
<div className="miu-section-head">
|
||||
<span>Channels</span>
|
||||
<button type="button" className="miu-section-add" title="Browse channels" onClick={() => setBrowsing(true)}>
|
||||
<button type="button" className="miu-section-add" title="Browse channels" onClick={openBrowse}>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
{channels.length > 0 ? (
|
||||
<ConversationList conversations={channels} selectedId={browsing ? null : selected} onSelect={pick} />
|
||||
<ConversationList conversations={channels} selectedId={browsing || composing ? null : selected} onSelect={pick} />
|
||||
) : (
|
||||
<div className="miu-empty miu-empty-sm">Browse to join a channel.</div>
|
||||
)}
|
||||
@@ -60,12 +74,17 @@ export function Messenger() {
|
||||
) : null}
|
||||
|
||||
<div className="miu-section">
|
||||
{channelsSupported ? (
|
||||
{channelsSupported || directorySupported ? (
|
||||
<div className="miu-section-head">
|
||||
<span>Direct messages</span>
|
||||
{directorySupported ? (
|
||||
<button type="button" className="miu-section-add" title="New message" onClick={openCompose}>
|
||||
+
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<ConversationList conversations={dms} selectedId={browsing ? null : selected} onSelect={pick} />
|
||||
<ConversationList conversations={dms} selectedId={browsing || composing ? null : selected} onSelect={pick} />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -77,12 +96,19 @@ export function Messenger() {
|
||||
pick(threadId);
|
||||
}}
|
||||
/>
|
||||
) : composing ? (
|
||||
<NewConversation
|
||||
onCreated={(threadId) => {
|
||||
refetch();
|
||||
pick(threadId);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Thread threadId={selected} activeRootId={activeRoot} onOpenThread={setActiveRoot} />
|
||||
)}
|
||||
</section>
|
||||
|
||||
{!browsing && selected && activeRoot ? (
|
||||
{!browsing && !composing && selected && activeRoot ? (
|
||||
<ThreadPane threadId={selected} rootId={activeRoot} onClose={() => setActiveRoot(null)} />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user