diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt index 1cd2949e9..2dd013878 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -92,15 +93,14 @@ fun DisplayLNAddress( onClick = { zapExpanded = !zapExpanded }, modifier = Modifier - .padding(top = 1.dp, bottom = 1.dp, start = 5.dp) - .weight(1f), + .padding(top = 1.dp, bottom = 1.dp, start = 5.dp), ) } if (zapExpanded) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(vertical = 5.dp), + modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp), ) { InvoiceRequestCard( lud16, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt index 83aa8448d..202a59ebc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt @@ -20,8 +20,12 @@ */ 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.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons @@ -35,12 +39,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -48,15 +54,18 @@ import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R 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.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji 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.routes.Route import com.vitorpamplona.amethyst.ui.note.DrawPlayName 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.screen.loggedIn.AccountViewModel 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.stringRes import com.vitorpamplona.amethyst.ui.theme.Size15Modifier -import com.vitorpamplona.amethyst.ui.theme.Size16Modifier -import com.vitorpamplona.amethyst.ui.theme.Size25Modifier +import com.vitorpamplona.amethyst.ui.theme.SpacedBy3dp import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer 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.TelegramIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity +import kotlinx.coroutines.launch private const val IDENTITY_ICON_CACHE_KEY = 0 +@OptIn(ExperimentalLayoutApi::class) @Composable fun DrawAdditionalInfo( baseUser: User, @@ -89,147 +99,186 @@ fun DrawAdditionalInfo( val userState by observeUserInfo(baseUser, accountViewModel) val user = userState ?: return val uri = LocalUriHandler.current - val clipboardManager = LocalClipboardManager.current + val clipboard = LocalClipboard.current + val scope = rememberCoroutineScope() val identities by externalIdentities.identities.collectAsStateWithLifecycle() - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 7.dp)) { - CreateTextWithEmoji( - text = user.info.bestName() ?: baseUser.pubkeyDisplayHex(), - tags = user.tags, - fontWeight = FontWeight.Bold, - fontSize = 25.sp, - ) - Spacer(StdHorzSpacer) - user.info.pronouns?.let { + val displayName = user.info.bestName() + + Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = SpacedBy3dp) { + if (displayName != null) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 7.dp), + ) { + 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 = "($it)", - ) - Spacer(StdHorzSpacer) - } - - 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, + color = MaterialTheme.colorScheme.placeholderText, + text = "@" + user.info.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } - 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) { - Icon( - tint = MaterialTheme.colorScheme.placeholderText, - imageVector = Icons.Default.Link, - contentDescription = stringRes(R.string.website), - modifier = Modifier.size(16.dp), + Text( + text = baseUser.pubkeyDisplayHex(), + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, ) - ClickableTextPrimary( - text = website.removePrefix("https://"), + IconButton( + modifier = Modifier.size(23.dp).padding(start = 5.dp), onClick = { - website.let { - runCatching { - if (it.contains("://")) { - uri.openUri(it) - } else { - uri.openUri("http://$it") - } - } + scope.launch { + clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("npub", baseUser.pubkeyNpub()))) } }, - 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( - tint = Color.Unspecified, - painter = painterRes(resourceId = getIdentityClaimIcon(identity), IDENTITY_ICON_CACHE_KEY), - contentDescription = stringRes(getIdentityClaimDescription(identity)), - modifier = Modifier.size(16.dp), - ) - - ClickableTextPrimary( - text = identity.identity, - onClick = { runCatching { uri.openUri(identity.toProofUrl()) } }, - modifier = - Modifier - .padding(top = 1.dp, bottom = 1.dp, start = 5.dp) - .weight(1f), + imageVector = Icons.Default.ContentCopy, + contentDescription = stringRes(id = R.string.copy_npub_to_clipboard), + modifier = Size15Modifier, + tint = MaterialTheme.colorScheme.placeholderText, ) } } - } - user.info.about?.let { - Row( - modifier = Modifier.padding(top = 5.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, + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = baseUser.toNProfile().toShortDisplay(6), + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, ) - } - } - 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 @@ -242,24 +291,22 @@ fun DisplayNip05ProfileStatus( when (val nip05State = nip05StateMetadata) { is Nip05State.Exists -> { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = SpacedBy5dp) { - ObserveAndRenderNIP05VerifiedSymbol(nip05State, 2, Size16Modifier, 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, - ) - } + ObserveAndRenderNIP05VerifiedSymbol(nip05State, 2, Size15Modifier, accountViewModel) val uri = LocalUriHandler.current + val color = MaterialTheme.colorScheme.primary - ClickableTextPrimary( - text = nip05State.nip05.domain, - onClick = { runCatching { uri.openUri("https://${nip05State.nip05.domain}") } }, + Text( + text = + remember(nip05State) { + buildAnnotatedString { + appendLink(nip05State.nip05.toValue(), color) { + runCatching { uri.openUri("https://${nip05State.nip05.domain}") } + } + } + }, modifier = Modifier.padding(top = 1.dp, bottom = 1.dp), + softWrap = true, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 85029f271..f51773ef6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -97,6 +97,7 @@ val HalfDoubleVertSpacer = Modifier.height(7.dp) val Size0dp = 0.dp val Size2dp = 2.dp +val Size3dp = 3.dp val Size5dp = 5.dp val Size6dp = 6.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 SpacedBy2dp = Arrangement.spacedBy(Size2dp) +val SpacedBy3dp = Arrangement.spacedBy(Size3dp) val SpacedBy5dp = Arrangement.spacedBy(Size5dp) val SpacedBy10dp = Arrangement.spacedBy(Size10dp) val SpacedBy55dp = Arrangement.spacedBy(Size55dp) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index c403c002a..5aee22a37 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1004,6 +1004,7 @@ Copy Stack Copy to clipboard + Copy nprofile to clipboard Copy npub to clipboard Share or Save Copy URL to clipboard @@ -1282,6 +1283,7 @@ Add content warning Remove content warning Show npub as a QR code + Show nprofile as a QR code Invalid address Amethyst received a URI to open but that uri was invalid: %1$s