Skip to content

refactor(push): hold a tapped notification natively until JS acts on it - #29678

Merged
chrisnojima merged 11 commits into
nojima/HOTPOT-js-07-livelocationfrom
nojima/HOTPOT-js-08-pushtap
Sep 23, 2026
Merged

chrisnojima merged 11 commits into
nojima/HOTPOT-js-07-livelocationfrom
nojima/HOTPOT-js-08-pushtap

Conversation

@chrisnojima

Copy link
Copy Markdown
Contributor

Stack 6/7 on #29650.

Why

  • A tap can be lost before JS acts on it. Master hands JS a tap as a one-shot event or a read-once initial notification, with iOS re-emitting on become-active and Android polling for a React context. A tap that lands before JS listens or during a reload can be dropped, and a tap that is read is gone even if navigation never happens.
  • A tap for another account goes through a separate pending-push store rather than the navigation intent, and non-tap pushes are handed to JS too.

What this changes

  • Native tap slot on both platforms. A tap replaces any held tap and gets a new id. peekPushTap() returns {payload, id} without clearing it, and ackPushTap(id) clears the slot only if id is still the held one. onPushTapAvailable announces a new tap.
    • Android: notifications open a non-exported PushTapActivity, which fills the slot and starts MainActivity.
    • iOS: didReceive fills the slot. A non-tap push no longer reaches JS. A read-message push that zeroes the badge for the current account clears pending notifications natively.
  • JS:
    • JS subscribes, then peeks. push-tap-resolve.tsx maps the payload to a keybase:// route and target uid.
    • The route is queued as a navigation intent, and the tap is acked when the intent is consumed, superseded or discarded.
    • A tap for another account switches accounts through account-link-switch.tsx.
    • On Android the tap's message is still unboxed with localUnboxMobilePushNotification, once per tap and only when the tap's account is current.
  • Removed: the old bridge surface (getInitialNotification, onPushNotification, engineReset), constants/types/push.tsx and followUser.

Judgment calls

  • A plain link is dropped while a switch for a tapped push is in progress. Only an untargeted link, only within the tap's lifetime; otherwise a new intent supersedes the tap and acks it.
  • The chat.extension mapping is dropped. Nothing on master sends that type; unknown types resolve to no route and are acked at once.
  • A tap on the conversation that is already open is acked without navigating. On a phone that means the conversation is on top; in the split layout, the chat tab is focused with nothing above it.
  • A dropped tap never unboxes later. Its pending unbox is cleared when the tap leaves the queue without its account becoming current.
  • The push-permission prompt wins over a cold tap in getInitialURL, as on master. The tap stays queued and navigates once the router is ready.

Tests

  • push-tap-resolve.test.ts ports the Go resolver table.
  • push-tap.test.ts covers:
    • subscribing before peeking
    • a tap that was held before JS started
    • repeated peeks
    • stale acks
    • acks on consume and on no route
    • one unbox per tap
  • PushTapSlotTest covers the Kotlin slot.
  • The navigation-intents, account-link-switch, intent-consumption (split and phone) and linking tests cover supersede, drop and open-conversation handling.

Mutation-checked.

An untargeted tap still supersedes the waiting one (and acks it), and nothing
is held once no switch is in progress or the tap is older than the intent
lifetime. At launch the push prompt comes before a tapped route again, as on
master; the tap stays queued and the router navigates to it once ready.
On a tablet the open thread is chatRoot's param inside the focused chat
tab, not a chatConversation on the root stack.
…count stores

The native tap ack is handed in by listenForPushTaps and the
switch-in-progress check by account-link-switch, so deep-link-emitter's
only dependency stays a leaf.
The pending unbox now lives only as long as its tap is queued; a tap
that expires, is superseded or is given up on clears it.
The app-state, login, network, dark-mode, nav-state and daemon
subscriptions and the NetInfo listener join the push listeners in
_platformUnsubs.
@chrisnojima
chrisnojima added this pull request to stack #29680 September 23, 2026 19:54
@chrisnojima
chrisnojima removed this pull request from stack #29680 September 23, 2026 19:56
@chrisnojima
chrisnojima added this pull request to stack #29681 September 23, 2026 19:56
@chrisnojima
chrisnojima merged commit 090ed6b into master Sep 23, 2026
1 check passed
@chrisnojima
chrisnojima deleted the nojima/HOTPOT-js-08-pushtap branch September 23, 2026 20:53
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