feat(room): поднятие руки и очередь для организатора
Транспорт — существующий аутентифицированный WS чата (api/chat.py), а не отдельный эндпоинт: сервер уже держит это соединение на каждого участника (обоснование — докстринг chat_websocket и useChat.ts). Состояние очереди — Redis (services/hand_queue.py), не Postgres: это эфемерное состояние звонка, а не история, и два процесса uvicorn делают наивную память одного процесса недостаточной. HSETNX даёт идемпотентное «поднять» (повторный клик не переставляет в конец очереди), снапшот шлётся всем участникам при любом изменении — организатор, зашедший позже, сразу видит актуальную картину. Опустить чужую руку может организатор (решение оператора) — проверка через conference.owner_id, не через identity клиента. Участник, вышедший из комнаты LiveKit (webhook participant_left), теряет место в очереди автоматически; переподключение WS чата место не сбрасывает (Redis не привязан к жизни соединения). room_finished чистит очередь целиком — она не должна пережить завершение звонка. Побочный эффект транспортного решения: поднять руку нельзя, если чат выключен настройкой инстанса (WS вообще не открывается) — принятый компромисс ради переиспользования уже готового канала. UI: кнопка «Рука» в тулбаре (у всех, бейдж — общий счётчик), бейдж на плитке говорящего (видно всем), панель «Очередь» организатору (HandQueuePanel). Кнопка «Рука» и панель «Очередь» намеренно НЕ прячутся в мобильную шторку настроек, в отличие от «Вида», — поднятие руки посреди разговора требует кнопки под рукой, а не в два клика вглубь настроек. Этим же коммитом (файлы разделяемые с задачей B2, RoomParticipantTile.tsx/ useChat.ts/RoomStage.tsx/RoomPage.tsx/room.css) — проброс conferenceId и каркас forced_mute-обработки, без которых кнопки принудительного мьюта не скомпилировались бы; сама реализация мьюта — следующим коммитом.
This commit is contained in:
@@ -323,6 +323,12 @@ video[data-lk-source='screen_share'] { object-fit: contain; background: #000; }
|
||||
.tb-btn.is-sharing span.label { color: var(--color-room-mic-on); font-weight: 700; }
|
||||
.tb-btn:disabled { opacity: 0.6; cursor: default; }
|
||||
|
||||
/* Кнопка «Рука» — та же зелёная подсветка активного состояния, что у
|
||||
«Демонстрации» (задача B1): своя поднятая рука — такой же позитивный
|
||||
индикатор «я сейчас что-то сигнализирую комнате». */
|
||||
.tb-btn.is-hand-raised .icon-shell { background: var(--color-room-mic-on); border-color: var(--color-room-mic-on); color: #10331f; }
|
||||
.tb-btn.is-hand-raised span.label { color: var(--color-room-mic-on); font-weight: 700; }
|
||||
|
||||
.tb-btn.danger .icon-shell { border-color: var(--color-room-danger); color: var(--color-room-danger); background: transparent; }
|
||||
.tb-btn.danger:hover .icon-shell { background: var(--color-room-danger-bg); color: #fff; border-color: var(--color-room-danger-bg); }
|
||||
.tb-btn.danger span.label { color: var(--color-room-danger); font-weight: 700; }
|
||||
@@ -574,6 +580,66 @@ video[data-lk-source='screen_share'] { object-fit: contain; background: #000; }
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Панель очереди поднятых рук (`HandQueuePanel`, задача B1) ----------
|
||||
* Контейнер — `.chat-panel` (та же геометрия и мобильное поведение), список
|
||||
* — свой. */
|
||||
.hand-queue-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: var(--space-3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.hand-queue-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-room-tile);
|
||||
}
|
||||
.hand-queue-position {
|
||||
flex-shrink: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-room-mic-on);
|
||||
color: #10331f;
|
||||
font: var(--text-caption);
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.hand-queue-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font: var(--text-body);
|
||||
color: var(--color-room-text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.hand-queue-name svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--color-room-mic-on); }
|
||||
.hand-queue-lower {
|
||||
flex-shrink: 0;
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-room-tile-border);
|
||||
background: transparent;
|
||||
color: var(--color-room-text-secondary);
|
||||
font: var(--text-caption);
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
.hand-queue-lower:hover { background: var(--color-room-tile-hover); color: var(--color-room-text-primary); }
|
||||
|
||||
/*
|
||||
* ---------- Диалог «Настройки устройств» ----------
|
||||
* Нет отдельного макета для диалога в design/mockups/room.html — собран из
|
||||
@@ -762,6 +828,54 @@ video[data-lk-source='screen_share'] { object-fit: contain; background: #000; }
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* Бейдж поднятой руки (задача B1) — левый верхний угол, зеркально булавке
|
||||
закрепления (правый верхний). В отличие от булавки ВСЕГДА видим, пока
|
||||
рука поднята, — это статус для ВСЕХ участников, а не собственный
|
||||
элемент управления, видимый по наведению. */
|
||||
.room-hand-badge {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
left: 0.25rem;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
padding: 0.25rem;
|
||||
border-radius: calc(var(--lk-border-radius, 0.5rem) / 2);
|
||||
background: var(--color-room-mic-on);
|
||||
color: #10331f;
|
||||
}
|
||||
.room-hand-badge svg { width: 18px; height: 18px; }
|
||||
|
||||
/* Кнопки принудительного мьюта организатором (задача B2) — нижний правый
|
||||
угол чужой плитки, видны по наведению (как булавка закрепления) —
|
||||
элемент управления, а не статус, прятать по умолчанию уместно. */
|
||||
.room-organizer-controls {
|
||||
position: absolute;
|
||||
bottom: 0.25rem;
|
||||
right: 0.25rem;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
transition-delay: 0.2s;
|
||||
}
|
||||
.lk-participant-tile:hover .room-organizer-controls,
|
||||
.lk-participant-tile:focus-within .room-organizer-controls { opacity: 1; transition-delay: 0s; }
|
||||
@media (hover: none) {
|
||||
.room-organizer-controls { opacity: 1; transition-delay: 0s; }
|
||||
}
|
||||
.room-organizer-controls button {
|
||||
display: flex;
|
||||
padding: 0.25rem;
|
||||
border: none;
|
||||
border-radius: calc(var(--lk-border-radius, 0.5rem) / 2);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: var(--color-room-text-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
.room-organizer-controls button:hover { background: var(--color-room-danger-bg); color: #fff; }
|
||||
.room-organizer-controls svg { width: 18px; height: 18px; }
|
||||
|
||||
/* ---------- Заглушка «конференция в мини-окне» (Document PiP) ---------- */
|
||||
.room-pip-placeholder {
|
||||
flex: 1;
|
||||
|
||||
Reference in New Issue
Block a user