web(realtime): refresh chat info panel on chat updates
Some checks are pending
CI / test (push) Has started running

This commit is contained in:
2026-03-08 18:56:54 +03:00
parent 661f8acf63
commit 8830192642
3 changed files with 64 additions and 47 deletions

View File

@@ -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%

View File

@@ -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") {

View File

@@ -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);