@rapidmx/meet-plugin 0.13.1 → 0.14.0

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.
Files changed (41) hide show
  1. package/README.md +55 -55
  2. package/apps/meet/[token].tsx +341 -341
  3. package/apps/meet/_CallControls.tsx +366 -394
  4. package/apps/meet/_CallView.tsx +987 -848
  5. package/apps/meet/_DiagnosticsWindow.tsx +80 -0
  6. package/apps/meet/_MeetLobby.tsx +224 -224
  7. package/apps/meet/_ParticipantTile.tsx +169 -169
  8. package/apps/meet/_ParticipantsDrawer.tsx +10 -104
  9. package/apps/meet/_SettingsModal.tsx +152 -0
  10. package/apps/meet/_icons.tsx +80 -82
  11. package/apps/meet/_meetApi.ts +212 -212
  12. package/apps/shared/media/filters/VideoFilterProcessor.ts +408 -408
  13. package/apps/shared/media/filters/mlModels.ts +174 -174
  14. package/apps/shared/push/GuestSignalingClient.ts +359 -359
  15. package/apps/shared/relay/RelayClient.ts +7 -2
  16. package/apps/shared/webrtc/MeshConnectionManager.ts +949 -795
  17. package/apps/shared/webrtc/types.ts +247 -222
  18. package/dist/apps/meet/_CallControls.d.ts +3 -8
  19. package/dist/apps/meet/_CallControls.js +3 -10
  20. package/dist/apps/meet/_CallView.d.ts +15 -2
  21. package/dist/apps/meet/_CallView.js +70 -4
  22. package/dist/apps/meet/_DiagnosticsWindow.d.ts +25 -0
  23. package/dist/apps/meet/_DiagnosticsWindow.js +45 -0
  24. package/dist/apps/meet/_ParticipantsDrawer.d.ts +10 -21
  25. package/dist/apps/meet/_ParticipantsDrawer.js +9 -39
  26. package/dist/apps/meet/_SettingsModal.d.ts +28 -0
  27. package/dist/apps/meet/_SettingsModal.js +59 -0
  28. package/dist/apps/meet/_icons.d.ts +1 -1
  29. package/dist/apps/meet/_icons.js +1 -1
  30. package/dist/apps/shared/relay/RelayClient.d.ts +6 -1
  31. package/dist/apps/shared/relay/RelayClient.js +7 -2
  32. package/dist/apps/shared/webrtc/MeshConnectionManager.d.ts +55 -2
  33. package/dist/apps/shared/webrtc/MeshConnectionManager.js +153 -9
  34. package/dist/apps/shared/webrtc/types.d.ts +25 -1
  35. package/dist/lib/util/RedisRelayBus.js +61 -5
  36. package/dist/lib/util/RedisRelayBus.js.map +1 -1
  37. package/dist/lib/util/RelayHub.js +10 -3
  38. package/dist/lib/util/RelayHub.js.map +1 -1
  39. package/dist/types/util/RedisRelayBus.d.ts +45 -6
  40. package/dist/types/util/RelayHub.d.ts +9 -2
  41. package/package.json +208 -208
