Glos bez zgloszenia z karty "Na sali"
Deploy PROD / deploy (push) Successful in 26s

Przycisk mikrofonu przy kazdym uczestniku daje i odbiera glos bez jego
prosby. Idzie przez kolejke zgloszen (SpeakRequestService.setVoice):
powstaje od razu przyznane zgloszenie albo zostaje przyznane oczekujace,
wiec kolejka, napis "Masz glos" i automatyczne wlaczenie mikrofonu
zgadzaja sie ze stanem. Serwer pokoju zmienia uprawnienia od razu.

Uczestnik bez wlasnego zgloszenia rozpoznaje zdarzenie jako swoje po
numerze zapisu z sesji, zamiast czekac na odpytanie co 10 sekund.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 13:11:18 +02:00
parent 589c317841
commit 8e20d5aeda
7 changed files with 171 additions and 9 deletions
+10 -2
View File
@@ -120,7 +120,12 @@ export function useInteractions(
* drugi klient STOMP oznaczalby drugie uwierzytelnienie i druga sesje
* obecnosci, czyli uczestnika liczonego podwojnie.
*/
onSpeakEvent?: (payload: SpeakEventPayload) => void,
/**
* `mine` mowi, czy zdarzenie dotyczy tego uczestnika. Bez tego uczestnik,
* ktoremu prowadzacy dal glos bez zgloszenia, nie rozpoznalby zdarzenia jako
* swojego i dowiedzialby sie o glosie dopiero przy odpytaniu co 10 sekund.
*/
onSpeakEvent?: (payload: SpeakEventPayload, mine: boolean) => void,
/** Wolane, gdy prowadzacy przelaczy tryb otwartego mikrofonu. */
onOpenMicChange?: (enabled: boolean) => void,
/** Wolane przy moderacji uczestnika (wyciszenie/blokada), tylko gdy dotyczy jego samego. */
@@ -262,7 +267,10 @@ export function useInteractions(
}
return
}
speakHandler.current?.(event.payload)
speakHandler.current?.(
event.payload,
event.payload?.registrationId === myRegistrationIdRef.current,
)
})
client.subscribe('/user/queue/errors', (message: IMessage) => {
+4 -1
View File
@@ -118,8 +118,11 @@ export function useSpeakRequest(token: string | undefined) {
* `registrationId` jednoznacznie wskazuje uczestnika i nie zmienia sie miedzy
* kolejnymi zgloszeniami tej samej osoby.
*/
const apply = useCallback((incoming: SpeakRequestView) => {
const apply = useCallback((incoming: SpeakRequestView, mine = false) => {
setRequest((current) => {
// glos dany przez prowadzacego bez zgloszenia: lokalnie nic jeszcze nie
// ma, ale zdarzenie jest nasze (`mine` sprawdza numer zapisu z sesji)
if (!current && mine) return incoming
// Bez biezacego zgloszenia nie wiemy, ktory registrationId jest nasz,
// a temat jest wspolny dla calej sali - wiec cudzego zdarzenia nie
// przyjmujemy (inaczej przyznanie glosu komus innemu wlaczyloby mikrofon
@@ -25,6 +25,8 @@ type Participant = {
cameraAllowed: boolean
/** Indywidualna zgoda prowadzacego na udostepnienie ekranu. */
screenAllowed: boolean
/** Przyznany glos, z kolejki albo bez zgloszenia. */
voiceGranted: boolean
}
type Presence = {
@@ -179,6 +181,29 @@ export function RoomParticipants({ webinarId, timezone }: { webinarId: string; t
}
}
/**
* Glos bez zgloszenia. Idzie przez kolejke zgloszen, wiec uczestnik widzi
* "Masz glos", mikrofon wlacza mu sie sam, a prowadzacy moze go odebrac
* takze z karty "Zgloszenia do glosu".
*/
async function toggleVoice(participant: Participant) {
if (participant.registrationId === null) return
const next = !participant.voiceGranted
setBusy(participant.identity)
try {
await client.post(
`/admin/webinars/${webinarId}/speak-requests/participants/${participant.registrationId}/voice`,
{ value: next },
)
toasts.success(next ? `${participant.name} ma głos` : `Odebrano głos: ${participant.name}`)
await refresh()
} catch (error) {
toasts.error(errorMessage(error, 'Nie udało się zmienić głosu'))
} finally {
setBusy(null)
}
}
async function unban(registrationId: number, name: string) {
setBusy(`ban-${registrationId}`)
try {
@@ -304,6 +329,31 @@ export function RoomParticipants({ webinarId, timezone }: { webinarId: string; t
egzekwuje osobno kazde zrodlo. Przy innych dostawcach
przycisk obiecywalby cos, czego nikt nie pilnuje.
*/}
{/*
Glos dziala u kazdego dostawcy, bo to ta sama droga co
przyznanie z kolejki. Kamera i ekran tylko przy LiveKit.
*/}
{participant.kind === 'PARTICIPANT' && (
<button
type="button"
disabled={busy === participant.identity}
onClick={() => void toggleVoice(participant)}
aria-pressed={participant.voiceGranted}
aria-label={
participant.voiceGranted
? `Odbierz głos: ${participant.name}`
: `Daj głos bez zgłoszenia: ${participant.name}`
}
title={participant.voiceGranted ? 'Odbierz głos' : 'Daj głos bez zgłoszenia'}
className={`flex h-9 w-9 items-center justify-center rounded-md disabled:opacity-40 ${
participant.voiceGranted
? 'bg-ok-50 text-ok-600 hover:bg-ok-50'
: 'text-slate-400 hover:bg-slate-100 hover:text-slate-700'
}`}
>
<Icon name={participant.voiceGranted ? 'mic' : 'mic-off'} size={16} />
</button>
)}
{participant.kind === 'PARTICIPANT' && data?.source === 'LIVEKIT' && (
<>
<button