Stop Chat and More from flashing on load in audio rooms.
Deploy Ladill Meet / deploy (push) Successful in 57s
Deploy Ladill Meet / deploy (push) Successful in 57s
Bootstrap room-type flags from meet-config before Alpine paints, gate toolbar and banners server-side per room type, and hide Alpine toolbar items until init completes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+112
-37
@@ -1,17 +1,92 @@
|
||||
import Alpine from 'alpinejs';
|
||||
import { ConnectionState, Room, RoomEvent, Track } from 'livekit-client';
|
||||
|
||||
function readMeetBootState() {
|
||||
function readMeetRoomBootState() {
|
||||
const el = document.getElementById('meet-config');
|
||||
|
||||
if (!el) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let presenterRefs = [];
|
||||
let panels = [];
|
||||
let sessionParticipants = [];
|
||||
let roomHost = null;
|
||||
|
||||
try {
|
||||
presenterRefs = JSON.parse(el.dataset.presenterRefs || '[]');
|
||||
} catch {
|
||||
presenterRefs = [];
|
||||
}
|
||||
|
||||
try {
|
||||
panels = JSON.parse(el.dataset.panels || '[]');
|
||||
} catch {
|
||||
panels = [];
|
||||
}
|
||||
|
||||
try {
|
||||
sessionParticipants = JSON.parse(el.dataset.initialParticipants || '[]');
|
||||
} catch {
|
||||
sessionParticipants = [];
|
||||
}
|
||||
|
||||
try {
|
||||
roomHost = JSON.parse(el.dataset.roomHost || 'null');
|
||||
} catch {
|
||||
roomHost = null;
|
||||
}
|
||||
|
||||
const audioOnly = el.dataset.audioOnly === '1';
|
||||
let stageLayout = el.dataset.stageLayout || 'plenary';
|
||||
const panelDiscussions = el.dataset.panelDiscussions === '1';
|
||||
|
||||
if (!panelDiscussions && stageLayout === 'panel') {
|
||||
stageLayout = 'plenary';
|
||||
}
|
||||
|
||||
const participantUuid = el.dataset.participant || '';
|
||||
const selfParticipant = sessionParticipants.find((person) => person.uuid === participantUuid);
|
||||
|
||||
return {
|
||||
isRecording: el?.dataset.recordingActive === '1',
|
||||
isLocked: el?.dataset.locked === '1',
|
||||
isHost: el.dataset.isHost === '1',
|
||||
canPublish: el.dataset.canPublish !== '0',
|
||||
audioOnlyPublish: el.dataset.audioOnlyPublish === '1',
|
||||
usesSpeakAccess: el.dataset.usesSpeakAccess === '1',
|
||||
canManageSpeak: el.dataset.canManageSpeak === '1',
|
||||
speakGranted: el.dataset.speakGranted === '1',
|
||||
speakRequested: el.dataset.speakRequested === '1',
|
||||
inBreakout: el.dataset.inBreakout === '1',
|
||||
isWebinar: el.dataset.isWebinar === '1',
|
||||
isConference: el.dataset.isConference === '1',
|
||||
sessionNounLabel: el.dataset.sessionNoun || '',
|
||||
isGreenRoom: el.dataset.isGreenRoom === '1',
|
||||
audioOnly,
|
||||
isSpace: el.dataset.isSpace === '1',
|
||||
muteOnJoin: el.dataset.muteOnJoin === '1',
|
||||
videoOffOnJoin: el.dataset.videoOffOnJoin === '1' || audioOnly,
|
||||
isRecording: el.dataset.recordingActive === '1',
|
||||
activeRecordingUuid: el.dataset.activeRecordingUuid || null,
|
||||
isLocked: el.dataset.locked === '1',
|
||||
watermark: el.dataset.watermark === '1',
|
||||
liveCaptions: el.dataset.liveCaptions === '1',
|
||||
displayName: el.dataset.displayName || '',
|
||||
presenterRefs,
|
||||
usesStageLayout: el.dataset.usesStageLayout === '1',
|
||||
panelDiscussions,
|
||||
stageLayout,
|
||||
activePanelId: el.dataset.activePanelId || '',
|
||||
spotlightSpeakerRef: el.dataset.spotlightSpeakerRef || '',
|
||||
panels,
|
||||
sessionParticipants,
|
||||
roomHost,
|
||||
waitingCount: sessionParticipants.filter((person) => person.status === 'waiting').length,
|
||||
assignedBreakoutRoomId: selfParticipant?.breakout_room_id ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function meetRoom() {
|
||||
const boot = readMeetBootState();
|
||||
const roomBoot = readMeetRoomBootState();
|
||||
// LiveKit Room must stay outside Alpine's reactive proxy — wrapping it breaks
|
||||
// structuredClone inside the SDK during connect and media setup.
|
||||
let room = null;
|
||||
@@ -33,38 +108,43 @@ function meetRoom() {
|
||||
isMuted: true,
|
||||
isVideoOff: true,
|
||||
isScreenSharing: false,
|
||||
isRecording: boot.isRecording,
|
||||
isLocked: boot.isLocked,
|
||||
isHost: false,
|
||||
watermark: false,
|
||||
liveCaptions: false,
|
||||
isRecording: roomBoot.isRecording ?? false,
|
||||
isLocked: roomBoot.isLocked ?? false,
|
||||
isHost: roomBoot.isHost ?? false,
|
||||
watermark: roomBoot.watermark ?? false,
|
||||
liveCaptions: roomBoot.liveCaptions ?? false,
|
||||
displayName: roomBoot.displayName ?? '',
|
||||
canPublish: roomBoot.canPublish ?? true,
|
||||
muteOnJoin: roomBoot.muteOnJoin ?? true,
|
||||
videoOffOnJoin: roomBoot.videoOffOnJoin ?? false,
|
||||
waitingCount: roomBoot.waitingCount ?? 0,
|
||||
isWebinar: roomBoot.isWebinar ?? false,
|
||||
isConference: roomBoot.isConference ?? false,
|
||||
sessionNounLabel: roomBoot.sessionNounLabel ?? '',
|
||||
isGreenRoom: roomBoot.isGreenRoom ?? false,
|
||||
presenterRefs: roomBoot.presenterRefs ?? [],
|
||||
audioOnly: roomBoot.audioOnly ?? false,
|
||||
audioOnlyPublish: roomBoot.audioOnlyPublish ?? false,
|
||||
usesSpeakAccess: roomBoot.usesSpeakAccess ?? false,
|
||||
canManageSpeak: roomBoot.canManageSpeak ?? false,
|
||||
speakGranted: roomBoot.speakGranted ?? false,
|
||||
speakRequested: roomBoot.speakRequested ?? false,
|
||||
isSpace: roomBoot.isSpace ?? false,
|
||||
usesStageLayout: roomBoot.usesStageLayout ?? false,
|
||||
panelDiscussions: roomBoot.panelDiscussions ?? false,
|
||||
stageLayout: roomBoot.stageLayout ?? 'plenary',
|
||||
panels: roomBoot.panels ?? [],
|
||||
activePanelId: roomBoot.activePanelId ?? '',
|
||||
spotlightSpeakerRef: roomBoot.spotlightSpeakerRef ?? '',
|
||||
inBreakout: roomBoot.inBreakout ?? false,
|
||||
assignedBreakoutRoomId: roomBoot.assignedBreakoutRoomId ?? null,
|
||||
activeRecordingUuid: roomBoot.activeRecordingUuid ?? null,
|
||||
sessionParticipants: roomBoot.sessionParticipants ?? [],
|
||||
roomHost: roomBoot.roomHost ?? null,
|
||||
captionText: '',
|
||||
displayName: '',
|
||||
canPublish: true,
|
||||
muteOnJoin: true,
|
||||
videoOffOnJoin: false,
|
||||
audioPlaybackBlocked: false,
|
||||
needsMediaUnlock: false,
|
||||
waitingCount: 0,
|
||||
isWebinar: false,
|
||||
isConference: false,
|
||||
sessionNounLabel: '',
|
||||
isGreenRoom: false,
|
||||
goingLive: false,
|
||||
presenterRefs: [],
|
||||
audioOnly: false,
|
||||
audioOnlyPublish: false,
|
||||
usesSpeakAccess: false,
|
||||
canManageSpeak: false,
|
||||
speakGranted: false,
|
||||
speakRequested: false,
|
||||
isSpace: false,
|
||||
usesStageLayout: false,
|
||||
panelDiscussions: false,
|
||||
stageLayout: 'plenary',
|
||||
panels: [],
|
||||
activePanelId: '',
|
||||
spotlightSpeakerRef: '',
|
||||
panelSetupOpen: false,
|
||||
savingStageLayout: false,
|
||||
_stageSnapshot: null,
|
||||
@@ -72,10 +152,8 @@ function meetRoom() {
|
||||
activePolls: [],
|
||||
breakoutBroadcast: '',
|
||||
breakoutBroadcastInput: '',
|
||||
inBreakout: false,
|
||||
breakoutsActive: false,
|
||||
activeBreakouts: [],
|
||||
assignedBreakoutRoomId: null,
|
||||
breakoutRoomName: '',
|
||||
breakoutClosesAt: '',
|
||||
sidebarPanel: null,
|
||||
@@ -87,15 +165,12 @@ function meetRoom() {
|
||||
breakoutModalOpen: false,
|
||||
breakoutCount: '4',
|
||||
endMeetingConfirmOpen: false,
|
||||
activeRecordingUuid: null,
|
||||
joinUrl: '',
|
||||
passcode: '',
|
||||
roomTitle: '',
|
||||
connectionStatus: 'Connecting…',
|
||||
participants: [],
|
||||
sessionParticipants: [],
|
||||
activeSpeakerIds: [],
|
||||
roomHost: null,
|
||||
floatingReactions: [],
|
||||
reactionsInitialized: false,
|
||||
pollTimer: null,
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
@include('partials.favicon')
|
||||
<style>
|
||||
[x-cloak] { display: none !important; }
|
||||
.meet-toolbar__track:not(.is-ready) [data-toolbar-conditional] { display: none !important; }
|
||||
.meet-toolbar__track:not(.is-ready) [data-toolbar-conditional],
|
||||
.meet-toolbar__track:not(.is-ready) [data-alpine-toolbar] { display: none !important; }
|
||||
</style>
|
||||
@vite(['resources/css/app.css', 'resources/js/meet-room.js'])
|
||||
</head>
|
||||
@@ -211,6 +212,7 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@if (($isConference ?? false) && ($isGreenRoom ?? false))
|
||||
<div x-show="isConference && isGreenRoom" x-cloak
|
||||
class="flex shrink-0 flex-col gap-2 border-b border-violet-500/30 bg-violet-500/15 px-4 py-2.5 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
|
||||
<div class="min-w-0 text-sm text-violet-100">
|
||||
@@ -233,7 +235,9 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (($inBreakout ?? false) && ! ($isSpace ?? false))
|
||||
<div x-show="inBreakout && !isSpace" x-cloak
|
||||
class="flex shrink-0 flex-col gap-1 border-b border-amber-500/30 bg-amber-500/10 px-4 py-2.5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="min-w-0">
|
||||
@@ -242,7 +246,9 @@
|
||||
</div>
|
||||
<p x-show="breakoutTimeRemaining()" class="shrink-0 text-xs font-medium text-amber-200/90" x-text="breakoutTimeRemaining()"></p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@unless ($isSpace ?? false)
|
||||
<div x-show="isHost && breakoutsActive && !inBreakout && !isSpace" x-cloak
|
||||
class="flex shrink-0 flex-col gap-2 border-b border-amber-500/25 bg-amber-500/10 px-4 py-2.5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="min-w-0 text-sm text-amber-100">
|
||||
@@ -254,7 +260,9 @@
|
||||
End breakouts
|
||||
</button>
|
||||
</div>
|
||||
@endunless
|
||||
|
||||
@if (($usesStageLayout ?? false) && ! ($isGreenRoom ?? false) && ! ($inBreakout ?? false))
|
||||
<div x-show="usesStageLayout && !isGreenRoom && !inBreakout" x-cloak
|
||||
class="flex shrink-0 flex-col gap-2 border-b px-4 py-2.5 sm:flex-row sm:items-center sm:justify-between sm:gap-3"
|
||||
:class="stageLayout === 'panel' ? 'border-violet-500/25 bg-violet-500/10' : 'border-sky-500/25 bg-sky-500/10'">
|
||||
@@ -279,6 +287,7 @@
|
||||
Change layout
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div x-show="isHost && waitingCount > 0" x-cloak
|
||||
class="flex shrink-0 items-center justify-between gap-3 border-b border-amber-500/30 bg-amber-500/15 px-4 py-2.5">
|
||||
@@ -372,6 +381,7 @@
|
||||
You can speak — your microphone is available
|
||||
</p>
|
||||
</div>
|
||||
@if (($isSpace ?? false) && ($speakRequested ?? false) && ! ($canPublish ?? true))
|
||||
<div x-show="isSpace && speakRequested && !canPublish" x-cloak
|
||||
class="absolute inset-x-0 -top-14 flex justify-center px-4 sm:-top-16">
|
||||
<div class="flex flex-wrap items-center justify-center gap-2 rounded-2xl bg-indigo-600/90 px-4 py-2.5 text-sm text-white shadow-lg ring-1 ring-indigo-400/40">
|
||||
@@ -382,6 +392,7 @@
|
||||
class="rounded-lg bg-indigo-800/80 px-3 py-1 text-xs font-medium text-indigo-100 hover:bg-indigo-800">Decline</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<div class="meet-toolbar mx-auto max-w-5xl">
|
||||
<div class="meet-toolbar__track sm:px-0">
|
||||
<div class="meet-toolbar__primary sm:contents">
|
||||
@@ -438,8 +449,9 @@
|
||||
title="React">
|
||||
@include('meet.room.partials.meet-icon', ['icon' => 'react'])
|
||||
</button>
|
||||
@if ($isSpace ?? false)
|
||||
<button @click="toggleParticipants()" type="button"
|
||||
x-show="isSpace"
|
||||
data-alpine-toolbar
|
||||
class="rounded-full p-3 transition-colors sm:hidden"
|
||||
:class="sidebarPanel === 'participants' ? 'bg-indigo-600 hover:bg-indigo-500' : 'bg-slate-700 hover:bg-slate-600'"
|
||||
title="People">
|
||||
@@ -447,20 +459,22 @@
|
||||
</button>
|
||||
@if ($participant->isHost())
|
||||
<button @click="toggleRecording()" type="button"
|
||||
x-show="isSpace"
|
||||
data-alpine-toolbar
|
||||
class="rounded-full p-3 transition-colors sm:hidden"
|
||||
:class="isRecording ? 'bg-red-600 hover:bg-red-500' : 'bg-slate-700 hover:bg-slate-600'"
|
||||
:title="isRecording ? 'Stop recording' : 'Start recording'">
|
||||
<span class="block h-3 w-3 rounded-full" :class="isRecording ? 'bg-white animate-pulse' : 'bg-red-400'"></span>
|
||||
</button>
|
||||
@endif
|
||||
@else
|
||||
<button @click="toggleFeatures()" type="button"
|
||||
x-show="!isSpace"
|
||||
data-alpine-toolbar
|
||||
class="rounded-full p-3 transition-colors sm:hidden"
|
||||
:class="sidebarPanel === 'features' ? 'bg-indigo-600 hover:bg-indigo-500' : 'bg-slate-700 hover:bg-slate-600'"
|
||||
title="More options">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"/></svg>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<button @click="raiseHand()" type="button" x-show="showAudienceControls()" data-toolbar-conditional @unless($toolbarShowAudienceDesktop) hidden @endunless class="hidden rounded-full bg-slate-700 p-3 hover:bg-slate-600 sm:inline-flex" title="Raise hand">
|
||||
@@ -472,26 +486,27 @@
|
||||
<button @click="sendReaction('heart')" type="button" x-show="showAudienceControls()" data-toolbar-conditional @unless($toolbarShowAudienceDesktop) hidden @endunless class="hidden rounded-full bg-slate-700 p-3 hover:bg-slate-600 sm:inline-flex" title="React">
|
||||
@include('meet.room.partials.meet-icon', ['icon' => 'react'])
|
||||
</button>
|
||||
@unless ($isSpace ?? false)
|
||||
<button @click="toggleChat()" type="button"
|
||||
x-show="!isSpace"
|
||||
data-alpine-toolbar
|
||||
class="hidden rounded-full p-3 transition-colors sm:inline-flex"
|
||||
:class="sidebarPanel === 'chat' ? 'bg-indigo-600 hover:bg-indigo-500' : 'bg-slate-700 hover:bg-slate-600'"
|
||||
title="Chat">
|
||||
@include('meet.room.partials.meet-icon', ['icon' => 'chat'])
|
||||
</button>
|
||||
@unless ($isSpace ?? false)
|
||||
<input type="file" id="meet-file-input" class="hidden" @change="uploadFile">
|
||||
@endunless
|
||||
<button @click="toggleFeatures()" type="button"
|
||||
x-show="!isSpace"
|
||||
data-alpine-toolbar
|
||||
class="hidden items-center gap-1.5 rounded-full px-3.5 py-3 text-xs font-medium transition-colors sm:inline-flex"
|
||||
:class="sidebarPanel === 'features' ? 'bg-indigo-600 hover:bg-indigo-500' : 'bg-slate-700 hover:bg-slate-600'"
|
||||
title="More options">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"/></svg>
|
||||
More
|
||||
</button>
|
||||
@endunless
|
||||
@if (($isSpace ?? false) && $participant->isHost())
|
||||
<button @click="toggleRecording()" type="button"
|
||||
data-alpine-toolbar
|
||||
class="hidden rounded-full p-3 transition-colors sm:inline-flex"
|
||||
:class="isRecording ? 'bg-red-600 hover:bg-red-500' : 'bg-slate-700 hover:bg-slate-600'"
|
||||
:title="isRecording ? 'Stop recording' : 'Start recording'">
|
||||
|
||||
@@ -1301,6 +1301,8 @@ class MeetWebTest extends TestCase
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('data-is-space="1"', false);
|
||||
$response->assertDontSee('title="Chat"', false);
|
||||
$response->assertDontSee('>More</', false);
|
||||
$response->assertDontSee('Start breakouts', false);
|
||||
$response->assertDontSee('Share files', false);
|
||||
$response->assertDontSee('View the room lineup from Events', false);
|
||||
|
||||
Reference in New Issue
Block a user