619bd91df4
Клиентская часть Фазы 3 (AGENT.md 7.13, 7.14): звуки сервера, локальные
звуки интерфейса и телеметрия медиа.
- саундборд: `api/sounds.ts` (лимиты 512 КБ и 30 на вид, multipart-загрузка,
play, `SOUND_EVENTS`), `lib/soundboard.ts` (blob-кэш `/files/{id}`,
наложение до 8 звуков, громкость = `outputVolume`, тишина при дефене),
попап в панели комнаты (`USE_SOUNDBOARD`), таймер на `rate_limited`,
подпись «включил звук» и защита от двойного воспроизведения;
- звуковая палитра: `SOUNDBOARD_PLAY` и события сервера (вход/выход
участника, упоминание, сообщение, вход/выход из голосовой, свои
микрофон/камера/экран, серверный мьют) → `playEventSound`;
- секция «Звуки» в настройках сервера (`MANAGE_SOUNDS`): два вида со
счётчиками, drag&drop, имя, эмодзи, событие палитры, переименование,
удаление и клиентские проверки файла;
- push-to-talk: режим и клавиша (`event.code`), хук в `AppLayout`, вход в
комнату с выключенным микрофоном, удержание включает публикацию, отпускание
и потеря фокуса выключают, серверный мьют приоритетнее;
- статистика: поповер с разрешением, fps, кодеком, битрейтом, RTT, потерями
и jitter (опрос раз в секунду, остановка при закрытии), выбор качества
публикации low/medium/high, пресеты шаринга экрана 1080p60…480p30;
- данные: `guilds[].sounds` из READY, `GUILD_SOUNDS_UPDATE`, селектор
`useGuildSounds`, тумблеры звуков в «Аудио-видео» (localStorage);
- тесты: `soundboard`, `soundSettings`, `pushToTalk`, `voiceStats` и
расширенный фейковый LiveKit SDK.
53 lines
2.0 KiB
TypeScript
53 lines
2.0 KiB
TypeScript
import { useTranslation } from 'react-i18next';
|
|
|
|
import { useAuthorDirectory } from '@/components/chat/useAuthorDirectory';
|
|
import { useSoundboardStore } from '@/stores/soundboard';
|
|
|
|
/**
|
|
* Всплывающая подпись «{имя} включил звук {name}» (AGENT.md 7.13).
|
|
*
|
|
* Живёт в оболочке приложения — рядом с подсказкой о выключенном микрофоне,
|
|
* поэтому её видно с любого экрана. Гаснет сама: время жизни держит
|
|
* `stores/soundboard` (3 секунды), компонент только рисует текст.
|
|
*/
|
|
export function SoundboardNotice() {
|
|
const { t } = useTranslation();
|
|
const notice = useSoundboardStore((state) => state.notice);
|
|
const clear = useSoundboardStore((state) => state.clearNotice);
|
|
// Имя того, кто сыграл звук, резолвит общий справочник авторов.
|
|
const authors = useAuthorDirectory(
|
|
notice?.guildId ?? null,
|
|
notice === null ? [] : [notice.userId],
|
|
);
|
|
|
|
if (notice === null) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
role="status"
|
|
aria-live="polite"
|
|
data-testid="soundboard-notice"
|
|
className="pointer-events-auto fixed bottom-40 left-1/2 z-50 flex -translate-x-1/2 items-center gap-2 rounded-[var(--radius-md)] border border-border/60 bg-surface-2 px-3 py-2 text-sm shadow-[var(--shadow-3)]"
|
|
>
|
|
<span aria-hidden="true">🔊</span>
|
|
<p>
|
|
{t('voice.soundboard.playedBy', {
|
|
name: authors.resolve(notice.userId).name,
|
|
sound: notice.soundName,
|
|
})}
|
|
</p>
|
|
<button
|
|
type="button"
|
|
aria-label={t('common.dismiss')}
|
|
title={t('common.dismiss')}
|
|
onClick={clear}
|
|
className="rounded-[var(--radius-sm)] px-1 text-fg-muted transition-colors duration-[var(--duration-fast)] hover:bg-surface-3 hover:text-fg"
|
|
>
|
|
<span aria-hidden="true">✕</span>
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|