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>
53 lines
1.7 KiB
TypeScript
53 lines
1.7 KiB
TypeScript
import type { Conversation } from '../types';
|
|
|
|
/** Initials for the avatar chip — first letters of the first two words. */
|
|
function initials(title: string): string {
|
|
const parts = title.trim().split(/\s+/).filter(Boolean);
|
|
const chars = (parts[0]?.[0] ?? '') + (parts[1]?.[0] ?? '');
|
|
return (chars || '?').toUpperCase();
|
|
}
|
|
|
|
/**
|
|
* Presentational conversation list. Owns no data fetching — the parent passes the
|
|
* conversations (from useConversations) so a host can also drive it from its own store.
|
|
*/
|
|
export function ConversationList({
|
|
conversations,
|
|
selectedId,
|
|
onSelect,
|
|
}: {
|
|
conversations: Conversation[];
|
|
selectedId?: string | null;
|
|
onSelect: (threadId: string) => void;
|
|
}) {
|
|
if (conversations.length === 0) {
|
|
return <div className="miu-empty">No conversations yet.</div>;
|
|
}
|
|
return (
|
|
<ul className="miu-convlist" role="list">
|
|
{conversations.map((c) => (
|
|
<li key={c.threadId}>
|
|
<button
|
|
type="button"
|
|
className={`miu-convrow${c.threadId === selectedId ? ' is-active' : ''}`}
|
|
onClick={() => onSelect(c.threadId)}
|
|
>
|
|
<span className="miu-avatar" aria-hidden="true">
|
|
{c.membership === 'channel' ? '#' : initials(c.title)}
|
|
</span>
|
|
<span className="miu-convrow-main">
|
|
<span className="miu-convrow-title">{c.title}</span>
|
|
{c.lastMessage ? <span className="miu-convrow-preview">{c.lastMessage}</span> : null}
|
|
</span>
|
|
{c.unread > 0 ? (
|
|
<span className="miu-badge" aria-label={`${c.unread} unread`}>
|
|
{c.unread}
|
|
</span>
|
|
) : null}
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|