Auto-join host into live session when going live from green room.
Deploy Ladill Meet / deploy (push) Successful in 2m1s
Deploy Ladill Meet / deploy (push) Successful in 2m1s
Green room poll was racing go-live and sending the host to the ended page; carry participant session into the new live session instead. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,6 +15,7 @@ use App\Services\Meet\FileSharingService;
|
|||||||
use App\Services\Meet\LiveStreamService;
|
use App\Services\Meet\LiveStreamService;
|
||||||
use App\Services\Meet\PollService;
|
use App\Services\Meet\PollService;
|
||||||
use App\Services\Meet\QaService;
|
use App\Services\Meet\QaService;
|
||||||
|
use App\Services\Meet\SessionService;
|
||||||
use App\Services\Meet\StageLayoutService;
|
use App\Services\Meet\StageLayoutService;
|
||||||
use App\Services\Meet\TownHallService;
|
use App\Services\Meet\TownHallService;
|
||||||
use App\Services\Meet\WhiteboardService;
|
use App\Services\Meet\WhiteboardService;
|
||||||
@@ -34,6 +35,7 @@ class MeetingFeaturesController extends Controller
|
|||||||
protected LiveStreamService $liveStreams,
|
protected LiveStreamService $liveStreams,
|
||||||
protected TownHallService $townHall,
|
protected TownHallService $townHall,
|
||||||
protected StageLayoutService $stageLayout,
|
protected StageLayoutService $stageLayout,
|
||||||
|
protected SessionService $sessions,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// --- Q&A ---
|
// --- Q&A ---
|
||||||
@@ -282,13 +284,24 @@ class MeetingFeaturesController extends Controller
|
|||||||
|
|
||||||
public function goLiveTownHall(Request $request, Session $session): JsonResponse
|
public function goLiveTownHall(Request $request, Session $session): JsonResponse
|
||||||
{
|
{
|
||||||
$this->hostOnly($request, $session);
|
$hostParticipant = $this->hostOnly($request, $session);
|
||||||
$live = $this->townHall->goLive($session, $request->user());
|
|
||||||
$participant = app(SessionService::class)->joinedParticipantForUser($live, $request->user());
|
|
||||||
|
|
||||||
if ($participant) {
|
$user = $request->user()
|
||||||
app(SessionService::class)->rememberParticipantSession($request, $participant, $live);
|
?? ($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([
|
return response()->json([
|
||||||
'session_uuid' => $live->uuid,
|
'session_uuid' => $live->uuid,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use App\Services\Meet\SecurityPolicyService;
|
|||||||
use App\Services\Meet\SessionService;
|
use App\Services\Meet\SessionService;
|
||||||
use App\Services\Meet\SpaceService;
|
use App\Services\Meet\SpaceService;
|
||||||
use App\Services\Meet\StageLayoutService;
|
use App\Services\Meet\StageLayoutService;
|
||||||
|
use App\Services\Meet\TownHallService;
|
||||||
use App\Services\Meet\TranscriptService;
|
use App\Services\Meet\TranscriptService;
|
||||||
use App\Services\Meet\WebinarService;
|
use App\Services\Meet\WebinarService;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
@@ -44,6 +45,12 @@ class MeetingRoomController extends Controller
|
|||||||
$kind = $room->isConference() ? 'conference' : 'meeting';
|
$kind = $room->isConference() ? 'conference' : 'meeting';
|
||||||
|
|
||||||
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
|
$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.");
|
abort_unless($participantUuid, 403, "Please join the {$kind} first.");
|
||||||
|
|
||||||
$participant = Participant::where('uuid', $participantUuid)
|
$participant = Participant::where('uuid', $participantUuid)
|
||||||
@@ -227,6 +234,27 @@ class MeetingRoomController extends Controller
|
|||||||
public function poll(Request $request, Session $session): JsonResponse
|
public function poll(Request $request, Session $session): JsonResponse
|
||||||
{
|
{
|
||||||
if (! $session->isLive()) {
|
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([
|
return response()->json([
|
||||||
'ended' => true,
|
'ended' => true,
|
||||||
'redirect' => route('meet.ended', $session),
|
'redirect' => route('meet.ended', $session),
|
||||||
|
|||||||
@@ -233,6 +233,68 @@ class SessionService
|
|||||||
$request->session()->put("meet.participant.{$session->uuid}", $participant->uuid);
|
$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
|
public function joinedParticipantForUser(Session $session, User $user): ?Participant
|
||||||
{
|
{
|
||||||
return $session->participants()
|
return $session->participants()
|
||||||
|
|||||||
@@ -66,6 +66,20 @@ class TownHallService
|
|||||||
return $liveSession;
|
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
|
public function handoffCoHost(Session $session, Participant $from, string $toUserRef): Participant
|
||||||
{
|
{
|
||||||
abort_unless(in_array($from->role, ['host', 'co_host'], true), 403);
|
abort_unless(in_array($from->role, ['host', 'co_host'], true), 403);
|
||||||
|
|||||||
@@ -1900,6 +1900,11 @@ function meetRoom() {
|
|||||||
|
|
||||||
this.goingLive = true;
|
this.goingLive = true;
|
||||||
|
|
||||||
|
if (this.pollTimer) {
|
||||||
|
clearInterval(this.pollTimer);
|
||||||
|
this.pollTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(this.config.goLiveUrl, {
|
const res = await fetch(this.config.goLiveUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -1911,9 +1916,10 @@ function meetRoom() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
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 {
|
} catch {
|
||||||
this.goingLive = false;
|
this.goingLive = false;
|
||||||
|
this.pollTimer = setInterval(() => this.poll(), 3000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -2046,6 +2052,9 @@ function meetRoom() {
|
|||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.ended && data.redirect) {
|
if (data.ended && data.redirect) {
|
||||||
|
if (this.goingLive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.redirectToMeetingEnded(data.redirect);
|
this.redirectToMeetingEnded(data.redirect);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -787,6 +787,121 @@ class MeetWebTest extends TestCase
|
|||||||
$this->assertSame('host', $participant->fresh()->role);
|
$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
|
public function test_host_can_end_conference_and_view_details(): void
|
||||||
{
|
{
|
||||||
$room = Room::create([
|
$room = Room::create([
|
||||||
|
|||||||
Reference in New Issue
Block a user