diff --git a/apps/web/app/(dashboard)/inbox/page.tsx b/apps/web/app/(dashboard)/inbox/page.tsx index 867a1b07..a4743a14 100644 --- a/apps/web/app/(dashboard)/inbox/page.tsx +++ b/apps/web/app/(dashboard)/inbox/page.tsx @@ -217,11 +217,13 @@ export default function InboxPage() { const handleSelect = async (item: InboxItem) => { setSelectedId(item.id); if (!item.read) { + useInboxStore.getState().markRead(item.id); try { await api.markInboxRead(item.id); - useInboxStore.getState().markRead(item.id); } catch { - // silent — selection still works even if mark-read fails + // Rollback: refetch to get server truth + useInboxStore.getState().fetch(); + toast.error("Failed to mark as read"); } } }; diff --git a/apps/web/features/issues/components/board-card.tsx b/apps/web/features/issues/components/board-card.tsx index 499d1583..9ef4f473 100644 --- a/apps/web/features/issues/components/board-card.tsx +++ b/apps/web/features/issues/components/board-card.tsx @@ -47,12 +47,14 @@ export const BoardCardContent = memo(function BoardCardContent({ const handleUpdate = useCallback( (updates: Partial) => { + const prev = { ...issue }; useIssueStore.getState().updateIssue(issue.id, updates); api.updateIssue(issue.id, updates).catch(() => { + useIssueStore.getState().updateIssue(issue.id, prev); toast.error("Failed to update issue"); }); }, - [issue.id] + [issue], ); const showPriority = storeProperties.priority; diff --git a/apps/web/features/realtime/use-realtime-sync.ts b/apps/web/features/realtime/use-realtime-sync.ts index bb6f0d54..74ba9a71 100644 --- a/apps/web/features/realtime/use-realtime-sync.ts +++ b/apps/web/features/realtime/use-realtime-sync.ts @@ -13,6 +13,7 @@ import type { MemberAddedPayload, WorkspaceDeletedPayload, MemberRemovedPayload, + IssueUpdatedPayload, } from "@/shared/types"; const logger = createLogger("realtime-sync"); @@ -78,7 +79,15 @@ export function useRealtimeSync(ws: WSClient | null) { if (refresh) debouncedRefresh(prefix, refresh); }); - // --- Side-effect handlers (toast, navigation, self-check) --- + // --- Side-effect handlers (toast, navigation, cross-store sync) --- + + // Keep inbox issue_status in sync when issues change + const unsubIssueUpdated = ws.on("issue:updated", (p) => { + const { issue } = p as IssueUpdatedPayload; + if (issue?.id && issue?.status) { + useInboxStore.getState().updateIssueStatus(issue.id, issue.status); + } + }); const unsubWsDeleted = ws.on("workspace:deleted", (p) => { const { workspace_id } = p as WorkspaceDeletedPayload; @@ -113,6 +122,7 @@ export function useRealtimeSync(ws: WSClient | null) { return () => { unsubAny(); + unsubIssueUpdated(); unsubWsDeleted(); unsubMemberRemoved(); unsubMemberAdded();