feat(web): вход с 2FA сразу, панель пользователя, модальные настройки, markdown
По правкам пользователя: - вход: поле «Код 2FA или резервный код» видно сразу, принимает 14-символьные резервные коды (раньше влезало 6 цифр), пустое значение не ломает вход; - нижняя панель: одна подложка, слева аватар и имя с мини-профилем и выбором статуса, справа микрофон, камера, наушники (переключатели, стрелка с выбором устройства и громкости, переход в «Аудио-видео») и шестерёнка; - кнопка «Покинуть сервер» из сайдбара убрана (выход — в «Опасной зоне»); - настройки открываются модальным окном со списком пунктов; «Профиль» имеет вкладки «Основной» и «Сервер» (список серверов, ник и «Применить ко всем серверам»), аватар загружается и удаляется, есть часовой пояс (МСК по умолчанию); - markdown: добавлены ==выделение==, __жирный__, ***оба***, ||спойлер||, #канал; - двойной клик по сообщению ставит ответ (не срабатывает на ссылках, кнопках и при выделенном тексте); - 219 тестов Vitest (было 176), check/lint/test/build зелёные.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Часовые пояса для настроек профиля и раздела «Язык и регион».
|
||||
*
|
||||
* Значение — идентификатор IANA, как его принимает сервер; подпись собирается
|
||||
* из названия зоны и текущего смещения (смещение считает `Intl`, поэтому
|
||||
* переход на летнее время учитывается автоматически).
|
||||
*/
|
||||
|
||||
/** Пояс по умолчанию: Москва (МСК). */
|
||||
export const DEFAULT_TIMEZONE = 'Europe/Moscow';
|
||||
|
||||
export const timezoneOptions = [
|
||||
'Europe/Kaliningrad',
|
||||
'Europe/Moscow',
|
||||
'Europe/Samara',
|
||||
'Asia/Yekaterinburg',
|
||||
'Asia/Almaty',
|
||||
'Asia/Tashkent',
|
||||
'Asia/Novosibirsk',
|
||||
'Asia/Krasnoyarsk',
|
||||
'Asia/Irkutsk',
|
||||
'Asia/Yakutsk',
|
||||
'Asia/Vladivostok',
|
||||
'Asia/Magadan',
|
||||
'Asia/Kamchatka',
|
||||
'Europe/London',
|
||||
'Europe/Berlin',
|
||||
'Europe/Paris',
|
||||
'Asia/Tbilisi',
|
||||
'Asia/Yerevan',
|
||||
'Asia/Dubai',
|
||||
'Asia/Tokyo',
|
||||
'Asia/Shanghai',
|
||||
'America/New_York',
|
||||
'America/Los_Angeles',
|
||||
'UTC',
|
||||
] as const;
|
||||
|
||||
/** Смещение вида `UTC+03:00` (для зоны, которой нет в ICU, — пустая строка). */
|
||||
export function timezoneOffset(timeZone: string, locale = 'ru'): string {
|
||||
try {
|
||||
const parts = new Intl.DateTimeFormat(locale, {
|
||||
timeZone,
|
||||
timeZoneName: 'shortOffset',
|
||||
}).formatToParts(new Date());
|
||||
return parts.find((part) => part.type === 'timeZoneName')?.value ?? '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** Подпись пункта списка: `Europe/Moscow (UTC+03:00)`. */
|
||||
export function timezoneLabel(timeZone: string, locale = 'ru'): string {
|
||||
const offset = timezoneOffset(timeZone, locale);
|
||||
return offset === '' ? timeZone : `${timeZone} (${offset})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Все зоны списка плюс значение из профиля, если его там ещё нет: селект не
|
||||
* должен «терять» часовой пояс, выставленный с другого клиента.
|
||||
*/
|
||||
export function timezoneChoices(value: string): string[] {
|
||||
const options: string[] = [...timezoneOptions];
|
||||
if (value !== '' && !options.includes(value)) {
|
||||
options.unshift(value);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
Reference in New Issue
Block a user