Add speak access for webinar/conference attendees and fix Events linking UX.
Deploy Ladill Meet / deploy (push) Successful in 33s

Let attendees request host-granted microphone access with LiveKit reconnect,
participant panel controls, and toolbar UI that only shows mic when allowed.
Fix Events create URL, surface API key setup guidance, and align the Webinars
sidebar icon with other nav items using currentColor strokes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-02 07:59:18 +00:00
co-authored by Cursor
parent 9ab7d3e685
commit d648f6c862
22 changed files with 674 additions and 44 deletions
@@ -77,6 +77,9 @@ class MeetingRoomController extends Controller
$authUser = $request->user();
$isRoomHost = $authUser && $authUser->ownerRef() === $room->host_user_ref;
$canManageConference = $participant->isHost() || $isRoomHost;
$canManageSpeakAccess = app(\App\Services\Meet\SpeakAccessService::class)->canManageSpeakAccess($participant);
$usesSpeakAccess = $room->isWebinar() || $room->isConference();
$mediaState = $this->sessions->mediaSessionState($participant);
$breakoutService = app(BreakoutService::class);
$inBreakout = $breakoutService->isInActiveBreakout($participant);
@@ -97,7 +100,12 @@ class MeetingRoomController extends Controller
'presenterRefs' => (array) $session->presenter_refs,
'isAudioOnly' => $isAudioOnly,
'isSpace' => $room->isSpace(),
'canPublish' => $this->sessions->canParticipantPublish($participant),
'canPublish' => $mediaState['can_publish'],
'audioOnlyPublish' => $mediaState['audio_only_publish'],
'speakGranted' => $mediaState['speak_granted'],
'speakRequested' => $mediaState['speak_requested'],
'usesSpeakAccess' => $usesSpeakAccess,
'canManageSpeakAccess' => $canManageSpeakAccess,
'isAttendee' => $participant->isAttendee() || $spaces->isListener($room, $participant),
'usesStageLayout' => $stageLayout->usesStageLayout($room) && ! $inBreakout,
'inBreakout' => $inBreakout,
@@ -186,6 +194,62 @@ class MeetingRoomController extends Controller
return response()->json(['hand_raised' => $participant->hand_raised]);
}
public function requestSpeak(Request $request, Session $session): JsonResponse
{
$participant = $this->currentParticipant($request, $session);
$speak = app(\App\Services\Meet\SpeakAccessService::class);
return response()->json([
'participant' => $speak->requestSpeak($participant),
]);
}
public function cancelSpeakRequest(Request $request, Session $session): JsonResponse
{
$participant = $this->currentParticipant($request, $session);
$speak = app(\App\Services\Meet\SpeakAccessService::class);
return response()->json([
'participant' => $speak->cancelSpeakRequest($participant),
]);
}
public function grantSpeak(Request $request, Session $session, Participant $participant): JsonResponse
{
$actor = $this->currentParticipant($request, $session);
abort_unless($participant->session_id === $session->id, 404);
$speak = app(\App\Services\Meet\SpeakAccessService::class);
return response()->json([
'participant' => $speak->grantSpeak($participant, $actor),
]);
}
public function revokeSpeak(Request $request, Session $session, Participant $participant): JsonResponse
{
$actor = $this->currentParticipant($request, $session);
abort_unless($participant->session_id === $session->id, 404);
$speak = app(\App\Services\Meet\SpeakAccessService::class);
return response()->json([
'participant' => $speak->revokeSpeak($participant, $actor),
]);
}
public function dismissSpeakRequest(Request $request, Session $session, Participant $participant): JsonResponse
{
$actor = $this->currentParticipant($request, $session);
abort_unless($participant->session_id === $session->id, 404);
$speak = app(\App\Services\Meet\SpeakAccessService::class);
return response()->json([
'participant' => $speak->dismissSpeakRequest($participant, $actor),
]);
}
public function sendMessage(Request $request, Session $session): JsonResponse
{
$participant = $this->currentParticipant($request, $session);