diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/GalleryCard.kt b/app/src/main/kotlin/com/wisp/app/ui/component/GalleryCard.kt index 753fe5e8..a58ac17e 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/GalleryCard.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/GalleryCard.kt @@ -324,21 +324,27 @@ fun GalleryCard( ) Spacer(Modifier.width(10.dp)) Column(modifier = Modifier.weight(1f)) { - Text( - text = displayName, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.clickable(onClick = onProfileClick) - ) - profile?.nip05?.let { nip05 -> - Nip05Badge( - nip05 = nip05, - pubkey = event.pubkey, - nip05Repo = nip05Repo, - onClick = onProfileClick + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = displayName, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(1f, fill = false) + .clickable(onClick = onProfileClick) ) + profile?.nip05?.let { nip05 -> + Spacer(Modifier.width(4.dp)) + Nip05Badge( + nip05 = nip05, + pubkey = event.pubkey, + nip05Repo = nip05Repo, + onClick = onProfileClick, + showHandle = false + ) + } } } Text( @@ -935,7 +941,7 @@ private fun formatGalleryTimestamp(epoch: Long): String { if (hours < 24) return "${hours}h" val days = diff / (24 * 60 * 60 * 1000L) - if (days == 1L) return "yesterday" + if (days < 7) return "${days}d" val date = Date(millis) val cal = java.util.Calendar.getInstance() diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/PostCard.kt b/app/src/main/kotlin/com/wisp/app/ui/component/PostCard.kt index 02230d6c..6be7f264 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/PostCard.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/PostCard.kt @@ -64,7 +64,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.graphics.Color import androidx.compose.ui.zIndex import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Verified import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close @@ -346,14 +346,28 @@ fun PostCard( ) Spacer(Modifier.width(10.dp)) Column(modifier = Modifier.weight(1f)) { - Text( - text = displayName, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.clickable(onClick = onProfileClick) - ) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = displayName, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(1f, fill = false) + .clickable(onClick = onProfileClick) + ) + profile?.nip05?.let { nip05 -> + Spacer(Modifier.width(4.dp)) + Nip05Badge( + nip05 = nip05, + pubkey = event.pubkey, + nip05Repo = nip05Repo, + onClick = onProfileClick, + showHandle = false + ) + } + } // NIP-38: user status (hide on replies to reduce clutter) val statusVersion by eventRepo?.statusVersion?.collectAsState() ?: remember { mutableIntStateOf(0) } val userStatus = remember(statusVersion, event.pubkey) { @@ -369,16 +383,6 @@ fun PostCard( fontStyle = androidx.compose.ui.text.font.FontStyle.Italic ) } - if (!Nip10.isReply(event)) { - profile?.nip05?.let { nip05 -> - Nip05Badge( - nip05 = nip05, - pubkey = event.pubkey, - nip05Repo = nip05Repo, - onClick = onProfileClick - ) - } - } } if (isPrivate) { Icon( @@ -1295,7 +1299,7 @@ private val dateTimeYearFormat = SimpleDateFormat("MMM d, yyyy", Locale.US) /** * Format an epoch timestamp into a relative or absolute time string. - * Avoids Calendar allocations — uses simple arithmetic for "yesterday" check. + * Avoids Calendar allocations for the s/m/h/d tiers — simple arithmetic. */ private fun formatTimestamp(epoch: Long): String { val now = System.currentTimeMillis() @@ -1313,7 +1317,7 @@ private fun formatTimestamp(epoch: Long): String { if (hours < 24) return "${hours}h" val days = diff / (24 * 60 * 60 * 1000L) - if (days == 1L) return "yesterday" + if (days < 7) return "${days}d" val date = Date(millis) val cal = java.util.Calendar.getInstance() @@ -1342,6 +1346,10 @@ internal fun Nip05Badge( maxLines: Int = 1, verifiedTint: Color = MaterialTheme.colorScheme.primary, iconLeading: Boolean = false, + /** When false, render only the verification icon — no handle text. The handle itself + * is reserved for the profile screen; everywhere else (feed, threads, comments) just + * the badge icon appears next to the username. */ + showHandle: Boolean = true, modifier: Modifier = Modifier ) { if (nip05.isBlank()) return @@ -1370,10 +1378,29 @@ internal fun Nip05Badge( } ) ) { + if (!showHandle) { + // Icon-only badge: appears once verification resolves. No retry icon here — + // a transient relay error shouldn't flag every row across the timeline. + when { + status == Nip05Status.VERIFIED -> Icon( + Icons.Default.Verified, + contentDescription = "Verified", + tint = verifiedTint, + modifier = Modifier.size(14.dp) + ) + isImpersonator -> Icon( + Icons.Default.Cancel, + contentDescription = "Impersonator", + tint = Color.Red, + modifier = Modifier.size(14.dp) + ) + } + return@Row + } if (iconLeading) { if (status == Nip05Status.VERIFIED) { Icon( - Icons.Default.CheckCircle, + Icons.Default.Verified, contentDescription = "Verified", tint = verifiedTint, modifier = Modifier.size(14.dp) @@ -1409,7 +1436,7 @@ internal fun Nip05Badge( if (status == Nip05Status.VERIFIED) { Spacer(Modifier.width(4.dp)) Icon( - Icons.Default.CheckCircle, + Icons.Default.Verified, contentDescription = "Verified", tint = verifiedTint, modifier = Modifier.size(14.dp) diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/ProfilePicture.kt b/app/src/main/kotlin/com/wisp/app/ui/component/ProfilePicture.kt index 96d2c352..e96261d5 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/ProfilePicture.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/ProfilePicture.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.ContentScale @@ -196,6 +197,10 @@ private fun BaseProfilePicture( @Composable private fun FollowBadge(size: Int, modifier: Modifier = Modifier) { val badgeSize = (size * 0.3f).coerceIn(10f, 16f) + // iOS parity: the check flips with the color scheme — black on the + // orange circle in dark mode, white in light mode. Derive from the + // active surface so theme presets stay correct too. + val checkColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5f) Color.Black else Color.White Box( contentAlignment = Alignment.Center, modifier = modifier @@ -207,7 +212,7 @@ private fun FollowBadge(size: Int, modifier: Modifier = Modifier) { Icon( Icons.Default.Check, contentDescription = "Following", - tint = MaterialTheme.colorScheme.onPrimary, + tint = checkColor, modifier = Modifier.size((badgeSize * 0.65f).dp) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/RichContent.kt b/app/src/main/kotlin/com/wisp/app/ui/component/RichContent.kt index 0cfab801..465a46b7 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/RichContent.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/RichContent.kt @@ -1316,15 +1316,28 @@ fun QuotedNote( Row(verticalAlignment = Alignment.CenterVertically) { ProfilePicture(url = profile?.picture, size = 34) Spacer(Modifier.width(10.dp)) - Column(modifier = Modifier.weight(1f)) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f) + ) { Text( text = profile?.displayString ?: event.pubkey.toNpub().let { "${it.take(12)}...${it.takeLast(4)}" }, style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false) ) + profile?.nip05?.let { nip05 -> + Spacer(Modifier.width(4.dp)) + Nip05Badge( + nip05 = nip05, + pubkey = event.pubkey, + nip05Repo = noteActions?.nip05Repo, + showHandle = false + ) + } } Text( text = formatQuotedTimestamp(event.created_at), @@ -2172,7 +2185,7 @@ private fun formatQuotedTimestamp(epoch: Long): String { seconds < 60 -> "${seconds}s" minutes < 60 -> "${minutes}m" hours < 24 -> "${hours}h" - days == 1L -> "yesterday" + days < 7 -> "${days}d" else -> java.text.SimpleDateFormat("MMM d", java.util.Locale.US).format(java.util.Date(epoch * 1000)) } } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/NotificationsScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/NotificationsScreen.kt index 502e52fe..ac24d25f 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/NotificationsScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/NotificationsScreen.kt @@ -2256,7 +2256,7 @@ private fun formatNotifTimestamp(epoch: Long): String { if (hours < 24) return "${hours}h" val days = diff / (24 * 60 * 60 * 1000L) - if (days == 1L) return "yesterday" + if (days < 7) return "${days}d" val date = Date(millis) val cal = java.util.Calendar.getInstance()