Skip to content

Commit 2432677

Browse files
committed
chat: add read counter
1 parent e2da7a2 commit 2432677

File tree

2 files changed

+116
-26
lines changed

2 files changed

+116
-26
lines changed

src/packages/frontend/chat/actions.ts

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -530,23 +530,63 @@ export class ChatActions extends Actions<ChatState> {
530530
};
531531

532532
renameThread = (threadKey: string, name: string): boolean => {
533-
if (this.syncdb == null || this.store == null) {
533+
if (this.syncdb == null) {
534+
return false;
535+
}
536+
const entry = this.getThreadRootDoc(threadKey);
537+
if (entry == null) {
538+
return false;
539+
}
540+
const trimmed = name.trim();
541+
if (trimmed) {
542+
entry.doc.name = trimmed;
543+
} else {
544+
delete entry.doc.name;
545+
}
546+
this.syncdb.set(entry.doc);
547+
this.syncdb.commit();
548+
return true;
549+
};
550+
551+
markThreadRead = (threadKey: string, count: number): boolean => {
552+
if (this.syncdb == null) {
553+
return false;
554+
}
555+
const account_id = this.redux.getStore("account").get_account_id();
556+
if (!account_id || !Number.isFinite(count)) {
557+
return false;
558+
}
559+
const entry = this.getThreadRootDoc(threadKey);
560+
if (entry == null) {
534561
return false;
535562
}
563+
entry.doc[`read-${account_id}`] = count;
564+
this.syncdb.set(entry.doc);
565+
this.syncdb.commit();
566+
return true;
567+
};
568+
569+
private getThreadRootDoc = (
570+
threadKey: string,
571+
): { doc: any; message: ChatMessageTyped } | null => {
572+
if (this.store == null) {
573+
return null;
574+
}
536575
const messages = this.store.get("messages");
537576
if (messages == null) {
538-
return false;
577+
return null;
539578
}
540579
const normalizedKey = toMsString(threadKey);
541-
const candidates = [normalizedKey, threadKey, `${parseInt(threadKey, 10)}`];
580+
const fallbackKey = `${parseInt(threadKey, 10)}`;
581+
const candidates = [normalizedKey, threadKey, fallbackKey];
542582
let message: ChatMessageTyped | undefined;
543583
for (const key of candidates) {
544584
if (!key) continue;
545585
message = messages.get(key);
546586
if (message != null) break;
547587
}
548588
if (message == null) {
549-
return false;
589+
return null;
550590
}
551591
const dateField = message.get("date");
552592
const dateIso =
@@ -555,16 +595,11 @@ export class ChatActions extends Actions<ChatState> {
555595
: typeof dateField === "string"
556596
? dateField
557597
: new Date(dateField).toISOString();
558-
const doc = message.toJS() as any;
559-
if (name.trim()) {
560-
doc.name = name.trim();
561-
} else {
562-
delete doc.name;
598+
if (!dateIso) {
599+
return null;
563600
}
564-
doc.date = dateIso;
565-
this.syncdb.set(doc);
566-
this.syncdb.commit();
567-
return true;
601+
const doc = { ...message.toJS(), date: dateIso };
602+
return { doc, message };
568603
};
569604

570605
save_scroll_state = (position, height, offset): void => {

src/packages/frontend/chat/chatroom.tsx

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import type { MenuProps } from "antd";
77
import {
8+
Badge,
89
Button,
910
Divider,
1011
Dropdown,
@@ -29,6 +30,7 @@ import {
2930
useRef,
3031
useMemo,
3132
useState,
33+
useTypedRedux,
3234
} from "@cocalc/frontend/app-framework";
3335
import { Icon, Loading } from "@cocalc/frontend/components";
3436
import StaticMarkdown from "@cocalc/frontend/editors/slate/static-markdown";
@@ -47,6 +49,14 @@ import {
4749
getThreadRootDate,
4850
} from "./utils";
4951
import { ALL_THREADS_KEY, useThreadList } from "./threads";
52+
import type { ThreadListItem } from "./threads";
53+
54+
type ThreadMeta = ThreadListItem & {
55+
displayLabel: string;
56+
hasCustomName: boolean;
57+
readCount: number;
58+
unreadCount: number;
59+
};
5060

5161
const FILTER_RECENT_NONE = {
5262
value: 0,
@@ -114,11 +124,6 @@ const THREAD_ITEM_LABEL_STYLE: React.CSSProperties = {
114124
pointerEvents: "none",
115125
} as const;
116126

117-
const THREAD_ITEM_COUNT_STYLE: React.CSSProperties = {
118-
fontSize: "11px",
119-
color: "#999",
120-
} as const;
121-
122127
export function ChatRoom({
123128
actions,
124129
project_id,
@@ -127,6 +132,7 @@ export function ChatRoom({
127132
desc,
128133
}: EditorComponentProps) {
129134
const useEditor = useEditorRedux<ChatState>({ project_id, path });
135+
const account_id = useTypedRedux("account", "account_id");
130136
const [input, setInput] = useState("");
131137
const search = desc?.get("data-search") ?? "";
132138
const filterRecentH: number = desc?.get("data-filterRecentH") ?? 0;
@@ -139,7 +145,37 @@ export function ChatRoom({
139145
const messages = useEditor("messages") as ChatMessages | undefined;
140146
const [filterRecentHCustom, setFilterRecentHCustom] = useState<string>("");
141147
const [filterRecentOpen, setFilterRecentOpen] = useState<boolean>(false);
142-
const threads = useThreadList(messages);
148+
const rawThreads = useThreadList(messages);
149+
const threads = useMemo<ThreadMeta[]>(() => {
150+
return rawThreads.map((thread) => {
151+
const rootMessage = thread.rootMessage;
152+
const storedName = (
153+
rootMessage?.get("name") as string | undefined
154+
)?.trim();
155+
const hasCustomName = !!storedName;
156+
const displayLabel = storedName || thread.label;
157+
const readField =
158+
account_id && rootMessage
159+
? rootMessage.get(`read-${account_id}`)
160+
: null;
161+
const readValue =
162+
typeof readField === "number"
163+
? readField
164+
: typeof readField === "string"
165+
? parseInt(readField, 10)
166+
: 0;
167+
const readCount =
168+
Number.isFinite(readValue) && readValue > 0 ? readValue : 0;
169+
const unreadCount = Math.max(thread.messageCount - readCount, 0);
170+
return {
171+
...thread,
172+
displayLabel,
173+
hasCustomName,
174+
readCount,
175+
unreadCount,
176+
};
177+
});
178+
}, [rawThreads, account_id]);
143179
const [selectedThreadKey, setSelectedThreadKey0] = useState<string | null>(
144180
desc?.get("data-selectedThreadKey") ?? null,
145181
);
@@ -190,6 +226,20 @@ export function ChatRoom({
190226
}
191227
}, [selectedThreadKey]);
192228

229+
useEffect(() => {
230+
if (!singleThreadView || !selectedThreadKey) {
231+
return;
232+
}
233+
const thread = threads.find((t) => t.key === selectedThreadKey);
234+
if (!thread) {
235+
return;
236+
}
237+
if (thread.unreadCount <= 0) {
238+
return;
239+
}
240+
actions.markThreadRead?.(thread.key, thread.messageCount);
241+
}, [singleThreadView, selectedThreadKey, threads, actions]);
242+
193243
useEffect(() => {
194244
if (!fragmentId || isAllThreadsSelected || messages == null) {
195245
return;
@@ -522,12 +572,7 @@ export function ChatRoom({
522572
threads.length === 0
523573
? []
524574
: threads.map((thread) => {
525-
const { key, label, messageCount, rootMessage } = thread;
526-
const customName = rootMessage?.get("name") as string | undefined;
527-
const trimmedName =
528-
typeof customName === "string" ? customName.trim() : "";
529-
const hasCustomName = trimmedName.length > 0;
530-
const displayLabel = hasCustomName ? trimmedName : label;
575+
const { key, displayLabel, hasCustomName, unreadCount } = thread;
531576
const isHovered = hoveredThread === key;
532577
const showMenu = isHovered || selectedThreadKey === key;
533578
return {
@@ -549,7 +594,17 @@ export function ChatRoom({
549594
value={displayLabel}
550595
style={THREAD_ITEM_LABEL_STYLE}
551596
/>
552-
<span style={THREAD_ITEM_COUNT_STYLE}>{messageCount}</span>
597+
{unreadCount > 0 && !isHovered && (
598+
<Badge
599+
count={unreadCount}
600+
size="small"
601+
overflowCount={99}
602+
style={{
603+
backgroundColor: COLORS.GRAY_L0,
604+
color: COLORS.GRAY_D,
605+
}}
606+
/>
607+
)}
553608
{showMenu && (
554609
<Dropdown
555610
menu={threadMenuProps(key, displayLabel, hasCustomName)}

0 commit comments

Comments
 (0)