feat: show Marmot group relays as clickable icons with activity status

Replaces the comma-separated relay text on the Marmot Group Info
screen with a FlowRow of 55dp relay avatars that open RelayInfo on
tap and copy the URL on long-press, matching the relay-icon pattern
used elsewhere in the app.

To surface whether each configured relay is actually carrying traffic
for the group, MarmotGroupChatroom now tracks the most recent
kind:445 event timestamp observed from each delivering relay and the
info screen renders a green dot when a relay has delivered a group
event within the last 7 days (gray otherwise).
This commit is contained in:
Claude
2026-04-20 16:59:19 +00:00
parent f76e4f4c49
commit 42e226bf8d
3 changed files with 163 additions and 5 deletions
@@ -27,9 +27,11 @@ import com.vitorpamplona.amethyst.commons.model.ListChange
import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.NotesGatherer
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import java.lang.ref.WeakReference
/**
@@ -51,6 +53,14 @@ class MarmotGroupChatroom(
var newestMessage: Note? = null
val unreadCount = MutableStateFlow(0)
/**
* Tracks the most recent createdAt (seconds) of a kind:445 group event
* observed from each relay, keyed by the relay that delivered it. Used by
* the Group Info screen to flag which configured relays are actively
* carrying traffic for this MLS group.
*/
val relayActivity = MutableStateFlow<Map<NormalizedRelayUrl, Long>>(emptyMap())
/**
* True if the local user has ever sent an application message in this
* group, OR explicitly created/owns it. Used by list UIs to split groups
@@ -134,6 +144,16 @@ class MarmotGroupChatroom(
unreadCount.value = 0
}
fun recordRelayActivity(
relay: NormalizedRelayUrl,
createdAt: Long,
) {
relayActivity.update { current ->
val existing = current[relay] ?: 0L
if (createdAt > existing) current + (relay to createdAt) else current
}
}
fun pruneMessagesToTheLatestOnly(): Set<Note> {
val sorted = messages.sortedWith(DefaultFeedOrder)
val toKeep =