@rapidmx/meet-plugin 0.11.0 → 0.12.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/apps/meet/_CallControls.tsx +7 -1
- package/apps/meet/_CallView.tsx +95 -93
- package/apps/meet/_ParticipantsDrawer.tsx +62 -64
- package/dist/apps/meet/_CallControls.d.ts +6 -0
- package/dist/apps/meet/_CallControls.js +7 -1
- package/dist/apps/meet/_CallView.js +4 -4
- package/dist/apps/meet/_ParticipantsDrawer.d.ts +4 -2
- package/dist/apps/meet/_ParticipantsDrawer.js +6 -4
- package/package.json +1 -1
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* screen, reactions, raise hand, grid/focus and leave. The effects button opens the video filter picker
|
|
10
10
|
* (`_EffectsPanel.tsx`) and lights up while any filter is on.
|
|
11
11
|
*
|
|
12
|
+
* The diagnostics button is the one exception to "opens near its own button": its panel is `fixed` to the
|
|
13
|
+
* viewport's top-left corner rather than anchored above the button, since it reads better as a HUD-style overlay
|
|
14
|
+
* sitting over the call than as a dropdown tucked into the bottom control bar. The outside-click/Escape handling
|
|
15
|
+
* (`barRef`) still closes it the same way, since it's still a DOM descendant of the bar - only its visual position
|
|
16
|
+
* moved, not its open/close wiring.
|
|
17
|
+
*
|
|
12
18
|
* The microphone button shows a live level while it is unmuted - bars that move with the sound the microphone is
|
|
13
19
|
* picking up, so a participant can see their audio is being sent - and the camera button shows a green dot while a
|
|
14
20
|
* camera is sending. Both are driven by `LocalMedia` (`apps/shared/media/useLocalMedia.ts`). The microphone button
|
|
@@ -322,7 +328,7 @@ export default function CallControls({
|
|
|
322
328
|
<div
|
|
323
329
|
role="dialog"
|
|
324
330
|
aria-label="Call diagnostics"
|
|
325
|
-
className="
|
|
331
|
+
className="fixed z-30 top-3 left-3 w-80 max-w-[calc(100vw-1rem)] max-h-[70vh] overflow-y-auto p-3 rounded-2xl bg-[#2b2d30]/95 text-white shadow-xl"
|
|
326
332
|
>
|
|
327
333
|
<DiagnosticsPanel selfName={selfName} micOn={media.micOn} cameraOn={media.cameraOn} participants={participants} />
|
|
328
334
|
</div>
|
package/apps/meet/_CallView.tsx
CHANGED
|
@@ -666,113 +666,115 @@ export default function CallView({
|
|
|
666
666
|
}
|
|
667
667
|
|
|
668
668
|
return (
|
|
669
|
-
<div className="fixed inset-0 z-50 flex
|
|
669
|
+
<div className="fixed inset-0 z-50 flex bg-[#202124] text-white overflow-hidden">
|
|
670
670
|
<style>{`@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; } }`}</style>
|
|
671
|
-
<
|
|
672
|
-
<
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
671
|
+
<div className="relative flex-1 min-w-0 flex flex-col overflow-hidden">
|
|
672
|
+
<header className="shrink-0 flex items-center justify-between gap-3 px-4 py-3">
|
|
673
|
+
<h1 className="min-w-0 truncate text-base font-medium">{meetingTitle}</h1>
|
|
674
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
675
|
+
{raisedNames.length > 0 && (
|
|
676
|
+
<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">
|
|
677
|
+
✋ {raisedNames.join(", ")}
|
|
678
|
+
</span>
|
|
679
|
+
)}
|
|
680
|
+
{talkingStickActive && (
|
|
681
|
+
<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">
|
|
682
|
+
🎙️ {talkingStickHolderName ? `${talkingStickHolderName} ${talkingStickHolderName === "You" ? "have" : "has"} the floor` : "Waiting for the host to choose a speaker"}
|
|
683
|
+
</span>
|
|
684
|
+
)}
|
|
685
|
+
{isHost && (
|
|
686
|
+
<button
|
|
687
|
+
type="button"
|
|
688
|
+
className={`px-3 py-1.5 rounded-full text-sm font-medium 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]"}`}
|
|
689
|
+
aria-pressed={talkingStickActive}
|
|
690
|
+
onClick={handleToggleTalkingStick}
|
|
691
|
+
>
|
|
692
|
+
{talkingStickActive ? "End talking stick" : "Talking stick"}
|
|
693
|
+
</button>
|
|
694
|
+
)}
|
|
685
695
|
<button
|
|
686
696
|
type="button"
|
|
687
|
-
className=
|
|
688
|
-
aria-
|
|
689
|
-
|
|
697
|
+
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"
|
|
698
|
+
aria-label={`${participants.length + 1} participants`}
|
|
699
|
+
aria-haspopup="dialog"
|
|
700
|
+
aria-expanded={drawerOpen}
|
|
701
|
+
onClick={() => setDrawerOpen((prev) => !prev)}
|
|
690
702
|
>
|
|
691
|
-
{
|
|
703
|
+
{participants.length + 1}
|
|
692
704
|
</button>
|
|
693
|
-
|
|
705
|
+
</div>
|
|
706
|
+
</header>
|
|
707
|
+
{connectError && (
|
|
708
|
+
<div role="alert" className="shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#601410] text-[#f9dedc] text-sm">
|
|
709
|
+
{connectError}
|
|
710
|
+
</div>
|
|
711
|
+
)}
|
|
712
|
+
{audioBlocked && (
|
|
694
713
|
<button
|
|
695
714
|
type="button"
|
|
696
|
-
className="px-3 py-
|
|
697
|
-
|
|
698
|
-
aria-haspopup="dialog"
|
|
699
|
-
aria-expanded={drawerOpen}
|
|
700
|
-
onClick={() => setDrawerOpen((prev) => !prev)}
|
|
715
|
+
className="shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#a8c7fa] text-[#062e6f] text-sm font-medium"
|
|
716
|
+
onClick={handleResumeAudio}
|
|
701
717
|
>
|
|
702
|
-
|
|
718
|
+
Click here to turn on sound
|
|
703
719
|
</button>
|
|
704
|
-
</div>
|
|
705
|
-
</header>
|
|
706
|
-
{connectError && (
|
|
707
|
-
<div role="alert" className="shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#601410] text-[#f9dedc] text-sm">
|
|
708
|
-
{connectError}
|
|
709
|
-
</div>
|
|
710
|
-
)}
|
|
711
|
-
{audioBlocked && (
|
|
712
|
-
<button
|
|
713
|
-
type="button"
|
|
714
|
-
className="shrink-0 mx-3 mb-2 px-3 py-2 rounded-lg bg-[#a8c7fa] text-[#062e6f] text-sm font-medium"
|
|
715
|
-
onClick={handleResumeAudio}
|
|
716
|
-
>
|
|
717
|
-
Click here to turn on sound
|
|
718
|
-
</button>
|
|
719
|
-
)}
|
|
720
|
-
<main className="relative flex-1 min-h-0 px-3 pb-3">
|
|
721
|
-
{stage}
|
|
722
|
-
{presenterName && presenterUid !== peerId && (
|
|
723
|
-
<p className="absolute top-2 left-5 px-2 py-0.5 rounded bg-black/60 text-sm">{presenterName} is presenting</p>
|
|
724
720
|
)}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
</div>
|
|
754
|
-
)}
|
|
755
|
-
|
|
756
|
-
<div className="pointer-events-none absolute left-4 bottom-28 w-40 h-[45vh]" aria-hidden="true">
|
|
757
|
-
{reactions.map((reaction) => (
|
|
721
|
+
<main className="relative flex-1 min-h-0 px-3 pb-3">
|
|
722
|
+
{stage}
|
|
723
|
+
{presenterName && presenterUid !== peerId && (
|
|
724
|
+
<p className="absolute top-2 left-5 px-2 py-0.5 rounded bg-black/60 text-sm">{presenterName} is presenting</p>
|
|
725
|
+
)}
|
|
726
|
+
</main>
|
|
727
|
+
<footer className="shrink-0 pt-1 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
|
|
728
|
+
<CallControls
|
|
729
|
+
media={media}
|
|
730
|
+
selfName={selfName}
|
|
731
|
+
participants={participants}
|
|
732
|
+
isPresenting={isPresenting}
|
|
733
|
+
presentingElsewhereName={presenterUid && presenterUid !== peerId ? presenterName : undefined}
|
|
734
|
+
onToggleShare={() => void handleToggleShare()}
|
|
735
|
+
screenTransform={screenTransform}
|
|
736
|
+
onRotateScreen={handleRotateScreen}
|
|
737
|
+
onFlipScreen={handleFlipScreen}
|
|
738
|
+
handRaised={handRaised}
|
|
739
|
+
onToggleHand={handleToggleHand}
|
|
740
|
+
onReaction={handleReaction}
|
|
741
|
+
viewMode={viewMode}
|
|
742
|
+
onToggleViewMode={() => setViewMode((prev) => (prev === "grid" ? "focus" : "grid"))}
|
|
743
|
+
micLocked={micLocked}
|
|
744
|
+
onLeave={onLeave}
|
|
745
|
+
/>
|
|
746
|
+
</footer>
|
|
747
|
+
|
|
748
|
+
{!alone && (
|
|
758
749
|
<div
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
style={{ left: `${(reaction.id % 4) * 36}px`, animation: `meet-float ${REACTION_MS}ms ease-out forwards` }}
|
|
762
|
-
data-testid="reaction"
|
|
750
|
+
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"
|
|
751
|
+
data-testid="self-view"
|
|
763
752
|
>
|
|
764
|
-
|
|
765
|
-
<span className="mt-1 px-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-xs">{reaction.name}</span>
|
|
753
|
+
{selfTile("h-full")}
|
|
766
754
|
</div>
|
|
767
|
-
)
|
|
768
|
-
|
|
755
|
+
)}
|
|
756
|
+
|
|
757
|
+
<div className="pointer-events-none absolute left-4 bottom-28 w-40 h-[45vh]" aria-hidden="true">
|
|
758
|
+
{reactions.map((reaction) => (
|
|
759
|
+
<div
|
|
760
|
+
key={reaction.id}
|
|
761
|
+
className="absolute bottom-0 flex flex-col items-center"
|
|
762
|
+
style={{ left: `${(reaction.id % 4) * 36}px`, animation: `meet-float ${REACTION_MS}ms ease-out forwards` }}
|
|
763
|
+
data-testid="reaction"
|
|
764
|
+
>
|
|
765
|
+
<span className="text-4xl leading-none">{reaction.emoji}</span>
|
|
766
|
+
<span className="mt-1 px-1.5 rounded-full bg-[#a8c7fa] text-[#062e6f] text-xs">{reaction.name}</span>
|
|
767
|
+
</div>
|
|
768
|
+
))}
|
|
769
|
+
</div>
|
|
769
770
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
771
|
+
{participants.map((p) => {
|
|
772
|
+
const stream = remoteStreams[p.uid];
|
|
773
|
+
return stream ? <RemoteAudio key={`${p.uid}:${audioNonce}`} stream={stream} onBlocked={handleAudioBlocked} /> : null;
|
|
774
|
+
})}
|
|
775
|
+
<div role="status" aria-live="polite" className="sr-only">
|
|
776
|
+
{announcement}
|
|
777
|
+
</div>
|
|
776
778
|
</div>
|
|
777
779
|
|
|
778
780
|
{drawerOpen && (
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* shows on a tile, as plain rows instead (a list reads better than tiles once there's a reason to scan for one
|
|
9
9
|
* name).
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* A docked sidebar, not an overlay: `_CallView.tsx` renders it as an ordinary flex sibling of the call's own
|
|
12
|
+
* content column, which shrinks to make room rather than being covered by a backdrop - meant to stay open and
|
|
13
|
+
* visible for as long as the participant wants, including while the call continues underneath. Closes on Escape or
|
|
14
|
+
* its own close button; there is no backdrop to click, on purpose.
|
|
13
15
|
*
|
|
14
16
|
* A "Host" tag marks the host's own row (`isSelfHost`/`isParticipantHost` - see `_CallView.tsx`'s doc comment on
|
|
15
17
|
* `hostUid` for what that identity is and isn't). Only while `isSelfHost` does every *other* row also get a "Mute"
|
|
@@ -328,70 +330,66 @@ export default function ParticipantsDrawer({
|
|
|
328
330
|
}, [onClose]);
|
|
329
331
|
|
|
330
332
|
return (
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
333
|
+
<aside
|
|
334
|
+
role="dialog"
|
|
335
|
+
aria-label="Participants"
|
|
336
|
+
className="relative z-10 shrink-0 w-72 max-w-[85vw] h-full flex flex-col bg-[#2b2d30] text-white shadow-xl border-l border-white/10"
|
|
337
|
+
>
|
|
338
|
+
<div className="flex items-center justify-between gap-2 px-3 py-3 border-b border-white/10">
|
|
339
|
+
<h2 className="text-sm font-medium">Participants ({participants.length + 1})</h2>
|
|
340
|
+
<button
|
|
341
|
+
type="button"
|
|
342
|
+
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
343
|
+
aria-label="Close participants"
|
|
344
|
+
onClick={onClose}
|
|
345
|
+
>
|
|
346
|
+
✕
|
|
347
|
+
</button>
|
|
348
|
+
</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
|
+
{isSelfHost && waitingRoomEnabled && (
|
|
363
|
+
<WaitingSection waitingParticipants={waitingParticipants} onAdmit={onAdmit} onDeny={onDeny} />
|
|
364
|
+
)}
|
|
365
|
+
<ul className="flex-1 min-h-0 overflow-y-auto p-2 text-sm">
|
|
366
|
+
<Row
|
|
367
|
+
name={selfName}
|
|
368
|
+
isSelf
|
|
369
|
+
isHost={isSelfHost}
|
|
370
|
+
micMuted={!micOn}
|
|
371
|
+
handRaised={handRaised}
|
|
372
|
+
hasStick={talkingStickActive && talkingStickHolder === selfPeerId}
|
|
373
|
+
canGiveStick={isSelfHost && talkingStickActive}
|
|
374
|
+
onGiveStick={() => onGiveTalkingStick(selfPeerId)}
|
|
375
|
+
/>
|
|
376
|
+
{participants.map((p) => (
|
|
367
377
|
<Row
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
isHost={
|
|
371
|
-
micMuted={!
|
|
372
|
-
handRaised={handRaised}
|
|
373
|
-
|
|
378
|
+
key={p.uid}
|
|
379
|
+
name={p.name}
|
|
380
|
+
isHost={isParticipantHost(p.uid)}
|
|
381
|
+
micMuted={!p.audioOn}
|
|
382
|
+
handRaised={p.handRaised}
|
|
383
|
+
transport={p.transport}
|
|
384
|
+
canModerate={isSelfHost}
|
|
385
|
+
onMute={() => onMute(p.uid)}
|
|
386
|
+
onKick={() => onKick(p.uid)}
|
|
387
|
+
hasStick={talkingStickActive && talkingStickHolder === p.uid}
|
|
374
388
|
canGiveStick={isSelfHost && talkingStickActive}
|
|
375
|
-
onGiveStick={() => onGiveTalkingStick(
|
|
389
|
+
onGiveStick={() => onGiveTalkingStick(p.uid)}
|
|
376
390
|
/>
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
name={p.name}
|
|
381
|
-
isHost={isParticipantHost(p.uid)}
|
|
382
|
-
micMuted={!p.audioOn}
|
|
383
|
-
handRaised={p.handRaised}
|
|
384
|
-
transport={p.transport}
|
|
385
|
-
canModerate={isSelfHost}
|
|
386
|
-
onMute={() => onMute(p.uid)}
|
|
387
|
-
onKick={() => onKick(p.uid)}
|
|
388
|
-
hasStick={talkingStickActive && talkingStickHolder === p.uid}
|
|
389
|
-
canGiveStick={isSelfHost && talkingStickActive}
|
|
390
|
-
onGiveStick={() => onGiveTalkingStick(p.uid)}
|
|
391
|
-
/>
|
|
392
|
-
))}
|
|
393
|
-
</ul>
|
|
394
|
-
</aside>
|
|
395
|
-
</>
|
|
391
|
+
))}
|
|
392
|
+
</ul>
|
|
393
|
+
</aside>
|
|
396
394
|
);
|
|
397
395
|
}
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* screen, reactions, raise hand, grid/focus and leave. The effects button opens the video filter picker
|
|
6
6
|
* (`_EffectsPanel.tsx`) and lights up while any filter is on.
|
|
7
7
|
*
|
|
8
|
+
* The diagnostics button is the one exception to "opens near its own button": its panel is `fixed` to the
|
|
9
|
+
* viewport's top-left corner rather than anchored above the button, since it reads better as a HUD-style overlay
|
|
10
|
+
* sitting over the call than as a dropdown tucked into the bottom control bar. The outside-click/Escape handling
|
|
11
|
+
* (`barRef`) still closes it the same way, since it's still a DOM descendant of the bar - only its visual position
|
|
12
|
+
* moved, not its open/close wiring.
|
|
13
|
+
*
|
|
8
14
|
* The microphone button shows a live level while it is unmuted - bars that move with the sound the microphone is
|
|
9
15
|
* picking up, so a participant can see their audio is being sent - and the camera button shows a green dot while a
|
|
10
16
|
* camera is sending. Both are driven by `LocalMedia` (`apps/shared/media/useLocalMedia.ts`). The microphone button
|
|
@@ -10,6 +10,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
10
10
|
* screen, reactions, raise hand, grid/focus and leave. The effects button opens the video filter picker
|
|
11
11
|
* (`_EffectsPanel.tsx`) and lights up while any filter is on.
|
|
12
12
|
*
|
|
13
|
+
* The diagnostics button is the one exception to "opens near its own button": its panel is `fixed` to the
|
|
14
|
+
* viewport's top-left corner rather than anchored above the button, since it reads better as a HUD-style overlay
|
|
15
|
+
* sitting over the call than as a dropdown tucked into the bottom control bar. The outside-click/Escape handling
|
|
16
|
+
* (`barRef`) still closes it the same way, since it's still a DOM descendant of the bar - only its visual position
|
|
17
|
+
* moved, not its open/close wiring.
|
|
18
|
+
*
|
|
13
19
|
* The microphone button shows a live level while it is unmuted - bars that move with the sound the microphone is
|
|
14
20
|
* picking up, so a participant can see their audio is being sent - and the camera button shows a green dot while a
|
|
15
21
|
* camera is sending. Both are driven by `LocalMedia` (`apps/shared/media/useLocalMedia.ts`). The microphone button
|
|
@@ -62,7 +68,7 @@ export default function CallControls({ media, selfName, participants, isPresenti
|
|
|
62
68
|
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: () => {
|
|
63
69
|
onReaction(emoji);
|
|
64
70
|
setOpenMenu(null);
|
|
65
|
-
}, 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}`, "aria-label": viewMode === "grid" ? "Switch to focused view" : "Switch to grid view", onClick: onToggleViewMode, children: viewMode === "grid" ? _jsx(FocusIcon, {}) : _jsx(GridIcon, {}) }), _jsxs("div", { className: "sm:relative", children: [_jsx("button", { type: "button", className: `${BUTTON} w-12 ${openMenu === "diagnostics" ? ACTIVE : NEUTRAL}`, "aria-label": "Call diagnostics", "aria-haspopup": "dialog", "aria-expanded": openMenu === "diagnostics", onClick: () => toggleMenu("diagnostics"), children: _jsx(DiagnosticsIcon, {}) }), openMenu === "diagnostics" && (_jsx("div", { role: "dialog", "aria-label": "Call diagnostics", className: "
|
|
71
|
+
}, 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}`, "aria-label": viewMode === "grid" ? "Switch to focused view" : "Switch to grid view", onClick: onToggleViewMode, children: viewMode === "grid" ? _jsx(FocusIcon, {}) : _jsx(GridIcon, {}) }), _jsxs("div", { className: "sm:relative", children: [_jsx("button", { type: "button", className: `${BUTTON} w-12 ${openMenu === "diagnostics" ? ACTIVE : NEUTRAL}`, "aria-label": "Call diagnostics", "aria-haspopup": "dialog", "aria-expanded": openMenu === "diagnostics", onClick: () => toggleMenu("diagnostics"), children: _jsx(DiagnosticsIcon, {}) }), openMenu === "diagnostics" && (_jsx("div", { role: "dialog", "aria-label": "Call diagnostics", className: "fixed z-30 top-3 left-3 w-80 max-w-[calc(100vw-1rem)] max-h-[70vh] overflow-y-auto p-3 rounded-2xl bg-[#2b2d30]/95 text-white shadow-xl", children: _jsx(DiagnosticsPanel, { selfName: selfName, micOn: media.micOn, cameraOn: media.cameraOn, participants: participants }) }))] }), _jsx("button", { type: "button", className: `${BUTTON} w-16 bg-[#d93025] text-white hover:bg-[#b3261e]`, "aria-label": "Leave call", onClick: () => onLeave(), children: _jsx(LeaveIcon, {}) })] }));
|
|
66
72
|
}
|
|
67
73
|
/** The device picker opened from the chevron beside the microphone/camera button: the available devices with the
|
|
68
74
|
* one in use ticked, or - while there are none to list - why, with a way to ask for access again. */
|
|
@@ -501,10 +501,10 @@ export default function CallView({ channel, token, selfUid, selfName, meetingTit
|
|
|
501
501
|
else {
|
|
502
502
|
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")) }));
|
|
503
503
|
}
|
|
504
|
-
return (_jsxs("div", { className: "fixed inset-0 z-50 flex
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
504
|
+
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: `px-3 py-1.5 rounded-full text-sm font-medium 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-pressed": talkingStickActive, onClick: handleToggleTalkingStick, children: talkingStickActive ? "End talking stick" : "Talking stick" })), _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, selfName: selfName, 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) => {
|
|
505
|
+
const stream = remoteStreams[p.uid];
|
|
506
|
+
return stream ? _jsx(RemoteAudio, { stream: stream, onBlocked: handleAudioBlocked }, `${p.uid}:${audioNonce}`) : null;
|
|
507
|
+
}), _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, forceMuteOnJoin: forceMuteOnJoin, onToggleForceMuteOnJoin: handleToggleForceMuteOnJoin, hasPassword: hasPassword, onSetPassword: handleSetPassword, waitingRoomEnabled: waitingRoomEnabled, onToggleWaitingRoomEnabled: handleToggleWaitingRoomEnabled, waitingParticipants: waitingParticipants, onAdmit: handleAdmitParticipant, onDeny: handleDenyParticipant, talkingStickActive: talkingStickActive, selfPeerId: peerId, talkingStickHolder: talkingStickHolder, onGiveTalkingStick: handleGiveTalkingStick, onClose: () => setDrawerOpen(false) }))] }));
|
|
508
508
|
}
|
|
509
509
|
/** Plays one remote participant's stream through a hidden `<audio>` element. `onBlocked` fires if the browser
|
|
510
510
|
* refuses to start it (an autoplay policy) - the view then asks for a click. */
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* shows on a tile, as plain rows instead (a list reads better than tiles once there's a reason to scan for one
|
|
5
5
|
* name).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* A docked sidebar, not an overlay: `_CallView.tsx` renders it as an ordinary flex sibling of the call's own
|
|
8
|
+
* content column, which shrinks to make room rather than being covered by a backdrop - meant to stay open and
|
|
9
|
+
* visible for as long as the participant wants, including while the call continues underneath. Closes on Escape or
|
|
10
|
+
* its own close button; there is no backdrop to click, on purpose.
|
|
9
11
|
*
|
|
10
12
|
* A "Host" tag marks the host's own row (`isSelfHost`/`isParticipantHost` - see `_CallView.tsx`'s doc comment on
|
|
11
13
|
* `hostUid` for what that identity is and isn't). Only while `isSelfHost` does every *other* row also get a "Mute"
|
|
@@ -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
|
|
@@ -9,8 +9,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
9
9
|
* shows on a tile, as plain rows instead (a list reads better than tiles once there's a reason to scan for one
|
|
10
10
|
* name).
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* A docked sidebar, not an overlay: `_CallView.tsx` renders it as an ordinary flex sibling of the call's own
|
|
13
|
+
* content column, which shrinks to make room rather than being covered by a backdrop - meant to stay open and
|
|
14
|
+
* visible for as long as the participant wants, including while the call continues underneath. Closes on Escape or
|
|
15
|
+
* its own close button; there is no backdrop to click, on purpose.
|
|
14
16
|
*
|
|
15
17
|
* A "Host" tag marks the host's own row (`isSelfHost`/`isParticipantHost` - see `_CallView.tsx`'s doc comment on
|
|
16
18
|
* `hostUid` for what that identity is and isn't). Only while `isSelfHost` does every *other* row also get a "Mute"
|
|
@@ -87,5 +89,5 @@ export default function ParticipantsDrawer({ selfName, micOn, handRaised, partic
|
|
|
87
89
|
document.addEventListener("keydown", onKeyDown);
|
|
88
90
|
return () => document.removeEventListener("keydown", onKeyDown);
|
|
89
91
|
}, [onClose]);
|
|
90
|
-
return (_jsxs(
|
|
92
|
+
return (_jsxs("aside", { role: "dialog", "aria-label": "Participants", className: "relative z-10 shrink-0 w-72 max-w-[85vw] h-full flex flex-col bg-[#2b2d30] text-white shadow-xl border-l border-white/10", children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-3 border-b border-white/10", children: [_jsxs("h2", { className: "text-sm font-medium", children: ["Participants (", participants.length + 1, ")"] }), _jsx("button", { type: "button", className: "w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80", "aria-label": "Close participants", onClick: onClose, children: "\u2715" })] }), isSelfHost && (_jsxs("label", { className: "flex items-center gap-2 px-3 py-2 border-b border-white/10 text-sm", children: [_jsx("input", { type: "checkbox", checked: forceMuteOnJoin, onChange: onToggleForceMuteOnJoin, className: "w-4 h-4" }), "Mute new participants on join"] })), isSelfHost && _jsx(PasswordSection, { hasPassword: hasPassword, onSetPassword: onSetPassword }), isSelfHost && (_jsxs("label", { className: "flex items-center gap-2 px-3 py-2 border-b border-white/10 text-sm", children: [_jsx("input", { type: "checkbox", checked: waitingRoomEnabled, onChange: onToggleWaitingRoomEnabled, className: "w-4 h-4" }), "Require the host to admit participants"] })), isSelfHost && waitingRoomEnabled && (_jsx(WaitingSection, { waitingParticipants: waitingParticipants, onAdmit: onAdmit, onDeny: onDeny })), _jsxs("ul", { className: "flex-1 min-h-0 overflow-y-auto p-2 text-sm", children: [_jsx(Row, { name: selfName, isSelf: true, isHost: isSelfHost, micMuted: !micOn, handRaised: handRaised, hasStick: talkingStickActive && talkingStickHolder === selfPeerId, canGiveStick: isSelfHost && talkingStickActive, onGiveStick: () => onGiveTalkingStick(selfPeerId) }), participants.map((p) => (_jsx(Row, { name: p.name, isHost: isParticipantHost(p.uid), micMuted: !p.audioOn, handRaised: p.handRaised, transport: p.transport, canModerate: isSelfHost, onMute: () => onMute(p.uid), onKick: () => onKick(p.uid), hasStick: talkingStickActive && talkingStickHolder === p.uid, canGiveStick: isSelfHost && talkingStickActive, onGiveStick: () => onGiveTalkingStick(p.uid) }, p.uid)))] })] }));
|
|
91
93
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rapidmx/meet-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
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>",
|