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.
59 lines
2.7 KiB
TypeScript
59 lines
2.7 KiB
TypeScript
/**
|
||
* Системные сообщения (AGENT.md §7.6).
|
||
*
|
||
* Сервер пишет их с `type: "system"`, а `content` — JSON-строка о событии:
|
||
* `{"event":"member_join","user_id":"123"}`, `{"event":"member_kick",…}` или
|
||
* `{"event":"channel_created","name":"общий","type":"text"}`. Разбор живёт
|
||
* отдельно от компонентов: лента рисует человекочитаемую строку, а
|
||
* справочник авторов по нему же добирает идентификаторы участников.
|
||
*
|
||
* Неизвестное событие и невалидный JSON возвращают `null` — вызывающий код
|
||
* показывает исходный текст сообщения как есть, ничего не теряя.
|
||
*/
|
||
|
||
/** Событие системного сообщения. */
|
||
export type SystemMessageEvent =
|
||
| { kind: 'member_join'; userId: string }
|
||
| { kind: 'member_leave'; userId: string }
|
||
| { kind: 'member_kick'; userId: string }
|
||
| { kind: 'channel_created'; name: string; channelType: string };
|
||
|
||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||
return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : null;
|
||
}
|
||
|
||
/** Разбор `content` системного сообщения. */
|
||
export function parseSystemMessage(content: string): SystemMessageEvent | null {
|
||
let payload: unknown;
|
||
try {
|
||
payload = JSON.parse(content) as unknown;
|
||
} catch {
|
||
// Не JSON — показываем текст как есть.
|
||
return null;
|
||
}
|
||
const record = asRecord(payload);
|
||
const event = record?.['event'];
|
||
const userId = record?.['user_id'];
|
||
if (event === 'member_join' || event === 'member_leave' || event === 'member_kick') {
|
||
return typeof userId === 'string' && userId !== '' ? { kind: event, userId } : null;
|
||
}
|
||
if (event === 'channel_created') {
|
||
const name = record?.['name'];
|
||
if (typeof name !== 'string' || name === '') {
|
||
return null;
|
||
}
|
||
const type = record?.['type'];
|
||
return { kind: 'channel_created', name, channelType: typeof type === 'string' ? type : 'text' };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Идентификатор пользователя из системного сообщения. Нужен ленте: имя
|
||
* автора события резолвится тем же справочником, что и авторы сообщений.
|
||
*/
|
||
export function systemMessageUserId(content: string): string | null {
|
||
const event = parseSystemMessage(content);
|
||
return event !== null && event.kind !== 'channel_created' ? event.userId : null;
|
||
}
|