Wire space listener email invites and restyle Meet index pages.
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>
This commit is contained in:
isaacclad
2026-07-04 02:07:27 +00:00
co-authored by Cursor
parent 21092015b8
commit 46d630221a
17 changed files with 463 additions and 92 deletions
+32 -6
View File
@@ -3,6 +3,7 @@
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;
@@ -17,7 +18,7 @@ use Illuminate\View\View;
class SpaceController extends Controller
{
use ScopesToAccount;
use BuildsMeetIndexPage, ScopesToAccount;
public function __construct(
protected RoomService $rooms,
@@ -41,7 +42,10 @@ class SpaceController extends Controller
$spaces = $query->orderByDesc('updated_at')->paginate(20);
return view('meet.spaces.index', compact('spaces', 'organization'));
$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
@@ -76,7 +80,18 @@ class SpaceController extends Controller
'duration_minutes' => ['nullable', 'integer', 'min:15', 'max:480'],
'timezone' => ['nullable', 'timezone'],
'passcode' => ['nullable', 'string', 'max:20'],
'invite_emails' => ['nullable', 'string'],
'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'],
@@ -115,7 +130,7 @@ class SpaceController 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' => 'attendee'])
->all();
$this->invitations->inviteToRoom($room, $invites, $request->user());
}
@@ -124,7 +139,18 @@ class SpaceController extends Controller
$this->calendar->syncRoomCreated($room, $request->user());
}
return redirect()->route('meet.spaces.show', $room)->with('success', 'Room created.');
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
@@ -133,7 +159,7 @@ class SpaceController extends Controller
$this->authorizeOwner($request, $room);
abort_unless($room->isSpace(), 404);
$room->load(['sessions.participants', 'invitations', 'sessions.recordings', 'sessions.aiSummaries', 'sessionFiles']);
$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))