From 775c81f4eec9b9be8c54b5a0ff9dc3342b7391b6 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Fri, 3 Jul 2026 22:22:07 +0000 Subject: [PATCH] Fix plenary spotlight for guest speakers without user accounts. Spotlight by participant uuid, add on-stage picker in stage layout, and show who is currently spotlighted in the plenary status bar. Co-authored-by: Cursor --- resources/js/meet-room.js | 100 ++++++++++++++++------- resources/views/meet/room/show.blade.php | 29 +++++-- tests/Feature/MeetWebTest.php | 60 ++++++++++++++ 3 files changed, 156 insertions(+), 33 deletions(-) diff --git a/resources/js/meet-room.js b/resources/js/meet-room.js index 1ec419a..7981630 100644 --- a/resources/js/meet-room.js +++ b/resources/js/meet-room.js @@ -675,6 +675,31 @@ function meetRoom() { return Boolean(person.user_ref && this.presenterRefs.includes(person.user_ref)); }, + /** Stable key for spotlight and panel assignment (user_ref when logged in, else participant uuid). */ + spotlightKey(person) { + if (!person) { + return ''; + } + + return person.user_ref || person.uuid || ''; + }, + + matchesSpotlightKey(key, person) { + if (!key || !person) { + return false; + } + + return key === person.user_ref || key === person.uuid; + }, + + findSpeakerBySpotlightKey(key) { + if (!key) { + return null; + } + + return this.speakerParticipants().find((person) => this.matchesSpotlightKey(key, person)) || null; + }, + speakerParticipants() { return this.inCallParticipants().filter((person) => this.isSpeakerPerson(person)); }, @@ -712,9 +737,9 @@ function meetRoom() { }, resolveSpotlightParticipantUuid() { - const spotlightRef = this.resolveSpotlightSpeakerRef(); - if (spotlightRef) { - const match = this.speakerParticipants().find((person) => person.user_ref === spotlightRef); + const spotlightKey = this.resolveSpotlightKey(); + if (spotlightKey) { + const match = this.findSpeakerBySpotlightKey(spotlightKey); if (match) { return match.uuid; } @@ -732,7 +757,7 @@ function meetRoom() { }, panelStripParticipants() { - const onPanelRefs = new Set(this.getActivePanelSpeakerRefs()); + const onPanelKeys = new Set(this.getActivePanelSpeakerKeys()); return this.inCallParticipants() .filter((person) => { @@ -740,7 +765,9 @@ function meetRoom() { return true; } - return !person.user_ref || !onPanelRefs.has(person.user_ref); + const key = this.spotlightKey(person); + + return !key || !onPanelKeys.has(key); }) .sort((a, b) => this.compareStripParticipants(a, b)); }, @@ -800,26 +827,31 @@ function meetRoom() { strip.appendChild(item); }, - resolveSpotlightSpeakerRef() { + resolveSpotlightKey() { const speakers = this.speakerParticipants(); - if (this.spotlightSpeakerRef && speakers.some((person) => person.user_ref === this.spotlightSpeakerRef)) { - return this.spotlightSpeakerRef; + if (this.spotlightSpeakerRef) { + const match = this.findSpeakerBySpotlightKey(this.spotlightSpeakerRef); + if (match) { + return this.spotlightKey(match); + } } - return speakers[0]?.user_ref || null; + const fallback = this.speakerParticipantsSorted()[0]; + + return fallback ? this.spotlightKey(fallback) : ''; }, - getActivePanelSpeakerRefs() { + getActivePanelSpeakerKeys() { if (!this.panelDiscussions || this.stageLayout !== 'panel') { return this.speakerParticipants() - .map((person) => person.user_ref) + .map((person) => this.spotlightKey(person)) .filter(Boolean); } if (!this.activePanelId) { return this.speakerParticipants() - .map((person) => person.user_ref) + .map((person) => this.spotlightKey(person)) .filter(Boolean); } @@ -873,12 +905,12 @@ function meetRoom() { return null; } - const spotlightRef = this.resolveSpotlightSpeakerRef(); - if (person?.user_ref && spotlightRef && person.user_ref === spotlightRef) { + const spotlightKey = this.resolveSpotlightKey(); + if (spotlightKey && this.matchesSpotlightKey(spotlightKey, person)) { return spotlight; } - if (!spotlightRef && this.speakerParticipantsSorted()[0]?.uuid === identity) { + if (!spotlightKey && this.speakerParticipantsSorted()[0]?.uuid === identity) { return spotlight; } @@ -890,8 +922,9 @@ function meetRoom() { return null; } - const refs = this.getActivePanelSpeakerRefs(); - if (!person?.user_ref || !refs.includes(person.user_ref)) { + const keys = this.getActivePanelSpeakerKeys(); + const personKey = this.spotlightKey(person); + if (!personKey || !keys.includes(personKey)) { return null; } @@ -968,7 +1001,7 @@ function meetRoom() { } if (grid) { - grid.dataset.panelCount = String(this.getActivePanelSpeakerRefs().length || this.speakerCount()); + grid.dataset.panelCount = String(this.getActivePanelSpeakerKeys().length || this.speakerCount()); } this.renderAudienceStrip(); @@ -1008,12 +1041,12 @@ function meetRoom() { } }, - async setSpotlight(userRef) { - if (!userRef) { + async setSpotlight(speakerKey) { + if (!speakerKey) { return; } - this.spotlightSpeakerRef = userRef; + this.spotlightSpeakerRef = speakerKey; this.stageLayout = 'plenary'; this.applyStageLayout(); @@ -1026,7 +1059,7 @@ function meetRoom() { headers: this.headers(), body: JSON.stringify({ stage_layout: 'plenary', - spotlight_speaker_ref: userRef, + spotlight_speaker_ref: speakerKey, }), }); }, @@ -1052,16 +1085,16 @@ function meetRoom() { this.panelDiscussions = true; }, - togglePanelSpeaker(panel, userRef) { - if (!panel || !userRef) { + togglePanelSpeaker(panel, speakerKey) { + if (!panel || !speakerKey) { return; } const refs = panel.speaker_refs || []; - if (refs.includes(userRef)) { - panel.speaker_refs = refs.filter((ref) => ref !== userRef); + if (refs.includes(speakerKey)) { + panel.speaker_refs = refs.filter((ref) => ref !== speakerKey); } else { - panel.speaker_refs = [...refs, userRef]; + panel.speaker_refs = [...refs, speakerKey]; } }, @@ -2384,7 +2417,16 @@ function meetRoom() { return 'Multiple speakers shown together in a split-screen grid.'; } - return ''; + const spotlight = this.findSpeakerBySpotlightKey(this.resolveSpotlightKey()); + if (spotlight) { + return `${spotlight.display_name} is on stage. Other speakers appear in the strip below.`; + } + + return 'Choose who fills the stage from Stage layout or Spotlight on a speaker.'; + }, + + spotlightSpeakerName() { + return this.findSpeakerBySpotlightKey(this.resolveSpotlightKey())?.display_name || ''; }, activePanelName() { @@ -2403,6 +2445,7 @@ function meetRoom() { panels: JSON.parse(JSON.stringify(this.panels)), activePanelId: this.activePanelId, panelDiscussions: this.panelDiscussions, + spotlightSpeakerRef: this.spotlightSpeakerRef, }; this.panelSetupOpen = true; }, @@ -2413,6 +2456,7 @@ function meetRoom() { this.panels = JSON.parse(JSON.stringify(this._stageSnapshot.panels)); this.activePanelId = this._stageSnapshot.activePanelId; this.panelDiscussions = this._stageSnapshot.panelDiscussions; + this.spotlightSpeakerRef = this._stageSnapshot.spotlightSpeakerRef || ''; this.applyStageLayout(); } diff --git a/resources/views/meet/room/show.blade.php b/resources/views/meet/room/show.blade.php index 671f8ba..3ebef27 100644 --- a/resources/views/meet/room/show.blade.php +++ b/resources/views/meet/room/show.blade.php @@ -640,8 +640,8 @@ - @@ -722,6 +722,25 @@ +

+ Plenary shows one speaker on the main stage. Everyone else appears in the strip below. +

+ +
+

On stage

+
+ +
+
+

Switching to panel discussion changes the live stage to a multi-speaker grid for everyone in the room.

@@ -744,10 +763,10 @@
diff --git a/tests/Feature/MeetWebTest.php b/tests/Feature/MeetWebTest.php index ef5f64a..67df16b 100644 --- a/tests/Feature/MeetWebTest.php +++ b/tests/Feature/MeetWebTest.php @@ -1048,6 +1048,66 @@ class MeetWebTest extends TestCase $this->assertCount(1, $room->setting('panels')); } + public function test_host_can_spotlight_guest_panelist_by_participant_uuid(): void + { + $room = Room::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'host_user_ref' => $this->user->public_id, + 'title' => 'Summit keynote', + 'type' => 'webinar', + 'status' => 'live', + 'scheduled_at' => now()->addHour(), + 'timezone' => 'UTC', + 'settings' => array_merge(config('meet.default_settings'), [ + 'stage_layout' => 'plenary', + ]), + ]); + + $session = Session::create([ + 'owner_ref' => $this->user->public_id, + 'room_id' => $room->id, + 'media_room_name' => 'meet-'.$room->uuid, + 'status' => 'live', + 'started_at' => now(), + ]); + + $hostParticipant = \App\Models\Participant::create([ + 'owner_ref' => $this->user->public_id, + 'session_id' => $session->id, + 'user_ref' => $this->user->public_id, + 'display_name' => $this->user->name, + 'role' => 'host', + 'status' => 'joined', + 'joined_at' => now(), + ]); + + $guestSpeaker = \App\Models\Participant::create([ + 'owner_ref' => $this->user->public_id, + 'session_id' => $session->id, + 'display_name' => 'Guest Speaker', + 'email' => 'speaker@example.com', + 'role' => 'panelist', + 'status' => 'joined', + 'joined_at' => now(), + ]); + + $this->actingAs($this->user) + ->withSession(["meet.participant.{$session->uuid}" => $hostParticipant->uuid]) + ->postJson('/room/'.$session->uuid.'/stage', [ + 'stage_layout' => 'plenary', + 'spotlight_speaker_ref' => $guestSpeaker->uuid, + ]) + ->assertOk() + ->assertJson([ + 'stage_layout' => 'plenary', + 'spotlight_speaker_ref' => $guestSpeaker->uuid, + ]); + + $room->refresh(); + $this->assertSame($guestSpeaker->uuid, $room->setting('spotlight_speaker_ref')); + } + public function test_poll_includes_stage_config_for_conference(): void { $room = Room::create([