00a2cc474a
Adds a third membership beyond dm/group: a discoverable, joinable room. - contract: Membership += 'channel'; Conversation.topic; ChannelSummary + CreateChannelInput; optional adapter methods browseChannels/createChannel/ joinChannel/leaveChannel (capability-degrading — absent hides the UI) - MockAdapter implements them (seeds a joined public, a joinable public, and a private channel); listConversations now returns only threads I'm in - useChannels hook (browse/create/join/leave + supported flag) - UI: sidebar sections (Channels vs Direct messages), a + that opens a ChannelBrowser (join + create), # / lock glyphs; themeable styles - KernelClientAdapter passes channel membership + topic through - 4 channel tests (mock browse/join/create + render browse→join); 52 total green Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
94 lines
3.5 KiB
TypeScript
94 lines
3.5 KiB
TypeScript
import { useState, type FormEvent } from 'react';
|
|
import { useChannels } from '../hooks/use-channels';
|
|
import type { ChannelVisibility } from '../types';
|
|
|
|
/** Browse + join discoverable channels, and create a new one. Shown in the main pane. */
|
|
export function ChannelBrowser({ onJoined }: { onJoined?: (threadId: string) => void }) {
|
|
const { browsable, loading, error, join, create } = useChannels();
|
|
const [name, setName] = useState('');
|
|
const [topic, setTopic] = useState('');
|
|
const [visibility, setVisibility] = useState<ChannelVisibility>('public');
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function submitCreate(e: FormEvent): Promise<void> {
|
|
e.preventDefault();
|
|
const n = name.trim();
|
|
if (!n || busy) return;
|
|
setBusy(true);
|
|
try {
|
|
const threadId = await create({ name: n, ...(topic.trim() ? { topic: topic.trim() } : {}), visibility });
|
|
setName('');
|
|
setTopic('');
|
|
onJoined?.(threadId);
|
|
} catch {
|
|
// surfaced via the hook's error
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
async function doJoin(threadId: string): Promise<void> {
|
|
await join(threadId);
|
|
onJoined?.(threadId);
|
|
}
|
|
|
|
return (
|
|
<div className="miu-browser">
|
|
<div className="miu-browser-head">Channels</div>
|
|
|
|
<form className="miu-channel-create" onSubmit={submitCreate}>
|
|
<input
|
|
className="miu-input"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="New channel name"
|
|
aria-label="Channel name"
|
|
/>
|
|
<input
|
|
className="miu-input"
|
|
value={topic}
|
|
onChange={(e) => setTopic(e.target.value)}
|
|
placeholder="Topic (optional)"
|
|
aria-label="Channel topic"
|
|
/>
|
|
<div className="miu-channel-vis">
|
|
<label>
|
|
<input type="radio" name="miu-vis" checked={visibility === 'public'} onChange={() => setVisibility('public')} /> Public
|
|
</label>
|
|
<label>
|
|
<input type="radio" name="miu-vis" checked={visibility === 'private'} onChange={() => setVisibility('private')} /> Private
|
|
</label>
|
|
</div>
|
|
<button type="submit" className="miu-send" disabled={!name.trim() || busy}>
|
|
Create
|
|
</button>
|
|
</form>
|
|
|
|
<div className="miu-browser-list">
|
|
{loading && browsable.length === 0 ? <div className="miu-empty">Loading…</div> : null}
|
|
{error ? <div className="miu-empty miu-error">{error}</div> : null}
|
|
{!loading && browsable.length === 0 ? <div className="miu-empty">No channels yet — create one above.</div> : null}
|
|
{browsable.map((c) => (
|
|
<div key={c.threadId} className="miu-browser-row">
|
|
<span className="miu-channel-glyph" aria-hidden="true">{c.visibility === 'private' ? '🔒' : '#'}</span>
|
|
<span className="miu-browser-main">
|
|
<span className="miu-browser-name">{c.name}</span>
|
|
{c.topic ? <span className="miu-browser-topic">{c.topic}</span> : null}
|
|
<span className="miu-browser-meta">
|
|
{c.memberCount} member{c.memberCount === 1 ? '' : 's'}
|
|
</span>
|
|
</span>
|
|
{c.joined ? (
|
|
<span className="miu-browser-joined">Joined</span>
|
|
) : (
|
|
<button type="button" className="miu-join" onClick={() => void doJoin(c.threadId)}>
|
|
Join
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|