feat(web): вход с 2FA сразу, панель пользователя, модальные настройки, markdown
По правкам пользователя: - вход: поле «Код 2FA или резервный код» видно сразу, принимает 14-символьные резервные коды (раньше влезало 6 цифр), пустое значение не ломает вход; - нижняя панель: одна подложка, слева аватар и имя с мини-профилем и выбором статуса, справа микрофон, камера, наушники (переключатели, стрелка с выбором устройства и громкости, переход в «Аудио-видео») и шестерёнка; - кнопка «Покинуть сервер» из сайдбара убрана (выход — в «Опасной зоне»); - настройки открываются модальным окном со списком пунктов; «Профиль» имеет вкладки «Основной» и «Сервер» (список серверов, ник и «Применить ко всем серверам»), аватар загружается и удаляется, есть часовой пояс (МСК по умолчанию); - markdown: добавлены ==выделение==, __жирный__, ***оба***, ||спойлер||, #канал; - двойной клик по сообщению ставит ответ (не срабатывает на ссылках, кнопках и при выделенном тексте); - 219 тестов Vitest (было 176), check/lint/test/build зелёные.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { VolumeSlider, VoiceDeviceList } from '@/components/voice/VoiceControls';
|
||||
import { useDeviceChoice } from '@/lib/useMediaDevices';
|
||||
import { useVoiceStore } from '@/stores/voice';
|
||||
import { SettingsSection } from '@/pages/settings/SettingsSection';
|
||||
|
||||
/**
|
||||
* «Аудио-видео»: выбор устройств ввода/вывода и громкости.
|
||||
*
|
||||
* Значения пока только сохраняются локально (zustand + localStorage):
|
||||
* голосовые комнаты появятся в Фазе 3, тогда их начнёт использовать RTC.
|
||||
*/
|
||||
export function VoiceSettingsSection() {
|
||||
const { t } = useTranslation();
|
||||
const mic = useDeviceChoice('audioinput');
|
||||
const camera = useDeviceChoice('videoinput');
|
||||
const output = useDeviceChoice('audiooutput');
|
||||
const micVolume = useVoiceStore((state) => state.micVolume);
|
||||
const outputVolume = useVoiceStore((state) => state.outputVolume);
|
||||
const setMicVolume = useVoiceStore((state) => state.setMicVolume);
|
||||
const setOutputVolume = useVoiceStore((state) => state.setOutputVolume);
|
||||
|
||||
return (
|
||||
<SettingsSection
|
||||
title={t('settings.voice.title')}
|
||||
description={t('settings.voice.description')}
|
||||
>
|
||||
<div className="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<VoiceDeviceList
|
||||
kind="audioinput"
|
||||
label={t('settings.voice.input')}
|
||||
value={mic.value}
|
||||
onChange={mic.select}
|
||||
/>
|
||||
<VoiceDeviceList
|
||||
kind="videoinput"
|
||||
label={t('settings.voice.camera')}
|
||||
value={camera.value}
|
||||
onChange={camera.select}
|
||||
/>
|
||||
<VoiceDeviceList
|
||||
kind="audiooutput"
|
||||
label={t('settings.voice.output')}
|
||||
value={output.value}
|
||||
onChange={output.select}
|
||||
/>
|
||||
<div className="flex flex-col justify-end gap-2">
|
||||
<VolumeSlider
|
||||
label={t('settings.voice.micVolume')}
|
||||
value={micVolume}
|
||||
onChange={setMicVolume}
|
||||
/>
|
||||
<VolumeSlider
|
||||
label={t('settings.voice.outputVolume')}
|
||||
value={outputVolume}
|
||||
onChange={setOutputVolume}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-4 text-xs text-fg-muted">{t('settings.voice.stub')}</p>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user