Improve conference attendee controls and stage participant strip.
Deploy Ladill Meet / deploy (push) Successful in 58s
Deploy Ladill Meet / deploy (push) Successful in 58s
Give attendees reaction buttons instead of media controls, gate applause audio until multiple people clap, show the avatar strip in panel layout, and remove the plenary stage subtitle. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+154
-13
@@ -19,6 +19,15 @@ function meetRoom() {
|
||||
let initialized = false;
|
||||
let recordingMediaRecorder = null;
|
||||
let recordingChunks = [];
|
||||
let seenReactionKeys = new Set();
|
||||
let applauseClapTimes = [];
|
||||
let applauseAudio = null;
|
||||
let lastApplauseSoundAt = 0;
|
||||
|
||||
const APPLAUSE_WINDOW_MS = 4000;
|
||||
const APPLAUSE_MIN_CLAPPERS = 2;
|
||||
const APPLAUSE_SOUND_COOLDOWN_MS = 3000;
|
||||
const APPLAUSE_EMOJIS = new Set(['👍', '👏', '🙌']);
|
||||
|
||||
return {
|
||||
isMuted: true,
|
||||
@@ -77,6 +86,7 @@ function meetRoom() {
|
||||
activeSpeakerIds: [],
|
||||
roomHost: null,
|
||||
floatingReactions: [],
|
||||
reactionsInitialized: false,
|
||||
pollTimer: null,
|
||||
config: {},
|
||||
|
||||
@@ -120,6 +130,8 @@ function meetRoom() {
|
||||
this.passcode = el.dataset.passcode || '';
|
||||
this.roomTitle = el.dataset.roomTitle || '';
|
||||
|
||||
this.initApplauseAudio(el.dataset.applauseAudio || '');
|
||||
|
||||
this.isHost = el.dataset.isHost === '1';
|
||||
this.canPublish = el.dataset.canPublish !== '0';
|
||||
this.isWebinar = el.dataset.isWebinar === '1';
|
||||
@@ -686,16 +698,40 @@ function meetRoom() {
|
||||
|
||||
return this.inCallParticipants()
|
||||
.filter((person) => person.uuid !== spotlightUuid)
|
||||
.sort((a, b) => {
|
||||
const aSpeaker = this.isSpeakerPerson(a);
|
||||
const bSpeaker = this.isSpeakerPerson(b);
|
||||
.sort((a, b) => this.compareStripParticipants(a, b));
|
||||
},
|
||||
|
||||
if (aSpeaker !== bSpeaker) {
|
||||
return aSpeaker ? -1 : 1;
|
||||
panelStripParticipants() {
|
||||
const onPanelRefs = new Set(this.getActivePanelSpeakerRefs());
|
||||
|
||||
return this.inCallParticipants()
|
||||
.filter((person) => {
|
||||
if (!this.isSpeakerPerson(person)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return a.display_name.localeCompare(b.display_name);
|
||||
});
|
||||
return !person.user_ref || !onPanelRefs.has(person.user_ref);
|
||||
})
|
||||
.sort((a, b) => this.compareStripParticipants(a, b));
|
||||
},
|
||||
|
||||
compareStripParticipants(a, b) {
|
||||
const aSpeaker = this.isSpeakerPerson(a);
|
||||
const bSpeaker = this.isSpeakerPerson(b);
|
||||
|
||||
if (aSpeaker !== bSpeaker) {
|
||||
return aSpeaker ? -1 : 1;
|
||||
}
|
||||
|
||||
return a.display_name.localeCompare(b.display_name);
|
||||
},
|
||||
|
||||
stripParticipants() {
|
||||
if (this.stageLayout === 'panel') {
|
||||
return this.panelStripParticipants();
|
||||
}
|
||||
|
||||
return this.plenaryStripParticipants();
|
||||
},
|
||||
|
||||
createStripInitials(person) {
|
||||
@@ -841,11 +877,11 @@ function meetRoom() {
|
||||
|
||||
strip.replaceChildren();
|
||||
|
||||
if (!this.usesStageLayout || this.stageLayout !== 'plenary') {
|
||||
if (!this.usesStageLayout) {
|
||||
return;
|
||||
}
|
||||
|
||||
const audience = this.plenaryStripParticipants();
|
||||
const audience = this.stripParticipants();
|
||||
if (audience.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -1644,7 +1680,7 @@ function meetRoom() {
|
||||
},
|
||||
|
||||
async toggleMute() {
|
||||
if (!room) return;
|
||||
if (!room || !this.canPublish) return;
|
||||
await this.ensureAudioPlayback();
|
||||
|
||||
const enabled = room.localParticipant.isMicrophoneEnabled;
|
||||
@@ -1671,7 +1707,7 @@ function meetRoom() {
|
||||
},
|
||||
|
||||
async toggleVideo() {
|
||||
if (!room || this.audioOnly) return;
|
||||
if (!room || this.audioOnly || !this.canPublish) return;
|
||||
await this.ensureAudioPlayback();
|
||||
const enabled = room.localParticipant.isCameraEnabled;
|
||||
await room.localParticipant.setCameraEnabled(!enabled);
|
||||
@@ -1690,7 +1726,7 @@ function meetRoom() {
|
||||
},
|
||||
|
||||
async toggleScreenShare() {
|
||||
if (!room || this.audioOnly) return;
|
||||
if (!room || this.audioOnly || !this.canPublish) return;
|
||||
this.isScreenSharing = !this.isScreenSharing;
|
||||
await room.localParticipant.setScreenShareEnabled(this.isScreenSharing);
|
||||
if (!this.isScreenSharing) {
|
||||
@@ -1844,6 +1880,10 @@ function meetRoom() {
|
||||
body: JSON.stringify({ emoji }),
|
||||
});
|
||||
this.showFloatingReaction(kind);
|
||||
|
||||
if (kind === 'thumbs') {
|
||||
this.registerApplauseClap(this.displayName);
|
||||
}
|
||||
},
|
||||
|
||||
async poll() {
|
||||
@@ -1884,6 +1924,7 @@ function meetRoom() {
|
||||
if (data.broadcasts?.length) {
|
||||
this.breakoutBroadcast = data.broadcasts[0].body;
|
||||
}
|
||||
this.processReactionsFromPoll(data.reactions || []);
|
||||
} catch (e) {
|
||||
// ignore poll errors
|
||||
}
|
||||
@@ -1935,7 +1976,7 @@ function meetRoom() {
|
||||
return 'Multiple speakers shown together in a split-screen grid.';
|
||||
}
|
||||
|
||||
return 'One main speaker on stage. Others appear in the strip below.';
|
||||
return '';
|
||||
},
|
||||
|
||||
activePanelName() {
|
||||
@@ -2045,6 +2086,106 @@ function meetRoom() {
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
initApplauseAudio(url) {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
applauseAudio = new Audio(url);
|
||||
applauseAudio.preload = 'auto';
|
||||
},
|
||||
|
||||
isApplauseEmoji(emoji) {
|
||||
return APPLAUSE_EMOJIS.has(emoji);
|
||||
},
|
||||
|
||||
registerApplauseClap(senderName) {
|
||||
if (!senderName) {
|
||||
return;
|
||||
}
|
||||
|
||||
applauseClapTimes.push({ at: Date.now(), sender: senderName });
|
||||
this.maybePlayApplauseSound();
|
||||
},
|
||||
|
||||
maybePlayApplauseSound() {
|
||||
if (!applauseAudio?.src) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
applauseClapTimes = applauseClapTimes.filter((entry) => now - entry.at <= APPLAUSE_WINDOW_MS);
|
||||
|
||||
const clappers = new Set(applauseClapTimes.map((entry) => entry.sender));
|
||||
if (clappers.size < APPLAUSE_MIN_CLAPPERS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (now - lastApplauseSoundAt < APPLAUSE_SOUND_COOLDOWN_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
const volume = 0.35 + Math.min(clappers.size - APPLAUSE_MIN_CLAPPERS + 1, 4) * 0.12;
|
||||
this.playApplauseSound(volume);
|
||||
},
|
||||
|
||||
playApplauseSound(volume = 0.7) {
|
||||
if (!applauseAudio?.src) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
applauseAudio.volume = Math.min(1, Math.max(0.2, volume));
|
||||
applauseAudio.currentTime = 0;
|
||||
applauseAudio.play().catch(() => {});
|
||||
lastApplauseSoundAt = Date.now();
|
||||
} catch {
|
||||
// Browser blocked playback until user gesture.
|
||||
}
|
||||
},
|
||||
|
||||
processReactionsFromPoll(reactions) {
|
||||
if (!Array.isArray(reactions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.reactionsInitialized) {
|
||||
reactions.forEach((reaction) => {
|
||||
seenReactionKeys.add(`${reaction.created_at}|${reaction.sender_name}|${reaction.emoji}`);
|
||||
});
|
||||
this.reactionsInitialized = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
reactions.forEach((reaction) => {
|
||||
const key = `${reaction.created_at}|${reaction.sender_name}|${reaction.emoji}`;
|
||||
if (seenReactionKeys.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
seenReactionKeys.add(key);
|
||||
|
||||
if (this.isApplauseEmoji(reaction.emoji)) {
|
||||
if (reaction.sender_name !== this.displayName) {
|
||||
this.showFloatingReaction('thumbs');
|
||||
}
|
||||
|
||||
this.registerApplauseClap(reaction.sender_name);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (reaction.emoji === '❤️' && reaction.sender_name !== this.displayName) {
|
||||
this.showFloatingReaction('heart');
|
||||
}
|
||||
});
|
||||
|
||||
if (seenReactionKeys.size > 200) {
|
||||
seenReactionKeys = new Set([...seenReactionKeys].slice(-100));
|
||||
}
|
||||
},
|
||||
|
||||
headers() {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
Reference in New Issue
Block a user