Files
glchat/web/src/api/users.ts
T
grendervill 20eed227ca feat(web): вход с 2FA сразу, панель пользователя, модальные настройки, markdown
По правкам пользователя:

- вход: поле «Код 2FA или резервный код» видно сразу, принимает 14-символьные
  резервные коды (раньше влезало 6 цифр), пустое значение не ломает вход;
- нижняя панель: одна подложка, слева аватар и имя с мини-профилем и выбором
  статуса, справа микрофон, камера, наушники (переключатели, стрелка с выбором
  устройства и громкости, переход в «Аудио-видео») и шестерёнка;
- кнопка «Покинуть сервер» из сайдбара убрана (выход — в «Опасной зоне»);
- настройки открываются модальным окном со списком пунктов; «Профиль» имеет
  вкладки «Основной» и «Сервер» (список серверов, ник и «Применить ко всем
  серверам»), аватар загружается и удаляется, есть часовой пояс (МСК по
  умолчанию);
- markdown: добавлены ==выделение==, __жирный__, ***оба***, ||спойлер||, #канал;
- двойной клик по сообщению ставит ответ (не срабатывает на ссылках, кнопках и
  при выделенном тексте);
- 219 тестов Vitest (было 176), check/lint/test/build зелёные.
2026-09-20 01:32:11 +03:00

90 lines
3.0 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 { request } from './client';
import type { GuildSummary, User, UserSummary } from './types';
export interface UpdateProfileInput {
display_name?: string;
bio?: string;
status?: string;
custom_status?: string;
custom_status_emoji?: string;
locale?: string;
/** Часовой пояс в формате IANA (например `Europe/Moscow`). */
timezone?: string;
}
export interface OnboardingInput {
display_name?: string;
bio?: string;
locale?: string;
}
export const currentUserQueryKey = ['users', '@me'] as const;
export const myGuildsQueryKey = ['users', '@me', 'guilds'] as const;
export function userQueryKey(userId: string) {
return ['users', userId] as const;
}
export async function fetchCurrentUser(signal?: AbortSignal): Promise<User> {
const payload = await request<{ user: User }>(
'/users/@me',
signal === undefined ? {} : { signal },
);
return payload.user;
}
export async function updateProfile(input: UpdateProfileInput): Promise<User> {
const payload = await request<{ user: User }>('/users/@me', { method: 'PATCH', body: input });
return payload.user;
}
export async function completeOnboarding(input: OnboardingInput): Promise<User> {
const payload = await request<{ user: User }>('/users/@me/onboarding/complete', {
method: 'POST',
body: input,
});
return payload.user;
}
/** Максимальный размер аватара: сервер отвечает `file.too_large` выше лимита. */
export const AVATAR_MAX_BYTES = 8 * 1024 * 1024;
/**
* Загрузка аватара: `POST /users/@me/avatar` multipart с полем `file`.
*
* Сервер может ответить как полным профилем, так и только идентификатором
* файла, поэтому возвращаем профиль, если он есть, иначе `null` — вызывающий
* код в этом случае перечитывает `GET /users/@me`.
*/
export async function uploadAvatar(file: File): Promise<User | null> {
const form = new FormData();
form.append('file', file);
const payload = await request<{ user?: User }>('/users/@me/avatar', {
method: 'POST',
form,
});
return payload.user ?? null;
}
/** Удаление аватара: `DELETE /users/@me/avatar`. */
export async function deleteAvatar(): Promise<User | null> {
const payload = await request<{ user?: User }>('/users/@me/avatar', { method: 'DELETE' });
return payload.user ?? null;
}
export async function fetchMyGuilds(signal?: AbortSignal): Promise<GuildSummary[]> {
const payload = await request<{ guilds: GuildSummary[] }>(
'/users/@me/guilds',
signal === undefined ? {} : { signal },
);
return payload.guilds;
}
export async function fetchUser(userId: string, signal?: AbortSignal): Promise<UserSummary> {
const payload = await request<{ user: UserSummary }>(
`/users/${encodeURIComponent(userId)}`,
signal === undefined ? {} : { signal },
);
return payload.user;
}