Skip to content

feat(zap-sheet): iOS-layout rewrite + instant-zap settings + post-card long-press + pin Send#574

Open
dmnyc wants to merge 6 commits into
barrydeen:mainfrom
dmnyc:feat/zap-sheet-and-instant-zap
Open

feat(zap-sheet): iOS-layout rewrite + instant-zap settings + post-card long-press + pin Send#574
dmnyc wants to merge 6 commits into
barrydeen:mainfrom
dmnyc:feat/zap-sheet-and-instant-zap

Conversation

@dmnyc
Copy link
Copy Markdown
Contributor

@dmnyc dmnyc commented May 26, 2026

Largest carve-out in the #556 series. Fuses five originally-separate commits into one branch because they share ZapDialog.kt and InterfaceScreen.kt in non-separable ways — splitting them produces conflicts on every merge.

Commits

  1. feat(zaps): instant-zap settings — InterfacePreferences + Interface UI
    • Adapted from 9ec4fea with NIP-78 AppSettingsRepository sync wiring stripped (deferred to a future phase)
    • New quick-zap accessors in InterfacePreferences: isQuickZapEnabled, getQuickZapAmountSats (clamped 1..10K), getQuickZapAmountFiat, getQuickZapMessage, QUICK_ZAP_MAX_SATS = 10_000L
    • InterfaceScreen gets a new "Zaps" section between Fiat Mode and the Zap Icon toggle, exposing the four settings
  2. feat(zap-sheet): instant-amount seed, in-sheet toggle, caps, confirmation, friendly errors2b482fb
    • Seeds the sheet's amount from the user's quick-zap default
    • In-sheet "Quick zap" toggle mirrors the Interface setting
    • Hard cap at 10K sats; soft confirmation prompt above that
    • Friendly error copy when wallet isn't configured / send fails
  3. feat(post-card): tap composer / long-press instant zap + self-zap disabled754d24f
    • Tap zap button → opens the composer sheet
    • combinedClickable gesture infrastructure with onLongClick and a fired-flag latch to suppress the tap-on-release that Compose normally also fires
    • Self-zap (zap on your own post) is disabled
    • ⚠️ Long-press wiring at call sites is NOT in this PR — see "Known limitation" below
  4. feat(zap-sheet): full iOS-layout rewrite + ModalBottomSheet for drag-dismiss9391dcf
    • Replaces the old custom dialog with a Material 3 ModalBottomSheet
    • Layout matches iOS: amount-first hero input, presets row, recipient row, message field, Send button
    • Drag down to dismiss (Material default behaviour)
  5. fix(zap-sheet): pin Zap button above keyboard + wire recipient on every call site263169b
    • Send button stays pinned above the IME (was getting covered when typing)
    • Recipient resolution wired correctly from each entry point: Feed, ProfileScreen, DM (uses peerProfile in scope), Article, Notifications, Thread, embedded profile shortcuts
  6. fix(interface): apply imePadding so the keyboard doesn't cover input fields
    • On-device fix surfaced during testing: editing the new instant-zap inputs (sats / fiat / message) on the Interface settings screen had the keyboard covering them. Adds Modifier.imePadding() to the scroll column so the field auto-scrolls into view

Known limitation — long-press instant-zap doesn't fire yet

The combinedClickable infrastructure on the post-card zap button is plumbed (commit 3 above), but every PostCard call site still passes onZapLongPress = null. So a long-press currently does nothing. The original 754d24f commit explicitly called this out:

What's NOT in this commit: The actual "instant zap fires the configured amount" wiring at call sites (RichContent's WispActions etc.). The gesture infrastructure is in place; plugging it in requires reaching into ZapSender / WalletViewModel and is a separate, larger commit that touches every call site that constructs WispActions.

The call-site wiring lives in 0be27d4, which is the next PR in the carve-out series (hero-input composer + Edit Presets sheet + per-account presets). Once that lands, long-press will fire as expected from Feed / Profile / Thread / DM / Article / Notifications / Search.

What's NOT included

Files

app/src/main/kotlin/com/wisp/app/Navigation.kt
app/src/main/kotlin/com/wisp/app/repo/InterfacePreferences.kt
app/src/main/kotlin/com/wisp/app/ui/component/ActionBar.kt
app/src/main/kotlin/com/wisp/app/ui/component/PostCard.kt
app/src/main/kotlin/com/wisp/app/ui/component/ZapDialog.kt
app/src/main/kotlin/com/wisp/app/ui/screen/DmConversationScreen.kt
app/src/main/kotlin/com/wisp/app/ui/screen/FeedScreen.kt
app/src/main/kotlin/com/wisp/app/ui/screen/InterfaceScreen.kt
app/src/main/kotlin/com/wisp/app/ui/screen/UserProfileScreen.kt

