@rapidmx/meet-plugin 0.14.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -14,12 +14,21 @@
|
|
|
14
14
|
* camera is sending. Both are driven by `LocalMedia` (`apps/shared/media/useLocalMedia.ts`). The microphone button
|
|
15
15
|
* is disabled (`micLocked`) while talking-stick mode is on and this participant isn't the current holder - see
|
|
16
16
|
* `_CallView.tsx`'s doc comment.
|
|
17
|
+
*
|
|
18
|
+
* ## The "…" menu
|
|
19
|
+
*
|
|
20
|
+
* Sits between the grid/focus toggle and Leave, a catch-all for controls that don't need a dedicated button of
|
|
21
|
+
* their own: "Diagnostics" (`onOpenDiagnostics`) and "Settings" (`onOpenSettings`, host-only) each open their own
|
|
22
|
+
* persistent window/modal in `_CallView.tsx` and close this menu; "Connection method" is a four-way
|
|
23
|
+
* `menuitemradio` group (`TransportMode` - see `MeshConnectionManager.setTransportMode()`'s own doc comment)
|
|
24
|
+
* forcing how *this tab's* media reaches everyone else, independent of what anyone else has chosen. Otherwise an
|
|
25
|
+
* ordinary menu of this bar's own shape (`openMenu`, closes on Escape/outside click like every other one here).
|
|
17
26
|
*/
|
|
18
27
|
import React, { useEffect, useRef, useState } from "react";
|
|
19
28
|
import { filtersActive } from "../shared/media/filters/filterTypes.js";
|
|
20
29
|
import type { ScreenTransformState } from "../shared/media/filters/ScreenTransform.js";
|
|
21
30
|
import type { LocalMedia, MediaKind } from "../shared/media/useLocalMedia.js";
|
|
22
|
-
import { REACTION_EMOJIS, type MeshParticipant } from "../shared/webrtc/types.js";
|
|
31
|
+
import { REACTION_EMOJIS, type MeshParticipant, type TransportMode } from "../shared/webrtc/types.js";
|
|
23
32
|
import EffectsPanel from "./_EffectsPanel.js";
|
|
24
33
|
import {
|
|
25
34
|
ChevronUpIcon,
|
|
@@ -32,12 +41,21 @@ import {
|
|
|
32
41
|
LeaveIcon,
|
|
33
42
|
MicIcon,
|
|
34
43
|
MicOffIcon,
|
|
44
|
+
OverflowIcon,
|
|
35
45
|
RotateIcon,
|
|
36
46
|
ScreenShareIcon,
|
|
37
47
|
VideoIcon,
|
|
38
48
|
VideoOffIcon,
|
|
39
49
|
} from "./_icons.js";
|
|
40
50
|
|
|
51
|
+
/** The "…" menu's "Connection method" options, in display order - see `TransportMode`'s own doc comment. */
|
|
52
|
+
const TRANSPORT_MODE_OPTIONS: { mode: TransportMode; label: string }[] = [
|
|
53
|
+
{ mode: "auto", label: "Auto" },
|
|
54
|
+
{ mode: "p2p", label: "P2P" },
|
|
55
|
+
{ mode: "relay", label: "Relay" },
|
|
56
|
+
{ mode: "websocket", label: "WebSocket Relay" },
|
|
57
|
+
];
|
|
58
|
+
|
|
41
59
|
export type CallViewMode = "grid" | "focus";
|
|
42
60
|
|
|
43
61
|
export interface CallControlsProps {
|
|
@@ -62,10 +80,16 @@ export interface CallControlsProps {
|
|
|
62
80
|
* doc comment on talking-stick mode. Disables the microphone toggle (not the device-picker chevron, which
|
|
63
81
|
* doesn't change whether anyone can hear them) rather than merely nudging it, unlike a `mute-request`. */
|
|
64
82
|
micLocked?: boolean;
|
|
83
|
+
/** Whether the "…" menu's "Settings" item is offered at all - see this module's doc comment on the menu. */
|
|
84
|
+
isHost: boolean;
|
|
85
|
+
transportMode: TransportMode;
|
|
86
|
+
onSetTransportMode: (mode: TransportMode) => void;
|
|
87
|
+
onOpenDiagnostics: () => void;
|
|
88
|
+
onOpenSettings: () => void;
|
|
65
89
|
onLeave: () => void;
|
|
66
90
|
}
|
|
67
91
|
|
|
68
|
-
type OpenMenu = MediaKind | "emoji" | "effects" | null;
|
|
92
|
+
type OpenMenu = MediaKind | "emoji" | "effects" | "overflow" | null;
|
|
69
93
|
|
|
70
94
|
const BUTTON = "flex items-center justify-center h-12 rounded-full transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80";
|
|
71
95
|
const NEUTRAL = "bg-[#3c4043] text-white hover:bg-[#4b4f53]";
|
|
@@ -102,6 +126,11 @@ export default function CallControls({
|
|
|
102
126
|
viewMode,
|
|
103
127
|
onToggleViewMode,
|
|
104
128
|
micLocked,
|
|
129
|
+
isHost,
|
|
130
|
+
transportMode,
|
|
131
|
+
onSetTransportMode,
|
|
132
|
+
onOpenDiagnostics,
|
|
133
|
+
onOpenSettings,
|
|
105
134
|
onLeave,
|
|
106
135
|
}: CallControlsProps) {
|
|
107
136
|
const [openMenu, setOpenMenu] = useState<OpenMenu>(null);
|
|
@@ -307,6 +336,74 @@ export default function CallControls({
|
|
|
307
336
|
{viewMode === "grid" ? <FocusIcon /> : <GridIcon />}
|
|
308
337
|
</button>
|
|
309
338
|
|
|
339
|
+
<div className="sm:relative">
|
|
340
|
+
<button
|
|
341
|
+
type="button"
|
|
342
|
+
className={`${BUTTON} w-12 ${openMenu === "overflow" ? ACTIVE : NEUTRAL}`}
|
|
343
|
+
aria-label="More options"
|
|
344
|
+
aria-haspopup="menu"
|
|
345
|
+
aria-expanded={openMenu === "overflow"}
|
|
346
|
+
onClick={() => toggleMenu("overflow")}
|
|
347
|
+
>
|
|
348
|
+
<OverflowIcon />
|
|
349
|
+
</button>
|
|
350
|
+
{openMenu === "overflow" && (
|
|
351
|
+
<div
|
|
352
|
+
role="menu"
|
|
353
|
+
aria-label="More options"
|
|
354
|
+
className="absolute bottom-full mb-3 right-0 w-56 max-w-[calc(100vw-1rem)] rounded-2xl bg-[#2b2d30] text-white shadow-xl py-1 text-sm"
|
|
355
|
+
>
|
|
356
|
+
<button
|
|
357
|
+
type="button"
|
|
358
|
+
role="menuitem"
|
|
359
|
+
className="w-full px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
360
|
+
onClick={() => {
|
|
361
|
+
onOpenDiagnostics();
|
|
362
|
+
setOpenMenu(null);
|
|
363
|
+
}}
|
|
364
|
+
>
|
|
365
|
+
Diagnostics
|
|
366
|
+
</button>
|
|
367
|
+
<div className="my-1 border-t border-white/10" />
|
|
368
|
+
<p className="px-3 pt-1 pb-0.5 text-xs font-semibold uppercase tracking-wide text-white/60">Connection method</p>
|
|
369
|
+
{TRANSPORT_MODE_OPTIONS.map(({ mode, label }) => (
|
|
370
|
+
<button
|
|
371
|
+
key={mode}
|
|
372
|
+
type="button"
|
|
373
|
+
role="menuitemradio"
|
|
374
|
+
aria-checked={transportMode === mode}
|
|
375
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
376
|
+
onClick={() => {
|
|
377
|
+
onSetTransportMode(mode);
|
|
378
|
+
setOpenMenu(null);
|
|
379
|
+
}}
|
|
380
|
+
>
|
|
381
|
+
<span className="w-4 text-[#8ab4f8]" aria-hidden="true">
|
|
382
|
+
{transportMode === mode ? "✓" : ""}
|
|
383
|
+
</span>
|
|
384
|
+
{label}
|
|
385
|
+
</button>
|
|
386
|
+
))}
|
|
387
|
+
{isHost && (
|
|
388
|
+
<>
|
|
389
|
+
<div className="my-1 border-t border-white/10" />
|
|
390
|
+
<button
|
|
391
|
+
type="button"
|
|
392
|
+
role="menuitem"
|
|
393
|
+
className="w-full px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
394
|
+
onClick={() => {
|
|
395
|
+
onOpenSettings();
|
|
396
|
+
setOpenMenu(null);
|
|
397
|
+
}}
|
|
398
|
+
>
|
|
399
|
+
Settings
|
|
400
|
+
</button>
|
|
401
|
+
</>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
)}
|
|
405
|
+
</div>
|
|
406
|
+
|
|
310
407
|
<button type="button" className={`${BUTTON} w-16 bg-[#d93025] text-white hover:bg-[#b3261e]`} aria-label="Leave call" onClick={() => onLeave()}>
|
|
311
408
|
<LeaveIcon />
|
|
312
409
|
</button>
|
package/apps/meet/_CallView.tsx
CHANGED
|
@@ -51,18 +51,17 @@
|
|
|
51
51
|
* `MeshConnectionManager`'s own doc comment on why that needs no extra code - the header chip then says nobody has
|
|
52
52
|
* the floor, until the host picks someone (or themselves) again.
|
|
53
53
|
*
|
|
54
|
-
* ##
|
|
54
|
+
* ## Diagnostics, connection method and settings
|
|
55
55
|
*
|
|
56
|
-
*
|
|
57
|
-
* `_DiagnosticsWindow.tsx` (a persistent, draggable
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* `_SettingsModal.tsx`, which used to be the participants drawer's
|
|
63
|
-
* waiting-room toggle) - moved out because none of those are
|
|
64
|
-
*
|
|
65
|
-
* close along with it.
|
|
56
|
+
* All three are opened from `_CallControls.tsx`'s own "…" menu (its doc comment covers the menu itself) via
|
|
57
|
+
* callback props, but owned here: `diagnosticsOpen` renders `_DiagnosticsWindow.tsx` (a persistent, draggable
|
|
58
|
+
* window - not a popover like the menus in the control bar, since troubleshooting a connection is something a
|
|
59
|
+
* participant wants to keep open while they poke at the rest of the call, not something that closes the moment
|
|
60
|
+
* they click elsewhere); `transportMode`/`handleSetTransportMode()` forces how *this tab's* media reaches everyone
|
|
61
|
+
* else (`TransportMode` - see `MeshConnectionManager.setTransportMode()`'s own doc comment), independent of what
|
|
62
|
+
* anyone else has chosen; `settingsOpen` renders `_SettingsModal.tsx`, which used to be the participants drawer's
|
|
63
|
+
* own header (mute-on-join, the join password, the waiting-room toggle) - moved out because none of those are
|
|
64
|
+
* about *who* is in the call, which is what the drawer is for.
|
|
66
65
|
*/
|
|
67
66
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
68
67
|
import { playRaisedHandChime } from "../shared/media/chime.js";
|
|
@@ -77,7 +76,7 @@ import type { MeshParticipant, RelayTransportLike, TransportMode } from "../shar
|
|
|
77
76
|
import { createRelayTransport } from "../shared/relay/RelayTransport.js";
|
|
78
77
|
import { GuestSignalingClient } from "../shared/push/GuestSignalingClient.js";
|
|
79
78
|
import CallControls, { type CallViewMode } from "./_CallControls.js";
|
|
80
|
-
import { BatonIcon
|
|
79
|
+
import { BatonIcon } from "./_icons.js";
|
|
81
80
|
import {
|
|
82
81
|
admitParticipant,
|
|
83
82
|
denyParticipant,
|
|
@@ -139,14 +138,6 @@ const MAX_REACTIONS = 12;
|
|
|
139
138
|
/** How often the host's drawer refreshes its own waiting-room list while open. */
|
|
140
139
|
const DEFAULT_WAITING_POLL_MS = 3_000;
|
|
141
140
|
|
|
142
|
-
/** The "…" menu's "Connection method" options, in display order - see `TransportMode`'s own doc comment. */
|
|
143
|
-
const TRANSPORT_MODE_OPTIONS: { mode: TransportMode; label: string }[] = [
|
|
144
|
-
{ mode: "auto", label: "Auto" },
|
|
145
|
-
{ mode: "p2p", label: "P2P" },
|
|
146
|
-
{ mode: "relay", label: "Relay" },
|
|
147
|
-
{ mode: "websocket", label: "WebSocket Relay" },
|
|
148
|
-
];
|
|
149
|
-
|
|
150
141
|
interface Reaction {
|
|
151
142
|
id: number;
|
|
152
143
|
emoji: string;
|
|
@@ -229,13 +220,11 @@ export default function CallView({
|
|
|
229
220
|
const [waitingParticipants, setWaitingParticipants] = useState<WaitingParticipant[]>([]);
|
|
230
221
|
const [talkingStickActive, setTalkingStickActive] = useState(false);
|
|
231
222
|
const [talkingStickHolder, setTalkingStickHolder] = useState<string | undefined>(undefined);
|
|
232
|
-
const [overflowMenuOpen, setOverflowMenuOpen] = useState(false);
|
|
233
223
|
const [diagnosticsOpen, setDiagnosticsOpen] = useState(false);
|
|
234
224
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
235
225
|
const [transportMode, setTransportModeState] = useState<TransportMode>("auto");
|
|
236
226
|
|
|
237
227
|
const managerRef = useRef<MeshConnectionManager | null>(null);
|
|
238
|
-
const overflowMenuRef = useRef<HTMLDivElement>(null);
|
|
239
228
|
/** The raw capture from `getDisplayMedia()` - only ever used to stop it (releasing the OS's own share
|
|
240
229
|
* indicator) and to notice the participant ending the share from the browser's own UI (`track.onended`). What's
|
|
241
230
|
* actually shown and sent is `screenTransformRef`'s output (`screenStream` state) - see `handleToggleShare()`. */
|
|
@@ -409,31 +398,6 @@ export default function CallView({
|
|
|
409
398
|
}
|
|
410
399
|
}, [talkingStickActive, selfHasTalkingStick]);
|
|
411
400
|
|
|
412
|
-
// The "…" menu closes on Escape or a press anywhere outside it - matching `_CallControls.tsx`'s own menus -
|
|
413
|
-
// but never the windows it opens (diagnostics, settings), which stay open independently (see this module's
|
|
414
|
-
// doc comment on the "…" menu).
|
|
415
|
-
useEffect(() => {
|
|
416
|
-
if (!overflowMenuOpen) {
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
const onPointerDown = (event: PointerEvent) => {
|
|
420
|
-
if (!overflowMenuRef.current?.contains(event.target as Node)) {
|
|
421
|
-
setOverflowMenuOpen(false);
|
|
422
|
-
}
|
|
423
|
-
};
|
|
424
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
425
|
-
if (event.key === "Escape") {
|
|
426
|
-
setOverflowMenuOpen(false);
|
|
427
|
-
}
|
|
428
|
-
};
|
|
429
|
-
document.addEventListener("pointerdown", onPointerDown);
|
|
430
|
-
document.addEventListener("keydown", onKeyDown);
|
|
431
|
-
return () => {
|
|
432
|
-
document.removeEventListener("pointerdown", onPointerDown);
|
|
433
|
-
document.removeEventListener("keydown", onKeyDown);
|
|
434
|
-
};
|
|
435
|
-
}, [overflowMenuOpen]);
|
|
436
|
-
|
|
437
401
|
useEffect(() => {
|
|
438
402
|
if (!presenterUid) {
|
|
439
403
|
setActiveSpeakerUid((prev) => pickActiveSpeaker(levels, prev));
|
|
@@ -573,12 +537,11 @@ export default function CallView({
|
|
|
573
537
|
}
|
|
574
538
|
|
|
575
539
|
/** Forces this tab's own transport policy - see `MeshConnectionManager.setTransportMode()`'s doc comment for
|
|
576
|
-
* what each mode actually does, and
|
|
577
|
-
* per-tab choice rather than a meeting setting. */
|
|
540
|
+
* what each mode actually does, and `_CallControls.tsx`'s doc comment on its "…" menu for why this is a
|
|
541
|
+
* personal, per-tab choice rather than a meeting setting. */
|
|
578
542
|
function handleSetTransportMode(mode: TransportMode) {
|
|
579
543
|
setTransportModeState(mode);
|
|
580
544
|
managerRef.current?.setTransportMode(mode);
|
|
581
|
-
setOverflowMenuOpen(false);
|
|
582
545
|
}
|
|
583
546
|
|
|
584
547
|
/** Admits one pending request - optimistically removed from the drawer's own list (the next poll would drop it
|
|
@@ -768,70 +731,6 @@ export default function CallView({
|
|
|
768
731
|
>
|
|
769
732
|
{participants.length + 1}
|
|
770
733
|
</button>
|
|
771
|
-
<div className="relative" ref={overflowMenuRef}>
|
|
772
|
-
<button
|
|
773
|
-
type="button"
|
|
774
|
-
className={`w-9 h-9 flex items-center justify-center rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 ${overflowMenuOpen ? "bg-[#4b4f53]" : "bg-[#3c4043] hover:bg-[#4b4f53]"}`}
|
|
775
|
-
aria-label="More options"
|
|
776
|
-
aria-haspopup="menu"
|
|
777
|
-
aria-expanded={overflowMenuOpen}
|
|
778
|
-
onClick={() => setOverflowMenuOpen((prev) => !prev)}
|
|
779
|
-
>
|
|
780
|
-
<OverflowIcon />
|
|
781
|
-
</button>
|
|
782
|
-
{overflowMenuOpen && (
|
|
783
|
-
<div
|
|
784
|
-
role="menu"
|
|
785
|
-
aria-label="More options"
|
|
786
|
-
className="absolute top-full right-0 mt-2 w-56 rounded-2xl bg-[#2b2d30] text-white shadow-xl py-1 text-sm"
|
|
787
|
-
>
|
|
788
|
-
<button
|
|
789
|
-
type="button"
|
|
790
|
-
role="menuitem"
|
|
791
|
-
className="w-full px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
792
|
-
onClick={() => {
|
|
793
|
-
setDiagnosticsOpen(true);
|
|
794
|
-
setOverflowMenuOpen(false);
|
|
795
|
-
}}
|
|
796
|
-
>
|
|
797
|
-
Diagnostics
|
|
798
|
-
</button>
|
|
799
|
-
<div className="my-1 border-t border-white/10" />
|
|
800
|
-
<p className="px-3 pt-1 pb-0.5 text-xs font-semibold uppercase tracking-wide text-white/60">Connection method</p>
|
|
801
|
-
{TRANSPORT_MODE_OPTIONS.map(({ mode, label }) => (
|
|
802
|
-
<button
|
|
803
|
-
key={mode}
|
|
804
|
-
type="button"
|
|
805
|
-
role="menuitemradio"
|
|
806
|
-
aria-checked={transportMode === mode}
|
|
807
|
-
className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
808
|
-
onClick={() => handleSetTransportMode(mode)}
|
|
809
|
-
>
|
|
810
|
-
<span className="w-4 text-[#8ab4f8]" aria-hidden="true">
|
|
811
|
-
{transportMode === mode ? "✓" : ""}
|
|
812
|
-
</span>
|
|
813
|
-
{label}
|
|
814
|
-
</button>
|
|
815
|
-
))}
|
|
816
|
-
{isHost && (
|
|
817
|
-
<>
|
|
818
|
-
<div className="my-1 border-t border-white/10" />
|
|
819
|
-
<button
|
|
820
|
-
type="button"
|
|
821
|
-
role="menuitem"
|
|
822
|
-
className="w-full px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
823
|
-
onClick={() => {
|
|
824
|
-
setSettingsOpen(true);
|
|
825
|
-
setOverflowMenuOpen(false);
|
|
826
|
-
}}
|
|
827
|
-
>
|
|
828
|
-
Settings
|
|
829
|
-
</button>
|
|
830
|
-
</>
|
|
831
|
-
)}
|
|
832
|
-
</div>
|
|
833
|
-
)}
|
|
834
|
-
</div>
|
|
835
734
|
</div>
|
|
836
735
|
</header>
|
|
837
736
|
{connectError && (
|
|
@@ -870,6 +769,11 @@ export default function CallView({
|
|
|
870
769
|
viewMode={viewMode}
|
|
871
770
|
onToggleViewMode={() => setViewMode((prev) => (prev === "grid" ? "focus" : "grid"))}
|
|
872
771
|
micLocked={micLocked}
|
|
772
|
+
isHost={isHost}
|
|
773
|
+
transportMode={transportMode}
|
|
774
|
+
onSetTransportMode={handleSetTransportMode}
|
|
775
|
+
onOpenDiagnostics={() => setDiagnosticsOpen(true)}
|
|
776
|
+
onOpenSettings={() => setSettingsOpen(true)}
|
|
873
777
|
onLeave={onLeave}
|
|
874
778
|
/>
|
|
875
779
|
</footer>
|
|
@@ -10,11 +10,20 @@
|
|
|
10
10
|
* camera is sending. Both are driven by `LocalMedia` (`apps/shared/media/useLocalMedia.ts`). The microphone button
|
|
11
11
|
* is disabled (`micLocked`) while talking-stick mode is on and this participant isn't the current holder - see
|
|
12
12
|
* `_CallView.tsx`'s doc comment.
|
|
13
|
+
*
|
|
14
|
+
* ## The "…" menu
|
|
15
|
+
*
|
|
16
|
+
* Sits between the grid/focus toggle and Leave, a catch-all for controls that don't need a dedicated button of
|
|
17
|
+
* their own: "Diagnostics" (`onOpenDiagnostics`) and "Settings" (`onOpenSettings`, host-only) each open their own
|
|
18
|
+
* persistent window/modal in `_CallView.tsx` and close this menu; "Connection method" is a four-way
|
|
19
|
+
* `menuitemradio` group (`TransportMode` - see `MeshConnectionManager.setTransportMode()`'s own doc comment)
|
|
20
|
+
* forcing how *this tab's* media reaches everyone else, independent of what anyone else has chosen. Otherwise an
|
|
21
|
+
* ordinary menu of this bar's own shape (`openMenu`, closes on Escape/outside click like every other one here).
|
|
13
22
|
*/
|
|
14
23
|
import React from "react";
|
|
15
24
|
import type { ScreenTransformState } from "../shared/media/filters/ScreenTransform.js";
|
|
16
25
|
import type { LocalMedia } from "../shared/media/useLocalMedia.js";
|
|
17
|
-
import { type MeshParticipant } from "../shared/webrtc/types.js";
|
|
26
|
+
import { type MeshParticipant, type TransportMode } from "../shared/webrtc/types.js";
|
|
18
27
|
export type CallViewMode = "grid" | "focus";
|
|
19
28
|
export interface CallControlsProps {
|
|
20
29
|
media: LocalMedia;
|
|
@@ -38,10 +47,16 @@ export interface CallControlsProps {
|
|
|
38
47
|
* doc comment on talking-stick mode. Disables the microphone toggle (not the device-picker chevron, which
|
|
39
48
|
* doesn't change whether anyone can hear them) rather than merely nudging it, unlike a `mute-request`. */
|
|
40
49
|
micLocked?: boolean;
|
|
50
|
+
/** Whether the "…" menu's "Settings" item is offered at all - see this module's doc comment on the menu. */
|
|
51
|
+
isHost: boolean;
|
|
52
|
+
transportMode: TransportMode;
|
|
53
|
+
onSetTransportMode: (mode: TransportMode) => void;
|
|
54
|
+
onOpenDiagnostics: () => void;
|
|
55
|
+
onOpenSettings: () => void;
|
|
41
56
|
onLeave: () => void;
|
|
42
57
|
}
|
|
43
58
|
/** Bars that rise and fall with the microphone's level (0-5) - a flat, dim set while there is no sound. */
|
|
44
59
|
export declare function LevelBars({ level }: {
|
|
45
60
|
level: number;
|
|
46
61
|
}): React.JSX.Element;
|
|
47
|
-
export default function CallControls({ media, participants, isPresenting, presentingElsewhereName, onToggleShare, screenTransform, onRotateScreen, onFlipScreen, handRaised, onToggleHand, onReaction, viewMode, onToggleViewMode, micLocked, onLeave, }: CallControlsProps): React.JSX.Element;
|
|
62
|
+
export default function CallControls({ media, participants, isPresenting, presentingElsewhereName, onToggleShare, screenTransform, onRotateScreen, onFlipScreen, handRaised, onToggleHand, onReaction, viewMode, onToggleViewMode, micLocked, isHost, transportMode, onSetTransportMode, onOpenDiagnostics, onOpenSettings, onLeave, }: CallControlsProps): React.JSX.Element;
|
|
@@ -15,12 +15,28 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
15
15
|
* camera is sending. Both are driven by `LocalMedia` (`apps/shared/media/useLocalMedia.ts`). The microphone button
|
|
16
16
|
* is disabled (`micLocked`) while talking-stick mode is on and this participant isn't the current holder - see
|
|
17
17
|
* `_CallView.tsx`'s doc comment.
|
|
18
|
+
*
|
|
19
|
+
* ## The "…" menu
|
|
20
|
+
*
|
|
21
|
+
* Sits between the grid/focus toggle and Leave, a catch-all for controls that don't need a dedicated button of
|
|
22
|
+
* their own: "Diagnostics" (`onOpenDiagnostics`) and "Settings" (`onOpenSettings`, host-only) each open their own
|
|
23
|
+
* persistent window/modal in `_CallView.tsx` and close this menu; "Connection method" is a four-way
|
|
24
|
+
* `menuitemradio` group (`TransportMode` - see `MeshConnectionManager.setTransportMode()`'s own doc comment)
|
|
25
|
+
* forcing how *this tab's* media reaches everyone else, independent of what anyone else has chosen. Otherwise an
|
|
26
|
+
* ordinary menu of this bar's own shape (`openMenu`, closes on Escape/outside click like every other one here).
|
|
18
27
|
*/
|
|
19
28
|
import { useEffect, useRef, useState } from "react";
|
|
20
29
|
import { filtersActive } from "../shared/media/filters/filterTypes.js";
|
|
21
30
|
import { REACTION_EMOJIS } from "../shared/webrtc/types.js";
|
|
22
31
|
import EffectsPanel from "./_EffectsPanel.js";
|
|
23
|
-
import { ChevronUpIcon, EffectsIcon, EmojiIcon, FlipIcon, FocusIcon, GridIcon, HandIcon, LeaveIcon, MicIcon, MicOffIcon, RotateIcon, ScreenShareIcon, VideoIcon, VideoOffIcon, } from "./_icons.js";
|
|
32
|
+
import { ChevronUpIcon, EffectsIcon, EmojiIcon, FlipIcon, FocusIcon, GridIcon, HandIcon, LeaveIcon, MicIcon, MicOffIcon, OverflowIcon, RotateIcon, ScreenShareIcon, VideoIcon, VideoOffIcon, } from "./_icons.js";
|
|
33
|
+
/** The "…" menu's "Connection method" options, in display order - see `TransportMode`'s own doc comment. */
|
|
34
|
+
const TRANSPORT_MODE_OPTIONS = [
|
|
35
|
+
{ mode: "auto", label: "Auto" },
|
|
36
|
+
{ mode: "p2p", label: "P2P" },
|
|
37
|
+
{ mode: "relay", label: "Relay" },
|
|
38
|
+
{ mode: "websocket", label: "WebSocket Relay" },
|
|
39
|
+
];
|
|
24
40
|
const BUTTON = "flex items-center justify-center h-12 rounded-full transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80";
|
|
25
41
|
const NEUTRAL = "bg-[#3c4043] text-white hover:bg-[#4b4f53]";
|
|
26
42
|
const ALERT = "bg-[#f9dedc] text-[#8c1d18] hover:bg-[#f2c4c0]";
|
|
@@ -29,7 +45,7 @@ const ACTIVE = "bg-[#a8c7fa] text-[#062e6f] hover:bg-[#8ab4f8]";
|
|
|
29
45
|
export function LevelBars({ level }) {
|
|
30
46
|
return (_jsx("span", { className: "flex items-center gap-[2px] h-5", "data-testid": "mic-level", "data-level": level, "aria-hidden": "true", children: [0.6, 1, 0.6].map((weight, i) => (_jsx("span", { className: `w-[3px] rounded-full ${level > 0 ? "bg-[#8ab4f8]" : "bg-white/50"}`, style: { height: `${4 + weight * level * 3}px` } }, i))) }));
|
|
31
47
|
}
|
|
32
|
-
export default function CallControls({ media, participants, isPresenting, presentingElsewhereName, onToggleShare, screenTransform, onRotateScreen, onFlipScreen, handRaised, onToggleHand, onReaction, viewMode, onToggleViewMode, micLocked, onLeave, }) {
|
|
48
|
+
export default function CallControls({ media, participants, isPresenting, presentingElsewhereName, onToggleShare, screenTransform, onRotateScreen, onFlipScreen, handRaised, onToggleHand, onReaction, viewMode, onToggleViewMode, micLocked, isHost, transportMode, onSetTransportMode, onOpenDiagnostics, onOpenSettings, onLeave, }) {
|
|
33
49
|
const [openMenu, setOpenMenu] = useState(null);
|
|
34
50
|
const barRef = useRef(null);
|
|
35
51
|
const shareDisabled = !isPresenting && !!presentingElsewhereName;
|
|
@@ -61,7 +77,16 @@ export default function CallControls({ media, participants, isPresenting, presen
|
|
|
61
77
|
return (_jsxs("div", { ref: barRef, className: "relative flex flex-wrap items-center justify-center gap-2 px-3", role: "toolbar", "aria-label": "Call controls", children: [_jsxs("div", { className: "sm:relative flex items-stretch gap-px", children: [_jsx("button", { type: "button", className: `${BUTTON} w-9 rounded-r-none ${media.micOn ? NEUTRAL : ALERT}`, "aria-label": "Choose microphone", "aria-haspopup": "menu", "aria-expanded": openMenu === "audio", onClick: () => toggleMenu("audio"), children: _jsx(ChevronUpIcon, {}) }), _jsxs("button", { type: "button", className: `${BUTTON} min-w-14 gap-1.5 px-3 rounded-l-none ${media.micOn ? NEUTRAL : ALERT} disabled:opacity-50 disabled:cursor-not-allowed`, "aria-label": media.micOn ? "Mute microphone" : "Unmute microphone", "aria-pressed": !media.micOn, disabled: micLocked, title: micLocked ? "Only the current talking-stick holder can unmute." : undefined, onClick: () => void media.toggleMic(), children: [media.micOn && _jsx(LevelBars, { level: media.audioLevel }), media.micOn ? _jsx(MicIcon, {}) : _jsx(MicOffIcon, {})] }), openMenu === "audio" && _jsx(DeviceMenu, { kind: "audio", media: media, onClose: () => setOpenMenu(null) })] }), _jsxs("div", { className: "sm:relative flex items-stretch gap-px", children: [_jsx("button", { type: "button", className: `${BUTTON} w-9 rounded-r-none ${media.cameraOn ? NEUTRAL : ALERT}`, "aria-label": "Choose camera", "aria-haspopup": "menu", "aria-expanded": openMenu === "video", onClick: () => toggleMenu("video"), children: _jsx(ChevronUpIcon, {}) }), _jsxs("button", { type: "button", className: `${BUTTON} relative min-w-14 px-3 rounded-l-none ${media.cameraOn ? NEUTRAL : ALERT}`, "aria-label": media.cameraOn ? "Turn off camera" : "Turn on camera", "aria-pressed": !media.cameraOn, onClick: () => void media.toggleCamera(), children: [media.cameraOn ? _jsx(VideoIcon, {}) : _jsx(VideoOffIcon, {}), media.cameraOn && (_jsx("span", { className: "absolute top-2 right-2 w-2 h-2 rounded-full bg-[#34a853] animate-pulse", "data-testid": "camera-live", title: "Your camera is sending" }))] }), openMenu === "video" && _jsx(DeviceMenu, { kind: "video", media: media, onClose: () => setOpenMenu(null) })] }), _jsxs("div", { className: "sm:relative", children: [_jsx("button", { type: "button", className: `${BUTTON} w-12 ${filtersActive(media.filters) ? ACTIVE : NEUTRAL}`, "aria-label": "Video effects", "aria-haspopup": "dialog", "aria-expanded": openMenu === "effects", onClick: () => toggleMenu("effects"), children: _jsx(EffectsIcon, {}) }), openMenu === "effects" && (_jsx("div", { role: "dialog", "aria-label": "Video effects", className: "absolute bottom-full mb-3 left-1/2 -translate-x-1/2 w-96 max-w-[calc(100vw-1rem)] max-h-[60vh] overflow-y-auto p-3 rounded-2xl bg-[#2b2d30] text-white shadow-xl", children: _jsx(EffectsPanel, { media: media, tone: "dark" }) }))] }), _jsx("button", { type: "button", className: `${BUTTON} w-12 ${isPresenting ? ACTIVE : NEUTRAL} disabled:opacity-40 disabled:cursor-not-allowed`, "aria-label": isPresenting ? "Stop sharing" : "Share screen", "aria-pressed": isPresenting, disabled: shareDisabled, title: shareDisabled ? `${presentingElsewhereName} is presenting - stop their share to present yourself.` : undefined, onClick: onToggleShare, children: _jsx(ScreenShareIcon, {}) }), isPresenting && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: `${BUTTON} w-12 ${NEUTRAL}`, "aria-label": "Rotate shared screen", title: "If your shared window looks sideways or upside down to everyone, rotate it here - this is a correction for your own capture, not a browser bug this app can fix automatically.", onClick: onRotateScreen, children: _jsx(RotateIcon, {}) }), _jsx("button", { type: "button", className: `${BUTTON} w-12 ${screenTransform.flipped ? ACTIVE : NEUTRAL}`, "aria-label": screenTransform.flipped ? "Unflip shared screen" : "Flip shared screen", "aria-pressed": screenTransform.flipped, onClick: onFlipScreen, children: _jsx(FlipIcon, {}) })] })), _jsxs("div", { className: "sm:relative", children: [_jsx("button", { type: "button", className: `${BUTTON} w-12 ${NEUTRAL}`, "aria-label": "Send a reaction", "aria-haspopup": "menu", "aria-expanded": openMenu === "emoji", onClick: () => toggleMenu("emoji"), children: _jsx(EmojiIcon, {}) }), openMenu === "emoji" && (_jsx("div", { role: "menu", "aria-label": "Reactions", className: "absolute bottom-full mb-3 left-1/2 -translate-x-1/2 w-max grid grid-cols-4 gap-1 p-2 rounded-2xl bg-[#2b2d30] shadow-xl", children: REACTION_EMOJIS.map((emoji) => (_jsx("button", { type: "button", role: "menuitem", className: "w-11 h-11 rounded-lg text-2xl hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80", "aria-label": `Send ${emoji}`, onClick: () => {
|
|
62
78
|
onReaction(emoji);
|
|
63
79
|
setOpenMenu(null);
|
|
64
|
-
}, children: emoji }, emoji))) }))] }), _jsx("button", { type: "button", className: `${BUTTON} w-12 ${handRaised ? ACTIVE : NEUTRAL}`, "aria-label": handRaised ? "Lower hand" : "Raise hand", "aria-pressed": handRaised, onClick: onToggleHand, children: _jsx(HandIcon, {}) }), _jsx("button", { type: "button", className: `${BUTTON} w-12 ${NEUTRAL} disabled:opacity-40 disabled:cursor-not-allowed`, "aria-label": viewMode === "grid" ? "Switch to focused view" : "Switch to grid view", disabled: participants.length < 2, title: participants.length < 2 ? "Grid and focused view look the same with only one other participant." : undefined, onClick: onToggleViewMode, children: viewMode === "grid" ? _jsx(FocusIcon, {}) : _jsx(GridIcon, {}) }), _jsx("button", { type: "button", className: `${BUTTON} w-
|
|
80
|
+
}, children: emoji }, emoji))) }))] }), _jsx("button", { type: "button", className: `${BUTTON} w-12 ${handRaised ? ACTIVE : NEUTRAL}`, "aria-label": handRaised ? "Lower hand" : "Raise hand", "aria-pressed": handRaised, onClick: onToggleHand, children: _jsx(HandIcon, {}) }), _jsx("button", { type: "button", className: `${BUTTON} w-12 ${NEUTRAL} disabled:opacity-40 disabled:cursor-not-allowed`, "aria-label": viewMode === "grid" ? "Switch to focused view" : "Switch to grid view", disabled: participants.length < 2, title: participants.length < 2 ? "Grid and focused view look the same with only one other participant." : undefined, onClick: onToggleViewMode, children: viewMode === "grid" ? _jsx(FocusIcon, {}) : _jsx(GridIcon, {}) }), _jsxs("div", { className: "sm:relative", children: [_jsx("button", { type: "button", className: `${BUTTON} w-12 ${openMenu === "overflow" ? ACTIVE : NEUTRAL}`, "aria-label": "More options", "aria-haspopup": "menu", "aria-expanded": openMenu === "overflow", onClick: () => toggleMenu("overflow"), children: _jsx(OverflowIcon, {}) }), openMenu === "overflow" && (_jsxs("div", { role: "menu", "aria-label": "More options", className: "absolute bottom-full mb-3 right-0 w-56 max-w-[calc(100vw-1rem)] rounded-2xl bg-[#2b2d30] text-white shadow-xl py-1 text-sm", children: [_jsx("button", { type: "button", role: "menuitem", className: "w-full px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80", onClick: () => {
|
|
81
|
+
onOpenDiagnostics();
|
|
82
|
+
setOpenMenu(null);
|
|
83
|
+
}, children: "Diagnostics" }), _jsx("div", { className: "my-1 border-t border-white/10" }), _jsx("p", { className: "px-3 pt-1 pb-0.5 text-xs font-semibold uppercase tracking-wide text-white/60", children: "Connection method" }), TRANSPORT_MODE_OPTIONS.map(({ mode, label }) => (_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": transportMode === mode, className: "flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80", onClick: () => {
|
|
84
|
+
onSetTransportMode(mode);
|
|
85
|
+
setOpenMenu(null);
|
|
86
|
+
}, children: [_jsx("span", { className: "w-4 text-[#8ab4f8]", "aria-hidden": "true", children: transportMode === mode ? "✓" : "" }), label] }, mode))), isHost && (_jsxs(_Fragment, { children: [_jsx("div", { className: "my-1 border-t border-white/10" }), _jsx("button", { type: "button", role: "menuitem", className: "w-full px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80", onClick: () => {
|
|
87
|
+
onOpenSettings();
|
|
88
|
+
setOpenMenu(null);
|
|
89
|
+
}, children: "Settings" })] }))] }))] }), _jsx("button", { type: "button", className: `${BUTTON} w-16 bg-[#d93025] text-white hover:bg-[#b3261e]`, "aria-label": "Leave call", onClick: () => onLeave(), children: _jsx(LeaveIcon, {}) })] }));
|
|
65
90
|
}
|
|
66
91
|
/** The device picker opened from the chevron beside the microphone/camera button: the available devices with the
|
|
67
92
|
* one in use ticked, or - while there are none to list - why, with a way to ask for access again. */
|
|
@@ -47,18 +47,17 @@
|
|
|
47
47
|
* `MeshConnectionManager`'s own doc comment on why that needs no extra code - the header chip then says nobody has
|
|
48
48
|
* the floor, until the host picks someone (or themselves) again.
|
|
49
49
|
*
|
|
50
|
-
* ##
|
|
50
|
+
* ## Diagnostics, connection method and settings
|
|
51
51
|
*
|
|
52
|
-
*
|
|
53
|
-
* `_DiagnosticsWindow.tsx` (a persistent, draggable
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* `_SettingsModal.tsx`, which used to be the participants drawer's
|
|
59
|
-
* waiting-room toggle) - moved out because none of those are
|
|
60
|
-
*
|
|
61
|
-
* close along with it.
|
|
52
|
+
* All three are opened from `_CallControls.tsx`'s own "…" menu (its doc comment covers the menu itself) via
|
|
53
|
+
* callback props, but owned here: `diagnosticsOpen` renders `_DiagnosticsWindow.tsx` (a persistent, draggable
|
|
54
|
+
* window - not a popover like the menus in the control bar, since troubleshooting a connection is something a
|
|
55
|
+
* participant wants to keep open while they poke at the rest of the call, not something that closes the moment
|
|
56
|
+
* they click elsewhere); `transportMode`/`handleSetTransportMode()` forces how *this tab's* media reaches everyone
|
|
57
|
+
* else (`TransportMode` - see `MeshConnectionManager.setTransportMode()`'s own doc comment), independent of what
|
|
58
|
+
* anyone else has chosen; `settingsOpen` renders `_SettingsModal.tsx`, which used to be the participants drawer's
|
|
59
|
+
* own header (mute-on-join, the join password, the waiting-room toggle) - moved out because none of those are
|
|
60
|
+
* about *who* is in the call, which is what the drawer is for.
|
|
62
61
|
*/
|
|
63
62
|
import React from "react";
|
|
64
63
|
import type { LocalMedia } from "../shared/media/useLocalMedia.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
///////////////////////////////////////////////////////////////////////////////
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
@@ -52,18 +52,17 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
52
52
|
* `MeshConnectionManager`'s own doc comment on why that needs no extra code - the header chip then says nobody has
|
|
53
53
|
* the floor, until the host picks someone (or themselves) again.
|
|
54
54
|
*
|
|
55
|
-
* ##
|
|
55
|
+
* ## Diagnostics, connection method and settings
|
|
56
56
|
*
|
|
57
|
-
*
|
|
58
|
-
* `_DiagnosticsWindow.tsx` (a persistent, draggable
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* `_SettingsModal.tsx`, which used to be the participants drawer's
|
|
64
|
-
* waiting-room toggle) - moved out because none of those are
|
|
65
|
-
*
|
|
66
|
-
* close along with it.
|
|
57
|
+
* All three are opened from `_CallControls.tsx`'s own "…" menu (its doc comment covers the menu itself) via
|
|
58
|
+
* callback props, but owned here: `diagnosticsOpen` renders `_DiagnosticsWindow.tsx` (a persistent, draggable
|
|
59
|
+
* window - not a popover like the menus in the control bar, since troubleshooting a connection is something a
|
|
60
|
+
* participant wants to keep open while they poke at the rest of the call, not something that closes the moment
|
|
61
|
+
* they click elsewhere); `transportMode`/`handleSetTransportMode()` forces how *this tab's* media reaches everyone
|
|
62
|
+
* else (`TransportMode` - see `MeshConnectionManager.setTransportMode()`'s own doc comment), independent of what
|
|
63
|
+
* anyone else has chosen; `settingsOpen` renders `_SettingsModal.tsx`, which used to be the participants drawer's
|
|
64
|
+
* own header (mute-on-join, the join password, the waiting-room toggle) - moved out because none of those are
|
|
65
|
+
* about *who* is in the call, which is what the drawer is for.
|
|
67
66
|
*/
|
|
68
67
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
69
68
|
import { playRaisedHandChime } from "../shared/media/chime.js";
|
|
@@ -76,7 +75,7 @@ import { MeshConnectionManager } from "../shared/webrtc/MeshConnectionManager.js
|
|
|
76
75
|
import { createRelayTransport } from "../shared/relay/RelayTransport.js";
|
|
77
76
|
import { GuestSignalingClient } from "../shared/push/GuestSignalingClient.js";
|
|
78
77
|
import CallControls from "./_CallControls.js";
|
|
79
|
-
import { BatonIcon
|
|
78
|
+
import { BatonIcon } from "./_icons.js";
|
|
80
79
|
import { admitParticipant, denyParticipant, kickParticipant, listWaitingParticipants, setForceMuteOnJoin as apiSetForceMuteOnJoin, setMeetingPassword, setWaitingRoomEnabled as apiSetWaitingRoomEnabled, } from "./_meetApi.js";
|
|
81
80
|
import DiagnosticsPanel from "./_DiagnosticsPanel.js";
|
|
82
81
|
import DiagnosticsWindow from "./_DiagnosticsWindow.js";
|
|
@@ -89,13 +88,6 @@ const REACTION_MS = 4000;
|
|
|
89
88
|
const MAX_REACTIONS = 12;
|
|
90
89
|
/** How often the host's drawer refreshes its own waiting-room list while open. */
|
|
91
90
|
const DEFAULT_WAITING_POLL_MS = 3000;
|
|
92
|
-
/** The "…" menu's "Connection method" options, in display order - see `TransportMode`'s own doc comment. */
|
|
93
|
-
const TRANSPORT_MODE_OPTIONS = [
|
|
94
|
-
{ mode: "auto", label: "Auto" },
|
|
95
|
-
{ mode: "p2p", label: "P2P" },
|
|
96
|
-
{ mode: "relay", label: "Relay" },
|
|
97
|
-
{ mode: "websocket", label: "WebSocket Relay" },
|
|
98
|
-
];
|
|
99
91
|
/** Who is shown large - see this module's doc comment. `undefined` when nobody but the local participant has
|
|
100
92
|
* joined yet (the grid/focus toggle still works, there's just nobody else to focus on). */
|
|
101
93
|
export function computeMainUid(options) {
|
|
@@ -150,12 +142,10 @@ export default function CallView({ channel, token, selfUid, selfName, meetingTit
|
|
|
150
142
|
const [waitingParticipants, setWaitingParticipants] = useState([]);
|
|
151
143
|
const [talkingStickActive, setTalkingStickActive] = useState(false);
|
|
152
144
|
const [talkingStickHolder, setTalkingStickHolder] = useState(undefined);
|
|
153
|
-
const [overflowMenuOpen, setOverflowMenuOpen] = useState(false);
|
|
154
145
|
const [diagnosticsOpen, setDiagnosticsOpen] = useState(false);
|
|
155
146
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
156
147
|
const [transportMode, setTransportModeState] = useState("auto");
|
|
157
148
|
const managerRef = useRef(null);
|
|
158
|
-
const overflowMenuRef = useRef(null);
|
|
159
149
|
/** The raw capture from `getDisplayMedia()` - only ever used to stop it (releasing the OS's own share
|
|
160
150
|
* indicator) and to notice the participant ending the share from the browser's own UI (`track.onended`). What's
|
|
161
151
|
* actually shown and sent is `screenTransformRef`'s output (`screenStream` state) - see `handleToggleShare()`. */
|
|
@@ -319,30 +309,6 @@ export default function CallView({ channel, token, selfUid, selfName, meetingTit
|
|
|
319
309
|
void mediaRef.current.toggleMic();
|
|
320
310
|
}
|
|
321
311
|
}, [talkingStickActive, selfHasTalkingStick]);
|
|
322
|
-
// The "…" menu closes on Escape or a press anywhere outside it - matching `_CallControls.tsx`'s own menus -
|
|
323
|
-
// but never the windows it opens (diagnostics, settings), which stay open independently (see this module's
|
|
324
|
-
// doc comment on the "…" menu).
|
|
325
|
-
useEffect(() => {
|
|
326
|
-
if (!overflowMenuOpen) {
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
const onPointerDown = (event) => {
|
|
330
|
-
if (!overflowMenuRef.current?.contains(event.target)) {
|
|
331
|
-
setOverflowMenuOpen(false);
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
const onKeyDown = (event) => {
|
|
335
|
-
if (event.key === "Escape") {
|
|
336
|
-
setOverflowMenuOpen(false);
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
document.addEventListener("pointerdown", onPointerDown);
|
|
340
|
-
document.addEventListener("keydown", onKeyDown);
|
|
341
|
-
return () => {
|
|
342
|
-
document.removeEventListener("pointerdown", onPointerDown);
|
|
343
|
-
document.removeEventListener("keydown", onKeyDown);
|
|
344
|
-
};
|
|
345
|
-
}, [overflowMenuOpen]);
|
|
346
312
|
useEffect(() => {
|
|
347
313
|
if (!presenterUid) {
|
|
348
314
|
setActiveSpeakerUid((prev) => pickActiveSpeaker(levels, prev));
|
|
@@ -468,12 +434,11 @@ export default function CallView({ channel, token, selfUid, selfName, meetingTit
|
|
|
468
434
|
managerRef.current?.setTalkingStick(true, uid);
|
|
469
435
|
}
|
|
470
436
|
/** Forces this tab's own transport policy - see `MeshConnectionManager.setTransportMode()`'s doc comment for
|
|
471
|
-
* what each mode actually does, and
|
|
472
|
-
* per-tab choice rather than a meeting setting. */
|
|
437
|
+
* what each mode actually does, and `_CallControls.tsx`'s doc comment on its "…" menu for why this is a
|
|
438
|
+
* personal, per-tab choice rather than a meeting setting. */
|
|
473
439
|
function handleSetTransportMode(mode) {
|
|
474
440
|
setTransportModeState(mode);
|
|
475
441
|
managerRef.current?.setTransportMode(mode);
|
|
476
|
-
setOverflowMenuOpen(false);
|
|
477
442
|
}
|
|
478
443
|
/** Admits one pending request - optimistically removed from the drawer's own list (the next poll would drop it
|
|
479
444
|
* anyway, once the requester's own next poll completes their join; removing it here just avoids the visible
|
|
@@ -562,13 +527,7 @@ export default function CallView({ channel, token, selfUid, selfName, meetingTit
|
|
|
562
527
|
else {
|
|
563
528
|
stage = (_jsx("div", { className: "h-full grid gap-2 auto-rows-fr [grid-template-columns:repeat(auto-fit,minmax(min(100%,320px),1fr))]", children: participants.map((p) => remoteTile(p, "h-full")) }));
|
|
564
529
|
}
|
|
565
|
-
return (_jsxs("div", { className: "fixed inset-0 z-50 flex bg-[#202124] text-white overflow-hidden", children: [_jsx("style", { children: `@keyframes meet-float { 0% { transform: translateY(0) scale(.6); opacity: 0; } 12% { opacity: 1; transform: translateY(-4vh) scale(1); } 100% { transform: translateY(-45vh) scale(1); opacity: 0; } }` }), _jsxs("div", { className: "relative flex-1 min-w-0 flex flex-col overflow-hidden", children: [_jsxs("header", { className: "shrink-0 flex items-center justify-between gap-3 px-4 py-3", children: [_jsx("h1", { className: "min-w-0 truncate text-base font-medium", children: meetingTitle }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [raisedNames.length > 0 && (_jsxs("span", { className: "max-w-[45vw] truncate px-3 py-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-sm font-medium", "data-testid": "raised-hands", children: ["\u270B ", raisedNames.join(", ")] })), talkingStickActive && (_jsxs("span", { className: "max-w-[45vw] truncate px-3 py-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-sm font-medium", "data-testid": "talking-stick-status", children: ["\uD83C\uDF99\uFE0F ", talkingStickHolderName ? `${talkingStickHolderName} ${talkingStickHolderName === "You" ? "have" : "has"} the floor` : "Waiting for the host to choose a speaker"] })), isHost && (_jsx("button", { type: "button", className: `w-9 h-9 flex items-center justify-center rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 ${talkingStickActive ? "bg-[#a8c7fa] text-[#062e6f] hover:bg-[#8ab4f8]" : "bg-[#3c4043] hover:bg-[#4b4f53]"}`, "aria-label": talkingStickActive ? "End talking stick" : "Talking stick", "aria-pressed": talkingStickActive, title: talkingStickActive ? "End talking stick" : "Start talking stick mode", onClick: handleToggleTalkingStick, children: _jsx(BatonIcon, {}) })), _jsx("button", { type: "button", className: "px-3 py-1.5 rounded-full bg-[#3c4043] hover:bg-[#4b4f53] text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80", "aria-label": `${participants.length + 1} participants`, "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen((prev) => !prev), children: participants.length + 1 }),
|
|
566
|
-
setDiagnosticsOpen(true);
|
|
567
|
-
setOverflowMenuOpen(false);
|
|
568
|
-
}, children: "Diagnostics" }), _jsx("div", { className: "my-1 border-t border-white/10" }), _jsx("p", { className: "px-3 pt-1 pb-0.5 text-xs font-semibold uppercase tracking-wide text-white/60", children: "Connection method" }), TRANSPORT_MODE_OPTIONS.map(({ mode, label }) => (_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": transportMode === mode, className: "flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80", onClick: () => handleSetTransportMode(mode), children: [_jsx("span", { className: "w-4 text-[#8ab4f8]", "aria-hidden": "true", children: transportMode === mode ? "✓" : "" }), label] }, mode))), isHost && (_jsxs(_Fragment, { children: [_jsx("div", { className: "my-1 border-t border-white/10" }), _jsx("button", { type: "button", role: "menuitem", className: "w-full px-3 py-2 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80", onClick: () => {
|
|
569
|
-
setSettingsOpen(true);
|
|
570
|
-
setOverflowMenuOpen(false);
|
|
571
|
-
}, children: "Settings" })] }))] }))] })] })] }), connectError && (_jsx("div", { role: "alert", className: "shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#601410] text-[#f9dedc] text-sm", children: connectError })), audioBlocked && (_jsx("button", { type: "button", className: "shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#a8c7fa] text-[#062e6f] text-sm font-medium", onClick: handleResumeAudio, children: "Click here to turn on sound" })), _jsxs("main", { className: "relative flex-1 min-h-0 px-3 pb-3", children: [stage, presenterName && presenterUid !== peerId && (_jsxs("p", { className: "absolute top-2 left-5 px-2 py-0.5 rounded bg-black/60 text-sm", children: [presenterName, " is presenting"] }))] }), _jsx("footer", { className: "shrink-0 pt-1 pb-[max(0.75rem,env(safe-area-inset-bottom))]", children: _jsx(CallControls, { media: media, participants: participants, isPresenting: isPresenting, presentingElsewhereName: presenterUid && presenterUid !== peerId ? presenterName : undefined, onToggleShare: () => void handleToggleShare(), screenTransform: screenTransform, onRotateScreen: handleRotateScreen, onFlipScreen: handleFlipScreen, handRaised: handRaised, onToggleHand: handleToggleHand, onReaction: handleReaction, viewMode: viewMode, onToggleViewMode: () => setViewMode((prev) => (prev === "grid" ? "focus" : "grid")), micLocked: micLocked, onLeave: onLeave }) }), !alone && (_jsx("div", { className: "absolute z-10 right-3 top-14 w-28 sm:top-auto sm:bottom-24 sm:w-52 aspect-video shadow-xl xl:right-4 xl:bottom-4", "data-testid": "self-view", children: selfTile("h-full") })), _jsx("div", { className: "pointer-events-none absolute left-4 bottom-28 w-40 h-[45vh]", "aria-hidden": "true", children: reactions.map((reaction) => (_jsxs("div", { className: "absolute bottom-0 flex flex-col items-center", style: { left: `${(reaction.id % 4) * 36}px`, animation: `meet-float ${REACTION_MS}ms ease-out forwards` }, "data-testid": "reaction", children: [_jsx("span", { className: "text-4xl leading-none", children: reaction.emoji }), _jsx("span", { className: "mt-1 px-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-xs", children: reaction.name })] }, reaction.id))) }), participants.map((p) => {
|
|
530
|
+
return (_jsxs("div", { className: "fixed inset-0 z-50 flex bg-[#202124] text-white overflow-hidden", children: [_jsx("style", { children: `@keyframes meet-float { 0% { transform: translateY(0) scale(.6); opacity: 0; } 12% { opacity: 1; transform: translateY(-4vh) scale(1); } 100% { transform: translateY(-45vh) scale(1); opacity: 0; } }` }), _jsxs("div", { className: "relative flex-1 min-w-0 flex flex-col overflow-hidden", children: [_jsxs("header", { className: "shrink-0 flex items-center justify-between gap-3 px-4 py-3", children: [_jsx("h1", { className: "min-w-0 truncate text-base font-medium", children: meetingTitle }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [raisedNames.length > 0 && (_jsxs("span", { className: "max-w-[45vw] truncate px-3 py-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-sm font-medium", "data-testid": "raised-hands", children: ["\u270B ", raisedNames.join(", ")] })), talkingStickActive && (_jsxs("span", { className: "max-w-[45vw] truncate px-3 py-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-sm font-medium", "data-testid": "talking-stick-status", children: ["\uD83C\uDF99\uFE0F ", talkingStickHolderName ? `${talkingStickHolderName} ${talkingStickHolderName === "You" ? "have" : "has"} the floor` : "Waiting for the host to choose a speaker"] })), isHost && (_jsx("button", { type: "button", className: `w-9 h-9 flex items-center justify-center rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 ${talkingStickActive ? "bg-[#a8c7fa] text-[#062e6f] hover:bg-[#8ab4f8]" : "bg-[#3c4043] hover:bg-[#4b4f53]"}`, "aria-label": talkingStickActive ? "End talking stick" : "Talking stick", "aria-pressed": talkingStickActive, title: talkingStickActive ? "End talking stick" : "Start talking stick mode", onClick: handleToggleTalkingStick, children: _jsx(BatonIcon, {}) })), _jsx("button", { type: "button", className: "px-3 py-1.5 rounded-full bg-[#3c4043] hover:bg-[#4b4f53] text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80", "aria-label": `${participants.length + 1} participants`, "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen((prev) => !prev), children: participants.length + 1 })] })] }), connectError && (_jsx("div", { role: "alert", className: "shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#601410] text-[#f9dedc] text-sm", children: connectError })), audioBlocked && (_jsx("button", { type: "button", className: "shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#a8c7fa] text-[#062e6f] text-sm font-medium", onClick: handleResumeAudio, children: "Click here to turn on sound" })), _jsxs("main", { className: "relative flex-1 min-h-0 px-3 pb-3", children: [stage, presenterName && presenterUid !== peerId && (_jsxs("p", { className: "absolute top-2 left-5 px-2 py-0.5 rounded bg-black/60 text-sm", children: [presenterName, " is presenting"] }))] }), _jsx("footer", { className: "shrink-0 pt-1 pb-[max(0.75rem,env(safe-area-inset-bottom))]", children: _jsx(CallControls, { media: media, participants: participants, isPresenting: isPresenting, presentingElsewhereName: presenterUid && presenterUid !== peerId ? presenterName : undefined, onToggleShare: () => void handleToggleShare(), screenTransform: screenTransform, onRotateScreen: handleRotateScreen, onFlipScreen: handleFlipScreen, handRaised: handRaised, onToggleHand: handleToggleHand, onReaction: handleReaction, viewMode: viewMode, onToggleViewMode: () => setViewMode((prev) => (prev === "grid" ? "focus" : "grid")), micLocked: micLocked, isHost: isHost, transportMode: transportMode, onSetTransportMode: handleSetTransportMode, onOpenDiagnostics: () => setDiagnosticsOpen(true), onOpenSettings: () => setSettingsOpen(true), onLeave: onLeave }) }), !alone && (_jsx("div", { className: "absolute z-10 right-3 top-14 w-28 sm:top-auto sm:bottom-24 sm:w-52 aspect-video shadow-xl xl:right-4 xl:bottom-4", "data-testid": "self-view", children: selfTile("h-full") })), _jsx("div", { className: "pointer-events-none absolute left-4 bottom-28 w-40 h-[45vh]", "aria-hidden": "true", children: reactions.map((reaction) => (_jsxs("div", { className: "absolute bottom-0 flex flex-col items-center", style: { left: `${(reaction.id % 4) * 36}px`, animation: `meet-float ${REACTION_MS}ms ease-out forwards` }, "data-testid": "reaction", children: [_jsx("span", { className: "text-4xl leading-none", children: reaction.emoji }), _jsx("span", { className: "mt-1 px-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-xs", children: reaction.name })] }, reaction.id))) }), participants.map((p) => {
|
|
572
531
|
const stream = remoteStreams[p.uid];
|
|
573
532
|
return stream ? _jsx(RemoteAudio, { stream: stream, onBlocked: handleAudioBlocked }, `${p.uid}:${audioNonce}`) : null;
|
|
574
533
|
}), _jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: announcement })] }), drawerOpen && (_jsx(ParticipantsDrawer, { selfName: selfName, micOn: media.micOn, handRaised: handRaised, participants: participants, isSelfHost: isHost, isParticipantHost: isParticipantHost, onMute: handleMuteParticipant, onKick: handleKickParticipant, waitingRoomEnabled: waitingRoomEnabled, waitingParticipants: waitingParticipants, onAdmit: handleAdmitParticipant, onDeny: handleDenyParticipant, talkingStickActive: talkingStickActive, selfPeerId: peerId, talkingStickHolder: talkingStickHolder, onGiveTalkingStick: handleGiveTalkingStick, onClose: () => setDrawerOpen(false) })), diagnosticsOpen && (_jsx(DiagnosticsWindow, { onClose: () => setDiagnosticsOpen(false), children: _jsx(DiagnosticsPanel, { selfName: selfName, micOn: media.micOn, cameraOn: media.cameraOn, participants: participants }) })), settingsOpen && isHost && (_jsx(SettingsModal, { forceMuteOnJoin: forceMuteOnJoin, onToggleForceMuteOnJoin: handleToggleForceMuteOnJoin, hasPassword: hasPassword, onSetPassword: handleSetPassword, waitingRoomEnabled: waitingRoomEnabled, onToggleWaitingRoomEnabled: handleToggleWaitingRoomEnabled, onClose: () => setSettingsOpen(false) }))] }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rapidmx/meet-plugin",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "RapidMX plugin that provides WebRTC video conferencing, joinable from a calendar invite or a shared link.",
|
|
5
5
|
"repository": "https://github.com/rapidmx/meet-plugin.git",
|
|
6
6
|
"author": "RapidREST <rapidrests@gmail.com>",
|