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
This commit is contained in:
2026-07-06 17:56:19 +00:00
parent 2b45b11ea8
commit 88194b17c4
6 changed files with 76 additions and 11 deletions
+20 -3
View File
@@ -2,6 +2,7 @@ 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) {
@@ -18,7 +19,10 @@ export function ConversationList() {
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);
@@ -28,15 +32,19 @@ export function ConversationList() {
}, [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);
// Find existing self-DM or create one
const existing = conversations.find((c) => isSelfDM(c, currentUser?.id || ""));
if (existing) {
navigate(`/dm/${existing.id}`);
openConversation(existing.id);
return;
}
try {
@@ -50,6 +58,11 @@ export function ConversationList() {
}
};
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">
@@ -82,6 +95,7 @@ export function ConversationList() {
: 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}
@@ -93,7 +107,10 @@ export function ConversationList() {
}`}
>
<span className="text-gb-fg-f">{self ? "📝" : "@"}</span>
<span className="truncate">{name}</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>
);
})}