Path: blob/master/src/packages/frontend/chat/threads.ts
2441 views
/*1Utility helpers for deriving thread metadata from the chat message list.2*/34import { React } from "@cocalc/frontend/app-framework";56import type { ChatMessageTyped, ChatMessages } from "./types";7import { newest_content } from "./utils";89export const ALL_THREADS_KEY = "__ALL_THREADS__";1011export interface ThreadListItem {12key: string;13label: string;14newestTime: number;15messageCount: number;16rootMessage?: ChatMessageTyped;17}1819export type ThreadSectionKey =20| "pinned"21| "today"22| "yesterday"23| "last7days"24| "older";2526export interface ThreadSection<T extends ThreadListItem = ThreadListItem> {27key: ThreadSectionKey;28title: string;29threads: T[];30}3132export function useThreadList(messages?: ChatMessages): ThreadListItem[] {33return React.useMemo(() => {34if (messages == null || messages.size === 0) {35return [];36}3738const threads = new Map<39string,40{41key: string;42newestTime: number;43messageCount: number;44rootMessage?: ChatMessageTyped;45}46>();4748for (const [timeRaw, message] of messages) {49if (message == null) continue;50const timeString =51typeof timeRaw === "string" ? timeRaw : `${timeRaw ?? ""}`;52const replyTo = message.get("reply_to");53const rootKey = replyTo ? `${new Date(replyTo).valueOf()}` : timeString;54let thread = threads.get(rootKey);55if (thread == null) {56thread = {57key: rootKey,58newestTime: 0,59messageCount: 0,60};61threads.set(rootKey, thread);62}63thread.messageCount += 1;64const dateValue = message.get("date")?.valueOf();65if (dateValue != null && dateValue > thread.newestTime) {66thread.newestTime = dateValue;67}68if (!replyTo) {69thread.rootMessage = message;70}71}7273const items: ThreadListItem[] = [];74for (const entry of threads.values()) {75if (entry.rootMessage == null) {76const maybeRoot = messages.get(entry.key);77if (maybeRoot) {78entry.rootMessage = maybeRoot;79}80}81items.push({82key: entry.key,83label: deriveThreadLabel(entry.rootMessage, entry.key),84newestTime: entry.newestTime,85messageCount: entry.messageCount,86rootMessage: entry.rootMessage,87});88}8990items.sort((a, b) => b.newestTime - a.newestTime);91return items;92}, [messages]);93}9495export function deriveThreadLabel(96rootMessage: ChatMessageTyped | undefined,97fallbackKey: string,98): string {99const explicitName = rootMessage?.get("name") as string | undefined;100if (typeof explicitName === "string") {101const trimmed = explicitName.trim();102if (trimmed) {103return trimmed;104}105}106const content = rootMessage ? newest_content(rootMessage) : "";107const normalized = content.replace(/\s+/g, " ").trim();108if (normalized) {109const words = normalized.split(" ");110const short = words.slice(0, 8).join(" ");111return words.length > 8 ? `${short}…` : short;112}113const timestamp = parseInt(fallbackKey);114if (!isNaN(timestamp)) {115return new Date(timestamp).toLocaleString();116}117return "Untitled Thread";118}119120const DAY_MS = 24 * 60 * 60 * 1000;121122interface GroupOptions {123now?: number;124}125126type RecencyKey = Exclude<ThreadSectionKey, "pinned">;127128const RECENCY_SECTIONS: { key: RecencyKey; title: string }[] = [129{ key: "today", title: "Today" },130{ key: "yesterday", title: "Yesterday" },131{ key: "last7days", title: "Last 7 Days" },132{ key: "older", title: "Older" },133];134135function recencyKeyForDelta(delta: number): RecencyKey {136if (delta < DAY_MS) {137return "today";138}139if (delta < 2 * DAY_MS) {140return "yesterday";141}142if (delta < 7 * DAY_MS) {143return "last7days";144}145return "older";146}147148export function groupThreadsByRecency<T extends ThreadListItem & { isPinned?: boolean }>(149threads: T[],150options: GroupOptions = {},151): ThreadSection<T>[] {152if (!threads || threads.length === 0) {153return [];154}155const now = options.now ?? Date.now();156const sections: ThreadSection<T>[] = [];157const pinned = threads.filter((thread) => !!thread.isPinned);158const remainder = threads.filter((thread) => !thread.isPinned);159if (pinned.length > 0) {160sections.push({ key: "pinned", title: "Pinned", threads: pinned });161}162const buckets: Record<RecencyKey, T[]> = {163today: [],164yesterday: [],165last7days: [],166older: [],167};168for (const thread of remainder) {169const delta = now - thread.newestTime;170const key = recencyKeyForDelta(delta);171buckets[key].push(thread);172}173for (const def of RECENCY_SECTIONS) {174const list = buckets[def.key];175if (list.length > 0) {176sections.push({ key: def.key, title: def.title, threads: list });177}178}179return sections;180}181182183