Require acceptance for space speak invites and add instant co-host promotion.
Deploy Ladill Meet / deploy (push) Successful in 40s

Remove duplicate host record button on desktop and split invite-to-speak from make co-host so listeners must accept before speaking while co-host is applied immediately.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-04 00:50:58 +00:00
co-authored by Cursor
parent 46af4f93b6
commit 2fae2e7ed8
6 changed files with 338 additions and 14 deletions
+101 -2
View File
@@ -124,6 +124,10 @@ function meetRoom() {
speakDismissUrl: el.dataset.speakDismissUrl,
spacePromoteUrl: el.dataset.spacePromoteUrl,
spaceDemoteUrl: el.dataset.spaceDemoteUrl,
spaceSpeakAcceptUrl: el.dataset.spaceSpeakAcceptUrl,
spaceSpeakDeclineUrl: el.dataset.spaceSpeakDeclineUrl,
spaceSpeakDismissUrl: el.dataset.spaceSpeakDismissUrl,
spaceCoHostUrl: el.dataset.spaceCoHostUrl,
recordingStartUrl: el.dataset.recordingStartUrl,
recordingStopUrl: el.dataset.recordingStopUrl,
lockUrl: el.dataset.lockUrl,
@@ -2175,7 +2179,18 @@ function meetRoom() {
},
handRaisedParticipants() {
return this.inCallParticipants().filter((person) => person.hand_raised && person.role === 'attendee');
return this.inCallParticipants().filter((person) => (
person.hand_raised
&& person.role === 'attendee'
&& !person.speak_requested
));
},
spaceSpeakInvitePendingParticipants() {
return this.inCallParticipants().filter((person) => (
person.role === 'attendee'
&& person.speak_requested
));
},
spaceSpeakerUrl(template, participantUuid) {
@@ -2194,10 +2209,19 @@ function meetRoom() {
if (participant.uuid === this.config.participantUuid) {
this.canPublish = ['host', 'co_host', 'panelist'].includes(participant.role);
this.speakRequested = Boolean(participant.speak_requested);
}
},
async promoteSpaceSpeaker(participantUuid) {
async refreshMediaAccess() {
if (!this.config.configured) {
return;
}
await this.reconnectLiveKit();
},
async inviteSpaceSpeaker(participantUuid) {
const url = this.spaceSpeakerUrl(this.config.spacePromoteUrl, participantUuid);
if (!this.isSpace || !this.isHost || !url) {
return;
@@ -2216,6 +2240,81 @@ function meetRoom() {
this.mergeSessionParticipant(data.participant);
},
async acceptSpaceSpeakInvitation() {
if (!this.isSpace || !this.config.spaceSpeakAcceptUrl) {
return;
}
const res = await fetch(this.config.spaceSpeakAcceptUrl, {
method: 'POST',
headers: this.headers(),
});
if (!res.ok) {
return;
}
const data = await res.json();
this.mergeSessionParticipant(data.participant);
await this.refreshMediaAccess();
},
async declineSpaceSpeakInvitation() {
if (!this.isSpace || !this.config.spaceSpeakDeclineUrl) {
return;
}
const res = await fetch(this.config.spaceSpeakDeclineUrl, {
method: 'POST',
headers: this.headers(),
});
if (!res.ok) {
return;
}
const data = await res.json();
this.mergeSessionParticipant(data.participant);
},
async dismissSpaceSpeakInvitation(participantUuid) {
const url = this.spaceSpeakerUrl(this.config.spaceSpeakDismissUrl, 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 promoteSpaceCoHost(participantUuid) {
const url = this.spaceSpeakerUrl(this.config.spaceCoHostUrl, 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) {
+44 -4
View File
@@ -84,6 +84,10 @@
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-space-speak-accept-url="{{ ($isSpace ?? false) ? route('meet.room.space.speak.accept', $session) : '' }}"
data-space-speak-decline-url="{{ ($isSpace ?? false) ? route('meet.room.space.speak.decline', $session) : '' }}"
data-space-speak-dismiss-url="{{ ($isSpace ?? false) && ($participant->isHost() || ($canManageConference ?? false)) ? route('meet.room.space.speak.dismiss', [$session, '__UUID__']) : '' }}"
data-space-co-host-url="{{ ($isSpace ?? false) && ($participant->isHost() || ($canManageConference ?? false)) ? route('meet.room.space.co-host', [$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) }}"
@@ -368,6 +372,16 @@
You can speak your microphone is available
</p>
</div>
<div x-show="isSpace && speakRequested && !canPublish" x-cloak
class="absolute inset-x-0 -top-14 flex justify-center px-4 sm:-top-16">
<div class="flex flex-wrap items-center justify-center gap-2 rounded-2xl bg-indigo-600/90 px-4 py-2.5 text-sm text-white shadow-lg ring-1 ring-indigo-400/40">
<span class="font-medium">You've been invited to speak</span>
<button type="button" @click="acceptSpaceSpeakInvitation()"
class="rounded-lg bg-white px-3 py-1 text-xs font-semibold text-indigo-700 hover:bg-indigo-50">Accept</button>
<button type="button" @click="declineSpaceSpeakInvitation()"
class="rounded-lg bg-indigo-800/80 px-3 py-1 text-xs font-medium text-indigo-100 hover:bg-indigo-800">Decline</button>
</div>
</div>
<div class="meet-toolbar mx-auto max-w-5xl">
<div class="meet-toolbar__track sm:px-0">
<div class="meet-toolbar__primary sm:contents">
@@ -434,7 +448,7 @@
@if ($participant->isHost())
<button @click="toggleRecording()" type="button"
x-show="isSpace"
class="rounded-full p-3 transition-colors"
class="rounded-full p-3 transition-colors sm:hidden"
: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>
@@ -651,12 +665,33 @@
<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)"
<button type="button" @click="inviteSpaceSpeaker(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="isSpace && isHost && spaceSpeakInvitePendingParticipants().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-sky-300">
Invited to speak
<span class="ml-1 rounded-full bg-sky-500/20 px-1.5 py-0.5 text-[10px] text-sky-200"
x-text="spaceSpeakInvitePendingParticipants().length"></span>
</p>
<template x-for="person in spaceSpeakInvitePendingParticipants()" :key="'space-invite-'+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-sky-500/20 text-sm font-semibold text-sky-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-sky-200/80">Waiting for response</span>
</span>
<button type="button" @click="dismissSpaceSpeakInvitation(person.uuid)"
class="rounded-lg bg-slate-700 px-2.5 py-1 text-xs font-medium text-slate-200 hover:bg-slate-600">Cancel invite</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">
@@ -738,12 +773,17 @@
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)"
<button x-show="isSpace && isHost && person.role === 'attendee' && !person.speak_requested"
type="button" @click="inviteSpaceSpeaker(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="promoteSpaceCoHost(person.uuid)"
class="rounded-lg bg-violet-600 px-2 py-1 text-[10px] font-medium text-white hover:bg-violet-500">
Make co-host
</button>
<button x-show="isSpace && isHost && (person.role === 'panelist' || person.role === 'co_host')"
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