Skip to content

feat: clear delivered iOS notifications when a channel or DM is read#2069

Open
rancur wants to merge 1 commit into
meshtastic:mainfrom
rancur:feat/clear-delivered-notifications-on-read
Open

feat: clear delivered iOS notifications when a channel or DM is read#2069
rancur wants to merge 1 commit into
meshtastic:mainfrom
rancur:feat/clear-delivered-notifications-on-read

Conversation

@rancur

@rancur rancur commented Jul 10, 2026

Copy link
Copy Markdown

What

Incoming message notifications stayed in iOS Notification Center even after the user had read the messages, so they piled up indefinitely. Reading a conversation only cancelled still-pending notification requests (removePendingNotificationRequests) — it never removed the ones that had already been delivered.

This makes reading a conversation clear its already-delivered notifications:

  • Opening / catching up on a channel now removes that channel's delivered notifications.
  • Opening / catching up on a DM now removes that conversation's delivered notifications.

Why

Users see Meshtastic notifications accumulate on the lock screen / Notification Center long after they've read the messages in-app. Catching up in the app should clear the corresponding notifications.

How

Message notifications are already tagged in userInfo when scheduled (scheduleNotifications in LocalNotificationManager) with the deep-link path, channel index, and sender userNum. Two new helpers query delivered notifications and remove only the matching ones:

  • removeDeliveredNotifications(forChannel:) — matches channel notifications via their ?channelId= path plus the channel index.
  • removeDeliveredNotifications(forDirectMessageUserNum:) — matches DM notifications via their ?userNum= path plus the remote node number.

Because both channel and DM notifications carry a channel and a userNum value, the deep-link path is used to tell the two conversation types apart so a channel and a DM are never cleared for one another. Other notification types (waypoint, low-battery, etc.) use different paths and are untouched.

They're invoked from the existing markMessagesAsRead() paths in ChannelMessageList and UserMessageList, right after the existing pending-cancel call.

Notes

Testing