Test plan (verified on-device)

  • Settings → Interface → Zaps section: toggle on, set sats (try >10K — clamps at 10K), set fiat fallback, set message; values persist after relaunch; keyboard no longer covers the input
  • Post card tap → composer opens (new ModalBottomSheet, iOS layout)
  • Post card long-press → fires instant zap — deferred to next PR (0be27d4)
  • Self-zap disabled
  • Zap sheet drags down to dismiss
  • Send button pinned above keyboard while typing
  • In-sheet toggle mirrors Interface setting (both directions)
  • >10K sats triggers confirmation prompt
  • Friendly error when wallet not configured
  • Recipient resolves from Feed / Profile / DM / Article / Notifications / Thread

Carve-out context

PRs in the series so far: #570 (wallet setup), #571 (wallet dashboard polish), #572 (drop relay-backup status list), #573 (bolt glow pulse). Next and final feature PR: hero-input composer + Edit Presets sheet + per-account presets (0be27d4) — which also lights up long-press at every call site.

dmnyc added 6 commits May 26, 2026 10:52
Adapted from feat/one-tap-zap commit 9ec4fea, with the AppSettingsRepository
sync wiring stripped out (NIP-78 cross-device sync of these prefs is
deferred to a future phase).

InterfacePreferences:
  • isQuickZapEnabled / setQuickZapEnabled
  • getQuickZapAmountSats / setQuickZapAmountSats (clamped 1..10K)
  • getQuickZapAmountFiat / setQuickZapAmountFiat
  • getQuickZapMessage / setQuickZapMessage
  • QUICK_ZAP_MAX_SATS = 10_000L

InterfaceScreen — new "Zaps" / "Payments" section between Fiat Mode and
the Zap Icon toggle, exposing the four settings.

No UI wiring yet for the long-press behavior or the in-sheet toggle —
those come in subsequent commits in this branch.
…tion, friendly errors

Ports the behavioral half of iOS commit barrydeen#4 from feat/one-tap-zap.
The full layout rewrite (hero amount, recipient row, FlowLayout
preset strip, privacy dropdown, hidden TextField with 450ms focus
deferral, register-style cents on every keystroke, EditPresetsSheet,
scroll-dismisses-keyboard) is deferred — the existing ZapDialog
layout still works and the new behaviors are the user-facing
material change.

