diff --git a/.env.example b/.env.example index aade2cc..219692a 100644 --- a/.env.example +++ b/.env.example @@ -63,6 +63,7 @@ MEET_API_KEY_SUPPORT= MEET_API_KEY_INVOICE= # --- Ladill Events (registration for webinars & conferences) --- +# Same secret on both apps: MEET_API_KEY_EVENTS here, EVENTS_API_KEY_MEET in ladill-events. LADILL_EVENTS_APP_URL=https://events.ladill.com LADILL_EVENTS_API_URL=https://events.ladill.com/api/service/v1 diff --git a/app/Http/Controllers/Meet/EventsLinkController.php b/app/Http/Controllers/Meet/EventsLinkController.php index d7c9627..f2122fc 100644 --- a/app/Http/Controllers/Meet/EventsLinkController.php +++ b/app/Http/Controllers/Meet/EventsLinkController.php @@ -30,7 +30,7 @@ class EventsLinkController extends Controller if (! $this->eventsClient->isConfigured()) { return redirect() ->route($room->isWebinar() ? 'meet.webinars.show' : 'meet.conferences.show', $room) - ->withErrors(['events' => 'Ladill Events integration is not configured on this Meet instance.']); + ->withErrors(['events' => 'Ladill Events linking is not configured. Set MEET_API_KEY_EVENTS in Meet (and the matching EVENTS_API_KEY_MEET in Events), then try again.']); } $events = $this->links->linkableEvents($request->user()); diff --git a/app/Http/Controllers/Meet/MeetingRoomController.php b/app/Http/Controllers/Meet/MeetingRoomController.php index 0dbec32..c22b4a8 100644 --- a/app/Http/Controllers/Meet/MeetingRoomController.php +++ b/app/Http/Controllers/Meet/MeetingRoomController.php @@ -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); diff --git a/app/Models/Participant.php b/app/Models/Participant.php index 616b7a0..2ce6b29 100644 --- a/app/Models/Participant.php +++ b/app/Models/Participant.php @@ -16,7 +16,7 @@ class Participant extends Model protected $fillable = [ 'uuid', 'owner_ref', 'session_id', 'user_ref', 'display_name', 'email', 'role', 'status', 'joined_at', 'left_at', 'device_info', 'ip_address', - 'hand_raised', 'is_muted', 'is_video_off', 'breakout_room_id', + 'hand_raised', 'speak_requested', 'speak_granted', 'is_muted', 'is_video_off', 'breakout_room_id', ]; protected function casts(): array @@ -25,6 +25,8 @@ class Participant extends Model 'joined_at' => 'datetime', 'left_at' => 'datetime', 'hand_raised' => 'boolean', + 'speak_requested' => 'boolean', + 'speak_granted' => 'boolean', 'is_muted' => 'boolean', 'is_video_off' => 'boolean', ]; @@ -66,9 +68,18 @@ class Participant extends Model public function canPublishMedia(): bool { + if ($this->speak_granted) { + return true; + } + return $this->role !== 'attendee'; } + public function hasSpeakAccess(): bool + { + return $this->speak_granted; + } + public function breakoutRoom(): BelongsTo { return $this->belongsTo(BreakoutRoom::class, 'breakout_room_id'); diff --git a/app/Services/Meet/ConferenceService.php b/app/Services/Meet/ConferenceService.php index 2b5ecce..8344cc9 100644 --- a/app/Services/Meet/ConferenceService.php +++ b/app/Services/Meet/ConferenceService.php @@ -87,6 +87,14 @@ class ConferenceService public function canPublish(Room $room, Participant $participant): bool { + if (in_array($participant->role, ['host', 'co_host', 'panelist'], true)) { + return true; + } + + if ($room->isTownHall() || $room->isWebinar()) { + return (bool) $participant->speak_granted; + } + return in_array($participant->role, ['host', 'co_host', 'panelist'], true); } diff --git a/app/Services/Meet/ParticipantPresenter.php b/app/Services/Meet/ParticipantPresenter.php index 3a4b6aa..4cf0b1a 100644 --- a/app/Services/Meet/ParticipantPresenter.php +++ b/app/Services/Meet/ParticipantPresenter.php @@ -41,6 +41,8 @@ class ParticipantPresenter 'role' => $participant->role, 'status' => $participant->status, 'hand_raised' => $participant->hand_raised, + 'speak_requested' => (bool) $participant->speak_requested, + 'speak_granted' => (bool) $participant->speak_granted, 'is_muted' => $participant->is_muted, 'is_video_off' => $participant->is_video_off, 'breakout_room_id' => $participant->breakout_room_id, diff --git a/app/Services/Meet/SessionService.php b/app/Services/Meet/SessionService.php index c9df16a..0488ad8 100644 --- a/app/Services/Meet/SessionService.php +++ b/app/Services/Meet/SessionService.php @@ -342,7 +342,7 @@ class SessionService ); } - /** @return array{can_publish: bool, uses_stage_layout: bool, in_breakout: bool, breakout_room_id: ?int, breakout: ?array{uuid: string, name: string, closes_at: ?string}} */ + /** @return array{can_publish: bool, audio_only_publish: bool, speak_granted: bool, speak_requested: bool, uses_stage_layout: bool, in_breakout: bool, breakout_room_id: ?int, breakout: ?array{uuid: string, name: string, closes_at: ?string}} */ public function mediaSessionState(Participant $participant): array { $participant->loadMissing(['breakoutRoom', 'session.room']); @@ -350,6 +350,11 @@ class SessionService $breakoutService = app(BreakoutService::class); $inBreakout = $breakoutService->isInActiveBreakout($participant); $usesStageLayout = app(StageLayoutService::class)->usesStageLayout($room) && ! $inBreakout; + $canPublish = $this->canParticipantPublish($participant, $inBreakout); + $audioOnlyPublish = $canPublish + && $participant->isAttendee() + && (bool) $participant->speak_granted + && ! $inBreakout; $breakout = null; if ($inBreakout && $participant->breakoutRoom) { @@ -361,7 +366,10 @@ class SessionService } return [ - 'can_publish' => $this->canParticipantPublish($participant, $inBreakout), + 'can_publish' => $canPublish, + 'audio_only_publish' => $audioOnlyPublish, + 'speak_granted' => (bool) $participant->speak_granted, + 'speak_requested' => (bool) $participant->speak_requested, 'uses_stage_layout' => $usesStageLayout, 'in_breakout' => $inBreakout, 'breakout_room_id' => $inBreakout ? $participant->breakout_room_id : null, diff --git a/app/Services/Meet/SpeakAccessService.php b/app/Services/Meet/SpeakAccessService.php new file mode 100644 index 0000000..e7bafa3 --- /dev/null +++ b/app/Services/Meet/SpeakAccessService.php @@ -0,0 +1,83 @@ +isWebinar() || $room->isConference(); + } + + public function canManageSpeakAccess(Participant $participant): bool + { + $room = $participant->session->room; + + if (! $this->usesSpeakAccess($room)) { + return false; + } + + return in_array($participant->role, ['host', 'co_host', 'panelist'], true); + } + + public function requestSpeak(Participant $participant): Participant + { + $room = $participant->session->room; + abort_unless($this->usesSpeakAccess($room), 422); + abort_unless($participant->role === 'attendee', 422); + abort_unless(! $participant->speak_granted, 422, 'You already have speak access.'); + + $participant->update(['speak_requested' => true]); + + return $participant->fresh(); + } + + public function cancelSpeakRequest(Participant $participant): Participant + { + $participant->update(['speak_requested' => false]); + + return $participant->fresh(); + } + + public function grantSpeak(Participant $target, Participant $actor): Participant + { + abort_unless($this->canManageSpeakAccess($actor), 403); + abort_unless($target->session_id === $actor->session_id, 404); + abort_unless($target->status === 'joined', 422); + abort_unless($this->usesSpeakAccess($target->session->room), 422); + + $target->update([ + 'speak_granted' => true, + 'speak_requested' => false, + ]); + + return $target->fresh(); + } + + public function revokeSpeak(Participant $target, Participant $actor): Participant + { + abort_unless($this->canManageSpeakAccess($actor), 403); + abort_unless($target->session_id === $actor->session_id, 404); + + $target->update([ + 'speak_granted' => false, + 'speak_requested' => false, + 'is_muted' => true, + ]); + + return $target->fresh(); + } + + public function dismissSpeakRequest(Participant $target, Participant $actor): Participant + { + abort_unless($this->canManageSpeakAccess($actor), 403); + abort_unless($target->session_id === $actor->session_id, 404); + + $target->update(['speak_requested' => false]); + + return $target->fresh(); + } +} diff --git a/app/Services/Meet/WebinarService.php b/app/Services/Meet/WebinarService.php index 8d3cd24..bd26b37 100644 --- a/app/Services/Meet/WebinarService.php +++ b/app/Services/Meet/WebinarService.php @@ -67,6 +67,10 @@ class WebinarService return true; } - return in_array($participant->role, ['host', 'co_host', 'panelist'], true); + if (in_array($participant->role, ['host', 'co_host', 'panelist'], true)) { + return true; + } + + return (bool) $participant->speak_granted; } } diff --git a/app/Support/EventsSourceLink.php b/app/Support/EventsSourceLink.php index c4c35c2..9292a5b 100644 --- a/app/Support/EventsSourceLink.php +++ b/app/Support/EventsSourceLink.php @@ -27,6 +27,16 @@ class EventsSourceLink return rtrim((string) config('meet.events_app_url', 'https://events.ladill.com'), '/'); } + public static function isApiConfigured(): bool + { + return filled(config('meet.events_api_url')) && filled(config('meet.events_api_key')); + } + + public static function createEventUrl(?Room $room = null): string + { + return self::baseUrl().'/events/create'; + } + public static function eventAdminUrl(Room $room): ?string { $eventId = self::eventId($room); diff --git a/database/migrations/2026_07_02_120000_add_speak_access_to_meet_participants.php b/database/migrations/2026_07_02_120000_add_speak_access_to_meet_participants.php new file mode 100644 index 0000000..f89d12b --- /dev/null +++ b/database/migrations/2026_07_02_120000_add_speak_access_to_meet_participants.php @@ -0,0 +1,23 @@ +boolean('speak_requested')->default(false)->after('hand_raised'); + $table->boolean('speak_granted')->default(false)->after('speak_requested'); + }); + } + + public function down(): void + { + Schema::table('meet_participants', function (Blueprint $table) { + $table->dropColumn(['speak_requested', 'speak_granted']); + }); + } +}; diff --git a/public/images/meet-icons/speak.svg b/public/images/meet-icons/speak.svg new file mode 100644 index 0000000..917465f --- /dev/null +++ b/public/images/meet-icons/speak.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/js/meet-room.js b/resources/js/meet-room.js index 1465c09..805c06c 100644 --- a/resources/js/meet-room.js +++ b/resources/js/meet-room.js @@ -53,6 +53,11 @@ function meetRoom() { goingLive: false, presenterRefs: [], audioOnly: false, + audioOnlyPublish: false, + usesSpeakAccess: false, + canManageSpeak: false, + speakGranted: false, + speakRequested: false, isSpace: false, usesStageLayout: false, panelDiscussions: false, @@ -112,6 +117,11 @@ function meetRoom() { chatUrl: el.dataset.chatUrl, reactionUrl: el.dataset.reactionUrl, raiseHandUrl: el.dataset.raiseHandUrl, + speakRequestUrl: el.dataset.speakRequestUrl, + speakCancelUrl: el.dataset.speakCancelUrl, + speakGrantUrl: el.dataset.speakGrantUrl, + speakRevokeUrl: el.dataset.speakRevokeUrl, + speakDismissUrl: el.dataset.speakDismissUrl, recordingStartUrl: el.dataset.recordingStartUrl, recordingStopUrl: el.dataset.recordingStopUrl, lockUrl: el.dataset.lockUrl, @@ -143,6 +153,11 @@ function meetRoom() { this.isHost = el.dataset.isHost === '1'; this.config.participantUuid = el.dataset.participant || ''; this.canPublish = el.dataset.canPublish !== '0'; + this.audioOnlyPublish = el.dataset.audioOnlyPublish === '1'; + this.usesSpeakAccess = el.dataset.usesSpeakAccess === '1'; + this.canManageSpeak = el.dataset.canManageSpeak === '1'; + this.speakGranted = el.dataset.speakGranted === '1'; + this.speakRequested = el.dataset.speakRequested === '1'; this.inBreakout = el.dataset.inBreakout === '1'; this.isWebinar = el.dataset.isWebinar === '1'; this.isConference = el.dataset.isConference === '1'; @@ -384,7 +399,7 @@ function meetRoom() { async enableLocalMedia(activeRoom, attempts = 3) { const wantMic = this.inBreakout ? !this.muteOnJoin : (!this.muteOnJoin || this.isHost); - const wantCamera = !this.videoOffOnJoin && !this.audioOnly; + const wantCamera = !this.videoOffOnJoin && !this.audioOnly && !this.audioOnlyPublish; const audioOptions = { echoCancellation: true, noiseSuppression: true, @@ -1583,6 +1598,9 @@ function meetRoom() { this.inBreakout = Boolean(media.in_breakout); this.assignedBreakoutRoomId = media.breakout_room_id ?? null; this.canPublish = Boolean(media.can_publish); + this.audioOnlyPublish = Boolean(media.audio_only_publish); + this.speakGranted = Boolean(media.speak_granted); + this.speakRequested = Boolean(media.speak_requested); this.usesStageLayout = Boolean(media.uses_stage_layout); this.breakoutRoomName = media.breakout?.name || ''; this.breakoutClosesAt = media.breakout?.closes_at || ''; @@ -1596,6 +1614,8 @@ function meetRoom() { const previousBreakoutId = this.assignedBreakoutRoomId; const previousCanPublish = this.canPublish; + const previousAudioOnlyPublish = this.audioOnlyPublish; + const previousSpeakGranted = this.speakGranted; const previousUsesStageLayout = this.usesStageLayout; this.applyMediaSessionState(media); @@ -1604,6 +1624,8 @@ function meetRoom() { const needsReconnect = previousBreakoutId !== this.assignedBreakoutRoomId || previousCanPublish !== this.canPublish + || previousAudioOnlyPublish !== this.audioOnlyPublish + || previousSpeakGranted !== this.speakGranted || previousUsesStageLayout !== this.usesStageLayout; if (needsReconnect && this.config.configured) { @@ -1828,7 +1850,7 @@ function meetRoom() { }, async toggleMute() { - if (!room || !this.canPublish) return; + if (!room || !this.showMicrophoneControl()) return; await this.ensureAudioPlayback(); const enabled = room.localParticipant.isMicrophoneEnabled; @@ -1855,7 +1877,7 @@ function meetRoom() { }, async toggleVideo() { - if (!room || this.audioOnly || !this.canPublish) return; + if (!room || this.audioOnly || this.audioOnlyPublish || !this.canPublish) return; await this.ensureAudioPlayback(); const enabled = room.localParticipant.isCameraEnabled; await room.localParticipant.setCameraEnabled(!enabled); @@ -1874,7 +1896,7 @@ function meetRoom() { }, async toggleScreenShare() { - if (!room || this.audioOnly || !this.canPublish) return; + if (!room || this.audioOnly || this.audioOnlyPublish || !this.canPublish) return; this.isScreenSharing = !this.isScreenSharing; await room.localParticipant.setScreenShareEnabled(this.isScreenSharing); if (!this.isScreenSharing) { @@ -1893,6 +1915,167 @@ function meetRoom() { }); }, + showMicrophoneControl() { + if (!this.canPublish) { + return false; + } + + if (this.audioOnlyPublish) { + return this.speakGranted; + } + + return true; + }, + + showVideoControl() { + return this.canPublish && !this.audioOnlyPublish; + }, + + showAudienceControls() { + if (!this.usesSpeakAccess) { + return this.canPublish; + } + + return !this.canPublish || this.audioOnlyPublish; + }, + + showStagePublisherControls() { + return this.usesSpeakAccess && this.canPublish && !this.audioOnlyPublish; + }, + + speakRequestParticipants() { + return this.inCallParticipants().filter((person) => person.speak_requested && !person.speak_granted); + }, + + syncSelfSpeakState() { + const self = this.sessionParticipants.find((person) => person.uuid === this.config.participantUuid); + if (!self) { + return; + } + + this.speakGranted = Boolean(self.speak_granted); + this.speakRequested = Boolean(self.speak_requested); + }, + + mergeSpeakParticipant(participant) { + if (!participant?.uuid) { + return; + } + + const index = this.sessionParticipants.findIndex((person) => person.uuid === participant.uuid); + if (index >= 0) { + this.sessionParticipants[index] = { + ...this.sessionParticipants[index], + speak_requested: Boolean(participant.speak_requested), + speak_granted: Boolean(participant.speak_granted), + }; + } + + if (participant.uuid === this.config.participantUuid) { + this.speakGranted = Boolean(participant.speak_granted); + this.speakRequested = Boolean(participant.speak_requested); + } + }, + + speakActionUrl(template, participantUuid) { + return template?.replace('__UUID__', participantUuid) || ''; + }, + + async requestSpeak() { + if (!this.config.speakRequestUrl) { + return; + } + + const res = await fetch(this.config.speakRequestUrl, { + method: 'POST', + headers: this.headers(), + }); + + if (!res.ok) { + return; + } + + const data = await res.json(); + this.mergeSpeakParticipant(data.participant); + }, + + async cancelSpeakRequest() { + if (!this.config.speakCancelUrl) { + return; + } + + const res = await fetch(this.config.speakCancelUrl, { + method: 'POST', + headers: this.headers(), + }); + + if (!res.ok) { + return; + } + + const data = await res.json(); + this.mergeSpeakParticipant(data.participant); + }, + + async grantSpeakAccess(participantUuid) { + const url = this.speakActionUrl(this.config.speakGrantUrl, participantUuid); + if (!url) { + return; + } + + const res = await fetch(url, { + method: 'POST', + headers: this.headers(), + }); + + if (!res.ok) { + return; + } + + const data = await res.json(); + this.mergeSpeakParticipant(data.participant); + await this.poll(); + }, + + async revokeSpeakAccess(participantUuid) { + const url = this.speakActionUrl(this.config.speakRevokeUrl, participantUuid); + if (!url) { + return; + } + + const res = await fetch(url, { + method: 'POST', + headers: this.headers(), + }); + + if (!res.ok) { + return; + } + + const data = await res.json(); + this.mergeSpeakParticipant(data.participant); + await this.poll(); + }, + + async dismissSpeakRequest(participantUuid) { + const url = this.speakActionUrl(this.config.speakDismissUrl, participantUuid); + if (!url) { + return; + } + + const res = await fetch(url, { + method: 'POST', + headers: this.headers(), + }); + + if (!res.ok) { + return; + } + + const data = await res.json(); + this.mergeSpeakParticipant(data.participant); + }, + async goLive() { if (!this.config.goLiveUrl || this.goingLive) { return; @@ -2059,10 +2242,18 @@ function meetRoom() { return; } if (data.participants) { + const previousSpeakRequestCount = this.speakRequestParticipants().length; this.sessionParticipants = data.participants; + if (this.usesSpeakAccess) { + this.syncSelfSpeakState(); + } this.pruneTilesFromSession(); this.refreshParticipantTileAvatars(); this.renderAudienceStrip(); + const speakRequestCount = this.speakRequestParticipants().length; + if (this.canManageSpeak && speakRequestCount > previousSpeakRequestCount) { + this.openParticipantsPanel(); + } } if (typeof data.uses_stage_layout === 'boolean' || typeof data.stage_layout === 'string') { this.mergeStageConfig(data); diff --git a/resources/views/meet/conferences/show.blade.php b/resources/views/meet/conferences/show.blade.php index f13afaf..22fcfea 100644 --- a/resources/views/meet/conferences/show.blade.php +++ b/resources/views/meet/conferences/show.blade.php @@ -6,12 +6,6 @@

