- 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>
136 lines
4.3 KiB
TypeScript
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: {} });
|
|
},
|
|
}));
|