feat(audio-rooms): surface in home live-bubble row
When a follow chats in a kind-30312 audio room, the room now
appears in the live-bubble row at the top of home — same place
streaming kind-30311 + ephemeral chats already show up.
The plumbing already exists below the surface:
LocalCache.consume(LiveActivitiesChatMessageEvent) calls
getOrCreateLiveChannel(activityAddress) regardless of whether
the address is a kind-30311 or kind-30312, so liveChatChannels
already grows entries for audio rooms whenever a follow chats
in one. The home filter just rejected them at the
acceptableChatEvent guard because it required `info.status() ==
LIVE` — and `info` is hard-typed to LiveActivitiesEvent so it's
null for audio rooms.
Three changes:
* HomeLiveFilter.acceptableChatEvent — branch on the chat's
activityAddress.kind. For kind-30311 keep the existing
info.status() == LIVE path; for kind-30312 read the
addressable's MeetingSpaceEvent and accept OPEN/PRIVATE.
"Closed" rooms drop out of the bubble even if a follow is
chatting in the (now-archived) chat.
* RenderLiveActivityBubble — when channel.address.kind ==
30312, pull room title from the addressable so the bubble
label reads "Lounge" instead of "naddr1abc…", and tap-launch
AudioRoomActivity directly (one-tap into the room) instead
of routing to ChannelView. Falls back to the channel route
if the address is malformed.
* LiveStatusIndicator.checkChannelIsOnline — the red live-dot
surfaces for kind-30312 channels whenever their addressable
is OPEN/PRIVATE, mirroring what status==LIVE means for
streaming.
Audio rooms with no chat yet still don't surface (would require
populating channel.info, which would mean widening the channel
model — deferred to a later cycle per the architectural
discussion). The chat-driven case covers nostrnests' typical UX:
a host opens a room, audience members start chatting, the bubble
pulls in their followers.
This commit is contained in:
+36
-3
@@ -38,8 +38,11 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag as MeetingSpaceStatusTag
|
||||
|
||||
class HomeLiveFilter(
|
||||
val account: Account,
|
||||
@@ -160,10 +163,40 @@ class HomeLiveFilter(
|
||||
val noteEvent = note.event
|
||||
|
||||
if (noteEvent is LiveActivitiesChatMessageEvent) {
|
||||
val stream = noteEvent.activityAddress() ?: return false
|
||||
val streamChannel = LocalCache.getLiveActivityChannelIfExists(stream) ?: return false
|
||||
val activity = noteEvent.activityAddress() ?: return false
|
||||
|
||||
if (streamChannel.info?.status() != StatusTag.STATUS.LIVE) return false
|
||||
// Two flavors of "live" share the same kind-1311 chat
|
||||
// channel: streaming (kind-30311, status=LIVE) and audio
|
||||
// rooms (kind-30312, status=OPEN/PRIVATE). The chat
|
||||
// surfaces in liveChatChannels for both because
|
||||
// consume(LiveActivitiesChatMessageEvent) just keys on
|
||||
// the a-tag — but the channel.info field only ever
|
||||
// gets populated for streaming. Read the audio-room
|
||||
// status straight off the addressable instead so the
|
||||
// bubble surfaces while a follow is chatting in a
|
||||
// currently-open kind-30312.
|
||||
when (activity.kind) {
|
||||
LiveActivitiesEvent.KIND -> {
|
||||
val streamChannel = LocalCache.getLiveActivityChannelIfExists(activity) ?: return false
|
||||
if (streamChannel.info?.status() != StatusTag.STATUS.LIVE) return false
|
||||
}
|
||||
|
||||
MeetingSpaceEvent.KIND -> {
|
||||
val room =
|
||||
LocalCache.getAddressableNoteIfExists(activity)?.event as? MeetingSpaceEvent
|
||||
?: return false
|
||||
val status = room.status()
|
||||
if (status != MeetingSpaceStatusTag.STATUS.OPEN &&
|
||||
status != MeetingSpaceStatusTag.STATUS.PRIVATE
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (noteEvent is EphemeralChatEvent || noteEvent is LiveActivitiesChatMessageEvent) &&
|
||||
|
||||
+23
-9
@@ -31,12 +31,15 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.model.Channel
|
||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.StatusTag as MeetingSpaceStatusTag
|
||||
|
||||
@Composable
|
||||
fun LiveStatusIndicator(
|
||||
@@ -80,17 +83,28 @@ private suspend fun checkChannelIsOnline(
|
||||
try {
|
||||
when (channel) {
|
||||
is LiveActivitiesChannel -> {
|
||||
// Check if streaming URL is online, fall back to relay check
|
||||
val streamingUrl = channel.info?.streaming()
|
||||
if (!streamingUrl.isNullOrBlank()) {
|
||||
accountViewModel.checkVideoIsOnline(streamingUrl)
|
||||
// Audio rooms (kind-30312) ride the same channel
|
||||
// model but channel.info is null (typed to
|
||||
// LiveActivitiesEvent). Their "is live" signal is
|
||||
// the addressable kind-30312's status — surface
|
||||
// the red dot whenever the room is OPEN/PRIVATE.
|
||||
if (channel.address.kind == MeetingSpaceEvent.KIND) {
|
||||
val room = LocalCache.getAddressableNoteIfExists(channel.address)?.event as? MeetingSpaceEvent
|
||||
room?.status() == MeetingSpaceStatusTag.STATUS.OPEN ||
|
||||
room?.status() == MeetingSpaceStatusTag.STATUS.PRIVATE
|
||||
} else {
|
||||
// Check relay connection
|
||||
val relayUrl = channel.relayHintUrl()
|
||||
if (relayUrl != null) {
|
||||
OnlineChecker.isOnline(relayUrl.url, accountViewModel.httpClientBuilder::okHttpClientForVideo)
|
||||
// Check if streaming URL is online, fall back to relay check
|
||||
val streamingUrl = channel.info?.streaming()
|
||||
if (!streamingUrl.isNullOrBlank()) {
|
||||
accountViewModel.checkVideoIsOnline(streamingUrl)
|
||||
} else {
|
||||
false
|
||||
// Check relay connection
|
||||
val relayUrl = channel.relayHintUrl()
|
||||
if (relayUrl != null) {
|
||||
OnlineChecker.isOnline(relayUrl.url, accountViewModel.httpClientBuilder::okHttpClientForVideo)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+50
-2
@@ -26,16 +26,22 @@ import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelNoteAuthors
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.Gallery
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room.AudioRoomActivity
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.audiorooms.room.AudioRoomBridge
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
|
||||
|
||||
@Composable
|
||||
fun RenderLiveActivityBubble(
|
||||
@@ -43,10 +49,49 @@ fun RenderLiveActivityBubble(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
// Audio rooms (kind-30312) share the kind-1311 chat infra with
|
||||
// streams (kind-30311), so they ride the same `LiveActivitiesChannel`
|
||||
// pump. But channel.info is null for audio rooms (typed to
|
||||
// LiveActivitiesEvent), so toBestDisplayName() would fall back
|
||||
// to the truncated bech32. Read the kind-30312 addressable
|
||||
// directly when the channel's address points to one and use the
|
||||
// room name + a launch path that goes straight to AudioRoomActivity.
|
||||
val meetingEvent =
|
||||
remember(channel.address) {
|
||||
if (channel.address.kind == MeetingSpaceEvent.KIND) {
|
||||
LocalCache.getAddressableNoteIfExists(channel.address)?.event as? MeetingSpaceEvent
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val context = LocalContext.current
|
||||
FilledTonalButton(
|
||||
contentPadding = PaddingValues(start = 8.dp, end = 10.dp, bottom = 0.dp, top = 0.dp),
|
||||
onClick = {
|
||||
nav.nav { routeFor(channel) }
|
||||
if (meetingEvent != null) {
|
||||
val service = meetingEvent.service()
|
||||
val endpoint = meetingEvent.endpoint()
|
||||
val dTag = meetingEvent.address().dTag
|
||||
if (!service.isNullOrBlank() && !endpoint.isNullOrBlank() && dTag.isNotBlank()) {
|
||||
AudioRoomBridge.set(accountViewModel)
|
||||
AudioRoomActivity.launch(
|
||||
context = context,
|
||||
addressValue = meetingEvent.address().toValue(),
|
||||
authBaseUrl = service,
|
||||
endpoint = endpoint,
|
||||
hostPubkey = meetingEvent.pubKey,
|
||||
roomId = dTag,
|
||||
kind = meetingEvent.kind,
|
||||
)
|
||||
} else {
|
||||
// Fall back to the channel route so the user
|
||||
// still lands somewhere — same as a malformed
|
||||
// streaming kind-30311.
|
||||
nav.nav { routeFor(channel) }
|
||||
}
|
||||
} else {
|
||||
nav.nav { routeFor(channel) }
|
||||
}
|
||||
},
|
||||
) {
|
||||
LiveStatusIndicatorForChannel(
|
||||
@@ -58,7 +103,10 @@ fun RenderLiveActivityBubble(
|
||||
RenderUsers(channel, accountViewModel, nav)
|
||||
Spacer(StdHorzSpacer)
|
||||
Text(
|
||||
channel.toBestDisplayName(),
|
||||
// Audio rooms have a real `room()` name on the addressable;
|
||||
// pick that up so the bubble reads "Lounge" instead of
|
||||
// "naddr1abc…".
|
||||
meetingEvent?.room() ?: channel.toBestDisplayName(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user