diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/ActionBar.kt b/app/src/main/kotlin/com/wisp/app/ui/component/ActionBar.kt index 22a68f35..e1ba35d5 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/ActionBar.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/ActionBar.kt @@ -104,12 +104,16 @@ fun ActionBar( modifier = Modifier.size(22.dp) ) } - Text( - text = replyCount.toString(), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1 - ) + // Hide the count when zero — matches iOS minimalistic action bar + // where empty metrics drop entirely instead of showing "0". + if (replyCount > 0) { + Text( + text = replyCount.toString(), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1 + ) + } // React + Zap are available on private replies as gift-wrapped/DIP-03 actions. // Repost / Quote stay hidden on private replies because those events would // publicly attach an e-tag pointing at the encrypted rumor id. @@ -160,12 +164,14 @@ fun ActionBar( ) } } - Text( - text = likeCount.toString(), - style = MaterialTheme.typography.labelSmall, - color = if (userReactionEmojis.isNotEmpty()) WispThemeColors.zapColor else MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1 - ) + if (likeCount > 0) { + Text( + text = likeCount.toString(), + style = MaterialTheme.typography.labelSmall, + color = if (userReactionEmojis.isNotEmpty()) WispThemeColors.zapColor else MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1 + ) + } if (!isPrivate) { Spacer(Modifier.width(8.dp)) Box { @@ -191,12 +197,14 @@ fun ActionBar( ) } } - Text( - text = repostCount.toString(), - style = MaterialTheme.typography.labelSmall, - color = if (hasUserReposted) WispThemeColors.repostColor else MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1 - ) + if (repostCount > 0) { + Text( + text = repostCount.toString(), + style = MaterialTheme.typography.labelSmall, + color = if (hasUserReposted) WispThemeColors.repostColor else MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1 + ) + } } Spacer(Modifier.width(8.dp)) Box { @@ -247,7 +255,7 @@ fun ActionBar( ) } } - if (!isZapInProgress) { + if (!isZapInProgress && zapSats > 0) { val context = LocalContext.current val fiatPrefs = remember { FiatPreferences.get(context) } fiatPrefs.fiatMode.collectAsState().value diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/BottomBar.kt b/app/src/main/kotlin/com/wisp/app/ui/component/BottomBar.kt index bc3653bc..a57d3cda 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/BottomBar.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/BottomBar.kt @@ -19,6 +19,8 @@ import androidx.compose.material.icons.outlined.Search import androidx.compose.material3.Icon import androidx.compose.ui.res.painterResource import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.height import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar @@ -28,6 +30,7 @@ import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.layout import androidx.compose.ui.unit.dp @@ -73,7 +76,18 @@ fun WispBottomBar( color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) ) NavigationBar( - containerColor = MaterialTheme.colorScheme.surface, + // Match the body color (background) instead of Material's + // default surface tone — iOS uses one near-black across body + // + chrome and reserves the lighter "surface" for elevated + // controls (pills, cards). + containerColor = MaterialTheme.colorScheme.background, + // Material's default 80dp NavigationBar reserves a tall slot + // for labels we never render — clamp to 56dp + the gesture + // inset for a chrome height closer to the iOS tab bar. + modifier = Modifier.height( + 56.dp + NavigationBarDefaults.windowInsets + .asPaddingValues().calculateBottomPadding() + ), windowInsets = NavigationBarDefaults.windowInsets ) { visibleTabs.forEach { tab -> @@ -92,7 +106,9 @@ fun WispBottomBar( colors = NavigationBarItemDefaults.colors( selectedIconColor = MaterialTheme.colorScheme.primary, unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - indicatorColor = MaterialTheme.colorScheme.surfaceVariant + // Drop the active-pill indicator — the orange tint on + // the selected icon is enough signal (matches iOS). + indicatorColor = Color.Transparent ), icon = { val useBolt = com.wisp.app.ui.util.useBoltIcon() @@ -129,13 +145,16 @@ fun WispBottomBar( ) } if (hasUnread) { + // Notification dot uses iOS-standard badge red + // (#FF3B30) instead of the primary accent so it + // reads as "alert" rather than "branded highlight." Box( modifier = Modifier .size(8.dp) .align(Alignment.TopEnd) .offset(x = 2.dp, y = (-2).dp) .background( - color = MaterialTheme.colorScheme.primary, + color = Color(0xFFFF3B30), shape = CircleShape ) ) 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 abc708be..8967ed2a 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 @@ -230,8 +230,12 @@ fun PostCard( } } + // Wrap content + divider so the divider can run full-width while the + // content keeps its 16dp horizontal padding. Tap-to-open lives on the + // content Column so the (tiny) divider area isn't tappable. + Column(modifier = modifier.fillMaxWidth()) { Column( - modifier = modifier + modifier = Modifier .fillMaxWidth() .then(if (onNoteClick != null) Modifier.pointerInput(onNoteClick) { awaitEachGesture { @@ -875,9 +879,13 @@ fun PostCard( } } } - if (showDivider) { - HorizontalDivider(color = MaterialTheme.colorScheme.outline, thickness = 0.5.dp) - } + } + if (showDivider) { + // Full-bleed inter-post separator — sits outside the content + // Column's 16dp horizontal padding so it spans edge to edge, + // matching the iOS feed. + HorizontalDivider(color = MaterialTheme.colorScheme.outline, thickness = 0.5.dp) + } } } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/ArticleScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/ArticleScreen.kt index cdb3180c..3537fc4e 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/ArticleScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/ArticleScreen.kt @@ -136,7 +136,7 @@ fun ArticleScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/BlossomServersScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/BlossomServersScreen.kt index cc7f206d..3ab0ef9b 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/BlossomServersScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/BlossomServersScreen.kt @@ -78,7 +78,7 @@ fun BlossomServersScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/BookmarkSetScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/BookmarkSetScreen.kt index 8b68ffea..548d8b0a 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/BookmarkSetScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/BookmarkSetScreen.kt @@ -103,7 +103,7 @@ fun BookmarkSetScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/BookmarksScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/BookmarksScreen.kt index c8036449..98255f8a 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/BookmarksScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/BookmarksScreen.kt @@ -71,7 +71,7 @@ fun BookmarksScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/CommunityScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/CommunityScreen.kt index 958c8349..e146f1e1 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/CommunityScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/CommunityScreen.kt @@ -77,7 +77,7 @@ fun CommunityScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/ComposeScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/ComposeScreen.kt index 168c6235..b85cb7c5 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/ComposeScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/ComposeScreen.kt @@ -303,7 +303,7 @@ fun ComposeScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } @@ -1267,6 +1267,11 @@ fun ComposeScreen( } } } else { + // Publish is disabled until the post has at least one + // character of text OR at least one uploaded attachment. + // Prevents accidental empty posts and matches the iOS + // composer's send-button gating. + val hasContent = content.text.isNotBlank() || uploadedUrls.isNotEmpty() Button( onClick = { viewModel.publish( @@ -1282,7 +1287,7 @@ fun ComposeScreen( resolvedEmojis = resolvedEmojis ) }, - enabled = !publishing && !isMiningBusy, + enabled = !publishing && !isMiningBusy && hasContent, modifier = Modifier.fillMaxWidth().height(44.dp), contentPadding = PaddingValues(0.dp) ) { diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/ContactPickerScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/ContactPickerScreen.kt index 4ab05a53..3893e622 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/ContactPickerScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/ContactPickerScreen.kt @@ -87,7 +87,7 @@ fun ContactPickerScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) }, diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/DmConversationScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/DmConversationScreen.kt index 5c02d207..f1b5c0f6 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/DmConversationScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/DmConversationScreen.kt @@ -279,7 +279,7 @@ fun DmConversationScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/DmListScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/DmListScreen.kt index 80d26b6a..584e1542 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/DmListScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/DmListScreen.kt @@ -121,7 +121,7 @@ fun DmListScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) }, diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/DraftsScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/DraftsScreen.kt index 1da50495..ed727d45 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/DraftsScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/DraftsScreen.kt @@ -88,7 +88,7 @@ fun DraftsScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/FeedScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/FeedScreen.kt index bd7ccefc..f62db203 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/FeedScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/FeedScreen.kt @@ -15,6 +15,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -125,6 +127,7 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.automirrored.outlined.Article import androidx.compose.material.icons.outlined.CurrencyBitcoin import androidx.compose.material.icons.outlined.Dashboard +import androidx.compose.material.icons.outlined.GridView import androidx.compose.material.icons.outlined.HowToVote import androidx.compose.material.icons.outlined.Photo import androidx.compose.material.icons.outlined.FavoriteBorder @@ -749,6 +752,14 @@ fun FeedScreen( contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = { CenterAlignedTopAppBar( + // Material's default 64dp content + status-bar inset + // pads a chunky gap below the icon row. Clamp to 48dp + // + status-bar inset so the chrome lands closer to + // the iOS navigation bar (~44dp content + safe-area). + modifier = Modifier.height( + 48.dp + WindowInsets.statusBars + .asPaddingValues().calculateTopPadding() + ), title = { Box { Surface( @@ -867,7 +878,7 @@ fun FeedScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ), navigationIcon = { Row(verticalAlignment = Alignment.CenterVertically) { @@ -888,7 +899,9 @@ fun FeedScreen( modifier = Modifier.size(36.dp) ) { val (icon, tint) = when (contentFilter) { - FeedContentFilter.ALL -> Icons.Outlined.Dashboard to MaterialTheme.colorScheme.onSurfaceVariant + // GridView = 2x2 of equal squares (matches iOS). + // Dashboard was 1 large + 3 small panels. + FeedContentFilter.ALL -> Icons.Outlined.GridView to MaterialTheme.colorScheme.onSurfaceVariant FeedContentFilter.TEXT_ONLY -> Icons.AutoMirrored.Outlined.Article to MaterialTheme.colorScheme.primary FeedContentFilter.GALLERY_ONLY -> Icons.Outlined.Photo to MaterialTheme.colorScheme.primary FeedContentFilter.POLLS_ONLY -> Icons.Outlined.HowToVote to MaterialTheme.colorScheme.primary diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/GroupDetailScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/GroupDetailScreen.kt index 1c2f9364..cbce428e 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/GroupDetailScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/GroupDetailScreen.kt @@ -126,7 +126,7 @@ fun GroupDetailScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/GroupRoomScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/GroupRoomScreen.kt index 33bc91e9..9137420f 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/GroupRoomScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/GroupRoomScreen.kt @@ -398,7 +398,7 @@ fun GroupRoomScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/HashtagFeedScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/HashtagFeedScreen.kt index fb9108ed..dbe58f2e 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/HashtagFeedScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/HashtagFeedScreen.kt @@ -148,7 +148,7 @@ fun HashtagFeedScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/InterfaceScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/InterfaceScreen.kt index 1739e902..110da1cb 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/InterfaceScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/InterfaceScreen.kt @@ -132,7 +132,7 @@ fun InterfaceScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/ListScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/ListScreen.kt index 93da2e15..e647b185 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/ListScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/ListScreen.kt @@ -190,7 +190,7 @@ fun ListScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/ListsHubScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/ListsHubScreen.kt index 2d9e8263..06805f92 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/ListsHubScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/ListsHubScreen.kt @@ -133,7 +133,7 @@ fun ListsHubScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) }, diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/LiveStreamScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/LiveStreamScreen.kt index 2fde73b1..4e2f5d85 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/LiveStreamScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/LiveStreamScreen.kt @@ -174,7 +174,7 @@ fun LiveStreamScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) 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 98372d50..5ed61e69 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 @@ -342,7 +342,7 @@ fun NotificationsScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/PowSettingsScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/PowSettingsScreen.kt index fcc61ced..c004ec44 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/PowSettingsScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/PowSettingsScreen.kt @@ -60,7 +60,7 @@ fun PowSettingsScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/ProfileEditScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/ProfileEditScreen.kt index 2564973f..7a9f2056 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/ProfileEditScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/ProfileEditScreen.kt @@ -100,7 +100,7 @@ fun ProfileEditScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/RelayDetailScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/RelayDetailScreen.kt index 56135bbc..9f0200d5 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/RelayDetailScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/RelayDetailScreen.kt @@ -111,7 +111,7 @@ fun RelayDetailScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/RelayHealthScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/RelayHealthScreen.kt index dc0c20bd..3ecfa9a0 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/RelayHealthScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/RelayHealthScreen.kt @@ -71,7 +71,7 @@ fun RelayHealthScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/RelayScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/RelayScreen.kt index df0a5644..acc081e3 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/RelayScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/RelayScreen.kt @@ -73,7 +73,7 @@ fun RelayScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/SafetyScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/SafetyScreen.kt index 8ad24485..a8f2ab3d 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/SafetyScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/SafetyScreen.kt @@ -76,7 +76,7 @@ fun SafetyScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/SocialGraphScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/SocialGraphScreen.kt index bf751c06..4422ebe6 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/SocialGraphScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/SocialGraphScreen.kt @@ -146,7 +146,7 @@ fun SocialGraphScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/ThreadScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/ThreadScreen.kt index ce55063f..ec6aa57d 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/ThreadScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/ThreadScreen.kt @@ -20,8 +20,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.outlined.Edit import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -29,6 +31,9 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.ui.res.stringResource import com.wisp.app.R import androidx.compose.material3.TopAppBar @@ -49,6 +54,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource @@ -211,6 +219,10 @@ fun ThreadScreen( Toast.makeText(zapDisabledContext, zapDisabledMessage, Toast.LENGTH_SHORT).show() } + // iOS-parity sticky composer at the bottom of the thread — tap opens + // compose with the thread's focal as the reply parent. Disabled until + // the focal has loaded (flatThread populated). + val focalEvent = flatThread.firstOrNull()?.first Scaffold( contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = { @@ -222,9 +234,15 @@ fun ThreadScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) + }, + bottomBar = { + ThreadReplyBar( + enabled = focalEvent != null, + onClick = { focalEvent?.let { onReply(it) } } + ) } ) { padding -> if (isLoading && flatThread.isEmpty()) { @@ -281,23 +299,59 @@ fun ThreadScreen( val userZapPollVote = remember(pollVoteVersion, event.id) { if (event.kind == 6969) eventRepo.getUserZapPollVote(event.id) else null } - val indentDp = 12 - val clampedDepth = min(depth, 8) - val lineColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.45f) + // iOS-parity depth connector (matches + // ThreadView.swift's ReplyConnectorShape + + // indentationWidth). Each non-root reply: + // • indents by `depth * 12dp` (capped at 5) + // • draws a single L on its left: vertical from + // top → (bottom − r), 8dp rounded fillet, + // horizontal from arc end → right edge + // • the connector itself sits 8dp LEFT of the + // post's content column so the curve hooks + // cleanly into the row. + val indentStepDp = 12.dp + val clampedDepth = min(depth, 5) + val cornerRadiusDp = 8.dp + val lineColor = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) + val showConnector = depth > 0 Box( modifier = Modifier .fillMaxWidth() .drawBehind { - val indentPx = indentDp.dp.toPx() - for (level in 0 until clampedDepth) { - val x = level * indentPx + indentPx / 2f - drawLine( - color = lineColor, - start = Offset(x, 0f), - end = Offset(x, size.height), - strokeWidth = 1.5.dp.toPx() - ) - } + if (!showConnector) return@drawBehind + // Vertical sits at the connector's left + // edge — `clampedDepth*12 − 8 + 1` from + // screen, matching the iOS + // `.padding(.leading, depth*12 − 8)` + // and `x = 1` inside the shape. + val lineX = (clampedDepth * indentStepDp.toPx()) - 8.dp.toPx() + 1.dp.toPx() + val r = cornerRadiusDp.toPx() + val strokePx = 1.dp.toPx() + drawLine( + color = lineColor, + start = Offset(lineX, 0f), + end = Offset(lineX, size.height - r), + strokeWidth = strokePx + ) + drawArc( + color = lineColor, + startAngle = 90f, + sweepAngle = 90f, + useCenter = false, + topLeft = Offset(lineX, size.height - 2f * r), + size = Size(2f * r, 2f * r), + style = Stroke(width = strokePx, cap = StrokeCap.Round) + ) + // Horizontal divider from arc end → right + // edge. PostCard's full-width divider is + // suppressed (`showDivider = false`) so + // no line renders LEFT of the curve. + drawLine( + color = lineColor, + start = Offset(lineX + r, size.height), + end = Offset(size.width, size.height), + strokeWidth = strokePx + ) } ) { if (isGalleryEvent(event)) { @@ -343,7 +397,8 @@ fun ThreadScreen( nip05Repo = nip05Repo, onQuotedNoteClick = onQuotedNoteClick, noteActions = noteActions, - modifier = Modifier.padding(start = (clampedDepth * indentDp).dp) + showDivider = !showConnector, + modifier = Modifier.padding(start = (clampedDepth * indentStepDp.value).dp) ) } else { PostCard( @@ -402,7 +457,8 @@ fun ThreadScreen( zapPollTotalSats = zapPollTotalSats, userZapPollVote = userZapPollVote, onZapPollVote = { idx -> onZapPollVote(event.id, idx) }, - modifier = Modifier.padding(start = (clampedDepth * indentDp).dp) + showDivider = !showConnector, + modifier = Modifier.padding(start = (clampedDepth * indentStepDp.value).dp) ) } } @@ -609,3 +665,61 @@ private fun SpamToggle(count: Int, expanded: Boolean, onToggle: () -> Unit) { } } +/** + * Sticky reply composer at the bottom of the thread view — iOS parity + * (see `wisp-ios/wisp/ThreadView.swift::composer`). + * + * A thin divider runs the full width above a tap-to-compose pill: a + * surfaceVariant-tinted rounded rect with "Reply…" placeholder text on + * the left and an orange edit icon on the right. The whole pill is the + * tap target; iOS uses `square.and.pencil` here so we use + * `Icons.Outlined.Edit` (the closest Material equivalent). + */ +@Composable +private fun ThreadReplyBar( + enabled: Boolean, + onClick: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + ) { + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + thickness = 0.5.dp + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { + Surface( + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + shape = RoundedCornerShape(18.dp), + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = enabled, onClick = onClick) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp) + ) { + Text( + text = "Reply…", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = Icons.Outlined.Edit, + contentDescription = "Reply", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(16.dp) + ) + } + } + } + } +} + diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/UserProfileScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/UserProfileScreen.kt index 0641de9d..ee8ccf25 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/UserProfileScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/UserProfileScreen.kt @@ -519,7 +519,7 @@ fun UserProfileScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } @@ -643,7 +643,10 @@ fun UserProfileScreen( } stickyHeader { - val surfaceColor = MaterialTheme.colorScheme.surface + // Tab strip uses `background` (true near-black) instead of + // `surface` so the chrome reads as part of the body and + // doesn't stack two distinct grey tiers — matches iOS. + val surfaceColor = MaterialTheme.colorScheme.background Column { Box( modifier = Modifier.background(surfaceColor).drawWithContent { @@ -689,7 +692,9 @@ fun UserProfileScreen( Box( contentAlignment = Alignment.Center, modifier = Modifier - .height(34.dp) + // 28dp (was 34) — matches iOS tighter + // tab row over the profile header. + .height(28.dp) .clickable { selectedTab = index } .padding(horizontal = 10.dp) ) { @@ -703,7 +708,7 @@ fun UserProfileScreen( } } } - Spacer(Modifier.height(12.dp)) + Spacer(Modifier.height(6.dp)) } } diff --git a/app/src/main/kotlin/com/wisp/app/ui/screen/WalletScreen.kt b/app/src/main/kotlin/com/wisp/app/ui/screen/WalletScreen.kt index 4a7fe06a..52ae009f 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/screen/WalletScreen.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/screen/WalletScreen.kt @@ -197,7 +197,7 @@ fun WalletScreen( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.background ) ) } diff --git a/app/src/main/kotlin/com/wisp/app/ui/theme/Theme.kt b/app/src/main/kotlin/com/wisp/app/ui/theme/Theme.kt index 2ce58a40..2117ddc4 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/theme/Theme.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/theme/Theme.kt @@ -98,6 +98,13 @@ fun WispTheme( val primaryContainerDark = remember(primary) { darkenColor(primary, 0.6f) } val primaryContainerLight = remember(primary) { lightenColor(primary, 0.7f) } + // iOS systemRed (#FF3B30) for destructive / error UI. Material 3's + // default `error` renders pinkish in dark mode (`#F2B8B5`) and a + // muted brick red in light mode (`#B3261E`); both read "off" next + // to the iOS counterpart. Setting `error` explicitly propagates to + // every `MaterialTheme.colorScheme.error` consumer (logout, alerts, + // destructive labels). + val iosRed = Color(0xFFFF3B30) val colorScheme = if (isDarkTheme) { if (isCustomTheme) { darkColorScheme( @@ -106,13 +113,15 @@ fun WispTheme( primaryContainer = primaryContainerDark, onPrimaryContainer = lightenColor(accentColor, 0.5f), secondary = secondary, - background = Color(0xFF131215), - surface = Color(0xFF1F1E21), - surfaceVariant = Color(0xFF2B2A2E), + background = Color(0xFF0A0A0B), + surface = Color(0xFF1C1C1E), + surfaceVariant = Color(0xFF2C2C2E), onBackground = Color(0xFFE0E0E0), onSurface = Color(0xFFE0E0E0), onSurfaceVariant = Color(0xFF9998A0), - outline = Color(0xFF343338) + outline = Color(0xFF38383A), + error = iosRed, + onError = Color.White ) } else { val colors = themePreset.dark @@ -129,7 +138,9 @@ fun WispTheme( onBackground = colors.onBackground, onSurface = colors.onSurface, onSurfaceVariant = colors.onSurfaceVariant, - outline = colors.outline + outline = colors.outline, + error = iosRed, + onError = Color.White ) } } else { @@ -146,7 +157,9 @@ fun WispTheme( onBackground = Color(0xFF1C1B1F), onSurface = Color(0xFF1C1B1F), onSurfaceVariant = Color(0xFF6B6B6B), - outline = Color(0xFFCCCCCC) + outline = Color(0xFFCCCCCC), + error = iosRed, + onError = Color.White ) } else { val colors = themePreset.light @@ -163,7 +176,9 @@ fun WispTheme( onBackground = colors.onBackground, onSurface = colors.onSurface, onSurfaceVariant = colors.onSurfaceVariant, - outline = colors.outline + outline = colors.outline, + error = iosRed, + onError = Color.White ) } } @@ -171,7 +186,7 @@ fun WispTheme( val wispColors = if (isDarkTheme) { if (isCustomTheme) { WispColors( - backgroundColor = Color(0xFF131215), + backgroundColor = Color(0xFF0A0A0B), zapColor = accentColor, repostColor = Color(0xFF4CAF50), bookmarkColor = accentColor, diff --git a/app/src/main/kotlin/com/wisp/app/ui/theme/Themes.kt b/app/src/main/kotlin/com/wisp/app/ui/theme/Themes.kt index 900d5de2..605baf29 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/theme/Themes.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/theme/Themes.kt @@ -10,13 +10,13 @@ object Themes { dark = ThemeColors( primary = Color(0xFFFF9800), secondary = Color(0xFFFFB74D), - background = Color(0xFF131215), - surface = Color(0xFF1F1E21), - surfaceVariant = Color(0xFF2B2A2E), + background = Color(0xFF0A0A0B), + surface = Color(0xFF1C1C1E), + surfaceVariant = Color(0xFF2C2C2E), onBackground = Color(0xFFE0E0E0), onSurface = Color(0xFFE0E0E0), onSurfaceVariant = Color(0xFF9998A0), - outline = Color(0xFF343338), + outline = Color(0xFF38383A), zapColor = Color(0xFFFF9800), repostColor = Color(0xFF4CAF50), bookmarkColor = Color(0xFFFF9800), diff --git a/app/src/main/kotlin/com/wisp/app/viewmodel/ThreadViewModel.kt b/app/src/main/kotlin/com/wisp/app/viewmodel/ThreadViewModel.kt index bf822f60..d1b62b4d 100644 --- a/app/src/main/kotlin/com/wisp/app/viewmodel/ThreadViewModel.kt +++ b/app/src/main/kotlin/com/wisp/app/viewmodel/ThreadViewModel.kt @@ -466,16 +466,14 @@ class ThreadViewModel : ViewModel() { scoreAuthorsAsync(pubkeysToScore) } - val myPubkey = currentUserPubkey + // Sort children by createdAt (oldest first), matching iOS + // `ThreadViewModel.buildNestedReplies`. Previously the user's own + // replies were bubbled to the top within each level — that's + // useful in some clients but diverges from iOS, so threads showed + // different orderings across platforms. for (children in parentToChildren.values) { children.sortWith(Comparator { a, b -> - val aIsOwn = myPubkey != null && a.pubkey == myPubkey - val bIsOwn = myPubkey != null && b.pubkey == myPubkey - if (aIsOwn != bIsOwn) { - if (aIsOwn) -1 else 1 - } else { - a.created_at.compareTo(b.created_at) - } + a.created_at.compareTo(b.created_at) }) }