feat(realtime): live online/offline events and unified search
Some checks failed
CI / test (push) Failing after 18s

- add websocket events user_online/user_offline
- broadcast presence changes on first connect and final disconnect only
- apply live presence updates in web chat store and realtime hook
- move public discover into unified left search (users + groups/channels)
- remove separate Discover Chats dialog/menu entry
This commit is contained in:
2026-03-08 02:12:11 +03:00
parent afeb0acbe7
commit 46dc601c84
7 changed files with 193 additions and 63 deletions

View File

@@ -37,6 +37,7 @@ interface ChatState {
setTypingUsers: (chatId: number, userIds: number[]) => void;
setReplyToMessage: (chatId: number, message: Message | null) => void;
updateChatPinnedMessage: (chatId: number, pinnedMessageId: number | null) => void;
applyPresenceEvent: (chatId: number, userId: number, isOnline: boolean, lastSeenAt?: string) => void;
}
export const useChatStore = create<ChatState>((set, get) => ({
@@ -244,5 +245,32 @@ export const useChatStore = create<ChatState>((set, get) => ({
updateChatPinnedMessage: (chatId, pinnedMessageId) =>
set((state) => ({
chats: state.chats.map((chat) => (chat.id === chatId ? { ...chat, pinned_message_id: pinnedMessageId } : chat))
})),
applyPresenceEvent: (chatId, userId, isOnline, lastSeenAt) =>
set((state) => ({
chats: state.chats.map((chat) => {
if (chat.id !== chatId) {
return chat;
}
if (chat.type === "private" && chat.counterpart_user_id === userId) {
return {
...chat,
counterpart_is_online: isOnline,
counterpart_last_seen_at: isOnline ? chat.counterpart_last_seen_at : (lastSeenAt ?? new Date().toISOString())
};
}
if (chat.type === "group") {
const currentOnline = chat.online_count ?? 0;
const membersCount = chat.members_count ?? currentOnline;
const nextOnline = isOnline
? Math.min(membersCount, currentOnline + 1)
: Math.max(0, currentOnline - 1);
return {
...chat,
online_count: nextOnline
};
}
return chat;
})
}))
}));