Add live speaker invites for audio rooms and simplify the toolbar.
Deploy Ladill Meet / deploy (push) Successful in 1m9s
Deploy Ladill Meet / deploy (push) Successful in 1m9s
Let hosts promote raised-hand listeners from People, remove the empty More menu in spaces, and put People and recording on the bottom bar. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -122,6 +122,8 @@ function meetRoom() {
|
||||
speakGrantUrl: el.dataset.speakGrantUrl,
|
||||
speakRevokeUrl: el.dataset.speakRevokeUrl,
|
||||
speakDismissUrl: el.dataset.speakDismissUrl,
|
||||
spacePromoteUrl: el.dataset.spacePromoteUrl,
|
||||
spaceDemoteUrl: el.dataset.spaceDemoteUrl,
|
||||
recordingStartUrl: el.dataset.recordingStartUrl,
|
||||
recordingStopUrl: el.dataset.recordingStopUrl,
|
||||
lockUrl: el.dataset.lockUrl,
|
||||
@@ -2172,6 +2174,67 @@ function meetRoom() {
|
||||
return this.inCallParticipants().filter((person) => person.speak_requested && !person.speak_granted);
|
||||
},
|
||||
|
||||
handRaisedParticipants() {
|
||||
return this.inCallParticipants().filter((person) => person.hand_raised && person.role === 'attendee');
|
||||
},
|
||||
|
||||
spaceSpeakerUrl(template, participantUuid) {
|
||||
return template?.replace('__UUID__', participantUuid) || '';
|
||||
},
|
||||
|
||||
mergeSessionParticipant(participant) {
|
||||
if (!participant?.uuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this.sessionParticipants.findIndex((person) => person.uuid === participant.uuid);
|
||||
if (index >= 0) {
|
||||
this.sessionParticipants[index] = { ...this.sessionParticipants[index], ...participant };
|
||||
}
|
||||
|
||||
if (participant.uuid === this.config.participantUuid) {
|
||||
this.canPublish = ['host', 'co_host', 'panelist'].includes(participant.role);
|
||||
}
|
||||
},
|
||||
|
||||
async promoteSpaceSpeaker(participantUuid) {
|
||||
const url = this.spaceSpeakerUrl(this.config.spacePromoteUrl, participantUuid);
|
||||
if (!this.isSpace || !this.isHost || !url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: this.headers(),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
this.mergeSessionParticipant(data.participant);
|
||||
},
|
||||
|
||||
async demoteSpaceSpeaker(participantUuid) {
|
||||
const url = this.spaceSpeakerUrl(this.config.spaceDemoteUrl, participantUuid);
|
||||
if (!this.isSpace || !this.isHost || !url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: this.headers(),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
this.mergeSessionParticipant(data.participant);
|
||||
},
|
||||
|
||||
syncSelfSpeakState() {
|
||||
const self = this.sessionParticipants.find((person) => person.uuid === this.config.participantUuid);
|
||||
if (!self) {
|
||||
|
||||
@@ -82,6 +82,8 @@
|
||||
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-space-promote-url="{{ ($isSpace ?? false) && ($participant->isHost() || ($canManageConference ?? false)) ? route('meet.room.space.promote', [$session, '__UUID__']) : '' }}"
|
||||
data-space-demote-url="{{ ($isSpace ?? false) && ($participant->isHost() || ($canManageConference ?? false)) ? route('meet.room.space.demote', [$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) }}"
|
||||
@@ -422,7 +424,24 @@
|
||||
title="React">
|
||||
@include('meet.room.partials.meet-icon', ['icon' => 'react'])
|
||||
</button>
|
||||
<button @click="toggleParticipants()" type="button"
|
||||
x-show="isSpace"
|
||||
class="rounded-full p-3 transition-colors sm:hidden"
|
||||
:class="sidebarPanel === 'participants' ? 'bg-indigo-600 hover:bg-indigo-500' : 'bg-slate-700 hover:bg-slate-600'"
|
||||
title="People">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"/></svg>
|
||||
</button>
|
||||
@if ($participant->isHost())
|
||||
<button @click="toggleRecording()" type="button"
|
||||
x-show="isSpace"
|
||||
class="rounded-full p-3 transition-colors"
|
||||
:class="isRecording ? 'bg-red-600 hover:bg-red-500' : 'bg-slate-700 hover:bg-slate-600'"
|
||||
:title="isRecording ? 'Stop recording' : 'Start recording'">
|
||||
<span class="block h-3 w-3 rounded-full" :class="isRecording ? 'bg-white animate-pulse' : 'bg-red-400'"></span>
|
||||
</button>
|
||||
@endif
|
||||
<button @click="toggleFeatures()" type="button"
|
||||
x-show="!isSpace"
|
||||
class="rounded-full p-3 transition-colors sm:hidden"
|
||||
:class="sidebarPanel === 'features' ? 'bg-indigo-600 hover:bg-indigo-500' : 'bg-slate-700 hover:bg-slate-600'"
|
||||
title="More options">
|
||||
@@ -450,12 +469,21 @@
|
||||
<input type="file" id="meet-file-input" class="hidden" @change="uploadFile">
|
||||
@endunless
|
||||
<button @click="toggleFeatures()" type="button"
|
||||
x-show="!isSpace"
|
||||
class="hidden items-center gap-1.5 rounded-full px-3.5 py-3 text-xs font-medium transition-colors sm:inline-flex"
|
||||
:class="sidebarPanel === 'features' ? 'bg-indigo-600 hover:bg-indigo-500' : 'bg-slate-700 hover:bg-slate-600'"
|
||||
title="More options">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"/></svg>
|
||||
More
|
||||
</button>
|
||||
@if (($isSpace ?? false) && $participant->isHost())
|
||||
<button @click="toggleRecording()" type="button"
|
||||
class="hidden rounded-full p-3 transition-colors sm:inline-flex"
|
||||
:class="isRecording ? 'bg-red-600 hover:bg-red-500' : 'bg-slate-700 hover:bg-slate-600'"
|
||||
:title="isRecording ? 'Stop recording' : 'Start recording'">
|
||||
<span class="block h-3 w-3 rounded-full" :class="isRecording ? 'bg-white animate-pulse' : 'bg-red-400'"></span>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<div class="meet-toolbar__divider sm:hidden" aria-hidden="true"></div>
|
||||
|
||||
@@ -604,10 +632,31 @@
|
||||
</template>
|
||||
</x-slot:subtitle>
|
||||
<x-slot:header>
|
||||
<h2 class="text-base font-semibold text-white" x-text="usesStageLayout ? 'Speakers' : 'Participants'"></h2>
|
||||
<h2 class="text-base font-semibold text-white" x-text="isSpace ? 'People' : (usesStageLayout ? 'Speakers' : 'Participants')"></h2>
|
||||
</x-slot:header>
|
||||
|
||||
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
||||
<template x-if="isSpace && isHost && handRaisedParticipants().length">
|
||||
<div class="mb-3 border-b border-slate-800 pb-3">
|
||||
<p class="px-2 pb-2 text-xs font-semibold uppercase tracking-wide text-amber-300">
|
||||
Raised hands
|
||||
<span class="ml-1 rounded-full bg-amber-500/20 px-1.5 py-0.5 text-[10px] text-amber-200"
|
||||
x-text="handRaisedParticipants().length"></span>
|
||||
</p>
|
||||
<template x-for="person in handRaisedParticipants()" :key="'hand-'+person.uuid">
|
||||
<div class="flex items-center gap-3 rounded-xl px-2 py-2.5">
|
||||
<span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-amber-500/20 text-sm font-semibold text-amber-200"
|
||||
x-text="participantInitials(person.display_name)"></span>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-sm font-medium text-white" x-text="person.display_name"></span>
|
||||
<span class="block text-xs text-amber-200/80">Wants to speak</span>
|
||||
</span>
|
||||
<button type="button" @click="promoteSpaceSpeaker(person.uuid)"
|
||||
class="rounded-lg bg-emerald-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-emerald-500">Invite to speak</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="isHost && waitingParticipants().length">
|
||||
<div class="mb-3 border-b border-slate-800 pb-3">
|
||||
<p class="px-2 pb-2 text-xs font-semibold uppercase tracking-wide text-amber-300">
|
||||
@@ -689,6 +738,16 @@
|
||||
class="rounded-lg bg-violet-600 px-2 py-1 text-[10px] font-medium text-white hover:bg-violet-500">
|
||||
Make speaker
|
||||
</button>
|
||||
<button x-show="isSpace && isHost && person.role === 'attendee'"
|
||||
type="button" @click="promoteSpaceSpeaker(person.uuid)"
|
||||
class="rounded-lg bg-emerald-600 px-2 py-1 text-[10px] font-medium text-white hover:bg-emerald-500">
|
||||
Invite to speak
|
||||
</button>
|
||||
<button x-show="isSpace && isHost && person.role === 'panelist'"
|
||||
type="button" @click="demoteSpaceSpeaker(person.uuid)"
|
||||
class="rounded-lg bg-slate-600 px-2 py-1 text-[10px] font-medium text-slate-100 hover:bg-slate-500">
|
||||
Move to audience
|
||||
</button>
|
||||
<button x-show="canManageSpeak && usesSpeakAccess && person.role === 'attendee' && !person.speak_granted"
|
||||
type="button" @click="grantSpeakAccess(person.uuid)"
|
||||
class="rounded-lg bg-emerald-600 px-2 py-1 text-[10px] font-medium text-white hover:bg-emerald-500">
|
||||
@@ -829,6 +888,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@unless ($isSpace ?? false)
|
||||
<x-meet.room.panel show="sidebarPanel === 'features'" title="More options">
|
||||
<x-slot:subtitle>
|
||||
<p>{{ ($isSpace ?? false) ? 'Audio room controls' : $sessionNounTitle.' tools and extras' }}</p>
|
||||
@@ -972,6 +1032,7 @@
|
||||
<p x-show="breakoutBroadcast && !isSpace" class="mt-3 rounded-xl bg-amber-950/40 px-3 py-3 text-sm text-amber-100" x-text="breakoutBroadcast"></p>
|
||||
</div>
|
||||
</x-meet.room.panel>
|
||||
@endunless
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<x-app-layout title="Create room">
|
||||
<div class="mx-auto max-w-xl">
|
||||
<h1 class="text-xl font-semibold text-slate-900">Create a room</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Audio-only. Host and assigned speakers talk; everyone else listens and can raise a hand to speak.</p>
|
||||
<p class="mt-1 text-sm text-slate-500">Audio-only. Pick speakers when you create the room, or invite listeners to speak during the live session from People.</p>
|
||||
|
||||
<form method="POST" action="{{ route('meet.spaces.store') }}" class="mt-6 space-y-5 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
@endif
|
||||
|
||||
<div class="mt-4 rounded-xl border border-emerald-100 bg-emerald-50 px-4 py-3 text-sm text-emerald-900">
|
||||
Host-led audio room. Assigned speakers can talk; everyone else listens and can request to speak.
|
||||
Host-led audio room. Assigned speakers can talk when the room opens; during a live session, open <strong>People</strong> to invite listeners who raised a hand.
|
||||
@if ($room->setting('invite_only'))
|
||||
This room is private — share the join link only with invited people.
|
||||
@endif
|
||||
|
||||
Reference in New Issue
Block a user