Compare commits
2 Commits
2cff986760
...
64d5bc391d
| Author | SHA1 | Date | |
|---|---|---|---|
| 64d5bc391d | |||
| ae696abff7 |
@@ -42,6 +42,7 @@ export function useSpeakRequest(token: string | undefined) {
|
|||||||
if (!token) return
|
if (!token) return
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|
||||||
|
function load() {
|
||||||
publicClient
|
publicClient
|
||||||
.get<{ request: SpeakRequestView | null }>(`/public/participation/${token}/speak-request`)
|
.get<{ request: SpeakRequestView | null }>(`/public/participation/${token}/speak-request`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -51,9 +52,31 @@ export function useSpeakRequest(token: string | undefined) {
|
|||||||
// brak stanu nie moze zablokowac wejscia na spotkanie: uczestnik
|
// brak stanu nie moze zablokowac wejscia na spotkanie: uczestnik
|
||||||
// zobaczy przycisk w stanie wyjsciowym i zglosi sie ponownie
|
// zobaczy przycisk w stanie wyjsciowym i zglosi sie ponownie
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
load()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ODSWIEZANIE OKRESOWE JAKO SIATKA BEZPIECZENSTWA
|
||||||
|
* -------------------------------------------------------------------------
|
||||||
|
* WebSocket zostaje glowna droga: uczestnik ma zobaczyc przyznany glos od
|
||||||
|
* razu, a nie po kilkunastu sekundach.
|
||||||
|
*
|
||||||
|
* Zdarzenie potrafi jednak nie dojsc, a polaczenie nie zawsze to
|
||||||
|
* sygnalizuje: karta uspiona przez system, chwilowa utrata sieci,
|
||||||
|
* przelaczenie z WiFi na komorke. Bez odpytywania uczestnik NIE DOWIE SIE,
|
||||||
|
* ZE MA GLOS: przycisk zostaje w starym stanie, a mikrofon zamkniety,
|
||||||
|
* chociaz uprawnienie jest juz przyznane po stronie serwera. Wyglada to na
|
||||||
|
* niedzialajacy mikrofon, a nie na zgubione zdarzenie.
|
||||||
|
*
|
||||||
|
* Odpowiedz to jeden maly obiekt albo null, wiec dziesiec sekund nie kosztuje
|
||||||
|
* nic zauwazalnego nawet przy duzej sali.
|
||||||
|
*/
|
||||||
|
const timer = window.setInterval(load, 10000)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
|
window.clearInterval(timer)
|
||||||
}
|
}
|
||||||
}, [token])
|
}, [token])
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,19 @@ export function MeetingActionBar({
|
|||||||
isSpeaker,
|
isSpeaker,
|
||||||
/** Tryb otwartego mikrofonu: kazdy mowi bez proszenia o glos. */
|
/** Tryb otwartego mikrofonu: kazdy mowi bez proszenia o glos. */
|
||||||
openMic,
|
openMic,
|
||||||
|
/**
|
||||||
|
* Faktyczne uprawnienie do mikrofonu, prosto z tokenu wydanego przez backend.
|
||||||
|
*
|
||||||
|
* JEDYNE ZRODLO PRAWDY O TYM, CZY UCZESTNIK MOZE MOWIC
|
||||||
|
* ---------------------------------------------------------------------
|
||||||
|
* Wczesniej pasek skladal te odpowiedz sam, ze stanu zgloszenia
|
||||||
|
* (`speak.granted`) i trybu otwartego mikrofonu. Rozjezdzalo sie to
|
||||||
|
* z rzeczywistoscia, bo prawo do audio daje takze rola ACTIVE_PARTICIPANT
|
||||||
|
* przy wlaczonym "wideo dla wszystkich": odebranie glosu nie zmienialo wtedy
|
||||||
|
* uprawnien, ale napis "Masz glos" znikal. Uczestnik widzial zamkniety
|
||||||
|
* mikrofon przy dzialajacym i odwrotnie.
|
||||||
|
*/
|
||||||
|
canPublishAudio,
|
||||||
/**
|
/**
|
||||||
* Temat wybrany klikiem w baner.
|
* Temat wybrany klikiem w baner.
|
||||||
*
|
*
|
||||||
@@ -46,6 +59,7 @@ export function MeetingActionBar({
|
|||||||
speak: ReturnType<typeof useSpeakRequest>
|
speak: ReturnType<typeof useSpeakRequest>
|
||||||
isSpeaker: boolean
|
isSpeaker: boolean
|
||||||
openMic: boolean
|
openMic: boolean
|
||||||
|
canPublishAudio: boolean
|
||||||
offerTopicFromBanner?: string | null
|
offerTopicFromBanner?: string | null
|
||||||
onOfferHandled?: () => void
|
onOfferHandled?: () => void
|
||||||
}) {
|
}) {
|
||||||
@@ -96,7 +110,7 @@ export function MeetingActionBar({
|
|||||||
Możesz mówić bez zgłaszania się
|
Możesz mówić bez zgłaszania się
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<SpeakButton speak={speak} />
|
<SpeakButton speak={speak} canPublishAudio={canPublishAudio} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
@@ -193,13 +207,24 @@ export function MeetingActionBar({
|
|||||||
* oczekiwania, czyli dokladnie ten, w ktorym uczestnik zastanawia sie,
|
* oczekiwania, czyli dokladnie ten, w ktorym uczestnik zastanawia sie,
|
||||||
* czy zgloszenie w ogole doszlo.
|
* czy zgloszenie w ogole doszlo.
|
||||||
*/
|
*/
|
||||||
function SpeakButton({ speak }: { speak: ReturnType<typeof useSpeakRequest> }) {
|
function SpeakButton({
|
||||||
|
speak,
|
||||||
|
canPublishAudio,
|
||||||
|
}: {
|
||||||
|
speak: ReturnType<typeof useSpeakRequest>
|
||||||
|
canPublishAudio: boolean
|
||||||
|
}) {
|
||||||
|
/*
|
||||||
|
* Napis o mikrofonie idzie za UPRAWNIENIEM, a nie za stanem zgloszenia:
|
||||||
|
* mowi o tym, co uczestnik faktycznie moze, a nie o tym, o co prosil.
|
||||||
|
* Przycisk "Oddaj glos" zostaje przy zgloszeniu, bo dotyczy wlasnie jego.
|
||||||
|
*/
|
||||||
if (speak.granted) {
|
if (speak.granted) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="inline-flex items-center gap-1.5 rounded-md bg-ok-50 px-3 py-1.5 text-sm font-semibold text-ok-600">
|
<span className="inline-flex items-center gap-1.5 rounded-md bg-ok-50 px-3 py-1.5 text-sm font-semibold text-ok-600">
|
||||||
<Icon name="check-circle" size={16} />
|
<Icon name="check-circle" size={16} />
|
||||||
Masz głos, mikrofon jest włączony
|
{canPublishAudio ? 'Masz głos, mikrofon jest włączony' : 'Masz głos, trwa włączanie mikrofonu...'}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -240,6 +240,27 @@ function MeetingRoom({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [speak.granted, openMic, openVideo])
|
}, [speak.granted, openMic, openVideo])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Slad w konsoli po kazdej zmianie uprawnien.
|
||||||
|
*
|
||||||
|
* Rozjazd miedzy tym, co pokazuje interfejs, a tym, co faktycznie pozwala
|
||||||
|
* serwer, jest trudny do zdiagnozowania po fakcie: uczestnik zglasza
|
||||||
|
* "mikrofon nie dziala", a log serwera pokazuje poprawnie wydany token.
|
||||||
|
* Ta linia pozwala porownac obie strony bez zgadywania - wystarczy otworzyc
|
||||||
|
* konsole na sali. Nie jest to debugowanie do usuniecia: to jedyne miejsce,
|
||||||
|
* w ktorym widac stan tokenu razem ze stanem zgloszenia.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
console.info('[sala] uprawnienia', {
|
||||||
|
audio: join.canPublishAudio,
|
||||||
|
video: join.canPublishVideo,
|
||||||
|
rola: join.role,
|
||||||
|
zgloszenieGlosu: speak.granted,
|
||||||
|
otwartyMikrofon: openMic,
|
||||||
|
wideoDlaWszystkich: openVideo,
|
||||||
|
})
|
||||||
|
}, [join.canPublishAudio, join.canPublishVideo, join.role, speak.granted, openMic, openVideo])
|
||||||
|
|
||||||
const src = useMemo(() => {
|
const src = useMemo(() => {
|
||||||
if (!join.embedded) return null
|
if (!join.embedded) return null
|
||||||
const url = new URL(join.url)
|
const url = new URL(join.url)
|
||||||
@@ -337,6 +358,7 @@ function MeetingRoom({
|
|||||||
speak={speak}
|
speak={speak}
|
||||||
isSpeaker={isSpeaker}
|
isSpeaker={isSpeaker}
|
||||||
openMic={openMic}
|
openMic={openMic}
|
||||||
|
canPublishAudio={join.canPublishAudio}
|
||||||
offerTopicFromBanner={offerTopicFromBanner}
|
offerTopicFromBanner={offerTopicFromBanner}
|
||||||
onOfferHandled={() => setOfferTopicFromBanner(null)}
|
onOfferHandled={() => setOfferTopicFromBanner(null)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -127,8 +127,29 @@ export function HostConsolePage() {
|
|||||||
}
|
}
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ODSWIEZANIE OKRESOWE JAKO SIATKA BEZPIECZENSTWA
|
||||||
|
* ---------------------------------------------------------------------------
|
||||||
|
* Kolejka odswieza sie zdarzeniem WebSocket i tak ma zostac: prowadzacy widzi
|
||||||
|
* podniesiona reke od razu, a nie po kilkunastu sekundach.
|
||||||
|
*
|
||||||
|
* Zdarzenie potrafi jednak nie dojsc, a polaczenie nie zawsze to sygnalizuje:
|
||||||
|
* karta uspiona przez system, chwilowa utrata sieci, przelaczenie WiFi na
|
||||||
|
* telefonie. Klient wznawia wtedy polaczenie, ale zdarzenia wyslanego
|
||||||
|
* w miedzyczasie nikt juz nie powtorzy, bo WebSocket nie ma historii.
|
||||||
|
* Prowadzacy widzial przez to "Nikt nie prosi o glos" przy zgloszeniach
|
||||||
|
* czekajacych juz kilka minut, i dowiadywal sie o nich dopiero po odswiezeniu
|
||||||
|
* strony. Zdarzylo sie to na zywo.
|
||||||
|
*
|
||||||
|
* Odpytywanie jest wiec DRUGA droga, nie glowna. Piec sekund jest tanie:
|
||||||
|
* kolejka to kilka wierszy, a odpowiedz przy pustej liscie wazy tyle co nic.
|
||||||
|
* Ten sam wzorzec co przy banerach na sali, gdzie front takze nie polega
|
||||||
|
* wylacznie na zdarzeniach.
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadSpeakQueue()
|
void loadSpeakQueue()
|
||||||
|
const timer = window.setInterval(() => void loadSpeakQueue(), 5000)
|
||||||
|
return () => window.clearInterval(timer)
|
||||||
}, [loadSpeakQueue])
|
}, [loadSpeakQueue])
|
||||||
|
|
||||||
const webinar = useQuery({
|
const webinar = useQuery({
|
||||||
|
|||||||
Reference in New Issue
Block a user