@@ -1,169 +1,169 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- /** One participant's video tile - used by the grid, the focused/thumbnail-strip layouts and the local participant's
6
- * corner tile in `_CallView.tsx`. Renders a placeholder (the participant's initial) instead of a `<video>` element
7
- * whenever there's no stream or their camera is off, rather than showing a black/frozen frame.
8
- *
9
- * A tile never plays sound: its `<video>` is always muted, and what a participant says is played by the call view's
10
- * own `<audio>` elements (`_CallView.tsx`'s `RemoteAudio`), so a tile that isn't showing video - or isn't on screen
11
- * in the current layout - can never silence anyone.
12
- *
13
- * The local tile is shown mirrored (`scaleX(-1)`), like a real mirror, so a participant's own movements feel
14
- * natural - this is a local display transform only, never applied to the video actually sent to anyone, so
15
- * everyone else always sees the participant the right way round. `mirrored={false}` (`_CallView.tsx`, when a custom
16
- * background image is on) turns that off: the picture the participant chose is a fixed reference, not a live
17
- * reflection, and mirroring it would show it backwards to no one but themselves. */
18
- import React, { useEffect, useRef } from "react";
19
- import type { MediaTransport } from "../shared/webrtc/types.js";
20
- import { MicOffIcon } from "./_icons.js";
21
-
22
- /** What a tile says about how its participant's media is arriving, for the paths that are not the ordinary direct one
23
- * (which needs no comment). Text rather than only an icon, so the reason a picture is degraded is never a guess.
24
- * Exported so `_DiagnosticsPanel.tsx` shows the identical label/title rather than describing the same paths in
25
- * slightly different words. */
26
- export const TRANSPORT_BADGES: Partial<Record<MediaTransport, { label: string; title: string }>> = {
27
- turn: { label: "Relayed", title: "Connected through the relay server, because a direct connection was not possible." },
28
- "turn-tcp": {
29
- label: "Relayed (TCP)",
30
- title:
31
- "Connected through the relay server over TCP, because this network blocks the relay's usual UDP connection too. " +
32
- "Audio or video may pause briefly under network strain rather than just glitching - a property of TCP, not a dropped connection.",
33
- },
34
- websocket: {
35
- label: "Server relay",
36
- title: "Sent through the server as a last resort, because no other connection was possible. Video is lower quality and sound may lag.",
37
- },
38
- failed: { label: "Can't connect", title: "This participant could not be reached from your network." },
39
- };
40
-
41
- /** A short line saying the connection is still being made, with a spinner - a polite live region (not `role="status"`,
42
- * which the call view's own announcement region already is), so a participant who cannot see the tile still learns
43
- * why nothing is happening yet. */
44
- function StatusPill({ status, className }: { status: string; className?: string }) {
45
- return (
46
- <span
47
- aria-live="polite"
48
- data-testid="tile-status"
49
- className={`flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-black/60 text-white text-xs ${className ?? ""}`}
50
- >
51
- <span className="inline-block w-3 h-3 rounded-full border-2 border-white/30 border-t-white animate-spin" aria-hidden="true" />
52
- {status}
53
- </span>
54
- );
55
- }
56
-
57
- export interface ParticipantTileProps {
58
- name: string;
59
- stream?: MediaStream | null;
60
- isLocal?: boolean;
61
- /** Shows the initial instead of the video. */
62
- cameraOff?: boolean;
63
- micMuted?: boolean;
64
- handRaised?: boolean;
65
- /** How this participant's media reaches you - a badge is shown for everything but a direct or still-connecting one. */
66
- transport?: MediaTransport;
67
- /** Says the connection is still being made, e.g. "Connecting..." on the local tile and "Awaiting connection..." on a
68
- * participant's - shown with a spinner until the caller clears it. */
69
- status?: string;
70
- /** Highlights this tile as the current presenter/focus. */
71
- isFocused?: boolean;
72
- /** Fits the whole picture inside the tile instead of filling it - a shared screen must not be cropped. */
73
- contain?: boolean;
74
- /** Whether the local tile is shown mirrored, like a real mirror - ignored for a remote tile, which is never
75
- * mirrored. Default `true`; see this module's doc comment. */
76
- mirrored?: boolean;
77
- onClick?: () => void;
78
- className?: string;
79
- }
80
-
81
- function initialOf(name: string): string {
82
- return (Array.from(name.trim())[0] ?? "?").toUpperCase();
83
- }
84
-
85
- export default function ParticipantTile({
86
- name,
87
- stream,
88
- isLocal,
89
- cameraOff,
90
- micMuted,
91
- handRaised,
92
- transport,
93
- status,
94
- isFocused,
95
- contain,
96
- mirrored = true,
97
- onClick,
98
- className,
99
- }: ParticipantTileProps) {
100
- const badge = transport ? TRANSPORT_BADGES[transport] : undefined;
101
- const videoRef = useRef<HTMLVideoElement>(null);
102
- const showVideo = !!stream && !cameraOff;
103
-
104
- // The `<video>` element only exists while `showVideo`, so its stream is bound whenever it (re)appears too.
105
- useEffect(() => {
106
- if (videoRef.current) {
107
- videoRef.current.srcObject = stream ?? null;
108
- }
109
- }, [stream, showVideo]);
110
-
111
- return (
112
- <div
113
- className={`relative overflow-hidden rounded-xl bg-[#3c4043] flex items-center justify-center min-h-[90px] ${
114
- isFocused ? "ring-2 ring-[#8ab4f8]" : ""
115
- } ${className ?? ""}`}
116
- onClick={onClick}
117
- role={onClick ? "button" : undefined}
118
- tabIndex={onClick ? 0 : undefined}
119
- aria-label={onClick ? `${name}${isLocal ? " (you)" : ""}` : undefined}
120
- >
121
- {showVideo ? (
122
- <>
123
- <video
124
- ref={videoRef}
125
- autoPlay
126
- playsInline
127
- muted
128
- className={`w-full h-full ${contain ? "object-contain bg-black" : "object-cover"} ${isLocal && !contain && mirrored ? "[transform:scaleX(-1)]" : ""}`}
129
- />
130
- {status && <StatusPill status={status} className="absolute top-2 left-1/2 -translate-x-1/2 whitespace-nowrap" />}
131
- </>
132
- ) : (
133
- <div className="flex flex-col items-center gap-2 max-w-full px-1">
134
- <div className="w-16 h-16 rounded-full bg-primary text-white flex items-center justify-center text-2xl font-bold" aria-hidden="true">
135
- {initialOf(name)}
136
- </div>
137
- {status && <StatusPill status={status} />}
138
- </div>
139
- )}
140
- {handRaised && (
141
- <span className="absolute top-2 left-2 flex items-center justify-center w-8 h-8 rounded-full bg-[#a8c7fa] text-base" role="img" aria-label="Hand raised">
142
- ✋
143
- </span>
144
- )}
145
- {micMuted && (
146
- <span className="absolute top-2 right-2 flex items-center justify-center w-7 h-7 rounded-full bg-black/60 text-white" role="img" aria-label="Muted">
147
- <span className="w-4 h-4 [&>svg]:w-4 [&>svg]:h-4">
148
- <MicOffIcon />
149
- </span>
150
- </span>
151
- )}
152
- {badge && (
153
- <span
154
- className={`absolute bottom-2 right-2 px-2 py-0.5 rounded text-xs ${
155
- transport === "failed" ? "bg-[#601410] text-[#f9dedc]" : "bg-black/60 text-white"
156
- }`}
157
- title={badge.title}
158
- data-testid="transport-badge"
159
- >
160
- {badge.label}
161
- </span>
162
- )}
163
- <div className="absolute bottom-2 left-2 max-w-[calc(100%-1rem)] truncate px-2 py-0.5 rounded bg-black/60 text-white text-sm">
164
- {name}
165
- {isLocal ? " (you)" : ""}
166
- </div>
167
- </div>
168
- );
169
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /** One participant's video tile - used by the grid, the focused/thumbnail-strip layouts and the local participant's
6
+ * corner tile in `_CallView.tsx`. Renders a placeholder (the participant's initial) instead of a `<video>` element
7
+ * whenever there's no stream or their camera is off, rather than showing a black/frozen frame.
8
+ *
9
+ * A tile never plays sound: its `<video>` is always muted, and what a participant says is played by the call view's
10
+ * own `<audio>` elements (`_CallView.tsx`'s `RemoteAudio`), so a tile that isn't showing video - or isn't on screen
11
+ * in the current layout - can never silence anyone.
12
+ *
13
+ * The local tile is shown mirrored (`scaleX(-1)`), like a real mirror, so a participant's own movements feel
14
+ * natural - this is a local display transform only, never applied to the video actually sent to anyone, so
15
+ * everyone else always sees the participant the right way round. `mirrored={false}` (`_CallView.tsx`, when a custom
16
+ * background image is on) turns that off: the picture the participant chose is a fixed reference, not a live
17
+ * reflection, and mirroring it would show it backwards to no one but themselves. */
18
+ import React, { useEffect, useRef } from "react";
19
+ import type { MediaTransport } from "../shared/webrtc/types.js";
20
+ import { MicOffIcon } from "./_icons.js";
21
+
22
+ /** What a tile says about how its participant's media is arriving, for the paths that are not the ordinary direct one
23
+ * (which needs no comment). Text rather than only an icon, so the reason a picture is degraded is never a guess.
24
+ * Exported so `_DiagnosticsPanel.tsx` shows the identical label/title rather than describing the same paths in
25
+ * slightly different words. */
26
+ export const TRANSPORT_BADGES: Partial<Record<MediaTransport, { label: string; title: string }>> = {
27
+ turn: { label: "Relayed", title: "Connected through the relay server, because a direct connection was not possible." },
28
+ "turn-tcp": {
29
+ label: "Relayed (TCP)",
30
+ title:
31
+ "Connected through the relay server over TCP, because this network blocks the relay's usual UDP connection too. " +
32
+ "Audio or video may pause briefly under network strain rather than just glitching - a property of TCP, not a dropped connection.",
33
+ },
34
+ websocket: {
35
+ label: "Server relay",
36
+ title: "Sent through the server as a last resort, because no other connection was possible. Video is lower quality and sound may lag.",
37
+ },
38
+ failed: { label: "Can't connect", title: "This participant could not be reached from your network." },
39
+ };
40
+
41
+ /** A short line saying the connection is still being made, with a spinner - a polite live region (not `role="status"`,
42
+ * which the call view's own announcement region already is), so a participant who cannot see the tile still learns
43
+ * why nothing is happening yet. */
44
+ function StatusPill({ status, className }: { status: string; className?: string }) {
45
+ return (
46
+ <span
47
+ aria-live="polite"
48
+ data-testid="tile-status"
49
+ className={`flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-black/60 text-white text-xs ${className ?? ""}`}
50
+ >
51
+ <span className="inline-block w-3 h-3 rounded-full border-2 border-white/30 border-t-white animate-spin" aria-hidden="true" />
52
+ {status}
53
+ </span>
54
+ );
55
+ }
56
+
57
+ export interface ParticipantTileProps {
58
+ name: string;
59
+ stream?: MediaStream | null;
60
+ isLocal?: boolean;
61
+ /** Shows the initial instead of the video. */
62
+ cameraOff?: boolean;
63
+ micMuted?: boolean;
64
+ handRaised?: boolean;
65
+ /** How this participant's media reaches you - a badge is shown for everything but a direct or still-connecting one. */
66
+ transport?: MediaTransport;
67
+ /** Says the connection is still being made, e.g. "Connecting..." on the local tile and "Awaiting connection..." on a
68
+ * participant's - shown with a spinner until the caller clears it. */
69
+ status?: string;
70
+ /** Highlights this tile as the current presenter/focus. */
71
+ isFocused?: boolean;
72
+ /** Fits the whole picture inside the tile instead of filling it - a shared screen must not be cropped. */
73
+ contain?: boolean;
74
+ /** Whether the local tile is shown mirrored, like a real mirror - ignored for a remote tile, which is never
75
+ * mirrored. Default `true`; see this module's doc comment. */
76
+ mirrored?: boolean;
77
+ onClick?: () => void;
78
+ className?: string;
79
+ }
80
+
81
+ function initialOf(name: string): string {
82
+ return (Array.from(name.trim())[0] ?? "?").toUpperCase();
83
+ }
84
+
85
+ export default function ParticipantTile({
86
+ name,
87
+ stream,
88
+ isLocal,
89
+ cameraOff,
90
+ micMuted,
91
+ handRaised,
92
+ transport,
93
+ status,
94
+ isFocused,
95
+ contain,
96
+ mirrored = true,
97
+ onClick,
98
+ className,
99
+ }: ParticipantTileProps) {
100
+ const badge = transport ? TRANSPORT_BADGES[transport] : undefined;
101
+ const videoRef = useRef<HTMLVideoElement>(null);
102
+ const showVideo = !!stream && !cameraOff;
103
+
104
+ // The `<video>` element only exists while `showVideo`, so its stream is bound whenever it (re)appears too.
105
+ useEffect(() => {
106
+ if (videoRef.current) {
107
+ videoRef.current.srcObject = stream ?? null;
108
+ }
109
+ }, [stream, showVideo]);
110
+
111
+ return (
112
+ <div
113
+ className={`relative overflow-hidden rounded-xl bg-[#3c4043] flex items-center justify-center min-h-[90px] ${
114
+ isFocused ? "ring-2 ring-[#8ab4f8]" : ""
115
+ } ${className ?? ""}`}
116
+ onClick={onClick}
117
+ role={onClick ? "button" : undefined}
118
+ tabIndex={onClick ? 0 : undefined}
119
+ aria-label={onClick ? `${name}${isLocal ? " (you)" : ""}` : undefined}
120
+ >
121
+ {showVideo ? (
122
+ <>
123
+ <video
124
+ ref={videoRef}
125
+ autoPlay
126
+ playsInline
127
+ muted
128
+ className={`w-full h-full ${contain ? "object-contain bg-black" : "object-cover"} ${isLocal && !contain && mirrored ? "[transform:scaleX(-1)]" : ""}`}
129
+ />
130
+ {status && <StatusPill status={status} className="absolute top-2 left-1/2 -translate-x-1/2 whitespace-nowrap" />}
131
+ </>
132
+ ) : (
133
+ <div className="flex flex-col items-center gap-2 max-w-full px-1">
134
+ <div className="w-16 h-16 rounded-full bg-primary text-white flex items-center justify-center text-2xl font-bold" aria-hidden="true">
135
+ {initialOf(name)}
136
+ </div>
137
+ {status && <StatusPill status={status} />}
138
+ </div>
139
+ )}
140
+ {handRaised && (
141
+ <span className="absolute top-2 left-2 flex items-center justify-center w-8 h-8 rounded-full bg-[#a8c7fa] text-base" role="img" aria-label="Hand raised">
142
+ ✋
143
+ </span>
144
+ )}
145
+ {micMuted && (
146
+ <span className="absolute top-2 right-2 flex items-center justify-center w-7 h-7 rounded-full bg-black/60 text-white" role="img" aria-label="Muted">
147
+ <span className="w-4 h-4 [&>svg]:w-4 [&>svg]:h-4">
148
+ <MicOffIcon />
149
+ </span>
150
+ </span>
151
+ )}
152
+ {badge && (
153
+ <span
154
+ className={`absolute bottom-2 right-2 px-2 py-0.5 rounded text-xs ${
155
+ transport === "failed" ? "bg-[#601410] text-[#f9dedc]" : "bg-black/60 text-white"
156
+ }`}
157
+ title={badge.title}
158
+ data-testid="transport-badge"
159
+ >
160
+ {badge.label}
161
+ </span>
162
+ )}
163
+ <div className="absolute bottom-2 left-2 max-w-[calc(100%-1rem)] truncate px-2 py-0.5 rounded bg-black/60 text-white text-sm">
164
+ {name}
165
+ {isLocal ? " (you)" : ""}
166
+ </div>
167
+ </div>
168
+ );
169
+ }
@@ -19,21 +19,19 @@
19
19
  * offer) and a "Remove" button (a native `confirm()` first, since removal can't be undone by the participant the
20
20
  * way a mute request can) - never on the local participant's own row, and never shown to anyone but the host.
