Promote Events speakers from attendee to panelist on rejoin.
Deploy Ladill Meet / deploy (push) Successful in 1m7s

Upgrade existing participant roles when a verified speaker token is present, and sync panelist access while already in the room so speaker-panel placement and publish rights apply.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-03 18:05:15 +00:00
co-authored by Cursor
parent f9c82d0d1f
commit 3d68f7a83b
3 changed files with 91 additions and 7 deletions
@@ -63,6 +63,8 @@ class MeetingRoomController extends Controller
abort_unless($participant->status === 'joined', 403, "You are not in this {$kind}."); abort_unless($participant->status === 'joined', 403, "You are not in this {$kind}.");
$participant = $this->sessions->syncEventsSpeakerRole($request, $participant);
$token = $this->media->isConfigured() $token = $this->media->isConfigured()
? $this->sessions->generateMediaToken($participant) ? $this->sessions->generateMediaToken($participant)
: ''; : '';
@@ -520,8 +522,11 @@ class MeetingRoomController extends Controller
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}"); $participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
abort_unless($participantUuid, 403); abort_unless($participantUuid, 403);
return Participant::where('uuid', $participantUuid) return $this->sessions->syncEventsSpeakerRole(
->where('session_id', $session->id) $request,
->firstOrFail(); Participant::where('uuid', $participantUuid)
->where('session_id', $session->id)
->firstOrFail()
);
} }
} }
+41 -4
View File
@@ -7,6 +7,7 @@ use App\Models\Room;
use App\Models\Session; use App\Models\Session;
use App\Models\User; use App\Models\User;
use App\Services\Meet\Media\MediaProviderInterface; use App\Services\Meet\Media\MediaProviderInterface;
use App\Support\EventsRegistrationSession;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -108,15 +109,13 @@ class SessionService
$updates['joined_at'] = $existing->joined_at ?? now(); $updates['joined_at'] = $existing->joined_at ?? now();
} }
if (in_array($role, ['host', 'co_host'], true)) { if ($this->shouldPromoteParticipantRole($existing->role, $role)) {
$updates['role'] = $role;
} elseif ($existing->role === 'guest' && $role !== 'guest') {
$updates['role'] = $role; $updates['role'] = $role;
} }
$existing->update($updates); $existing->update($updates);
return $existing; return $existing->fresh();
} }
$joinedAt = $status === 'joined' ? now() : null; $joinedAt = $status === 'joined' ? now() : null;
@@ -196,6 +195,43 @@ class SessionService
return $participant->fresh(); return $participant->fresh();
} }
public function syncEventsSpeakerRole(Request $request, Participant $participant): Participant
{
$participant->loadMissing('session.room');
$room = $participant->session->room;
if (! $room->isEventsLinked() || ! EventsRegistrationSession::isSpeakerVerified($request, $room)) {
return $participant;
}
return $this->promoteParticipantRole($participant, 'panelist');
}
private function promoteParticipantRole(Participant $participant, string $role): Participant
{
if ($this->shouldPromoteParticipantRole($participant->role, $role)) {
$participant->update(['role' => $role]);
return $participant->fresh();
}
return $participant;
}
private function shouldPromoteParticipantRole(string $currentRole, string $newRole): bool
{
static $rank = [
'guest' => 10,
'attendee' => 20,
'participant' => 25,
'panelist' => 40,
'co_host' => 50,
'host' => 60,
];
return ($rank[$newRole] ?? 0) > ($rank[$currentRole] ?? 0);
}
protected function findExistingParticipant(Session $session, ?string $userRef, ?string $email): ?Participant protected function findExistingParticipant(Session $session, ?string $userRef, ?string $email): ?Participant
{ {
$active = fn ($q) => $q->whereIn('status', ['invited', 'waiting', 'joined']); $active = fn ($q) => $q->whereIn('status', ['invited', 'waiting', 'joined']);
@@ -244,6 +280,7 @@ class SessionService
if ($user) { if ($user) {
$existing = $this->joinedParticipantForUser($session, $user); $existing = $this->joinedParticipantForUser($session, $user);
if ($existing) { if ($existing) {
$existing = $this->promoteParticipantRole($existing, $role);
$this->rememberParticipantSession($request, $existing, $session); $this->rememberParticipantSession($request, $existing, $session);
return $existing; return $existing;
+42
View File
@@ -225,6 +225,48 @@ class EventsJoinGateTest extends TestCase
]); ]);
} }
public function test_speaker_rejoin_promotes_existing_attendee_to_panelist(): void
{
$room = $this->createEventsLinkedRoom('town_hall', [
'settings' => array_merge(config('meet.default_settings'), [
'join_before_host' => true,
'waiting_room' => false,
]),
]);
$session = Session::create([
'owner_ref' => $this->user->public_id,
'room_id' => $room->id,
'media_room_name' => 'room-'.$room->uuid,
'status' => 'live',
'started_at' => now(),
]);
$room->update(['status' => 'live']);
\App\Models\Participant::create([
'owner_ref' => $this->user->public_id,
'session_id' => $session->id,
'display_name' => 'Keynote Speaker',
'email' => 'speaker@example.com',
'role' => 'attendee',
'status' => 'joined',
'joined_at' => now(),
]);
$this->get('/r/'.$room->uuid.'?speaker=speaker-token-123')
->assertRedirect(route('meet.join', $room));
$this->post('/r/'.$room->uuid.'/enter', [
'display_name' => 'Keynote Speaker',
])->assertRedirect(route('meet.room', $session));
$this->assertDatabaseHas('meet_participants', [
'session_id' => $session->id,
'email' => 'speaker@example.com',
'role' => 'panelist',
]);
}
/** @param array<string, mixed> $overrides */ /** @param array<string, mixed> $overrides */
protected function createEventsLinkedRoom(string $type, array $overrides = []): Room protected function createEventsLinkedRoom(string $type, array $overrides = []): Room
{ {