realtime: emit and handle chat_deleted for full chat removals
All checks were successful
CI / test (push) Successful in 38s
All checks were successful
CI / test (push) Successful in 38s
This commit is contained in:
@@ -263,7 +263,9 @@ async def delete_chat(
|
||||
await delete_chat_for_user(db, chat_id=chat_id, user_id=current_user.id, payload=ChatDeleteRequest(for_all=for_all))
|
||||
if chat_before_delete and not chat_before_delete.is_saved:
|
||||
realtime_gateway.remove_chat_subscription(chat_id=chat_id, user_id=current_user.id)
|
||||
if not delete_for_all:
|
||||
if delete_for_all:
|
||||
await realtime_gateway.publish_chat_deleted(chat_id=chat_id)
|
||||
else:
|
||||
await realtime_gateway.publish_chat_updated(chat_id=chat_id)
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ RealtimeEventName = Literal[
|
||||
"user_online",
|
||||
"user_offline",
|
||||
"chat_updated",
|
||||
"chat_deleted",
|
||||
"pong",
|
||||
"error",
|
||||
]
|
||||
|
||||
@@ -162,6 +162,13 @@ class RealtimeGateway:
|
||||
payload={"chat_id": chat_id},
|
||||
)
|
||||
|
||||
async def publish_chat_deleted(self, *, chat_id: int) -> None:
|
||||
await self._publish_chat_event(
|
||||
chat_id,
|
||||
event="chat_deleted",
|
||||
payload={"chat_id": chat_id},
|
||||
)
|
||||
|
||||
async def disconnect_user(self, user_id: int, *, code: int = 4401, reason: str = "Session revoked") -> None:
|
||||
user_connections = self._connections.get(user_id, {})
|
||||
if not user_connections:
|
||||
|
||||
@@ -39,8 +39,8 @@ Legend:
|
||||
30. Blacklist - `DONE`
|
||||
31. Privacy - `PARTIAL` (avatar/last-seen/group-invites + PM policy `everyone|contacts|nobody`; policy behavior covered by integration tests, remaining UX/matrix hardening)
|
||||
32. Security - `PARTIAL` (sessions + revoke + 2FA base + access-session visibility; revoke-all now force-disconnects active realtime sessions; 2FA setup now blocked after enable to prevent secret re-issuance; one-time recovery codes added; UX polish 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, chat-info panel hot-refreshes on `chat_updated`, delete/leave now updates realtime subscriptions)
|
||||
33. Realtime Events - `DONE` (connect/disconnect/send/receive/typing/read/delivered/online/offline + chat/message updates + chat_deleted)
|
||||
34. Sync - `PARTIAL` (cross-device via backend state + realtime; reconciliation improved for loaded chats/messages, chat-info panel hot-refreshes on `chat_updated`, delete/leave updates realtime subscriptions, full-chat delete emits `chat_deleted`)
|
||||
35. Additional - `PARTIAL` (drafts/link preview partial/autoload media basic)
|
||||
|
||||
## Current Focus to reach ~80%
|
||||
|
||||
@@ -211,7 +211,21 @@ Sent when chat metadata/membership/roles/title changes:
|
||||
}
|
||||
```
|
||||
|
||||
## 3.8 `error`
|
||||
## 3.8 `chat_deleted`
|
||||
|
||||
Sent when a chat is removed for all members/subscribers:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "chat_deleted",
|
||||
"payload": {
|
||||
"chat_id": 10
|
||||
},
|
||||
"timestamp": "2026-03-08T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 3.9 `error`
|
||||
|
||||
Validation/runtime error during WS processing:
|
||||
|
||||
@@ -236,6 +250,7 @@ Validation/runtime error during WS processing:
|
||||
- Send periodic `ping` and expect `pong`.
|
||||
- Reconnect with exponential backoff.
|
||||
- On `chat_updated`, refresh chat metadata via REST (`GET /api/v1/chats` or `GET /api/v1/chats/{chat_id}`).
|
||||
- On `chat_deleted`, remove the chat from local state immediately and clear active-chat selection if needed.
|
||||
- On reconnect/visibility restore, reconcile state by reloading already-opened chats/messages via REST
|
||||
to recover missed `message_deleted`/delivery updates after transient disconnects or backend restarts.
|
||||
- For browser notifications, mentions (`@username`) should be treated as high-priority and can bypass
|
||||
|
||||
@@ -140,6 +140,13 @@ export function useRealtime() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event.event === "chat_deleted") {
|
||||
const chatId = Number(event.payload.chat_id);
|
||||
if (Number.isFinite(chatId)) {
|
||||
chatStore.removeChat(chatId);
|
||||
scheduleReloadChats();
|
||||
}
|
||||
}
|
||||
if (event.event === "pong") {
|
||||
lastPongAtRef.current = Date.now();
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ interface ChatState {
|
||||
setEditingMessage: (chatId: number, message: Message | null) => void;
|
||||
updateChatPinnedMessage: (chatId: number, pinnedMessageId: number | null) => void;
|
||||
applyPresenceEvent: (chatId: number, userId: number, isOnline: boolean, lastSeenAt?: string) => void;
|
||||
removeChat: (chatId: number) => void;
|
||||
setDraft: (chatId: number, text: string) => void;
|
||||
clearDraft: (chatId: number) => void;
|
||||
setFocusedMessage: (chatId: number, messageId: number | null) => void;
|
||||
@@ -431,6 +432,42 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
return chat;
|
||||
})
|
||||
})),
|
||||
removeChat: (chatId) =>
|
||||
set((state) => {
|
||||
const nextMessagesByChat = { ...state.messagesByChat };
|
||||
const nextHasMoreByChat = { ...state.hasMoreByChat };
|
||||
const nextLoadingMoreByChat = { ...state.loadingMoreByChat };
|
||||
const nextTypingByChat = { ...state.typingByChat };
|
||||
const nextReplyToByChat = { ...state.replyToByChat };
|
||||
const nextEditingByChat = { ...state.editingByChat };
|
||||
const nextUnreadBoundaryByChat = { ...state.unreadBoundaryByChat };
|
||||
const nextFocusedMessageByChat = { ...state.focusedMessageIdByChat };
|
||||
const nextDraftsByChat = { ...state.draftsByChat };
|
||||
delete nextMessagesByChat[chatId];
|
||||
delete nextHasMoreByChat[chatId];
|
||||
delete nextLoadingMoreByChat[chatId];
|
||||
delete nextTypingByChat[chatId];
|
||||
delete nextReplyToByChat[chatId];
|
||||
delete nextEditingByChat[chatId];
|
||||
delete nextUnreadBoundaryByChat[chatId];
|
||||
delete nextFocusedMessageByChat[chatId];
|
||||
delete nextDraftsByChat[chatId];
|
||||
saveDraftsToStorage(nextDraftsByChat);
|
||||
const nextChats = state.chats.filter((chat) => chat.id !== chatId);
|
||||
return {
|
||||
chats: nextChats,
|
||||
activeChatId: state.activeChatId === chatId ? (nextChats[0]?.id ?? null) : state.activeChatId,
|
||||
messagesByChat: nextMessagesByChat,
|
||||
hasMoreByChat: nextHasMoreByChat,
|
||||
loadingMoreByChat: nextLoadingMoreByChat,
|
||||
typingByChat: nextTypingByChat,
|
||||
replyToByChat: nextReplyToByChat,
|
||||
editingByChat: nextEditingByChat,
|
||||
unreadBoundaryByChat: nextUnreadBoundaryByChat,
|
||||
focusedMessageIdByChat: nextFocusedMessageByChat,
|
||||
draftsByChat: nextDraftsByChat,
|
||||
};
|
||||
}),
|
||||
setDraft: (chatId, text) =>
|
||||
set((state) => {
|
||||
const nextDrafts = {
|
||||
|
||||
Reference in New Issue
Block a user