Improves layout of the Relay Monitor events.

This commit is contained in:
Vitor Pamplona
2026-03-28 18:04:42 -04:00
parent 485ed871d1
commit c596e16182
6 changed files with 60 additions and 66 deletions
@@ -182,6 +182,8 @@ import com.vitorpamplona.quartz.nip64Chess.game.ChessGameEvent
import com.vitorpamplona.quartz.nip64Chess.jester.JesterEvent import com.vitorpamplona.quartz.nip64Chess.jester.JesterEvent
import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.RelayDiscoveryEvent
import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.RelayMonitorEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
@@ -341,10 +343,10 @@ object LocalCache : ILocalCache, ICacheProvider {
} }
}.buffer(kotlinx.coroutines.channels.Channel.CONFLATED) }.buffer(kotlinx.coroutines.channels.Channel.CONFLATED)
fun observeEvents(filter: Filter): Flow<List<Event>> = fun <T : Event> observeEvents(filter: Filter): Flow<List<T>> =
callbackFlow { callbackFlow {
val cachedFilter = val cachedFilter =
EventListMatchingFilter(filter, this@LocalCache::filter) { EventListMatchingFilter<T>(filter, this@LocalCache::filter) {
trySend(it) trySend(it)
} }
@@ -358,7 +360,7 @@ object LocalCache : ILocalCache, ICacheProvider {
}.buffer(kotlinx.coroutines.channels.Channel.CONFLATED) }.buffer(kotlinx.coroutines.channels.Channel.CONFLATED)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : Event> observeLatestEvent(filter: Filter) = observeEvents(filter).map { it.firstNotNullOfOrNull { it as? T } } fun <T : Event> observeLatestEvent(filter: Filter) = observeEvents<T>(filter).map { it.firstOrNull() }
fun observeLatestNote(filter: Filter) = observeNotes(filter).map { it.firstOrNull() } fun observeLatestNote(filter: Filter) = observeNotes(filter).map { it.firstOrNull() }
@@ -1349,6 +1351,18 @@ object LocalCache : ILocalCache, ICacheProvider {
wasVerified: Boolean, wasVerified: Boolean,
) = consumeRegularEvent(event, relay, wasVerified) ) = consumeRegularEvent(event, relay, wasVerified)
private fun consume(
event: RelayDiscoveryEvent,
relay: NormalizedRelayUrl?,
wasVerified: Boolean,
) = consumeBaseReplaceable(event, relay, wasVerified)
private fun consume(
event: RelayMonitorEvent,
relay: NormalizedRelayUrl?,
wasVerified: Boolean,
) = consumeBaseReplaceable(event, relay, wasVerified)
fun consume( fun consume(
event: StatusEvent, event: StatusEvent,
relay: NormalizedRelayUrl?, relay: NormalizedRelayUrl?,
@@ -3208,6 +3222,8 @@ object LocalCache : ILocalCache, ICacheProvider {
is ZapPollEvent -> consume(event, relay, wasVerified) is ZapPollEvent -> consume(event, relay, wasVerified)
is PollEvent -> consume(event, relay, wasVerified) is PollEvent -> consume(event, relay, wasVerified)
is PollResponseEvent -> consume(event, relay, wasVerified) is PollResponseEvent -> consume(event, relay, wasVerified)
is RelayDiscoveryEvent -> consume(event, relay, wasVerified)
is RelayMonitorEvent -> consume(event, relay, wasVerified)
is ReactionEvent -> consume(event, relay, wasVerified) is ReactionEvent -> consume(event, relay, wasVerified)
is ContactCardEvent -> consume(event, relay, wasVerified) is ContactCardEvent -> consume(event, relay, wasVerified)
is RelaySetEvent -> consume(event, relay, wasVerified) is RelaySetEvent -> consume(event, relay, wasVerified)
@@ -66,7 +66,7 @@ class UserProfileFollowersUserFeedViewModel(
@OptIn(kotlinx.coroutines.FlowPreview::class) @OptIn(kotlinx.coroutines.FlowPreview::class)
val followersFlow: StateFlow<List<User>> = val followersFlow: StateFlow<List<User>> =
account.cache account.cache
.observeEvents(followerFilter) .observeEvents<Event>(followerFilter)
.sample(500) .sample(500)
.map { followerContactLists -> .map { followerContactLists ->
followerContactLists.toNonHiddenOwners().sortedWith(sortingModel) followerContactLists.toNonHiddenOwners().sortedWith(sortingModel)
@@ -28,7 +28,6 @@ import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.utils.BigDecimal import com.vitorpamplona.quartz.utils.BigDecimal
@@ -96,16 +95,14 @@ class UserProfileZapsViewModel(
} }
} }
suspend fun List<Event>.sumAmountsByUser(): List<ZapAmount> { suspend fun List<LnZapEvent>.sumAmountsByUser(): List<ZapAmount> {
val results = mutableMapOf<User, BigDecimal>() val results = mutableMapOf<User, BigDecimal>()
this.forEach { zapEvent -> this.forEach { zapEvent ->
if (zapEvent is LnZapEvent) { val zapAmount = mapRequest(zapEvent)
val zapAmount = mapRequest(zapEvent) if (zapAmount != null) {
if (zapAmount != null) { val existingAmount = results[zapAmount.user] ?: BigDecimal.ZERO
val existingAmount = results[zapAmount.user] ?: BigDecimal.ZERO results[zapAmount.user] = existingAmount + zapAmount.amount
results[zapAmount.user] = existingAmount + zapAmount.amount
}
} }
} }
@@ -115,7 +112,7 @@ class UserProfileZapsViewModel(
@OptIn(kotlinx.coroutines.FlowPreview::class) @OptIn(kotlinx.coroutines.FlowPreview::class)
val receivedZapAmountsByUser: StateFlow<List<ZapAmount>> = val receivedZapAmountsByUser: StateFlow<List<ZapAmount>> =
account.cache account.cache
.observeEvents(zapsToUser) .observeEvents<LnZapEvent>(zapsToUser)
.sample(500) .sample(500)
.map { zapEvents -> .map { zapEvents ->
zapEvents.sumAmountsByUser() zapEvents.sumAmountsByUser()
@@ -85,7 +85,6 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -102,6 +101,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.util.timeDiffAgoShortish import com.vitorpamplona.amethyst.commons.util.timeDiffAgoShortish
@@ -115,6 +115,7 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.note.graspLink import com.vitorpamplona.amethyst.ui.note.graspLink
import com.vitorpamplona.amethyst.ui.note.nipLink import com.vitorpamplona.amethyst.ui.note.nipLink
import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot import com.vitorpamplona.amethyst.ui.note.timeAgoNoDot
@@ -283,6 +284,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.map
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract import kotlin.contracts.contract
@@ -440,7 +442,7 @@ fun RelayInformationBody(
if (discoveryEvents.isNotEmpty()) { if (discoveryEvents.isNotEmpty()) {
item { SectionHeader(stringRes(R.string.relay_monitor_reports)) } item { SectionHeader(stringRes(R.string.relay_monitor_reports)) }
items(discoveryEvents, key = { it.id }) { event -> items(discoveryEvents, key = { it.addressTag() }) { event ->
RelayMonitorReportCard(event, accountViewModel, nav) RelayMonitorReportCard(event, accountViewModel, nav)
} }
} }
@@ -1575,21 +1577,19 @@ fun PoliciesCard(relay: Nip11RelayInformation) {
@Composable @Composable
fun loadRelayDiscoveryEvents(relay: NormalizedRelayUrl): State<ImmutableList<RelayDiscoveryEvent>> = fun loadRelayDiscoveryEvents(relay: NormalizedRelayUrl): State<ImmutableList<RelayDiscoveryEvent>> =
produceState<ImmutableList<RelayDiscoveryEvent>>(persistentListOf(), relay) { remember(relay) {
LocalCache LocalCache
.observeEvents( .observeEvents<RelayDiscoveryEvent>(
Filter( Filter(
kinds = listOf(RelayDiscoveryEvent.KIND), kinds = listOf(RelayDiscoveryEvent.KIND),
tags = mapOf("d" to listOf(relay.url)), tags = mapOf("d" to listOf(relay.url)),
since = TimeUtils.oneWeekAgo(),
limit = 3,
), ),
).collect { events -> ).map {
value = it.toImmutableList()
events
.filterIsInstance<RelayDiscoveryEvent>()
.sortedByDescending { it.createdAt }
.toImmutableList()
} }
} }.collectAsStateWithLifecycle(persistentListOf())
@Composable @Composable
private fun RelayMonitorReportCard( private fun RelayMonitorReportCard(
@@ -1622,11 +1622,7 @@ private fun RelayMonitorReportCard(
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) )
UserCompose( UsernameDisplay(user, weight = Modifier.weight(1f), accountViewModel = accountViewModel)
baseUser = user,
accountViewModel = accountViewModel,
nav = nav,
)
} }
} }
} }
@@ -1646,11 +1642,6 @@ private fun RelayMonitorReportCard(
val rttWrite = event.rttWrite() val rttWrite = event.rttWrite()
if (rttOpen != null || rttRead != null || rttWrite != null) { if (rttOpen != null || rttRead != null || rttWrite != null) {
Text(
stringRes(R.string.relay_monitor_rtt),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
)
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
@@ -1687,33 +1678,6 @@ private fun RelayMonitorReportCard(
) )
} }
// Supported NIPs
val supportedNips = event.supportedNips()
if (supportedNips.isNotEmpty()) {
Text(
stringRes(R.string.relay_monitor_supported_nips),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
)
val uri = LocalUriHandler.current
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
supportedNips.forEach { nip ->
val nipStr = nip.toString().padStart(2, '0')
SuggestionChip(
onClick = {
runCatching {
uri.openUri(nipLink(nipStr))
}
},
label = { Text(nipStr) },
)
}
}
}
// Requirements // Requirements
val requirements = event.requirements() val requirements = event.requirements()
if (requirements.isNotEmpty()) { if (requirements.isNotEmpty()) {
@@ -20,7 +20,9 @@
*/ */
package com.vitorpamplona.amethyst.commons.model.observables package com.vitorpamplona.amethyst.commons.model.observables
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import java.util.SortedSet import java.util.SortedSet
@@ -31,18 +33,27 @@ import java.util.concurrent.ConcurrentSkipListSet
* that is updated every time a new event that matches * that is updated every time a new event that matches
* the filter is received, including addressables. * the filter is received, including addressables.
*/ */
class EventListMatchingFilter( class EventListMatchingFilter<T : Event>(
private val filter: Filter, private val filter: Filter,
private val atOnce: (filter: Filter) -> SortedSet<Note>, private val atOnce: (filter: Filter) -> SortedSet<Note>,
private val update: (List<Event>) -> Unit, private val update: (List<T>) -> Unit,
) : Observable { ) : Observable {
// Keeping this here blocks it from being cleared from memory // Keeping this here blocks it from being cleared from memory
var currentResults: ConcurrentSkipListSet<Note> = ConcurrentSkipListSet(CreatedAtIdHexComparator) var currentResults: ConcurrentSkipListSet<Note> = ConcurrentSkipListSet(CreatedAtIdHexComparator)
@Suppress("UNCHECKED_CAST")
override fun new( override fun new(
event: Event, event: Event,
note: Note, note: Note,
) { ) {
if (event is AddressableEvent && note !is AddressableNote) {
// event update
if (currentResults.contains(note)) {
update(currentResults.mapNotNull { it.event as? T })
}
return
}
if (filter.match(event)) { if (filter.match(event)) {
currentResults.add(note) currentResults.add(note)
val limit = filter.limit val limit = filter.limit
@@ -50,18 +61,20 @@ class EventListMatchingFilter(
currentResults.remove(currentResults.last()) currentResults.remove(currentResults.last())
} }
update(currentResults.mapNotNull { it.event }) update(currentResults.mapNotNull { it.event as? T })
} }
} }
@Suppress("UNCHECKED_CAST")
override fun remove(note: Note) { override fun remove(note: Note) {
if (currentResults.remove(note)) { if (currentResults.remove(note)) {
update(currentResults.mapNotNull { it.event }) update(currentResults.mapNotNull { it.event as? T })
} }
} }
@Suppress("UNCHECKED_CAST")
fun init() { fun init() {
currentResults = ConcurrentSkipListSet(atOnce(filter)) currentResults = ConcurrentSkipListSet(atOnce(filter))
update(currentResults.mapNotNull { it.event }) update(currentResults.mapNotNull { it.event as? T })
} }
} }
@@ -20,7 +20,9 @@
*/ */
package com.vitorpamplona.amethyst.commons.model.observables package com.vitorpamplona.amethyst.commons.model.observables
import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import java.util.SortedSet import java.util.SortedSet
@@ -43,6 +45,8 @@ class NoteListMatchingFilter(
event: Event, event: Event,
note: Note, note: Note,
) { ) {
if (event is AddressableEvent && note !is AddressableNote) return
if (filter.match(event)) { if (filter.match(event)) {
if (currentResults.add(note)) { if (currentResults.add(note)) {
val limit = filter.limit val limit = filter.limit