Fix plenary spotlight for guest speakers without user accounts.
Deploy Ladill Meet / deploy (push) Successful in 52s
Deploy Ladill Meet / deploy (push) Successful in 52s
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 <cursoragent@cursor.com>
This commit is contained in:
+72
-28
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -640,8 +640,8 @@
|
||||
<span x-show="!isSpeaking(person.uuid)" class="block text-xs text-slate-400" x-text="roleLabel(person.role)"></span>
|
||||
</span>
|
||||
<span class="flex shrink-0 items-center gap-1">
|
||||
<button x-show="usesStageLayout && isHost && person.user_ref && spotlightSpeakerRef !== person.user_ref"
|
||||
type="button" @click="setSpotlight(person.user_ref)"
|
||||
<button x-show="usesStageLayout && isHost && isSpeakerPerson(person) && !matchesSpotlightKey(resolveSpotlightKey(), person)"
|
||||
type="button" @click="setSpotlight(spotlightKey(person))"
|
||||
class="rounded-lg bg-indigo-600 px-2 py-1 text-[10px] font-medium text-white hover:bg-indigo-500">
|
||||
Spotlight
|
||||
</button>
|
||||
@@ -722,6 +722,25 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p x-show="stageLayout === 'plenary'" x-cloak class="mt-3 rounded-lg border border-sky-500/30 bg-sky-500/10 px-3 py-2 text-xs text-sky-100">
|
||||
Plenary shows one speaker on the main stage. Everyone else appears in the strip below.
|
||||
</p>
|
||||
|
||||
<div x-show="stageLayout === 'plenary'" x-cloak class="mt-5 space-y-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-slate-400">On stage</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template x-for="speaker in speakerParticipantsSorted()" :key="'plenary-' + speaker.uuid">
|
||||
<button type="button" @click="setSpotlight(spotlightKey(speaker))"
|
||||
class="rounded-full px-3 py-1.5 text-xs font-medium transition-colors"
|
||||
:class="matchesSpotlightKey(resolveSpotlightKey(), speaker)
|
||||
? 'bg-sky-600 text-white ring-1 ring-sky-400/60'
|
||||
: 'bg-slate-800 text-slate-300 hover:bg-slate-700'">
|
||||
<span x-text="speaker.display_name"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p x-show="stageLayout === 'panel'" x-cloak class="mt-3 rounded-lg border border-violet-500/30 bg-violet-500/10 px-3 py-2 text-xs text-violet-100">
|
||||
Switching to panel discussion changes the live stage to a multi-speaker grid for everyone in the room.
|
||||
</p>
|
||||
@@ -744,10 +763,10 @@
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<template x-for="speaker in speakerParticipantsSorted()" :key="panel.id + '-' + speaker.uuid">
|
||||
<label class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs"
|
||||
:class="panel.speaker_refs.includes(speaker.user_ref) ? 'bg-violet-600/30 text-violet-100 ring-1 ring-violet-500/50' : 'bg-slate-800 text-slate-300'">
|
||||
:class="panel.speaker_refs.includes(spotlightKey(speaker)) ? 'bg-violet-600/30 text-violet-100 ring-1 ring-violet-500/50' : 'bg-slate-800 text-slate-300'">
|
||||
<input type="checkbox" class="sr-only"
|
||||
:checked="panel.speaker_refs.includes(speaker.user_ref)"
|
||||
@change="togglePanelSpeaker(panel, speaker.user_ref)">
|
||||
:checked="panel.speaker_refs.includes(spotlightKey(speaker))"
|
||||
@change="togglePanelSpeaker(panel, spotlightKey(speaker))">
|
||||
<span x-text="speaker.display_name"></span>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user