import { create } from 'zustand'; import { fetchRelationships, type RelationshipUser, type Relationships } from '@/api/friends'; import type { UserStatus } from '@/api/types'; /** * Друзья и заявки (AGENT.md §7.8). * * Список живёт в Zustand, а не в TanStack Query: он обновляется событиями * шлюза (`RELATIONSHIP_UPDATE`, `PRESENCE_UPDATE`) точечно, без перезагрузки. * Личные беседы остаются в сторе сессии (`dmChannels` из снапшота READY), * непрочитанные считаются по стору сообщений — здесь только связи. */ export type FriendsStatus = 'idle' | 'loading' | 'ready' | 'error'; /** Часть `PRESENCE_UPDATE`: сервер присылает только изменившиеся поля. */ export interface PresencePatch { status?: UserStatus; visible_status?: UserStatus; custom_status?: string; last_seen_at?: string; } interface FriendsState { friends: RelationshipUser[]; incoming: RelationshipUser[]; outgoing: RelationshipUser[]; blocked: RelationshipUser[]; status: FriendsStatus; error: unknown; /** * Загрузить связи. Повторный вызов при `ready` ничего не делает — компоненты * могут звать его из эффекта, не опасаясь цикла запросов. */ load: (options?: { force?: boolean }) => Promise; setRelationships: (data: Relationships) => void; /** RELATIONSHIP_UPDATE: перечитать список связей. */ reload: () => Promise; /** PRESENCE_UPDATE: обновить статус друга без перезагрузки списка. */ applyPresence: (userId: string, patch: PresencePatch) => void; reset: () => void; } const defaults = { friends: [] as RelationshipUser[], incoming: [] as RelationshipUser[], outgoing: [] as RelationshipUser[], blocked: [] as RelationshipUser[], status: 'idle' as FriendsStatus, error: null as unknown, }; /** Применяет патч присутствия к одной записи списка. */ function withPresence(user: RelationshipUser, patch: PresencePatch): RelationshipUser { const next: RelationshipUser = { ...user }; if (patch.status !== undefined) { next.status = patch.status; } if (patch.visible_status !== undefined) { next.visible_status = patch.visible_status; } else if (patch.status !== undefined) { // Статус без явного «видимого» считаем видимым как есть. next.visible_status = patch.status; } if (patch.custom_status !== undefined) { next.custom_status = patch.custom_status; } if (patch.last_seen_at !== undefined) { next.last_seen_at = patch.last_seen_at; } return next; } export const useFriendsStore = create((set, get) => ({ ...defaults, load: async (options) => { const current = get().status; if (options?.force !== true && (current === 'loading' || current === 'ready')) { return; } set({ status: 'loading', error: null }); try { const data = await fetchRelationships(); set({ ...data, status: 'ready', error: null }); } catch (error) { set({ status: 'error', error }); } }, setRelationships: (data) => { set({ ...data, status: 'ready', error: null }); }, reload: async () => { await get().load({ force: true }); }, applyPresence: (userId, patch) => { const state = get(); const update = (list: RelationshipUser[]): RelationshipUser[] => list.map((user) => (user.user_id === userId ? withPresence(user, patch) : user)); set({ friends: update(state.friends), incoming: update(state.incoming), outgoing: update(state.outgoing), blocked: update(state.blocked), }); }, reset: () => { set({ ...defaults }); }, })); /** Друг по id (для шапки личной беседы). */ export function selectFriendById(userId: string | undefined) { return (state: FriendsState): RelationshipUser | null => userId === undefined ? null : (state.friends.find((user) => user.user_id === userId) ?? null); }