feat: reply to chat messages straight from the notification - #75
Conversation
Chat-message pushes now use a dedicated Android "messages" channel and carry a "Reply" text-input action on both platforms, so the user can answer without opening the app. The reply is sent through the existing message.send tRPC mutation from a root-level listener so it works whether the app is foregrounded or backgrounded. Tapping the notification body still deep-links into the right conversation (existing behavior, verified working, left untouched). Server sets channelId/categoryId on the push payload; no native config changed, so no version bump.
… dog's photo (#80) * feat(mobile): render chat pushes as iOS communication notifications Notification Service Extension (Swift, via @bacons/apple-targets) that intercepts chat pushes, downloads the sender dog's avatar and donates an INSendMessageIntent so iOS styles them like iMessage/WhatsApp messages. - server marks chat pushes mutable-content and ships senderName + senderAvatarUrl (first approved photo) in the data payload - communication entitlement wired on both the app and the extension - NSE always falls back to the untouched notification on any failure (missing fields, avatar download, intent donation) Stacks on GSTJ/chat-notifications (reply action + messages channel). * chore: dedupe lockfile
The root listener never sees the response that launched the app. Native buffers it and replays it into the module the moment it is created, which is before any JS listener exists, so the event goes nowhere and only `getLastNotificationResponseAsync` still has it. Replying from a force-closed app therefore parsed the deep link, navigated into the chat and dropped the typed message on the floor. Route reply responses from there too. A failed send now raises a local notification. Submitting the reply dismisses the original notification, so the in-app "failed" bubble has no equivalent once the app is off-screen and the message was vanishing with nothing but an error report behind it. Channel, category and action ids move to @pegada/shared, so the push sender and the app that registers them can no longer drift apart. Claude-Session: https://claude.ai/code/session_01VfiCAFzqYFLkBWfQuQWCvK
b5119be to
15ab9eb
Compare
|
Rebased onto The root listener never sees the response that launched the app. Two smaller things while I was in here: a failed send raises a local notification instead of disappearing (iOS dismisses the original the moment you submit, so there's no bubble left to mark as failed), and the channel, category and action ids moved to The rebase also dropped a duplicate
Plus I couldn't verify the gesture itself. The simulator's SpringBoard ignores synthesised touches on a notification banner, so nothing can type into the Reply field from a script. The screenshots show the push arriving with its category, and the server log shows the request leaving the device once the handler gets a reply response, though I called the handler from a temporary button rather than the OS delivering it. Steps 1-4 still want a real device. |
…124) * Revert "fix(mobile): give the widget and notification-service targets a team id (#109)" This reverts commit 2777a62. * Revert "feat: reply to chat messages straight from the notification (#75)" This reverts commit 69d0d85. * Revert "feat(mobile): home-screen quick actions (#76)" This reverts commit 64210c9. * Revert "feat(mobile): dark iOS icon variant and Android themed icon (#77)" This reverts commit 7721cb0. * Revert "feat: home-screen widgets with matches waiting for a reply (WidgetKit + Glance) (#83)" This reverts commit b375c66. * Revert "feat(mobile): add Liquid Glass to translucent overlays (#78)" This reverts commit 49fbcb3. * chore: sync lockfile after native feature reverts * fix: point restored Glassmorphism at the renamed blur-view module * chore: satisfy oxlint/oxfmt on the restored pre-glass files
Summary
Chat pushes get a Reply action you can answer without opening the app, land on their own "Messages" channel on Android, and render iMessage-style on iOS with the sender dog's photo.
Details
Reply action
categoryId, which is the real expo-server-sdk field name; the docs'categoryIdentifierisn't on the type.message.sendmutation the chat screen uses, from a root-level listener, so no chat screen has to be mounted.getLastNotificationResponseAsyncstill has it. That path parsed the deep link and navigated into the chat without ever sending. Reply responses are handled there too now.Android channel
@pegada/shared, so the push sender and the app that registers them read the same values.iOS communication notifications (this is #80, merged into this branch)
@bacons/apple-targets, that intercepts chat pushes, downloads the sender dog's avatar and donates anINSendMessageIntent.senderNameandsenderAvatarUrl, using only the first APPROVED photo, which also stops unapproved sender images leaking into the payload.Before this ships
com.apple.developer.usernotifications.communicationhas to be enabled on both App IDs, the app and the notification service, in the Apple Developer portal. EAS in non-interactive mode can only reuse an existing provisioning profile, so a release build fails to sign until that's done. I'm the only one with portal access, so that's on me.mainis already on 1.5.0 with no 1.5.0 build behind it, so this rides along with whatever ships that version.Proof
iPhone 17 Pro Max simulator, iOS 26.5, Release build, with
EXPO_PUBLIC_API_URLpointed at a local stub that logs every tRPC call.Chat push arriving with the app backgrounded, and the same notification sitting on the lock screen:
The reply itself I drove from a temporary in-app button that hands
handleReplyActionthe response object expo-notifications produces for a text-input action. The simulator's SpringBoard ignores synthesised touches on a banner, so nothing can type into the real Reply field from a script. That covers everything downstream of the handler getting a reply response, including the request leaving the device:The gesture itself still wants a real device.
Testing steps
Needs a real device and a fresh install, since Android only registers the new channel on first launch.