Files
glchat/web/src/lib/hooks.ts
T
grendervill b01eb1f34c feat(web): чат Фазы 2 — лента, композер, реакции, ответы, поиск и вложения
Полноценный текстовый чат в веб-клиенте:

- стор сообщений по комнатам (кэш 200, оптимистичная отправка с nonce,
  применение всех событий Gateway, пагинация курсором, непрочитанные и
  упоминания, сброс при логауте и INVALID_SESSION);
- лента: группировка подряд идущих сообщений одного автора, разделители дат,
  разделитель «новые сообщения», подгрузка истории вверх, пустые состояния;
- разметка: жирный/курсив/код/блок кода/зачёркнутый/цитата/ссылки (только
  http(s)) и упоминания <@id> — безопасный рендер React-узлами без
  dangerouslySetInnerHTML;
- реакции чипами с подсветкой своих, быстрые эмодзи и пикер, панель действий и
  контекстное меню (ответ, реакция, закрепление, правка, удаление, копирование);
- ответы с переходом к оригиналу, inline-правка, «печатает…» с POST /typing раз
  в 3 с, пины в модалке, поиск по комнате с подсветкой и переходом;
- композер: автовысота, Enter/Shift+Enter, лимит 4000, drag&drop и скрепка
  (POST /channels/{id}/files → attachment_ids), обработка 429 с таймером и
  perm.denied с причиной, read-only без SEND_MESSAGES;
- бейджи непрочитанных и упоминаний в сайдбаре, авто-POST /ack при активном окне;
- 176 тестов Vitest (было 100), check/lint/test/build зелёные.
2026-09-20 00:15:18 +03:00

53 lines
1.9 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
import { fetchInstance, instanceQueryKey } from '@/api/instance';
import { fetchCurrentUser, currentUserQueryKey } from '@/api/users';
import type { InstanceInfo, User } from '@/api/types';
/** Текущий пользователь: единственный источник правды о сессии (cookie). */
export function useCurrentUser(): UseQueryResult<User> {
return useQuery({
queryKey: currentUserQueryKey,
queryFn: ({ signal }) => fetchCurrentUser(signal),
retry: false,
staleTime: 30_000,
});
}
export function useInstance(): UseQueryResult<InstanceInfo> {
return useQuery({
queryKey: instanceQueryKey,
queryFn: ({ signal }) => fetchInstance(signal),
staleTime: 60_000,
});
}
/**
* Активно ли окно: вкладка видима и окно в фокусе.
*
* Нужен для отметки прочтения — `POST /ack` уходит только когда пользователь
* действительно смотрит на комнату.
*/
export function useWindowActive(): boolean {
const [active, setActive] = useState(
() => typeof document === 'undefined' || document.visibilityState !== 'hidden',
);
useEffect(() => {
const onFocus = (): void => setActive(true);
const onBlur = (): void => setActive(false);
const onVisibility = (): void => setActive(document.visibilityState !== 'hidden');
window.addEventListener('focus', onFocus);
window.addEventListener('blur', onBlur);
document.addEventListener('visibilitychange', onVisibility);
return () => {
window.removeEventListener('focus', onFocus);
window.removeEventListener('blur', onBlur);
document.removeEventListener('visibilitychange', onVisibility);
};
}, []);
return active;
}