refactor: integrate relay event counts into each ViewModel

Move count query logic from separate RelayEventCountViewModel into each
relay list's own ViewModel. BasicRelaySetupInfoModel now manages counts
via countFilters() override. Nip65RelayListViewModel gets its own count
infrastructure for home (outbox) and notification (inbox) relay lists.
Simplify AllRelayListScreen by removing 7 extra count ViewModels and
their LaunchedEffects.

https://claude.ai/code/session_016158D5mq5BygS1uBbLNbsA
This commit is contained in:
Claude
2026-03-15 15:02:09 +00:00
parent b1723a5dfb
commit 764c2241ca
9 changed files with 261 additions and 262 deletions
@@ -60,7 +60,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.BlockedRelay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.renderBlockedItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.renderBlockedItems
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.BroadcastRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.BroadcastRelayListViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.renderBroadcastItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.renderBroadcastItems
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayEventCountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayExporter import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayExporter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayListCollection import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayListCollection
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayZipExporter import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayZipExporter
@@ -110,13 +109,6 @@ fun AllRelayListScreen(
val indexerViewModel: IndexerRelayListViewModel = viewModel() val indexerViewModel: IndexerRelayListViewModel = viewModel()
val proxyViewModel: ProxyRelayListViewModel = viewModel() val proxyViewModel: ProxyRelayListViewModel = viewModel()
val relayFeedsViewModel: RelayFeedsListViewModel = viewModel() val relayFeedsViewModel: RelayFeedsListViewModel = viewModel()
val outboxCountViewModel: RelayEventCountViewModel = viewModel(key = "outboxCount")
val inboxCountViewModel: RelayEventCountViewModel = viewModel(key = "inboxCount")
val dmCountViewModel: RelayEventCountViewModel = viewModel(key = "dmCount")
val privateHomeCountViewModel: RelayEventCountViewModel = viewModel(key = "privateHomeCount")
val proxyCountViewModel: RelayEventCountViewModel = viewModel(key = "proxyCount")
val indexerCountViewModel: RelayEventCountViewModel = viewModel(key = "indexerCount")
val searchCountViewModel: RelayEventCountViewModel = viewModel(key = "searchCount")
dmViewModel.init(accountViewModel) dmViewModel.init(accountViewModel)
nip65ViewModel.init(accountViewModel) nip65ViewModel.init(accountViewModel)
@@ -159,13 +151,6 @@ fun AllRelayListScreen(
indexerViewModel, indexerViewModel,
proxyViewModel, proxyViewModel,
relayFeedsViewModel, relayFeedsViewModel,
outboxCountViewModel,
inboxCountViewModel,
dmCountViewModel,
privateHomeCountViewModel,
proxyCountViewModel,
indexerCountViewModel,
searchCountViewModel,
accountViewModel, accountViewModel,
nav, nav,
) )
@@ -186,13 +171,6 @@ fun MappedAllRelayListView(
indexerViewModel: IndexerRelayListViewModel, indexerViewModel: IndexerRelayListViewModel,
proxyViewModel: ProxyRelayListViewModel, proxyViewModel: ProxyRelayListViewModel,
relayFeedsViewModel: RelayFeedsListViewModel, relayFeedsViewModel: RelayFeedsListViewModel,
outboxCountViewModel: RelayEventCountViewModel,
inboxCountViewModel: RelayEventCountViewModel,
dmCountViewModel: RelayEventCountViewModel,
privateHomeCountViewModel: RelayEventCountViewModel,
proxyCountViewModel: RelayEventCountViewModel,
indexerCountViewModel: RelayEventCountViewModel,
searchCountViewModel: RelayEventCountViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
@@ -210,71 +188,13 @@ fun MappedAllRelayListView(
val proxyRelays by proxyViewModel.relays.collectAsStateWithLifecycle() val proxyRelays by proxyViewModel.relays.collectAsStateWithLifecycle()
val relayFeedsFeedState by relayFeedsViewModel.relays.collectAsStateWithLifecycle() val relayFeedsFeedState by relayFeedsViewModel.relays.collectAsStateWithLifecycle()
val outboxCounts by outboxCountViewModel.counts.collectAsStateWithLifecycle() val outboxCounts by nip65ViewModel.homeCountResults.collectAsStateWithLifecycle()
val inboxCounts by inboxCountViewModel.counts.collectAsStateWithLifecycle() val inboxCounts by nip65ViewModel.notifCountResults.collectAsStateWithLifecycle()
val dmCounts by dmCountViewModel.counts.collectAsStateWithLifecycle() val dmCounts by dmViewModel.countResults.collectAsStateWithLifecycle()
val privateHomeCounts by privateHomeCountViewModel.counts.collectAsStateWithLifecycle() val privateHomeCounts by privateOutboxViewModel.countResults.collectAsStateWithLifecycle()
val proxyCounts by proxyCountViewModel.counts.collectAsStateWithLifecycle() val proxyCounts by proxyViewModel.countResults.collectAsStateWithLifecycle()
val indexerCounts by indexerCountViewModel.counts.collectAsStateWithLifecycle() val indexerCounts by indexerViewModel.countResults.collectAsStateWithLifecycle()
val searchCounts by searchCountViewModel.counts.collectAsStateWithLifecycle() val searchCounts by searchViewModel.countResults.collectAsStateWithLifecycle()
val userPubKey = accountViewModel.account.pubKey
LaunchedEffect(homeFeedState) {
if (homeFeedState.isNotEmpty()) {
outboxCountViewModel.queryCountsForRelays(
RelayEventCountViewModel.authorCountFilters(userPubKey, homeFeedState.map { it.relay }),
)
}
}
LaunchedEffect(notifFeedState) {
if (notifFeedState.isNotEmpty()) {
inboxCountViewModel.queryCountsForRelays(
RelayEventCountViewModel.pTagCountFilters(userPubKey, notifFeedState.map { it.relay }),
)
}
}
LaunchedEffect(dmFeedState) {
if (dmFeedState.isNotEmpty()) {
dmCountViewModel.queryCountsForRelays(
RelayEventCountViewModel.dmCountFilters(userPubKey, dmFeedState.map { it.relay }),
)
}
}
LaunchedEffect(privateOutboxFeedState) {
if (privateOutboxFeedState.isNotEmpty()) {
privateHomeCountViewModel.queryCountsForRelays(
RelayEventCountViewModel.authorCountFilters(userPubKey, privateOutboxFeedState.map { it.relay }),
)
}
}
LaunchedEffect(proxyRelays) {
if (proxyRelays.isNotEmpty()) {
proxyCountViewModel.queryCountsForRelays(
RelayEventCountViewModel.totalCountFilters(proxyRelays.map { it.relay }),
)
}
}
LaunchedEffect(indexerRelays) {
if (indexerRelays.isNotEmpty()) {
indexerCountViewModel.queryCountsForRelays(
RelayEventCountViewModel.indexerCountFilters(indexerRelays.map { it.relay }),
)
}
}
LaunchedEffect(searchFeedState) {
if (searchFeedState.isNotEmpty()) {
searchCountViewModel.queryCountsForRelays(
RelayEventCountViewModel.totalCountFilters(searchFeedState.map { it.relay }),
)
}
}
Scaffold( Scaffold(
topBar = { topBar = {
@@ -26,6 +26,11 @@ import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.service.replace
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -33,13 +38,19 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
abstract class BasicRelaySetupInfoModel : ViewModel() { abstract class BasicRelaySetupInfoModel : ViewModel(), IRelayClientListener {
lateinit var accountViewModel: AccountViewModel lateinit var accountViewModel: AccountViewModel
lateinit var account: Account lateinit var account: Account
private val _relays = MutableStateFlow<List<BasicRelaySetupInfo>>(emptyList()) private val _relays = MutableStateFlow<List<BasicRelaySetupInfo>>(emptyList())
val relays = _relays.asStateFlow() val relays = _relays.asStateFlow()
private val _countResults = MutableStateFlow<Map<NormalizedRelayUrl, RelayCountResult>>(emptyMap())
val countResults = _countResults.asStateFlow()
private val subIdToRelay = mutableMapOf<String, Pair<NormalizedRelayUrl, Int>>()
private val relayQueryInfos = mutableMapOf<NormalizedRelayUrl, List<CountQueryInfo>>()
var hasModified = false var hasModified = false
fun init(accountViewModel: AccountViewModel) { fun init(accountViewModel: AccountViewModel) {
@@ -50,12 +61,15 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
fun load() { fun load() {
clear() clear()
loadRelayDocuments() loadRelayDocuments()
loadCounts()
} }
abstract fun getRelayList(): List<NormalizedRelayUrl>? abstract fun getRelayList(): List<NormalizedRelayUrl>?
abstract suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) abstract suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>)
open fun countFilters(relayUrl: NormalizedRelayUrl): List<CountFilter> = emptyList()
fun create() { fun create() {
if (hasModified) { if (hasModified) {
accountViewModel.launchSigner { accountViewModel.launchSigner {
@@ -79,6 +93,83 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
} }
} }
private fun loadCounts() {
val client = Amethyst.instance.client
cleanupCounts()
val relayList = _relays.value
if (relayList.isEmpty()) return
val hasFilters = relayList.any { countFilters(it.relay).isNotEmpty() }
if (!hasFilters) return
client.subscribe(this)
relayList.forEach { item ->
val filters = countFilters(item.relay)
if (filters.isEmpty()) return@forEach
val queryInfos = mutableListOf<CountQueryInfo>()
filters.forEachIndexed { index, countFilter ->
val subId = newSubId()
subIdToRelay[subId] = Pair(item.relay, index)
queryInfos.add(CountQueryInfo(subId, countFilter.label, index))
client.queryCount(
subId = subId,
filters = mapOf(item.relay to listOf(countFilter.filter)),
)
}
relayQueryInfos[item.relay] = queryInfos
}
}
override fun onIncomingMessage(
relay: IRelayClient,
msgStr: String,
msg: Message,
) {
if (msg is CountMessage) {
val (relayUrl, filterIndex) = subIdToRelay[msg.queryId] ?: return
val queryInfos = relayQueryInfos[relayUrl] ?: return
val queryInfo = queryInfos.find { it.filterIndex == filterIndex } ?: return
_countResults.update { currentMap ->
val currentResult = currentMap[relayUrl] ?: RelayCountResult()
val updatedEntries = currentResult.counts.toMutableList()
val existingIndex = updatedEntries.indexOfFirst { it.label == queryInfo.label }
val newEntry =
RelayCountResult.CountEntry(
label = queryInfo.label,
count = msg.result.count,
approximate = msg.result.approximate,
)
if (existingIndex >= 0) {
updatedEntries[existingIndex] = newEntry
} else {
updatedEntries.add(newEntry)
}
currentMap + (relayUrl to RelayCountResult(updatedEntries))
}
}
}
private fun cleanupCounts() {
val client = Amethyst.instance.client
subIdToRelay.keys.forEach { subId ->
client.close(subId)
}
subIdToRelay.clear()
relayQueryInfos.clear()
_countResults.value = emptyMap()
client.unsubscribe(this)
}
open fun relayListBuilder(): List<BasicRelaySetupInfo> { open fun relayListBuilder(): List<BasicRelaySetupInfo> {
val relayList = getRelayList() ?: emptyList() val relayList = getRelayList() ?: emptyList()
@@ -122,4 +213,15 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
) { ) {
_relays.update { it.replace(relay, relay.copy(paidRelay = paid)) } _relays.update { it.replace(relay, relay.copy(paidRelay = paid)) }
} }
override fun onCleared() {
cleanupCounts()
super.onCleared()
}
private data class CountQueryInfo(
val subId: String,
val label: String,
val filterIndex: Int,
)
} }
@@ -21,21 +21,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
@Immutable @Immutable
data class RelayCountResult( data class RelayCountResult(
@@ -49,165 +35,6 @@ data class RelayCountResult(
) )
} }
class RelayEventCountViewModel : ViewModel(), IRelayClientListener {
private val client: INostrClient get() = Amethyst.instance.client
private val _counts = MutableStateFlow<Map<NormalizedRelayUrl, RelayCountResult>>(emptyMap())
val counts = _counts.asStateFlow()
// Maps subId -> (relay, filterIndex) so we can route count results
private val subIdToRelay = mutableMapOf<String, Pair<NormalizedRelayUrl, Int>>()
// Maps relay -> list of (subId, label) to track active queries
private val relayQueries = mutableMapOf<NormalizedRelayUrl, MutableList<QueryInfo>>()
private data class QueryInfo(
val subId: String,
val label: String,
val filterIndex: Int,
)
fun queryCountsForRelays(queries: Map<NormalizedRelayUrl, List<CountFilter>>) {
cleanup()
client.subscribe(this)
queries.forEach { (relay, filters) ->
val queryInfos = mutableListOf<QueryInfo>()
filters.forEachIndexed { index, countFilter ->
val subId = newSubId()
subIdToRelay[subId] = Pair(relay, index)
queryInfos.add(QueryInfo(subId, countFilter.label, index))
client.queryCount(
subId = subId,
filters = mapOf(relay to listOf(countFilter.filter)),
)
}
relayQueries[relay] = queryInfos
}
}
override fun onIncomingMessage(
relay: IRelayClient,
msgStr: String,
msg: Message,
) {
if (msg is CountMessage) {
val (relayUrl, filterIndex) = subIdToRelay[msg.queryId] ?: return
val queryInfos = relayQueries[relayUrl] ?: return
val queryInfo = queryInfos.find { it.filterIndex == filterIndex } ?: return
_counts.update { currentMap ->
val currentResult = currentMap[relayUrl] ?: RelayCountResult()
val updatedEntries = currentResult.counts.toMutableList()
// Replace or add the entry for this filter
val existingIndex = updatedEntries.indexOfFirst { it.label == queryInfo.label }
val newEntry =
RelayCountResult.CountEntry(
label = queryInfo.label,
count = msg.result.count,
approximate = msg.result.approximate,
)
if (existingIndex >= 0) {
updatedEntries[existingIndex] = newEntry
} else {
updatedEntries.add(newEntry)
}
currentMap + (relayUrl to RelayCountResult(updatedEntries))
}
}
}
private fun cleanup() {
// Close existing queries
subIdToRelay.keys.forEach { subId ->
client.close(subId)
}
subIdToRelay.clear()
relayQueries.clear()
_counts.value = emptyMap()
client.unsubscribe(this)
}
override fun onCleared() {
cleanup()
super.onCleared()
}
companion object {
fun authorCountFilters(
userPubKey: HexKey,
relays: List<NormalizedRelayUrl>,
): Map<NormalizedRelayUrl, List<CountFilter>> =
relays.associateWith {
listOf(
CountFilter(
label = "events",
filter = Filter(authors = listOf(userPubKey)),
),
)
}
fun pTagCountFilters(
userPubKey: HexKey,
relays: List<NormalizedRelayUrl>,
): Map<NormalizedRelayUrl, List<CountFilter>> =
relays.associateWith {
listOf(
CountFilter(
label = "events",
filter = Filter(tags = mapOf("p" to listOf(userPubKey))),
),
)
}
fun dmCountFilters(
userPubKey: HexKey,
relays: List<NormalizedRelayUrl>,
): Map<NormalizedRelayUrl, List<CountFilter>> =
relays.associateWith {
listOf(
CountFilter(
label = "events",
filter =
Filter(
kinds = listOf(GiftWrapEvent.KIND, PrivateDmEvent.KIND),
tags = mapOf("p" to listOf(userPubKey)),
),
),
)
}
fun totalCountFilters(relays: List<NormalizedRelayUrl>): Map<NormalizedRelayUrl, List<CountFilter>> =
relays.associateWith {
listOf(
CountFilter(
label = "events",
filter = Filter(kinds = null),
),
)
}
fun indexerCountFilters(relays: List<NormalizedRelayUrl>): Map<NormalizedRelayUrl, List<CountFilter>> =
relays.associateWith {
listOf(
CountFilter(
label = "kind 0",
filter = Filter(kinds = listOf(0)),
),
CountFilter(
label = "kind 10002",
filter = Filter(kinds = listOf(10002)),
),
)
}
}
}
data class CountFilter( data class CountFilter(
val label: String, val label: String,
val filter: Filter, val filter: Filter,
@@ -22,7 +22,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
@Stable @Stable
class DMRelayListViewModel : BasicRelaySetupInfoModel() { class DMRelayListViewModel : BasicRelaySetupInfoModel() {
@@ -31,4 +35,16 @@ class DMRelayListViewModel : BasicRelaySetupInfoModel() {
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) { override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
account.saveDMRelayList(urlList) account.saveDMRelayList(urlList)
} }
override fun countFilters(relayUrl: NormalizedRelayUrl): List<CountFilter> =
listOf(
CountFilter(
label = "events",
filter =
Filter(
kinds = listOf(GiftWrapEvent.KIND, PrivateDmEvent.KIND),
tags = mapOf("p" to listOf(account.pubKey)),
),
),
)
} }
@@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.indexer
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@Stable @Stable
@@ -33,4 +35,16 @@ class IndexerRelayListViewModel : BasicRelaySetupInfoModel() {
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) { override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
account.saveIndexerRelayList(urlList) account.saveIndexerRelayList(urlList)
} }
override fun countFilters(relayUrl: NormalizedRelayUrl): List<CountFilter> =
listOf(
CountFilter(
label = "kind 0",
filter = Filter(kinds = listOf(0)),
),
CountFilter(
label = "kind 10002",
filter = Filter(kinds = listOf(10002)),
),
)
} }
@@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@Stable @Stable
@@ -33,4 +35,12 @@ class PrivateOutboxRelayListViewModel : BasicRelaySetupInfoModel() {
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) { override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
account.savePrivateOutboxRelayList(urlList) account.savePrivateOutboxRelayList(urlList)
} }
override fun countFilters(relayUrl: NormalizedRelayUrl): List<CountFilter> =
listOf(
CountFilter(
label = "events",
filter = Filter(authors = listOf(account.pubKey)),
),
)
} }
@@ -28,7 +28,15 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.service.replace
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -38,7 +46,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Stable @Stable
class Nip65RelayListViewModel : ViewModel() { class Nip65RelayListViewModel : ViewModel(), IRelayClientListener {
private lateinit var accountViewModel: AccountViewModel private lateinit var accountViewModel: AccountViewModel
private lateinit var account: Account private lateinit var account: Account
@@ -48,6 +56,14 @@ class Nip65RelayListViewModel : ViewModel() {
private val _notificationRelays = MutableStateFlow<List<BasicRelaySetupInfo>>(emptyList()) private val _notificationRelays = MutableStateFlow<List<BasicRelaySetupInfo>>(emptyList())
val notificationRelays = _notificationRelays.asStateFlow() val notificationRelays = _notificationRelays.asStateFlow()
private val _homeCountResults = MutableStateFlow<Map<NormalizedRelayUrl, RelayCountResult>>(emptyMap())
val homeCountResults = _homeCountResults.asStateFlow()
private val _notifCountResults = MutableStateFlow<Map<NormalizedRelayUrl, RelayCountResult>>(emptyMap())
val notifCountResults = _notifCountResults.asStateFlow()
private val subIdToRelay = mutableMapOf<String, Pair<NormalizedRelayUrl, Boolean>>()
var hasModified = false var hasModified = false
fun init(accountViewModel: AccountViewModel) { fun init(accountViewModel: AccountViewModel) {
@@ -58,6 +74,7 @@ class Nip65RelayListViewModel : ViewModel() {
fun load() { fun load() {
clear() clear()
loadRelayDocuments() loadRelayDocuments()
loadCounts()
} }
fun create() { fun create() {
@@ -111,6 +128,74 @@ class Nip65RelayListViewModel : ViewModel() {
} }
} }
private fun loadCounts() {
val client = Amethyst.instance.client
cleanupCounts()
val homeList = _homeRelays.value
val notifList = _notificationRelays.value
if (homeList.isEmpty() && notifList.isEmpty()) return
client.subscribe(this)
homeList.forEach { item ->
val subId = newSubId()
subIdToRelay[subId] = Pair(item.relay, true)
client.queryCount(
subId = subId,
filters = mapOf(item.relay to listOf(Filter(authors = listOf(account.pubKey)))),
)
}
notifList.forEach { item ->
val subId = newSubId()
subIdToRelay[subId] = Pair(item.relay, false)
client.queryCount(
subId = subId,
filters = mapOf(item.relay to listOf(Filter(tags = mapOf("p" to listOf(account.pubKey))))),
)
}
}
override fun onIncomingMessage(
relay: IRelayClient,
msgStr: String,
msg: Message,
) {
if (msg is CountMessage) {
val (relayUrl, isHome) = subIdToRelay[msg.queryId] ?: return
val newResult =
RelayCountResult(
listOf(
RelayCountResult.CountEntry(
label = "events",
count = msg.result.count,
approximate = msg.result.approximate,
),
),
)
if (isHome) {
_homeCountResults.update { it + (relayUrl to newResult) }
} else {
_notifCountResults.update { it + (relayUrl to newResult) }
}
}
}
private fun cleanupCounts() {
val client = Amethyst.instance.client
subIdToRelay.keys.forEach { subId ->
client.close(subId)
}
subIdToRelay.clear()
_homeCountResults.value = emptyMap()
_notifCountResults.value = emptyMap()
client.unsubscribe(this)
}
fun clear() { fun clear() {
hasModified = false hasModified = false
_homeRelays.update { _homeRelays.update {
@@ -181,4 +266,9 @@ class Nip65RelayListViewModel : ViewModel() {
) { ) {
_notificationRelays.update { it.replace(relay, relay.copy(paidRelay = paid)) } _notificationRelays.update { it.replace(relay, relay.copy(paidRelay = paid)) }
} }
override fun onCleared() {
cleanupCounts()
super.onCleared()
}
} }
@@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.proxy
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@Stable @Stable
@@ -33,4 +35,12 @@ class ProxyRelayListViewModel : BasicRelaySetupInfoModel() {
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) { override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
account.saveProxyRelayList(urlList) account.saveProxyRelayList(urlList)
} }
override fun countFilters(relayUrl: NormalizedRelayUrl): List<CountFilter> =
listOf(
CountFilter(
label = "events",
filter = Filter(),
),
)
} }
@@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@Stable @Stable
@@ -33,4 +35,12 @@ class SearchRelayListViewModel : BasicRelaySetupInfoModel() {
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) { override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
account.saveSearchRelayList(urlList) account.saveSearchRelayList(urlList)
} }
override fun countFilters(relayUrl: NormalizedRelayUrl): List<CountFilter> =
listOf(
CountFilter(
label = "events",
filter = Filter(),
),
)
} }