chore: auto-commit (21 archivos)
- app.md - backend/dist/assets/index-D_Kep7Fb.js - backend/dist/index.html - backend/handlers.go - backend/main.go - frontend/src/App.tsx - frontend/src/api.ts - frontend/src/components/CardChatPanel.tsx - frontend/src/components/LoginPage.tsx - frontend/src/types.ts - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+96
-7
@@ -81,7 +81,9 @@ import { StickerPicker } from "./components/StickerPicker";
|
||||
import { ColorPickerGrid, CustomColorModal } from "./components/ColorPickerGrid";
|
||||
import { AVATAR_COLORS } from "./components/colors";
|
||||
import { colorBg, colorBorder } from "./components/colors";
|
||||
import type { Board, Card, CardColor, Column, ColumnLocation, User } from "./types";
|
||||
import { NotificationsBell } from "./components/NotificationsBell";
|
||||
import { useEventStream } from "./hooks/useEventStream";
|
||||
import type { Board, Card, CardColor, Column, ColumnLocation, Notification, User } from "./types";
|
||||
|
||||
const COL_PREFIX = "column-";
|
||||
|
||||
@@ -326,12 +328,71 @@ export function App() {
|
||||
return () => clearInterval(t);
|
||||
}, [activeCard, activeColumnId]);
|
||||
|
||||
// Notifications state (populated by SSE + initial fetch).
|
||||
const [notifs, setNotifs] = useState<Notification[]>([]);
|
||||
const [notifUnread, setNotifUnread] = useState(0);
|
||||
|
||||
// Build version (injected at compile time via -ldflags). Fetched once.
|
||||
const [appVersion, setAppVersion] = useState<string>("");
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => {
|
||||
reload();
|
||||
}, 30000);
|
||||
return () => clearInterval(t);
|
||||
}, [reload]);
|
||||
api
|
||||
.getVersion()
|
||||
.then((v) => setAppVersion(v.version))
|
||||
.catch(() => setAppVersion(""));
|
||||
}, []);
|
||||
|
||||
const reloadNotifs = useCallback(async () => {
|
||||
try {
|
||||
const [list, c] = await Promise.all([api.listNotifications(false), api.unreadNotificationCount()]);
|
||||
setNotifs(list);
|
||||
setNotifUnread(c.count);
|
||||
} catch {
|
||||
// best-effort; SSE will reconcile
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (auth.user) reloadNotifs();
|
||||
}, [auth.user, reloadNotifs]);
|
||||
|
||||
// Replace 30s polling with SSE. Server pushes board.invalidated on every
|
||||
// mutation, message.created on chat traffic and notification.created on
|
||||
// per-user notifications. We refetch /api/board on invalidate (cheap +
|
||||
// keeps merge logic simple) and patch notification state in-place.
|
||||
useEventStream(
|
||||
useMemo(
|
||||
() => ({
|
||||
"board.invalidated": () => {
|
||||
reload();
|
||||
},
|
||||
"notification.created": (payload: unknown) => {
|
||||
const n = payload as Notification;
|
||||
if (!n || !n.id) return;
|
||||
setNotifs((prev) => (prev.some((x) => x.id === n.id) ? prev : [n, ...prev].slice(0, 100)));
|
||||
setNotifUnread((c) => c + 1);
|
||||
const who = n.actor_name || "Alguien";
|
||||
const card = n.card_seq_num ? `#${n.card_seq_num}` : n.card_title;
|
||||
notifications.show({
|
||||
color: n.kind === "mention" ? "grape" : "blue",
|
||||
title: `${who} en ${card}`,
|
||||
message: n.snippet,
|
||||
});
|
||||
},
|
||||
"notification.read": (payload: unknown) => {
|
||||
const p = payload as { id?: string } | null;
|
||||
if (!p?.id) return;
|
||||
setNotifs((prev) => prev.map((x) => (x.id === p.id ? { ...x, read_at: new Date().toISOString() } : x)));
|
||||
setNotifUnread((c) => Math.max(0, c - 1));
|
||||
},
|
||||
"notification.read_all": () => {
|
||||
setNotifs((prev) => prev.map((x) => (x.read_at ? x : { ...x, read_at: new Date().toISOString() })));
|
||||
setNotifUnread(0);
|
||||
},
|
||||
}),
|
||||
[reload],
|
||||
),
|
||||
!!auth.user,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeSticker) return;
|
||||
@@ -1113,6 +1174,25 @@ export function App() {
|
||||
<ActionIcon variant="subtle" onClick={reload} aria-label="Refresh">
|
||||
<IconRefresh size={16} />
|
||||
</ActionIcon>
|
||||
{auth.user && (
|
||||
<NotificationsBell
|
||||
unreadCount={notifUnread}
|
||||
notifications={notifs}
|
||||
onOpenCard={async (cardId) => {
|
||||
const card = board?.cards.find((c) => c.id === cardId);
|
||||
if (card) {
|
||||
setActiveCard(card);
|
||||
} else {
|
||||
// Card may be archived/trashed/missing locally — refetch and retry.
|
||||
await reload();
|
||||
const b = await api.getBoard();
|
||||
const c2 = b.cards.find((c) => c.id === cardId);
|
||||
if (c2) setActiveCard(c2);
|
||||
}
|
||||
}}
|
||||
onChanged={reloadNotifs}
|
||||
/>
|
||||
)}
|
||||
<ActionIcon
|
||||
variant={chatOpen ? "filled" : "subtle"}
|
||||
onClick={() => setChatOpen((v) => !v)}
|
||||
@@ -1130,7 +1210,16 @@ export function App() {
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>{auth.user.display_name || auth.user.username}</Menu.Label>
|
||||
<Menu.Label>
|
||||
<Group justify="space-between" gap={6} wrap="nowrap">
|
||||
<Text size="xs" fw={600} truncate>
|
||||
{auth.user.display_name || auth.user.username}
|
||||
</Text>
|
||||
{appVersion && (
|
||||
<Text size="xs" c="dimmed" ff="monospace">v{appVersion}</Text>
|
||||
)}
|
||||
</Group>
|
||||
</Menu.Label>
|
||||
<Box p="xs">
|
||||
<Text size="xs" c="dimmed" mb={4}>Color del avatar</Text>
|
||||
<ColorPickerGrid
|
||||
|
||||
Reference in New Issue
Block a user