Files
AgentCompany/AgentCompany-New/src/renderer/src/pages/chat/ChatSidebar.tsx
T
ZacharyZhang-NY d528cd6f75 Turn the Messages rail into a real conversation list
Every channel and direct conversation is now its own row the way messengers
work: avatar or channel glyph, name, last-message preview, relative time,
and a true unread count from per-employee read state (the old badge showed
total message count). Rows sort by latest activity and the newest
conversation opens by default. Direct rows title themselves after the
counterpart resolved from memberships; agent-to-agent conversations the
board observes keep their two-name title, and opening one clears its unread
because read state is per-employee bookkeeping, not access control.
Conversation rows get their own .convo-item style instead of fighting the
sidebar's single-line nav-item rules.
2026-07-06 22:56:02 -04:00

143 lines
4.5 KiB
TypeScript

import { Avatar, Icon, IconButton } from "../../shared/ui";
import type { ChatPresenceRecord } from "../../../../shared/domain";
import { formatRelative } from "../types";
import type { ConversationItem } from "./chat-helpers";
function presenceColor(status: string | undefined): string | null {
switch (status) {
case "active":
case "online":
return "var(--green)";
case "waiting_approval":
case "busy":
return "var(--amber)";
case "thinking":
return "var(--accent)";
default:
return null;
}
}
/**
* The conversation list: every channel and direct conversation is one row —
* avatar, name, last-message preview, time, unread — newest first, the way
* messengers do it.
*/
export function ChatSidebar({
conversations,
presence,
activeSpaceId,
onSelect,
onCreateSpace,
}: {
conversations: ConversationItem[];
presence: ChatPresenceRecord[];
activeSpaceId: string | null;
onSelect: (spaceId: string) => void;
onCreateSpace: () => void;
}) {
const presenceByPrincipal = new Map(presence.map((entry) => [entry.principalId, entry.status]));
return (
<aside className="secondary-pane">
<div className="row" style={{ padding: "4px 8px 12px" }}>
<div style={{ fontSize: 14, fontWeight: 600 }}>Messages</div>
<IconButton icon="plus" label="New space" onClick={onCreateSpace} />
</div>
<div className="pane-scroll">
<div style={{ display: "flex", flexDirection: "column", gap: 1 }}>
{conversations.map((conversation) => (
<ConversationRow
key={conversation.space.id}
conversation={conversation}
active={conversation.space.id === activeSpaceId}
presenceStatus={conversation.counterpart ? presenceByPrincipal.get(conversation.counterpart.id) : undefined}
onClick={() => onSelect(conversation.space.id)}
/>
))}
</div>
</div>
</aside>
);
}
function ConversationRow({
conversation,
active,
presenceStatus,
onClick,
}: {
conversation: ConversationItem;
active: boolean;
presenceStatus: string | undefined;
onClick: () => void;
}) {
const dotColor = presenceColor(presenceStatus);
return (
<button
type="button"
className={`convo-item ${active ? "active" : ""}`.trim()}
onClick={onClick}
>
{conversation.isDirect ? (
<span className="presence" style={{ marginTop: 2 }}>
<Avatar name={conversation.counterpart?.name ?? conversation.title} size="sm" bot={conversation.counterpart?.type !== "employee"} />
{dotColor && <span className="presence-dot" style={{ background: dotColor }} />}
</span>
) : (
<span
style={{
width: 22,
height: 22,
borderRadius: 6,
background: "var(--panel-3)",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
marginTop: 2,
}}
>
<Icon name={conversation.space.visibility === "restricted" ? "lock" : "hash"} size={13} color="var(--text-3)" />
</span>
)}
<span style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 1 }}>
<span className="row" style={{ gap: 8 }}>
<span
style={{
fontWeight: conversation.unread > 0 ? 600 : 520,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
flex: 1,
minWidth: 0,
}}
>
{conversation.title}
</span>
<span style={{ fontSize: 11, color: "var(--text-3)", flexShrink: 0 }}>
{conversation.preview ? formatRelative(conversation.lastActivityAt) : ""}
</span>
</span>
<span className="row" style={{ gap: 8 }}>
<span
style={{
fontSize: 12,
color: "var(--text-3)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
flex: 1,
minWidth: 0,
}}
>
{conversation.preview ?? conversation.space.topic}
</span>
{conversation.unread > 0 && (
<span className="nav-badge" style={{ flexShrink: 0 }}>{conversation.unread > 99 ? "99+" : conversation.unread}</span>
)}
</span>
</span>
</button>
);
}