Files
glchat/web/src/lib/systemMessage.ts
T
grendervill 619bd91df4 feat(voice): саундборд, звуковая палитра, push-to-talk и статистика соединения
Клиентская часть Фазы 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.
2026-09-20 19:30:43 +03:00

59 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Системные сообщения (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;
}