mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 15:44:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11e9046199 |
+35
@@ -20,6 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -32,14 +34,47 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.LeftHalfCircleButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun FollowButtonPreview() {
|
||||
ThemeComparisonRow {
|
||||
Row(horizontalArrangement = Arrangement.Center) {
|
||||
FollowButton {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun UnfollowButtonPreview() {
|
||||
ThemeComparisonRow {
|
||||
Row(horizontalArrangement = Arrangement.Center) {
|
||||
UnfollowButton {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun FollowWithListButtonPreview() {
|
||||
ThemeComparisonRow {
|
||||
Row(horizontalArrangement = Arrangement.Center) {
|
||||
FollowButton(isInProfileActions = true) {}
|
||||
ListButton {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FollowButton(
|
||||
text: Int = R.string.follow,
|
||||
|
||||
+17
-7
@@ -70,10 +70,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal.UserP
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.TabNotesConversations
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.dal.UserProfileConversationsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource.UserProfileFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.FollowersTabHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.TabFollowers
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal.UserProfileFollowersUserFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.FollowTabHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.TabFollows
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal.UserProfileFollowsUserFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.TabGallery
|
||||
@@ -93,7 +91,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.ReportsTabH
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.TabReports
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal.UserProfileReportFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.TabReceivedZaps
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ZapTabHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
@@ -391,9 +388,22 @@ private fun RenderScreen(
|
||||
nav: INav,
|
||||
) {
|
||||
val pagerState = rememberPagerState { 11 }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Column {
|
||||
ProfileHeader(baseUser, appRecommendations, externalIdentities, nav, accountViewModel)
|
||||
ProfileHeader(
|
||||
baseUser,
|
||||
appRecommendations,
|
||||
externalIdentities,
|
||||
followsFeedViewModel,
|
||||
followersFeedViewModel,
|
||||
zapFeedViewModel,
|
||||
nav,
|
||||
accountViewModel,
|
||||
onFollowingClick = { coroutineScope.launch { pagerState.animateScrollToPage(4) } },
|
||||
onFollowersClick = { coroutineScope.launch { pagerState.animateScrollToPage(5) } },
|
||||
onZapsClick = { coroutineScope.launch { pagerState.animateScrollToPage(6) } },
|
||||
)
|
||||
ScrollableTabRow(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
@@ -516,9 +526,9 @@ private fun CreateAndRenderTabs(
|
||||
{ Text(text = stringRes(R.string.replies)) },
|
||||
{ Text(text = stringRes(R.string.mutual)) },
|
||||
{ Text(text = stringRes(R.string.gallery)) },
|
||||
{ FollowTabHeader(followsFeedViewModel, accountViewModel) },
|
||||
{ FollowersTabHeader(baseUser, followersFeedViewModel, accountViewModel) },
|
||||
{ ZapTabHeader(zapFeedViewModel, accountViewModel) },
|
||||
{ Text(text = stringRes(R.string.following).trim()) },
|
||||
{ Text(text = stringRes(R.string.followers).trim()) },
|
||||
{ Text(text = stringRes(R.string.zaps)) },
|
||||
{ BookmarkTabHeader(baseUser, accountViewModel) },
|
||||
{ FollowedTagsTabHeader(baseUser, accountViewModel) },
|
||||
{ ReportsTabHeader(baseUser, accountViewModel) },
|
||||
|
||||
+172
-82
@@ -21,6 +21,11 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -30,6 +35,8 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -40,6 +47,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -49,6 +57,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -77,7 +86,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.identity.Use
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
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
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -112,17 +120,21 @@ fun DrawAdditionalInfo(
|
||||
|
||||
val displayName = user.info.bestName()
|
||||
|
||||
var showTechnicalDetails by remember { mutableStateOf(false) }
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = SpacedBy3dp) {
|
||||
// Display name - centered, large
|
||||
if (displayName != null) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(top = 7.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||
) {
|
||||
CreateTextWithEmoji(
|
||||
text = displayName,
|
||||
tags = user.tags,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 22.sp,
|
||||
fontSize = 24.sp,
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
user.info.pronouns?.let {
|
||||
@@ -133,84 +145,26 @@ fun DrawAdditionalInfo(
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
}
|
||||
|
||||
DrawPlayName(displayName)
|
||||
}
|
||||
}
|
||||
|
||||
// Username (if different from display name) - centered
|
||||
if (displayName != user.info.name && !user.info.name.isNullOrBlank()) {
|
||||
Text(
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
text = "@" + user.info.name,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = baseUser.pubkeyDisplayHex(),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.size(23.dp).padding(start = 5.dp),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("npub", baseUser.pubkeyNpub())))
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ContentCopy,
|
||||
contentDescription = stringRes(id = R.string.copy_npub_to_clipboard),
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = baseUser.toNProfile().toShortDisplay(6),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DisplayLastSeen(baseUser, accountViewModel)
|
||||
|
||||
// NIP-05 verification - centered
|
||||
DisplayNip05ProfileStatus(baseUser, accountViewModel)
|
||||
|
||||
// Lightning address
|
||||
val lud16 =
|
||||
remember(userState) {
|
||||
userState?.info?.lud16?.trim()
|
||||
@@ -218,9 +172,14 @@ fun DrawAdditionalInfo(
|
||||
}
|
||||
DisplayLNAddress(lud16, baseUser, accountViewModel, nav)
|
||||
|
||||
// Website
|
||||
val website = user.info.website
|
||||
if (!website.isNullOrEmpty()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Icon(
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
imageVector = Icons.Default.Link,
|
||||
@@ -244,28 +203,39 @@ fun DrawAdditionalInfo(
|
||||
}
|
||||
}
|
||||
|
||||
// External identities
|
||||
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),
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 2.dp),
|
||||
) {
|
||||
displayIdentities.forEach { identity: IdentityClaimTag ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 6.dp),
|
||||
) {
|
||||
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),
|
||||
)
|
||||
ClickableTextPrimary(
|
||||
text = identity.identity,
|
||||
onClick = { runCatching { uri.openUri(identity.toProofUrl()) } },
|
||||
modifier = Modifier.padding(horizontal = 5.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Badges
|
||||
DisplayBadges(baseUser, accountViewModel, nav)
|
||||
|
||||
// Bio/About section
|
||||
user.info.about?.let {
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 10.dp, bottom = 5.dp),
|
||||
@@ -286,6 +256,119 @@ fun DrawAdditionalInfo(
|
||||
}
|
||||
}
|
||||
|
||||
// Collapsible technical details (npub, nprofile, last seen)
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { showTechnicalDetails = !showTechnicalDetails }
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = baseUser.pubkeyDisplayHex(),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
Icon(
|
||||
imageVector =
|
||||
if (showTechnicalDetails) {
|
||||
Icons.Default.ExpandLess
|
||||
} else {
|
||||
Icons.Default.ExpandMore
|
||||
},
|
||||
contentDescription = stringRes(R.string.show_more),
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = showTechnicalDetails,
|
||||
enter = expandVertically(),
|
||||
exit = shrinkVertically(),
|
||||
) {
|
||||
Column(verticalArrangement = SpacedBy3dp) {
|
||||
// npub
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = baseUser.pubkeyDisplayHex(),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.size(23.dp).padding(start = 5.dp),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
clipboard.setClipEntry(ClipEntry(ClipData.newPlainText("npub", baseUser.pubkeyNpub())))
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ContentCopy,
|
||||
contentDescription = stringRes(id = R.string.copy_npub_to_clipboard),
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// nprofile
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = baseUser.toNProfile().toShortDisplay(6),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Last seen
|
||||
DisplayLastSeen(baseUser, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
// App recommendations
|
||||
DisplayAppRecommendations(appRecommendations, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
@@ -313,6 +396,9 @@ fun DisplayLastSeen(
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -326,7 +412,11 @@ fun DisplayNip05ProfileStatus(
|
||||
|
||||
when (val nip05State = nip05StateMetadata) {
|
||||
is Nip05State.Exists -> {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = SpacedBy5dp) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ObserveAndRenderNIP05VerifiedSymbol(nip05State, 2, Size15Modifier, accountViewModel)
|
||||
|
||||
val uri = LocalUriHandler.current
|
||||
@@ -349,7 +439,7 @@ fun DisplayNip05ProfileStatus(
|
||||
}
|
||||
}
|
||||
|
||||
else -> { }
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+65
-18
@@ -22,15 +22,22 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
@@ -62,25 +69,53 @@ fun DrawBanner(
|
||||
banner: String?,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val backgroundColor = MaterialTheme.colorScheme.background
|
||||
val gradientBrush =
|
||||
remember(backgroundColor) {
|
||||
Brush.verticalGradient(
|
||||
colors =
|
||||
listOf(
|
||||
Color.Transparent,
|
||||
Color.Transparent,
|
||||
backgroundColor.copy(alpha = 0.5f),
|
||||
backgroundColor,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (!banner.isNullOrBlank()) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
var zoomImageDialogOpen by remember { mutableStateOf(false) }
|
||||
|
||||
AsyncImage(
|
||||
model = banner,
|
||||
contentDescription = stringRes(id = R.string.profile_image),
|
||||
contentScale = ContentScale.Crop,
|
||||
placeholder = painterRes(R.drawable.profile_banner, 1),
|
||||
error = painterRes(R.drawable.profile_banner, 1),
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(150.dp)
|
||||
.combinedClickable(
|
||||
onClick = { zoomImageDialogOpen = true },
|
||||
onLongClick = { clipboardManager.setText(AnnotatedString(banner)) },
|
||||
),
|
||||
)
|
||||
.height(200.dp),
|
||||
) {
|
||||
AsyncImage(
|
||||
model = banner,
|
||||
contentDescription = stringRes(id = R.string.profile_image),
|
||||
contentScale = ContentScale.Crop,
|
||||
placeholder = painterRes(R.drawable.profile_banner, 1),
|
||||
error = painterRes(R.drawable.profile_banner, 1),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.combinedClickable(
|
||||
onClick = { zoomImageDialogOpen = true },
|
||||
onLongClick = { clipboardManager.setText(AnnotatedString(banner)) },
|
||||
),
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(gradientBrush)
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
|
||||
if (zoomImageDialogOpen) {
|
||||
ZoomableImageDialog(
|
||||
@@ -90,14 +125,26 @@ fun DrawBanner(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Image(
|
||||
painter = painterRes(R.drawable.profile_banner, 2),
|
||||
contentDescription = stringRes(id = R.string.profile_banner),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(150.dp),
|
||||
)
|
||||
.height(200.dp),
|
||||
) {
|
||||
Image(
|
||||
painter = painterRes(R.drawable.profile_banner, 2),
|
||||
contentDescription = stringRes(id = R.string.profile_banner),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(gradientBrush)
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -22,10 +22,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Mail
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@@ -35,9 +38,36 @@ import com.vitorpamplona.amethyst.ui.painterRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun MessageButtonPreview() {
|
||||
ThemeComparisonRow {
|
||||
InnerMessageButton {}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InnerMessageButton(onClick: () -> Unit) {
|
||||
FilledTonalButton(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 3.dp)
|
||||
.width(50.dp),
|
||||
onClick = onClick,
|
||||
contentPadding = ZeroPadding,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Mail,
|
||||
contentDescription = "Message",
|
||||
modifier = Size20Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageButton(
|
||||
user: User,
|
||||
|
||||
+128
-26
@@ -21,7 +21,14 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
|
||||
|
||||
import android.content.ClipData
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -51,6 +58,10 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -68,15 +79,15 @@ import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal.UserProfileFollowersUserFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal.UserProfileFollowsUserFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.identity.UserExternalIdentitiesViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size100dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.userProfileBorderModifier
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@@ -84,14 +95,21 @@ fun ProfileHeader(
|
||||
baseUser: User,
|
||||
appRecommendations: UserAppRecommendationsFeedViewModel,
|
||||
externalIdentities: UserExternalIdentitiesViewModel,
|
||||
followsFeedViewModel: UserProfileFollowsUserFeedViewModel,
|
||||
followersFeedViewModel: UserProfileFollowersUserFeedViewModel,
|
||||
zapFeedViewModel: UserProfileZapsViewModel,
|
||||
nav: INav,
|
||||
accountViewModel: AccountViewModel,
|
||||
onFollowingClick: () -> Unit,
|
||||
onFollowersClick: () -> Unit,
|
||||
onZapsClick: () -> Unit,
|
||||
) {
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box {
|
||||
DrawBanner(baseUser, accountViewModel)
|
||||
|
||||
// More options button in top-right
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
@@ -109,7 +127,7 @@ fun ProfileHeader(
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
containerColor = MaterialTheme.colorScheme.background.copy(alpha = 0.7f),
|
||||
),
|
||||
contentPadding = ZeroPadding,
|
||||
) {
|
||||
@@ -128,41 +146,125 @@ fun ProfileHeader(
|
||||
}
|
||||
}
|
||||
|
||||
// Main content overlapping the banner
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp)
|
||||
.padding(top = 100.dp),
|
||||
.padding(top = 140.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
ProfilePictureWithUploadOverlay(
|
||||
baseUser = baseUser,
|
||||
accountViewModel = accountViewModel,
|
||||
size = Size100dp,
|
||||
)
|
||||
// Avatar with animated ring - overlaps banner
|
||||
ProfilePictureWithAnimatedRing(
|
||||
baseUser = baseUser,
|
||||
accountViewModel = accountViewModel,
|
||||
size = 110.dp,
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.height(Size35dp)
|
||||
.padding(bottom = 3.dp),
|
||||
) {
|
||||
ProfileActions(baseUser, accountViewModel, nav)
|
||||
}
|
||||
// Action buttons row right below avatar
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = 8.dp)
|
||||
.height(35.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ProfileActions(baseUser, accountViewModel, nav)
|
||||
}
|
||||
|
||||
DrawAdditionalInfo(baseUser, appRecommendations, externalIdentities, accountViewModel, nav)
|
||||
// Stats row: Following | Followers | Zaps
|
||||
ProfileStatsRow(
|
||||
baseUser = baseUser,
|
||||
followsFeedViewModel = followsFeedViewModel,
|
||||
followersFeedViewModel = followersFeedViewModel,
|
||||
zapFeedViewModel = zapFeedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
onFollowingClick = onFollowingClick,
|
||||
onFollowersClick = onFollowersClick,
|
||||
onZapsClick = onZapsClick,
|
||||
)
|
||||
|
||||
// Additional info (name, bio, etc.)
|
||||
Column(modifier = Modifier.padding(horizontal = 10.dp)) {
|
||||
DrawAdditionalInfo(baseUser, appRecommendations, externalIdentities, accountViewModel, nav)
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(top = 6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfilePictureWithAnimatedRing(
|
||||
baseUser: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
size: Dp,
|
||||
) {
|
||||
val isMe by remember(accountViewModel) { derivedStateOf { accountViewModel.userProfile() == baseUser } }
|
||||
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
val secondaryColor = MaterialTheme.colorScheme.tertiary
|
||||
val backgroundColor = MaterialTheme.colorScheme.background
|
||||
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "profile_ring")
|
||||
val rotation by infiniteTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 360f,
|
||||
animationSpec =
|
||||
infiniteRepeatable(
|
||||
animation = tween(8000, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart,
|
||||
),
|
||||
label = "ring_rotation",
|
||||
)
|
||||
|
||||
val ringBrush =
|
||||
remember(primaryColor, secondaryColor) {
|
||||
Brush.sweepGradient(
|
||||
listOf(
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
primaryColor.copy(alpha = 0.3f),
|
||||
secondaryColor,
|
||||
primaryColor,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(size + 8.dp)
|
||||
.drawBehind {
|
||||
rotate(rotation) {
|
||||
drawCircle(
|
||||
brush = ringBrush,
|
||||
radius = (size.toPx() + 8.dp.toPx()) / 2f,
|
||||
style = Stroke(width = 3.dp.toPx()),
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
// White/background ring to separate avatar from animated ring
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(size + 4.dp)
|
||||
.clip(CircleShape)
|
||||
.background(backgroundColor),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
ProfilePictureWithUploadOverlay(
|
||||
baseUser = baseUser,
|
||||
accountViewModel = accountViewModel,
|
||||
size = size,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZoomableUserPicture(
|
||||
baseUser: User,
|
||||
@@ -178,7 +280,7 @@ fun ZoomableUserPicture(
|
||||
baseUser = baseUser,
|
||||
accountViewModel = accountViewModel,
|
||||
size = size,
|
||||
modifier = MaterialTheme.colorScheme.userProfileBorderModifier,
|
||||
modifier = Modifier.border(0.dp, MaterialTheme.colorScheme.background, CircleShape),
|
||||
onClick = {
|
||||
val pic = baseUser.profilePicture()
|
||||
if (pic != null) {
|
||||
|
||||
+827
@@ -0,0 +1,827 @@
|
||||
/*
|
||||
* 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.ui.screen.loggedIn.profile.header
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.List
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.EditNote
|
||||
import androidx.compose.material.icons.filled.ExpandLess
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material.icons.filled.Mail
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Verified
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
// ─── Full Header ───────────────────────────────────────────────────────────────
|
||||
|
||||
@Preview(showBackground = true, widthDp = 400, name = "Full Profile Header")
|
||||
@Composable
|
||||
fun ProfileHeaderRedesignPreview() {
|
||||
ThemeComparisonColumn {
|
||||
ProfileHeaderMockup()
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Stats Row ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@Preview(showBackground = true, widthDp = 400, name = "Stats Row")
|
||||
@Composable
|
||||
fun ProfileStatsRowPreview() {
|
||||
ThemeComparisonColumn {
|
||||
StatsRowMockup()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 400, name = "Stats Row - Loading")
|
||||
@Composable
|
||||
fun ProfileStatsRowLoadingPreview() {
|
||||
ThemeComparisonColumn {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
StatItemMockup(value = "--", label = "Following")
|
||||
StatItemMockup(value = "--", label = "Followers")
|
||||
StatItemMockup(value = "--", label = "Zaps")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Animated Ring Avatar ──────────────────────────────────────────────────────
|
||||
|
||||
@Preview(showBackground = true, widthDp = 200, name = "Animated Ring Avatar")
|
||||
@Composable
|
||||
fun ProfileAvatarRingPreview() {
|
||||
ThemeComparisonColumn {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
AvatarWithRingMockup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Banner with Gradient ──────────────────────────────────────────────────────
|
||||
|
||||
@Preview(showBackground = true, widthDp = 400, name = "Banner with Gradient")
|
||||
@Composable
|
||||
fun BannerWithGradientPreview() {
|
||||
ThemeComparisonColumn {
|
||||
BannerWithGradientMockup()
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Action Buttons ────────────────────────────────────────────────────────────
|
||||
|
||||
@Preview(showBackground = true, widthDp = 400, name = "Action Buttons - Other User")
|
||||
@Composable
|
||||
fun ProfileActionButtonsOtherUserPreview() {
|
||||
ThemeComparisonColumn {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(12.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ActionButtonsMockup(isOwnProfile = false, isFollowing = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 400, name = "Action Buttons - Following")
|
||||
@Composable
|
||||
fun ProfileActionButtonsFollowingPreview() {
|
||||
ThemeComparisonColumn {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(12.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ActionButtonsMockup(isOwnProfile = false, isFollowing = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 400, name = "Action Buttons - Own Profile")
|
||||
@Composable
|
||||
fun ProfileActionButtonsOwnPreview() {
|
||||
ThemeComparisonColumn {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(12.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ActionButtonsMockup(isOwnProfile = true, isFollowing = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── User Info Section ─────────────────────────────────────────────────────────
|
||||
|
||||
@Preview(showBackground = true, widthDp = 400, name = "User Info - Full")
|
||||
@Composable
|
||||
fun UserInfoFullPreview() {
|
||||
ThemeComparisonColumn {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
UserInfoMockup(
|
||||
displayName = "Vitor Pamplona",
|
||||
username = "@vitor",
|
||||
nip05 = "_@vitorpamplona.com",
|
||||
bio = "Building decentralized social networks with Nostr. Creator of Amethyst. Privacy advocate.",
|
||||
website = "vitorpamplona.com",
|
||||
lnAddress = "vitor@getalby.com",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 400, name = "User Info - Minimal")
|
||||
@Composable
|
||||
fun UserInfoMinimalPreview() {
|
||||
ThemeComparisonColumn {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
UserInfoMockup(
|
||||
displayName = "anon",
|
||||
username = null,
|
||||
nip05 = null,
|
||||
bio = null,
|
||||
website = null,
|
||||
lnAddress = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Collapsible Technical Details ─────────────────────────────────────────────
|
||||
|
||||
@Preview(showBackground = true, widthDp = 400, name = "Technical Details - Collapsed")
|
||||
@Composable
|
||||
fun TechnicalDetailsCollapsedPreview() {
|
||||
ThemeComparisonColumn {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
TechnicalDetailsMockup(expanded = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 400, name = "Technical Details - Expanded")
|
||||
@Composable
|
||||
fun TechnicalDetailsExpandedPreview() {
|
||||
ThemeComparisonColumn {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
TechnicalDetailsMockup(expanded = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── External Identities Row ───────────────────────────────────────────────────
|
||||
|
||||
@Preview(showBackground = true, widthDp = 400, name = "External Identities")
|
||||
@Composable
|
||||
fun ExternalIdentitiesPreview() {
|
||||
ThemeComparisonColumn {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
ExternalIdentitiesMockup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── NIP-05 Verification ───────────────────────────────────────────────────────
|
||||
|
||||
@Preview(showBackground = true, widthDp = 400, name = "NIP-05 Verified")
|
||||
@Composable
|
||||
fun Nip05VerifiedPreview() {
|
||||
ThemeComparisonColumn {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Nip05MockupRow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Lightning Address ─────────────────────────────────────────────────────────
|
||||
|
||||
@Preview(showBackground = true, widthDp = 400, name = "Lightning Address")
|
||||
@Composable
|
||||
fun LightningAddressPreview() {
|
||||
ThemeComparisonColumn {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
LightningAddressMockup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// Mockup implementations
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@Composable
|
||||
private fun ProfileHeaderMockup() {
|
||||
val backgroundColor = MaterialTheme.colorScheme.background
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
val secondaryColor = MaterialTheme.colorScheme.tertiary
|
||||
|
||||
Surface(color = backgroundColor) {
|
||||
Box {
|
||||
BannerWithGradientMockup()
|
||||
|
||||
// More options button
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
|
||||
.size(30.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
) {
|
||||
Icon(
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "More",
|
||||
)
|
||||
}
|
||||
|
||||
// Main content
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 140.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
AvatarWithRingMockup()
|
||||
|
||||
// Action buttons
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 8.dp).height(35.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
ActionButtonsMockup(isOwnProfile = false, isFollowing = false)
|
||||
}
|
||||
|
||||
StatsRowMockup()
|
||||
|
||||
Column(modifier = Modifier.padding(horizontal = 10.dp)) {
|
||||
UserInfoMockup(
|
||||
displayName = "Vitor Pamplona",
|
||||
username = "@vitor",
|
||||
nip05 = "_@vitorpamplona.com",
|
||||
bio =
|
||||
"Building decentralized social networks with Nostr. " +
|
||||
"Creator of Amethyst. Privacy advocate.",
|
||||
website = "vitorpamplona.com",
|
||||
lnAddress = "vitor@getalby.com",
|
||||
)
|
||||
|
||||
TechnicalDetailsMockup(expanded = false)
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(top = 6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BannerWithGradientMockup() {
|
||||
val backgroundColor = MaterialTheme.colorScheme.background
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
val secondaryColor = MaterialTheme.colorScheme.tertiary
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp)
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
listOf(
|
||||
primaryColor.copy(alpha = 0.6f),
|
||||
secondaryColor.copy(alpha = 0.4f),
|
||||
),
|
||||
),
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(80.dp)
|
||||
.align(Alignment.BottomCenter)
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
listOf(Color.Transparent, backgroundColor),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatsRowMockup() {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
StatItemMockup(value = "847", label = "Following")
|
||||
StatItemMockup(value = "12.3k", label = "Followers")
|
||||
StatItemMockup(value = "2.1M", label = "Zaps")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatItemMockup(
|
||||
value: String,
|
||||
label: String,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = value,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AvatarWithRingMockup() {
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
val secondaryColor = MaterialTheme.colorScheme.tertiary
|
||||
val backgroundColor = MaterialTheme.colorScheme.background
|
||||
|
||||
val size = 110.dp
|
||||
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "preview_ring")
|
||||
val rotation by infiniteTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 360f,
|
||||
animationSpec =
|
||||
infiniteRepeatable(
|
||||
animation = tween(8000, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart,
|
||||
),
|
||||
label = "preview_ring_rotation",
|
||||
)
|
||||
|
||||
val ringBrush =
|
||||
Brush.sweepGradient(
|
||||
listOf(
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
primaryColor.copy(alpha = 0.3f),
|
||||
secondaryColor,
|
||||
primaryColor,
|
||||
),
|
||||
)
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.size(size + 8.dp)
|
||||
.drawBehind {
|
||||
rotate(rotation) {
|
||||
drawCircle(
|
||||
brush = ringBrush,
|
||||
radius = (size.toPx() + 8.dp.toPx()) / 2f,
|
||||
style = Stroke(width = 3.dp.toPx()),
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(size + 4.dp)
|
||||
.clip(CircleShape)
|
||||
.background(backgroundColor),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(
|
||||
primaryColor.copy(alpha = 0.5f),
|
||||
secondaryColor.copy(alpha = 0.3f),
|
||||
),
|
||||
),
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "VP",
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 36.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionButtonsMockup(
|
||||
isOwnProfile: Boolean,
|
||||
isFollowing: Boolean,
|
||||
) {
|
||||
// Message button
|
||||
FilledTonalButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(horizontal = 3.dp).width(50.dp),
|
||||
contentPadding =
|
||||
androidx.compose.foundation.layout
|
||||
.PaddingValues(0.dp),
|
||||
) {
|
||||
Icon(Icons.Default.Mail, contentDescription = "Message", modifier = Modifier.size(20.dp))
|
||||
}
|
||||
|
||||
if (isOwnProfile) {
|
||||
// Edit button
|
||||
FilledTonalButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(horizontal = 3.dp).width(50.dp),
|
||||
contentPadding =
|
||||
androidx.compose.foundation.layout
|
||||
.PaddingValues(0.dp),
|
||||
) {
|
||||
Icon(Icons.Default.EditNote, contentDescription = "Edit")
|
||||
}
|
||||
} else {
|
||||
if (isFollowing) {
|
||||
// Unfollow button
|
||||
FilledTonalButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
) {
|
||||
Text("Unfollow")
|
||||
}
|
||||
} else {
|
||||
// Follow button
|
||||
FilledTonalButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
) {
|
||||
Text("Follow")
|
||||
}
|
||||
}
|
||||
|
||||
// List button
|
||||
TextButton(
|
||||
onClick = {},
|
||||
contentPadding =
|
||||
androidx.compose.foundation.layout
|
||||
.PaddingValues(0.dp),
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.List, contentDescription = "Lists")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserInfoMockup(
|
||||
displayName: String,
|
||||
username: String?,
|
||||
nip05: String?,
|
||||
bio: String?,
|
||||
website: String?,
|
||||
lnAddress: String?,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp),
|
||||
) {
|
||||
// Display name
|
||||
Text(
|
||||
text = displayName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 24.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
// Username
|
||||
if (username != null) {
|
||||
Text(
|
||||
text = username,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
// NIP-05
|
||||
if (nip05 != null) {
|
||||
Nip05MockupRow()
|
||||
}
|
||||
|
||||
// Lightning address
|
||||
if (lnAddress != null) {
|
||||
LightningAddressMockup()
|
||||
}
|
||||
|
||||
// Website
|
||||
if (website != null) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Icon(
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
imageVector = Icons.Default.Link,
|
||||
contentDescription = "Website",
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
Text(
|
||||
text = website,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 5.dp),
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Bio
|
||||
if (bio != null) {
|
||||
Text(
|
||||
text = bio,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 10.dp, bottom = 5.dp),
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TechnicalDetailsMockup(expanded: Boolean) {
|
||||
var showDetails by remember { mutableStateOf(expanded) }
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
// Collapsed trigger
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { showDetails = !showDetails }
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "npub1gc...8lxl",
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
maxLines = 1,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
Icon(
|
||||
imageVector = if (showDetails) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
|
||||
contentDescription = "Toggle details",
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
|
||||
// Expandable details
|
||||
AnimatedVisibility(
|
||||
visible = showDetails,
|
||||
enter = expandVertically(),
|
||||
exit = shrinkVertically(),
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
// npub row
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = "npub1gc...8lxl",
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.ContentCopy,
|
||||
contentDescription = "Copy npub",
|
||||
modifier = Modifier.padding(start = 5.dp).size(15.dp),
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
|
||||
// nprofile row
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = "nprofi...q8e2p",
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
fontSize = 12.sp,
|
||||
)
|
||||
Icon(
|
||||
imageVector = Icons.Default.ContentCopy,
|
||||
contentDescription = "Copy nprofile",
|
||||
modifier = Modifier.padding(start = 5.dp).size(15.dp),
|
||||
tint = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
}
|
||||
|
||||
// Last seen
|
||||
Text(
|
||||
text = "Last seen 2 hours ago",
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
fontSize = 12.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExternalIdentitiesMockup() {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||
) {
|
||||
// Twitter/X
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 6.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(18.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.onBackground),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(text = "X", color = MaterialTheme.colorScheme.background, fontSize = 10.sp, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
Text(
|
||||
text = "@vitorpamplona",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 5.dp),
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
|
||||
// GitHub
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 6.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.size(18.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.onBackground),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(text = "G", color = MaterialTheme.colorScheme.background, fontSize = 10.sp, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
Text(
|
||||
text = "vitorpamplona",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 5.dp),
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Nip05MockupRow() {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Verified,
|
||||
contentDescription = "NIP-05 Verified",
|
||||
modifier = Modifier.size(15.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "_@vitorpamplona.com",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LightningAddressMockup() {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = "\u26A1",
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "vitor@getalby.com",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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.ui.screen.loggedIn.profile.header
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserContactCardsFollowerCount
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmountInteger
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.dal.UserProfileFollowersUserFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.dal.UserProfileFollowsUserFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@Composable
|
||||
fun ProfileStatsRow(
|
||||
baseUser: User,
|
||||
followsFeedViewModel: UserProfileFollowsUserFeedViewModel,
|
||||
followersFeedViewModel: UserProfileFollowersUserFeedViewModel,
|
||||
zapFeedViewModel: UserProfileZapsViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
onFollowingClick: () -> Unit,
|
||||
onFollowersClick: () -> Unit,
|
||||
onZapsClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
FollowingStatItem(
|
||||
followsFeedViewModel = followsFeedViewModel,
|
||||
onClick = onFollowingClick,
|
||||
)
|
||||
|
||||
FollowersStatItem(
|
||||
baseUser = baseUser,
|
||||
followersFeedViewModel = followersFeedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
onClick = onFollowersClick,
|
||||
)
|
||||
|
||||
ZapsStatItem(
|
||||
zapFeedViewModel = zapFeedViewModel,
|
||||
onClick = onZapsClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FollowingStatItem(
|
||||
followsFeedViewModel: UserProfileFollowsUserFeedViewModel,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val followCount by followsFeedViewModel.followCount.collectAsStateWithLifecycle()
|
||||
|
||||
val displayValue =
|
||||
if (followCount > 0) {
|
||||
followCount.toString()
|
||||
} else {
|
||||
"--"
|
||||
}
|
||||
|
||||
StatItem(
|
||||
value = displayValue,
|
||||
label = stringRes(R.string.following).trim(),
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FollowersStatItem(
|
||||
baseUser: User,
|
||||
followersFeedViewModel: UserProfileFollowersUserFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val followerCountFromCards by observeUserContactCardsFollowerCount(baseUser, accountViewModel)
|
||||
val followerCountLocal by followersFeedViewModel.followerCount.collectAsStateWithLifecycle()
|
||||
|
||||
val displayValue =
|
||||
if (followerCountFromCards != "--") {
|
||||
followerCountFromCards
|
||||
} else if (followerCountLocal > 0) {
|
||||
followerCountLocal.toString()
|
||||
} else {
|
||||
"--"
|
||||
}
|
||||
|
||||
StatItem(
|
||||
value = displayValue,
|
||||
label = stringRes(R.string.followers).trim(),
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ZapsStatItem(
|
||||
zapFeedViewModel: UserProfileZapsViewModel,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val zapAmount by zapFeedViewModel.totalReceivedZaps.collectAsStateWithLifecycle()
|
||||
|
||||
val displayValue = showAmountInteger(zapAmount)
|
||||
|
||||
StatItem(
|
||||
value = displayValue.ifBlank { "--" },
|
||||
label = stringRes(R.string.zaps),
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatItem(
|
||||
value: String,
|
||||
label: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier =
|
||||
Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = value,
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||
label = "stat_value_$label",
|
||||
) { targetValue ->
|
||||
Text(
|
||||
text = targetValue,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user