Improve conferences, guest entry, Afia, and cross-app scheduling.
Deploy Ladill Meet / deploy (push) Successful in 50s
Deploy Ladill Meet / deploy (push) Successful in 50s
Route guests through silent SSO to the Meet product page, fix Afia context query, add conference green-room UX and copy, and extend service API for Care/CRM/Invoice calendar integration. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -52,7 +52,7 @@ class AiController extends Controller
|
||||
'meetings_upcoming' => Room::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('status', 'scheduled')
|
||||
->where('starts_at', '>=', now())
|
||||
->where('scheduled_at', '>=', now())
|
||||
->count(),
|
||||
'recordings_total' => Recording::owned($owner)
|
||||
->whereHas('session.room', fn ($q) => $q->where('organization_id', $organization->id))
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Meet;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Meet\Concerns\ScopesToAccount;
|
||||
use App\Models\Member;
|
||||
use App\Models\Room;
|
||||
use App\Models\Template;
|
||||
use App\Services\Meet\CalendarService;
|
||||
@@ -56,9 +57,16 @@ class ConferenceController extends Controller
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$members = Member::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->where('user_ref', '!=', $this->ownerRef($request))
|
||||
->orderBy('user_ref')
|
||||
->get();
|
||||
|
||||
return view('meet.conferences.create', [
|
||||
'organization' => $organization,
|
||||
'templates' => $templates,
|
||||
'members' => $members,
|
||||
'timezones' => timezone_identifiers_list(),
|
||||
'defaultSettings' => config('meet.default_settings'),
|
||||
]);
|
||||
@@ -78,6 +86,8 @@ class ConferenceController extends Controller
|
||||
'passcode' => ['nullable', 'string', 'max:20'],
|
||||
'template_id' => ['nullable', 'integer', 'exists:meet_templates,id'],
|
||||
'invite_emails' => ['nullable', 'string'],
|
||||
'presenter_refs' => ['nullable', 'array'],
|
||||
'presenter_refs.*' => ['string', 'max:64'],
|
||||
'waiting_room' => ['sometimes', 'boolean'],
|
||||
'join_before_host' => ['sometimes', 'boolean'],
|
||||
'mute_on_join' => ['sometimes', 'boolean'],
|
||||
@@ -87,6 +97,12 @@ class ConferenceController extends Controller
|
||||
'practice_mode' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$presenterRefs = collect($validated['presenter_refs'] ?? [])
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$settings = [
|
||||
'waiting_room' => $request->boolean('waiting_room', true),
|
||||
'join_before_host' => $request->boolean('join_before_host'),
|
||||
@@ -96,6 +112,7 @@ class ConferenceController extends Controller
|
||||
'green_room' => $request->boolean('green_room', true),
|
||||
'practice_mode' => $request->boolean('practice_mode'),
|
||||
'stage_mode' => true,
|
||||
'presenter_refs' => $presenterRefs,
|
||||
];
|
||||
|
||||
$data = array_merge($validated, [
|
||||
@@ -115,7 +132,7 @@ class ConferenceController extends Controller
|
||||
if ($emails = trim((string) ($validated['invite_emails'] ?? ''))) {
|
||||
$invites = collect(preg_split('/[\s,;]+/', $emails))
|
||||
->filter()
|
||||
->map(fn ($email) => ['email' => $email])
|
||||
->map(fn ($email) => ['email' => $email, 'role' => 'panelist'])
|
||||
->all();
|
||||
$this->invitations->inviteToRoom($room, $invites, $request->user());
|
||||
}
|
||||
@@ -135,7 +152,13 @@ class ConferenceController extends Controller
|
||||
|
||||
$room->load(['sessions.participants', 'invitations', 'sessions.recordings', 'sessions.aiSummaries', 'sessionFiles']);
|
||||
|
||||
return view('meet.conferences.show', compact('room'));
|
||||
$presenterRefs = (array) $room->setting('presenter_refs', []);
|
||||
$speakers = Member::owned($this->ownerRef($request))
|
||||
->where('organization_id', $room->organization_id)
|
||||
->whereIn('user_ref', $presenterRefs)
|
||||
->get();
|
||||
|
||||
return view('meet.conferences.show', compact('room', 'speakers'));
|
||||
}
|
||||
|
||||
public function start(Request $request, Room $room): RedirectResponse
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\Participant;
|
||||
use App\Models\Room;
|
||||
use App\Models\Session;
|
||||
use App\Services\Meet\ConferenceService;
|
||||
use App\Services\Meet\InvitationService;
|
||||
use App\Services\Meet\SecurityPolicyService;
|
||||
use App\Services\Meet\SessionService;
|
||||
@@ -24,6 +25,7 @@ class JoinController extends Controller
|
||||
protected SecurityPolicyService $security,
|
||||
protected WebinarService $webinars,
|
||||
protected SpaceService $spaces,
|
||||
protected ConferenceService $conferences,
|
||||
) {}
|
||||
|
||||
public function show(Request $request, Room $room): View|RedirectResponse
|
||||
@@ -120,9 +122,11 @@ class JoinController extends Controller
|
||||
$displayName = $user?->name ?? ($validated['display_name'] ?? 'Guest');
|
||||
$email = $user?->email ?? ($validated['email'] ?? null);
|
||||
|
||||
$kind = $room->isConference() ? 'conference' : 'meeting';
|
||||
|
||||
[$allowed, $reason] = $this->security->canJoin($room, $user, $email ?? $displayName.'@guest.local');
|
||||
if (! $allowed && $reason !== 'passcode_required') {
|
||||
return back()->withErrors(['join' => $reason ?? 'Unable to join meeting.']);
|
||||
return back()->withErrors(['join' => $reason ?? "Unable to join {$kind}."]);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -136,20 +140,20 @@ class JoinController extends Controller
|
||||
if (! $room->activeSession() && $user && $user->ownerRef() === $room->host_user_ref) {
|
||||
$session = $this->sessions->start($room, $user);
|
||||
} elseif (! $room->activeSession()) {
|
||||
return back()->withErrors(['join' => 'Meeting has not started yet. Please wait for the host.']);
|
||||
return back()->withErrors(['join' => ucfirst($kind).' has not started yet. Please wait for the host.']);
|
||||
} else {
|
||||
$session = $room->activeSession();
|
||||
}
|
||||
} else {
|
||||
if (! $room->activeSession()) {
|
||||
return back()->withErrors(['join' => 'Meeting has not started yet. Please wait for the host.']);
|
||||
return back()->withErrors(['join' => ucfirst($kind).' has not started yet. Please wait for the host.']);
|
||||
}
|
||||
|
||||
$session = $room->activeSession();
|
||||
}
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
|
||||
if (in_array($e->getStatusCode(), [402, 503], true)) {
|
||||
return back()->withErrors(['join' => $e->getMessage() ?: 'Unable to start meeting.']);
|
||||
return back()->withErrors(['join' => $e->getMessage() ?: "Unable to start {$kind}."]);
|
||||
}
|
||||
|
||||
throw $e;
|
||||
@@ -159,6 +163,8 @@ class JoinController extends Controller
|
||||
|
||||
if ($room->isSpace()) {
|
||||
$role = $this->spaces->resolveJoinRole($room, $user, $role);
|
||||
} elseif ($room->isConference() && $role !== 'host') {
|
||||
$role = $this->conferences->resolveJoinRole($room, $user, $email, $role);
|
||||
} elseif ($room->isWebinar() && $role !== 'host') {
|
||||
$token = $request->session()->get("meet.webinar.{$room->uuid}");
|
||||
$reg = $token
|
||||
@@ -174,10 +180,20 @@ class JoinController extends Controller
|
||||
$displayName = $reg->display_name;
|
||||
}
|
||||
|
||||
if ($room->isConference() && ! $this->conferences->canJoinSession($room, $session, $role)) {
|
||||
return back()->withErrors([
|
||||
'join' => 'The conference has not gone live yet. Assigned speakers can join from the green room link when the host starts.',
|
||||
]);
|
||||
}
|
||||
|
||||
$joinStatus = $room->requiresWaitingRoom($user, $role) ? 'waiting' : 'joined';
|
||||
|
||||
$participant = $this->sessions->joinParticipant($session, $user, $role, $displayName, $email, $joinStatus);
|
||||
|
||||
if ($room->isConference() && $session->session_mode === 'green_room') {
|
||||
$this->conferences->registerPresenter($session, $user, $role);
|
||||
}
|
||||
|
||||
app(InvitationService::class)->markAcceptedOnJoin($room, $user, $email);
|
||||
|
||||
$this->sessions->rememberParticipantSession($request, $participant, $session);
|
||||
|
||||
@@ -282,8 +282,32 @@ class MeetingFeaturesController extends Controller
|
||||
{
|
||||
$this->hostOnly($request, $session);
|
||||
$live = $this->townHall->goLive($session, $request->user());
|
||||
$participant = app(SessionService::class)->joinedParticipantForUser($live, $request->user());
|
||||
|
||||
return response()->json(['session_uuid' => $live->uuid, 'mode' => 'live']);
|
||||
if ($participant) {
|
||||
app(SessionService::class)->rememberParticipantSession($request, $participant, $live);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'session_uuid' => $live->uuid,
|
||||
'mode' => 'live',
|
||||
'room_url' => route('meet.room', $live),
|
||||
]);
|
||||
}
|
||||
|
||||
public function addPresenter(Request $request, Session $session): JsonResponse
|
||||
{
|
||||
$this->hostOnly($request, $session);
|
||||
abort_unless($session->room->isTownHall(), 422);
|
||||
|
||||
$validated = $request->validate(['user_ref' => ['required', 'string', 'max:64']]);
|
||||
|
||||
$updated = $this->townHall->addPresenter($session, $validated['user_ref']);
|
||||
|
||||
return response()->json([
|
||||
'session' => $updated,
|
||||
'presenter_refs' => $updated->presenter_refs,
|
||||
]);
|
||||
}
|
||||
|
||||
public function handoffCoHost(Request $request, Session $session): JsonResponse
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Meet;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Participant;
|
||||
use App\Models\Recording;
|
||||
use App\Models\Room;
|
||||
use App\Models\Session;
|
||||
use App\Services\Meet\ChatService;
|
||||
use App\Services\Meet\Media\MediaProviderInterface;
|
||||
@@ -37,8 +38,11 @@ class MeetingRoomController extends Controller
|
||||
return redirect()->route('meet.ended', $session);
|
||||
}
|
||||
|
||||
$room = $session->room;
|
||||
$kind = $room->isConference() ? 'conference' : 'meeting';
|
||||
|
||||
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
|
||||
abort_unless($participantUuid, 403, 'Please join the meeting first.');
|
||||
abort_unless($participantUuid, 403, "Please join the {$kind} first.");
|
||||
|
||||
$participant = Participant::where('uuid', $participantUuid)
|
||||
->where('session_id', $session->id)
|
||||
@@ -48,7 +52,7 @@ class MeetingRoomController extends Controller
|
||||
return redirect()->route('meet.join.waiting', $session->room);
|
||||
}
|
||||
|
||||
abort_unless($participant->status === 'joined', 403, 'You are not in this meeting.');
|
||||
abort_unless($participant->status === 'joined', 403, "You are not in this {$kind}.");
|
||||
|
||||
$token = $this->media->isConfigured()
|
||||
? $this->sessions->generateMediaToken($participant)
|
||||
@@ -72,6 +76,10 @@ class MeetingRoomController extends Controller
|
||||
'activeRecording' => $session->recordings()->where('status', 'recording')->first(),
|
||||
'watermark' => $room->organization?->securitySetting('watermark_enabled', false),
|
||||
'isWebinar' => $room->isWebinar() || $room->isConference() || $room->isSpace(),
|
||||
'isConference' => $room->isConference(),
|
||||
'isGreenRoom' => $session->session_mode === 'green_room',
|
||||
'sessionMode' => $session->session_mode,
|
||||
'presenterRefs' => (array) $session->presenter_refs,
|
||||
'isAudioOnly' => $isAudioOnly,
|
||||
'isSpace' => $room->isSpace(),
|
||||
'canPublish' => $room->isSpace()
|
||||
@@ -91,10 +99,43 @@ class MeetingRoomController extends Controller
|
||||
$this->sessions->end($session, $participant->user_ref ?? $participant->uuid);
|
||||
|
||||
$room = $session->room;
|
||||
$route = $room->isWebinar() ? 'meet.webinars.show' : 'meet.rooms.show';
|
||||
|
||||
return redirect()->route($route, $room)
|
||||
->with('success', $room->isWebinar() ? 'Webinar ended.' : 'Meeting ended.');
|
||||
return redirect()->route($this->roomShowRoute($room), $room)
|
||||
->with('success', $this->roomEndedMessage($room));
|
||||
}
|
||||
|
||||
protected function roomShowRoute(Room $room): string
|
||||
{
|
||||
if ($room->isWebinar()) {
|
||||
return 'meet.webinars.show';
|
||||
}
|
||||
|
||||
if ($room->isConference()) {
|
||||
return 'meet.conferences.show';
|
||||
}
|
||||
|
||||
if ($room->isSpace()) {
|
||||
return 'meet.spaces.show';
|
||||
}
|
||||
|
||||
return 'meet.rooms.show';
|
||||
}
|
||||
|
||||
protected function roomEndedMessage(Room $room): string
|
||||
{
|
||||
if ($room->isWebinar()) {
|
||||
return 'Webinar ended.';
|
||||
}
|
||||
|
||||
if ($room->isConference()) {
|
||||
return 'Conference ended.';
|
||||
}
|
||||
|
||||
if ($room->isSpace()) {
|
||||
return 'Room closed.';
|
||||
}
|
||||
|
||||
return 'Meeting ended.';
|
||||
}
|
||||
|
||||
public function leave(Request $request, Session $session): RedirectResponse
|
||||
|
||||
Reference in New Issue
Block a user