21
21
  *
22
- * The header also gets a host-only "Mute on join" checkbox (`forceMuteOnJoin`/`onToggleForceMuteOnJoin`) - a
23
- * setting for whoever joins *next*, not an action on anyone already here (muting someone already in the call is
24
- * what each row's own "Mute" button is for) - a host-only password section (`hasPassword`/`onSetPassword`), the
25
- * same "next joiner" scope: setting or changing it never affects anyone already in the call - and a host-only
26
- * waiting-room toggle plus list (`waitingRoomEnabled`/`onToggleWaitingRoomEnabled`,
27
- * `waitingParticipants`/`onAdmit`/`onDeny`): `_CallView.tsx` polls the list while this drawer is open and the
28
- * caller is the host (there is no push signal for a newly filed admission request), so it stays current without
29
- * the host needing to close and reopen the drawer.
22
+ * The header also gets a host-only "who's waiting" list (`waitingParticipants`/`onAdmit`/`onDeny`), shown whenever
23
+ * `waitingRoomEnabled` is on - `_CallView.tsx` polls the list while this drawer is open and the caller is the host
24
+ * (there is no push signal for a newly filed admission request), so it stays current without the host needing to
25
+ * close and reopen the drawer. The waiting-room toggle itself, along with "mute new participants on join" and the
26
+ * join password, lives in `_SettingsModal.tsx` instead (opened from the navbar's "…" menu) - this drawer only
27
+ * reads `waitingRoomEnabled` to decide whether the list has anywhere to come from, it doesn't set it.
30
28
  *
31
29
  * While talking-stick mode is on (`talkingStickActive`), every row - including the host's own - shows a "Give
32
30
  * stick" button, host-only, hidden on whichever row currently holds it (`talkingStickHolder`); everyone (not just
33
31
  * the host) sees a badge on that row instead, same visibility as the "Host" tag. See `_CallView.tsx`'s doc comment
34
32
  * on talking-stick mode for what granting it actually does to the recipient's microphone.
35
33
  */
36
- import React, { useEffect, useState } from "react";
34
+ import React, { useEffect } from "react";
37
35
  import type { MeshParticipant } from "../shared/webrtc/types.js";
38
36
  import type { WaitingParticipant } from "./_meetApi.js";
39
37
  import { TRANSPORT_BADGES } from "./_ParticipantTile.js";
@@ -52,19 +50,10 @@ export interface ParticipantsDrawerProps {
52
50
  /** Removes the given participant's `uid` - see `_CallView.tsx`'s `handleKickParticipant()` for what this
53
51
  * actually does (the cooperative signal plus the enforced server-side revoke). */
54
52
  onKick: (uid: string) => void;
55
- /** Whether a newly joining participant currently starts muted - the header checkbox's own state, host-only. */
56
- forceMuteOnJoin: boolean;
57
- onToggleForceMuteOnJoin: () => void;
58
- /** Whether this meeting currently requires a password - never the password itself, which the server never
59
- * sends back (see `PublicVideoMeeting.hasPassword`'s own doc comment). */
60
- hasPassword: boolean;
61
- /** Sets (a non-empty string), replaces, or removes (`null`) the join password. Rejecting lets
62
- * `PasswordSection` show its own inline error without this drawer needing to know anything about it. */
63
- onSetPassword: (password: string | null) => Promise<void>;
64
- /** Whether the meeting currently requires the host to admit each participant - the header checkbox's own
65
- * state, host-only. */
53
+ /** Whether the meeting currently requires the host to admit each participant - read-only here, set from
54
+ * `_SettingsModal.tsx` instead; this drawer only uses it to decide whether the waiting list has anywhere to
55
+ * come from. */
66
56
  waitingRoomEnabled: boolean;
67
- onToggleWaitingRoomEnabled: () => void;
68
57
  /** Everyone currently waiting to be admitted - `_CallView.tsx`'s own polled, always-current list; empty
69
58
  * whenever this drawer isn't open and host, by construction. */
70
59
  waitingParticipants: WaitingParticipant[];
@@ -84,71 +73,6 @@ export interface ParticipantsDrawerProps {
84
73
  onClose: () => void;
85
74
  }
86
75
 
87
- /** The host-only password form: one text input plus a Set/Change button, and a Remove button once one is set.
88
- * Owns its own input/error/busy state locally - `_CallView.tsx` only needs to know whether a password exists, not
89
- * what a half-typed one currently says. */
90
- function PasswordSection({ hasPassword, onSetPassword }: { hasPassword: boolean; onSetPassword: (password: string | null) => Promise<void> }) {
91
- const [input, setInput] = useState("");
92
- const [error, setError] = useState<string | null>(null);
93
- const [busy, setBusy] = useState(false);
94
-
95
- async function submit(password: string | null) {
96
- setBusy(true);
97
- setError(null);
98
- try {
99
- await onSetPassword(password);
100
- setInput("");
101
- } catch {
102
- setError("Could not save - try again.");
103
- } finally {
104
- setBusy(false);
105
- }
106
- }
107
-
108
- return (
109
- <div className="px-3 py-2 border-b border-white/10 text-sm">
110
- <p className="text-white/70 mb-1.5">{hasPassword ? "Password protection is on." : "No password required to join."}</p>
111
- <form
112
- className="flex gap-2"
113
- onSubmit={(event) => {
114
- event.preventDefault();
115
- if (input) {
116
- void submit(input);
117
- }
118
- }}
119
- >
120
- <input
121
- type="password"
122
- aria-label={hasPassword ? "New password" : "Set a password"}
123
- placeholder={hasPassword ? "New password" : "Set a password"}
124
- className="flex-1 min-w-0 px-2 py-1 rounded bg-white/10 text-white placeholder:text-white/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
125
- value={input}
126
- onChange={(event) => setInput(event.target.value)}
127
- disabled={busy}
128
- />
129
- <button
130
- type="submit"
131
- className="px-2 py-1 rounded text-xs whitespace-nowrap bg-white/10 hover:bg-white/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 disabled:opacity-50"
132
- disabled={busy || !input}
133
- >
134
- {hasPassword ? "Change" : "Set"}
135
- </button>
136
- </form>
137
- {hasPassword && (
138
- <button
139
- type="button"
140
- className="mt-1.5 text-xs text-[#f2b8b5] underline focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 disabled:opacity-50"
141
- disabled={busy}
142
- onClick={() => void submit(null)}
143
- >
144
- Remove password
145
- </button>
146
- )}
147
- {error && <p className="text-[#f2b8b5] text-xs mt-1.5">{error}</p>}
148
- </div>
149
- );
150
- }
151
-
152
76
  /** The host-only "who's waiting" list - shown only while `waitingRoomEnabled` is on, since there is otherwise never
153
77
  * anyone to list. Says so explicitly when the list is empty, rather than rendering nothing (silence here would be
154
78
  * indistinguishable from the list just not having loaded yet). */
@@ -304,12 +228,7 @@ export default function ParticipantsDrawer({
304
228
  isParticipantHost,
305
229
  onMute,
306
230
  onKick,
307
- forceMuteOnJoin,
308
- onToggleForceMuteOnJoin,
309
- hasPassword,
310
- onSetPassword,
311
231
  waitingRoomEnabled,
312
- onToggleWaitingRoomEnabled,
313
232
  waitingParticipants,
314
233
  onAdmit,
315
234
  onDeny,
@@ -346,19 +265,6 @@ export default function ParticipantsDrawer({
346
265
  ✕
347
266
  </button>
348
267
  </div>
349
- {isSelfHost && (
350
- <label className="flex items-center gap-2 px-3 py-2 border-b border-white/10 text-sm">
351
- <input type="checkbox" checked={forceMuteOnJoin} onChange={onToggleForceMuteOnJoin} className="w-4 h-4" />
352
- Mute new participants on join
353
- </label>
354
- )}
355
- {isSelfHost && <PasswordSection hasPassword={hasPassword} onSetPassword={onSetPassword} />}
356
- {isSelfHost && (
357
- <label className="flex items-center gap-2 px-3 py-2 border-b border-white/10 text-sm">
358
- <input type="checkbox" checked={waitingRoomEnabled} onChange={onToggleWaitingRoomEnabled} className="w-4 h-4" />
359
- Require the host to admit participants
360
- </label>
361
- )}
362
268
  {isSelfHost && waitingRoomEnabled && (
363
269
  <WaitingSection waitingParticipants={waitingParticipants} onAdmit={onAdmit} onDeny={onDeny} />
364
270
  )}