type !== 'space') { return $fallbackRole; } if ($user && $user->ownerRef() === $room->host_user_ref) { return 'host'; } $speakerRefs = (array) $room->setting('speaker_refs', []); if ($user && in_array($user->ownerRef(), $speakerRefs, true)) { return 'panelist'; } return 'attendee'; } public function canPublish(Room $room, Participant $participant): bool { if ($room->type !== 'space') { return true; } return in_array($participant->role, ['host', 'co_host', 'panelist'], true); } public function isListener(Room $room, Participant $participant): bool { return $room->type === 'space' && $participant->role === 'attendee'; } public function isAudioOnly(Room $room): bool { return $room->isSpace() || (bool) $room->setting('audio_only', false); } /** * Whether a live-room feature is available for this room type. * Audio rooms (Spaces) mirror Twitter Spaces — no breakouts, chat, Q&A, etc. */ public function allowsRoomFeature(Room $room, string $feature): bool { if (! $room->isSpace()) { return true; } return ! in_array($feature, [ 'breakouts', 'chat', 'qa', 'polls', 'files', 'whiteboard', 'programme', 'live_stream', 'lock', ], true); } public function inviteToSpeak(Room $room, Participant $participant): Participant { abort_unless($room->isSpace(), 422); abort_unless(in_array($participant->role, ['attendee', 'guest', 'participant'], true), 422); abort_unless(! $participant->speak_requested, 422, 'An invitation is already pending.'); $participant->update([ 'speak_requested' => true, 'hand_raised' => false, ]); return $participant->fresh(); } public function acceptSpeakInvitation(Room $room, Participant $participant): Participant { abort_unless($room->isSpace(), 422); abort_unless(in_array($participant->role, ['attendee', 'guest', 'participant'], true), 422); abort_unless($participant->speak_requested, 422, 'No speak invitation to accept.'); return $this->promoteToSpeaker($room, $participant); } public function declineSpeakInvitation(Room $room, Participant $participant): Participant { abort_unless($room->isSpace(), 422); abort_unless($participant->speak_requested, 422); $participant->update(['speak_requested' => false]); return $participant->fresh(); } public function dismissSpeakInvitation(Room $room, Participant $participant): Participant { abort_unless($room->isSpace(), 422); abort_unless($participant->speak_requested, 422); $participant->update(['speak_requested' => false]); return $participant->fresh(); } public function promoteToSpeaker(Room $room, Participant $participant): Participant { abort_unless($room->isSpace(), 422); abort_unless(in_array($participant->role, ['attendee', 'guest', 'participant'], true), 422); $participant->update([ 'role' => 'panelist', 'hand_raised' => false, 'speak_requested' => false, ]); if ($participant->user_ref) { $speakerRefs = collect((array) $room->setting('speaker_refs', [])) ->push($participant->user_ref) ->unique() ->values() ->all(); $room->update([ 'settings' => array_merge($room->settings ?? [], [ 'speaker_refs' => $speakerRefs, ]), ]); } return $participant->fresh(); } public function promoteToCoHost(Room $room, Participant $participant): Participant { abort_unless($room->isSpace(), 422); abort_unless($participant->role === 'panelist', 422); $participant->update(['role' => 'co_host']); return $participant->fresh(); } public function demoteFromSpeaker(Room $room, Participant $participant): Participant { abort_unless($room->isSpace(), 422); abort_unless(in_array($participant->role, ['panelist', 'co_host'], true), 422); $participant->update([ 'role' => 'attendee', 'speak_requested' => false, ]); if ($participant->user_ref) { $speakerRefs = collect((array) $room->setting('speaker_refs', [])) ->reject(fn (string $ref) => $ref === $participant->user_ref) ->values() ->all(); $room->update([ 'settings' => array_merge($room->settings ?? [], [ 'speaker_refs' => $speakerRefs, ]), ]); } return $participant->fresh(); } }