fix(nests): keep presence out of channel chat + render kind:10312
Two-part fix for kind-10312 [MeetingRoomPresenceEvent] showing up as
empty cards in the channel chat panel.
(1) Filter at the chat-feed level. ChannelFeedFilter now rejects
MeetingRoomPresenceEvent — these only land in `channel.notes` so the
home live-bubble (HomeLiveFilter) can detect a follow broadcasting
in a Nest by walking channel.notes. They aren't chat content; the
chat panel rendering them as empty rows was a leak from that
home-bubble plumbing.
The filter is in ChannelFeedFilter rather than upstream in
LocalCache.consume because removing presence from channel.notes
would silently break the home-bubble's broadcasting-now detection.
This way the channel keeps the events for non-chat consumers and
the chat just refuses to surface them.
(2) Renderer fallback. NoteCompose now dispatches kind-10312 to
RenderMeetingRoomPresence — a one-line italic status row ("@user ·
raised their hand" / "stepped on stage" / "is speaking" / "joined
as audience" / "left the nest"). After (1) this only shows up on
non-chat surfaces (search results, thread view of a quoted
presence, profile timeline, …), but renders informatively instead
of as the empty card it was before.
Five new strings under nest_presence_*.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<ParticipantTag>,
|
||||
|
||||
+16
-2
@@ -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<Note> = sort(channel.notes.filterIntoSet { key, it -> account.isAcceptable(it) })
|
||||
override fun feed(): List<Note> = sort(channel.notes.filterIntoSet { key, it -> isChatEvent(it) && account.isAcceptable(it) })
|
||||
|
||||
override fun applyFilter(newItems: Set<Note>): Set<Note> =
|
||||
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<Note>): List<Note> = 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
|
||||
}
|
||||
|
||||
@@ -530,6 +530,11 @@
|
||||
<string name="nest_notification_stop">Stop</string>
|
||||
<string name="nest_join">Join nest</string>
|
||||
<string name="nest_lobby_host_label">Host</string>
|
||||
<string name="nest_presence_raised_hand">raised their hand</string>
|
||||
<string name="nest_presence_on_stage">stepped on stage</string>
|
||||
<string name="nest_presence_speaking">is speaking</string>
|
||||
<string name="nest_presence_listening">joined as audience</string>
|
||||
<string name="nest_presence_left">left the nest</string>
|
||||
<string name="nest_leave">Leave</string>
|
||||
<string name="nest_leave_host_title">End this nest?</string>
|
||||
<string name="nest_leave_host_body">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.</string>
|
||||
|
||||
Reference in New Issue
Block a user