Stop Chat and More from flashing on load in audio rooms.
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:
isaacclad
2026-07-04 01:00:28 +00:00
co-authored by Cursor
parent 1d1a4cd877
commit 4dd7883763
3 changed files with 137 additions and 45 deletions
+112 -37
View File
@@ -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,
+23 -8
View File
@@ -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'">