diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 9b48d1d76..b2cf50fba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -1039,6 +1039,11 @@ private fun RenderNoteRow( RenderMeetingRoomEvent(baseNote, accountViewModel, nav) } + is com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent -> { + com.vitorpamplona.amethyst.ui.note.types + .RenderMeetingRoomPresence(baseNote, accountViewModel, nav) + } + is GitRepositoryEvent -> { RenderGitRepositoryEvent(baseNote, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt index cfc1b3ca5..3d2bd891a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MeetingSpace.kt @@ -293,6 +293,72 @@ fun RenderMeetingRoomEventInner( RenderParticipants(participants, accountViewModel, nav) } +/** + * Inline renderer for kind-10312 [MeetingRoomPresenceEvent] events. + * The event's `content` is empty; the interesting bits are the + * presence flags. Render a one-line italic status — "@user · raised + * their hand" — so a presence event that leaks into a feed surface + * outside the room (search results, thread view of a quoted + * presence, …) renders as a status note instead of an empty card. + * + * The active room's chat panel does NOT render presence events at + * all — `ChannelFeedFilter` filters them out so the chat stays + * just chat. This composable is the fallback for everything else. + */ +@Composable +fun RenderMeetingRoomPresence( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + val event = baseNote.event as? com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent ?: return + + val handRaised = remember(event) { event.handRaised() == true } + val publishing = remember(event) { event.publishing() == true } + val onstage = remember(event) { event.onstage() == true } + + val stateRes = + when { + !publishing && !onstage && !handRaised -> R.string.nest_presence_left + handRaised -> R.string.nest_presence_raised_hand + onstage && publishing -> R.string.nest_presence_speaking + onstage -> R.string.nest_presence_on_stage + else -> R.string.nest_presence_listening + } + + val user = + remember(event.pubKey) { + com.vitorpamplona.amethyst.model.LocalCache + .getOrCreateUser(event.pubKey) + } + + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .clickable { nav.nav(routeFor(user)) }, + verticalAlignment = Alignment.CenterVertically, + ) { + ClickableUserPicture(user, 20.dp, accountViewModel) + Spacer(modifier = Modifier.padding(horizontal = 4.dp)) + UsernameDisplay( + baseUser = user, + fontWeight = androidx.compose.ui.text.font.FontWeight.Normal, + accountViewModel = accountViewModel, + ) + Spacer(modifier = Modifier.padding(horizontal = 4.dp)) + Text( + text = stringRes(stateRes), + style = + MaterialTheme.typography.bodySmall.copy( + fontStyle = androidx.compose.ui.text.font.FontStyle.Italic, + ), + color = MaterialTheme.colorScheme.placeholderText, + ) + } +} + @Composable private fun RenderParticipants( participants: List, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt index 32ac5f3f4..1115ce7ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder +import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent class ChannelFeedFilter( val channel: Channel, @@ -37,12 +38,25 @@ class ChannelFeedFilter( override fun changesFlow() = channel.changesFlow() // returns the last Note of each user. - override fun feed(): List = sort(channel.notes.filterIntoSet { key, it -> account.isAcceptable(it) }) + override fun feed(): List = sort(channel.notes.filterIntoSet { key, it -> isChatEvent(it) && account.isAcceptable(it) }) override fun applyFilter(newItems: Set): Set = newItems - .filter { channel.notes.containsKey(it.idHex) && account.isAcceptable(it) } + .filter { channel.notes.containsKey(it.idHex) && isChatEvent(it) && account.isAcceptable(it) } .toSet() override fun sort(items: Set): List = items.sortedWith(DefaultFeedOrder) + + /** + * Reject non-chat events that get attached to a [Channel] for other + * surfaces. The current case: kind-10312 + * [MeetingRoomPresenceEvent]s land in `channel.notes` so the home + * live-bubble can detect a follow broadcasting in a Nest + * (HomeLiveFilter scans channel.notes); they have no chat content + * and would otherwise render as an empty card in the chat panel. + * + * Anything else is passed through — chat messages, zaps, raids, + * clips, channel-create / metadata events all belong here. + */ + private fun isChatEvent(note: Note): Boolean = note.event !is MeetingRoomPresenceEvent } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index c3324cac1..e18e41a0c 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -530,6 +530,11 @@ Stop Join nest Host + raised their hand + stepped on stage + is speaking + joined as audience + left the nest Leave End this nest? You\'re the host. Closing the room will disconnect everyone. Choose \"Just leave\" if you want to come back later — the room will auto-close after 8 hours of inactivity.