{{ config('meet.room_statuses')[$room->status] ?? $room->status }}

- @if ($errors->any()) -
- {{ $errors->first() }} -
- @endif - @if (session('success'))
{{ session('success') }} diff --git a/resources/views/meet/events/link.blade.php b/resources/views/meet/events/link.blade.php index e035042..0fb9f77 100644 --- a/resources/views/meet/events/link.blade.php +++ b/resources/views/meet/events/link.blade.php @@ -1,3 +1,4 @@ +@php use App\Support\EventsSourceLink; @endphp
← Back @@ -15,7 +16,7 @@ @if ($events === [])

No virtual or hybrid events found in your Ladill Events account.

- Create event in Events + Create event in Events
@else
diff --git a/resources/views/meet/partials/events-integration.blade.php b/resources/views/meet/partials/events-integration.blade.php index d137b96..ce8de73 100644 --- a/resources/views/meet/partials/events-integration.blade.php +++ b/resources/views/meet/partials/events-integration.blade.php @@ -4,7 +4,8 @@ $eventsUrls = EventsSourceLink::urls($room); $isLinked = EventsSourceLink::isLinked($room); $eventTitle = EventsSourceLink::eventTitle($room); - $eventsBase = EventsSourceLink::baseUrl(); + $eventsConfigured = EventsSourceLink::isApiConfigured(); + $createEventUrl = EventsSourceLink::createEventUrl($room); $kind = $room->isConference() ? 'conference' : 'webinar'; @endphp @@ -32,6 +33,17 @@ @endif
+ @if (! $isLinked && ! $eventsConfigured) +
+

