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
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.RelayStats
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
@@ -31,6 +32,7 @@ data class BasicRelaySetupInfo(
val relayStat: RelayStat,
val paidRelay: Boolean = false,
val forcesTor: Boolean = false,
val users: List<User> = emptyList(),
)
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.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
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.UserPicture
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.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
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.ReactionRowHeightChatMaxWidth
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
@OptIn(ExperimentalFoundationApi::class)
@@ -53,6 +64,7 @@ fun BasicRelaySetupInfoClickableRow(
onDelete: ((BasicRelaySetupInfo) -> Unit)?,
onClick: () -> Unit,
accountViewModel: AccountViewModel,
nav: INav,
) {
val clipboardManager = LocalClipboardManager.current
Column(
@@ -90,6 +102,8 @@ fun BasicRelaySetupInfoClickableRow(
ReactionRowHeightChatMaxWidth,
)
UsedBy(item, accountViewModel, nav)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = ReactionRowHeightChatMaxWidth,
@@ -106,3 +120,56 @@ fun BasicRelaySetupInfoClickableRow(
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,
accountViewModel = accountViewModel,
onClick = { nav.nav(Route.RelayInfo(item.relay.url)) },
nav = nav,
)
}

View File

@@ -83,22 +83,24 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
}
}
fun clear() {
_relays.update {
val relayList = getRelayList() ?: emptyList()
open fun relayListBuilder(): List<BasicRelaySetupInfo> {
val relayList = getRelayList() ?: emptyList()
relayList
.map {
relaySetupInfoBuilder(
normalized = it,
forcesTor =
account.torRelayState.flow.value
.useTor(it),
)
}.distinctBy { it.relay }
.sortedBy { it.relayStat.receivedBytes }
.reversed()
}
return relayList
.map {
relaySetupInfoBuilder(
normalized = it,
forcesTor =
account.torRelayState.flow.value
.useTor(it),
)
}.distinctBy { it.relay }
.sortedBy { it.relayStat.receivedBytes }
.reversed()
}
fun clear() {
_relays.update { relayListBuilder() }
}
fun addRelay(relay: BasicRelaySetupInfo) {

View File

@@ -20,10 +20,34 @@
*/
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.quartz.nip01Core.relay.client.stats.RelayStats
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
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> =
account.client
.relayStatusFlow()

View File

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