diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index b6439293b..c8e05f1f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -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> = + fun observeEvents(filter: Filter): Flow> = callbackFlow { val cachedFilter = - EventListMatchingFilter(filter, this@LocalCache::filter) { + EventListMatchingFilter(filter, this@LocalCache::filter) { trySend(it) } @@ -358,7 +360,7 @@ object LocalCache : ILocalCache, ICacheProvider { }.buffer(kotlinx.coroutines.channels.Channel.CONFLATED) @Suppress("UNCHECKED_CAST") - fun observeLatestEvent(filter: Filter) = observeEvents(filter).map { it.firstNotNullOfOrNull { it as? T } } + fun observeLatestEvent(filter: Filter) = observeEvents(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) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt index 122ec9654..b86e6b380 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt @@ -66,7 +66,7 @@ class UserProfileFollowersUserFeedViewModel( @OptIn(kotlinx.coroutines.FlowPreview::class) val followersFlow: StateFlow> = account.cache - .observeEvents(followerFilter) + .observeEvents(followerFilter) .sample(500) .map { followerContactLists -> followerContactLists.toNonHiddenOwners().sortedWith(sortingModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt index 35fd0bd23..33ebc1b20 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt @@ -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,16 +95,14 @@ class UserProfileZapsViewModel( } } - suspend fun List.sumAmountsByUser(): List { + suspend fun List.sumAmountsByUser(): List { val results = mutableMapOf() 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 - } + val zapAmount = mapRequest(zapEvent) + if (zapAmount != null) { + val existingAmount = results[zapAmount.user] ?: BigDecimal.ZERO + results[zapAmount.user] = existingAmount + zapAmount.amount } } @@ -115,7 +112,7 @@ class UserProfileZapsViewModel( @OptIn(kotlinx.coroutines.FlowPreview::class) val receivedZapAmountsByUser: StateFlow> = account.cache - .observeEvents(zapsToUser) + .observeEvents(zapsToUser) .sample(500) .map { zapEvents -> zapEvents.sumAmountsByUser() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt index ad98c32bf..9317e0a4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt @@ -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> = - produceState>(persistentListOf(), relay) { + remember(relay) { LocalCache - .observeEvents( + .observeEvents( Filter( kinds = listOf(RelayDiscoveryEvent.KIND), tags = mapOf("d" to listOf(relay.url)), + since = TimeUtils.oneWeekAgo(), + limit = 3, ), - ).collect { events -> - value = - events - .filterIsInstance() - .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()) { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt index 09e5b58d8..9117d13cd 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/EventListMatchingFilter.kt @@ -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( private val filter: Filter, private val atOnce: (filter: Filter) -> SortedSet, - private val update: (List) -> Unit, + private val update: (List) -> Unit, ) : Observable { // Keeping this here blocks it from being cleared from memory var currentResults: ConcurrentSkipListSet = 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 }) } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt index f94bed11c..e938fb288 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/observables/NoteListMatchingFilter.kt @@ -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