Improvements to profile page

This commit is contained in:
Vitor Pamplona
2026-03-09 10:44:33 -04:00
parent 961b8cf19e
commit 76598c304e
4 changed files with 194 additions and 143 deletions
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -92,15 +93,14 @@ fun DisplayLNAddress(
onClick = { zapExpanded = !zapExpanded }, onClick = { zapExpanded = !zapExpanded },
modifier = modifier =
Modifier Modifier
.padding(top = 1.dp, bottom = 1.dp, start = 5.dp) .padding(top = 1.dp, bottom = 1.dp, start = 5.dp),
.weight(1f),
) )
} }
if (zapExpanded) { if (zapExpanded) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 5.dp), modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp),
) { ) {
InvoiceRequestCard( InvoiceRequestCard(
lud16, lud16,
@@ -20,8 +20,12 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
import android.content.ClipData
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.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -35,12 +39,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -48,15 +54,18 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.appendLink
import com.vitorpamplona.amethyst.ui.navigation.navs.INav 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.DrawPlayName import com.vitorpamplona.amethyst.ui.note.DrawPlayName
import com.vitorpamplona.amethyst.ui.note.ObserveAndRenderNIP05VerifiedSymbol import com.vitorpamplona.amethyst.ui.note.ObserveAndRenderNIP05VerifiedSymbol
import com.vitorpamplona.amethyst.ui.note.toShortDisplay
import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.DisplayAppRecommendations import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.DisplayAppRecommendations
@@ -65,8 +74,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.badges.Displ
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.identity.UserExternalIdentitiesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.identity.UserExternalIdentitiesViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier import com.vitorpamplona.amethyst.ui.theme.SpacedBy3dp
import com.vitorpamplona.amethyst.ui.theme.Size25Modifier
import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
@@ -75,9 +83,11 @@ import com.vitorpamplona.quartz.nip39ExtIdentities.IdentityClaimTag
import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity
import com.vitorpamplona.quartz.nip39ExtIdentities.TelegramIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.TelegramIdentity
import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity
import kotlinx.coroutines.launch
private const val IDENTITY_ICON_CACHE_KEY = 0 private const val IDENTITY_ICON_CACHE_KEY = 0
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun DrawAdditionalInfo( fun DrawAdditionalInfo(
baseUser: User, baseUser: User,
@@ -89,147 +99,186 @@ fun DrawAdditionalInfo(
val userState by observeUserInfo(baseUser, accountViewModel) val userState by observeUserInfo(baseUser, accountViewModel)
val user = userState ?: return val user = userState ?: return
val uri = LocalUriHandler.current val uri = LocalUriHandler.current
val clipboardManager = LocalClipboardManager.current val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
val identities by externalIdentities.identities.collectAsStateWithLifecycle() val identities by externalIdentities.identities.collectAsStateWithLifecycle()
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 7.dp)) { val displayName = user.info.bestName()
CreateTextWithEmoji(
text = user.info.bestName() ?: baseUser.pubkeyDisplayHex(), Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = SpacedBy3dp) {
tags = user.tags, if (displayName != null) {
fontWeight = FontWeight.Bold, Row(
fontSize = 25.sp, verticalAlignment = Alignment.CenterVertically,
) modifier = Modifier.padding(top = 7.dp),
Spacer(StdHorzSpacer) ) {
user.info.pronouns?.let { CreateTextWithEmoji(
text = displayName,
tags = user.tags,
fontWeight = FontWeight.Bold,
fontSize = 22.sp,
)
Spacer(StdHorzSpacer)
user.info.pronouns?.let {
Text(
text = "($it)",
color = MaterialTheme.colorScheme.placeholderText,
fontSize = 14.sp,
)
Spacer(StdHorzSpacer)
}
DrawPlayName(displayName)
}
}
if (displayName != user.info.name && !user.info.name.isNullOrBlank()) {
Text( Text(
text = "($it)", color = MaterialTheme.colorScheme.placeholderText,
) text = "@" + user.info.name,
Spacer(StdHorzSpacer) maxLines = 1,
} overflow = TextOverflow.Ellipsis,
user.info.bestName()?.let {
DrawPlayName(it)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = baseUser.pubkeyDisplayHex(),
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp),
color = MaterialTheme.colorScheme.placeholderText,
)
IconButton(
modifier =
Modifier
.size(25.dp)
.padding(start = 5.dp),
onClick = { clipboardManager.setText(AnnotatedString(baseUser.pubkeyNpub())) },
) {
Icon(
imageVector = Icons.Default.ContentCopy,
contentDescription = stringRes(id = R.string.copy_npub_to_clipboard),
modifier = Size15Modifier,
tint = MaterialTheme.colorScheme.placeholderText,
) )
} }
IconButton(
modifier = Size25Modifier,
onClick = { nav.nav(Route.QRDisplay(baseUser.pubkeyHex)) },
) {
Icon(
painter = painterRes(R.drawable.ic_qrcode, 1),
contentDescription = stringRes(id = R.string.show_npub_as_a_qr_code),
modifier = Size15Modifier,
tint = MaterialTheme.colorScheme.placeholderText,
)
}
}
DisplayBadges(baseUser, accountViewModel, nav)
DisplayNip05ProfileStatus(baseUser, accountViewModel)
val website = user.info.website
if (!website.isNullOrEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Icon( Text(
tint = MaterialTheme.colorScheme.placeholderText, text = baseUser.pubkeyDisplayHex(),
imageVector = Icons.Default.Link, color = MaterialTheme.colorScheme.placeholderText,
contentDescription = stringRes(R.string.website), maxLines = 1,
modifier = Modifier.size(16.dp),
) )
ClickableTextPrimary( IconButton(
text = website.removePrefix("https://"), modifier = Modifier.size(23.dp).padding(start = 5.dp),
onClick = { onClick = {
website.let { scope.launch {
runCatching { clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("npub", baseUser.pubkeyNpub())))
if (it.contains("://")) {
uri.openUri(it)
} else {
uri.openUri("http://$it")
}
}
} }
}, },
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp), ) {
)
}
}
val lud16 =
remember(userState) {
userState?.info?.lud16?.trim()
?: userState?.info?.lud06?.trim()
}
DisplayLNAddress(lud16, baseUser, accountViewModel, nav)
val displayIdentities = identities.ifEmpty { user.identities }
if (displayIdentities.isNotEmpty()) {
displayIdentities.forEach { identity: IdentityClaimTag ->
Row(verticalAlignment = Alignment.CenterVertically) {
Icon( Icon(
tint = Color.Unspecified, imageVector = Icons.Default.ContentCopy,
painter = painterRes(resourceId = getIdentityClaimIcon(identity), IDENTITY_ICON_CACHE_KEY), contentDescription = stringRes(id = R.string.copy_npub_to_clipboard),
contentDescription = stringRes(getIdentityClaimDescription(identity)), modifier = Size15Modifier,
modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.placeholderText,
)
ClickableTextPrimary(
text = identity.identity,
onClick = { runCatching { uri.openUri(identity.toProofUrl()) } },
modifier =
Modifier
.padding(top = 1.dp, bottom = 1.dp, start = 5.dp)
.weight(1f),
) )
} }
} }
}
user.info.about?.let { Row(verticalAlignment = Alignment.CenterVertically) {
Row( Text(
modifier = Modifier.padding(top = 5.dp, bottom = 5.dp), text = baseUser.toNProfile().toShortDisplay(6),
) { color = MaterialTheme.colorScheme.placeholderText,
val defaultBackground = MaterialTheme.colorScheme.background maxLines = 1,
val background = remember { mutableStateOf(defaultBackground) }
TranslatableRichTextViewer(
content = it,
canPreview = false,
quotesLeft = 1,
tags = user.tags,
backgroundColor = background,
id = it,
accountViewModel = accountViewModel,
nav = nav,
) )
}
}
DisplayAppRecommendations(appRecommendations, accountViewModel, nav) IconButton(
modifier = Modifier.size(23.dp).padding(start = 5.dp),
onClick = {
scope.launch {
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("nprofile", baseUser.toNProfile())))
}
},
) {
Icon(
imageVector = Icons.Default.ContentCopy,
contentDescription = stringRes(id = R.string.copy_nprofile_to_clipboard),
modifier = Size15Modifier,
tint = MaterialTheme.colorScheme.placeholderText,
)
}
IconButton(
modifier = Modifier.size(23.dp),
onClick = { nav.nav(Route.QRDisplay(baseUser.pubkeyHex)) },
) {
Icon(
painter = painterRes(R.drawable.ic_qrcode, 1),
contentDescription = stringRes(id = R.string.show_nprofile_as_a_qr_code),
modifier = Size15Modifier,
tint = MaterialTheme.colorScheme.placeholderText,
)
}
}
DisplayNip05ProfileStatus(baseUser, accountViewModel)
val lud16 =
remember(userState) {
userState?.info?.lud16?.trim()
?: userState?.info?.lud06?.trim()
}
DisplayLNAddress(lud16, baseUser, accountViewModel, nav)
val website = user.info.website
if (!website.isNullOrEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
tint = MaterialTheme.colorScheme.placeholderText,
imageVector = Icons.Default.Link,
contentDescription = stringRes(R.string.website),
modifier = Modifier.size(18.dp),
)
ClickableTextPrimary(
text = website.removePrefix("https://").removePrefix("http://").removeSuffix("/"),
onClick = {
runCatching {
if (website.contains("://")) {
uri.openUri(website)
} else {
uri.openUri("http://$website")
}
}
},
modifier = Modifier.padding(vertical = 1.dp, horizontal = 5.dp),
)
}
}
val displayIdentities = identities.ifEmpty { user.identities }
if (displayIdentities.isNotEmpty()) {
displayIdentities.forEach { identity: IdentityClaimTag ->
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
tint = Color.Unspecified,
painter = painterRes(resourceId = getIdentityClaimIcon(identity), IDENTITY_ICON_CACHE_KEY),
contentDescription = stringRes(getIdentityClaimDescription(identity)),
modifier = Modifier.size(18.dp),
)
ClickableTextPrimary(
text = identity.identity,
onClick = { runCatching { uri.openUri(identity.toProofUrl()) } },
modifier = Modifier.padding(horizontal = 5.dp),
)
}
}
}
DisplayBadges(baseUser, accountViewModel, nav)
user.info.about?.let {
Row(
modifier = Modifier.padding(top = 10.dp, bottom = 5.dp),
) {
val defaultBackground = MaterialTheme.colorScheme.background
val background = remember { mutableStateOf(defaultBackground) }
TranslatableRichTextViewer(
content = it,
canPreview = false,
quotesLeft = 1,
tags = user.tags,
backgroundColor = background,
id = it,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
DisplayAppRecommendations(appRecommendations, accountViewModel, nav)
}
} }
@Composable @Composable
@@ -242,24 +291,22 @@ fun DisplayNip05ProfileStatus(
when (val nip05State = nip05StateMetadata) { when (val nip05State = nip05StateMetadata) {
is Nip05State.Exists -> { is Nip05State.Exists -> {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = SpacedBy5dp) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = SpacedBy5dp) {
ObserveAndRenderNIP05VerifiedSymbol(nip05State, 2, Size16Modifier, accountViewModel) ObserveAndRenderNIP05VerifiedSymbol(nip05State, 2, Size15Modifier, accountViewModel)
if (nip05State.nip05.name != "_") {
Text(
text = "${nip05State.nip05.name}@",
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
val uri = LocalUriHandler.current val uri = LocalUriHandler.current
val color = MaterialTheme.colorScheme.primary
ClickableTextPrimary( Text(
text = nip05State.nip05.domain, text =
onClick = { runCatching { uri.openUri("https://${nip05State.nip05.domain}") } }, remember(nip05State) {
buildAnnotatedString {
appendLink(nip05State.nip05.toValue(), color) {
runCatching { uri.openUri("https://${nip05State.nip05.domain}") }
}
}
},
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp), modifier = Modifier.padding(top = 1.dp, bottom = 1.dp),
softWrap = true,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
@@ -97,6 +97,7 @@ val HalfDoubleVertSpacer = Modifier.height(7.dp)
val Size0dp = 0.dp val Size0dp = 0.dp
val Size2dp = 2.dp val Size2dp = 2.dp
val Size3dp = 3.dp
val Size5dp = 5.dp val Size5dp = 5.dp
val Size6dp = 6.dp val Size6dp = 6.dp
val Size8dp = 8.dp val Size8dp = 8.dp
@@ -375,6 +376,7 @@ val PaddingHorizontal12Modifier = Modifier.padding(horizontal = 12.dp)
val QuickActionPopupShadow = Modifier.shadow(elevation = Size6dp, shape = SmallestBorder) val QuickActionPopupShadow = Modifier.shadow(elevation = Size6dp, shape = SmallestBorder)
val SpacedBy2dp = Arrangement.spacedBy(Size2dp) val SpacedBy2dp = Arrangement.spacedBy(Size2dp)
val SpacedBy3dp = Arrangement.spacedBy(Size3dp)
val SpacedBy5dp = Arrangement.spacedBy(Size5dp) val SpacedBy5dp = Arrangement.spacedBy(Size5dp)
val SpacedBy10dp = Arrangement.spacedBy(Size10dp) val SpacedBy10dp = Arrangement.spacedBy(Size10dp)
val SpacedBy55dp = Arrangement.spacedBy(Size55dp) val SpacedBy55dp = Arrangement.spacedBy(Size55dp)
+2
View File
@@ -1004,6 +1004,7 @@
<string name="copy_stack_to_clipboard">Copy Stack</string> <string name="copy_stack_to_clipboard">Copy Stack</string>
<string name="copy_to_clipboard">Copy to clipboard</string> <string name="copy_to_clipboard">Copy to clipboard</string>
<string name="copy_nprofile_to_clipboard">Copy nprofile to clipboard</string>
<string name="copy_npub_to_clipboard">Copy npub to clipboard</string> <string name="copy_npub_to_clipboard">Copy npub to clipboard</string>
<string name="share_or_save">Share or Save</string> <string name="share_or_save">Share or Save</string>
<string name="copy_url_to_clipboard">Copy URL to clipboard</string> <string name="copy_url_to_clipboard">Copy URL to clipboard</string>
@@ -1282,6 +1283,7 @@
<string name="add_content_warning">Add content warning</string> <string name="add_content_warning">Add content warning</string>
<string name="remove_content_warning">Remove content warning</string> <string name="remove_content_warning">Remove content warning</string>
<string name="show_npub_as_a_qr_code">Show npub as a QR code</string> <string name="show_npub_as_a_qr_code">Show npub as a QR code</string>
<string name="show_nprofile_as_a_qr_code">Show nprofile as a QR code</string>
<string name="invalid_nip19_uri">Invalid address</string> <string name="invalid_nip19_uri">Invalid address</string>
<string name="invalid_nip19_uri_description">Amethyst received a URI to open but that uri was invalid: %1$s</string> <string name="invalid_nip19_uri_description">Amethyst received a URI to open but that uri was invalid: %1$s</string>