Adds a list of users of a relay to the relay information dialog.
This commit is contained in:
@@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountOutboxRelayStat
|
|||||||
import com.vitorpamplona.amethyst.model.nip01UserMetadata.NotificationInboxRelayState
|
import com.vitorpamplona.amethyst.model.nip01UserMetadata.NotificationInboxRelayState
|
||||||
import com.vitorpamplona.amethyst.model.nip01UserMetadata.UserMetadataState
|
import com.vitorpamplona.amethyst.model.nip01UserMetadata.UserMetadataState
|
||||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.DeclaredFollowsPerOutboxRelay
|
import com.vitorpamplona.amethyst.model.nip02FollowLists.DeclaredFollowsPerOutboxRelay
|
||||||
|
import com.vitorpamplona.amethyst.model.nip02FollowLists.DeclaredFollowsPerUsingRelay
|
||||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays
|
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays
|
||||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListReusedOutboxOrProxyRelays
|
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListReusedOutboxOrProxyRelays
|
||||||
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowsPerOutboxRelay
|
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowsPerOutboxRelay
|
||||||
@@ -340,7 +341,8 @@ class Account(
|
|||||||
val defaultGlobalRelays = MergedFollowPlusMineRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, scope)
|
val defaultGlobalRelays = MergedFollowPlusMineRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, scope)
|
||||||
|
|
||||||
// keeps a cache of the declared outbox relays for each author
|
// keeps a cache of the declared outbox relays for each author
|
||||||
val declaredFollowsPerRelay = DeclaredFollowsPerOutboxRelay(kind3FollowList, cache, scope).flow
|
val declaredFollowsPerOutboxRelay = DeclaredFollowsPerOutboxRelay(kind3FollowList, cache, scope).flow
|
||||||
|
val declaredFollowsPerUsingRelay = DeclaredFollowsPerUsingRelay(kind3FollowList, cache, scope).flow
|
||||||
|
|
||||||
// keeps a cache of the outbox relays for each author
|
// keeps a cache of the outbox relays for each author
|
||||||
val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope).flow
|
val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope).flow
|
||||||
|
|||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* 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.model.nip02FollowLists
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.topNavFeeds.UsingRelayUnwrapper
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.emitAll
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.transformLatest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a list of all declared relays for each user in the follow list.
|
||||||
|
*/
|
||||||
|
class DeclaredFollowsPerUsingRelay(
|
||||||
|
kind3Follows: Kind3FollowListState,
|
||||||
|
val cache: LocalCache,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
) {
|
||||||
|
val calculator = UsingRelayUnwrapper()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val flow: StateFlow<Map<NormalizedRelayUrl, Set<HexKey>>> =
|
||||||
|
kind3Follows.flow
|
||||||
|
.transformLatest {
|
||||||
|
emitAll(
|
||||||
|
calculator.toUsersPerRelayFlow(it.authors, cache) { it },
|
||||||
|
)
|
||||||
|
}.onStart {
|
||||||
|
emit(
|
||||||
|
calculator.usersPerRelaySnapshot(kind3Follows.flow.value.authors, cache) { it },
|
||||||
|
)
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.stateIn(
|
||||||
|
scope,
|
||||||
|
SharingStarted.Eagerly,
|
||||||
|
emptyMap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* 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.model.topNavFeeds
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.model.NoteState
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||||
|
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||||
|
import com.vitorpamplona.quartz.utils.mapOfSet
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
||||||
|
class UsingRelayUnwrapper {
|
||||||
|
fun usersPerRelay(relayListNotes: Array<NoteState>): Map<NormalizedRelayUrl, Set<HexKey>> =
|
||||||
|
mapOfSet {
|
||||||
|
relayListNotes.forEach { outboxNote ->
|
||||||
|
val note = outboxNote.note
|
||||||
|
|
||||||
|
val authorHex =
|
||||||
|
if (note is AddressableNote) {
|
||||||
|
note.address.pubKeyHex
|
||||||
|
} else {
|
||||||
|
note.author?.pubkeyHex
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authorHex != null) {
|
||||||
|
val relays =
|
||||||
|
when (val noteEvent = outboxNote.note.event) {
|
||||||
|
is AdvertisedRelayListEvent -> noteEvent.relaysNorm()
|
||||||
|
is ChatMessageRelayListEvent -> noteEvent.relays()
|
||||||
|
else -> emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
relays.forEach {
|
||||||
|
add(it, authorHex)
|
||||||
|
}
|
||||||
|
relays.forEach {
|
||||||
|
add(it, authorHex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> usersPerRelaySnapshot(
|
||||||
|
users: Set<HexKey>,
|
||||||
|
cache: LocalCache,
|
||||||
|
transformation: (Map<NormalizedRelayUrl, Set<HexKey>>) -> T,
|
||||||
|
): T {
|
||||||
|
val nip65RelayNotes =
|
||||||
|
users
|
||||||
|
.map { pubkeyHex ->
|
||||||
|
cache
|
||||||
|
.getOrCreateAddressableNote(AdvertisedRelayListEvent.createAddress(pubkeyHex))
|
||||||
|
.flow()
|
||||||
|
.metadata.stateFlow.value
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
val nip17RelayNotes =
|
||||||
|
users
|
||||||
|
.map { pubkeyHex ->
|
||||||
|
cache
|
||||||
|
.getOrCreateAddressableNote(ChatMessageRelayListEvent.createAddress(pubkeyHex))
|
||||||
|
.flow()
|
||||||
|
.metadata.stateFlow.value
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
return transformation(usersPerRelay(nip65RelayNotes + nip17RelayNotes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> toUsersPerRelayFlow(
|
||||||
|
users: Set<HexKey>,
|
||||||
|
cache: LocalCache,
|
||||||
|
transformation: (Map<NormalizedRelayUrl, Set<HexKey>>) -> T,
|
||||||
|
): Flow<T> {
|
||||||
|
val relayNoteFlows =
|
||||||
|
users.map { pubkeyHex ->
|
||||||
|
val note = cache.getOrCreateAddressableNote(AdvertisedRelayListEvent.createAddress(pubkeyHex))
|
||||||
|
note.flow().metadata.stateFlow
|
||||||
|
} +
|
||||||
|
users.map { pubkeyHex ->
|
||||||
|
val note = cache.getOrCreateAddressableNote(ChatMessageRelayListEvent.createAddress(pubkeyHex))
|
||||||
|
note.flow().metadata.stateFlow
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (relayNoteFlows.isEmpty()) {
|
||||||
|
MutableStateFlow(transformation(emptyMap()))
|
||||||
|
} else {
|
||||||
|
combine(relayNoteFlows) { relayListNotes ->
|
||||||
|
transformation(usersPerRelay(relayListNotes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -67,7 +67,7 @@ class UserReportsSubAssembler(
|
|||||||
|
|
||||||
val trustedAccountsPerRelay =
|
val trustedAccountsPerRelay =
|
||||||
mapOfSet {
|
mapOfSet {
|
||||||
accounts.map { it.declaredFollowsPerRelay.value }.forEach {
|
accounts.map { it.declaredFollowsPerOutboxRelay.value }.forEach {
|
||||||
add(it)
|
add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+43
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays
|
|||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -97,6 +98,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
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
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||||
import com.vitorpamplona.amethyst.ui.components.appendLink
|
import com.vitorpamplona.amethyst.ui.components.appendLink
|
||||||
@@ -105,6 +107,7 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
|||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
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.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
|
||||||
@@ -113,7 +116,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Height25Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size100dp
|
import com.vitorpamplona.amethyst.ui.theme.Size100dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||||
@@ -345,6 +350,13 @@ fun RelayInformationBody(
|
|||||||
val activeCounts = remember(relay) { nostrClient?.activeCounts(relay) ?: emptyMap() }
|
val activeCounts = remember(relay) { nostrClient?.activeCounts(relay) ?: emptyMap() }
|
||||||
val activeOutbox = remember(relay) { nostrClient?.activeOutboxCache(relay) ?: emptySet() }
|
val activeOutbox = remember(relay) { nostrClient?.activeOutboxCache(relay) ?: emptySet() }
|
||||||
|
|
||||||
|
val usedBy =
|
||||||
|
remember(relay) {
|
||||||
|
accountViewModel.account.declaredFollowsPerUsingRelay.value[relay]?.mapNotNull { hex ->
|
||||||
|
LocalCache.checkGetOrCreateUser(hex)
|
||||||
|
} ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
@@ -407,6 +419,37 @@ fun RelayInformationBody(
|
|||||||
item { SoftwareCard(relayInfo) }
|
item { SoftwareCard(relayInfo) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (usedBy.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
SectionHeader(stringRes(R.string.used_by))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
) {
|
||||||
|
FlowRow(modifier = Modifier.padding(10.dp), verticalArrangement = Arrangement.Center) {
|
||||||
|
usedBy.take(30).forEach {
|
||||||
|
UserPicture(
|
||||||
|
user = it,
|
||||||
|
size = Size25dp,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (usedBy.size > 30) {
|
||||||
|
Box(contentAlignment = Alignment.Center, modifier = Height25Modifier) {
|
||||||
|
Text(
|
||||||
|
text = stringRes(R.string.and_more, usedBy.size - 30),
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Active subscriptions and outbox section
|
// Active subscriptions and outbox section
|
||||||
val hasReqs = activeReqs.isNotEmpty()
|
val hasReqs = activeReqs.isNotEmpty()
|
||||||
val hasCounts = activeCounts.isNotEmpty()
|
val hasCounts = activeCounts.isNotEmpty()
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() {
|
|||||||
Amethyst.instance.torEvaluatorFlow.flow.value
|
Amethyst.instance.torEvaluatorFlow.flow.value
|
||||||
.useTor(it),
|
.useTor(it),
|
||||||
users =
|
users =
|
||||||
account.declaredFollowsPerRelay.value[it]?.mapNotNull { hex ->
|
account.declaredFollowsPerUsingRelay.value[it]?.mapNotNull { hex ->
|
||||||
LocalCache.checkGetOrCreateUser(hex)
|
LocalCache.checkGetOrCreateUser(hex)
|
||||||
} ?: emptyList(),
|
} ?: emptyList(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -772,6 +772,7 @@
|
|||||||
<string name="read_from_relay_description">The amount in bytes that was received from 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="an_error_occurred_trying_to_get_relay_information">An error occurred trying to get relay information from %1$s</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="owner">Owner</string>
|
||||||
|
<string name="used_by">Used By</string>
|
||||||
<string name="self">Service Key</string>
|
<string name="self">Service Key</string>
|
||||||
<string name="running_software">Running %1$s</string>
|
<string name="running_software">Running %1$s</string>
|
||||||
<string name="running_software_version">Running %1$s (%2$s)</string>
|
<string name="running_software_version">Running %1$s (%2$s)</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user