Files
dumpsterChat/web/src/components/ConversationList.tsx
T
hobokenchicken 88194b17c4 feat: unread markers for channels and DMs
- Read states store: smarter hasUnread compares against latest message
- ConversationList: orange dot + bold name for unread DMs
- DMChat: auto-mark-read when viewing conversation
- WS handler: mark DM read on new message while active+focused
2026-07-06 17:56:19 +00:00

125 lines
4.7 KiB
TypeScript

import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useConversationStore } from "../stores/conversation.ts";
import { useAuthStore } from "../stores/auth.ts";
import { useReadStatesStore } from "../stores/readStates.ts";
import { NewConversationModal } from "./NewConversationModal.tsx";
function otherMemberName(conv: { members: { id: string; username: string }[] }, currentUserId: string) {
const other = conv.members.find((m) => m.id !== currentUserId);
return other?.username || "unknown";
}
function isSelfDM(conv: { members: { id: string }[] }, currentUserId: string) {
return conv.members.length === 1 && conv.members[0].id === currentUserId;
}
export function ConversationList() {
const conversations = useConversationStore((s) => s.conversations);
const activeId = useConversationStore((s) => s.activeConversationId);
const fetchConversations = useConversationStore((s) => s.fetchConversations);
const createConversation = useConversationStore((s) => s.createConversation);
const messagesByConv = useConversationStore((s) => s.messagesByConversation);
const currentUser = useAuthStore((s) => s.user);
const hasConvUnread = useReadStatesStore((s) => s.hasConvUnread);
const markConvRead = useReadStatesStore((s) => s.markConvRead);
const navigate = useNavigate();
const [showNew, setShowNew] = useState(false);
const [notesError, setNotesError] = useState<string | null>(null);
useEffect(() => {
fetchConversations();
}, [fetchConversations]);
const openConversation = (convId: string) => {
// mark as read when opening
const msgs = messagesByConv[convId] || [];
if (msgs.length > 0) {
markConvRead(convId, msgs[msgs.length - 1].id);
}
navigate(`/dm/${convId}`);
};
const openNotes = async () => {
setNotesError(null);
const existing = conversations.find((c) => isSelfDM(c, currentUser?.id || ""));
if (existing) {
openConversation(existing.id);
return;
}
try {
const conv = await createConversation([]);
if (conv) {
navigate(`/dm/${conv.id}`);
}
} catch (err) {
console.error("Failed to create notes:", err);
setNotesError(err instanceof Error ? err.message : "Failed to create notes");
}
};
const getLatestMessageId = (convId: string): string | undefined => {
const msgs = messagesByConv[convId] || [];
return msgs.length > 0 ? msgs[msgs.length - 1].id : undefined;
};
return (
<div className="h-full w-56 bg-gb-bg-s border-r border-gb-bg-t flex flex-col">
<div className="terminal-border border-t-0 border-x-0 px-3 py-2 text-gb-fg truncate flex items-center justify-between">
<span>[DIRECT MESSAGES]</span>
<div className="flex gap-1">
<button
onClick={openNotes}
className="terminal-button text-xs"
title="Notes to self"
>
[📝]
</button>
<button
onClick={() => setShowNew(true)}
className="terminal-button text-xs"
title="New conversation"
>
[+]
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-2 font-mono text-sm">
{conversations.length === 0 && (
<p className="text-gb-fg-f">[no conversations]</p>
)}
{conversations.map((conv) => {
const self = isSelfDM(conv, currentUser?.id || "");
const name = self
? "Notes"
: conv.type === "group_dm"
? conv.name || conv.members.map((m) => m.username).join(", ")
: otherMemberName(conv, currentUser?.id || "");
const unread = hasConvUnread(conv.id, getLatestMessageId(conv.id));
return (
<button
key={conv.id}
onClick={() => openConversation(conv.id)}
className={`w-full text-left px-2 py-1 rounded-sm flex items-center gap-2 ${
conv.id === activeId
? "terminal-active"
: "hover:bg-gb-bg-t text-gb-fg-s"
}`}
>
<span className="text-gb-fg-f">{self ? "📝" : "@"}</span>
<span className={`truncate flex-1 ${unread ? "text-gb-fg font-bold" : ""}`}>{name}</span>
{unread && (
<span className="text-gb-orange text-xs font-bold shrink-0" title="Unread messages"></span>
)}
</button>
);
})}
</div>
{notesError && (
<div className="px-3 py-1 text-gb-red text-xs font-mono">ERR: {notesError}</div>
)}
{showNew && <NewConversationModal onClose={() => setShowNew(false)} />}
</div>
);
}