20eed227ca
По правкам пользователя: - вход: поле «Код 2FA или резервный код» видно сразу, принимает 14-символьные резервные коды (раньше влезало 6 цифр), пустое значение не ломает вход; - нижняя панель: одна подложка, слева аватар и имя с мини-профилем и выбором статуса, справа микрофон, камера, наушники (переключатели, стрелка с выбором устройства и громкости, переход в «Аудио-видео») и шестерёнка; - кнопка «Покинуть сервер» из сайдбара убрана (выход — в «Опасной зоне»); - настройки открываются модальным окном со списком пунктов; «Профиль» имеет вкладки «Основной» и «Сервер» (список серверов, ник и «Применить ко всем серверам»), аватар загружается и удаляется, есть часовой пояс (МСК по умолчанию); - markdown: добавлены ==выделение==, __жирный__, ***оба***, ||спойлер||, #канал; - двойной клик по сообщению ставит ответ (не срабатывает на ссылках, кнопках и при выделенном тексте); - 219 тестов Vitest (было 176), check/lint/test/build зелёные.
90 lines
3.0 KiB
TypeScript
90 lines
3.0 KiB
TypeScript
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;
|
||
}
|