Files
janichat/frontend/src/store/index.ts
Ai 20daa2ca2f fix: read receipts (single/double check), push notifications for all users
- Track when others read messages via messages_read WS event
- Show Check (single) for sent, CheckCheck (double) only when read
- Send push to all recipients, SW suppresses if app is focused

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 13:47:38 +03:00

136 lines
4.3 KiB
TypeScript

import { create } from 'zustand';
import { Chat, Message, User } from '../types';
interface AppStore {
user: User | null;
chats: Chat[];
activeChat: Chat | null;
messages: Record<string, Message[]>;
onlineUsers: Set<string>;
typingUsers: Record<string, string[]>;
connected: boolean;
chatReadAt: Record<string, string>; // chatId → ISO timestamp others last read
setUser: (user: User | null) => void;
setChats: (chats: Chat[]) => void;
updateChat: (chat: Partial<Chat> & { id: string }) => void;
setActiveChat: (chat: Chat | null) => void;
setMessages: (chatId: string, messages: Message[]) => void;
prependMessages: (chatId: string, messages: Message[]) => void;
addMessage: (message: Message) => void;
updateMessage: (message: Message) => void;
removeMessage: (chatId: string, messageId: string) => void;
setOnline: (userId: string, online: boolean) => void;
setTyping: (chatId: string, userId: string, typing: boolean) => void;
setConnected: (v: boolean) => void;
markRead: (chatId: string) => void;
markReadByOther: (chatId: string) => void;
logout: () => void;
}
function loadUser(): User | null {
try {
const s = localStorage.getItem('jc_user');
return s ? JSON.parse(s) : null;
} catch { return null; }
}
export const useStore = create<AppStore>((set, get) => ({
user: loadUser(),
chats: [],
activeChat: null,
messages: {},
onlineUsers: new Set(),
typingUsers: {},
connected: false,
chatReadAt: {},
setUser: (user) => set({ user }),
setChats: (chats) => set({ chats }),
updateChat: (partial) => set(state => ({
chats: state.chats.map(c => c.id === partial.id ? { ...c, ...partial } : c),
activeChat: state.activeChat?.id === partial.id ? { ...state.activeChat, ...partial } : state.activeChat,
})),
setActiveChat: (chat) => set({ activeChat: chat }),
setMessages: (chatId, messages) => set(state => ({
messages: { ...state.messages, [chatId]: messages }
})),
prependMessages: (chatId, messages) => set(state => ({
messages: {
...state.messages,
[chatId]: [...messages, ...(state.messages[chatId] || [])]
}
})),
addMessage: (message) => set(state => {
const existing = state.messages[message.chatId] || [];
if (existing.find(m => m.id === message.id)) return state;
const updated = [...existing, message];
// Update chat last message
const chats = state.chats.map(c =>
c.id === message.chatId
? { ...c, lastMessage: message.content, lastMessageAt: message.createdAt,
unreadCount: state.activeChat?.id === message.chatId ? 0 : c.unreadCount + 1 }
: c
).sort((a, b) => {
const ta = a.lastMessageAt || a.createdAt;
const tb = b.lastMessageAt || b.createdAt;
return new Date(tb).getTime() - new Date(ta).getTime();
});
return { messages: { ...state.messages, [message.chatId]: updated }, chats };
}),
updateMessage: (message) => set(state => ({
messages: {
...state.messages,
[message.chatId]: (state.messages[message.chatId] || []).map(m =>
m.id === message.id ? message : m
)
}
})),
removeMessage: (chatId, messageId) => set(state => ({
messages: {
...state.messages,
[chatId]: (state.messages[chatId] || []).map(m =>
m.id === messageId ? { ...m, isDeleted: true, content: 'Сообщение удалено' } : m
)
}
})),
setOnline: (userId, online) => set(state => {
const next = new Set(state.onlineUsers);
online ? next.add(userId) : next.delete(userId);
return { onlineUsers: next };
}),
setTyping: (chatId, userId, typing) => set(state => {
const current = state.typingUsers[chatId] || [];
const next = typing
? [...new Set([...current, userId])]
: current.filter(id => id !== userId);
return { typingUsers: { ...state.typingUsers, [chatId]: next } };
}),
setConnected: (v) => set({ connected: v }),
markRead: (chatId) => set(state => ({
chats: state.chats.map(c => c.id === chatId ? { ...c, unreadCount: 0 } : c)
})),
markReadByOther: (chatId) => set(state => ({
chatReadAt: { ...state.chatReadAt, [chatId]: new Date().toISOString() }
})),
logout: () => {
localStorage.removeItem('jc_token');
localStorage.removeItem('jc_user');
set({ user: null, chats: [], activeChat: null, messages: {}, connected: false, chatReadAt: {} });
},
}));