diff --git a/docs/core-checklist-status.md b/docs/core-checklist-status.md index c7470fd..272a414 100644 --- a/docs/core-checklist-status.md +++ b/docs/core-checklist-status.md @@ -40,7 +40,7 @@ Legend: 31. Privacy - `PARTIAL` (avatar/last-seen/group-invites + PM policy `everyone|contacts|nobody`; remaining edge UX/matrix hardening) 32. Security - `PARTIAL` (sessions + revoke + 2FA base + access-session visibility; UX/TOTP recovery flow ongoing) 33. Realtime Events - `DONE` (connect/disconnect/send/receive/typing/read/delivered/online/offline + chat/message updates) -34. Sync - `PARTIAL` (cross-device via backend state + realtime; reconciliation improved for loaded chats/messages) +34. Sync - `PARTIAL` (cross-device via backend state + realtime; reconciliation improved for loaded chats/messages, chat-info panel now hot-refreshes on `chat_updated`) 35. Additional - `PARTIAL` (drafts/link preview partial/autoload media basic) ## Current Focus to reach ~80% diff --git a/web/src/components/ChatInfoPanel.tsx b/web/src/components/ChatInfoPanel.tsx index 1e99f91..11f8b04 100644 --- a/web/src/components/ChatInfoPanel.tsx +++ b/web/src/components/ChatInfoPanel.tsx @@ -107,6 +107,49 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) { setMemberUsers(byId); } + async function refreshPanelData(targetChatId: number, withLoading = false) { + if (withLoading) { + setLoading(true); + setError(null); + setAttachmentsLoading(true); + } + try { + const detail = await getChatDetail(targetChatId); + setChat(detail); + setTitleDraft((prev) => (prev.trim() ? prev : (detail.title ?? ""))); + const notificationSettings = await getChatNotificationSettings(targetChatId); + setMuted(notificationSettings.muted); + if (detail.type === "private" && !detail.is_saved && detail.counterpart_user_id) { + try { + const counterpart = await getUserById(detail.counterpart_user_id); + setCounterpartProfile(counterpart); + const blocked = await listBlockedUsers(); + setCounterpartBlocked(blocked.some((u) => u.id === detail.counterpart_user_id)); + } catch { + setCounterpartProfile(null); + setCounterpartBlocked(false); + } + } else { + setCounterpartProfile(null); + setCounterpartBlocked(false); + } + await refreshMembers(targetChatId); + const chatAttachments = await getChatAttachments(targetChatId, 120); + const messages = await getRecentMessagesForLinks(targetChatId); + setAttachments(chatAttachments); + setLinkItems(extractLinkItems(messages)); + } catch { + if (withLoading) { + setError("Failed to load chat info"); + } + } finally { + if (withLoading) { + setLoading(false); + setAttachmentsLoading(false); + } + } + } + function jumpToMessage(messageId: number) { if (!chatId) { return; @@ -140,53 +183,10 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) { return; } let cancelled = false; - setLoading(true); - setError(null); - setAttachmentsLoading(true); void (async () => { - try { - const detail = await getChatDetail(chatId); - if (cancelled) return; - setChat(detail); - setTitleDraft(detail.title ?? ""); - const notificationSettings = await getChatNotificationSettings(chatId); - if (!cancelled) { - setMuted(notificationSettings.muted); - } - if (detail.type === "private" && !detail.is_saved && detail.counterpart_user_id) { - try { - const counterpart = await getUserById(detail.counterpart_user_id); - if (!cancelled) { - setCounterpartProfile(counterpart); - } - const blocked = await listBlockedUsers(); - if (!cancelled) { - setCounterpartBlocked(blocked.some((u) => u.id === detail.counterpart_user_id)); - } - } catch { - if (!cancelled) { - setCounterpartProfile(null); - setCounterpartBlocked(false); - } - } - } else if (!cancelled) { - setCounterpartProfile(null); - setCounterpartBlocked(false); - } - await refreshMembers(chatId); - const chatAttachments = await getChatAttachments(chatId, 120); - const messages = await getRecentMessagesForLinks(chatId); - if (!cancelled) { - setAttachments(chatAttachments); - setLinkItems(extractLinkItems(messages)); - } - } catch { - if (!cancelled) setError("Failed to load chat info"); - } finally { - if (!cancelled) { - setLoading(false); - setAttachmentsLoading(false); - } + await refreshPanelData(chatId, true); + if (cancelled) { + return; } })(); return () => { @@ -205,6 +205,22 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) { setSearchResults([]); }, [chatId, open]); + useEffect(() => { + if (!open || !chatId) { + return; + } + const onRealtimeChatUpdated = (event: Event) => { + const realtimeEvent = event as CustomEvent<{ chatId?: number }>; + const updatedChatId = Number(realtimeEvent.detail?.chatId); + if (!Number.isFinite(updatedChatId) || updatedChatId !== chatId) { + return; + } + void refreshPanelData(chatId, false); + }; + window.addEventListener("bm:chat-updated", onRealtimeChatUpdated); + return () => window.removeEventListener("bm:chat-updated", onRealtimeChatUpdated); + }, [open, chatId]); + useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { diff --git a/web/src/hooks/useRealtime.ts b/web/src/hooks/useRealtime.ts index 04a3785..46f79d4 100644 --- a/web/src/hooks/useRealtime.ts +++ b/web/src/hooks/useRealtime.ts @@ -133,6 +133,7 @@ export function useRealtime() { if (event.event === "chat_updated") { const chatId = Number(event.payload.chat_id); if (Number.isFinite(chatId)) { + window.dispatchEvent(new CustomEvent("bm:chat-updated", { detail: { chatId } })); scheduleReloadChats(); if (chatStore.activeChatId === chatId || (chatStore.messagesByChat[chatId]?.length ?? 0) > 0) { void chatStore.loadMessages(chatId);