mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Adds a live presence layer so the Settings → Team members roster and the inbox Assign dropdown show who is actively using the dashboard, idle, or gone. Implements #269. How it works - New `member_presence` table + `touch_presence` SECURITY DEFINER RPC (migration 024). Account-scoped SELECT RLS mirrors the read-only roster; the RPC derives account_id server-side, never client-supplied. - The active client heartbeats 'online'/'away' every ~30s; 'offline' is derived by viewers from staleness, so a closed tab resolves to offline without an unreliable unload write. - Pure, unit-tested derivation/formatting in src/lib/presence.ts. - `usePresence` reads the table, subscribes to Realtime (subscribe-then- merge so events landing mid-fetch aren't clobbered), and re-derives offline on a 15s tick. - Roster: avatar badge + tooltip + "N online · N away · N offline" summary, with an aria-label so AT/keyboard users get presence too. Inbox Assign dropdown: matching dot before each teammate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
41 lines
1.1 KiB
TypeScript
41 lines
1.1 KiB
TypeScript
import { cn } from "@/lib/utils";
|
|
import type { PresenceStatus } from "@/lib/presence";
|
|
|
|
// Single source of truth for presence colours. Semantic accents
|
|
// (emerald / amber / muted), mirroring the role-chip palette already
|
|
// used across settings, so they're intentionally not tokenized.
|
|
export const PRESENCE_DOT_CLASS: Record<PresenceStatus, string> = {
|
|
online: "bg-emerald-500",
|
|
away: "bg-amber-500",
|
|
offline: "bg-muted-foreground/50",
|
|
};
|
|
|
|
/**
|
|
* A small presence dot. Used inline (e.g. the inbox Assign dropdown)
|
|
* and, with `asAvatarBadge`-style positioning supplied via className,
|
|
* layered onto an avatar. `label` powers the native title/aria for
|
|
* the lightweight inline case; the roster wraps it in a real Tooltip.
|
|
*/
|
|
export function PresenceDot({
|
|
status,
|
|
label,
|
|
className,
|
|
}: {
|
|
status: PresenceStatus;
|
|
label?: string;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<span
|
|
role={label ? "img" : undefined}
|
|
aria-label={label}
|
|
title={label}
|
|
className={cn(
|
|
"inline-block size-2 shrink-0 rounded-full",
|
|
PRESENCE_DOT_CLASS[status],
|
|
className,
|
|
)}
|
|
/>
|
|
);
|
|
}
|