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())
- @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('>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);
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'));
}
}