From 7ae26a467c0d22b8bccbe2426d9943cc9efa9254 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Wed, 1 Jul 2026 13:33:48 +0000 Subject: [PATCH] Add kiosk join/leave pages, waiting room, and meeting feedback. Redesign public join flows on the Frontdesk kiosk template, enforce host admission when waiting room is enabled, and collect star ratings after leave. Co-authored-by: Cursor --- app/Http/Controllers/Meet/JoinController.php | 100 ++++++++++++++++- app/Http/Controllers/Meet/LeaveController.php | 80 ++++++++++++++ .../Meet/MeetingRoomController.php | 40 ++++++- app/Models/Room.php | 13 +++ app/Models/SessionFeedback.php | 27 +++++ app/Services/Meet/ParticipantPresenter.php | 1 + app/Services/Meet/SessionService.php | 83 +++++++++++--- bootstrap/app.php | 3 + ...000_create_meet_session_feedback_table.php | 31 ++++++ resources/js/meet-public.js | 70 ++++++++++++ resources/js/meet-room.js | 35 +++++- .../components/meet-kiosk-layout.blade.php | 45 ++++++++ resources/views/meet/join/cancelled.blade.php | 22 ++-- resources/views/meet/join/denied.blade.php | 31 +++--- resources/views/meet/join/passcode.blade.php | 47 ++++---- resources/views/meet/join/show.blade.php | 102 +++++++++++------- resources/views/meet/join/waiting.blade.php | 29 +++++ resources/views/meet/left/show.blade.php | 91 ++++++++++++++++ resources/views/meet/left/thanks.blade.php | 27 +++++ .../views/meet/partials/kiosk-brand.blade.php | 8 ++ resources/views/meet/room/show.blade.php | 28 ++++- resources/views/meet/rooms/create.blade.php | 8 +- .../views/meet/templates/create.blade.php | 2 +- routes/web.php | 9 ++ tests/Feature/MeetWebTest.php | 55 ++++++++++ vite.config.js | 2 +- 26 files changed, 865 insertions(+), 124 deletions(-) create mode 100644 app/Http/Controllers/Meet/LeaveController.php create mode 100644 app/Models/SessionFeedback.php create mode 100644 database/migrations/2026_07_01_120000_create_meet_session_feedback_table.php create mode 100644 resources/js/meet-public.js create mode 100644 resources/views/components/meet-kiosk-layout.blade.php create mode 100644 resources/views/meet/join/waiting.blade.php create mode 100644 resources/views/meet/left/show.blade.php create mode 100644 resources/views/meet/left/thanks.blade.php create mode 100644 resources/views/meet/partials/kiosk-brand.blade.php diff --git a/app/Http/Controllers/Meet/JoinController.php b/app/Http/Controllers/Meet/JoinController.php index f921a2e..cb9e7a7 100644 --- a/app/Http/Controllers/Meet/JoinController.php +++ b/app/Http/Controllers/Meet/JoinController.php @@ -3,11 +3,14 @@ namespace App\Http\Controllers\Meet; use App\Http\Controllers\Controller; +use App\Models\Participant; use App\Models\Room; +use App\Models\Session; use App\Services\Meet\InvitationService; use App\Services\Meet\SecurityPolicyService; use App\Services\Meet\SessionService; use App\Services\Meet\WebinarService; +use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; @@ -23,8 +26,13 @@ class JoinController extends Controller public function show(Request $request, Room $room): View|RedirectResponse { + $room->loadMissing('organization'); + if ($room->status === 'cancelled') { - return view('meet.join.cancelled', compact('room')); + return view('meet.join.cancelled', [ + 'room' => $room, + 'organization' => $room->organization, + ]); } if ($room->isWebinar() && $request->query('token')) { @@ -33,15 +41,39 @@ class JoinController extends Controller [$allowed, $reason] = $this->security->canJoin($room, $request->user(), $request->user()?->email); if (! $allowed && $reason !== 'passcode_required') { - return view('meet.join.denied', compact('room', 'reason')); + return view('meet.join.denied', [ + 'room' => $room, + 'organization' => $room->organization, + 'reason' => $reason, + ]); } if ($room->passcode && ! $request->session()->get("meet.passcode.{$room->uuid}")) { - return view('meet.join.passcode', compact('room')); + return view('meet.join.passcode', [ + 'room' => $room, + 'organization' => $room->organization, + ]); + } + + $session = $room->activeSession(); + if ($session) { + $participantUuid = $request->session()->get("meet.participant.{$session->uuid}"); + $participant = $participantUuid + ? Participant::where('uuid', $participantUuid)->where('session_id', $session->id)->first() + : null; + + if ($participant?->status === 'waiting') { + return redirect()->route('meet.join.waiting', $room); + } + + if ($participant?->status === 'joined') { + return redirect()->route('meet.room', $session); + } } return view('meet.join.show', [ 'room' => $room, + 'organization' => $room->organization, 'user' => $request->user(), 'isWebinar' => $room->isWebinar(), ]); @@ -130,12 +162,72 @@ class JoinController extends Controller $displayName = $reg->display_name; } - $participant = $this->sessions->joinParticipant($session, $user, $role, $displayName, $email); + $joinStatus = $room->requiresWaitingRoom($user, $role) ? 'waiting' : 'joined'; + + $participant = $this->sessions->joinParticipant($session, $user, $role, $displayName, $email, $joinStatus); app(InvitationService::class)->markAcceptedOnJoin($room, $user, $email); $this->sessions->rememberParticipantSession($request, $participant, $session); + if ($participant->status === 'waiting') { + return redirect()->route('meet.join.waiting', $room); + } + return redirect()->route('meet.room', $session); } + + public function waiting(Request $request, Room $room): View|RedirectResponse + { + $room->loadMissing('organization'); + + $session = $room->activeSession(); + abort_unless($session?->isLive(), 404); + + $participant = $this->participantFromSession($request, $session); + + if (! $participant || $participant->status === 'removed') { + return redirect()->route('meet.join', $room); + } + + if ($participant->status === 'joined') { + return redirect()->route('meet.room', $session); + } + + return view('meet.join.waiting', [ + 'room' => $room, + 'session' => $session, + 'organization' => $room->organization, + 'participant' => $participant, + ]); + } + + public function waitingStatus(Request $request, Room $room): JsonResponse + { + $session = $room->activeSession(); + abort_unless($session, 404); + + $participant = $this->participantFromSession($request, $session); + + return response()->json([ + 'status' => $participant?->status ?? 'unknown', + 'redirect' => ($participant && $participant->status === 'joined') + ? route('meet.room', $session) + : null, + 'denied' => $participant?->status === 'removed', + ]); + } + + protected function participantFromSession(Request $request, Session $session): ?Participant + { + $participantUuid = $request->session()->get("meet.participant.{$session->uuid}"); + if (! $participantUuid) { + return null; + } + + return Participant::query() + ->where('uuid', $participantUuid) + ->where('session_id', $session->id) + ->first(); + } } diff --git a/app/Http/Controllers/Meet/LeaveController.php b/app/Http/Controllers/Meet/LeaveController.php new file mode 100644 index 0000000..d1f6e7f --- /dev/null +++ b/app/Http/Controllers/Meet/LeaveController.php @@ -0,0 +1,80 @@ +loadMissing('room.organization'); + abort_unless($session->room, 404); + + $feedback = (array) $request->session()->get('meet.left.feedback', []); + + if (($feedback['session_uuid'] ?? null) !== $session->uuid) { + return redirect()->route('meet.join', $session->room); + } + + return view('meet.left.show', [ + 'session' => $session, + 'room' => $session->room, + 'organization' => $session->room->organization, + 'feedback' => $feedback, + ]); + } + + public function store(Request $request, Session $session): RedirectResponse + { + $feedback = (array) $request->session()->get('meet.left.feedback', []); + + abort_unless(($feedback['session_uuid'] ?? null) === $session->uuid, 403); + + $validated = $request->validate([ + 'rating' => ['required', 'integer', 'min:1', 'max:5'], + 'audio_rating' => ['nullable', 'integer', 'min:1', 'max:5'], + 'video_rating' => ['nullable', 'integer', 'min:1', 'max:5'], + 'comment' => ['nullable', 'string', 'max:1000'], + ]); + + SessionFeedback::updateOrCreate( + [ + 'session_id' => $session->id, + 'participant_uuid' => $feedback['participant_uuid'] ?? null, + ], + [ + 'display_name' => $feedback['display_name'] ?? null, + 'user_ref' => $feedback['user_ref'] ?? null, + 'rating' => $validated['rating'], + 'audio_rating' => $validated['audio_rating'] ?? null, + 'video_rating' => $validated['video_rating'] ?? null, + 'comment' => $validated['comment'] ?? null, + ], + ); + + $request->session()->forget('meet.left.feedback'); + + return redirect() + ->route('meet.left.thanks', $session) + ->with('rated', true); + } + + public function thanks(Request $request, Session $session): View + { + $session->loadMissing('room.organization'); + + return view('meet.left.thanks', [ + 'session' => $session, + 'room' => $session->room, + 'organization' => $session->room->organization, + ]); + } +} diff --git a/app/Http/Controllers/Meet/MeetingRoomController.php b/app/Http/Controllers/Meet/MeetingRoomController.php index 019491b..57f5e36 100644 --- a/app/Http/Controllers/Meet/MeetingRoomController.php +++ b/app/Http/Controllers/Meet/MeetingRoomController.php @@ -39,6 +39,12 @@ class MeetingRoomController extends Controller ->where('session_id', $session->id) ->firstOrFail(); + if ($participant->status === 'waiting') { + return redirect()->route('meet.join.waiting', $session->room); + } + + abort_unless($participant->status === 'joined', 403, 'You are not in this meeting.'); + $token = $this->media->isConfigured() ? $this->sessions->generateMediaToken($participant) : ''; @@ -81,11 +87,18 @@ class MeetingRoomController extends Controller if ($participant) { $this->sessions->leaveParticipant($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 redirect()->route('meet.dashboard'); + return redirect()->route('meet.left', $session); } public function raiseHand(Request $request, Session $session): JsonResponse @@ -145,7 +158,7 @@ class MeetingRoomController extends Controller { $this->currentParticipant($request, $session); - $session->load(['participants' => fn ($q) => $q->where('status', 'joined')]); + $session->load(['participants' => fn ($q) => $q->whereIn('status', ['joined', 'waiting'])]); $avatars = ParticipantPresenter::avatarMap($session->participants, $session->room->host_user_ref); @@ -167,6 +180,7 @@ class MeetingRoomController extends Controller 'participants' => $session->participants->map( fn ($p) => ParticipantPresenter::toArray($p, $avatars, $session->room->host_user_ref) ), + 'waiting_count' => $session->participants->where('status', 'waiting')->count(), 'messages' => $messages->map(fn ($m) => [ 'uuid' => $m->uuid, 'sender_name' => $m->sender_name, @@ -271,6 +285,28 @@ class MeetingRoomController extends Controller return response()->json(['ok' => true]); } + public function admit(Request $request, Session $session, Participant $participant): JsonResponse + { + $current = $this->currentParticipant($request, $session); + abort_unless($current->isHost(), 403); + abort_unless($participant->session_id === $session->id, 404); + + $this->sessions->admitParticipant($participant); + + return response()->json(['ok' => true, 'status' => 'joined']); + } + + public function deny(Request $request, Session $session, Participant $participant): JsonResponse + { + $current = $this->currentParticipant($request, $session); + abort_unless($current->isHost(), 403); + abort_unless($participant->session_id === $session->id, 404); + + $this->sessions->denyParticipant($participant); + + return response()->json(['ok' => true, 'status' => 'removed']); + } + protected function currentParticipant(Request $request, Session $session): Participant { $participantUuid = $request->session()->get("meet.participant.{$session->uuid}"); diff --git a/app/Models/Room.php b/app/Models/Room.php index b6f0048..5fd4a1a 100644 --- a/app/Models/Room.php +++ b/app/Models/Room.php @@ -111,6 +111,19 @@ class Room extends Model return $this->status === 'live'; } + public function requiresWaitingRoom(?User $user, string $role): bool + { + if (! $this->setting('waiting_room', true)) { + return false; + } + + if ($role === 'host' || ($user && $user->ownerRef() === $this->host_user_ref)) { + return false; + } + + return true; + } + public function joinUrl(): string { return rtrim((string) config('app.meet_url', 'https://meet.ladill.com'), '/').'/r/'.$this->uuid; diff --git a/app/Models/SessionFeedback.php b/app/Models/SessionFeedback.php new file mode 100644 index 0000000..9d64ed5 --- /dev/null +++ b/app/Models/SessionFeedback.php @@ -0,0 +1,27 @@ +belongsTo(Session::class, 'session_id'); + } +} diff --git a/app/Services/Meet/ParticipantPresenter.php b/app/Services/Meet/ParticipantPresenter.php index a83c678..b51ee19 100644 --- a/app/Services/Meet/ParticipantPresenter.php +++ b/app/Services/Meet/ParticipantPresenter.php @@ -38,6 +38,7 @@ class ParticipantPresenter 'uuid' => $participant->uuid, 'display_name' => $participant->display_name, 'role' => $participant->role, + 'status' => $participant->status, 'hand_raised' => $participant->hand_raised, 'is_muted' => $participant->is_muted, 'is_video_off' => $participant->is_video_off, diff --git a/app/Services/Meet/SessionService.php b/app/Services/Meet/SessionService.php index f5f8220..dec07c4 100644 --- a/app/Services/Meet/SessionService.php +++ b/app/Services/Meet/SessionService.php @@ -63,7 +63,7 @@ class SessionService $session->room->update(['status' => 'ended']); $session->participants() - ->where('status', 'joined') + ->whereIn('status', ['joined', 'waiting']) ->update(['status' => 'left', 'left_at' => now()]); foreach ($session->recordings()->where('status', 'recording')->get() as $recording) { @@ -89,22 +89,32 @@ class SessionService string $role, ?string $displayName = null, ?string $email = null, + string $status = 'joined', ): Participant { $userRef = $user?->ownerRef(); $existing = $this->findExistingParticipant($session, $userRef, $email); if ($existing) { - $existing->update([ - 'status' => 'joined', - 'joined_at' => now(), + $updates = [ 'left_at' => null, 'display_name' => $displayName ?? $existing->display_name, 'ip_address' => request()?->ip(), - ]); + ]; + + if ($existing->status === 'waiting') { + $updates['status'] = 'waiting'; + } else { + $updates['status'] = 'joined'; + $updates['joined_at'] = $existing->joined_at ?? now(); + } + + $existing->update($updates); return $existing; } + $joinedAt = $status === 'joined' ? now() : null; + $participant = Participant::create([ 'owner_ref' => $session->owner_ref, 'session_id' => $session->id, @@ -112,23 +122,72 @@ class SessionService 'display_name' => $displayName ?? $user?->name ?? 'Guest', 'email' => $email, 'role' => $role, - 'status' => 'joined', - 'joined_at' => now(), + 'status' => $status, + 'joined_at' => $joinedAt, 'ip_address' => request()?->ip(), ]); - $this->updatePeakParticipants($session); + if ($status === 'joined') { + $this->updatePeakParticipants($session); + } + + if ($status === 'joined') { + AuditLogger::record( + $session->owner_ref, + 'participant.joined', + $session->room->organization_id, + $userRef, + Participant::class, + $participant->id, + ); + } + + return $participant; + } + + public function admitParticipant(Participant $participant): Participant + { + abort_unless($participant->status === 'waiting', 422); + + $participant->update([ + 'status' => 'joined', + 'joined_at' => now(), + 'left_at' => null, + ]); + + $this->updatePeakParticipants($participant->session); AuditLogger::record( - $session->owner_ref, + $participant->owner_ref, 'participant.joined', - $session->room->organization_id, - $userRef, + $participant->session->room->organization_id, + $participant->user_ref, Participant::class, $participant->id, ); - return $participant; + return $participant->fresh(); + } + + public function denyParticipant(Participant $participant): Participant + { + abort_unless($participant->status === 'waiting', 422); + + $participant->update([ + 'status' => 'removed', + 'left_at' => now(), + ]); + + AuditLogger::record( + $participant->owner_ref, + 'participant.removed', + $participant->session->room->organization_id, + $participant->user_ref, + Participant::class, + $participant->id, + ); + + return $participant->fresh(); } protected function findExistingParticipant(Session $session, ?string $userRef, ?string $email): ?Participant diff --git a/bootstrap/app.php b/bootstrap/app.php index d8ed3c1..e3d552c 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -51,6 +51,9 @@ return Application::configure(basePath: dirname(__DIR__)) \Illuminate\Support\Facades\Route::bind('registration', function (string $value) { return \App\Models\WebinarRegistration::where('uuid', $value)->firstOrFail(); }); + \Illuminate\Support\Facades\Route::bind('participant', function (string $value) { + return \App\Models\Participant::where('uuid', $value)->firstOrFail(); + }); }, ) ->withMiddleware(function (Middleware $middleware): void { diff --git a/database/migrations/2026_07_01_120000_create_meet_session_feedback_table.php b/database/migrations/2026_07_01_120000_create_meet_session_feedback_table.php new file mode 100644 index 0000000..9ed4698 --- /dev/null +++ b/database/migrations/2026_07_01_120000_create_meet_session_feedback_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('session_id')->constrained('meet_sessions')->cascadeOnDelete(); + $table->uuid('participant_uuid')->nullable()->index(); + $table->string('display_name')->nullable(); + $table->string('user_ref')->nullable()->index(); + $table->unsignedTinyInteger('rating'); + $table->unsignedTinyInteger('audio_rating')->nullable(); + $table->unsignedTinyInteger('video_rating')->nullable(); + $table->text('comment')->nullable(); + $table->timestamps(); + + $table->unique(['session_id', 'participant_uuid']); + }); + } + + public function down(): void + { + Schema::dropIfExists('meet_session_feedback'); + } +}; diff --git a/resources/js/meet-public.js b/resources/js/meet-public.js new file mode 100644 index 0000000..5fe419c --- /dev/null +++ b/resources/js/meet-public.js @@ -0,0 +1,70 @@ +import Alpine from 'alpinejs'; + +Alpine.data('waitingRoom', () => ({ + message: 'Waiting for the host to let you in…', + denied: false, + pollTimer: null, + + init() { + this.pollTimer = setInterval(() => this.poll(), 2500); + this.poll(); + }, + + destroy() { + if (this.pollTimer) { + clearInterval(this.pollTimer); + } + }, + + async poll() { + const url = this.$el.dataset.statusUrl; + if (!url) return; + + try { + const res = await fetch(url, { headers: { Accept: 'application/json' } }); + const data = await res.json(); + + if (data.redirect) { + window.location.href = data.redirect; + return; + } + + if (data.denied) { + this.denied = true; + this.message = 'The host declined your request to join.'; + clearInterval(this.pollTimer); + } + } catch { + // ignore transient errors + } + }, +})); + +Alpine.data('leaveFeedback', () => ({ + rating: 0, + audioRating: 0, + videoRating: 0, + hoverRating: 0, + hoverAudio: 0, + hoverVideo: 0, + comment: '', + + setRating(value) { + this.rating = value; + }, + + setAudioRating(value) { + this.audioRating = value; + }, + + setVideoRating(value) { + this.videoRating = value; + }, + + canSubmit() { + return this.rating > 0; + }, +})); + +window.Alpine = Alpine; +Alpine.start(); diff --git a/resources/js/meet-room.js b/resources/js/meet-room.js index fdf1d8d..400944f 100644 --- a/resources/js/meet-room.js +++ b/resources/js/meet-room.js @@ -85,6 +85,7 @@ function meetRoom() { whiteboardUrl: el.dataset.whiteboardUrl, whiteboardSaveUrl: el.dataset.whiteboardSaveUrl, csrf: el.dataset.csrf, + sessionUuid: el.dataset.sessionUuid, }; this.joinUrl = el.dataset.joinUrl || ''; @@ -334,7 +335,7 @@ function meetRoom() { }, hostParticipant() { - const hosts = this.sessionParticipants.filter((p) => p.role === 'host' || p.role === 'co_host'); + const hosts = this.inCallParticipants().filter((p) => p.role === 'host' || p.role === 'co_host'); if (hosts.length) { return hosts[0]; } @@ -352,13 +353,21 @@ function meetRoom() { }, participantCount() { - return Math.max(this.sessionParticipants.length, this.participants.length); + return this.inCallParticipants().length; + }, + + inCallParticipants() { + return this.sessionParticipants.filter((p) => p.status === 'joined'); + }, + + waitingParticipants() { + return this.sessionParticipants.filter((p) => p.status === 'waiting'); }, sortedParticipants() { const order = { host: 0, co_host: 1, panelist: 2, participant: 3, guest: 4, attendee: 5 }; - return [...this.sessionParticipants].sort((a, b) => { + return [...this.inCallParticipants()].sort((a, b) => { const diff = (order[a.role] ?? 9) - (order[b.role] ?? 9); return diff !== 0 ? diff : a.display_name.localeCompare(b.display_name); @@ -378,6 +387,26 @@ function meetRoom() { return labels[role] || 'Participant'; }, + async admitParticipant(uuid) { + if (!this.isHost || !this.config.sessionUuid) return; + + await fetch(`/room/${this.config.sessionUuid}/participants/${uuid}/admit`, { + method: 'POST', + headers: this.headers(), + }); + await this.poll(); + }, + + async denyParticipant(uuid) { + if (!this.isHost || !this.config.sessionUuid) return; + + await fetch(`/room/${this.config.sessionUuid}/participants/${uuid}/deny`, { + method: 'POST', + headers: this.headers(), + }); + await this.poll(); + }, + getOrCreateParticipantTile(participant) { const grid = document.getElementById('video-grid'); if (!grid) return null; diff --git a/resources/views/components/meet-kiosk-layout.blade.php b/resources/views/components/meet-kiosk-layout.blade.php new file mode 100644 index 0000000..b476195 --- /dev/null +++ b/resources/views/components/meet-kiosk-layout.blade.php @@ -0,0 +1,45 @@ +@props([ + 'title' => 'Ladill Meet', + 'organization' => null, + 'eyebrow' => null, +]) + + + + + + + + {{ $title }} + @include('partials.favicon') + + + @vite(['resources/css/app.css', 'resources/js/meet-public.js']) + @stack('head') + +merge(['class' => 'min-h-screen bg-slate-100 font-sans text-slate-900 antialiased']) }}> + + +
+ + @if ($organization) + @include('meet.partials.kiosk-brand', ['organization' => $organization]) + @else +
+ Ladill Meet +
+ @endif + +
+ @if ($eyebrow) +

{{ $eyebrow }}

+ @endif + {{ $slot }} +
+ + diff --git a/resources/views/meet/join/cancelled.blade.php b/resources/views/meet/join/cancelled.blade.php index aa08e37..54074da 100644 --- a/resources/views/meet/join/cancelled.blade.php +++ b/resources/views/meet/join/cancelled.blade.php @@ -1,16 +1,8 @@ - - - - - - Meeting cancelled · Ladill Meet - @include('partials.favicon') - @vite(['resources/css/app.css']) - - -
-

Meeting cancelled

-

{{ $room->title }} is no longer available.

+ +
+
+

{{ $room->title }}

+

This meeting has been cancelled by the host.

+
- - +
diff --git a/resources/views/meet/join/denied.blade.php b/resources/views/meet/join/denied.blade.php index 70dfb4b..635351a 100644 --- a/resources/views/meet/join/denied.blade.php +++ b/resources/views/meet/join/denied.blade.php @@ -1,17 +1,16 @@ - - - - - - Access denied · Ladill Meet - @include('partials.favicon') - @vite(['resources/css/app.css']) - - -
-

Unable to join

-

{{ $reason ?? 'You do not have access to this meeting.' }}

- Sign in + +
+
+
+ + + +
+

Unable to join

+

{{ $reason ?? 'You do not have access to this meeting.' }}

+ + Sign in with Ladill + +
- - +
diff --git a/resources/views/meet/join/passcode.blade.php b/resources/views/meet/join/passcode.blade.php index 0cbf896..70b8316 100644 --- a/resources/views/meet/join/passcode.blade.php +++ b/resources/views/meet/join/passcode.blade.php @@ -1,27 +1,24 @@ - - - - - - - Join · {{ $room->title }} · Ladill Meet - @include('partials.favicon') - @vite(['resources/css/app.css']) - - -
-

{{ $room->title }}

-

Enter the meeting passcode to continue.

+ +
+
+

{{ $room->title }}

+

This meeting is protected. Enter the passcode to continue.

-
- @csrf - - @error('passcode') -

{{ $message }}

- @enderror - -
+ @if ($errors->any()) +
+ {{ $errors->first() }} +
+ @endif + +
+ @csrf +
+ + +
+ +
+
- - +
diff --git a/resources/views/meet/join/show.blade.php b/resources/views/meet/join/show.blade.php index 161cb87..2d63d2d 100644 --- a/resources/views/meet/join/show.blade.php +++ b/resources/views/meet/join/show.blade.php @@ -1,46 +1,66 @@ - - - - - - - Join · {{ $room->title }} · Ladill Meet - @include('partials.favicon') - @vite(['resources/css/app.css']) - - -
-

{{ $room->title }}

-

Ready to join?

+ +
+
+

{{ $room->title }}

- @if ($errors->any()) -
- {{ $errors->first() }} -
- @endif + @if ($room->scheduled_at) +

+ {{ $room->scheduled_at->timezone($room->timezone)->format('l, j F Y · g:i A T') }} +

+ @endif -
- @csrf - @guest -
- - + @if ($room->host_user_ref && ($host = \App\Models\User::query()->where('public_id', $room->host_user_ref)->first())) +

Hosted by {{ $host->name }}

+ @endif + + @if ($room->description) +

{{ $room->description }}

+ @endif + + @if ($errors->any()) +
+ {{ $errors->first() }}
- @if ($isWebinar ?? false) -
- - -
- @endif - @endguest - - + @endif - @auth -

Joining as {{ auth()->user()->name }}

- @endauth +
+ @csrf + + @guest + + + + @if ($isWebinar ?? false) + + + @endif + @endguest + + @auth +
+ Joining as {{ auth()->user()->name }} +
+ @endauth + + @if ($room->setting('waiting_room', true) && ! ($user && $user->ownerRef() === $room->host_user_ref)) +

The host will admit you when the meeting is ready.

+ @endif + + +
+ + @guest +

+ Have a Ladill account? + Sign in +

+ @endguest +
- - + diff --git a/resources/views/meet/join/waiting.blade.php b/resources/views/meet/join/waiting.blade.php new file mode 100644 index 0000000..7f74678 --- /dev/null +++ b/resources/views/meet/join/waiting.blade.php @@ -0,0 +1,29 @@ + +
+
+
+ + + +
+
+ + + +
+ +

{{ $room->title }}

+

Hi {{ $participant->display_name }}, please wait.

+

+ +

You will join automatically once the host admits you.

+ + + Back to join page + +
+
+
diff --git a/resources/views/meet/left/show.blade.php b/resources/views/meet/left/show.blade.php new file mode 100644 index 0000000..7ece8a7 --- /dev/null +++ b/resources/views/meet/left/show.blade.php @@ -0,0 +1,91 @@ + +
+
+ @csrf + +
+

You left the meeting

+

{{ $room->title }}

+ @if (! empty($feedback['display_name'])) +

Thanks, {{ $feedback['display_name'] }}.

+ @endif +
+ +
+

How was the meeting overall?

+
+ @foreach (range(1, 5) as $star) + + @endforeach +
+ +
+ +
+
+

Audio quality

+
+ @foreach (range(1, 5) as $star) + + @endforeach +
+ +
+
+

Video quality

+
+ @foreach (range(1, 5) as $star) + + @endforeach +
+ +
+
+ +
+ + +
+ +
+ + + Skip + +
+
+
+
diff --git a/resources/views/meet/left/thanks.blade.php b/resources/views/meet/left/thanks.blade.php new file mode 100644 index 0000000..29cde7d --- /dev/null +++ b/resources/views/meet/left/thanks.blade.php @@ -0,0 +1,27 @@ + +
+
+
+ + + +
+

+ @if (session('rated')) + Thanks for your feedback + @else + You have left the meeting + @endif +

+

{{ $room->title }}

+ +
+ @auth + Back to Meet + @else + Rejoin meeting + @endauth +
+
+
+
diff --git a/resources/views/meet/partials/kiosk-brand.blade.php b/resources/views/meet/partials/kiosk-brand.blade.php new file mode 100644 index 0000000..5a1b61f --- /dev/null +++ b/resources/views/meet/partials/kiosk-brand.blade.php @@ -0,0 +1,8 @@ +@php + use App\Support\OrganizationBranding; +@endphp +
+ {{ OrganizationBranding::logoAlt($organization) }} +
diff --git a/resources/views/meet/room/show.blade.php b/resources/views/meet/room/show.blade.php index 4440413..1afecd8 100644 --- a/resources/views/meet/room/show.blade.php +++ b/resources/views/meet/room/show.blade.php @@ -13,7 +13,7 @@ use App\Models\User; use App\Services\Meet\ParticipantPresenter; - $joined = $session->participants->where('status', 'joined'); + $joined = $session->participants->whereIn('status', ['joined', 'waiting']); $participantAvatars = ParticipantPresenter::avatarMap($joined, $room->host_user_ref); $joinedParticipants = $joined ->map(fn ($p) => ParticipantPresenter::toArray($p, $participantAvatars, $room->host_user_ref)) @@ -61,6 +61,7 @@ data-room-title="{{ $room->title }}" data-passcode="{{ $room->passcode ?? '' }}" data-csrf="{{ csrf_token() }}" + data-session-uuid="{{ $session->uuid }}" data-initial-participants='@json($joinedParticipants)' data-room-host='@json($roomHost)' hidden>
@@ -384,7 +385,28 @@
-