feat: add relay event count stats to AllRelay settings screen

Use NostrClient's NIP-45 COUNT queries to show how many events each
relay stores, with filters specific to each relay role:
- Outbox: events authored by the user
- Inbox: events where the user is p-tagged
- DM Inbox: DM events (kind 4, 1059) tagging the user
- Private Home: events authored by the user
- Proxy: total event count
- Search: total event count
- Indexer: kind 0 and kind 10002 counts separately
- Broadcast: no count (as specified)

https://claude.ai/code/session_016158D5mq5BygS1uBbLNbsA
This commit is contained in:
Claude
2026-03-15 14:41:19 +00:00
parent a21e38f428
commit b1723a5dfb
12 changed files with 429 additions and 7 deletions
@@ -60,6 +60,7 @@ 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.broadcast.BroadcastRelayListViewModel
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.RelayListCollection
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayZipExporter
@@ -109,6 +110,13 @@ fun AllRelayListScreen(
val indexerViewModel: IndexerRelayListViewModel = viewModel()
val proxyViewModel: ProxyRelayListViewModel = 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)
nip65ViewModel.init(accountViewModel)
@@ -151,6 +159,13 @@ fun AllRelayListScreen(
indexerViewModel,
proxyViewModel,
relayFeedsViewModel,
outboxCountViewModel,
inboxCountViewModel,
dmCountViewModel,
privateHomeCountViewModel,
proxyCountViewModel,
indexerCountViewModel,
searchCountViewModel,
accountViewModel,
nav,
)
@@ -171,6 +186,13 @@ fun MappedAllRelayListView(
indexerViewModel: IndexerRelayListViewModel,
proxyViewModel: ProxyRelayListViewModel,
relayFeedsViewModel: RelayFeedsListViewModel,
outboxCountViewModel: RelayEventCountViewModel,
inboxCountViewModel: RelayEventCountViewModel,
dmCountViewModel: RelayEventCountViewModel,
privateHomeCountViewModel: RelayEventCountViewModel,
proxyCountViewModel: RelayEventCountViewModel,
indexerCountViewModel: RelayEventCountViewModel,
searchCountViewModel: RelayEventCountViewModel,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -188,6 +210,72 @@ fun MappedAllRelayListView(
val proxyRelays by proxyViewModel.relays.collectAsStateWithLifecycle()
val relayFeedsFeedState by relayFeedsViewModel.relays.collectAsStateWithLifecycle()
val outboxCounts by outboxCountViewModel.counts.collectAsStateWithLifecycle()
val inboxCounts by inboxCountViewModel.counts.collectAsStateWithLifecycle()
val dmCounts by dmCountViewModel.counts.collectAsStateWithLifecycle()
val privateHomeCounts by privateHomeCountViewModel.counts.collectAsStateWithLifecycle()
val proxyCounts by proxyCountViewModel.counts.collectAsStateWithLifecycle()
val indexerCounts by indexerCountViewModel.counts.collectAsStateWithLifecycle()
val searchCounts by searchCountViewModel.counts.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(
topBar = {
SavingTopBar(
@@ -261,7 +349,7 @@ fun MappedAllRelayListView(
SettingsCategoryFirstModifier,
)
}
renderNip65HomeItems(homeFeedState, nip65ViewModel, accountViewModel, nav)
renderNip65HomeItems(homeFeedState, nip65ViewModel, accountViewModel, nav, outboxCounts)
item {
SettingsCategory(
@@ -270,7 +358,7 @@ fun MappedAllRelayListView(
SettingsCategorySpacingModifier,
)
}
renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, nav)
renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, nav, inboxCounts)
item {
SettingsCategoryWithButton(
@@ -282,7 +370,7 @@ fun MappedAllRelayListView(
},
)
}
renderDMItems(dmFeedState, dmViewModel, accountViewModel, nav)
renderDMItems(dmFeedState, dmViewModel, accountViewModel, nav, dmCounts)
item {
SettingsCategory(
@@ -291,7 +379,7 @@ fun MappedAllRelayListView(
SettingsCategorySpacingModifier,
)
}
renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, nav)
renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, nav, privateHomeCounts)
item {
SettingsCategory(
@@ -300,7 +388,7 @@ fun MappedAllRelayListView(
SettingsCategorySpacingModifier,
)
}
renderProxyItems(proxyRelays, proxyViewModel, accountViewModel, nav)
renderProxyItems(proxyRelays, proxyViewModel, accountViewModel, nav, proxyCounts)
item {
SettingsCategory(
@@ -320,7 +408,7 @@ fun MappedAllRelayListView(
ResetIndexerRelays(indexerViewModel)
}
}
renderIndexerItems(indexerRelays, indexerViewModel, accountViewModel, nav)
renderIndexerItems(indexerRelays, indexerViewModel, accountViewModel, nav, indexerCounts)
item {
SettingsCategoryWithButton(
@@ -331,7 +419,7 @@ fun MappedAllRelayListView(
ResetSearchRelays(searchViewModel)
}
}
renderSearchItems(searchFeedState, searchViewModel, accountViewModel, nav)
renderSearchItems(searchFeedState, searchViewModel, accountViewModel, nav, searchCounts)
item {
SettingsCategory(
@@ -63,6 +63,7 @@ fun BasicRelaySetupInfoClickableRow(
onClick: () -> Unit,
nip11CachedRetriever: Nip11CachedRetriever,
modifier: Modifier = Modifier,
countResult: RelayCountResult? = null,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -104,6 +105,11 @@ fun BasicRelaySetupInfoClickableRow(
UsedBy(item, accountViewModel, nav)
RelayEventCountRow(
countResult = countResult,
modifier = ReactionRowHeightChatMaxWidth,
)
RelayStatusRow(
item = item,
onClick = onClick,
@@ -32,6 +32,7 @@ fun BasicRelaySetupInfoDialog(
item: BasicRelaySetupInfo,
nip11CachedRetriever: Nip11CachedRetriever,
onDelete: ((BasicRelaySetupInfo) -> Unit)?,
countResult: RelayCountResult? = null,
accountViewModel: AccountViewModel,
nav: INav,
) {
@@ -43,6 +44,7 @@ fun BasicRelaySetupInfoDialog(
onClick = { nav.nav(Route.RelayInfo(item.relay.url)) },
nip11CachedRetriever = nip11CachedRetriever,
modifier = HalfVertPadding,
countResult = countResult,
accountViewModel = accountViewModel,
nav = nav,
)
@@ -0,0 +1,85 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.countToHumanReadable
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Font12SP
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
fun RelayEventCountRow(
countResult: RelayCountResult?,
modifier: Modifier,
) {
if (countResult == null || countResult.counts.isEmpty()) return
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
modifier = modifier,
) {
Icon(
imageVector = Icons.Default.Storage,
contentDescription = stringRes(R.string.relay_event_count),
modifier = Size15Modifier,
tint = MaterialTheme.colorScheme.allGoodColor,
)
countResult.counts.forEachIndexed { index, entry ->
if (index > 0) {
Spacer(modifier = Modifier.width(8.dp))
}
val text =
if (entry.approximate) {
"~${countToHumanReadable(entry.count, entry.label)}"
} else {
countToHumanReadable(entry.count, entry.label)
}
Text(
text = text,
maxLines = 1,
fontSize = Font12SP,
modifier = HalfStartPadding,
color = MaterialTheme.colorScheme.placeholderText,
)
}
}
}
@@ -0,0 +1,214 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common
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.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
data class RelayCountResult(
val counts: List<CountEntry> = emptyList(),
) {
@Immutable
data class CountEntry(
val label: String,
val count: Int,
val approximate: Boolean = false,
)
}
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(
val label: String,
val filter: Filter,
)
@@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
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.BasicRelaySetupInfoDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@Composable
fun DMRelayList(
@@ -64,12 +66,14 @@ fun LazyListScope.renderDMItems(
postViewModel: DMRelayListViewModel,
accountViewModel: AccountViewModel,
nav: INav,
countResults: Map<NormalizedRelayUrl, RelayCountResult> = emptyMap(),
) {
itemsIndexed(feedState, key = { _, item -> "DM" + item.relay.url }) { index, item ->
BasicRelaySetupInfoDialog(
item,
onDelete = { postViewModel.deleteRelay(item) },
nip11CachedRetriever = Amethyst.instance.nip11Cache,
countResult = countResults[item.relay],
accountViewModel = accountViewModel,
nav = nav,
)
@@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
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.BasicRelaySetupInfoDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@Composable
fun IndexerRelayList(
@@ -65,12 +67,14 @@ fun LazyListScope.renderIndexerItems(
postViewModel: IndexerRelayListViewModel,
accountViewModel: AccountViewModel,
nav: INav,
countResults: Map<NormalizedRelayUrl, RelayCountResult> = emptyMap(),
) {
itemsIndexed(feedState, key = { _, item -> "Indexer" + item.relay.url }) { index, item ->
BasicRelaySetupInfoDialog(
item,
onDelete = { postViewModel.deleteRelay(item) },
nip11CachedRetriever = Amethyst.instance.nip11Cache,
countResult = countResults[item.relay],
accountViewModel = accountViewModel,
nav = nav,
)
@@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
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.BasicRelaySetupInfoDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@Composable
fun PrivateOutboxRelayList(
@@ -64,12 +66,14 @@ fun LazyListScope.renderPrivateOutboxItems(
postViewModel: PrivateOutboxRelayListViewModel,
accountViewModel: AccountViewModel,
nav: INav,
countResults: Map<NormalizedRelayUrl, RelayCountResult> = emptyMap(),
) {
itemsIndexed(feedState, key = { _, item -> "Outbox" + item.relay.url }) { index, item ->
BasicRelaySetupInfoDialog(
item,
onDelete = { postViewModel.deleteRelay(item) },
nip11CachedRetriever = Amethyst.instance.nip11Cache,
countResult = countResults[item.relay],
accountViewModel = accountViewModel,
nav = nav,
)
@@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
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.BasicRelaySetupInfoDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@Composable
fun Nip65RelayList(
@@ -107,12 +109,14 @@ fun LazyListScope.renderNip65HomeItems(
postViewModel: Nip65RelayListViewModel,
accountViewModel: AccountViewModel,
nav: INav,
countResults: Map<NormalizedRelayUrl, RelayCountResult> = emptyMap(),
) {
itemsIndexed(feedState, key = { _, item -> "Nip65Home" + item.relay.url }) { index, item ->
BasicRelaySetupInfoDialog(
item,
onDelete = { postViewModel.deleteHomeRelay(item) },
nip11CachedRetriever = Amethyst.instance.nip11Cache,
countResult = countResults[item.relay],
accountViewModel = accountViewModel,
nav = nav,
)
@@ -133,12 +137,14 @@ fun LazyListScope.renderNip65NotifItems(
postViewModel: Nip65RelayListViewModel,
accountViewModel: AccountViewModel,
nav: INav,
countResults: Map<NormalizedRelayUrl, RelayCountResult> = emptyMap(),
) {
itemsIndexed(feedState, key = { _, item -> "Nip65Notif" + item.relay.url }) { index, item ->
BasicRelaySetupInfoDialog(
item,
onDelete = { postViewModel.deleteNotifRelay(item) },
nip11CachedRetriever = Amethyst.instance.nip11Cache,
countResult = countResults[item.relay],
accountViewModel = accountViewModel,
nav = nav,
)
@@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
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.BasicRelaySetupInfoDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@Composable
fun ProxyRelayList(
@@ -65,12 +67,14 @@ fun LazyListScope.renderProxyItems(
postViewModel: ProxyRelayListViewModel,
accountViewModel: AccountViewModel,
nav: INav,
countResults: Map<NormalizedRelayUrl, RelayCountResult> = emptyMap(),
) {
itemsIndexed(feedState, key = { _, item -> "Proxy" + item.relay.url }) { index, item ->
BasicRelaySetupInfoDialog(
item,
onDelete = { postViewModel.deleteRelay(item) },
nip11CachedRetriever = Amethyst.instance.nip11Cache,
countResult = countResults[item.relay],
accountViewModel = accountViewModel,
nav = nav,
)
@@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav
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.BasicRelaySetupInfoDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@Composable
fun SearchRelayList(
@@ -65,12 +67,14 @@ fun LazyListScope.renderSearchItems(
postViewModel: SearchRelayListViewModel,
accountViewModel: AccountViewModel,
nav: INav,
countResults: Map<NormalizedRelayUrl, RelayCountResult> = emptyMap(),
) {
itemsIndexed(feedState, key = { _, item -> "Search" + item.relay.url }) { index, item ->
BasicRelaySetupInfoDialog(
item,
onDelete = { postViewModel.deleteRelay(item) },
nip11CachedRetriever = Amethyst.instance.nip11Cache,
countResult = countResults[item.relay],
accountViewModel = accountViewModel,
nav = nav,
)
+1
View File
@@ -776,6 +776,7 @@
<string name="write_to_relay">Write to Relay</string>
<string name="write_to_relay_description">The amount in bytes that was sent to this relay, including filters and events</string>
<string name="read_from_relay_description">The amount in bytes that was received from this relay, including filters and events</string>
<string name="relay_event_count">Events stored</string>
<string name="an_error_occurred_trying_to_get_relay_information">An error occurred trying to get relay information from %1$s</string>
<string name="owner">Owner</string>
<string name="used_by">Used By</string>