Files
ladill-meet/app/Http/Controllers/Meet/SpaceController.php
T
isaaccladandCursor 46d630221a
Deploy Ladill Meet / deploy (push) Successful in 42s
Wire space listener email invites and restyle Meet index pages.
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>
2026-07-04 02:07:27 +00:00

194 lines
7.1 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\Services\Meet\CalendarService;
use App\Services\Meet\InvitationService;
use App\Services\Meet\RoomService;
use App\Services\Meet\SessionService;
use App\Services\Meet\SpaceService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class SpaceController extends Controller
{
use BuildsMeetIndexPage, ScopesToAccount;
public function __construct(
protected RoomService $rooms,
protected SessionService $sessions,
protected InvitationService $invitations,
protected CalendarService $calendar,
protected SpaceService $spaces,
) {}
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', 'space');
$this->scopeToBranch($request, $query);
$spaces = $query->orderByDesc('updated_at')->paginate(20);
$stats = $this->meetIndexStats($request, $query, 'Rooms', 'Live now');
$items = $this->meetIndexItems($spaces, fn (Room $room) => route('meet.spaces.show', $room));
return view('meet.spaces.index', compact('spaces', 'organization', 'stats', 'items'));
}
public function create(Request $request): View
{
$this->authorizeAbility($request, 'meetings.create');
$organization = $this->organization($request);
$owner = $this->ownerRef($request);
$members = Member::owned($owner)
->where('organization_id', $organization->id)
->where('user_ref', '!=', $owner)
->orderBy('user_ref')
->get();
return view('meet.spaces.create', [
'organization' => $organization,
'members' => $members,
'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' => ['nullable', 'date'],
'duration_minutes' => ['nullable', 'integer', 'min:15', 'max:480'],
'timezone' => ['nullable', 'timezone'],
'passcode' => ['nullable', 'string', 'max:20'],
'invite_emails' => ['nullable', 'string', 'max:2000', function (string $attribute, mixed $value, \Closure $fail): void {
if (! is_string($value) || trim($value) === '') {
return;
}
foreach (preg_split('/[\s,;]+/', $value) as $email) {
$email = trim($email);
if ($email !== '' && ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
$fail("Invalid email address: {$email}");
}
}
}],
'speaker_refs' => ['nullable', 'array'],
'speaker_refs.*' => ['string', 'max:64'],
'invite_only' => ['sometimes', 'boolean'],
'mute_listeners' => ['sometimes', 'boolean'],
]);
$speakerRefs = collect($validated['speaker_refs'] ?? [])
->filter()
->unique()
->values()
->all();
$settings = [
'waiting_room' => false,
'join_before_host' => false,
'mute_on_join' => $request->boolean('mute_listeners', true),
'video_off_on_join' => true,
'audio_only' => true,
'space_mode' => true,
'stage_mode' => true,
'invite_only' => $request->boolean('invite_only', true),
'speaker_refs' => $speakerRefs,
];
$data = array_merge($validated, [
'settings' => $settings,
'timezone' => $validated['timezone'] ?? $organization->timezone,
'type' => 'space',
'status' => empty($validated['scheduled_at']) ? 'scheduled' : 'scheduled',
]);
unset($data['speaker_refs'], $data['invite_only'], $data['mute_listeners']);
$room = $this->rooms->create($request->user(), $organization, $data);
if ($emails = trim((string) ($validated['invite_emails'] ?? ''))) {
$invites = collect(preg_split('/[\s,;]+/', $emails))
->filter()
->map(fn ($email) => ['email' => $email, 'role' => 'attendee'])
->all();
$this->invitations->inviteToRoom($room, $invites, $request->user());
}
if (! empty($room->scheduled_at)) {
$this->calendar->syncRoomCreated($room, $request->user());
}
return redirect()->route('meet.spaces.show', $room)->with('success', $this->spaceCreatedMessage($room, $validated['invite_emails'] ?? ''));
}
protected function spaceCreatedMessage(Room $room, string $inviteEmails): string
{
$count = count(array_filter(preg_split('/[\s,;]+/', trim($inviteEmails)) ?: []));
if ($count === 0) {
return 'Room created.';
}
return "Room created. {$count} listener invitation(s) sent.";
}
public function show(Request $request, Room $room): View
{
$this->authorizeAbility($request, 'meetings.view');
$this->authorizeOwner($request, $room);
abort_unless($room->isSpace(), 404);
$room->load(['sessions.participants', 'invitations' => fn ($q) => $q->orderByDesc('created_at'), 'sessions.recordings', 'sessions.aiSummaries', 'sessionFiles']);
$speakerRefs = (array) $room->setting('speaker_refs', []);
$speakers = Member::owned($this->ownerRef($request))
->where('organization_id', $room->organization_id)
->whereIn('user_ref', $speakerRefs)
->get();
return view('meet.spaces.show', compact('room', 'speakers'));
}
public function start(Request $request, Room $room): RedirectResponse
{
$this->authorizeAbility($request, 'meetings.host');
$this->authorizeOwner($request, $room);
abort_unless($room->isSpace(), 404);
if ($room->status === 'cancelled') {
return redirect()->route('meet.spaces.show', $room)
->withErrors(['start' => 'Room was closed.']);
}
$session = $this->sessions->start($room, $request->user());
$participant = $this->sessions->joinedParticipantForUser($session, $request->user());
abort_unless($participant, 500, 'Unable to open room as host.');
$this->sessions->rememberParticipantSession($request, $participant, $session);
return redirect()->route('meet.room', $session);
}
}