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:
@@ -0,0 +1,97 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAdapter } from '../provider';
|
||||
import type { Person } from '../types';
|
||||
|
||||
/**
|
||||
* Start a direct message or a group — Slack-style. Pick people from the org directory: one selected
|
||||
* opens a DM (deduped by the adapter), two or more create a group with an optional name. Shown in
|
||||
* the main pane like the channel browser.
|
||||
*/
|
||||
export function NewConversation({ onCreated }: { onCreated?: (threadId: string) => void }) {
|
||||
const adapter = useAdapter();
|
||||
const [people, setPeople] = useState<Person[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [q, setQ] = useState('');
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [name, setName] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!adapter.directory) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
let alive = true;
|
||||
adapter
|
||||
.directory()
|
||||
.then((p) => {
|
||||
if (alive) {
|
||||
setPeople(p);
|
||||
setError(null);
|
||||
}
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
if (alive) setError(e instanceof Error ? e.message : String(e));
|
||||
})
|
||||
.finally(() => {
|
||||
if (alive) setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [adapter]);
|
||||
|
||||
const filtered = people.filter((p) => p.name.toLowerCase().includes(q.trim().toLowerCase()));
|
||||
const isGroup = selected.length > 1;
|
||||
const canStart = selected.length >= 1 && !busy;
|
||||
|
||||
function toggle(id: string): void {
|
||||
setSelected((s) => (s.includes(id) ? s.filter((x) => x !== id) : [...s, id]));
|
||||
}
|
||||
|
||||
async function start(): Promise<void> {
|
||||
if (!canStart) return;
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await adapter.openThread({
|
||||
participantIds: selected,
|
||||
membership: isGroup ? 'group' : 'dm',
|
||||
...(isGroup && name.trim() ? { subject: name.trim() } : {}),
|
||||
});
|
||||
onCreated?.(res.threadId);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="miu-browser">
|
||||
<div className="miu-browser-head">New message</div>
|
||||
<div className="miu-newconv">
|
||||
<input className="miu-input" value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search people…" aria-label="Search people" />
|
||||
{isGroup ? (
|
||||
<input className="miu-input" value={name} onChange={(e) => setName(e.target.value)} placeholder="Group name (optional)" aria-label="Group name" />
|
||||
) : null}
|
||||
{error ? <div className="miu-empty miu-error">{error}</div> : null}
|
||||
<div className="miu-browser-list">
|
||||
{loading && people.length === 0 ? <div className="miu-empty">Loading…</div> : null}
|
||||
{!loading && filtered.length === 0 ? <div className="miu-empty">No people found.</div> : null}
|
||||
{filtered.map((p) => (
|
||||
<label key={p.id} className={`miu-person-row${selected.includes(p.id) ? ' is-active' : ''}`}>
|
||||
<input type="checkbox" checked={selected.includes(p.id)} onChange={() => toggle(p.id)} />
|
||||
<span className="miu-browser-name">{p.name}</span>
|
||||
<span className="miu-pill">{p.kind}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<button type="button" className="miu-send" onClick={() => void start()} disabled={!canStart}>
|
||||
{busy ? 'Starting…' : isGroup ? `Create group (${selected.length})` : 'Start chat'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user