From 777a3e9126750305dbe9c56da74ed874f3822f8a Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sat, 4 Jul 2026 01:10:12 +0000 Subject: [PATCH] Restyle audio room speaker cards like Spaces and add room chat to the toolbar. 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 --- app/Services/Meet/SpaceService.php | 3 +- resources/css/app.css | 64 ++++++++++++++++++++ resources/js/meet-room.js | 75 ++++++++++++++++++++---- resources/views/meet/room/show.blade.php | 27 +++++---- tests/Feature/MeetWebTest.php | 4 +- tests/Unit/SpaceServiceTest.php | 5 +- 6 files changed, 150 insertions(+), 28 deletions(-) diff --git a/app/Services/Meet/SpaceService.php b/app/Services/Meet/SpaceService.php index 22a46f1..4e0f2ff 100644 --- a/app/Services/Meet/SpaceService.php +++ b/app/Services/Meet/SpaceService.php @@ -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', diff --git a/resources/css/app.css b/resources/css/app.css index 8573dd9..458734e 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -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; diff --git a/resources/js/meet-room.js b/resources/js/meet-room.js index c331916..356efc5 100644 --- a/resources/js/meet-room.js +++ b/resources/js/meet-room.js @@ -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 ` +
+
+
+ +
+ + + +
`; + }, + participantTileShellHtml() { return `
@@ -1019,14 +1051,26 @@ 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; - 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(); + + 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'); }, diff --git a/resources/views/meet/room/show.blade.php b/resources/views/meet/room/show.blade.php index c1ef4e2..03c5d93 100644 --- a/resources/views/meet/room/show.blade.php +++ b/resources/views/meet/room/show.blade.php @@ -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 @@
-
+
@@ -450,12 +450,12 @@ @include('meet.room.partials.meet-icon', ['icon' => 'react']) @if ($isSpace ?? false) - @if ($participant->isHost()) + @endif @if (($isSpace ?? false) && $participant->isHost())
- @unless ($isSpace ?? false) - + -

Messages are visible to everyone in the call

+

Messages are visible to everyone in the {{ ($isSpace ?? false) ? 'room' : 'call' }}

@@ -627,7 +635,6 @@
- @endunless @unless ($isSpace ?? false) diff --git a/tests/Feature/MeetWebTest.php b/tests/Feature/MeetWebTest.php index e4c23d8..1c2aa04 100644 --- a/tests/Feature/MeetWebTest.php +++ b/tests/Feature/MeetWebTest.php @@ -1301,8 +1301,8 @@ class MeetWebTest extends TestCase $response->assertOk(); $response->assertSee('data-is-space="1"', false); - $response->assertDontSee('title="Chat"', false); - $response->assertDontSee('>MoreassertSee('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); diff --git a/tests/Unit/SpaceServiceTest.php b/tests/Unit/SpaceServiceTest.php index 3172e2c..3b29342 100644 --- a/tests/Unit/SpaceServiceTest.php +++ b/tests/Unit/SpaceServiceTest.php @@ -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')); } }