mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +02:00
Archiving a channel-bound chat session now severs its channel_chat_session_binding in the same tx as the status flip. The web send path already treats status='archived' as read-only, but the channel engine (Feishu/Slack) resolves inbound traffic through the binding without checking session status, so an archived-but-still-bound session kept receiving agent replies and a stuck, uncleared unread badge. Dropping the binding makes the next inbound message fork a fresh session; unarchive does NOT recreate it (a later session may already own the channel). The FAB unread badge counted status=all sessions without excluding archived, so residual unread on an archived session (archive does not mark-read) held the badge even though the session is hidden and read-only. Extract countUnreadChatSessions() and exclude archived. This matches what mobile already computes (active-only list), restoring count parity. Tests: backend archive-clears-binding + unarchive-does-not-recreate; frontend countUnreadChatSessions archived-exclusion cases. MUL-4372 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
74 lines
2.8 KiB
TypeScript
74 lines
2.8 KiB
TypeScript
"use client";
|
|
|
|
import { MessageCircle } from "lucide-react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { cn } from "@multica/ui/lib/utils";
|
|
import { useChatStore } from "@multica/core/chat";
|
|
import {
|
|
chatSessionsOptions,
|
|
countUnreadChatSessions,
|
|
hasPendingChatTasksOptions,
|
|
} from "@multica/core/chat/queries";
|
|
import { useWorkspaceId } from "@multica/core/hooks";
|
|
import { createLogger } from "@multica/core/logger";
|
|
import {
|
|
Tooltip,
|
|
TooltipTrigger,
|
|
TooltipContent,
|
|
} from "@multica/ui/components/ui/tooltip";
|
|
import { useT } from "../../i18n";
|
|
|
|
const logger = createLogger("chat.ui");
|
|
|
|
export function ChatFab() {
|
|
const { t } = useT("chat");
|
|
const wsId = useWorkspaceId();
|
|
const isOpen = useChatStore((s) => s.isOpen);
|
|
const toggle = useChatStore((s) => s.toggle);
|
|
const { data: sessions = [] } = useQuery(chatSessionsOptions(wsId));
|
|
// FAB only needs a boolean "is anything running", and only while the window
|
|
// is closed (when open, ChatWindow owns the detailed pending query). Gating
|
|
// on `enabled: !isOpen` keeps the minimised button off the per-message
|
|
// aggregate hot path entirely (MUL-4159).
|
|
const { data: hasPending } = useQuery({
|
|
...hasPendingChatTasksOptions(wsId),
|
|
enabled: !isOpen,
|
|
});
|
|
|
|
if (isOpen) return null;
|
|
|
|
const unreadSessionCount = countUnreadChatSessions(sessions);
|
|
const isRunning = hasPending?.has_pending ?? false;
|
|
|
|
const handleClick = () => {
|
|
logger.info("fab.click (open chat)", { unreadSessionCount, isRunning });
|
|
toggle();
|
|
};
|
|
|
|
// Tooltip text carries the running/unread state on hover; the FAB itself no
|
|
// longer shows an unread-count badge (it duplicated the chat tab's, MUL-4374).
|
|
const tooltip = isRunning
|
|
? t(($) => $.fab.running)
|
|
: unreadSessionCount > 0
|
|
? t(($) => $.fab.unread, { count: unreadSessionCount })
|
|
: t(($) => $.fab.default);
|
|
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
onClick={handleClick}
|
|
aria-label={tooltip}
|
|
className={cn(
|
|
"absolute bottom-2 right-2 z-50 flex size-10 touch-manipulation items-center justify-center rounded-full bg-surface-raised text-muted-foreground shadow-[var(--floating-shadow)] ring-1 ring-surface-border transition-[background-color,color,box-shadow] hover:bg-surface-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/70 focus-visible:ring-offset-2 focus-visible:ring-offset-page-canvas active:bg-surface-hover",
|
|
// Impulse the button itself while a chat task is running — no
|
|
// outer ring to keep things calm.
|
|
isRunning && "animate-chat-impulse",
|
|
)}
|
|
>
|
|
<MessageCircle className="size-5" />
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" sideOffset={10}>{tooltip}</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|