Deploy Ladill Meet / deploy (push) Successful in 42s
Enforce invite-only room access, validate invite emails on create, assign attendee role for listener invitations, and add invite UI on the room show page. Redesign meetings, rooms, conferences, and webinars indexes to match the Events hero, stats cards, and icon list layout. Co-authored-by: Cursor <cursoragent@cursor.com>
217 lines
8.2 KiB
PHP
217 lines
8.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Meet;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Controllers\Meet\Concerns\BuildsMeetIndexPage;
|
|
use App\Http\Controllers\Meet\Concerns\ScopesToAccount;
|
|
use App\Models\Member;
|
|
use App\Models\Room;
|
|
use App\Models\User;
|
|
use App\Services\Meet\CalendarService;
|
|
use App\Services\Meet\ConferenceService;
|
|
use App\Services\Meet\InvitationService;
|
|
use App\Services\Meet\RoomService;
|
|
use App\Services\Meet\SessionService;
|
|
use App\Services\Meet\TownHallService;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\View\View;
|
|
|
|
class ConferenceController extends Controller
|
|
{
|
|
use BuildsMeetIndexPage, ScopesToAccount;
|
|
|
|
public function __construct(
|
|
protected RoomService $rooms,
|
|
protected SessionService $sessions,
|
|
protected InvitationService $invitations,
|
|
protected CalendarService $calendar,
|
|
protected TownHallService $townHalls,
|
|
protected ConferenceService $conferences,
|
|
) {}
|
|
|
|
public function index(Request $request): View
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.view');
|
|
$organization = $this->organization($request);
|
|
$owner = $this->ownerRef($request);
|
|
|
|
$query = Room::owned($owner)
|
|
->where('organization_id', $organization->id)
|
|
->where('type', 'town_hall');
|
|
|
|
$this->scopeToBranch($request, $query);
|
|
|
|
$conferences = $query->orderByDesc('scheduled_at')->paginate(20);
|
|
|
|
$stats = $this->meetIndexStats($request, $query, 'Conferences', 'Live now');
|
|
$items = $this->meetIndexItems($conferences, fn (Room $room) => route('meet.conferences.show', $room));
|
|
|
|
return view('meet.conferences.index', compact('conferences', 'organization', 'stats', 'items'));
|
|
}
|
|
|
|
public function create(Request $request): View
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.create');
|
|
$organization = $this->organization($request);
|
|
|
|
return view('meet.conferences.create', [
|
|
'organization' => $organization,
|
|
'timezones' => timezone_identifiers_list(),
|
|
'defaultSettings' => config('meet.default_settings'),
|
|
]);
|
|
}
|
|
|
|
public function store(Request $request): RedirectResponse
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.create');
|
|
$organization = $this->organization($request);
|
|
|
|
$validated = $request->validate([
|
|
'title' => ['required', 'string', 'max:255'],
|
|
'description' => ['nullable', 'string', 'max:2000'],
|
|
'scheduled_at' => ['required', 'date'],
|
|
'duration_minutes' => ['nullable', 'integer', 'min:15', 'max:480'],
|
|
'timezone' => ['nullable', 'timezone'],
|
|
'passcode' => ['nullable', 'string', 'max:20'],
|
|
'waiting_room' => ['sometimes', 'boolean'],
|
|
'join_before_host' => ['sometimes', 'boolean'],
|
|
'mute_on_join' => ['sometimes', 'boolean'],
|
|
'auto_record' => ['sometimes', 'boolean'],
|
|
'live_captions' => ['sometimes', 'boolean'],
|
|
'green_room' => ['sometimes', 'boolean'],
|
|
'practice_mode' => ['sometimes', 'boolean'],
|
|
'panel_discussions' => ['sometimes', 'boolean'],
|
|
]);
|
|
|
|
$settings = [
|
|
'waiting_room' => $request->boolean('waiting_room', true),
|
|
'join_before_host' => $request->boolean('join_before_host'),
|
|
'mute_on_join' => $request->boolean('mute_on_join', true),
|
|
'auto_record' => $request->boolean('auto_record'),
|
|
'live_captions' => $request->boolean('live_captions'),
|
|
'green_room' => $request->boolean('green_room', true),
|
|
'practice_mode' => $request->boolean('practice_mode'),
|
|
'panel_discussions' => $request->boolean('panel_discussions'),
|
|
'stage_layout' => 'plenary',
|
|
'panels' => [],
|
|
'stage_mode' => true,
|
|
'presenter_refs' => [],
|
|
];
|
|
|
|
$data = array_merge($validated, [
|
|
'settings' => $settings,
|
|
'timezone' => $validated['timezone'] ?? $organization->timezone,
|
|
'type' => 'town_hall',
|
|
'status' => 'scheduled',
|
|
]);
|
|
|
|
$room = $this->rooms->create($request->user(), $organization, $data);
|
|
|
|
if ($room->scheduled_at) {
|
|
$this->calendar->syncRoomCreated($room, $request->user());
|
|
}
|
|
|
|
return redirect()->route('meet.conferences.show', $room)->with('success', 'Conference created.');
|
|
}
|
|
|
|
public function show(Request $request, Room $room): View
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.view');
|
|
$this->authorizeOwner($request, $room);
|
|
abort_unless($room->isConference(), 404);
|
|
|
|
$room->load(['sessions.participants', 'invitations', 'sessions.recordings', 'sessions.aiSummaries', 'sessionFiles']);
|
|
|
|
return view('meet.conferences.show', compact('room'));
|
|
}
|
|
|
|
public function updateSpeakers(Request $request, Room $room): RedirectResponse
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.manage');
|
|
$this->authorizeOwner($request, $room);
|
|
abort_unless($room->isConference(), 404);
|
|
|
|
if ($room->isEventsLinked()) {
|
|
if ($request->has('panel_discussions')) {
|
|
$settings = array_merge($room->settings ?? [], [
|
|
'panel_discussions' => $request->boolean('panel_discussions'),
|
|
]);
|
|
$room->update(['settings' => $settings]);
|
|
}
|
|
|
|
return redirect()->route('meet.conferences.show', $room)->with('success', 'Conference settings updated.');
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'presenter_refs' => ['nullable', 'array'],
|
|
'presenter_refs.*' => ['string', 'max:64'],
|
|
'invite_emails' => ['nullable', 'string', 'max:2000'],
|
|
'panel_discussions' => ['sometimes', 'boolean'],
|
|
]);
|
|
|
|
$allowedRefs = Member::owned($this->ownerRef($request))
|
|
->where('organization_id', $room->organization_id)
|
|
->pluck('user_ref')
|
|
->all();
|
|
|
|
$presenterRefs = collect($validated['presenter_refs'] ?? [])
|
|
->filter()
|
|
->unique()
|
|
->intersect($allowedRefs)
|
|
->values()
|
|
->all();
|
|
|
|
$this->conferences->syncPresenterRefs($room, $presenterRefs);
|
|
|
|
if ($request->has('panel_discussions')) {
|
|
$settings = array_merge($room->settings ?? [], [
|
|
'panel_discussions' => $request->boolean('panel_discussions'),
|
|
]);
|
|
$room->update(['settings' => $settings]);
|
|
}
|
|
|
|
$inviteCount = 0;
|
|
if ($emails = trim((string) ($validated['invite_emails'] ?? ''))) {
|
|
$invites = collect(preg_split('/[\s,;]+/', $emails))
|
|
->filter()
|
|
->map(fn ($email) => ['email' => $email, 'role' => 'panelist'])
|
|
->all();
|
|
$inviteCount = count($invites);
|
|
$this->invitations->inviteToRoom($room, $invites, $request->user());
|
|
}
|
|
|
|
$message = $inviteCount > 0
|
|
? 'Speakers updated and '.$inviteCount.' invitation(s) sent.'
|
|
: 'Speakers updated.';
|
|
|
|
return redirect()->route('meet.conferences.show', $room)->with('success', $message);
|
|
}
|
|
|
|
public function start(Request $request, Room $room): RedirectResponse
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.host');
|
|
$this->authorizeOwner($request, $room);
|
|
abort_unless($room->isConference(), 404);
|
|
|
|
if ($room->status === 'cancelled') {
|
|
return redirect()->route('meet.conferences.show', $room)
|
|
->withErrors(['start' => 'Conference was cancelled.']);
|
|
}
|
|
|
|
if ($room->setting('green_room')) {
|
|
$session = $this->townHalls->startGreenRoom($room, $request->user());
|
|
} else {
|
|
$session = $this->sessions->start($room, $request->user());
|
|
}
|
|
|
|
$participant = $this->sessions->joinedParticipantForUser($session, $request->user());
|
|
abort_unless($participant, 500, 'Unable to join as host.');
|
|
|
|
$this->sessions->rememberParticipantSession($request, $participant, $session);
|
|
|
|
return redirect()->route('meet.room', $session);
|
|
}
|
|
}
|