Add inline reply and mark-as-read notification actions#644
Add inline reply and mark-as-read notification actions#644
Conversation
Register a MESSAGE_CATEGORY with two actions on push notifications: - Reply: UNTextInputNotificationAction lets users type and send a reply directly from the notification banner/lock screen - Mark as Read: clears delivered notifications for the conversation NotificationAction constants in ConvosCore define the identifiers used by both the main app and the NSE. The NSE sets categoryIdentifier on all decoded message notifications. SessionManager.sendNotificationReply wakes the correct inbox for the conversation, gets a message writer, and sends the reply text. The app delegate handles the action responses in didReceive, routing to the appropriate handler based on actionIdentifier.
|
Claude finished @yewreeka's task —— View job Code Review Complete ✅Todo List:
SummaryThis PR adds inline reply and mark-as-read notification actions to push notifications. The implementation is solid overall, with good architecture and proper error handling. Below are my detailed findings. ✅ Code Quality & Best PracticesStrengths
Minor Style Issues❗ ConvosAppDelegate.swift:156-157 - Empty reply text is handled with guard, but could benefit from user feedback logging: guard !replyText.isEmpty else { return }Consider adding: guard !replyText.isEmpty else {
Log.debug("Ignoring empty notification reply")
return
}🐛 Potential Bugs & Issues1. Race Condition in Inbox Wake + Send FlowSessionManager.swift:544-575 - The await wakeInboxForNotification(clientId: conversation.clientId, inboxId: conversation.inboxId)
let service = try await messagingService(for: conversation.clientId, inboxId: conversation.inboxId)
await service.inboxStateManager.ensureForeground()
for _ in 0..<20 {
if case .ready = service.inboxStateManager.currentState { break }
try await Task.sleep(for: .milliseconds(100))
}Concerns:
Recommendation: // After the loop, verify ready state
guard case .ready = service.inboxStateManager.currentState else {
Log.error("Inbox not ready after 2s wait for notification reply")
throw SessionManagerError.inboxNotReady
}2. Silent Failure on Conversation Not FoundSessionManager.swift:549-552 - When conversation is not found, the method returns silently instead of throwing: guard let conversation = ... else {
Log.error("Cannot reply: conversation \(conversationId) not found")
return // Silent failure
}This was already flagged by Macroscope and marked as "no longer relevant" - let me verify if this was actually fixed: Looking at the latest commit (418007a), the code still returns silently. However, this might be intentional since:
Verdict: This is acceptable defensive programming, but the PR description should note this behavior. 3. Missing MockInboxesService Implementation DetailsMockInboxesService.swift:33 - Mock returns empty implementation: public func sendNotificationReply(text: String, conversationId: String) async throws {}For unit tests, consider tracking calls: public var repliesSent: [(text: String, conversationId: String)] = []
public func sendNotificationReply(text: String, conversationId: String) async throws {
repliesSent.append((text, conversationId))
}🔒 Security Concerns✅ No Security Issues Found
⚡ Performance Considerations1. Notification Action Registration on Every LaunchConvosAppDelegate.swift:18 - func application(_ application: UIApplication, didFinishLaunchingWithOptions ...) -> Bool {
SentryConfiguration.configure()
UNUserNotificationCenter.current().delegate = self
registerNotificationCategories() // Every launchImpact: Minimal - Verdict: ✅ Acceptable pattern (standard iOS practice). 2. Avatar Loading in NSENotificationService.swift:173 & MessagingService+PushNotifications.swift:390-423 - Avatar data is loaded synchronously during notification delivery: let avatarData = await loadSenderAvatarData(
senderInboxId: decodedMessage.senderInboxId,
conversationId: conversationId
)The
Concerns:
Verdict: ✅ Acceptable - failure is handled gracefully, and avatars enhance UX significantly. 🧪 Test CoverageMissing TestsNo unit tests found for:
Recommended TestsSessionManagerTests.swift already has infrastructure for testing. Add: @Test("sendNotificationReply sends message after waking inbox")
func testSendNotificationReply() async throws {
let fixtures = try await makeIntegrationTestFixtures()
// Create inbox and conversation
let (service, _) = await fixtures.sessionManager.addInbox()
let result = try await service.inboxStateManager.waitForInboxReadyResult()
let conversationId = "test-conversation"
// Store conversation in database
try await fixtures.databaseManager.dbWriter.write { db in
try DBConversation(...).insert(db)
}
// Send notification reply
try await fixtures.sessionManager.sendNotificationReply(
text: "Reply text",
conversationId: conversationId
)
// Verify message was sent (check database or mock message writer)
}Suggested test files:
📋 SwiftLint ComplianceChecked against CLAUDE.md rules: ✅ No force unwrapping - Uses safe unwrapping throughout Recommendation: Run 🎯 RecommendationsHigh Priority
Medium Priority
Low Priority
✨ Overall AssessmentThis is a well-implemented feature that follows project conventions and integrates cleanly with the existing notification infrastructure. The architecture is sound, error handling is comprehensive, and security is properly maintained. Verdict: ✅ Approve with minor recommendations The main gap is test coverage for the new functionality. Consider adding tests before merging, especially for the critical |
ApprovabilityVerdict: Needs human review This PR introduces new user-facing notification functionality (inline reply and mark-as-read actions) with new message-sending behavior triggered from notifications. As a new feature adding user-facing capabilities and runtime behavior changes, it warrants human review. You can customize Macroscope's approvability policy. Learn more. |
The inbox may be in backgrounded state when the notification reply fires. Call ensureForeground() and poll for ready state before creating the message writer, preventing missingClientProvider errors from stale XMTP client connections.
When the sender has a profile photo, download and decrypt it during NSE processing and attach it as a UNNotificationAttachment. The avatar appears as a thumbnail next to the notification title. The avatar loading is best-effort: if the sender has no photo, the encrypted image can't be decrypted, or the download fails, the notification is delivered without an avatar. Flow: look up DBMemberProfile for sender -> get encryptedImageRef + avatarKey -> download and decrypt via EncryptedImageLoader -> write to temp file -> attach as JPEG to UNMutableNotificationContent.
Replace UNNotificationAttachment approach (which showed the avatar as a large media preview) with INSendMessageIntent (Communication Notifications). This renders the sender's avatar as a circular overlay on the app icon, matching how iMessage shows contact photos. The NSE creates an INPerson with the sender's display name and decrypted avatar image, donates an INInteraction, and updates the notification content via content.updating(from: intent). Falls back to the standard app icon if no avatar is available.
Changes
Adds two notification actions to message push notifications:
1. Inline Reply
Users can reply directly from the notification banner or lock screen via a
UNTextInputNotificationAction. The reply is sent through the existing messaging pipeline — wakes the correct inbox, gets a message writer, and sends.2. Mark as Read
Clears all delivered notifications for the conversation from the notification center.
Implementation
NotificationActionconstants in ConvosCore define identifiers shared between the main app and NSEcategoryIdentifieron all decoded message notificationsSessionManager.sendNotificationReplyhandles the inbox wake → message write → send flowFiles changed
ConvosCore/Notifications/NotificationAction.swift(new)ConvosCore/Sessions/SessionManager.swiftConvosCore/Sessions/SessionManagerProtocol.swiftConvosCore/Inboxes/MockInboxesService.swiftConvos/ConvosAppDelegate.swiftNotificationService/NotificationService.swiftNote
Add inline reply and mark-as-read actions to push notifications
UNNotificationCategorywith Reply and Mark as Read actions on launch via a newregisterNotificationCategories()helper in ConvosAppDelegate.swift.UNTextInputNotificationResponseand send it viaSessionManager.sendNotificationReply(text:conversationId:), which wakes the inbox and obtains a message writer before sending.handleNotificationTaphelper.DBMemberProfile) and is backed by anINSendMessageIntent, enabling system-level messaging UI in the notification.categoryIdentifierand anINSendMessageIntentdonation, which may affect how iOS groups or displays them.Macroscope summarized 6f17c52.