diff --git a/app/Http/Controllers/Meet/MeetingFeaturesController.php b/app/Http/Controllers/Meet/MeetingFeaturesController.php index b0291a0..571b688 100644 --- a/app/Http/Controllers/Meet/MeetingFeaturesController.php +++ b/app/Http/Controllers/Meet/MeetingFeaturesController.php @@ -15,6 +15,7 @@ use App\Services\Meet\FileSharingService; use App\Services\Meet\LiveStreamService; use App\Services\Meet\PollService; use App\Services\Meet\QaService; +use App\Services\Meet\SessionService; use App\Services\Meet\StageLayoutService; use App\Services\Meet\TownHallService; use App\Services\Meet\WhiteboardService; @@ -34,6 +35,7 @@ class MeetingFeaturesController extends Controller protected LiveStreamService $liveStreams, protected TownHallService $townHall, protected StageLayoutService $stageLayout, + protected SessionService $sessions, ) {} // --- Q&A --- @@ -282,13 +284,24 @@ class MeetingFeaturesController extends Controller public function goLiveTownHall(Request $request, Session $session): JsonResponse { - $this->hostOnly($request, $session); - $live = $this->townHall->goLive($session, $request->user()); - $participant = app(SessionService::class)->joinedParticipantForUser($live, $request->user()); + $hostParticipant = $this->hostOnly($request, $session); - if ($participant) { - app(SessionService::class)->rememberParticipantSession($request, $participant, $live); - } + $user = $request->user() + ?? ($hostParticipant->user_ref ? \App\Models\User::where('public_id', $hostParticipant->user_ref)->first() : null); + abort_unless($user, 403); + + $live = $this->townHall->goLive($session, $user); + + $this->sessions->ensureJoinedParticipant( + $request, + $live, + $user, + 'host', + $hostParticipant->display_name, + $hostParticipant->email, + ); + + $request->session()->forget("meet.participant.{$session->uuid}"); return response()->json([ 'session_uuid' => $live->uuid, diff --git a/app/Http/Controllers/Meet/MeetingRoomController.php b/app/Http/Controllers/Meet/MeetingRoomController.php index 960c216..0dbec32 100644 --- a/app/Http/Controllers/Meet/MeetingRoomController.php +++ b/app/Http/Controllers/Meet/MeetingRoomController.php @@ -16,6 +16,7 @@ use App\Services\Meet\SecurityPolicyService; use App\Services\Meet\SessionService; use App\Services\Meet\SpaceService; use App\Services\Meet\StageLayoutService; +use App\Services\Meet\TownHallService; use App\Services\Meet\TranscriptService; use App\Services\Meet\WebinarService; use Illuminate\Http\JsonResponse; @@ -44,6 +45,12 @@ class MeetingRoomController extends Controller $kind = $room->isConference() ? 'conference' : 'meeting'; $participantUuid = $request->session()->get("meet.participant.{$session->uuid}"); + + if (! $participantUuid && $request->user() && $request->user()->ownerRef() === $room->host_user_ref) { + $hostParticipant = $this->sessions->ensureJoinedParticipant($request, $session, $request->user(), 'host'); + $participantUuid = $hostParticipant->uuid; + } + abort_unless($participantUuid, 403, "Please join the {$kind} first."); $participant = Participant::where('uuid', $participantUuid) @@ -227,6 +234,27 @@ class MeetingRoomController extends Controller public function poll(Request $request, Session $session): JsonResponse { if (! $session->isLive()) { + $townHall = app(TownHallService::class); + $liveSession = $townHall->successorLiveSession($session); + + if ($liveSession) { + $participantUuid = $request->session()->get("meet.participant.{$session->uuid}"); + + if ($participantUuid) { + $greenParticipant = Participant::where('uuid', $participantUuid) + ->where('session_id', $session->id) + ->first(); + + if ($greenParticipant && $this->sessions->transitionGreenRoomParticipant($request, $greenParticipant, $liveSession)) { + return response()->json([ + 'ended' => true, + 'redirect' => route('meet.room', $liveSession), + 'transition' => 'go_live', + ]); + } + } + } + return response()->json([ 'ended' => true, 'redirect' => route('meet.ended', $session), diff --git a/app/Services/Meet/SessionService.php b/app/Services/Meet/SessionService.php index d723deb..c9df16a 100644 --- a/app/Services/Meet/SessionService.php +++ b/app/Services/Meet/SessionService.php @@ -233,6 +233,68 @@ class SessionService $request->session()->put("meet.participant.{$session->uuid}", $participant->uuid); } + public function ensureJoinedParticipant( + Request $request, + Session $session, + ?User $user, + string $role, + ?string $displayName = null, + ?string $email = null, + ): Participant { + if ($user) { + $existing = $this->joinedParticipantForUser($session, $user); + if ($existing) { + $this->rememberParticipantSession($request, $existing, $session); + + return $existing; + } + } + + $participant = $this->joinParticipant($session, $user, $role, $displayName, $email); + $this->rememberParticipantSession($request, $participant, $session); + + return $participant; + } + + public function transitionGreenRoomParticipant( + Request $request, + Participant $greenParticipant, + Session $liveSession, + ): ?Participant { + $presenters = (array) $liveSession->presenter_refs; + $isPresenter = in_array($greenParticipant->role, ['host', 'co_host', 'panelist'], true) + || ($greenParticipant->user_ref && in_array($greenParticipant->user_ref, $presenters, true)); + + if (! $isPresenter) { + return null; + } + + $user = $greenParticipant->user_ref + ? User::where('public_id', $greenParticipant->user_ref)->first() + : null; + + $role = $greenParticipant->role; + if ($user && $user->ownerRef() === $liveSession->room->host_user_ref) { + $role = 'host'; + } elseif (! in_array($role, ['host', 'co_host', 'panelist'], true)) { + $role = 'panelist'; + } + + $greenSession = $greenParticipant->session; + $participant = $this->ensureJoinedParticipant( + $request, + $liveSession, + $user, + $role, + $greenParticipant->display_name, + $greenParticipant->email, + ); + + $request->session()->forget("meet.participant.{$greenSession->uuid}"); + + return $participant; + } + public function joinedParticipantForUser(Session $session, User $user): ?Participant { return $session->participants() diff --git a/app/Services/Meet/TownHallService.php b/app/Services/Meet/TownHallService.php index 4f88f39..4393d94 100644 --- a/app/Services/Meet/TownHallService.php +++ b/app/Services/Meet/TownHallService.php @@ -66,6 +66,20 @@ class TownHallService return $liveSession; } + public function successorLiveSession(Session $greenRoomSession): ?Session + { + if ($greenRoomSession->session_mode !== 'green_room' || $greenRoomSession->isLive()) { + return null; + } + + return $greenRoomSession->room->sessions() + ->where('status', 'live') + ->where('session_mode', 'live') + ->where('id', '!=', $greenRoomSession->id) + ->latest('started_at') + ->first(); + } + public function handoffCoHost(Session $session, Participant $from, string $toUserRef): Participant { abort_unless(in_array($from->role, ['host', 'co_host'], true), 403); diff --git a/resources/js/meet-room.js b/resources/js/meet-room.js index 00e4f30..1465c09 100644 --- a/resources/js/meet-room.js +++ b/resources/js/meet-room.js @@ -1900,6 +1900,11 @@ function meetRoom() { this.goingLive = true; + if (this.pollTimer) { + clearInterval(this.pollTimer); + this.pollTimer = null; + } + try { const res = await fetch(this.config.goLiveUrl, { method: 'POST', @@ -1911,9 +1916,10 @@ function meetRoom() { } const data = await res.json(); - window.location.href = data.room_url || `/room/${data.session_uuid}`; + window.location.replace(data.room_url || `/room/${data.session_uuid}`); } catch { this.goingLive = false; + this.pollTimer = setInterval(() => this.poll(), 3000); } }, @@ -2046,6 +2052,9 @@ function meetRoom() { const data = await res.json(); if (data.ended && data.redirect) { + if (this.goingLive) { + return; + } this.redirectToMeetingEnded(data.redirect); return; } diff --git a/tests/Feature/MeetWebTest.php b/tests/Feature/MeetWebTest.php index 4ce9555..39f77e4 100644 --- a/tests/Feature/MeetWebTest.php +++ b/tests/Feature/MeetWebTest.php @@ -787,6 +787,121 @@ class MeetWebTest extends TestCase $this->assertSame('host', $participant->fresh()->role); } + public function test_host_go_live_auto_joins_live_session(): void + { + $room = Room::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'host_user_ref' => $this->user->public_id, + 'title' => 'NextGen Summit', + 'type' => 'town_hall', + 'status' => 'live', + 'scheduled_at' => now()->addHour(), + 'timezone' => 'UTC', + 'settings' => array_merge(config('meet.default_settings'), [ + 'green_room' => true, + 'stage_mode' => true, + ]), + ]); + + $greenSession = Session::create([ + 'owner_ref' => $this->user->public_id, + 'room_id' => $room->id, + 'media_room_name' => 'meet-'.$room->uuid.'-green', + 'status' => 'live', + 'session_mode' => 'green_room', + 'presenter_refs' => [$this->user->public_id], + 'started_at' => now(), + ]); + + $hostParticipant = \App\Models\Participant::create([ + 'owner_ref' => $this->user->public_id, + 'session_id' => $greenSession->id, + 'user_ref' => $this->user->public_id, + 'display_name' => $this->user->name, + 'role' => 'host', + 'status' => 'joined', + 'joined_at' => now(), + ]); + + $this->actingAs($this->user) + ->withSession(["meet.participant.{$greenSession->uuid}" => $hostParticipant->uuid]) + ->postJson(route('meet.room.townhall.live', $greenSession)) + ->assertOk() + ->assertJson(['mode' => 'live']); + + $liveSession = Session::query() + ->where('room_id', $room->id) + ->where('session_mode', 'live') + ->first(); + + $this->assertNotNull($liveSession); + $this->assertSame('ended', $greenSession->fresh()->status); + + $this->actingAs($this->user) + ->get(route('meet.room', $liveSession)) + ->assertOk(); + + $this->assertDatabaseHas('meet_participants', [ + 'session_id' => $liveSession->id, + 'user_ref' => $this->user->public_id, + 'role' => 'host', + 'status' => 'joined', + ]); + } + + public function test_green_room_poll_redirects_presenters_to_live_session(): void + { + $room = Room::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'host_user_ref' => $this->user->public_id, + 'title' => 'Panel prep', + 'type' => 'town_hall', + 'status' => 'live', + 'scheduled_at' => now()->addHour(), + 'timezone' => 'UTC', + 'settings' => config('meet.default_settings'), + ]); + + $greenSession = Session::create([ + 'owner_ref' => $this->user->public_id, + 'room_id' => $room->id, + 'media_room_name' => 'meet-'.$room->uuid.'-green', + 'status' => 'live', + 'session_mode' => 'green_room', + 'presenter_refs' => [$this->user->public_id], + 'started_at' => now(), + ]); + + $hostParticipant = \App\Models\Participant::create([ + 'owner_ref' => $this->user->public_id, + 'session_id' => $greenSession->id, + 'user_ref' => $this->user->public_id, + 'display_name' => $this->user->name, + 'role' => 'host', + 'status' => 'joined', + 'joined_at' => now(), + ]); + + app(\App\Services\Meet\TownHallService::class)->goLive($greenSession, $this->user); + + $liveSession = Session::query() + ->where('room_id', $room->id) + ->where('session_mode', 'live') + ->firstOrFail(); + + $this->actingAs($this->user) + ->withSession(["meet.participant.{$greenSession->uuid}" => $hostParticipant->uuid]) + ->getJson(route('meet.room.poll', $greenSession)) + ->assertOk() + ->assertJson([ + 'ended' => true, + 'transition' => 'go_live', + 'redirect' => route('meet.room', $liveSession), + ]); + } + public function test_host_can_end_conference_and_view_details(): void { $room = Room::create([