feat(thread): iOS-parity depth connectors, reply order, sticky reply bar + publish gating#567
Open
dmnyc wants to merge 9 commits into
Open
feat(thread): iOS-parity depth connectors, reply order, sticky reply bar + publish gating#567dmnyc wants to merge 9 commits into
dmnyc wants to merge 9 commits into
Conversation
The default ("custom") Android dark theme rendered noticeably lighter
than iOS, which uses near-black backgrounds. Align with iOS HIG dark
system colors (slight off-black for the base, iOS secondary/tertiary
greys for elevated surfaces) so the two platforms feel like the same
app in dark mode.
- background: #131215 → #0A0A0B (slight off-black, OLED-friendly
without the harsh #000 step on LCD)
- surface: #1F1E21 → #1C1C1E (iOS secondarySystemBackground)
- surfaceVariant: #2B2A2E → #2C2C2E (iOS tertiarySystemBackground)
- outline: #343338 → #38383A (iOS separator on dark)
Named presets (Nord, Dracula, Gruvbox, …) are left untouched — their
distinctive backgrounds are part of each preset's identity.
Default `TopAppBarDefaults.topAppBarColors` uses `MaterialTheme.color Scheme.surface`, which sat noticeably lighter than the body after the preceding dark-mode background darken. iOS uses one near-black across body + chrome and reserves the lighter "surface" tone for elevated controls (pills, cards). Switch every screen's TopAppBar container to `background` so chrome reads as part of the page, not as a raised layer above it. 30 screens touched; only `containerColor` lines inside `TopAppBarDefaults.topAppBarColors(...)` blocks are changed, so other surface usages (cards, dialogs, sheets, the elevated pills the home top bar overlays) keep their existing tone.
iOS-style cleanup on the home screen's top + bottom chrome: - `FeedScreen` `CenterAlignedTopAppBar` clamps to 48dp content + status-bar inset (was Material's default ~64dp + inset). Drops the gap below the icon row that pushed the feed down. - `BottomBar` `NavigationBar` clamps to 56dp content + gesture inset (was Material's default 80dp). The 80dp slot reserves space for the label text we don't render — pure waste on small phones. - Tab indicator pill is suppressed (`indicatorColor = Color.Transparent`). The selected-icon orange tint is enough signal; matches iOS where the tab bar has no rounded background on the active tab. - Notification dot uses iOS systemRed (#FF3B30) instead of the app's primary accent so it reads as "alert" rather than "branded highlight" — same red iOS shows on the bell. - Filter icon for "All" content types switches from `Icons.Outlined.Dashboard` (1 large + 3 small panels) to `Icons.Outlined.GridView` (2x2 of equal squares) to match the iOS toolbar icon.
Two post-card refinements that move the feed toward the iOS look: - `PostCard` now wraps content + the inter-post `HorizontalDivider` in an outer Column. The content Column keeps its 16dp horizontal padding (so post body / action bar / metadata stay inset), but the divider sits outside that padding and runs edge-to-edge. Matches iOS where the separator spans the full viewport width. - `ActionBar` gates each of the four counters (`replyCount`, `likeCount`, `repostCount`, `zapSats`) on `> 0`. Empty engagement no longer shows "0" beside the icon — matches iOS where the count text only appears when there's something to show. As soon as the count crosses 1, the number reappears.
Two more iOS-parity tweaks: - `WispTheme` sets `error = #FF3B30` (and `onError = white`) explicitly on every color-scheme variant (custom dark/light + preset dark/light). Material 3's defaults for `error` render pinkish in dark mode and a muted brick red in light mode — neither matches the iOS systemRed used by the rest of the destructive UI in this app. With this override, every `MaterialTheme.colorScheme.error` consumer (logout button, destructive labels, error text) now matches the iOS counterpart and the existing #FF3B30 used directly on Disconnect/Switch wallet flows. - `UserProfileScreen` sticky-header tab strip + the sort-pill row below it use `background` (#0A0A0B) instead of `surface` (#1C1C1E). The two grey tiers stacked above each other read as visually noisy on the profile; the iOS profile uses one near-black across both. Body posts below still render with the elevated tier where they need to.
Reduce per-depth indent column from 12dp to 8dp and stroke width from 1.5dp to 1dp, with rounded stroke caps. Aesthetic only — no change to thread structure or interaction.
Replace the stacked vertical depth lines with a single L-shaped outline per non-root reply — matches the iOS thread view 1:1. For each reply (depth > 0): - Vertical line at `x = depth * indentStep` (16dp per level) running from the top of the post box down to the start of the corner arc. - Counter-clockwise rounded 90° arc (12dp corner radius) at the post's bottom-left, curving the vertical into the horizontal divider. - The horizontal portion of the L is the existing PostCard `HorizontalDivider` at the bottom — no extra drawing needed. Same-depth siblings each get their own self-contained L (no chaining across siblings, matching iOS). Posts are indented by `depth * 16dp` so the L's vertical sits at the post's left content edge. Replaces the earlier per-depth multi-line stack (which drew through avatars and didn't curve) and the top-right elbow attempt (which landed at avatar height, not the bottom-left where iOS terminates).
Three changes to match wisp-ios `ThreadView.swift` / `ThreadViewModel.swift`: Indent + connector geometry - Per-depth step `12.dp` (was 16) capped at depth 5 (was 8), matching `indentationWidth = min(depth, 5) * 12` on iOS. - Connector vertical sits at `clampedDepth * 12 − 7`dp from the screen edge — same math iOS uses (`.padding(.leading, depth*12 − 8)` plus `x = 1` inside the shape). - Corner radius `8.dp` (was 12), `1.dp` stroke. - PostCard's full-width divider is suppressed for non-root replies (`showDivider = false`); the connector draws its own partial divider starting at the arc's end so no horizontal line extends to the LEFT of the curve. Reply sort order - `ThreadViewModel.kt` no longer bubbles the user's own replies to the top within each level. Children sorted purely by `created_at` oldest-first to match iOS's `buildNestedReplies`. Threads now order identically across platforms. Sticky reply bar - New `ThreadReplyBar` composable wired into the Scaffold's `bottomBar`. Thin 0.5dp `outlineVariant @ 50%` divider, then an 18dp rounded `surfaceVariant @ 50%` pill with "Reply…" placeholder text + an orange `Icons.Outlined.Edit` icon (closest Material to iOS's `square.and.pencil`). - Tap-anywhere on the pill opens compose with the thread's focal (the first entry in `flatThread`) as the reply parent. Disabled until flatThread populates. - Outer padding `12.dp` horizontal × `8.dp` vertical, container background `MaterialTheme.colorScheme.background`. Matches `ThreadView.swift::composer` 1:1.
Material's filled Button stays fully tappable even when the post body is empty — easy to fire off an empty note by accident. Gate the Publish button's `enabled` on having either at least one non-whitespace character or at least one uploaded attachment. Matches the iOS composer's send-button gating. `!publishing && !isMiningBusy` continues to gate against double-taps during publish / PoW mining.
e60fc9f to
7e204b9
Compare
dmnyc
added a commit
to dmnyc/wisp
that referenced
this pull request
May 24, 2026
Round of polish on the compose screen after on-device testing. ## Live preview is always visible Drop the `!imeVisible` gate on the preview card so it updates while the keyboard is open. Re-render the "Preview" label as a small grey pill on the right of the user-info row (display name takes weight(1f)), matching the iOS layout. ## Inline highlights for #hashtags and URLs buildMentionAnnotatedString() now also finds `#hashtag` tokens and `http(s)://…` URLs and styles them in `linkColor` (defaults to pillForeground). Highlight ranges are merged with mention ranges and overlap is resolved with mentions winning. The composer no-longer-empty early-return is dropped so hashtags/URLs still get coloured when no pills are tracked. The live preview itself receives the *materialised* draft content — a new `previewMaterializedContent()` helper on the viewmodel splices the `@DisplayName` ranges back into `nostr:nprofile1…` URIs so RichContent can detect them as NostrProfileSegments and render them in the link colour the same way published notes do. ## Publish gating Cherry-pick the gating from PR barrydeen#567: the Publish button stays disabled until `content.text.isNotBlank() || uploadedUrls.isNotEmpty()`. Prevents accidental empty posts. ## Candidate dropdown + Following pill Drop `tonalElevation = 3.dp` on the mention candidates Surface (the elevation overlay was tinting the background with the primary colour). Use `surfaceContainerHigh` for a neutral grey that matches iOS. Re-render the per-row "Following" label as a muted grey pill (`surfaceContainerHighest` background, `onSurfaceVariant` text) instead of plain primary-coloured text, matching iOS and visually consistent with the new Preview badge.
This was referenced May 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
iOS-parity refinements for the thread view and compose screen. Ports specific behaviors from
wisp-ios/ThreadView.swift,ThreadViewModel.swift, andComposeView.swift.Thread depth connectors (
fedd321)Replace the per-depth stacked vertical lines with a single L-shaped outline per non-root reply — matches iOS
ReplyConnectorShape:12dpper level, capped at depth 5 (matchesindentationWidth = min(depth, 5) * 12).clampedDepth * 12 − 7dp from screen edge (iOS's.padding(.leading, depth*12 − 8)+x = 1inside the shape).1dpstroke,outlineVariant @ 50%.showDivider = false); the connector draws its own partial divider starting at the arc's end so no horizontal extends to the LEFT of the curve.Thread reply ordering (
5b2b120)ThreadViewModel.ktno longer bubbles the user's own replies to the top within each level. Children sorted purely bycreated_atoldest-first to match iOS'sbuildNestedReplies. Threads now order identically across platforms.Sticky reply bar (
5b2b120)New
ThreadReplyBarcomposable wired intoThreadScreen'sScaffold.bottomBar. Thin 0.5dpoutlineVariant @ 50%divider, then an 18dp roundedsurfaceVariant @ 50%pill with "Reply…" placeholder + orangeIcons.Outlined.Editicon. Tap-anywhere on the pill opens compose with the thread's focal (first entry inflatThread) as the reply parent. Disabled until flatThread populates. MatchesThreadView.swift::composer1:1.Publish button gating (
e60fc9f)ComposeScreen.ktPublish button is disabled until the post has at least one non-whitespace character OR at least one uploaded attachment. Prevents accidental empty posts; matches the iOS composer's send-button gating.Overlap with #565
This branch was rebased onto current
origin/main(which includes PR #546's countdown UI). The first 6 commits in this PR's diff (c33447dthrough138701f) are the same conceptual dark-mode polish as PR #565 — they have different SHAs because the two branches were rebased independently. Whichever PR merges second will see those commits collapse to no-ops automatically. The unique commits here are the last 4 (thread + composer).Test plan
🤖 Generated with Claude Code