123 lines
4.2 KiB
TypeScript
123 lines
4.2 KiB
TypeScript
|
|
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<void>;
|
|||
|
|
setRelationships: (data: Relationships) => void;
|
|||
|
|
/** RELATIONSHIP_UPDATE: перечитать список связей. */
|
|||
|
|
reload: () => Promise<void>;
|
|||
|
|
/** 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<FriendsState>((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);
|
|||
|
|
}
|