I was not able to build or run this on a device/simulator in my environment (no full Xcode available), so this has not yet been verified at runtime — relying on CI to compile. The change is small, follows the existing cancelNotificationsForMessageIds pattern in the same file, and reuses userInfo tags that are already populated. Runtime confirmation on a device (generate channel + DM notifications, background the app, open each conversation, confirm only that conversation's notifications clear) would be appreciated before merge. Automated coverage of UNUserNotificationCenter delivery isn't practical in the unit-test target.

Summary by CodeRabbit

  • Bug Fixes
    • Clearing a channel or direct-message conversation as read now also removes its already-delivered notifications.
    • Prevents previously delivered conversation alerts from remaining in Notification Center after messages have been viewed.

Incoming message notifications remained in iOS Notification Center after
the user read the messages, so they piled up indefinitely. Reading a
conversation only cancelled still-pending notification requests, never
the ones already delivered.

When a channel or DM is opened and its messages are marked read, also
remove the matching delivered notifications from Notification Center.
Notifications are matched by the deep-link path already stored in
userInfo (?channelId= vs ?userNum=) plus the channel index / node
number, so channels and DMs are cleared independently and only the
opened conversation is affected. The app icon badge continues to be
driven by the unread counts updated in the same mark-as-read path, so
muted-channel badge behavior is unchanged.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


OpenClaw seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

1 similar comment
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


OpenClaw seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Delivered notifications are now removed for channel and direct-message conversations when their messages are marked as read. Matching uses notification metadata and conversation identifiers.

Changes

Notification cleanup

Layer / File(s) Summary
Delivered notification filters
Meshtastic/Helpers/LocalNotificationManager.swift
Adds channel and direct-message helpers that filter delivered notifications by path and numeric identifier, remove matching requests, and log removal counts.
Read-flow integration
Meshtastic/Views/Messages/ChannelMessageList.swift, Meshtastic/Views/Messages/UserMessageList.swift
Marks delivered notifications as removed when channel or direct-message conversations are marked read, alongside pending notification cancellation.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

A rabbit hops through alerts so bright,
Clearing read notes from day to night.
Channels hush, direct chats too,
Old delivered pings bid adieu.
Thump-thump—clean queues through!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title matches the main change: clearing delivered iOS notifications for channel and DM reads.
Description check ✅ Passed The description covers what changed, why, how, notes, and testing, with only minor template omissions like checklist and screenshots.

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

@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: 2

🧹 Nitpick comments (2)
Meshtastic/Helpers/LocalNotificationManager.swift (2)

148-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate filtering logic between the two new methods.

removeDeliveredNotifications(forChannel:) and removeDeliveredNotifications(forDirectMessageUserNum:) share nearly identical structure (fetch delivered notifications, filter by path substring + numeric field match, log, remove). Consider extracting a shared private helper to reduce duplication and centralize future changes (e.g., to the fetch/removal mechanism).

♻️ Proposed refactor
+	private func removeDeliveredNotifications(matching predicate: `@escaping` (UNNotification) -> Bool, logLabel: String) {
+		UNUserNotificationCenter.current().getDeliveredNotifications { notifications in
+			let identifiers = notifications.filter(predicate).map { $0.request.identifier }
+			if !identifiers.isEmpty {
+				Logger.services.debug("Removing \(identifiers.count, privacy: .public) delivered \(logLabel, privacy: .public) notifications")
+				UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
+			}
+		}
+	}
+
 	func removeDeliveredNotifications(forChannel channelIndex: Int32) {
-		UNUserNotificationCenter.current().getDeliveredNotifications { notifications in
-			let identifiers = notifications.compactMap { notification -> String? in
-				let userInfo = notification.request.content.userInfo
-				guard let path = userInfo["path"] as? String, path.contains("?channelId="),
-					  let channel = (userInfo["channel"] as? NSNumber)?.int32Value,
-					  channel == channelIndex else {
-					return nil
-				}
-				return notification.request.identifier
-			}
-
-			if !identifiers.isEmpty {
-				Logger.services.debug("Removing \(identifiers.count, privacy: .public) delivered channel notifications")
-				UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
-			}
-		}
+		removeDeliveredNotifications(logLabel: "channel") { notification in
+			let userInfo = notification.request.content.userInfo
+			guard let path = userInfo["path"] as? String, path.contains("?channelId="),
+				  let channel = (userInfo["channel"] as? NSNumber)?.int32Value else {
+				return false
+			}
+			return channel == channelIndex
+		}
 	}
🤖 Prompt for 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.

In `@Meshtastic/Helpers/LocalNotificationManager.swift` around lines 148 - 189,
Extract the shared fetch, filtering, logging, and removal flow from
removeDeliveredNotifications(forChannel:) and
removeDeliveredNotifications(forDirectMessageUserNum:) into a private helper
that accepts the path marker, numeric userInfo key, expected value, and
notification-type description. Update both public methods to supply their
specific matching criteria while preserving the existing channel and
direct-message behavior.

148-165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer the async notifications API here. Both methods can use await UNUserNotificationCenter.current().deliveredNotifications() instead of getDeliveredNotifications(completionHandler:), which fits the project’s async/await convention and is available on the supported targets.

🤖 Prompt for 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.

In `@Meshtastic/Helpers/LocalNotificationManager.swift` around lines 148 - 165,
Update removeDeliveredNotifications(forChannel:) and the other
delivered-notifications method to use await
UNUserNotificationCenter.current().deliveredNotifications() instead of
getDeliveredNotifications(completionHandler:). Propagate async as needed through
callers while preserving the existing filtering, logging, and removal behavior.

Source: Coding guidelines

🤖 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/Views/Messages/ChannelMessageList.swift`:
- Around line 94-96: Update docs/user/messages.md to document that opening or
catching up on a channel removes its already-delivered notifications from
Notification Center, reflecting the behavior implemented by
removeDeliveredNotifications(forChannel:) in ChannelMessageList.

In `@Meshtastic/Views/Messages/UserMessageList.swift`:
- Around line 86-88: Document the new user-visible behavior in
docs/user/messages.md: explain that opening a direct-message thread clears its
already-delivered Notification Center notifications, alongside the existing
channel behavior documentation. Update the relevant messages documentation
section without changing the implementation in UserMessageList.

---

Nitpick comments:
In `@Meshtastic/Helpers/LocalNotificationManager.swift`:
- Around line 148-189: Extract the shared fetch, filtering, logging, and removal
flow from removeDeliveredNotifications(forChannel:) and
removeDeliveredNotifications(forDirectMessageUserNum:) into a private helper
that accepts the path marker, numeric userInfo key, expected value, and
notification-type description. Update both public methods to supply their
specific matching criteria while preserving the existing channel and
direct-message behavior.
- Around line 148-165: Update removeDeliveredNotifications(forChannel:) and the
other delivered-notifications method to use await
UNUserNotificationCenter.current().deliveredNotifications() instead of
getDeliveredNotifications(completionHandler:). Propagate async as needed through
callers while preserving the existing filtering, logging, and removal behavior.
🪄 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: 012350d1-0524-4295-95c3-64eba8fd3a39

📥 Commits

Reviewing files that changed from the base of the PR and between 76da012 and 41e836d.

📒 Files selected for processing (3)
  • Meshtastic/Helpers/LocalNotificationManager.swift
  • Meshtastic/Views/Messages/ChannelMessageList.swift
  • Meshtastic/Views/Messages/UserMessageList.swift

Comment on lines +94 to +96
// Also clear any notifications for this channel that were already delivered to Notification
// Center, so catching up on the channel wipes its piled-up notifications.
notificationManager.removeDeliveredNotifications(forChannel: channelIndex)

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Update docs/user/messages.md for this user-visible behavior change.

This change makes delivered notifications disappear from Notification Center when a channel is read — a user-visible behavior change in Meshtastic/Views/Messages/.

As per path instructions, Meshtastic/Views/Messages/**/*.swift: "Update docs/user/messages.md when code in Meshtastic/Views/Messages/ changes in a user-visible way."

🤖 Prompt for 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.

In `@Meshtastic/Views/Messages/ChannelMessageList.swift` around lines 94 - 96,
Update docs/user/messages.md to document that opening or catching up on a
channel removes its already-delivered notifications from Notification Center,
reflecting the behavior implemented by removeDeliveredNotifications(forChannel:)
in ChannelMessageList.

Source: Path instructions

Comment on lines +86 to +88
// Also clear any notifications for this conversation that were already delivered to
// Notification Center, so opening the DM wipes its piled-up notifications.
notificationManager.removeDeliveredNotifications(forDirectMessageUserNum: user.num)

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Update docs/user/messages.md for this user-visible behavior change.

Same as the channel-side change: clearing delivered notifications when a DM thread is opened is a user-visible behavior change under Meshtastic/Views/Messages/.

As per path instructions, Meshtastic/Views/Messages/**/*.swift: "Update docs/user/messages.md when code in Meshtastic/Views/Messages/ changes in a user-visible way."

🤖 Prompt for 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.

In `@Meshtastic/Views/Messages/UserMessageList.swift` around lines 86 - 88,
Document the new user-visible behavior in docs/user/messages.md: explain that
opening a direct-message thread clears its already-delivered Notification Center
notifications, alongside the existing channel behavior documentation. Update the
relevant messages documentation section without changing the implementation in
UserMessageList.

Source: Path instructions

@garthvh

garthvh commented Jul 15, 2026

Copy link
Copy Markdown
Member

Looks good, can merge it once the CLA is signed.

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.

3 participants