mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
feat(desktop): five profile tabs — Followers, Following, Relays, Bookmarks, Mutual
- Scrollable tab row; Followers/Following render UserSearchCard from live contact-list subscriptions; Relays render a new RelayRowCard from kind 10002; Bookmarks (kind 10003 → DesktopBookmarkFeedFilter) and Mutual (new DesktopMutualFeedFilter) render FeedNoteCard - Respects the shared mute/block hidden-set via DesktopFeedViewModel Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e862b3ecac
commit
bc482cb6eb
+34
@@ -253,6 +253,40 @@ class DesktopProfileFeedFilter(
|
||||
override fun limit(): Int = 1000
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutual feed: notes authored by the logged-in user ([myPubkey]) that tag the
|
||||
* viewed profile ([profilePubkey]) — "posts you wrote that mention them".
|
||||
*/
|
||||
class DesktopMutualFeedFilter(
|
||||
private val myPubkey: HexKey,
|
||||
private val profilePubkey: HexKey,
|
||||
private val cache: DesktopLocalCache,
|
||||
private val hidden: () -> LiveHiddenUsers = { LiveHiddenUsers.EMPTY },
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String = "mutual-$myPubkey-$profilePubkey"
|
||||
|
||||
private fun isMutualNote(note: Note): Boolean {
|
||||
val event = note.event ?: return false
|
||||
return note.author?.pubkeyHex == myPubkey &&
|
||||
isFeedNote(event) &&
|
||||
event.isTaggedUser(profilePubkey) &&
|
||||
!note.isHiddenFor(hidden())
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> =
|
||||
cache.notes
|
||||
.filterIntoSet { _, note -> isMutualNote(note) }
|
||||
.sortedWith(DefaultFeedOrder)
|
||||
.deduplicateReposts()
|
||||
.take(limit())
|
||||
|
||||
override fun applyFilter(newItems: Set<Note>): Set<Note> = newItems.filterTo(HashSet()) { isMutualNote(it) }
|
||||
|
||||
override fun sort(items: Set<Note>): List<Note> = items.sortedWith(DefaultFeedOrder).deduplicateReposts()
|
||||
|
||||
override fun limit(): Int = 200
|
||||
}
|
||||
|
||||
/**
|
||||
* Bookmark feed: notes by ID set (from BookmarkListEvent).
|
||||
*/
|
||||
|
||||
+295
-5
@@ -50,7 +50,7 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
import androidx.compose.material3.PrimaryScrollableTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -82,9 +82,12 @@ import com.vitorpamplona.amethyst.commons.profile.ui.ProfileBroadcastBanner
|
||||
import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser
|
||||
import com.vitorpamplona.amethyst.commons.state.FollowState
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.UserSearchCard
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
||||
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
|
||||
import com.vitorpamplona.amethyst.desktop.feeds.DesktopBookmarkFeedFilter
|
||||
import com.vitorpamplona.amethyst.desktop.feeds.DesktopMutualFeedFilter
|
||||
import com.vitorpamplona.amethyst.desktop.feeds.DesktopProfileFeedFilter
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||
@@ -99,11 +102,14 @@ import com.vitorpamplona.amethyst.desktop.ui.note.RichTextCallbacks
|
||||
import com.vitorpamplona.amethyst.desktop.ui.note.WoTBadgedAvatar
|
||||
import com.vitorpamplona.amethyst.desktop.ui.profile.EditProfileDialog
|
||||
import com.vitorpamplona.amethyst.desktop.ui.profile.GalleryTab
|
||||
import com.vitorpamplona.amethyst.desktop.ui.profile.RelayRowCard
|
||||
import com.vitorpamplona.amethyst.desktop.viewmodels.DesktopFeedViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent
|
||||
@@ -111,6 +117,9 @@ import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -252,6 +261,52 @@ fun UserProfileScreen(
|
||||
} else {
|
||||
kotlinx.collections.immutable.persistentListOf()
|
||||
}
|
||||
|
||||
// Mutual — notes the logged-in user authored that tag this profile.
|
||||
val mutualViewModel =
|
||||
remember(pubKeyHex, account, iAccount) {
|
||||
if (account != null) {
|
||||
DesktopFeedViewModel(
|
||||
DesktopMutualFeedFilter(account.pubKeyHex, pubKeyHex, localCache, hidden = hidden),
|
||||
localCache,
|
||||
iAccount?.hiddenUsers,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
DisposableEffect(mutualViewModel) { onDispose { mutualViewModel?.destroy() } }
|
||||
val mutualLoadedNotes =
|
||||
mutualViewModel?.let { vm ->
|
||||
val state by vm.feedState.feedContent.collectAsState()
|
||||
if (state is FeedState.Loaded) {
|
||||
val loaded by (state as FeedState.Loaded).feed.collectAsState()
|
||||
loaded.list
|
||||
} else {
|
||||
kotlinx.collections.immutable.persistentListOf()
|
||||
}
|
||||
} ?: kotlinx.collections.immutable.persistentListOf()
|
||||
|
||||
// Bookmarks — public bookmarks (kind 10003) resolved to notes from cache.
|
||||
var bookmarkIds by remember(pubKeyHex) { mutableStateOf<Set<HexKey>>(emptySet()) }
|
||||
val bookmarkViewModel =
|
||||
remember(pubKeyHex) {
|
||||
DesktopFeedViewModel(
|
||||
DesktopBookmarkFeedFilter({ bookmarkIds }, localCache),
|
||||
localCache,
|
||||
)
|
||||
}
|
||||
DisposableEffect(bookmarkViewModel) { onDispose { bookmarkViewModel.destroy() } }
|
||||
LaunchedEffect(bookmarkIds) { bookmarkViewModel.invalidateData(false) }
|
||||
val bookmarkFeedState by bookmarkViewModel.feedState.feedContent.collectAsState()
|
||||
val bookmarkLoadedNotes =
|
||||
if (bookmarkFeedState is FeedState.Loaded) {
|
||||
val loaded by (bookmarkFeedState as FeedState.Loaded).feed.collectAsState()
|
||||
loaded.list
|
||||
} else {
|
||||
kotlinx.collections.immutable.persistentListOf()
|
||||
}
|
||||
|
||||
var retryTrigger by remember { mutableStateOf(0) }
|
||||
|
||||
// Subscribe to profile user's text notes (kind 1) — populates cache for DesktopFeedViewModel
|
||||
@@ -284,6 +339,12 @@ fun UserProfileScreen(
|
||||
val articleEvents = remember { mutableStateListOf<LongTextNoteEvent>() }
|
||||
val highlightEvents = remember { mutableStateListOf<HighlightEvent>() }
|
||||
|
||||
// Followers / Following user lists (pubkeys) and the profile's relay list.
|
||||
val followerList = remember(pubKeyHex) { mutableStateListOf<String>() }
|
||||
val followingList = remember(pubKeyHex) { mutableStateListOf<String>() }
|
||||
// Each relay entry: (url, canRead, canWrite)
|
||||
var relayList by remember(pubKeyHex) { mutableStateOf<List<Triple<String, Boolean, Boolean>>>(emptyList()) }
|
||||
|
||||
// Follow state
|
||||
val followState =
|
||||
remember(account) {
|
||||
@@ -390,9 +451,11 @@ fun UserProfileScreen(
|
||||
pubKeyHex = pubKeyHex,
|
||||
onEvent = { event, _, _, _ ->
|
||||
if (event is ContactListEvent) {
|
||||
val count = event.verifiedFollowKeySet().size
|
||||
followingCount = count
|
||||
localCache.cacheFollowingCount(pubKeyHex, count)
|
||||
val follows = event.verifiedFollowKeySet()
|
||||
followingCount = follows.size
|
||||
followingList.clear()
|
||||
followingList.addAll(follows)
|
||||
localCache.cacheFollowingCount(pubKeyHex, follows.size)
|
||||
}
|
||||
},
|
||||
onEose = { _, _ -> },
|
||||
@@ -410,6 +473,7 @@ fun UserProfileScreen(
|
||||
if (connectedRelays.isNotEmpty()) {
|
||||
// Clear dedup set but keep cached followersCount visible until new data arrives
|
||||
followerAuthors.clear()
|
||||
followerList.clear()
|
||||
|
||||
SubscriptionConfig(
|
||||
subId = "followers-${pubKeyHex.take(8)}-${System.currentTimeMillis()}",
|
||||
@@ -425,6 +489,7 @@ fun UserProfileScreen(
|
||||
onEvent = { event, _, _, _ ->
|
||||
// Count unique authors who follow this user
|
||||
if (followerAuthors.add(event.pubKey)) {
|
||||
followerList.add(event.pubKey)
|
||||
val count = followerAuthors.size
|
||||
followersCount = count
|
||||
localCache.cacheFollowerCount(pubKeyHex, count)
|
||||
@@ -518,6 +583,103 @@ fun UserProfileScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to the profile user's relay list (kind 10002) for the Relays tab
|
||||
rememberSubscription(connectedRelays, pubKeyHex, retryTrigger, relayManager = relayManager) {
|
||||
if (connectedRelays.isNotEmpty()) {
|
||||
SubscriptionConfig(
|
||||
subId = generateSubId("relays-${pubKeyHex.take(8)}"),
|
||||
filters =
|
||||
listOf(
|
||||
FilterBuilders.byAuthors(
|
||||
authors = listOf(pubKeyHex),
|
||||
kinds = listOf(AdvertisedRelayListEvent.KIND),
|
||||
limit = 1,
|
||||
),
|
||||
),
|
||||
relays = connectedRelays,
|
||||
onEvent = { event, _, _, _ ->
|
||||
if (event is AdvertisedRelayListEvent) {
|
||||
val writes = event.writeRelaysNorm()?.map { it.url }?.toSet() ?: emptySet()
|
||||
val reads = event.readRelaysNorm()?.map { it.url }?.toSet() ?: emptySet()
|
||||
relayList = (writes + reads).sorted().map { url -> Triple(url, url in reads, url in writes) }
|
||||
}
|
||||
},
|
||||
onEose = { _, _ -> },
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to the profile user's public bookmarks list (kind 10003)
|
||||
rememberSubscription(connectedRelays, pubKeyHex, retryTrigger, relayManager = relayManager) {
|
||||
if (connectedRelays.isNotEmpty()) {
|
||||
SubscriptionConfig(
|
||||
subId = generateSubId("bmlist-${pubKeyHex.take(8)}"),
|
||||
filters =
|
||||
listOf(
|
||||
FilterBuilders.byAuthors(
|
||||
authors = listOf(pubKeyHex),
|
||||
kinds = listOf(BookmarkListEvent.KIND),
|
||||
limit = 1,
|
||||
),
|
||||
),
|
||||
relays = connectedRelays,
|
||||
onEvent = { event, _, _, _ ->
|
||||
if (event is BookmarkListEvent) {
|
||||
bookmarkIds =
|
||||
event
|
||||
.publicBookmarks()
|
||||
.filterIsInstance<EventBookmark>()
|
||||
.map { it.eventId }
|
||||
.toSet()
|
||||
}
|
||||
},
|
||||
onEose = { _, _ -> },
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the bookmarked notes themselves once their ids are known
|
||||
rememberSubscription(connectedRelays, bookmarkIds, relayManager = relayManager) {
|
||||
if (connectedRelays.isNotEmpty() && bookmarkIds.isNotEmpty()) {
|
||||
SubscriptionConfig(
|
||||
subId = generateSubId("bmnotes-${pubKeyHex.take(8)}"),
|
||||
filters = listOf(FilterBuilders.byIds(bookmarkIds.toList())),
|
||||
relays = connectedRelays,
|
||||
onEvent = { event, _, relay, _ -> subscriptionsCoordinator?.consumeEvent(event, relay) },
|
||||
onEose = { _, _ -> },
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the logged-in user's notes that tag this profile (Mutual tab)
|
||||
rememberSubscription(connectedRelays, pubKeyHex, account, retryTrigger, relayManager = relayManager) {
|
||||
if (connectedRelays.isNotEmpty() && account != null) {
|
||||
SubscriptionConfig(
|
||||
subId = generateSubId("mutual-${pubKeyHex.take(8)}"),
|
||||
filters =
|
||||
listOf(
|
||||
Filter(
|
||||
kinds = listOf(TextNoteEvent.KIND),
|
||||
authors = listOf(account.pubKeyHex),
|
||||
tags = mapOf("p" to listOf(pubKeyHex)),
|
||||
limit = 200,
|
||||
),
|
||||
),
|
||||
relays = connectedRelays,
|
||||
onEvent = { event, _, relay, _ -> subscriptionsCoordinator?.consumeEvent(event, relay) },
|
||||
onEose = { _, _ -> },
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll state for detecting scroll direction
|
||||
val listState = rememberLazyListState()
|
||||
var showFloatingHeader by remember { mutableStateOf(false) }
|
||||
@@ -936,7 +1098,7 @@ fun UserProfileScreen(
|
||||
|
||||
// Tabs
|
||||
item(key = "tabs") {
|
||||
PrimaryTabRow(selectedTabIndex = selectedTab) {
|
||||
PrimaryScrollableTabRow(selectedTabIndex = selectedTab, edgePadding = 0.dp) {
|
||||
Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) {
|
||||
Text("Notes", modifier = Modifier.padding(12.dp))
|
||||
}
|
||||
@@ -958,6 +1120,30 @@ fun UserProfileScreen(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
)
|
||||
}
|
||||
Tab(selected = selectedTab == 5, onClick = { selectedTab = 5 }) {
|
||||
Text(
|
||||
"Followers${if (followersCount > 0) " ($followersCount)" else ""}",
|
||||
modifier = Modifier.padding(12.dp),
|
||||
)
|
||||
}
|
||||
Tab(selected = selectedTab == 6, onClick = { selectedTab = 6 }) {
|
||||
Text(
|
||||
"Following${if (followingCount > 0) " ($followingCount)" else ""}",
|
||||
modifier = Modifier.padding(12.dp),
|
||||
)
|
||||
}
|
||||
Tab(selected = selectedTab == 7, onClick = { selectedTab = 7 }) {
|
||||
Text(
|
||||
"Relays${if (relayList.isNotEmpty()) " (${relayList.size})" else ""}",
|
||||
modifier = Modifier.padding(12.dp),
|
||||
)
|
||||
}
|
||||
Tab(selected = selectedTab == 8, onClick = { selectedTab = 8 }) {
|
||||
Text("Bookmarks", modifier = Modifier.padding(12.dp))
|
||||
}
|
||||
Tab(selected = selectedTab == 9, onClick = { selectedTab = 9 }) {
|
||||
Text("Mutual", modifier = Modifier.padding(12.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1214,6 +1400,96 @@ fun UserProfileScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5 -> {
|
||||
if (followerList.isEmpty()) {
|
||||
item(key = "no-followers") { ProfileTabMessage("No followers found yet") }
|
||||
} else {
|
||||
items(followerList.toList(), key = { "follower-$it" }) { pk ->
|
||||
val user = remember(pk) { localCache.getOrCreateUser(pk) }
|
||||
UserSearchCard(user = user, onClick = { onNavigateToProfile(pk) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6 -> {
|
||||
if (followingList.isEmpty()) {
|
||||
item(key = "no-following") { ProfileTabMessage("No following found yet") }
|
||||
} else {
|
||||
items(followingList.toList(), key = { "following-$it" }) { pk ->
|
||||
val user = remember(pk) { localCache.getOrCreateUser(pk) }
|
||||
UserSearchCard(user = user, onClick = { onNavigateToProfile(pk) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7 -> {
|
||||
if (relayList.isEmpty()) {
|
||||
item(key = "no-relays") { ProfileTabMessage("No relay list published") }
|
||||
} else {
|
||||
items(relayList, key = { "relay-${it.first}" }) { (url, read, write) ->
|
||||
RelayRowCard(url = url, canRead = read, canWrite = write)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8 -> {
|
||||
if (bookmarkLoadedNotes.isEmpty()) {
|
||||
item(key = "no-bookmarks") { ProfileTabMessage("No public bookmarks") }
|
||||
} else {
|
||||
items(bookmarkLoadedNotes, key = { "bm-${it.idHex}" }) { note ->
|
||||
FeedNoteCard(
|
||||
note = note,
|
||||
relayManager = relayManager,
|
||||
localCache = localCache,
|
||||
account = account,
|
||||
myPubKeyHex = account?.pubKeyHex,
|
||||
nwcConnection = nwcConnection,
|
||||
onReply = onCompose,
|
||||
onZapFeedback = onZapFeedback,
|
||||
onNavigateToProfile = onNavigateToProfile,
|
||||
onNavigateToThread = onNavigateToThread,
|
||||
onImageClick = { urls, index -> lightboxState = LightboxState(urls, index) },
|
||||
onMediaClick = { urls, index, seekPos ->
|
||||
com.vitorpamplona.amethyst.desktop.service.media.GlobalMediaPlayer
|
||||
.playVideo(urls[index], seekPos)
|
||||
com.vitorpamplona.amethyst.desktop.service.media.GlobalMediaPlayer
|
||||
.toggleFullscreen()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9 -> {
|
||||
if (account == null) {
|
||||
item(key = "mutual-login") { ProfileTabMessage("Log in to see mutual posts") }
|
||||
} else if (mutualLoadedNotes.isEmpty()) {
|
||||
item(key = "no-mutual") { ProfileTabMessage("You haven't posted about this user") }
|
||||
} else {
|
||||
items(mutualLoadedNotes, key = { "mutual-${it.idHex}" }) { note ->
|
||||
FeedNoteCard(
|
||||
note = note,
|
||||
relayManager = relayManager,
|
||||
localCache = localCache,
|
||||
account = account,
|
||||
myPubKeyHex = account.pubKeyHex,
|
||||
nwcConnection = nwcConnection,
|
||||
onReply = onCompose,
|
||||
onZapFeedback = onZapFeedback,
|
||||
onNavigateToProfile = onNavigateToProfile,
|
||||
onNavigateToThread = onNavigateToThread,
|
||||
onImageClick = { urls, index -> lightboxState = LightboxState(urls, index) },
|
||||
onMediaClick = { urls, index, seekPos ->
|
||||
com.vitorpamplona.amethyst.desktop.service.media.GlobalMediaPlayer
|
||||
.playVideo(urls[index], seekPos)
|
||||
com.vitorpamplona.amethyst.desktop.service.media.GlobalMediaPlayer
|
||||
.toggleFullscreen()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1365,6 +1641,20 @@ private suspend fun unfollowUser(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileTabMessage(text: String) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(32.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PublishedHighlightCard(
|
||||
highlight: HighlightEvent,
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.desktop.ui.profile
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
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.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
|
||||
/**
|
||||
* A single relay row for the profile Relays tab: the relay URL plus a compact
|
||||
* read / write role label. Mouse-first, matches the surfaceVariant card style
|
||||
* used elsewhere on the profile screen.
|
||||
*/
|
||||
@Composable
|
||||
fun RelayRowCard(
|
||||
url: String,
|
||||
canRead: Boolean,
|
||||
canWrite: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val role =
|
||||
when {
|
||||
canRead && canWrite -> "read / write"
|
||||
canWrite -> "write"
|
||||
canRead -> "read"
|
||||
else -> ""
|
||||
}
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) {
|
||||
Icon(
|
||||
MaterialSymbols.Language,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.width(18.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
url.removePrefix("wss://").removePrefix("ws://").removeSuffix("/"),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
if (role.isNotEmpty()) {
|
||||
Text(
|
||||
role,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user