Files
glchat/web/src/stores/friends.ts
T
grendervill d02ef7527a fix(files): аватары и баннеры доступны авторизованным пользователям
Аватар нельзя было показать в списке друзей: файл без комнаты был доступен
только загрузившему. Добавлено назначение файла (`files.purpose`: attachment |
avatar | banner, миграция 00006): аватары и баннеры отдаются любому
авторизованному пользователю, вложения по-прежнему проверяются по правам
комнаты, а непривязанные загрузки остаются приватными.
2026-09-20 01:39:04 +03:00

123 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}