feat(compose): iOS-parity composer redesign — pills, inline highlights, layout#568
Open
dmnyc wants to merge 5 commits into
Open
feat(compose): iOS-parity composer redesign — pills, inline highlights, layout#568dmnyc wants to merge 5 commits into
dmnyc wants to merge 5 commits into
Conversation
…rydeen#131 + barrydeen#132) Port iOS PRs barrydeen#131 (compose-mention-pills) and barrydeen#132 (mention-relay-fallback) as a single Android commit. ## Visual pills (PR barrydeen#131) - Switch compose BasicTextField from state-based to value-based API so AnnotatedString spans can carry SpanStyle pill backgrounds - buildMentionAnnotatedString() wraps tracked @mention ranges with SpanStyle(background=primary@18%, color=primary, fontWeight=Medium) - contentWithSpans derived from ViewModel content + mentions StateFlow; rebuilds on every mention or text change ## Atomic editing (PR barrydeen#131) - handleAtomicMentionEdit() in ComposeScreen intercepts onValueChange: - Backspace/delete inside pill: deletes entire mention, cursor at start - Typing inside pill: rejected, cursor snapped to pill end - Cursor move (arrow key / tap) into pill: snapped to pill end (right) or start (left), based on direction ## Paste / draft rehydration (PR barrydeen#131) - rehydrateMentionsFromContent() converts pasted nostr:nprofile1/npub1 URIs to @DisplayName + seeds tracked Mention entries, so pasted profile links get pill rendering and materialize correctly at publish - Called in updateContent() (paste detection), init() (process-death draft restore), and loadDraft() - profileRepoRef stored in ViewModel to support rehydration lookup ## Search / compose fixes (PR barrydeen#132) - MentionSearchRepository: normalize all pubkeys to .lowercase() before adding to seenPubkeys set (dedup by canonical hex) - sanitizeMentionDisplay: strip \n/\r from display names before underscore-joining whitespace - materializeMentions: replace ADJACENT_NOSTR_URI_REGEX to insert a space between back-to-back nostr: URIs so parsers see them separately - SearchViewModel.updateQuery / searchAuthors / search: strip leading '@' from query before sending to relay (People search parity)
Layout cleanup after the @mention pill rendering landed. Three classes of bug surfaced on device once typing was actively driving keyboard state: ## Bottom inset / Publish positioning - ComposeScreen lives inside WispNavHost's Scaffold, whose own bottomBar was reserving its height as bottom padding for the screen. Combined with imePadding(), the Publish button sat a full nav-bar height above the keyboard. Add Routes.COMPOSE to hideBottomBarRoutes so the parent bar disappears while composing and ComposeScreen owns the whole bottom. - Replace consumeWindowInsets(navBars).imePadding() with windowInsetsPadding(WindowInsets.ime.union(navigationBars).only(Bottom)) so the keyboard-closed state still clears the gesture indicator and the keyboard-open state pins Publish flush to the IME top. - Bottom-bar Column padding tightened from vertical=12.dp to 8.dp top/bottom for visual breathing room without a dead gap. ## Candidate dropdown placement - Move mention + emoji autocomplete panels from above the BasicTextField to below the toolbar row (iOS layout). Composer + toolbar stay visible while the user is searching instead of being shoved off-screen. - BasicTextField switches from fixed height(160.dp) to heightIn(min=100.dp) so it stays compact when the keyboard is open and grows naturally with multi-line content. ## Bugs uncovered in review - ComposeViewModel.publishNote() non-PoW success path didn't clear _mentions / draft_mentions, leaking stale ranges into the next compose session. Scheduled-publish and PoW paths already did; add it here too. - EmojiVisualTransformation rebuilt the transformed text as a plain AnnotatedString, stripping every SpanStyle from the input — meaning mention pills vanished as soon as a :shortcode: appeared in the note. Switch to AnnotatedString.Builder + text.subSequence(...) so incoming spans carry through the transformation.
Three small polishes to the compose text field that came out of on-device testing of the @mention pill work. ## Borderless composer (matches iOS) Drop OutlinedTextFieldDefaults.DecorationBox in favour of a minimal Box decoration: just the inner text field with a placeholder overlay when the content is empty. No outline, no surface tint — the cursor and pills now sit on the screen background, the way iOS draws it. ## Auto-space after a pill enforceMentionTrailingSpace() runs on every onValueChange. When the user inserts (typing or pasting) text whose first character is a non-whitespace, non-punctuation char exactly at a pill's end offset, a space is slipped in before it. Result: "@alice" + "hi" → "@alice hi". Punctuation (`.,!?;:)]}"'…/-` and newline) is intentionally allowed to abut so users can still write "@alice, hi" or "@alice." without the space. ## Adjacent-pill collision defense preventPillCollision() rejects the deletion of the single space character between two adjacent pill ranges. Removing it would leave the pills visually merged and would force materializeMentions() to inject a separator anyway. The backspace is silently absorbed and the cursor parks at the space position so the user sees the input was a no-op. Both helpers chain in onValueChange before handleAtomicMentionEdit so a single keystroke flows: collision check → atomic-pill edit → trailing-space enforcement → viewmodel update.
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.
DmListScreen's FAB took the M3 default shape (`RoundedCornerShape(16.dp)`) while FeedScreen explicitly set `CircleShape`. Same Scaffold bottom-end slot, same primary container — but the different shape made them look mis-positioned. Add `shape = CircleShape` so both FABs now land 1:1 in position and geometry, with only the glyph (`Edit` vs `GroupAdd`) distinguishing them.
5da57d4 to
414eea6
Compare
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.
Ports the iOS compose-mention-pills work (iOS PRs #131 + #132) and then keeps going on Android-only composer UX issues that surfaced during on-device testing.
Summary
AnnotatedStringwithSpanStylepill backgrounds for tracked mention ranges; atomic-editing handler (backspace nukes the whole pill, typing inside is rejected, cursor snaps out)OutlinedTextFieldDefaults.DecorationBox; cursor + pills sit on the screen background (iOS look)windowInsetsPadding(ime ∪ navigationBars)instead of the originalconsumeWindowInsets(navigationBars).imePadding()combo that under-padded once the parent NavHost's bottom nav was hidden;Routes.COMPOSEadded tohideBottomBarRoutesso the parent nav doesn't fight the IME#hashtagandhttp(s)://…ranges painted in the link colour inside the composer; live preview now uses materialised content so@DisplayNameranges resolve throughRichContentasNostrProfileSegments!imeVisiblegate so the preview updates as you type; "Preview" rendered as a right-aligned pill badge matching iOS.,!?;:)]}\"'…/-and newline are allowed to abut)content.isNotBlank() || uploadedUrls.isNotEmpty()surfaceContainerHighgrey; "Following" rendered as muted grey pill instead of primary textBug fixes uncovered in review
EmojiVisualTransformationrebuilt the transformed text as a plainAnnotatedString, stripping everySpanStylefrom the input — meaning pills vanished as soon as a:shortcode:appeared. Rebuilt withAnnotatedString.Builder+text.subSequence(...)so incoming spans carry through.ComposeViewModel.publishNote()non-PoW success path didn't clear_mentions/draft_mentions(scheduled-publish and PoW paths already did) — stale ranges leaked into the next compose session. Now clears both.MentionSearchRepositorypubkey dedup now lowercases keys defensively.materializeMentions()adjacent-URI regex tightened so a greedy bech32 alphabet doesn't eat the nextnostr:prefix.Test plan
:shortcode:to confirm pill survives the emoji transform,or.right after a pill → no space insertednostr:nprofile1…URIs with proper spacing and both pubkeys inptagsnostr:nprofile1…for a cached profile → rehydrates to a pill#bitcoinandhttps://example.com→ both highlighted in link colour in composer AND preview