web(realtime): refresh chat info panel on chat updates
Some checks are pending
CI / test (push) Has started running
Some checks are pending
CI / test (push) Has started running
This commit is contained in:
@@ -40,7 +40,7 @@ Legend:
|
|||||||
31. Privacy - `PARTIAL` (avatar/last-seen/group-invites + PM policy `everyone|contacts|nobody`; remaining edge UX/matrix hardening)
|
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)
|
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)
|
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)
|
35. Additional - `PARTIAL` (drafts/link preview partial/autoload media basic)
|
||||||
|
|
||||||
## Current Focus to reach ~80%
|
## Current Focus to reach ~80%
|
||||||
|
|||||||
@@ -107,6 +107,49 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
|||||||
setMemberUsers(byId);
|
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) {
|
function jumpToMessage(messageId: number) {
|
||||||
if (!chatId) {
|
if (!chatId) {
|
||||||
return;
|
return;
|
||||||
@@ -140,53 +183,10 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
setAttachmentsLoading(true);
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
await refreshPanelData(chatId, true);
|
||||||
const detail = await getChatDetail(chatId);
|
if (cancelled) {
|
||||||
if (cancelled) return;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
return () => {
|
return () => {
|
||||||
@@ -205,6 +205,22 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
|||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
}, [chatId, open]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ export function useRealtime() {
|
|||||||
if (event.event === "chat_updated") {
|
if (event.event === "chat_updated") {
|
||||||
const chatId = Number(event.payload.chat_id);
|
const chatId = Number(event.payload.chat_id);
|
||||||
if (Number.isFinite(chatId)) {
|
if (Number.isFinite(chatId)) {
|
||||||
|
window.dispatchEvent(new CustomEvent("bm:chat-updated", { detail: { chatId } }));
|
||||||
scheduleReloadChats();
|
scheduleReloadChats();
|
||||||
if (chatStore.activeChatId === chatId || (chatStore.messagesByChat[chatId]?.length ?? 0) > 0) {
|
if (chatStore.activeChatId === chatId || (chatStore.messagesByChat[chatId]?.length ?? 0) > 0) {
|
||||||
void chatStore.loadMessages(chatId);
|
void chatStore.loadMessages(chatId);
|
||||||
|
|||||||
Reference in New Issue
Block a user