What landed:

  • **Instant-amount seed on open.** First mount with no
    initialSatsHint pre-fills `customAmount` from
    `interfacePrefs.getQuickZapAmountSats()` (or the fiat
    equivalent in cents), and `message` from
    `interfacePrefs.getQuickZapMessage()`. Treats the configured
    instant-zap amount as the "preferred opening amount" even when
    quick zaps are disabled — matches iOS.

  • **In-sheet Instant-zaps toggle.** New row above the action
    buttons, bound directly to `InterfacePreferences.isQuickZapEnabled`
    so flipping it from the sheet propagates to the post-card
    long-press behavior (and the NIP-78 backup) without navigating
    to settings. Label flips with fiat mode.

  • **1,000,000-sat hard cap.** Zap button disables and a red
    "Max 1,000,000 sats per zap" caption surfaces above the action
    row when the effective amount crosses the cap. Hard cap, not
    a confirmation.

  • **10,000-sat soft confirmation.** Below 10K the Zap button
    fires immediately. At/above 10K it routes through an
    AlertDialog ("Zap N sats? — This is a large amount, double-check
    before sending") with Send / Cancel. Below the cap so users
    can recover from a stray preset tap.

  • **`friendlyZapErrorMessage()` utility.** Mirrors iOS's
    `ZapAnimationStore.friendlyMessage(for:)` substring-match table
    plus the Swift-enum description fallback (extracts `("…")`
    when present). Internal so post-card error pills + future
    layouts can both call it.
…abled

Ports iOS commit barrydeen#5 from feat/one-tap-zap. Splits the zap-glyph
gesture into two paths and renders the button as disabled for the
user's own posts.

ActionBar:
  • New optional `onZapLongPress: (() -> Unit)?` parameter — null
    means "no long-press behavior, tap-only" (existing call sites
    keep working without changes).
  • Zap glyph switches from IconButton to a Box with
    `combinedClickable`, supporting onClick (open composer) AND
    onLongClick (fire instant zap). When `onZapLongPress` is null,
    the long-press handler is omitted entirely so the glyph behaves
    exactly as before.
  • `longPressFired` flag pinned in remember{} — Compose, like
    SwiftUI, fires both onClick AND onLongClick on release of a
    long-press, so the tap handler short-circuits the second fire
    when the flag is set.
  • Disabled tint moved from 0.4f to 0.35f opacity to match the
    iOS self-zap rendering.

PostCard:
  • Plumbs `onZapLongPress` through to ActionBar.
  • Self-zap disabled: `zapEnabled = zapEnabled && !isOwnEvent`
    so the user's own posts render the glyph at low opacity AND
    both tap + long-press are short-circuited (the long-press
    handler returns null when zapEnabled is false in ActionBar).

What's NOT in this commit:
  • The actual "instant zap fires the configured amount" wiring at
    call sites (RichContent's WispActions etc.). The gesture
    infrastructure is in place; plugging it in requires reaching
    into ZapSender / WalletViewModel and is a separate, larger
    commit that touches every call site that constructs WispActions.
…dismiss

Previous pass only added behaviors (instant-amount seed, in-sheet
toggle, caps, confirmation). The layout still used a centered
Dialog that filled the screen and offered no dismiss gesture —
"too tall and impossible to dismiss". This commit rebuilds the
composer from scratch to match the iOS reference screenshot.

Container: switch from `Dialog` to `ModalBottomSheet`. Gives you:
  • Drag handle at the top (Material3 supplies it).
  • Swipe-down dismiss + scrim-tap dismiss.
  • Partial-height presentation so the sheet doesn't take over
    the whole viewport.

Layout (top to bottom, mirrors iOS spec §2.6 of the port doc):
  1. **Toolbar** — "Close" pill on the left, orange-tinted
     "Presets" pill on the right (opens the Save-preset dialog).
  2. **Recipient row** (when `recipientPubkey` + `profileLookup`
     are provided) — 32dp avatar, display name + lud16 stacked,
     trailing copy-icon button that pushes the lud16 to the
     clipboard. Hidden gracefully when no profile data is wired.
  3. **Hero amount** — 56sp orange rounded-bold number with a
     muted-orange unit caption ("sats" or fiat code) underneath.
  4. **Preset strip** — wrapping FlowRow of pills. Last chip is
     `Custom` with an inline + badge that saves the current
     amount as a new preset (disabled at 8-preset max or when
     the amount already exists).
  5. **Custom amount field** — inline OutlinedTextField, only
     visible when the Custom chip is selected. Digit-only.
  6. **Message field** — single-line OutlinedTextField with
     "Message (optional)" placeholder. Preset taps auto-fill
     their default message only when the field is blank, so a
     mid-type tap doesn't clobber what the user wrote.
  7. **Privacy dropdown** — single-row pill with eye / eye-slash
     / lock icons and helper subtext. Material3 DropdownMenu
     opens on tap. Hidden when `forcePrivate` is on.
  8. **Instant zaps toggle** — bound to the existing
     `interfacePrefs.isQuickZapEnabled` setting (and therefore
     to the NIP-78 sync). Flipping it here propagates without
     re-opening Interface settings.
  9. **Zap button** — full-width, accent fill, white bolt + sats
     copy. Disabled when amount is 0 or over the 1M hard cap. At
     >10K it routes through the existing soft-confirmation alert.

Stripped:
  • LightningBackground (decorative animated dots).
  • AnimatedBoltHeader (centered pulsing bolt).
  • drawMiniBolt + bespoke ZapPresetChip / ZapChipButton scaffolding.
None of these matched the iOS reference; the new layout is simpler
and reads cleaner at a glance.

Signature kept compatible — added two optional params
(`recipientPubkey`, `profileLookup`); existing callers keep
working, the recipient row just hides. Wired the FeedScreen and
thread Navigation.kt call sites to pass profile data; remaining
call sites (groups, DM, profile, hashtag, set-feed, article,
notifications) still compile but won't show the recipient row
until they're updated.
…ry call site

Two follow-ups to the ZapSheet rewrite:

1. **Zap button no longer hides behind the keyboard.** The previous
   layout put the Zap button at the end of a single Column inside
   the bottom sheet, so when the amount field focused, the
   keyboard pushed the whole content up and the button went
   off-screen with no scroll to reach it.

   Restructured the sheet body into two rows: a scrollable upper
   region (toolbar, recipient, hero, presets, message, privacy,
   instant-zaps toggle) holding `weight(1f, fill = false)` and a
   pinned lower region with the cap warning + Zap button. The
   outer Column gets `imePadding()` so the whole stack floats
   above the IME — button stays visible, upper region scrolls if
   the keyboard cuts into it.

2. **Recipient row now renders on every call site.** Only 3 of
   ~12 ZapDialog call sites were passing `recipientPubkey` +
   `profileLookup` in the rewrite commit — the row hid silently
   on the other 9. Wired the rest:
     • Navigation.kt — search, hashtag feed, set feed, article,
       live stream (uses streamer override pubkey when set),
       notifications (post + DM target).
     • FeedScreen — zap-poll target.
     • UserProfileScreen — post zap (eventRepo lookup) AND
       profile-direct zap (embedded profile shortcut).
     • DmConversationScreen — uses the peerProfile already in
       scope.
…fields

The Interface settings screen's scroll column had no IME inset handling,
so any text field that takes focus (most visibly the new instant-zap
Amount / Fiat / Message inputs in the Zaps section, but also any future
input on this screen) got covered by the soft keyboard.

Adds Modifier.imePadding() between .padding(padding) and the
verticalScroll. With the IME inset accounted for, the scroll viewport
shrinks to fit above the keyboard and the focused field auto-scrolls
into view as Compose normally handles.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant