feat(web): интерфейс звонков в беседах (Фаза 7)

В шапке беседы — меню звонка: аудио, видео, «Присоединиться» к идущему и
история. Когда голос на инстансе выключен, меню нет вовсе: обещать звонок,
который не поднимется, нельзя.

Входящий звонок показывается карточкой поверх любого экрана с принять и
отклонить и рингтоном; идущий звонок — маркером в списке бесед и в шапке, а
в области переписки вместо ленты открывается панель с плитками участников и
кнопками микрофона, звука, камеры, шаринга и выхода. История звонков — кто
звонил, когда, сколько длился разговор и кто не ответил.

Тесты (vitest): начало звонка из шапки с подключением к комнате LiveKit,
входящий звонок и его принятие, отклонение, маркер «идёт звонок» по событию
и его снятие после завершения, флаги микрофона, камеры и экрана, история с
пропущенными и длительностью, выключенный голос и маркер после
перезагрузки (active_call в READY).
This commit is contained in:
2026-09-26 17:12:56 +03:00
parent 431013c3e3
commit 2b9bf55f16
12 changed files with 1384 additions and 7 deletions
+6
View File
@@ -13,6 +13,7 @@ import { useMessagesStore } from '@/stores/messages';
import { useSessionStore } from '@/stores/session';
import { useSoundboardStore } from '@/stores/soundboard';
import { useUiStore } from '@/stores/ui';
import { useDmCallStore } from '@/stores/dmCall';
import { useVoiceStore } from '@/stores/voice';
import { useVoiceConnectionStore } from '@/stores/voiceConnection';
import type { User } from '@/api/types';
@@ -153,6 +154,8 @@ export function resetStores(): void {
useVoiceStore.getState().reset();
// Голосовая сессия могла остаться от прошлого теста: гасим её без запросов.
useVoiceConnectionStore.getState().reset();
// Звонок в беседе — тоже сессия: комната LiveKit не должна переживать тест.
useDmCallStore.getState().reset();
// Саундборд: подпись и очередь звуков не должны переживать тест.
useSoundboardStore.getState().reset();
useFriendsStore.getState().reset();
@@ -275,6 +278,8 @@ export interface SnapshotDmInput {
last_message_id?: string;
last_message_at?: string;
can_send?: boolean;
/** Идущий в беседе звонок (AGENT.md 7.8, Фаза 7): маркер «идёт звонок». */
active_call?: unknown;
}
/** Снапшот READY в формате шлюза. */
@@ -359,6 +364,7 @@ export function makeReadySnapshot(
...(channel.last_message_at === undefined
? {}
: { last_message_at: channel.last_message_at }),
...(channel.active_call === undefined ? {} : { active_call: channel.active_call }),
can_send: channel.can_send ?? true,
})),
read_states: [],