From bbf984bc6015300bd819ef592c0b4062fd0bc45b Mon Sep 17 00:00:00 2001 From: isaacclad Date: Thu, 2 Jul 2026 06:39:32 +0000 Subject: [PATCH] Fix live room header flash and instant sidebar panel switching. Server-render the speaker pill on load, hide noisy connection status on mobile, and use a single sidebarPanel slot so desktop panels replace in place instead of animating side by side. Co-authored-by: Cursor --- app/Services/Meet/ParticipantPresenter.php | 79 +++++++++++++++ resources/css/app.css | 23 +++++ resources/js/meet-room.js | 78 ++++++++------- .../components/meet/room/panel.blade.php | 27 +----- resources/views/meet/room/show.blade.php | 96 +++++++++++++------ 5 files changed, 210 insertions(+), 93 deletions(-) diff --git a/app/Services/Meet/ParticipantPresenter.php b/app/Services/Meet/ParticipantPresenter.php index fa3fe54..3a4b6aa 100644 --- a/app/Services/Meet/ParticipantPresenter.php +++ b/app/Services/Meet/ParticipantPresenter.php @@ -63,4 +63,83 @@ class ParticipantPresenter return null; } + + /** + * @param list> $participants + * @param list $presenterRefs + */ + public static function isSpeakerPerson(array $person, array $presenterRefs = []): bool + { + if (in_array($person['role'] ?? '', ['host', 'co_host', 'panelist'], true)) { + return true; + } + + $userRef = $person['user_ref'] ?? null; + + return is_string($userRef) && $userRef !== '' && in_array($userRef, $presenterRefs, true); + } + + /** + * @param list> $participants + * @param list $presenterRefs + */ + public static function headerCountLabel(array $participants, bool $usesStageLayout, array $presenterRefs = []): string + { + $joined = array_values(array_filter($participants, fn ($p) => ($p['status'] ?? '') === 'joined')); + + if ($usesStageLayout) { + $count = count(array_filter($joined, fn ($p) => self::isSpeakerPerson($p, $presenterRefs))); + + return $count === 1 ? '1 speaker' : "{$count} speakers"; + } + + $count = count($joined); + + return "{$count} in call"; + } + + /** + * @param list> $participants + * @return array{display_name: string, avatar_url: ?string}|null + */ + public static function headerHost(array $participants, array $roomHost): ?array + { + foreach ($participants as $person) { + if (($person['status'] ?? '') !== 'joined') { + continue; + } + + if (in_array($person['role'] ?? '', ['host', 'co_host'], true)) { + return [ + 'display_name' => (string) ($person['display_name'] ?? 'Host'), + 'avatar_url' => $person['avatar_url'] ?? null, + ]; + } + } + + if (($roomHost['display_name'] ?? '') !== '') { + return [ + 'display_name' => (string) $roomHost['display_name'], + 'avatar_url' => $roomHost['avatar_url'] ?? null, + ]; + } + + return null; + } + + public static function initials(?string $name): string + { + $parts = preg_split('/\s+/', trim($name ?? '')) ?: []; + $parts = array_values(array_filter($parts, fn ($part) => $part !== '')); + + if ($parts === []) { + return '?'; + } + + if (count($parts) === 1) { + return strtoupper(substr($parts[0], 0, 2)); + } + + return strtoupper(substr($parts[0], 0, 1).substr($parts[count($parts) - 1], 0, 1)); + } } diff --git a/resources/css/app.css b/resources/css/app.css index f31beb0..d3af9f1 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -361,6 +361,29 @@ html.meet-room-page { } @media (min-width: 640px) { + .meet-room-page .meet-room-panels-stack { + position: relative; + height: 100%; + flex-shrink: 0; + transition: width 0s; + } + + .meet-room-page .meet-room-panels-stack.is-open { + width: 20rem; + } + + .meet-room-page .meet-room-panels-stack:not(.is-open) { + width: 0; + overflow: hidden; + } + + .meet-room-page .meet-room-panels-stack .meet-room-panel__pane { + position: absolute; + inset: 0; + width: 20rem; + height: 100%; + } + .meet-room-page .meet-room-panel { position: relative; inset: auto; diff --git a/resources/js/meet-room.js b/resources/js/meet-room.js index aeb084f..570ccbe 100644 --- a/resources/js/meet-room.js +++ b/resources/js/meet-room.js @@ -73,12 +73,9 @@ function meetRoom() { assignedBreakoutRoomId: null, breakoutRoomName: '', breakoutClosesAt: '', - featuresOpen: false, - programmeOpen: false, + sidebarPanel: null, whiteboardOpen: false, qaInput: '', - chatOpen: false, - participantsOpen: false, chatInput: '', shareOpen: false, shareCopied: false, @@ -1074,6 +1071,16 @@ function meetRoom() { return this.inCallParticipants().length; }, + headerParticipantLabel() { + if (this.usesStageLayout) { + const count = this.speakerCount(); + + return count === 1 ? '1 speaker' : `${count} speakers`; + } + + return `${this.participantCount()} in call`; + }, + inCallParticipants() { return this.sessionParticipants.filter((p) => p.status === 'joined'); }, @@ -1518,7 +1525,7 @@ function meetRoom() { if (!this.isHost || !this.config.breakoutsCreateUrl) return; this.breakoutCount = '4'; this.breakoutModalOpen = true; - this.featuresOpen = false; + this.closeSidebar(); }, async submitBreakouts() { @@ -1545,7 +1552,7 @@ function meetRoom() { if (!this.isHost || !this.config.breakoutsCloseUrl) return; await fetch(this.config.breakoutsCloseUrl, { method: 'POST', headers: this.headers() }); - this.featuresOpen = false; + this.closeSidebar(); this.breakoutsActive = false; this.activeBreakouts = []; }, @@ -1665,28 +1672,22 @@ function meetRoom() { }, toggleProgramme() { - this.programmeOpen = !this.programmeOpen; - if (this.programmeOpen) { - this.chatOpen = false; - this.participantsOpen = false; - this.featuresOpen = false; - this.shareOpen = false; - } + this.toggleSidebarPanel('programme'); }, openUploadPicker() { document.getElementById('meet-file-input')?.click(); - this.featuresOpen = false; + this.closeSidebar(); }, async toggleRecordingFromMenu() { await this.toggleRecording(); - this.featuresOpen = false; + this.closeSidebar(); }, async toggleLockFromMenu() { await this.toggleLock(); - this.featuresOpen = false; + this.closeSidebar(); }, openEndMeetingConfirm() { @@ -1941,34 +1942,33 @@ function meetRoom() { this.applyStageLayout(); }, - toggleChat() { - this.chatOpen = !this.chatOpen; - if (this.chatOpen) { - this.participantsOpen = false; - this.featuresOpen = false; - this.programmeOpen = false; + closeSidebar() { + this.sidebarPanel = null; + }, + + toggleSidebarPanel(name) { + this.sidebarPanel = this.sidebarPanel === name ? null : name; + + if (this.sidebarPanel) { this.shareOpen = false; } }, + toggleChat() { + this.toggleSidebarPanel('chat'); + }, + toggleParticipants() { - this.participantsOpen = !this.participantsOpen; - if (this.participantsOpen) { - this.chatOpen = false; - this.featuresOpen = false; - this.programmeOpen = false; - this.shareOpen = false; - } + this.toggleSidebarPanel('participants'); }, toggleFeatures() { - this.featuresOpen = !this.featuresOpen; - if (this.featuresOpen) { - this.chatOpen = false; - this.participantsOpen = false; - this.programmeOpen = false; - this.shareOpen = false; - } + this.toggleSidebarPanel('features'); + }, + + openParticipantsPanel() { + this.sidebarPanel = 'participants'; + this.shareOpen = false; }, async openShare() { @@ -1992,9 +1992,7 @@ function meetRoom() { this.shareOpen = true; this.shareCopied = false; - this.participantsOpen = false; - this.chatOpen = false; - this.featuresOpen = false; + this.closeSidebar(); }, async copyJoinLink() { @@ -2067,7 +2065,7 @@ function meetRoom() { const previous = this.waitingCount; this.waitingCount = data.waiting_count; if (this.isHost && data.waiting_count > previous) { - this.participantsOpen = true; + this.openParticipantsPanel(); } } this.updateChatFromPoll(data.messages || []); diff --git a/resources/views/components/meet/room/panel.blade.php b/resources/views/components/meet/room/panel.blade.php index 6c728b5..02ed91e 100644 --- a/resources/views/components/meet/room/panel.blade.php +++ b/resources/views/components/meet/room/panel.blade.php @@ -1,30 +1,13 @@ @props([ 'show', 'title' => null, + 'close' => 'closeSidebar()', ])