Redirect participants to meeting ended screen instead of 404.
Deploy Ladill Meet / deploy (push) Successful in 49s
Deploy Ladill Meet / deploy (push) Successful in 49s
Poll and waiting-room clients follow the same redirect when the host ends the call; chat and participants use mobile bottom sheets. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -181,8 +181,15 @@ class JoinController extends Controller
|
|||||||
{
|
{
|
||||||
$room->loadMissing('organization');
|
$room->loadMissing('organization');
|
||||||
|
|
||||||
$session = $room->activeSession();
|
$session = $room->activeSession() ?? $this->participantSessionForRoom($request, $room);
|
||||||
abort_unless($session?->isLive(), 404);
|
|
||||||
|
if (! $session || ! $session->isLive()) {
|
||||||
|
if ($session) {
|
||||||
|
return redirect()->route('meet.ended', $session);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('meet.join', $room);
|
||||||
|
}
|
||||||
|
|
||||||
$participant = $this->participantFromSession($request, $session);
|
$participant = $this->participantFromSession($request, $session);
|
||||||
|
|
||||||
@@ -204,8 +211,18 @@ class JoinController extends Controller
|
|||||||
|
|
||||||
public function waitingStatus(Request $request, Room $room): JsonResponse
|
public function waitingStatus(Request $request, Room $room): JsonResponse
|
||||||
{
|
{
|
||||||
$session = $room->activeSession();
|
$session = $room->activeSession() ?? $this->participantSessionForRoom($request, $room);
|
||||||
abort_unless($session, 404);
|
|
||||||
|
if (! $session || ! $session->isLive()) {
|
||||||
|
if ($session) {
|
||||||
|
return response()->json([
|
||||||
|
'ended' => true,
|
||||||
|
'redirect' => route('meet.ended', $session),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
$participant = $this->participantFromSession($request, $session);
|
$participant = $this->participantFromSession($request, $session);
|
||||||
|
|
||||||
@@ -230,4 +247,15 @@ class JoinController extends Controller
|
|||||||
->where('session_id', $session->id)
|
->where('session_id', $session->id)
|
||||||
->first();
|
->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function participantSessionForRoom(Request $request, Room $room): ?Session
|
||||||
|
{
|
||||||
|
foreach ($room->sessions()->orderByDesc('started_at')->get() as $session) {
|
||||||
|
if ($request->session()->has("meet.participant.{$session->uuid}")) {
|
||||||
|
return $session;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,4 +77,50 @@ class LeaveController extends Controller
|
|||||||
'organization' => $session->room->organization,
|
'organization' => $session->room->organization,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function ended(Request $request, Session $session): View|RedirectResponse
|
||||||
|
{
|
||||||
|
$session->loadMissing('room.organization');
|
||||||
|
abort_unless($session->room, 404);
|
||||||
|
|
||||||
|
if ($session->isLive()) {
|
||||||
|
return redirect()->route('meet.room', $session);
|
||||||
|
}
|
||||||
|
|
||||||
|
$canGiveFeedback = $this->prepareFeedbackSession($request, $session);
|
||||||
|
|
||||||
|
return view('meet.ended.show', [
|
||||||
|
'session' => $session,
|
||||||
|
'room' => $session->room,
|
||||||
|
'organization' => $session->room->organization,
|
||||||
|
'canGiveFeedback' => $canGiveFeedback,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prepareFeedbackSession(Request $request, Session $session): bool
|
||||||
|
{
|
||||||
|
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
|
||||||
|
|
||||||
|
if ($participantUuid) {
|
||||||
|
$participant = Participant::where('uuid', $participantUuid)
|
||||||
|
->where('session_id', $session->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($participant) {
|
||||||
|
$request->session()->put('meet.left.feedback', [
|
||||||
|
'session_uuid' => $session->uuid,
|
||||||
|
'participant_uuid' => $participant->uuid,
|
||||||
|
'display_name' => $participant->display_name,
|
||||||
|
'user_ref' => $participant->user_ref,
|
||||||
|
]);
|
||||||
|
$request->session()->forget("meet.participant.{$session->uuid}");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$feedback = (array) $request->session()->get('meet.left.feedback', []);
|
||||||
|
|
||||||
|
return ($feedback['session_uuid'] ?? null) === $session->uuid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ class MeetingRoomController extends Controller
|
|||||||
|
|
||||||
public function show(Request $request, Session $session): View|RedirectResponse
|
public function show(Request $request, Session $session): View|RedirectResponse
|
||||||
{
|
{
|
||||||
abort_unless($session->isLive(), 404, 'Meeting has ended.');
|
if (! $session->isLive()) {
|
||||||
|
return redirect()->route('meet.ended', $session);
|
||||||
|
}
|
||||||
|
|
||||||
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
|
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
|
||||||
abort_unless($participantUuid, 403, 'Please join the meeting first.');
|
abort_unless($participantUuid, 403, 'Please join the meeting first.');
|
||||||
@@ -156,6 +158,13 @@ class MeetingRoomController extends Controller
|
|||||||
|
|
||||||
public function poll(Request $request, Session $session): JsonResponse
|
public function poll(Request $request, Session $session): JsonResponse
|
||||||
{
|
{
|
||||||
|
if (! $session->isLive()) {
|
||||||
|
return response()->json([
|
||||||
|
'ended' => true,
|
||||||
|
'redirect' => route('meet.ended', $session),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$this->currentParticipant($request, $session);
|
$this->currentParticipant($request, $session);
|
||||||
|
|
||||||
$session->load(['participants' => fn ($q) => $q->whereIn('status', ['joined', 'waiting'])]);
|
$session->load(['participants' => fn ($q) => $q->whereIn('status', ['joined', 'waiting'])]);
|
||||||
|
|||||||
@@ -72,6 +72,101 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Meeting room chat / participants: bottom sheet on mobile, sidebar on desktop */
|
||||||
|
.meet-room-panel {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meet-room-panel__backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-color: rgb(2 6 23 / 0.72);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meet-room-panel__sheet {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
max-height: min(85vh, 100%);
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 1.5rem 1.5rem 0 0;
|
||||||
|
background-color: rgb(2 6 23);
|
||||||
|
padding: 0 0.75rem max(0.75rem, env(safe-area-inset-bottom, 0px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.meet-room-panel__handle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.75rem 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meet-room-panel__handle span {
|
||||||
|
display: block;
|
||||||
|
height: 0.25rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background-color: rgb(51 65 85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meet-room-panel__header {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0 0.25rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meet-room-panel__body {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background-color: rgb(15 23 42 / 0.9);
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.meet-room-panel {
|
||||||
|
position: relative;
|
||||||
|
inset: auto;
|
||||||
|
z-index: auto;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 20rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meet-room-panel__backdrop,
|
||||||
|
.meet-room-panel__handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meet-room-panel__sheet {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
max-height: none;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meet-room-panel__body {
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* QR create/show: flush mobile action bar to the physical screen bottom (mobile only) */
|
/* QR create/show: flush mobile action bar to the physical screen bottom (mobile only) */
|
||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
.mobile-action-bar {
|
.mobile-action-bar {
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ Alpine.data('waitingRoom', () => ({
|
|||||||
const res = await fetch(url, { headers: { Accept: 'application/json' } });
|
const res = await fetch(url, { headers: { Accept: 'application/json' } });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.ended && data.redirect) {
|
||||||
|
window.location.href = data.redirect;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.redirect) {
|
if (data.redirect) {
|
||||||
window.location.href = data.redirect;
|
window.location.href = data.redirect;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ function meetRoom() {
|
|||||||
url: el.dataset.url,
|
url: el.dataset.url,
|
||||||
configured: el.dataset.configured === '1',
|
configured: el.dataset.configured === '1',
|
||||||
pollUrl: el.dataset.pollUrl,
|
pollUrl: el.dataset.pollUrl,
|
||||||
|
endedUrl: el.dataset.endedUrl,
|
||||||
chatUrl: el.dataset.chatUrl,
|
chatUrl: el.dataset.chatUrl,
|
||||||
reactionUrl: el.dataset.reactionUrl,
|
reactionUrl: el.dataset.reactionUrl,
|
||||||
raiseHandUrl: el.dataset.raiseHandUrl,
|
raiseHandUrl: el.dataset.raiseHandUrl,
|
||||||
@@ -1044,7 +1045,18 @@ function meetRoom() {
|
|||||||
async poll() {
|
async poll() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(this.config.pollUrl, { headers: { Accept: 'application/json' } });
|
const res = await fetch(this.config.pollUrl, { headers: { Accept: 'application/json' } });
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 404 && this.config.endedUrl) {
|
||||||
|
this.redirectToMeetingEnded(this.config.endedUrl);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
if (data.ended && data.redirect) {
|
||||||
|
this.redirectToMeetingEnded(data.redirect);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (data.participants) {
|
if (data.participants) {
|
||||||
this.sessionParticipants = data.participants;
|
this.sessionParticipants = data.participants;
|
||||||
this.pruneTilesFromSession();
|
this.pruneTilesFromSession();
|
||||||
@@ -1067,6 +1079,19 @@ function meetRoom() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
redirectToMeetingEnded(url) {
|
||||||
|
if (this.pollTimer) {
|
||||||
|
clearInterval(this.pollTimer);
|
||||||
|
this.pollTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (room) {
|
||||||
|
room.disconnect().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = url;
|
||||||
|
},
|
||||||
|
|
||||||
updateChatFromPoll(messages) {
|
updateChatFromPoll(messages) {
|
||||||
const container = document.getElementById('chat-messages');
|
const container = document.getElementById('chat-messages');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
@props([
|
||||||
|
'show',
|
||||||
|
'title',
|
||||||
|
])
|
||||||
|
|
||||||
|
<aside x-show="{{ $show }}" x-cloak
|
||||||
|
class="meet-room-panel"
|
||||||
|
@keydown.escape.window="{{ $show }} = false">
|
||||||
|
<div x-show="{{ $show }}"
|
||||||
|
x-transition:enter="transition-opacity ease-out duration-200"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="transition-opacity ease-in duration-150"
|
||||||
|
x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
class="meet-room-panel__backdrop"
|
||||||
|
@click="{{ $show }} = false"
|
||||||
|
aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<div x-show="{{ $show }}"
|
||||||
|
x-transition:enter="transition ease-out duration-300"
|
||||||
|
x-transition:enter-start="translate-y-full sm:translate-y-0"
|
||||||
|
x-transition:enter-end="translate-y-0"
|
||||||
|
x-transition:leave="transition ease-in duration-200"
|
||||||
|
x-transition:leave-start="translate-y-0"
|
||||||
|
x-transition:leave-end="translate-y-full sm:translate-y-0"
|
||||||
|
class="meet-room-panel__sheet">
|
||||||
|
<div class="meet-room-panel__handle" aria-hidden="true"><span></span></div>
|
||||||
|
|
||||||
|
<div class="meet-room-panel__header">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<h2 class="text-sm font-semibold text-white">{{ $title }}</h2>
|
||||||
|
@isset($subtitle)
|
||||||
|
<div class="text-xs text-slate-400">{{ $subtitle }}</div>
|
||||||
|
@endisset
|
||||||
|
</div>
|
||||||
|
<button @click="{{ $show }} = false" type="button"
|
||||||
|
class="shrink-0 rounded-lg p-2 text-slate-400 transition-colors hover:bg-slate-800 hover:text-white"
|
||||||
|
title="Close">
|
||||||
|
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="meet-room-panel__body">
|
||||||
|
{{ $slot }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<x-meet-kiosk-layout :title="'Meeting ended · '.$room->title" :organization="$organization">
|
||||||
|
<div class="flex min-h-[calc(100vh-8rem)] flex-col items-center justify-center text-center">
|
||||||
|
<div class="w-full max-w-md rounded-3xl border border-slate-200 bg-white p-8 shadow-sm">
|
||||||
|
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-slate-100 text-slate-600">
|
||||||
|
<svg class="h-7 w-7" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 class="mt-6 text-2xl font-bold text-slate-900">Meeting ended</h1>
|
||||||
|
<p class="mt-3 text-sm text-slate-600">The host has ended this meeting.</p>
|
||||||
|
<p class="mt-1 text-sm font-medium text-slate-700">{{ $room->title }}</p>
|
||||||
|
|
||||||
|
<div class="mt-8 flex flex-col gap-3">
|
||||||
|
@if ($canGiveFeedback)
|
||||||
|
<a href="{{ route('meet.left', $session) }}" class="btn-primary btn-primary-lg py-3.5 font-semibold">
|
||||||
|
Share feedback
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
@auth
|
||||||
|
<a href="{{ route('meet.dashboard') }}"
|
||||||
|
class="{{ $canGiveFeedback ? 'btn-secondary py-3.5 font-semibold' : 'btn-primary btn-primary-lg py-3.5 font-semibold' }}">
|
||||||
|
Back to Meet
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<a href="{{ route('meet.join', $room) }}"
|
||||||
|
class="{{ $canGiveFeedback ? 'btn-secondary py-3.5 font-semibold' : 'btn-primary btn-primary-lg py-3.5 font-semibold' }}">
|
||||||
|
Return to join page
|
||||||
|
</a>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-meet-kiosk-layout>
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
data-display-name="{{ $participant->display_name }}"
|
data-display-name="{{ $participant->display_name }}"
|
||||||
data-is-host="{{ $participant->isHost() ? '1' : '0' }}"
|
data-is-host="{{ $participant->isHost() ? '1' : '0' }}"
|
||||||
data-poll-url="{{ route('meet.room.poll', $session) }}"
|
data-poll-url="{{ route('meet.room.poll', $session) }}"
|
||||||
|
data-ended-url="{{ route('meet.ended', $session) }}"
|
||||||
data-chat-url="{{ route('meet.room.chat', $session) }}"
|
data-chat-url="{{ route('meet.room.chat', $session) }}"
|
||||||
data-reaction-url="{{ route('meet.room.reaction', $session) }}"
|
data-reaction-url="{{ route('meet.room.reaction', $session) }}"
|
||||||
data-raise-hand-url="{{ route('meet.room.raise-hand', $session) }}"
|
data-raise-hand-url="{{ route('meet.room.raise-hand', $session) }}"
|
||||||
@@ -389,20 +390,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside x-show="chatOpen" x-cloak
|
<x-meet.room.panel show="chatOpen" title="Meeting chat">
|
||||||
class="flex h-full w-80 shrink-0 flex-col bg-slate-950 px-3 py-3">
|
<x-slot:subtitle>
|
||||||
<div class="flex shrink-0 items-center justify-between px-1 pb-3">
|
<p>Messages are visible to everyone in the call</p>
|
||||||
<div>
|
</x-slot:subtitle>
|
||||||
<h2 class="text-sm font-semibold text-white">Meeting chat</h2>
|
|
||||||
<p class="text-xs text-slate-400">Messages are visible to everyone in the call</p>
|
<div class="flex min-h-0 flex-1 flex-col">
|
||||||
</div>
|
|
||||||
<button @click="chatOpen = false" type="button"
|
|
||||||
class="rounded-lg p-2 text-slate-400 transition-colors hover:bg-slate-800 hover:text-white"
|
|
||||||
title="Close chat">
|
|
||||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex min-h-0 flex-1 flex-col rounded-2xl bg-slate-900/90 p-3">
|
|
||||||
<div class="flex-1 space-y-3 overflow-y-auto" id="chat-messages">
|
<div class="flex-1 space-y-3 overflow-y-auto" id="chat-messages">
|
||||||
@foreach ($messages as $message)
|
@foreach ($messages as $message)
|
||||||
@php $isOwn = $message->sender_name === $participant->display_name; @endphp
|
@php $isOwn = $message->sender_name === $participant->display_name; @endphp
|
||||||
@@ -427,22 +420,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</x-meet.room.panel>
|
||||||
|
|
||||||
<aside x-show="participantsOpen" x-cloak
|
<x-meet.room.panel show="participantsOpen" title="Participants">
|
||||||
class="flex h-full w-80 shrink-0 flex-col bg-slate-950 px-3 py-3">
|
<x-slot:subtitle>
|
||||||
<div class="flex shrink-0 items-center justify-between px-1 pb-3">
|
<p x-text="participantCount() + (participantCount() === 1 ? ' person' : ' people') + ' in this call'"></p>
|
||||||
<div>
|
</x-slot:subtitle>
|
||||||
<h2 class="text-sm font-semibold text-white">Participants</h2>
|
|
||||||
<p class="text-xs text-slate-400" x-text="participantCount() + (participantCount() === 1 ? ' person' : ' people') + ' in this call'"></p>
|
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
||||||
</div>
|
|
||||||
<button @click="participantsOpen = false" type="button"
|
|
||||||
class="rounded-lg p-2 text-slate-400 transition-colors hover:bg-slate-800 hover:text-white"
|
|
||||||
title="Close participants">
|
|
||||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto rounded-2xl bg-slate-900/90 p-2">
|
|
||||||
<template x-if="isHost && waitingParticipants().length">
|
<template x-if="isHost && waitingParticipants().length">
|
||||||
<div class="mb-3 border-b border-slate-800 pb-3">
|
<div class="mb-3 border-b border-slate-800 pb-3">
|
||||||
<p class="px-2 pb-2 text-xs font-semibold uppercase tracking-wide text-amber-300">
|
<p class="px-2 pb-2 text-xs font-semibold uppercase tracking-wide text-amber-300">
|
||||||
@@ -499,7 +484,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<p x-show="inCallParticipants().length === 0 && waitingParticipants().length === 0" class="px-2 py-4 text-center text-xs text-slate-500">No participants yet.</p>
|
<p x-show="inCallParticipants().length === 0 && waitingParticipants().length === 0" class="px-2 py-4 text-center text-xs text-slate-500">No participants yet.</p>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</x-meet.room.panel>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ Route::post('/r/{room}/enter', [JoinController::class, 'enter'])->name('meet.joi
|
|||||||
Route::get('/room/{session}/left', [LeaveController::class, 'show'])->name('meet.left');
|
Route::get('/room/{session}/left', [LeaveController::class, 'show'])->name('meet.left');
|
||||||
Route::post('/room/{session}/left', [LeaveController::class, 'store'])->name('meet.left.feedback');
|
Route::post('/room/{session}/left', [LeaveController::class, 'store'])->name('meet.left.feedback');
|
||||||
Route::get('/room/{session}/thanks', [LeaveController::class, 'thanks'])->name('meet.left.thanks');
|
Route::get('/room/{session}/thanks', [LeaveController::class, 'thanks'])->name('meet.left.thanks');
|
||||||
|
Route::get('/room/{session}/ended', [LeaveController::class, 'ended'])->name('meet.ended');
|
||||||
|
|
||||||
Route::match(['get', 'post'], '/invite/{invitation}/rsvp', [InvitationController::class, 'rsvp'])->name('meet.invitations.rsvp');
|
Route::match(['get', 'post'], '/invite/{invitation}/rsvp', [InvitationController::class, 'rsvp'])->name('meet.invitations.rsvp');
|
||||||
|
|
||||||
|
|||||||
@@ -251,6 +251,94 @@ class MeetWebTest extends TestCase
|
|||||||
->assertRedirect(route('meet.left', $session));
|
->assertRedirect(route('meet.left', $session));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_participant_redirected_to_ended_page_when_meeting_ended(): void
|
||||||
|
{
|
||||||
|
$room = $this->createRoom(['settings' => array_merge(config('meet.default_settings'), ['waiting_room' => false])]);
|
||||||
|
$session = Session::create([
|
||||||
|
'owner_ref' => $this->user->public_id,
|
||||||
|
'room_id' => $room->id,
|
||||||
|
'media_room_name' => 'room-'.$room->uuid,
|
||||||
|
'status' => 'ended',
|
||||||
|
'started_at' => now()->subHour(),
|
||||||
|
'ended_at' => now(),
|
||||||
|
]);
|
||||||
|
$room->update(['status' => 'ended']);
|
||||||
|
$participant = \App\Models\Participant::create([
|
||||||
|
'owner_ref' => $this->user->public_id,
|
||||||
|
'session_id' => $session->id,
|
||||||
|
'display_name' => 'Guest',
|
||||||
|
'role' => 'guest',
|
||||||
|
'status' => 'left',
|
||||||
|
'joined_at' => now()->subHour(),
|
||||||
|
'left_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withSession(["meet.participant.{$session->uuid}" => $participant->uuid])
|
||||||
|
->get('/room/'.$session->uuid)
|
||||||
|
->assertRedirect(route('meet.ended', $session));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_poll_returns_ended_payload_when_meeting_ended(): void
|
||||||
|
{
|
||||||
|
$room = $this->createRoom();
|
||||||
|
$session = Session::create([
|
||||||
|
'owner_ref' => $this->user->public_id,
|
||||||
|
'room_id' => $room->id,
|
||||||
|
'media_room_name' => 'room-'.$room->uuid,
|
||||||
|
'status' => 'ended',
|
||||||
|
'started_at' => now()->subHour(),
|
||||||
|
'ended_at' => now(),
|
||||||
|
]);
|
||||||
|
$participant = \App\Models\Participant::create([
|
||||||
|
'owner_ref' => $this->user->public_id,
|
||||||
|
'session_id' => $session->id,
|
||||||
|
'display_name' => 'Guest',
|
||||||
|
'role' => 'guest',
|
||||||
|
'status' => 'left',
|
||||||
|
'joined_at' => now()->subHour(),
|
||||||
|
'left_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withSession(["meet.participant.{$session->uuid}" => $participant->uuid])
|
||||||
|
->getJson('/room/'.$session->uuid.'/poll')
|
||||||
|
->assertOk()
|
||||||
|
->assertJson([
|
||||||
|
'ended' => true,
|
||||||
|
'redirect' => route('meet.ended', $session),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_waiting_guest_redirected_when_host_ends_meeting(): void
|
||||||
|
{
|
||||||
|
$room = $this->createRoom(['settings' => array_merge(config('meet.default_settings'), ['waiting_room' => true])]);
|
||||||
|
$session = Session::create([
|
||||||
|
'owner_ref' => $this->user->public_id,
|
||||||
|
'room_id' => $room->id,
|
||||||
|
'media_room_name' => 'room-'.$room->uuid,
|
||||||
|
'status' => 'ended',
|
||||||
|
'started_at' => now()->subHour(),
|
||||||
|
'ended_at' => now(),
|
||||||
|
]);
|
||||||
|
$room->update(['status' => 'ended']);
|
||||||
|
$participant = \App\Models\Participant::create([
|
||||||
|
'owner_ref' => $this->user->public_id,
|
||||||
|
'session_id' => $session->id,
|
||||||
|
'display_name' => 'Waiting Guest',
|
||||||
|
'role' => 'guest',
|
||||||
|
'status' => 'left',
|
||||||
|
'joined_at' => now()->subHour(),
|
||||||
|
'left_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withSession(["meet.participant.{$session->uuid}" => $participant->uuid])
|
||||||
|
->getJson('/r/'.$room->uuid.'/waiting/status')
|
||||||
|
->assertOk()
|
||||||
|
->assertJson([
|
||||||
|
'ended' => true,
|
||||||
|
'redirect' => route('meet.ended', $session),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_authenticated_user_can_join_live_meeting_without_display_name(): void
|
public function test_authenticated_user_can_join_live_meeting_without_display_name(): void
|
||||||
{
|
{
|
||||||
$room = $this->createRoom();
|
$room = $this->createRoom();
|
||||||
|
|||||||
Reference in New Issue
Block a user