Events linking is not configured on this Meet instance.

+

+ Set MEET_API_KEY_EVENTS in Meet and the matching + EVENTS_API_KEY_MEET in Events so Meet can list and link your events. + You can still create an event in Events using the button below. +

+
+ @endif + @if ($isLinked)
@if ($eventsUrls['registration']) @@ -72,8 +84,12 @@

Programme speakers and panelists are configured on the event programme in Events and sync to this session when linked.

@else
- Link to Ladill Events - Create event in Events + @if ($eventsConfigured) + Link to Ladill Events + @else + Link to Ladill Events + @endif + Create event in Events
@endif
diff --git a/resources/views/meet/room/partials/meet-icon.blade.php b/resources/views/meet/room/partials/meet-icon.blade.php index eace17f..ba58d42 100644 --- a/resources/views/meet/room/partials/meet-icon.blade.php +++ b/resources/views/meet/room/partials/meet-icon.blade.php @@ -18,6 +18,7 @@ 'start-breakout', 'upload-file', 'programme', + 'speak', ]; $iconName = in_array($icon, $allowed, true) ? $icon : null; $iconPath = $iconName ? public_path('images/meet-icons/'.$iconName.'.svg') : null; diff --git a/resources/views/meet/room/show.blade.php b/resources/views/meet/room/show.blade.php index 5e24ce4..656d5c1 100644 --- a/resources/views/meet/room/show.blade.php +++ b/resources/views/meet/room/show.blade.php @@ -68,6 +68,13 @@ data-chat-url="{{ route('meet.room.chat', $session) }}" data-reaction-url="{{ route('meet.room.reaction', $session) }}" data-raise-hand-url="{{ route('meet.room.raise-hand', $session) }}" + data-speak-request-url="{{ ($usesSpeakAccess ?? false) ? route('meet.room.speak.request', $session) : '' }}" + data-speak-cancel-url="{{ ($usesSpeakAccess ?? false) ? route('meet.room.speak.cancel', $session) : '' }}" + data-speak-grant-url="{{ ($canManageSpeakAccess ?? false) ? route('meet.room.speak.grant', [$session, '__UUID__']) : '' }}" + data-speak-revoke-url="{{ ($canManageSpeakAccess ?? false) ? route('meet.room.speak.revoke', [$session, '__UUID__']) : '' }}" + data-speak-dismiss-url="{{ ($canManageSpeakAccess ?? false) ? route('meet.room.speak.dismiss', [$session, '__UUID__']) : '' }}" + data-uses-speak-access="{{ ($usesSpeakAccess ?? false) ? '1' : '0' }}" + data-can-manage-speak="{{ ($canManageSpeakAccess ?? false) ? '1' : '0' }}" data-leave-url="{{ route('meet.room.leave', $session) }}" data-end-url="{{ route('meet.room.end', $session) }}" data-recording-start-url="{{ route('meet.room.recording.start', $session) }}" @@ -99,6 +106,9 @@ data-spotlight-speaker-ref="{{ $stageConfig['spotlight_speaker_ref'] ?? '' }}" data-presenter-refs='@json($presenterRefs ?? [])' data-can-publish="{{ ($canPublish ?? true) ? '1' : '0' }}" + data-audio-only-publish="{{ ($audioOnlyPublish ?? false) ? '1' : '0' }}" + data-speak-granted="{{ ($speakGranted ?? false) ? '1' : '0' }}" + data-speak-requested="{{ ($speakRequested ?? false) ? '1' : '0' }}" data-in-breakout="{{ ($inBreakout ?? false) ? '1' : '0' }}" data-qa-url="{{ route('meet.room.qa.submit', $session) }}" data-polls-create-url="{{ route('meet.room.polls.create', $session) }}" @@ -318,11 +328,17 @@