feat: audio room stage with NIP-53 presence (kind 10312)
Phase 2 of the Clubhouse/nests integration: when the live-activity channel screen is opened on a kind 30312 MeetingSpaceEvent, render an audio-room "stage" above the chat that shows host/speaker/audience avatars and exposes hand-raise + mute toggles backed by NIP-53 kind 10312 presence events. Quartz: - New MutedTag (`["muted","1"|"0"]`) and `muted()` builder helper for MeetingRoomPresenceEvent. - New MeetingRoomPresenceEvent.build() overload that accepts a 30312 MeetingSpaceEvent root (matching the existing 30313 overload) and optionally encodes hand-raise + mute flags in one call. Amethyst: - AudioRoomStage composable: filters participants from the 30312 event into hosts / speakers / audience, renders avatars, and publishes presence on enter + every 30 s while composed (on dispose it pushes one final lowered-hand presence so peers drop us before timeout). - ChannelView wires AudioRoomStage in next to ShowVideoStreaming; the latter is a no-op for non-30311 events so non-audio rooms are unaffected. No audio is captured yet — the mic toggle is a Nostr-only signal until Phase 3 brings the MoQ/WebTransport transport. https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D
This commit is contained in:
+258
@@ -0,0 +1,258 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Mic
|
||||||
|
import androidx.compose.material.icons.filled.MicOff
|
||||||
|
import androidx.compose.material.icons.filled.PanTool
|
||||||
|
import androidx.compose.material.icons.outlined.PanTool
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.FilledIconButton
|
||||||
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ROLE
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clubhouse-style audio-room "stage" rendered in place of the video player when
|
||||||
|
* the underlying activity is a NIP-53 kind 30312 [MeetingSpaceEvent].
|
||||||
|
*
|
||||||
|
* Phase 2 scope: shows host + speaker + audience avatars and lets the local user
|
||||||
|
* publish their kind 10312 presence (with hand-raise + mute flags) on relays.
|
||||||
|
* The mic toggle is a Nostr-only signal at this point — actual audio capture
|
||||||
|
* arrives in Phase 3 with the MoQ/WebTransport transport.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AudioRoomStage(
|
||||||
|
baseChannel: LiveActivitiesChannel,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
LoadAddressableNote(baseChannel.address, accountViewModel) { addressableNote ->
|
||||||
|
addressableNote ?: return@LoadAddressableNote
|
||||||
|
val event = addressableNote.event as? MeetingSpaceEvent ?: return@LoadAddressableNote
|
||||||
|
AudioRoomStageContent(event, accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AudioRoomStageContent(
|
||||||
|
event: MeetingSpaceEvent,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
val participants = remember(event) { event.participants() }
|
||||||
|
val hosts = remember(participants) { participants.filter { it.role.equals(ROLE.HOST.code, true) } }
|
||||||
|
val speakers = remember(participants) { participants.filter { it.role.equals(ROLE.SPEAKER.code, true) } }
|
||||||
|
val audience =
|
||||||
|
remember(participants) {
|
||||||
|
participants.filter {
|
||||||
|
!it.role.equals(ROLE.HOST.code, true) &&
|
||||||
|
!it.role.equals(ROLE.SPEAKER.code, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var handRaised by rememberSaveable(event.address().toValue()) { mutableStateOf(false) }
|
||||||
|
var muted by rememberSaveable(event.address().toValue()) { mutableStateOf(true) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val account = accountViewModel.account
|
||||||
|
|
||||||
|
// Publish initial presence on enter and refresh every PRESENCE_REFRESH_MS while composed.
|
||||||
|
LaunchedEffect(event.address().toValue(), handRaised, muted) {
|
||||||
|
publishPresence(account, event, handRaised, muted)
|
||||||
|
while (isActive) {
|
||||||
|
delay(PRESENCE_REFRESH_MS)
|
||||||
|
publishPresence(account, event, handRaised, muted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Best-effort "leave" — re-publish a CLOSED presence so peers see us drop sooner
|
||||||
|
// than the 30 s heartbeat would otherwise allow.
|
||||||
|
DisposableEffect(event.address().toValue()) {
|
||||||
|
onDispose {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
runCatching { publishPresence(account, event, handRaised = false, muted = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
event.room()?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
event.summary()?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hosts.isNotEmpty() || speakers.isNotEmpty()) {
|
||||||
|
StagePeopleRow(
|
||||||
|
label = stringRes(R.string.audio_room_stage),
|
||||||
|
people = hosts + speakers,
|
||||||
|
avatarSize = Size40dp,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audience.isNotEmpty()) {
|
||||||
|
StagePeopleRow(
|
||||||
|
label = stringRes(R.string.audio_room_audience),
|
||||||
|
people = audience,
|
||||||
|
avatarSize = Size35dp,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
FilledTonalIconButton(onClick = { handRaised = !handRaised }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (handRaised) Icons.Filled.PanTool else Icons.Outlined.PanTool,
|
||||||
|
contentDescription =
|
||||||
|
stringRes(
|
||||||
|
if (handRaised) R.string.audio_room_lower_hand else R.string.audio_room_raise_hand,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
StdHorzSpacer
|
||||||
|
FilledIconButton(
|
||||||
|
onClick = { muted = !muted },
|
||||||
|
colors =
|
||||||
|
if (muted) {
|
||||||
|
IconButtonDefaults.filledIconButtonColors()
|
||||||
|
} else {
|
||||||
|
IconButtonDefaults.filledIconButtonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.error,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onError,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (muted) Icons.Filled.MicOff else Icons.Filled.Mic,
|
||||||
|
contentDescription =
|
||||||
|
stringRes(
|
||||||
|
if (muted) R.string.audio_room_unmute else R.string.audio_room_mute,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StagePeopleRow(
|
||||||
|
label: String,
|
||||||
|
people: List<ParticipantTag>,
|
||||||
|
avatarSize: androidx.compose.ui.unit.Dp,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(top = 8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
) {
|
||||||
|
items(items = people, key = { it.pubKey }) {
|
||||||
|
ClickableUserPicture(
|
||||||
|
baseUserHex = it.pubKey,
|
||||||
|
size = avatarSize,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val PRESENCE_REFRESH_MS = 30_000L
|
||||||
|
|
||||||
|
private suspend fun publishPresence(
|
||||||
|
account: com.vitorpamplona.amethyst.model.Account,
|
||||||
|
event: MeetingSpaceEvent,
|
||||||
|
handRaised: Boolean,
|
||||||
|
muted: Boolean,
|
||||||
|
) {
|
||||||
|
runCatching {
|
||||||
|
account.signAndComputeBroadcast(
|
||||||
|
MeetingRoomPresenceEvent.build(
|
||||||
|
root = event,
|
||||||
|
handRaised = handRaised,
|
||||||
|
muted = muted,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
|||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel
|
import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room.AudioRoomStage
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription
|
||||||
@@ -129,6 +130,7 @@ fun LiveActivityChannelView(
|
|||||||
.weight(1f, true),
|
.weight(1f, true),
|
||||||
) {
|
) {
|
||||||
ShowVideoStreaming(channel, accountViewModel)
|
ShowVideoStreaming(channel, accountViewModel)
|
||||||
|
AudioRoomStage(channel, accountViewModel)
|
||||||
LiveStreamTopZappers(channel, accountViewModel, nav)
|
LiveStreamTopZappers(channel, accountViewModel, nav)
|
||||||
LiveStreamGoalHeader(channel, accountViewModel, nav)
|
LiveStreamGoalHeader(channel, accountViewModel, nav)
|
||||||
RefreshingChatroomFeedView(
|
RefreshingChatroomFeedView(
|
||||||
|
|||||||
@@ -489,6 +489,12 @@
|
|||||||
<string name="follow_packs">Follow Packs</string>
|
<string name="follow_packs">Follow Packs</string>
|
||||||
<string name="live_streams">Live Streams</string>
|
<string name="live_streams">Live Streams</string>
|
||||||
<string name="audio_rooms">Audio Rooms</string>
|
<string name="audio_rooms">Audio Rooms</string>
|
||||||
|
<string name="audio_room_stage">Stage</string>
|
||||||
|
<string name="audio_room_audience">Audience</string>
|
||||||
|
<string name="audio_room_raise_hand">Raise hand</string>
|
||||||
|
<string name="audio_room_lower_hand">Lower hand</string>
|
||||||
|
<string name="audio_room_mute">Mute</string>
|
||||||
|
<string name="audio_room_unmute">Unmute</string>
|
||||||
<string name="longs">Videos</string>
|
<string name="longs">Videos</string>
|
||||||
<string name="articles">Articles</string>
|
<string name="articles">Articles</string>
|
||||||
<string name="private_bookmarks">Private Bookmarks</string>
|
<string name="private_bookmarks">Private Bookmarks</string>
|
||||||
|
|||||||
+26
@@ -31,8 +31,10 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
|||||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.MeetingSpaceTag
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.MeetingSpaceTag
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.presence.tags.HandRaisedTag
|
import com.vitorpamplona.quartz.nip53LiveActivities.presence.tags.HandRaisedTag
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.presence.tags.MutedTag
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -53,6 +55,8 @@ class MeetingRoomPresenceEvent(
|
|||||||
|
|
||||||
fun handRaised() = tags.firstNotNullOfOrNull(HandRaisedTag::parse)
|
fun handRaised() = tags.firstNotNullOfOrNull(HandRaisedTag::parse)
|
||||||
|
|
||||||
|
fun muted() = tags.firstNotNullOfOrNull(MutedTag::parse)
|
||||||
|
|
||||||
companion object Companion {
|
companion object Companion {
|
||||||
const val KIND = 10312
|
const val KIND = 10312
|
||||||
const val ALT = "Room Presence tag"
|
const val ALT = "Room Presence tag"
|
||||||
@@ -78,5 +82,27 @@ class MeetingRoomPresenceEvent(
|
|||||||
}
|
}
|
||||||
initializer()
|
initializer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience builder when the parent is a kind 30312 [MeetingSpaceEvent]
|
||||||
|
* (Clubhouse-style audio room). Mirrors the [MeetingRoomEvent] overload so
|
||||||
|
* a participant can publish presence + hand-raise + mute against an audio
|
||||||
|
* room without adapting to the meeting-room (kind 30313) variant.
|
||||||
|
*/
|
||||||
|
fun build(
|
||||||
|
root: MeetingSpaceEvent,
|
||||||
|
handRaised: Boolean? = null,
|
||||||
|
muted: Boolean? = null,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
initializer: TagArrayBuilder<MeetingRoomPresenceEvent>.() -> Unit = {},
|
||||||
|
) = eventTemplate(KIND, "", createdAt) {
|
||||||
|
alt(root.room() ?: ALT)
|
||||||
|
|
||||||
|
roomMeeting(MeetingSpaceTag(root.address(), root.relays().firstOrNull()))
|
||||||
|
|
||||||
|
handRaised?.let { handRaised(it) }
|
||||||
|
muted?.let { muted(it) }
|
||||||
|
initializer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -26,9 +26,12 @@ import com.vitorpamplona.quartz.nip01Core.tags.aTag.toATag
|
|||||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.MeetingSpaceTag
|
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.MeetingSpaceTag
|
||||||
import com.vitorpamplona.quartz.nip53LiveActivities.presence.tags.HandRaisedTag
|
import com.vitorpamplona.quartz.nip53LiveActivities.presence.tags.HandRaisedTag
|
||||||
|
import com.vitorpamplona.quartz.nip53LiveActivities.presence.tags.MutedTag
|
||||||
|
|
||||||
fun TagArrayBuilder<MeetingRoomPresenceEvent>.roomMeeting(rep: MeetingSpaceTag) = addUnique(rep.toTagArray())
|
fun TagArrayBuilder<MeetingRoomPresenceEvent>.roomMeeting(rep: MeetingSpaceTag) = addUnique(rep.toTagArray())
|
||||||
|
|
||||||
fun TagArrayBuilder<MeetingRoomPresenceEvent>.roomMeeting(rep: EventHintBundle<MeetingRoomEvent>) = addUnique(rep.toATag().toATagArray())
|
fun TagArrayBuilder<MeetingRoomPresenceEvent>.roomMeeting(rep: EventHintBundle<MeetingRoomEvent>) = addUnique(rep.toATag().toATagArray())
|
||||||
|
|
||||||
fun TagArrayBuilder<MeetingRoomPresenceEvent>.handRaised(raised: Boolean) = addUnique(HandRaisedTag.assemble(raised))
|
fun TagArrayBuilder<MeetingRoomPresenceEvent>.handRaised(raised: Boolean) = addUnique(HandRaisedTag.assemble(raised))
|
||||||
|
|
||||||
|
fun TagArrayBuilder<MeetingRoomPresenceEvent>.muted(muted: Boolean) = addUnique(MutedTag.assemble(muted))
|
||||||
|
|||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip53LiveActivities.presence.tags
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||||
|
import com.vitorpamplona.quartz.utils.ensure
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mute flag for kind 10312 presence events used by Clubhouse-style audio
|
||||||
|
* rooms (e.g. nostrnests/nests). Carried alongside the optional `hand` tag so
|
||||||
|
* other clients can render the speaker as muted without waiting for the audio
|
||||||
|
* transport to drop frames.
|
||||||
|
*/
|
||||||
|
class MutedTag {
|
||||||
|
companion object Companion {
|
||||||
|
const val TAG_NAME = "muted"
|
||||||
|
|
||||||
|
fun parse(tag: Array<String>): Boolean? {
|
||||||
|
ensure(tag.has(1)) { return null }
|
||||||
|
ensure(tag[0] == TAG_NAME) { return null }
|
||||||
|
ensure(tag[1].isNotEmpty()) { return null }
|
||||||
|
return tag[1] == "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assemble(muted: Boolean) = arrayOf(TAG_NAME, if (muted) "1" else "0")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user