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.move.LiveChessMoveEvent
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.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent
@@ -341,10 +343,10 @@ object LocalCache : ILocalCache, ICacheProvider {
}
}.buffer(kotlinx.coroutines.channels.Channel.CONFLATED)
fun observeEvents(filter: Filter): Flow<List<Event>> =
fun <T : Event> observeEvents(filter: Filter): Flow<List<T>> =
callbackFlow {
val cachedFilter =
EventListMatchingFilter(filter, this@LocalCache::filter) {
EventListMatchingFilter<T>(filter, this@LocalCache::filter) {
trySend(it)
}
@@ -358,7 +360,7 @@ object LocalCache : ILocalCache, ICacheProvider {
}.buffer(kotlinx.coroutines.channels.Channel.CONFLATED)
@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() }
@@ -1349,6 +1351,18 @@ object LocalCache : ILocalCache, ICacheProvider {
wasVerified: Boolean,
) = 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(
event: StatusEvent,
relay: NormalizedRelayUrl?,
@@ -3208,6 +3222,8 @@ object LocalCache : ILocalCache, ICacheProvider {
is ZapPollEvent -> consume(event, relay, wasVerified)
is PollEvent -> 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 ContactCardEvent -> consume(event, relay, wasVerified)
is RelaySetEvent -> consume(event, relay, wasVerified)
@@ -66,7 +66,7 @@ class UserProfileFollowersUserFeedViewModel(
@OptIn(kotlinx.coroutines.FlowPreview::class)
val followersFlow: StateFlow<List<User>> =
account.cache
.observeEvents(followerFilter)
.observeEvents<Event>(followerFilter)
.sample(500)
.map { followerContactLists ->
followerContactLists.toNonHiddenOwners().sortedWith(sortingModel)
@@ -28,7 +28,6 @@ import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
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.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.utils.BigDecimal
@@ -96,18 +95,16 @@ class UserProfileZapsViewModel(
}
}
suspend fun List<Event>.sumAmountsByUser(): List<ZapAmount> {
suspend fun List<LnZapEvent>.sumAmountsByUser(): List<ZapAmount> {
val results = mutableMapOf<User, BigDecimal>()
this.forEach { zapEvent ->
if (zapEvent is LnZapEvent) {
val zapAmount = mapRequest(zapEvent)
if (zapAmount != null) {
val existingAmount = results[zapAmount.user] ?: BigDecimal.ZERO
results[zapAmount.user] = existingAmount + zapAmount.amount
}
}
}
return results.map { (user, amount) -> ZapAmount(user, amount) }.sortedWith(sortingModel)
}
@@ -115,7 +112,7 @@ class UserProfileZapsViewModel(
@OptIn(kotlinx.coroutines.FlowPreview::class)
val receivedZapAmountsByUser: StateFlow<List<ZapAmount>> =
account.cache
.observeEvents(zapsToUser)
.observeEvents<LnZapEvent>(zapsToUser)
.sample(500)
.map { zapEvents ->
zapEvents.sumAmountsByUser()
@@ -85,7 +85,6 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R
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.UserCompose
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.nipLink
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.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.map
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@@ -440,7 +442,7 @@ fun RelayInformationBody(
if (discoveryEvents.isNotEmpty()) {
item { SectionHeader(stringRes(R.string.relay_monitor_reports)) }
items(discoveryEvents, key = { it.id }) { event ->
items(discoveryEvents, key = { it.addressTag() }) { event ->
RelayMonitorReportCard(event, accountViewModel, nav)
}
}
@@ -1575,21 +1577,19 @@ fun PoliciesCard(relay: Nip11RelayInformation) {
@Composable
fun loadRelayDiscoveryEvents(relay: NormalizedRelayUrl): State<ImmutableList<RelayDiscoveryEvent>> =
produceState<ImmutableList<RelayDiscoveryEvent>>(persistentListOf(), relay) {
remember(relay) {
LocalCache
.observeEvents(
.observeEvents<RelayDiscoveryEvent>(
Filter(
kinds = listOf(RelayDiscoveryEvent.KIND),
tags = mapOf("d" to listOf(relay.url)),
since = TimeUtils.oneWeekAgo(),
limit = 3,
),
).collect { events ->
value =
events
.filterIsInstance<RelayDiscoveryEvent>()
.sortedByDescending { it.createdAt }
.toImmutableList()
}
).map {
it.toImmutableList()
}
}.collectAsStateWithLifecycle(persistentListOf())
@Composable
private fun RelayMonitorReportCard(
@@ -1622,11 +1622,7 @@ private fun RelayMonitorReportCard(
accountViewModel = accountViewModel,
nav = nav,
)
UserCompose(
baseUser = user,
accountViewModel = accountViewModel,
nav = nav,
)
UsernameDisplay(user, weight = Modifier.weight(1f), accountViewModel = accountViewModel)
}
}
}
@@ -1646,11 +1642,6 @@ private fun RelayMonitorReportCard(
val rttWrite = event.rttWrite()
if (rttOpen != null || rttRead != null || rttWrite != null) {
Text(
stringRes(R.string.relay_monitor_rtt),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
)
Row(
modifier = Modifier.fillMaxWidth(),
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
val requirements = event.requirements()
if (requirements.isNotEmpty()) {
@@ -20,7 +20,9 @@
*/
package com.vitorpamplona.amethyst.commons.model.observables
import com.vitorpamplona.amethyst.commons.model.AddressableNote
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.relay.filters.Filter
import java.util.SortedSet
@@ -31,18 +33,27 @@ import java.util.concurrent.ConcurrentSkipListSet
* that is updated every time a new event that matches
* the filter is received, including addressables.
*/
class EventListMatchingFilter(
class EventListMatchingFilter<T : Event>(
private val filter: Filter,
private val atOnce: (filter: Filter) -> SortedSet<Note>,
private val update: (List<Event>) -> Unit,
private val update: (List<T>) -> Unit,
) : Observable {
// Keeping this here blocks it from being cleared from memory
var currentResults: ConcurrentSkipListSet<Note> = ConcurrentSkipListSet(CreatedAtIdHexComparator)
@Suppress("UNCHECKED_CAST")
override fun new(
event: Event,
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)) {
currentResults.add(note)
val limit = filter.limit
@@ -50,18 +61,20 @@ class EventListMatchingFilter(
currentResults.remove(currentResults.last())
}
update(currentResults.mapNotNull { it.event })
update(currentResults.mapNotNull { it.event as? T })
}
}
@Suppress("UNCHECKED_CAST")
override fun remove(note: Note) {
if (currentResults.remove(note)) {
update(currentResults.mapNotNull { it.event })
update(currentResults.mapNotNull { it.event as? T })
}
}
@Suppress("UNCHECKED_CAST")
fun init() {
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
import com.vitorpamplona.amethyst.commons.model.AddressableNote
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.relay.filters.Filter
import java.util.SortedSet
@@ -43,6 +45,8 @@ class NoteListMatchingFilter(
event: Event,
note: Note,
) {
if (event is AddressableEvent && note !is AddressableNote) return
if (filter.match(event)) {
if (currentResults.add(note)) {
val limit = filter.limit