Limit audio rooms to Twitter Spaces features and fix live toolbar UX.
Deploy Ladill Meet / deploy (push) Successful in 1m3s

Block breakouts, chat, Q&A, and other meeting-only tools in spaces, hide video controls for audio-only rooms, and stabilize mobile nav and toolbar layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-04 00:25:40 +00:00
co-authored by Cursor
parent b087b7e95a
commit c51e59945e
10 changed files with 310 additions and 35 deletions
@@ -16,6 +16,7 @@ use App\Services\Meet\LiveStreamService;
use App\Services\Meet\PollService;
use App\Services\Meet\QaService;
use App\Services\Meet\SessionService;
use App\Services\Meet\SpaceService;
use App\Services\Meet\StageLayoutService;
use App\Services\Meet\TownHallService;
use App\Services\Meet\WhiteboardService;
@@ -42,6 +43,7 @@ class MeetingFeaturesController extends Controller
public function submitQuestion(Request $request, Session $session): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'qa');
$participant = $this->participant($request, $session);
$validated = $request->validate(['question' => ['required', 'string', 'max:1000']]);
$question = $this->qa->submit($session, $participant, $validated['question']);
@@ -51,6 +53,7 @@ class MeetingFeaturesController extends Controller
public function approveQuestion(Request $request, Session $session, QaQuestion $question): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'qa');
$this->hostOnly($request, $session);
abort_unless($question->session_id === $session->id, 404);
@@ -59,6 +62,7 @@ class MeetingFeaturesController extends Controller
public function answerQuestion(Request $request, Session $session, QaQuestion $question): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'qa');
$participant = $this->hostOnly($request, $session);
abort_unless($question->session_id === $session->id, 404);
$validated = $request->validate(['answer' => ['required', 'string', 'max:2000']]);
@@ -70,6 +74,7 @@ class MeetingFeaturesController extends Controller
public function upvoteQuestion(Request $request, Session $session, QaQuestion $question): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'qa');
$this->participant($request, $session);
abort_unless($question->session_id === $session->id, 404);
@@ -80,6 +85,7 @@ class MeetingFeaturesController extends Controller
public function createPoll(Request $request, Session $session): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'polls');
$this->hostOnly($request, $session);
$validated = $request->validate([
'question' => ['required', 'string', 'max:500'],
@@ -94,6 +100,7 @@ class MeetingFeaturesController extends Controller
public function votePoll(Request $request, Session $session, MeetPoll $poll): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'polls');
$participant = $this->participant($request, $session);
abort_unless($poll->session_id === $session->id, 404);
$validated = $request->validate(['option_index' => ['required', 'integer', 'min:0']]);
@@ -104,6 +111,7 @@ class MeetingFeaturesController extends Controller
public function closePoll(Request $request, Session $session, MeetPoll $poll): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'polls');
$this->hostOnly($request, $session);
abort_unless($poll->session_id === $session->id, 404);
@@ -114,6 +122,7 @@ class MeetingFeaturesController extends Controller
public function createBreakouts(Request $request, Session $session): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'breakouts');
$this->hostOnly($request, $session);
$validated = $request->validate([
'count' => ['required', 'integer', 'min:1', 'max:20'],
@@ -131,6 +140,7 @@ class MeetingFeaturesController extends Controller
public function moveToBreakout(Request $request, Session $session, BreakoutRoom $breakout): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'breakouts');
$participant = $this->participant($request, $session);
abort_unless($breakout->session_id === $session->id, 404);
$this->breakouts->moveParticipant($participant, $breakout);
@@ -140,6 +150,7 @@ class MeetingFeaturesController extends Controller
public function returnFromBreakout(Request $request, Session $session): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'breakouts');
$participant = $this->participant($request, $session);
$this->breakouts->returnToMain($participant);
@@ -148,6 +159,7 @@ class MeetingFeaturesController extends Controller
public function broadcastBreakout(Request $request, Session $session): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'breakouts');
$participant = $this->hostOnly($request, $session);
$validated = $request->validate(['message' => ['required', 'string', 'max:1000']]);
$message = $this->breakouts->broadcast($session, $validated['message'], $participant->display_name);
@@ -157,6 +169,7 @@ class MeetingFeaturesController extends Controller
public function closeBreakouts(Request $request, Session $session): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'breakouts');
$this->hostOnly($request, $session);
$this->breakouts->closeAll($session);
@@ -167,6 +180,7 @@ class MeetingFeaturesController extends Controller
public function uploadFile(Request $request, Session $session): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'files');
$participant = $this->participant($request, $session);
$validated = $request->validate(['file' => ['required', 'file', 'max:51200']]);
@@ -199,6 +213,7 @@ class MeetingFeaturesController extends Controller
public function showWhiteboard(Request $request, Session $session): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'whiteboard');
$this->participant($request, $session);
$board = $this->whiteboards->ensureForSession($session);
@@ -207,6 +222,7 @@ class MeetingFeaturesController extends Controller
public function saveWhiteboard(Request $request, Session $session): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'whiteboard');
$this->participant($request, $session);
$validated = $request->validate(['state' => ['required', 'array']]);
$board = $this->whiteboards->ensureForSession($session);
@@ -216,6 +232,7 @@ class MeetingFeaturesController extends Controller
public function exportWhiteboard(Request $request, Session $session): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'whiteboard');
$this->participant($request, $session);
$validated = $request->validate(['image_data' => ['required', 'string']]);
$data = preg_replace('#^data:image/\w+;base64,#i', '', $validated['image_data']);
@@ -232,6 +249,7 @@ class MeetingFeaturesController extends Controller
public function configureLiveStream(Request $request, Session $session): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'live_stream');
$this->hostOnly($request, $session);
$validated = $request->validate([
'platform' => ['required', 'string', 'in:'.implode(',', config('meet.live_stream.platforms'))],
@@ -247,6 +265,7 @@ class MeetingFeaturesController extends Controller
public function startLiveStream(Request $request, Session $session): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'live_stream');
$this->hostOnly($request, $session);
$validated = $request->validate([
'platform' => ['required', 'string', 'in:'.implode(',', config('meet.live_stream.platforms'))],
@@ -260,6 +279,7 @@ class MeetingFeaturesController extends Controller
public function stopLiveStream(Request $request, Session $session): JsonResponse
{
$this->abortUnlessRoomFeature($session, 'live_stream');
$this->hostOnly($request, $session);
$validated = $request->validate([
'platform' => ['required', 'string', 'in:'.implode(',', config('meet.live_stream.platforms'))],
@@ -382,4 +402,13 @@ class MeetingFeaturesController extends Controller
return $participant;
}
protected function abortUnlessRoomFeature(Session $session, string $feature): void
{
abort_unless(
app(SpaceService::class)->allowsRoomFeature($session->room, $feature),
422,
'This feature is not available in audio rooms.',
);
}
}
@@ -95,7 +95,7 @@ class MeetingRoomController extends Controller
'messages' => $this->chat->recentMessages($session),
'activeRecording' => $session->recordings()->where('status', 'recording')->first(),
'watermark' => $room->organization?->securitySetting('watermark_enabled', false),
'isWebinar' => $room->isWebinar() || $room->isConference() || $room->isSpace(),
'isWebinar' => $room->isWebinar() || $room->isConference(),
'isConference' => $room->isConference(),
'isGreenRoom' => $session->session_mode === 'green_room',
'sessionMode' => $session->session_mode,
@@ -254,6 +254,8 @@ class MeetingRoomController extends Controller
public function sendMessage(Request $request, Session $session): JsonResponse
{
abort_unless(app(SpaceService::class)->allowsRoomFeature($session->room, 'chat'), 422);
$participant = $this->currentParticipant($request, $session);
$validated = $request->validate(['body' => ['required', 'string', 'max:2000']]);
@@ -493,6 +495,8 @@ class MeetingRoomController extends Controller
public function lock(Request $request, Session $session): JsonResponse
{
abort_unless(app(SpaceService::class)->allowsRoomFeature($session->room, 'lock'), 422);
$participant = $this->currentParticipant($request, $session);
abort_unless($participant->isHost(), 403);
$this->sessions->lock($session, $participant->user_ref ?? $participant->uuid);
@@ -502,6 +506,8 @@ class MeetingRoomController extends Controller
public function unlock(Request $request, Session $session): JsonResponse
{
abort_unless(app(SpaceService::class)->allowsRoomFeature($session->room, 'lock'), 422);
$participant = $this->currentParticipant($request, $session);
abort_unless($participant->isHost(), 403);
$this->sessions->unlock($session, $participant->user_ref ?? $participant->uuid);