mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 22:59:04 +02:00
The aggregate chat unread badge used two different definitions: web/ desktop's sidebar summed unread messages while mobile's tab badge (and the since-removed ChatFab badge it mirrored) counted sessions — the same account state showed e.g. 5 on web and 2 on iOS. The sidebar also summed ALL sessions while the thread list zeroes the row being viewed, so a reply landing in the open conversation flashed a sidebar count with no matching row. - packages/core/chat/unread.ts: countUnreadChatMessages() as the single shared definition (IM-style message total, optional viewed-session exclusion); pure so mobile can import it. - app-sidebar: use the helper and exclude the actively-viewed session, but only while a chat surface is actually showing it (chat route or floating window open) — a remembered selection with both surfaces closed still counts, since nothing will auto mark-read there. - mobile: tab badge switches to the shared message count (99+ cap like the sidebar), ChatSessionSchema parses unread_count, and markRead's optimistic patch zeroes unread_count alongside has_unread. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
94 lines
3.5 KiB
TypeScript
94 lines
3.5 KiB
TypeScript
/**
|
|
* Mobile chat mutations — create session, delete session, mark session read.
|
|
*
|
|
* Send-message is NOT a mutation: the chat screen runs a hand-written
|
|
* optimistic burst (seed messages cache → seed pendingTask cache → flip
|
|
* activeSession → POST → patch with real task_id) that doesn't map cleanly
|
|
* onto useMutation. See the chat tab screen for the send path.
|
|
*
|
|
* Mirrors the optimistic-update + rollback + onSettled-invalidate pattern
|
|
* of data/mutations/inbox.ts and web's packages/core/chat/mutations.ts.
|
|
*/
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import type { ChatSession } from "@multica/core/types";
|
|
import { api } from "@/data/api";
|
|
import { useWorkspaceStore } from "@/data/workspace-store";
|
|
import { chatKeys } from "@/data/queries/chat";
|
|
|
|
export function useCreateChatSession() {
|
|
const qc = useQueryClient();
|
|
const wsId = useWorkspaceStore((s) => s.currentWorkspaceId);
|
|
|
|
return useMutation({
|
|
mutationFn: (data: { agent_id: string; title?: string }) =>
|
|
api.createChatSession(data),
|
|
onSettled: () => {
|
|
// Optimistic prepend isn't done here — the chat screen seeds caches
|
|
// synchronously around its send burst and uses the returned session
|
|
// id directly. The invalidate ensures the dropdown picks up the new
|
|
// row (and any has_unread / title server defaults) without a refetch
|
|
// race on switch.
|
|
qc.invalidateQueries({ queryKey: chatKeys.sessions(wsId) });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteChatSession() {
|
|
const qc = useQueryClient();
|
|
const wsId = useWorkspaceStore((s) => s.currentWorkspaceId);
|
|
|
|
return useMutation({
|
|
mutationFn: (id: string) => api.deleteChatSession(id),
|
|
onMutate: async (id) => {
|
|
const key = chatKeys.sessions(wsId);
|
|
await qc.cancelQueries({ queryKey: key });
|
|
const prev = qc.getQueryData<ChatSession[]>(key);
|
|
qc.setQueryData<ChatSession[]>(key, (old) =>
|
|
old ? old.filter((s) => s.id !== id) : old,
|
|
);
|
|
return { prev, key };
|
|
},
|
|
onError: (_err, _id, ctx) => {
|
|
if (ctx?.prev) qc.setQueryData(ctx.key, ctx.prev);
|
|
},
|
|
onSettled: (_data, _err, id) => {
|
|
qc.invalidateQueries({ queryKey: chatKeys.sessions(wsId) });
|
|
// Detail-side caches the screen may still hold for this id.
|
|
qc.removeQueries({ queryKey: chatKeys.messages(id) });
|
|
qc.removeQueries({ queryKey: chatKeys.pendingTask(id) });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useMarkChatSessionRead() {
|
|
const qc = useQueryClient();
|
|
const wsId = useWorkspaceStore((s) => s.currentWorkspaceId);
|
|
|
|
return useMutation({
|
|
mutationFn: (sessionId: string) => api.markChatSessionRead(sessionId),
|
|
onMutate: async (sessionId) => {
|
|
const key = chatKeys.sessions(wsId);
|
|
await qc.cancelQueries({ queryKey: key });
|
|
const prev = qc.getQueryData<ChatSession[]>(key);
|
|
// Zero unread_count together with has_unread — the tab badge sums
|
|
// unread_count (see lib/unread-counts.ts), so clearing only the flag
|
|
// would leave a stale badge until the settle refetch. Mirrors web's
|
|
// useMarkChatSessionRead in packages/core/chat/mutations.ts.
|
|
qc.setQueryData<ChatSession[]>(key, (old) =>
|
|
old?.map((s) =>
|
|
s.id === sessionId
|
|
? { ...s, has_unread: false, unread_count: 0 }
|
|
: s,
|
|
),
|
|
);
|
|
return { prev, key };
|
|
},
|
|
onError: (_err, _id, ctx) => {
|
|
if (ctx?.prev) qc.setQueryData(ctx.key, ctx.prev);
|
|
},
|
|
onSettled: () => {
|
|
qc.invalidateQueries({ queryKey: chatKeys.sessions(wsId) });
|
|
},
|
|
});
|
|
}
|