@rapidmx/meet-plugin 0.14.1 → 0.16.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.
- package/README.md +35 -55
- package/apps/meet/[token].tsx +341 -341
- package/apps/meet/_CallControls.tsx +508 -463
- package/apps/meet/_CallView.tsx +1060 -891
- package/apps/meet/_ChatPanel.tsx +194 -0
- package/apps/meet/_ColorWheel.tsx +136 -0
- package/apps/meet/_DiagnosticsPanel.tsx +65 -7
- package/apps/meet/_MeetLobby.tsx +224 -224
- package/apps/meet/_ParticipantTile.tsx +243 -169
- package/apps/meet/_ParticipantsDrawer.tsx +10 -7
- package/apps/meet/_Whiteboard.tsx +577 -0
- package/apps/meet/_icons.tsx +135 -80
- package/apps/meet/_meetApi.ts +213 -212
- package/apps/shared/collab/CollabSession.ts +675 -0
- package/apps/shared/collab/WhiteboardModel.ts +216 -0
- package/apps/shared/collab/chatMarkdown.tsx +375 -0
- package/apps/shared/collab/color.ts +67 -0
- package/apps/shared/collab/protocol.ts +321 -0
- package/apps/shared/collab/whiteboardRender.ts +311 -0
- package/apps/shared/media/filters/VideoFilterProcessor.ts +408 -408
- package/apps/shared/media/filters/mlModels.ts +174 -174
- package/apps/shared/relay/AudioPlayer.ts +29 -0
- package/apps/shared/relay/AudioSender.ts +23 -0
- package/apps/shared/relay/RelayClient.ts +54 -2
- package/apps/shared/relay/RelayReceiver.ts +43 -2
- package/apps/shared/relay/RelaySender.ts +23 -1
- package/apps/shared/relay/RelayTransport.ts +21 -1
- package/apps/shared/relay/SilenceMeter.ts +59 -0
- package/apps/shared/relay/relayEnv.ts +1 -1
- package/apps/shared/signaling/SignalingClient.ts +335 -0
- package/apps/shared/webrtc/MeshConnectionManager.ts +971 -949
- package/apps/shared/webrtc/types.ts +335 -247
- package/dist/apps/meet/[token].js +1 -1
- package/dist/apps/meet/_CallControls.d.ts +21 -6
- package/dist/apps/meet/_CallControls.js +14 -8
- package/dist/apps/meet/_CallView.d.ts +36 -7
- package/dist/apps/meet/_CallView.js +132 -16
- package/dist/apps/meet/_ChatPanel.d.ts +24 -0
- package/dist/apps/meet/_ChatPanel.js +116 -0
- package/dist/apps/meet/_ColorWheel.d.ts +14 -0
- package/dist/apps/meet/_ColorWheel.js +82 -0
- package/dist/apps/meet/_DiagnosticsPanel.d.ts +5 -3
- package/dist/apps/meet/_DiagnosticsPanel.js +17 -1
- package/dist/apps/meet/_ParticipantTile.d.ts +11 -3
- package/dist/apps/meet/_ParticipantTile.js +36 -5
- package/dist/apps/meet/_ParticipantsDrawer.js +3 -3
- package/dist/apps/meet/_Whiteboard.d.ts +64 -0
- package/dist/apps/meet/_Whiteboard.js +312 -0
- package/dist/apps/meet/_icons.d.ts +16 -0
- package/dist/apps/meet/_icons.js +16 -0
- package/dist/apps/meet/_meetApi.d.ts +8 -7
- package/dist/apps/meet/_meetApi.js +2 -2
- package/dist/apps/shared/collab/CollabSession.d.ts +208 -0
- package/dist/apps/shared/collab/CollabSession.js +559 -0
- package/dist/apps/shared/collab/WhiteboardModel.d.ts +54 -0
- package/dist/apps/shared/collab/WhiteboardModel.js +197 -0
- package/dist/apps/shared/collab/chatMarkdown.d.ts +65 -0
- package/dist/apps/shared/collab/chatMarkdown.js +311 -0
- package/dist/apps/shared/collab/color.d.ts +18 -0
- package/dist/apps/shared/collab/color.js +59 -0
- package/dist/apps/shared/collab/protocol.d.ts +157 -0
- package/dist/apps/shared/collab/protocol.js +222 -0
- package/dist/apps/shared/collab/whiteboardRender.d.ts +31 -0
- package/dist/apps/shared/collab/whiteboardRender.js +268 -0
- package/dist/apps/shared/relay/AudioPlayer.d.ts +9 -0
- package/dist/apps/shared/relay/AudioPlayer.js +24 -4
- package/dist/apps/shared/relay/AudioSender.d.ts +8 -0
- package/dist/apps/shared/relay/AudioSender.js +20 -4
- package/dist/apps/shared/relay/RelayClient.d.ts +16 -0
- package/dist/apps/shared/relay/RelayClient.js +46 -2
- package/dist/apps/shared/relay/RelayReceiver.d.ts +5 -0
- package/dist/apps/shared/relay/RelayReceiver.js +38 -6
- package/dist/apps/shared/relay/RelaySender.d.ts +7 -0
- package/dist/apps/shared/relay/RelaySender.js +20 -5
- package/dist/apps/shared/relay/RelayTransport.d.ts +6 -1
- package/dist/apps/shared/relay/RelayTransport.js +16 -1
- package/dist/apps/shared/relay/SilenceMeter.d.ts +25 -0
- package/dist/apps/shared/relay/SilenceMeter.js +55 -0
- package/dist/apps/shared/relay/relayEnv.d.ts +1 -1
- package/dist/apps/shared/signaling/SignalingClient.d.ts +95 -0
- package/dist/apps/shared/signaling/SignalingClient.js +278 -0
- package/dist/apps/shared/webrtc/MeshConnectionManager.d.ts +7 -7
- package/dist/apps/shared/webrtc/MeshConnectionManager.js +28 -9
- package/dist/apps/shared/webrtc/types.d.ts +104 -26
- package/dist/apps/shared/webrtc/types.js +3 -4
- package/dist/lib/routes/BaseVideoMeetingRoute.js +171 -69
- package/dist/lib/routes/BaseVideoMeetingRoute.js.map +1 -1
- package/dist/lib/util/RelayHub.js +15 -2
- package/dist/lib/util/RelayHub.js.map +1 -1
- package/dist/lib/util/SignalingHub.js +339 -0
- package/dist/lib/util/SignalingHub.js.map +1 -0
- package/dist/types/routes/BaseVideoMeetingRoute.d.ts +68 -46
- package/dist/types/util/RelayHub.d.ts +5 -2
- package/dist/types/util/SignalingHub.d.ts +112 -0
- package/package.json +208 -208
- package/apps/shared/push/GuestSignalingClient.ts +0 -359
- package/dist/apps/shared/push/GuestSignalingClient.d.ts +0 -98
- package/dist/apps/shared/push/GuestSignalingClient.js +0 -312
|
@@ -1,169 +1,243 @@
|
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
{
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
+
*
|
|
19
|
+
* `onHideSelf` adds a small "…" menu to the tile's corner, local-tile-only - see `_CallView.tsx`'s doc comment on
|
|
20
|
+
* hiding the local tile from grid view. */
|
|
21
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
22
|
+
import type { MediaTransport } from "../shared/webrtc/types.js";
|
|
23
|
+
import { MicOffIcon, OverflowIcon } from "./_icons.js";
|
|
24
|
+
|
|
25
|
+
/** What a tile says about how its participant's media is arriving, for the paths that are not the ordinary direct one
|
|
26
|
+
* (which needs no comment). Text rather than only an icon, so the reason a picture is degraded is never a guess.
|
|
27
|
+
* Exported so `_DiagnosticsPanel.tsx` shows the identical label/title rather than describing the same paths in
|
|
28
|
+
* slightly different words. */
|
|
29
|
+
export const TRANSPORT_BADGES: Partial<Record<MediaTransport, { label: string; title: string }>> = {
|
|
30
|
+
turn: { label: "Relayed", title: "Connected through the relay server, because a direct connection was not possible." },
|
|
31
|
+
"turn-tcp": {
|
|
32
|
+
label: "Relayed (TCP)",
|
|
33
|
+
title:
|
|
34
|
+
"Connected through the relay server over TCP, because this network blocks the relay's usual UDP connection too. " +
|
|
35
|
+
"Audio or video may pause briefly under network strain rather than just glitching - a property of TCP, not a dropped connection.",
|
|
36
|
+
},
|
|
37
|
+
websocket: {
|
|
38
|
+
label: "Server relay",
|
|
39
|
+
title: "Sent through the server as a last resort, because no other connection was possible. Video is lower quality and sound may lag.",
|
|
40
|
+
},
|
|
41
|
+
failed: { label: "Can't connect", title: "This participant could not be reached from your network." },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** A short line saying the connection is still being made, with a spinner - a polite live region (not `role="status"`,
|
|
45
|
+
* which the call view's own announcement region already is), so a participant who cannot see the tile still learns
|
|
46
|
+
* why nothing is happening yet. */
|
|
47
|
+
function StatusPill({ status, className }: { status: string; className?: string }) {
|
|
48
|
+
return (
|
|
49
|
+
<span
|
|
50
|
+
aria-live="polite"
|
|
51
|
+
data-testid="tile-status"
|
|
52
|
+
className={`flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-black/60 text-white text-xs ${className ?? ""}`}
|
|
53
|
+
>
|
|
54
|
+
<span className="inline-block w-3 h-3 rounded-full border-2 border-white/30 border-t-white animate-spin" aria-hidden="true" />
|
|
55
|
+
{status}
|
|
56
|
+
</span>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ParticipantTileProps {
|
|
61
|
+
name: string;
|
|
62
|
+
stream?: MediaStream | null;
|
|
63
|
+
isLocal?: boolean;
|
|
64
|
+
/** Shows the initial instead of the video. */
|
|
65
|
+
cameraOff?: boolean;
|
|
66
|
+
micMuted?: boolean;
|
|
67
|
+
handRaised?: boolean;
|
|
68
|
+
/** How this participant's media reaches you - a badge is shown for everything but a direct or still-connecting one. */
|
|
69
|
+
transport?: MediaTransport;
|
|
70
|
+
/** Says the connection is still being made, e.g. "Connecting..." on the local tile and "Awaiting connection..." on a
|
|
71
|
+
* participant's - shown with a spinner until the caller clears it. */
|
|
72
|
+
status?: string;
|
|
73
|
+
/** Highlights this tile as the current presenter/focus. */
|
|
74
|
+
isFocused?: boolean;
|
|
75
|
+
/** Marks this tile as showing a screen share rather than a camera - a screen share is never mirrored
|
|
76
|
+
* (regardless of `mirrored`/`isLocal`), since mirroring someone else's shared content would be disorienting. */
|
|
77
|
+
contain?: boolean;
|
|
78
|
+
/** Whether the local tile is shown mirrored, like a real mirror - ignored for a remote tile, which is never
|
|
79
|
+
* mirrored. Default `true`; see this module's doc comment. */
|
|
80
|
+
mirrored?: boolean;
|
|
81
|
+
/** Offers a small "…" menu in the tile's top-right corner with one item, "Hide self" - see `_CallView.tsx`'s
|
|
82
|
+
* doc comment on hiding the local tile. Omitted everywhere that shouldn't offer it: every remote tile, and the
|
|
83
|
+
* local tile while alone in the call, where hiding it would just blank the only thing on screen. */
|
|
84
|
+
onHideSelf?: () => void;
|
|
85
|
+
onClick?: () => void;
|
|
86
|
+
className?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function initialOf(name: string): string {
|
|
90
|
+
return (Array.from(name.trim())[0] ?? "?").toUpperCase();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default function ParticipantTile({
|
|
94
|
+
name,
|
|
95
|
+
stream,
|
|
96
|
+
isLocal,
|
|
97
|
+
cameraOff,
|
|
98
|
+
micMuted,
|
|
99
|
+
handRaised,
|
|
100
|
+
transport,
|
|
101
|
+
status,
|
|
102
|
+
isFocused,
|
|
103
|
+
contain,
|
|
104
|
+
mirrored = true,
|
|
105
|
+
onHideSelf,
|
|
106
|
+
onClick,
|
|
107
|
+
className,
|
|
108
|
+
}: ParticipantTileProps) {
|
|
109
|
+
const badge = transport ? TRANSPORT_BADGES[transport] : undefined;
|
|
110
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
111
|
+
const showVideo = !!stream && !cameraOff;
|
|
112
|
+
const [hideMenuOpen, setHideMenuOpen] = useState(false);
|
|
113
|
+
const hideMenuRef = useRef<HTMLDivElement>(null);
|
|
114
|
+
|
|
115
|
+
// The `<video>` element only exists while `showVideo`, so its stream is bound whenever it (re)appears too.
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (videoRef.current) {
|
|
118
|
+
videoRef.current.srcObject = stream ?? null;
|
|
119
|
+
}
|
|
120
|
+
}, [stream, showVideo]);
|
|
121
|
+
|
|
122
|
+
// The tile's own "…" menu closes on Escape or a press anywhere outside it, same shape as every other menu in
|
|
123
|
+
// this app.
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!hideMenuOpen) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
129
|
+
if (!hideMenuRef.current?.contains(event.target as Node)) {
|
|
130
|
+
setHideMenuOpen(false);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
134
|
+
if (event.key === "Escape") {
|
|
135
|
+
setHideMenuOpen(false);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
139
|
+
document.addEventListener("keydown", onKeyDown);
|
|
140
|
+
return () => {
|
|
141
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
142
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
143
|
+
};
|
|
144
|
+
}, [hideMenuOpen]);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div
|
|
148
|
+
className={`relative overflow-hidden rounded-xl bg-[#3c4043] flex items-center justify-center min-h-[90px] ${
|
|
149
|
+
isFocused ? "ring-2 ring-[#8ab4f8]" : ""
|
|
150
|
+
} ${className ?? ""}`}
|
|
151
|
+
onClick={onClick}
|
|
152
|
+
role={onClick ? "button" : undefined}
|
|
153
|
+
tabIndex={onClick ? 0 : undefined}
|
|
154
|
+
aria-label={onClick ? `${name}${isLocal ? " (you)" : ""}` : undefined}
|
|
155
|
+
>
|
|
156
|
+
{showVideo ? (
|
|
157
|
+
<>
|
|
158
|
+
<video
|
|
159
|
+
ref={videoRef}
|
|
160
|
+
autoPlay
|
|
161
|
+
playsInline
|
|
162
|
+
muted
|
|
163
|
+
className={`w-full h-full object-contain bg-black ${isLocal && !contain && mirrored ? "[transform:scaleX(-1)]" : ""}`}
|
|
164
|
+
/>
|
|
165
|
+
{status && <StatusPill status={status} className="absolute top-2 left-1/2 -translate-x-1/2 whitespace-nowrap" />}
|
|
166
|
+
</>
|
|
167
|
+
) : (
|
|
168
|
+
<div className="flex flex-col items-center gap-2 max-w-full px-1">
|
|
169
|
+
<div className="w-16 h-16 rounded-full bg-primary text-white flex items-center justify-center text-2xl font-bold" aria-hidden="true">
|
|
170
|
+
{initialOf(name)}
|
|
171
|
+
</div>
|
|
172
|
+
{status && <StatusPill status={status} />}
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
{handRaised && (
|
|
176
|
+
<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">
|
|
177
|
+
✋
|
|
178
|
+
</span>
|
|
179
|
+
)}
|
|
180
|
+
{(micMuted || onHideSelf) && (
|
|
181
|
+
<div className="absolute top-2 right-2 flex items-center gap-1">
|
|
182
|
+
{micMuted && (
|
|
183
|
+
<span className="flex items-center justify-center w-7 h-7 rounded-full bg-black/60 text-white" role="img" aria-label="Muted">
|
|
184
|
+
<span className="w-4 h-4 [&>svg]:w-4 [&>svg]:h-4">
|
|
185
|
+
<MicOffIcon />
|
|
186
|
+
</span>
|
|
187
|
+
</span>
|
|
188
|
+
)}
|
|
189
|
+
{onHideSelf && (
|
|
190
|
+
<div className="relative" ref={hideMenuRef}>
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
className="flex items-center justify-center w-7 h-7 rounded-full bg-black/60 hover:bg-black/80 text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
194
|
+
aria-label="Local video options"
|
|
195
|
+
aria-haspopup="menu"
|
|
196
|
+
aria-expanded={hideMenuOpen}
|
|
197
|
+
onClick={() => setHideMenuOpen((prev) => !prev)}
|
|
198
|
+
>
|
|
199
|
+
<span className="w-4 h-4 [&>svg]:w-4 [&>svg]:h-4">
|
|
200
|
+
<OverflowIcon />
|
|
201
|
+
</span>
|
|
202
|
+
</button>
|
|
203
|
+
{hideMenuOpen && (
|
|
204
|
+
<div
|
|
205
|
+
role="menu"
|
|
206
|
+
aria-label="Local video options"
|
|
207
|
+
className="absolute top-full right-0 mt-1 w-32 rounded-xl bg-[#2b2d30] text-white shadow-xl py-1 text-sm"
|
|
208
|
+
>
|
|
209
|
+
<button
|
|
210
|
+
type="button"
|
|
211
|
+
role="menuitem"
|
|
212
|
+
className="w-full px-3 py-1.5 text-left hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
213
|
+
onClick={() => {
|
|
214
|
+
setHideMenuOpen(false);
|
|
215
|
+
onHideSelf();
|
|
216
|
+
}}
|
|
217
|
+
>
|
|
218
|
+
Hide self
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
{badge && (
|
|
227
|
+
<span
|
|
228
|
+
className={`absolute bottom-2 right-2 px-2 py-0.5 rounded text-xs ${
|
|
229
|
+
transport === "failed" ? "bg-[#601410] text-[#f9dedc]" : "bg-black/60 text-white"
|
|
230
|
+
}`}
|
|
231
|
+
title={badge.title}
|
|
232
|
+
data-testid="transport-badge"
|
|
233
|
+
>
|
|
234
|
+
{badge.label}
|
|
235
|
+
</span>
|
|
236
|
+
)}
|
|
237
|
+
<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">
|
|
238
|
+
{name}
|
|
239
|
+
{isLocal ? " (you)" : ""}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
@@ -35,7 +35,7 @@ import React, { useEffect } from "react";
|
|
|
35
35
|
import type { MeshParticipant } from "../shared/webrtc/types.js";
|
|
36
36
|
import type { WaitingParticipant } from "./_meetApi.js";
|
|
37
37
|
import { TRANSPORT_BADGES } from "./_ParticipantTile.js";
|
|
38
|
-
import { MicOffIcon } from "./_icons.js";
|
|
38
|
+
import { BatonIcon, MicOffIcon, RemoveIcon } from "./_icons.js";
|
|
39
39
|
|
|
40
40
|
export interface ParticipantsDrawerProps {
|
|
41
41
|
selfName: string;
|
|
@@ -184,35 +184,38 @@ function Row({
|
|
|
184
184
|
{canGiveStick && !hasStick && (
|
|
185
185
|
<button
|
|
186
186
|
type="button"
|
|
187
|
-
className="
|
|
187
|
+
className="w-7 h-7 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 [&>svg]:w-4 [&>svg]:h-4"
|
|
188
188
|
aria-label={`Give the talking stick to ${name}`}
|
|
189
|
+
title={`Give the talking stick to ${name}`}
|
|
189
190
|
onClick={onGiveStick}
|
|
190
191
|
>
|
|
191
|
-
|
|
192
|
+
<BatonIcon />
|
|
192
193
|
</button>
|
|
193
194
|
)}
|
|
194
195
|
{canModerate && !micMuted && (
|
|
195
196
|
<button
|
|
196
197
|
type="button"
|
|
197
|
-
className="
|
|
198
|
+
className="w-7 h-7 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 [&>svg]:w-4 [&>svg]:h-4"
|
|
198
199
|
aria-label={`Mute ${name}`}
|
|
200
|
+
title={`Mute ${name}`}
|
|
199
201
|
onClick={onMute}
|
|
200
202
|
>
|
|
201
|
-
|
|
203
|
+
<MicOffIcon />
|
|
202
204
|
</button>
|
|
203
205
|
)}
|
|
204
206
|
{canModerate && (
|
|
205
207
|
<button
|
|
206
208
|
type="button"
|
|
207
|
-
className="
|
|
209
|
+
className="w-7 h-7 flex items-center justify-center rounded-full bg-[#601410] text-[#f9dedc] hover:bg-[#7a1b16] focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 [&>svg]:w-4 [&>svg]:h-4"
|
|
208
210
|
aria-label={`Remove ${name}`}
|
|
211
|
+
title={`Remove ${name}`}
|
|
209
212
|
onClick={() => {
|
|
210
213
|
if (window.confirm(`Remove ${name} from the call?`)) {
|
|
211
214
|
onKick?.();
|
|
212
215
|
}
|
|
213
216
|
}}
|
|
214
217
|
>
|
|
215
|
-
|
|
218
|
+
<RemoveIcon />
|
|
216
219
|
</button>
|
|
217
220
|
)}
|
|
218
221
|
</li>
|