Adds a user list to each connected relay to know where this is coming from

This commit is contained in:
Vitor Pamplona
2025-08-16 13:21:10 -04:00
parent 88933fba8d
commit fca4aee5f5
6 changed files with 112 additions and 15 deletions

View File

@@ -21,6 +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 com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -31,6 +32,7 @@ data class BasicRelaySetupInfo(
val relayStat: RelayStat, val relayStat: RelayStat,
val paidRelay: Boolean = false, val paidRelay: Boolean = false,
val forcesTor: Boolean = false, val forcesTor: Boolean = false,
val users: List<User> = emptyList(),
) )
fun relaySetupInfoBuilder( fun relaySetupInfoBuilder(

View File

@@ -22,26 +22,37 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache.users
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.nav
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding
import com.vitorpamplona.amethyst.ui.theme.Height25Modifier
import com.vitorpamplona.amethyst.ui.theme.LargeRelayIconModifier import com.vitorpamplona.amethyst.ui.theme.LargeRelayIconModifier
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChatMaxWidth import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChatMaxWidth
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -53,6 +64,7 @@ fun BasicRelaySetupInfoClickableRow(
onDelete: ((BasicRelaySetupInfo) -> Unit)?, onDelete: ((BasicRelaySetupInfo) -> Unit)?,
onClick: () -> Unit, onClick: () -> Unit,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav,
) { ) {
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
Column( Column(
@@ -90,6 +102,8 @@ fun BasicRelaySetupInfoClickableRow(
ReactionRowHeightChatMaxWidth, ReactionRowHeightChatMaxWidth,
) )
UsedBy(item, accountViewModel, nav)
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = ReactionRowHeightChatMaxWidth, modifier = ReactionRowHeightChatMaxWidth,
@@ -106,3 +120,56 @@ fun BasicRelaySetupInfoClickableRow(
HorizontalDivider(thickness = DividerThickness) HorizontalDivider(thickness = DividerThickness)
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun UsedBy(
item: BasicRelaySetupInfo,
accountViewModel: AccountViewModel,
nav: INav,
) {
if (item.users.isNotEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) {
item.users.getOrNull(0)?.let {
UserPicture(
user = it,
size = Size25dp,
accountViewModel = accountViewModel,
nav = nav,
)
}
item.users.getOrNull(1)?.let {
UserPicture(
user = it,
size = Size25dp,
accountViewModel = accountViewModel,
nav = nav,
)
}
item.users.getOrNull(2)?.let {
UserPicture(
user = it,
size = Size25dp,
accountViewModel = accountViewModel,
nav = nav,
)
}
item.users.getOrNull(3)?.let {
UserPicture(
user = it,
size = Size25dp,
accountViewModel = accountViewModel,
nav = nav,
)
}
if (item.users.size > 4) {
Box(contentAlignment = Alignment.Center, modifier = Height25Modifier) {
Text(
text = stringRes(R.string.and_more, item.users.size - 4),
maxLines = 1,
)
}
}
}
}
}

View File

@@ -44,5 +44,6 @@ fun BasicRelaySetupInfoDialog(
onDelete = onDelete, onDelete = onDelete,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
onClick = { nav.nav(Route.RelayInfo(item.relay.url)) }, onClick = { nav.nav(Route.RelayInfo(item.relay.url)) },
nav = nav,
) )
} }

View File

@@ -83,11 +83,10 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
} }
} }
fun clear() { open fun relayListBuilder(): List<BasicRelaySetupInfo> {
_relays.update {
val relayList = getRelayList() ?: emptyList() val relayList = getRelayList() ?: emptyList()
relayList return relayList
.map { .map {
relaySetupInfoBuilder( relaySetupInfoBuilder(
normalized = it, normalized = it,
@@ -99,6 +98,9 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
.sortedBy { it.relayStat.receivedBytes } .sortedBy { it.relayStat.receivedBytes }
.reversed() .reversed()
} }
fun clear() {
_relays.update { relayListBuilder() }
} }
fun addRelay(relay: BasicRelaySetupInfo) { fun addRelay(relay: BasicRelaySetupInfo) {

View File

@@ -20,10 +20,34 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() { class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() {
override fun relayListBuilder(): List<BasicRelaySetupInfo> {
val relayList = getRelayList()
return relayList
.map {
BasicRelaySetupInfo(
relay = it,
relayStat = RelayStats.get(it),
forcesTor =
account.torRelayState.flow.value
.useTor(it),
users =
account.followsPerRelay.value[it]?.mapNotNull { hex ->
LocalCache.checkGetOrCreateUser(hex)
} ?: emptyList(),
)
}.distinctBy { it.relay }
.sortedBy { it.relayStat.receivedBytes }
.reversed()
}
override fun getRelayList(): List<NormalizedRelayUrl> = override fun getRelayList(): List<NormalizedRelayUrl> =
account.client account.client
.relayStatusFlow() .relayStatusFlow()

View File

@@ -189,6 +189,7 @@ val UserNameMaxRowHeight = Modifier.fillMaxWidth()
val Height24dpModifier = Modifier.height(24.dp) val Height24dpModifier = Modifier.height(24.dp)
val Height4dpModifier = Modifier.height(4.dp) val Height4dpModifier = Modifier.height(4.dp)
val Height25Modifier = Modifier.height(Size25dp)
val Height24dpFilledModifier = Modifier.fillMaxWidth().height(24.dp) val Height24dpFilledModifier = Modifier.fillMaxWidth().height(24.dp)
val Height4dpFilledModifier = Modifier.fillMaxWidth().height(4.dp) val Height4dpFilledModifier = Modifier.fillMaxWidth().height(4.dp)