FORGE-740 and FORGE-741-Chat Notifications#118
Merged
Bryan Malumphy (bmalumphy) merged 3 commits intomainfrom Mar 17, 2026
Merged
FORGE-740 and FORGE-741-Chat Notifications#118Bryan Malumphy (bmalumphy) merged 3 commits intomainfrom
Bryan Malumphy (bmalumphy) merged 3 commits intomainfrom
Conversation
Contributor
🟢 Test Coverage Report -
|
| Metric | Coverage | Status |
|---|---|---|
| 🟢 Lines | 86.6% | green |
| 🟢 Statements | 86.6% | green |
| 🟢 Functions | 92.45% | green |
| 🟢 Branches | 86.08% | green |
📊 View Detailed Coverage Report
ℹ️ Coverage Thresholds
- 🟢 Excellent (≥ 80%)
- 🟡 Good (60-79%)
- 🟠 Fair (40-59%)
- 🔴 Poor (< 40%)
Contributor
🟢 Test Coverage Report -
|
| Metric | Coverage | Status |
|---|---|---|
| 🟢 Lines | 91.94% | green |
| 🟢 Statements | 91.94% | green |
| 🟢 Functions | 86.11% | green |
| 🟢 Branches | 88.67% | green |
📊 View Detailed Coverage Report
ℹ️ Coverage Thresholds
- 🟢 Excellent (≥ 80%)
- 🟡 Good (60-79%)
- 🟠 Fair (40-59%)
- 🔴 Poor (< 40%)
929e7ee to
461e78a
Compare
Erik Everson (ErikEverson)
approved these changes
Mar 17, 2026
Collaborator
Erik Everson (ErikEverson)
left a comment
There was a problem hiding this comment.
Some things to think about and maybe add a linear ticket for. otherwise LGTM
sdks/kotlin/src/main/java/com/ditto/dittochat/ChatNotificationManager.kt
Show resolved
Hide resolved
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ScreenRecording_03-14-2026.13-54-10_1.MP4
FORGE-740 — Chat Notifications via Local Communications
Summary
Adds automatic local push notifications to the DittoChat Swift SDK. When a new message arrives in any synced room the notification fires as a
UNUserNotificationbanner — no server required. Notifications continue working while the app is backgrounded via a combination of rawDittoStoreObserverretention, BLE background modes, and aBGAppRefreshTaskfor WiFi-only scenarios.Changes
sdks/swift/Sources/DittoChatCore/— SDKPushNotifications/ChatNotificationManager.swift(new)Internal
@MainActorclass that owns oneDittoStoreObserverper synced room.DittoStoreObserver(not the CombineobservePublisherwrapper) so observations are held by the strong[roomId: DittoStoreObserver]dictionary and cannot be cancelled by anAnyCancellablegoing out of scope.deliverOn: .global(qos: .utility)— callback fires immediately on a background thread when Ditto's sync engine has data, without waiting for the main RunLoop. Message parsing happens on the background thread; only the actor-isolated state write hops to.mainviaDispatchQueue.main.async.syncRooms(_ rooms: [Room])reconciles the observed set as the rooms list changes.stopAll()tears down cleanly on logout.PushNotifications/DittoChatPushNotification.swift(new)Public types for APNs (server-side push) integration:
DittoChatNotificationKeyuserInfokey constants (dittoChatRoomId,dittoChatMessageId)DittoChatNotificationActionhandleNotification(userInfo:):.openRoom,.openMessage,.noneDittoChatPushNotificationDelegatedidSendMessageanddidSendImageMessagecallbacks so the host app can forward events to a push serverDittoChat.swiftChatNotificationManagerininitand subscribes it top2pStore.publicRoomsPublishervia a retainedAnyCancellable.notificationManager?.stopAll()inlogout().registerDeviceToken(_ data: Data)— converts raw APNs token to hex, stores indeviceToken.handleNotification(userInfo:) -> DittoChatNotificationAction— interprets an incoming push payload for navigation routing.pushNotificationDelegate— weak property; set via builder.DittoChatBuilder.setPushNotificationDelegate(_:)— optional builder setter.createMessageandcreateImageMessagenow call the delegate after each write.CLAUDE.md(new)Architecture reference for the Swift SDK covering builder pattern, MVVM conventions, Ditto integration patterns, error handling, and the full local + APNs notification architecture.
apps/ios/DittoChatDemo/— Demo AppInfo.plistAdded three entries:
Without
bluetooth-centralandbluetooth-peripheraliOS suspends Ditto's BLE transport immediately on background transition, so no data arrives and no observer ever fires. Without theBGTaskSchedulerPermittedIdentifiersentry,BGTaskSchedulersilently rejects the task submission at runtime.DittoChatDemoApp.swiftAdded
AppDelegate(via@UIApplicationDelegateAdaptor) with two responsibilities:UNUserNotificationCenterDelegatewillPresent— returns.banner + .soundso message notifications display as banners even when the app is already open (iOS suppresses them by default in foreground).didReceive— extractsdittoChatRoomIdfromuserInfoand broadcasts it onNotificationCenter.default(.dittoChatOpenRoom) soContentViewcan navigate to the correct room without needing a direct reference to the AppDelegate.BGTaskSchedulercom.ditto.DittoChatDemo.refreshindidFinishLaunchingWithOptions.applicationDidEnterBackgroundand at the end of each handler so there is always a pending request queued.setTaskCompleted. During that window Ditto reconnects to peers, syncs pending documents, and fires any queuedDittoStoreObservercallbacks — covering WiFi-only scenarios where no BLE event would trigger a natural wakeup.ContentViewModel.swift@MainActor(required becauseDittoChatBuilder.build()is@MainActor).DittoChatinstance moved here as@Published var dittoChat: DittoChat?, created once insetup()alongsideDitto. PreviouslyContentView.bodycreated a newDittoChaton every SwiftUI render withtry!, which would have re-requested notification permissions and spun up duplicate observers on every state change.ContentView.swift@StateObjectsoContentViewModelis owned by (not re-created with) the view.viewModel.dittoChat(stable) instead of inlineDittoChatBuilder().build().NavigationLink+RoomDeepLinkView: listens for.dittoChatOpenRoomonNotificationCenter, setspendingRoomId, activates the link.RoomDeepLinkViewcallsdittoChat.readRoomById(id:)async via.taskand presentsChatScreenonce the room resolves — deep-linking directly into the relevant conversation on notification tap.How It Works End-to-End
Testing Notes
BGAppRefreshTaskwakeup (~15 min, or trigger manually in Xcode viae -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.ditto.DittoChatDemo.refresh"]).