Skip to content

feat(notifications): surface tapback/reaction notifications (#2039)#2073

Merged
garthvh merged 4 commits into
meshtastic:mainfrom
bruschill:feat/reaction-notifications-2039
Jul 17, 2026
Merged

feat(notifications): surface tapback/reaction notifications (#2039)#2073
garthvh merged 4 commits into
meshtastic:mainfrom
bruschill:feat/reaction-notifications-2039

Conversation

@bruschill

@bruschill bruschill commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

What changed?

Receiving a tapback/reaction now surfaces a local notification. Previously both notification branches in MeshPackets.textMessageAppPacket (DM and channel) gated on newMessage.isEmoji == false, so a reaction was stored but produced no notification at all — you could only see it with the conversation already open.

Now, when a reaction arrives and the sender/channel isn't muted:

  • The reacted-to message is looked up locally by messageId == replyID.
  • If found, an iMessage-style notification is scheduled, e.g. Alice reacted 👍 to "See you soon".
  • If not found — a "phantom" tapback whose replyID matches nothing locally — the reaction is still stored but no notification is shown, mirroring Android's rememberReaction guard.

Implementation:

  • New MeshPackets.reactionNotificationBody(replyID:emoji:senderName:context:)static + context-injected so it's unit-testable; returns nil for phantom tapbacks, otherwise the localized body.
  • New makeMessageNotification(...) factory collapsing the DM/channel × regular/reaction Notification construction into one path.
  • New localized format key in Localizable.xcstrings.

Why did it change?

This is the iOS side of meshtastic/design#35 (cross-platform reaction-notification alignment). Android already notifies on a received reaction; iOS was silent. The phantom-tapback guard matches the behavior @garthvh raised in that issue — don't notify about a reaction when there's no local message to show it against.

Fixes #2039.

How is this tested?

  • New MeshtasticTests/ReactionNotificationTests.swift7 tests, all passing. Verified on two runtimes:
    • iOS 18.2 (iPhone 16 / iPhone16-iOS18-test) — 7/7 passed
    • iOS 17-era simulator (iPhone 16 Pro Max) — 7/7 passed
  • Coverage:
    • phantom-tapback guard (unknown replyID, replyID == 0, negative replyID) → no notification
    • known original → formatted body (Alice reacted 👍 to "See you soon")
    • empty / nil emoji → ❤️ fallback
    • original present but nil payload → still notifies (not a phantom)
  • The helper was made static + context-injected specifically so the phantom guard and body formatting are exercised directly against an in-memory SwiftData context.
  • SourceKit-LSP diagnostics clean; SwiftLint clean for new code (pre-existing function_body_length/type_body_length on MeshPackets were left as-is, and the touched function shrank ~299 → ~265 lines via the factory extraction).

Explicitly out of scope: unread-count/badge behavior is unchanged. The isEmoji == false filters in ChannelEntityExtension / MyInfoEntityExtension / UserEntityExtension unread counts stay, so a reaction notifies without bumping the conversation's unread badge — matching iMessage and Google Messages per the design#35 analysis.

Screenshots/Videos (when applicable)

N/A — notification-content/behavior change; covered by unit tests above.

Checklist

  • My code adheres to the project's coding and style guidelines.
  • I have conducted a self-review of my code.
  • I have commented my code, particularly in complex areas.
  • I have verified whether these changes require updates to the in-app documentation under docs/user/ or docs/developer/, and updated accordingly. Updated docs/user/messages.md (Tapback Reactions — now describes received-reaction notifications), added a docs/user/whats-new.md entry, and regenerated the bundled HTML under Meshtastic/Resources/docs/ via scripts/build-docs.sh.
  • I have tested the change to ensure that it works as intended.

Summary by CodeRabbit

  • New Features

    • Added notifications for tapback reactions, including the sender, emoji, and original message.
    • Reaction notifications respect muted direct conversations and channels.
    • Added support for targeting the original message when replying or reacting from a notification.
  • Bug Fixes

    • Reactions to messages not previously received are stored silently without notifying or affecting unread badges.
  • Documentation

    • Updated tapback reaction and conversation search guidance.
    • Added the changes to the “What’s New” documentation.

@bruschill bruschill added the skip-docs-check Use this label to skip the automatic docs audit label Jul 11, 2026
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 50aeeac0-a8e9-46ae-94cf-00331ce0f019

📥 Commits

Reviewing files that changed from the base of the PR and between 9a80cb6 and 1dbe01e.

📒 Files selected for processing (7)
  • Localizable.xcstrings
  • Meshtastic/Helpers/MeshPackets.swift
  • Meshtastic/MeshtasticAppDelegate.swift
  • Meshtastic/Resources/docs/index.json
  • Meshtastic/Resources/docs/markdown/user/messages.md
  • Meshtastic/Resources/docs/user/messages.html
  • docs/user/messages.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • Localizable.xcstrings
  • docs/user/messages.md
  • Meshtastic/Resources/docs/markdown/user/messages.md
  • Meshtastic/MeshtasticAppDelegate.swift
  • Meshtastic/Helpers/MeshPackets.swift

📝 Walkthrough

Walkthrough

Tapback reactions now produce localized notifications when the original message is stored locally. Notification actions target the reacted-to message, while cancellation uses the reaction packet. Tests, localization, documentation, and search metadata were updated.

Changes

Reaction notifications

Layer / File(s) Summary
Notification resolution and delivery
Localizable.xcstrings, Meshtastic/Helpers/..., Meshtastic/MeshtasticAppDelegate.swift, Meshtastic/Accessory/...
Reaction bodies resolve original messages, direct and channel notifications respect mute settings, and actionable notifications target replyMessageId.
Reaction behavior validation
MeshtasticTests/ReactionNotificationTests.swift
Tests cover phantom reactions, valid formatting, emoji fallbacks, and missing message payloads.
Reaction notification documentation
Meshtastic/Resources/docs/..., docs/user/...
Guides and release notes describe reaction notifications, mute behavior, unread badges, unseen messages, and updated search controls; documentation metadata is refreshed.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MeshPackets
  participant ModelContext
  participant LocalNotificationManager
  participant MeshtasticAppDelegate
  MeshPackets->>ModelContext: Fetch original message by replyID
  ModelContext-->>MeshPackets: Return stored message or no match
  MeshPackets->>LocalNotificationManager: Schedule localized reaction notification
  LocalNotificationManager->>MeshtasticAppDelegate: Use replyMessageId for actions
Loading

Possibly related issues

  • meshtastic/design#35 — Covers cross-platform tapback notification behavior implemented by this change.

Suggested reviewers: garthvh, rcgv1

Poem

A bunny taps a heart so bright,
“A reaction ping!” it says tonight.
The message trail is safely found,
Replies leap to the proper ground.
Muted burrows make no sound.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: surfacing tapback/reaction notifications, and it matches the linked issue.
Description check ✅ Passed The description follows the template with all required sections, including testing and checklist items.
Linked Issues check ✅ Passed The PR implements #2039 by notifying on non-muted reactions, resolving the original message, and skipping phantom tapbacks.
Out of Scope Changes check ✅ Passed The added docs, tests, localization, and notification plumbing all support the reaction-notification feature rather than unrelated work.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ic#2039)

Receiving a tapback/reaction previously produced no local notification:
both notification branches in textMessageAppPacket gated on
isEmoji == false, so a reaction was stored but only visible with the
conversation already open.

Now, when a reaction arrives and the sender/channel isn't muted, look up
the reacted-to message locally by messageId == replyID and schedule an
iMessage-style notification (e.g. `Alice reacted 👍 to "See you soon"`).
If the original message isn't known locally (a "phantom" tapback whose
replyID matches nothing), the reaction is still stored but no
notification is shown — mirroring Android's rememberReaction guard.

- Add MeshPackets.reactionNotificationBody(replyID:emoji:senderName:context:)
  (static + context-injected so it's unit-testable) which returns nil for
  phantom tapbacks and formats the localized body otherwise.
- Add makeMessageNotification(...) factory to collapse the DM/channel by
  regular/reaction Notification construction into one path.
- Add the reaction body format key to Localizable.xcstrings.

Out of scope (unchanged): unread-count/badge behavior. The isEmoji == false
filters in the *EntityExtension unread counts stay, so a reaction notifies
without bumping the conversation's unread badge (matching iMessage/Google
Messages).

Adds ReactionNotificationTests (7 tests) covering the phantom-tapback guard
(unknown/zero/negative replyID), the formatted body, emoji fallbacks, and
the nil-original-payload-still-notifies edge. Verified on iOS 18.2
(iPhone 16) and iOS 17-era simulators.

Docs: update docs/user/messages.md (Tapback Reactions) to describe received
reaction notifications, add a docs/user/whats-new.md entry, and regenerate
the bundled HTML under Meshtastic/Resources/docs/.
@bruschill
bruschill force-pushed the feat/reaction-notifications-2039 branch from b82500c to daeea8b Compare July 11, 2026 19:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Meshtastic/Helpers/MeshPackets.swift`:
- Line 1444: Update the channelNotificationsEnabled calculation in the
newMessage channel-notification flow to also require that
newMessage.fromUser?.mute is false or unset. Preserve the existing global
notification setting and unmuted-channel checks, ensuring notifications are
suppressed when either the sender or channel is muted.
- Around line 1167-1169: Update the conditional compilation guard around
CarPlayIntentDonation.incomingMessageIntent in the notification handling flow to
require iOS while excluding targetEnvironment(macCatalyst), keeping the donation
enabled for standard iOS builds only.
- Around line 1412-1416: Update the reaction notification construction around
makeMessageNotification so notification metadata keeps messageId for
cancellation while storing the original replyID in a separate field. Adjust the
reaction notification action handlers to read that separate original-reply
identifier, ensuring tapback/reply actions target the original message rather
than the reaction packet.

In `@MeshtasticTests/ReactionNotificationTests.swift`:
- Around line 65-89: Update the reaction notification tests around body(...) to
make formatting deterministic by forcing the English locale/bundle used by
reactionNotificationBody(...), or replace the hard-coded localized assertions
with locale-stable expectations. Apply this consistently to
knownReplyID_formatsBody, knownReplyID_emptyEmoji_fallsBackToHeart,
knownReplyID_nilEmoji_fallsBackToHeart, and
knownReplyID_nilOriginalPayload_stillNotifies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7766e955-d66a-4b18-8838-5466a4fecf1c

📥 Commits

Reviewing files that changed from the base of the PR and between 8e0eaaf and b82500c.

📒 Files selected for processing (4)
  • Localizable.xcstrings
  • Meshtastic.xcodeproj/project.pbxproj
  • Meshtastic/Helpers/MeshPackets.swift
  • MeshtasticTests/ReactionNotificationTests.swift

Comment thread Meshtastic/Helpers/MeshPackets.swift Outdated
Comment thread Meshtastic/Helpers/MeshPackets.swift
Comment thread Meshtastic/Helpers/MeshPackets.swift Outdated
Comment thread MeshtasticTests/ReactionNotificationTests.swift
@bruschill bruschill removed the skip-docs-check Use this label to skip the automatic docs audit label Jul 11, 2026
- Exclude CarPlay donation from Mac Catalyst builds
  (#if os(iOS) && !targetEnvironment(macCatalyst)).
- Split reaction notification metadata: carry replyMessageId separately
  from messageId so tapback/reply actions target the original message,
  not the reaction packet, while cancellation still keys on messageId.
- Honor sender mute for channel notifications.
- Pin reaction-body tests to locale-stable assertions.
)

Follow-up to the reaction-notification review: apply the same
!targetEnvironment(macCatalyst) guard to the incoming and outgoing
CarPlayIntentDonation calls so SiriKit donation is consistently
iPhone-only. CarPlay has no surface on macOS, so donating there is a
no-op; this removes the asymmetry where only the notification-intent
path was Catalyst-excluded. The INInteraction.donate path in
LocalNotificationManager is already inert on Catalyst since senderIntent
is now never set there.
…ications-2039

# Conflicts:
#	Localizable.xcstrings
#	Meshtastic.xcodeproj/project.pbxproj
#	Meshtastic/Resources/docs/index.json
@garthvh
garthvh merged commit 608a2ca into meshtastic:main Jul 17, 2026
4 checks passed
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.

Notify on received tapback/reaction (parity with Android)

2 participants