From e862b3ecacb55a64f9b5aac27fb8c39fa101cba5 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Mon, 27 Jul 2026 11:51:35 +0300 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20profile=20header=20polish=20?= =?UTF-8?q?=E2=80=94=20banner,=20rich-text=20bio,=20CLINK=20offer=20field?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Render kind-0 banner image in the profile card header - Render bio as rich text (mentions/hashtags/links) via DesktopRichText - Add CLINK offer (noffer) field to Edit Profile + shared EditProfileFields Co-Authored-By: Claude Opus 4.8 --- .../commons/profile/EditProfileFields.kt | 4 ++ .../amethyst/desktop/ui/UserProfileScreen.kt | 43 ++++++++++++++++--- .../desktop/ui/profile/EditProfileScreen.kt | 10 +++++ 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/profile/EditProfileFields.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/profile/EditProfileFields.kt index 7055b3f459..0974975482 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/profile/EditProfileFields.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/profile/EditProfileFields.kt @@ -54,6 +54,7 @@ class EditProfileFields { val nip05 = MutableStateFlow("") val lnAddress = MutableStateFlow("") val lnURL = MutableStateFlow("") + val clinkOffer = MutableStateFlow("") // Social proofs (NIP-39) val twitter = MutableStateFlow("") @@ -79,6 +80,7 @@ class EditProfileFields { nip05.value = info.nip05 ?: "" lnAddress.value = info.lud16 ?: "" lnURL.value = info.lud06 ?: "" + clinkOffer.value = info.clinkOffer ?: "" } twitter.value = "" @@ -114,6 +116,7 @@ class EditProfileFields { nip05.value = "" lnAddress.value = "" lnURL.value = "" + clinkOffer.value = "" twitter.value = "" github.value = "" mastodon.value = "" @@ -132,6 +135,7 @@ class EditProfileFields { "nip05" to nip05.value, "lnAddress" to lnAddress.value, "lnURL" to lnURL.value, + "clinkOffer" to clinkOffer.value, "twitter" to twitter.value, "github" to github.value, "mastodon" to mastodon.value, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index 8682b79941..ec5c0bdc3b 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -65,16 +65,21 @@ import androidx.compose.runtime.rememberCoroutineScope 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.input.pointer.isSecondaryPressed import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.nip02FollowList.FollowAction import com.vitorpamplona.amethyst.commons.profile.ProfileBroadcastStatus import com.vitorpamplona.amethyst.commons.profile.ui.ProfileBroadcastBanner +import com.vitorpamplona.amethyst.commons.richtext.CachedRichTextParser import com.vitorpamplona.amethyst.commons.state.FollowState import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState @@ -89,6 +94,8 @@ import com.vitorpamplona.amethyst.desktop.subscriptions.createContactListSubscri import com.vitorpamplona.amethyst.desktop.subscriptions.generateSubId import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.desktop.ui.media.LightboxOverlay +import com.vitorpamplona.amethyst.desktop.ui.note.DesktopRichText +import com.vitorpamplona.amethyst.desktop.ui.note.RichTextCallbacks import com.vitorpamplona.amethyst.desktop.ui.note.WoTBadgedAvatar import com.vitorpamplona.amethyst.desktop.ui.profile.EditProfileDialog import com.vitorpamplona.amethyst.desktop.ui.profile.GalleryTab @@ -149,6 +156,15 @@ fun UserProfileScreen( ) } var picture by remember { mutableStateOf(cachedMetadata?.profilePicture()) } + var banner by remember { + mutableStateOf( + cachedMetadata + ?.flow + ?.value + ?.info + ?.banner, + ) + } var nip05 by remember { mutableStateOf( cachedMetadata @@ -334,6 +350,7 @@ fun UserProfileScreen( displayName = metadata.displayName ?: metadata.name about = metadata.about picture = metadata.picture + banner = metadata.banner nip05 = metadata.nip05 website = metadata.website lnAddress = metadata.lnAddress() @@ -735,6 +752,19 @@ fun UserProfileScreen( ), ) { Column(modifier = Modifier.padding(16.dp)) { + banner?.takeIf { it.isNotBlank() }?.let { bannerUrl -> + AsyncImage( + model = bannerUrl, + contentDescription = "Profile banner", + contentScale = ContentScale.Crop, + modifier = + Modifier + .fillMaxWidth() + .height(120.dp) + .clip(MaterialTheme.shapes.medium), + ) + Spacer(Modifier.height(12.dp)) + } Row( horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.Top, @@ -798,12 +828,15 @@ fun UserProfileScreen( } } - if (about != null) { + about?.takeIf { it.isNotBlank() }?.let { bio -> Spacer(Modifier.height(12.dp)) - Text( - about!!, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, + val bioState = remember(bio) { CachedRichTextParser.parseText(bio, EmptyTagList) } + DesktopRichText( + content = bio, + state = bioState, + localCache = localCache, + callbacks = RichTextCallbacks(onMentionClick = onNavigateToProfile), + modifier = Modifier.fillMaxWidth(), ) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/profile/EditProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/profile/EditProfileScreen.kt index 3a9b606d44..317b36f1c4 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/profile/EditProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/profile/EditProfileScreen.kt @@ -232,6 +232,7 @@ fun EditProfileDialog( nip05 = fields.nip05.value, lnAddress = fields.lnAddress.value, lnURL = fields.lnURL.value, + clinkOffer = fields.clinkOffer.value, ) } else { MetadataEvent.createNew( @@ -245,6 +246,7 @@ fun EditProfileDialog( nip05 = fields.nip05.value, lnAddress = fields.lnAddress.value, lnURL = fields.lnURL.value, + clinkOffer = fields.clinkOffer.value, ) } val signedMetadata = account.signer.sign(metadataTemplate) @@ -374,6 +376,7 @@ fun EditProfileContent( val nip05Value by fields.nip05.collectAsState() val lnAddressValue by fields.lnAddress.collectAsState() val lnURLValue by fields.lnURL.collectAsState() + val clinkOfferValue by fields.clinkOffer.collectAsState() val twitterValue by fields.twitter.collectAsState() val githubValue by fields.github.collectAsState() val mastodonValue by fields.mastodon.collectAsState() @@ -676,6 +679,13 @@ fun EditProfileContent( singleLine = true, modifier = Modifier.fillMaxWidth(), ) + OutlinedTextField( + value = clinkOfferValue, + onValueChange = { fields.clinkOffer.value = it }, + label = { Text("CLINK offer (noffer…)") }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) // Social Proofs — collapsible SocialProofsSection(