Restyle audio room speaker cards like Spaces and add room chat to the toolbar.
Deploy Ladill Meet / deploy (push) Successful in 49s
Deploy Ladill Meet / deploy (push) Successful in 49s
Use a three-column avatar grid with role labels and speaking/muted indicators, enable chat in audio rooms, and swap the mobile People nav button for Chat since People stays in the header. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -51,7 +51,7 @@ class SpaceService
|
||||
|
||||
/**
|
||||
* Whether a live-room feature is available for this room type.
|
||||
* Audio rooms (Spaces) mirror Twitter Spaces — no breakouts, chat, Q&A, etc.
|
||||
* Audio rooms (Spaces) mirror Twitter Spaces — no breakouts, Q&A, etc.
|
||||
*/
|
||||
public function allowsRoomFeature(Room $room, string $feature): bool
|
||||
{
|
||||
@@ -61,7 +61,6 @@ class SpaceService
|
||||
|
||||
return ! in_array($feature, [
|
||||
'breakouts',
|
||||
'chat',
|
||||
'qa',
|
||||
'polls',
|
||||
'files',
|
||||
|
||||
@@ -370,6 +370,70 @@ html.meet-room-page {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.meet-room-page .meet-audio-grid.meet-space-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1.25rem 0.75rem;
|
||||
padding: 1rem 0.75rem 1.5rem;
|
||||
max-width: 24rem;
|
||||
margin-inline: auto;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.meet-room-page .meet-audio-grid.meet-space-grid {
|
||||
max-width: 28rem;
|
||||
gap: 1.5rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.meet-room-page .meet-audio-grid.meet-space-grid [data-participant-tile] {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.meet-room-page .meet-space-tile {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.meet-room-page .meet-space-tile.meet-tile-speaking {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.meet-room-page .meet-space-tile.meet-tile-speaking [data-tile-avatar-circle] {
|
||||
box-shadow: 0 0 0 2px rgb(56 189 248 / 0.85);
|
||||
}
|
||||
|
||||
.meet-room-page .meet-space-tile [data-tile-name] {
|
||||
width: 100%;
|
||||
padding-inline: 0.125rem;
|
||||
}
|
||||
|
||||
.meet-room-page .meet-space-tile [data-tile-role-row] {
|
||||
min-height: 1rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.meet-room-page .meet-space-tile [data-tile-role] {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.meet-room-page .meet-space-tile [data-tile-avatar-circle] {
|
||||
height: 4.75rem;
|
||||
width: 4.75rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.meet-room-page .meet-space-tile [data-tile-avatar-circle] {
|
||||
height: 5.25rem;
|
||||
width: 5.25rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.meet-room-page .meet-audio-tile {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
||||
@@ -711,7 +711,10 @@ function meetRoom() {
|
||||
const displayName = this.displayNameForParticipant(participant);
|
||||
const avatarUrl = this.avatarUrlForParticipant(participant);
|
||||
const isAudioTile = tile.classList.contains('meet-audio-tile');
|
||||
const sizeClass = isAudioTile ? 'h-[4.5rem] w-[4.5rem] text-xl' : 'h-20 w-20 text-2xl';
|
||||
const isSpaceTile = tile.classList.contains('meet-space-tile');
|
||||
const sizeClass = isSpaceTile
|
||||
? 'h-[4.75rem] w-[4.75rem] text-lg sm:h-[5.25rem] sm:w-[5.25rem] sm:text-xl'
|
||||
: isAudioTile ? 'h-[4.5rem] w-[4.5rem] text-xl' : 'h-20 w-20 text-2xl';
|
||||
|
||||
circle.replaceChildren();
|
||||
|
||||
@@ -989,6 +992,35 @@ function meetRoom() {
|
||||
return room.remoteParticipants.get(identity) || null;
|
||||
},
|
||||
|
||||
spaceRoleLabel(role) {
|
||||
return {
|
||||
host: 'Host',
|
||||
co_host: 'Co-host',
|
||||
panelist: 'Speaker',
|
||||
}[role] || '';
|
||||
},
|
||||
|
||||
spaceSpeakerTileShellHtml() {
|
||||
return `
|
||||
<div class="flex w-full justify-center">
|
||||
<div data-tile-avatar-circle class="flex h-[4.75rem] w-[4.75rem] shrink-0 items-center justify-center rounded-full text-lg font-semibold text-white"></div>
|
||||
</div>
|
||||
<span data-tile-name class="mt-2 max-w-full truncate text-center text-sm font-bold leading-tight text-white"></span>
|
||||
<div data-tile-role-row class="mt-1 flex max-w-full items-center justify-center gap-1">
|
||||
<span data-tile-speaking class="hidden shrink-0 text-sky-400" title="Speaking">
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
||||
<rect class="meet-speaking-bar meet-speaking-bar--1" x="1" y="5" width="2.5" height="6" rx="1"/>
|
||||
<rect class="meet-speaking-bar meet-speaking-bar--2" x="6.75" y="3" width="2.5" height="10" rx="1"/>
|
||||
<rect class="meet-speaking-bar meet-speaking-bar--3" x="12.5" y="6" width="2.5" height="4" rx="1"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span data-tile-mic class="hidden shrink-0 text-slate-500" title="Muted">
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 0 1 6 0v8.25a3 3 0 0 1-3 3Z"/><path stroke-linecap="round" stroke-linejoin="round" d="m3 3 18 18"/></svg>
|
||||
</span>
|
||||
<span data-tile-role class="truncate text-xs text-slate-400"></span>
|
||||
</div>`;
|
||||
},
|
||||
|
||||
participantTileShellHtml() {
|
||||
return `
|
||||
<div data-tile-body class="relative min-h-0 flex-1">
|
||||
@@ -1019,15 +1051,27 @@ function meetRoom() {
|
||||
|
||||
ensureParticipantTileShell(identity) {
|
||||
let tile = this.findParticipantTile(identity);
|
||||
const wantsSpaceTile = this.isSpace && !this.usesStageLayout;
|
||||
|
||||
if (tile && wantsSpaceTile !== tile.classList.contains('meet-space-tile')) {
|
||||
tile.remove();
|
||||
tile = null;
|
||||
}
|
||||
|
||||
if (!tile) {
|
||||
tile = document.createElement('div');
|
||||
tile.dataset.participantTile = identity;
|
||||
|
||||
if (wantsSpaceTile) {
|
||||
tile.className = 'meet-space-tile flex w-full min-w-0 flex-col items-center px-1 py-1';
|
||||
tile.innerHTML = this.spaceSpeakerTileShellHtml();
|
||||
} else {
|
||||
tile.className = this.audioOnly
|
||||
? 'meet-audio-tile relative flex aspect-square w-full flex-col overflow-hidden rounded-2xl bg-slate-900'
|
||||
: 'relative flex aspect-video w-full flex-col overflow-hidden rounded-xl bg-slate-900 min-h-0';
|
||||
tile.innerHTML = this.participantTileShellHtml();
|
||||
}
|
||||
}
|
||||
|
||||
return tile;
|
||||
},
|
||||
@@ -1040,15 +1084,24 @@ function meetRoom() {
|
||||
|
||||
const pseudoParticipant = { identity: person.uuid, name: person.display_name };
|
||||
this.updateTileAvatarCircle(tile, pseudoParticipant);
|
||||
tile.querySelector('[data-tile-name]').textContent = person.display_name;
|
||||
|
||||
const speaking = this.isSpeaking(person.uuid);
|
||||
tile.classList.toggle('meet-tile-speaking', speaking);
|
||||
tile.querySelector('[data-tile-speaking]')?.classList.toggle('hidden', !speaking);
|
||||
|
||||
const muted = liveParticipant
|
||||
? !liveParticipant.isMicrophoneEnabled
|
||||
: Boolean(person.is_muted);
|
||||
|
||||
if (tile.classList.contains('meet-space-tile')) {
|
||||
tile.querySelector('[data-tile-name]').textContent = person.display_name;
|
||||
tile.querySelector('[data-tile-role]').textContent = this.spaceRoleLabel(person.role);
|
||||
tile.classList.toggle('meet-tile-speaking', speaking);
|
||||
tile.querySelector('[data-tile-speaking]')?.classList.toggle('hidden', !speaking);
|
||||
tile.querySelector('[data-tile-mic]')?.classList.toggle('hidden', !muted || speaking);
|
||||
return;
|
||||
}
|
||||
|
||||
tile.querySelector('[data-tile-name]').textContent = person.display_name;
|
||||
tile.classList.toggle('meet-tile-speaking', speaking);
|
||||
tile.querySelector('[data-tile-speaking]')?.classList.toggle('hidden', !speaking);
|
||||
tile.querySelector('[data-tile-mic]')?.classList.toggle('hidden', !muted);
|
||||
tile.querySelector('[data-tile-cam]')?.classList.add('hidden');
|
||||
},
|
||||
@@ -2755,10 +2808,6 @@ function meetRoom() {
|
||||
},
|
||||
|
||||
toggleChat() {
|
||||
if (this.isSpace) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.toggleSidebarPanel('chat');
|
||||
},
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
data-is-host="{{ ($canManageConference ?? $participant->isHost()) ? '1' : '0' }}"
|
||||
data-poll-url="{{ route('meet.room.poll', $session) }}"
|
||||
data-ended-url="{{ route('meet.ended', $session) }}"
|
||||
data-chat-url="{{ ($isSpace ?? false) ? '' : route('meet.room.chat', $session) }}"
|
||||
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) : '' }}"
|
||||
@@ -339,7 +339,7 @@
|
||||
<main class="relative flex min-h-0 min-w-0 flex-1 flex-col p-4 pb-2 meet-room-main">
|
||||
<div id="meet-stage" class="flex h-full min-h-0 flex-1 flex-col gap-3" :class="usesStageLayout ? 'meet-stage meet-stage--' + stageLayout : ''">
|
||||
<div id="meet-stage-spotlight" class="meet-stage-spotlight relative min-h-0 flex-1 overflow-hidden rounded-xl bg-slate-900"></div>
|
||||
<div id="video-grid" class="min-h-0 min-w-0" :class="audioOnly ? 'meet-audio-grid flex-1' : (usesStageLayout ? 'meet-stage-panel-grid flex-1 h-full' : 'meet-gallery-grid h-full')"></div>
|
||||
<div id="video-grid" class="min-h-0 min-w-0" :class="audioOnly ? ((isSpace ? 'meet-audio-grid meet-space-grid flex-1' : 'meet-audio-grid flex-1')) : (usesStageLayout ? 'meet-stage-panel-grid flex-1 h-full' : 'meet-gallery-grid h-full')"></div>
|
||||
<div id="meet-audience-strip" class="meet-audience-strip shrink-0"></div>
|
||||
</div>
|
||||
<div id="screen-share" class="absolute inset-4 hidden rounded-xl bg-black"></div>
|
||||
@@ -450,12 +450,12 @@
|
||||
@include('meet.room.partials.meet-icon', ['icon' => 'react'])
|
||||
</button>
|
||||
@if ($isSpace ?? false)
|
||||
<button @click="toggleParticipants()" type="button"
|
||||
<button @click="toggleChat()" type="button"
|
||||
data-alpine-toolbar
|
||||
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>
|
||||
:class="sidebarPanel === 'chat' ? 'bg-indigo-600 hover:bg-indigo-500' : 'bg-slate-700 hover:bg-slate-600'"
|
||||
title="Chat">
|
||||
@include('meet.room.partials.meet-icon', ['icon' => 'chat'])
|
||||
</button>
|
||||
@if ($participant->isHost())
|
||||
<button @click="toggleRecording()" type="button"
|
||||
@@ -504,6 +504,15 @@
|
||||
More
|
||||
</button>
|
||||
@endunless
|
||||
@if ($isSpace ?? false)
|
||||
<button @click="toggleChat()" type="button"
|
||||
data-alpine-toolbar
|
||||
class="hidden rounded-full p-3 transition-colors sm:inline-flex"
|
||||
:class="sidebarPanel === 'chat' ? 'bg-indigo-600 hover:bg-indigo-500' : 'bg-slate-700 hover:bg-slate-600'"
|
||||
title="Chat">
|
||||
@include('meet.room.partials.meet-icon', ['icon' => 'chat'])
|
||||
</button>
|
||||
@endif
|
||||
@if (($isSpace ?? false) && $participant->isHost())
|
||||
<button @click="toggleRecording()" type="button"
|
||||
data-alpine-toolbar
|
||||
@@ -595,10 +604,9 @@
|
||||
@click="closeSidebar()"
|
||||
aria-hidden="true"></div>
|
||||
|
||||
@unless ($isSpace ?? false)
|
||||
<x-meet.room.panel show="sidebarPanel === 'chat'" title="{{ $sessionNounTitle }} chat">
|
||||
<x-meet.room.panel show="sidebarPanel === 'chat'" title="{{ ($isSpace ?? false) ? 'Room chat' : $sessionNounTitle.' chat' }}">
|
||||
<x-slot:subtitle>
|
||||
<p>Messages are visible to everyone in the call</p>
|
||||
<p>Messages are visible to everyone in the {{ ($isSpace ?? false) ? 'room' : 'call' }}</p>
|
||||
</x-slot:subtitle>
|
||||
|
||||
<div class="flex min-h-0 flex-1 flex-col">
|
||||
@@ -627,7 +635,6 @@
|
||||
</form>
|
||||
</div>
|
||||
</x-meet.room.panel>
|
||||
@endunless
|
||||
|
||||
@unless ($isSpace ?? false)
|
||||
<x-meet.room.panel show="sidebarPanel === 'programme'" title="Programme">
|
||||
|
||||
@@ -1301,8 +1301,8 @@ class MeetWebTest extends TestCase
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('data-is-space="1"', false);
|
||||
$response->assertDontSee('title="Chat"', false);
|
||||
$response->assertDontSee('>More</', false);
|
||||
$response->assertSee('title="Chat"', false);
|
||||
$response->assertDontSee('title="People"', false);
|
||||
$response->assertDontSee('Start breakouts', false);
|
||||
$response->assertDontSee('Share files', false);
|
||||
$response->assertDontSee('View the room lineup from Events', false);
|
||||
|
||||
@@ -14,9 +14,12 @@ class SpaceServiceTest extends TestCase
|
||||
$space = new Room(['type' => 'space']);
|
||||
$meeting = new Room(['type' => 'meeting']);
|
||||
|
||||
foreach (['breakouts', 'chat', 'qa', 'polls', 'files', 'whiteboard', 'programme', 'live_stream', 'lock'] as $feature) {
|
||||
foreach (['breakouts', 'qa', 'polls', 'files', 'whiteboard', 'programme', 'live_stream', 'lock'] as $feature) {
|
||||
$this->assertFalse($service->allowsRoomFeature($space, $feature), $feature);
|
||||
$this->assertTrue($service->allowsRoomFeature($meeting, $feature), $feature);
|
||||
}
|
||||
|
||||
$this->assertTrue($service->allowsRoomFeature($space, 'chat'));
|
||||
$this->assertTrue($service->allowsRoomFeature($meeting, 'chat'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user