diff --git a/modules/BlueskyNSE/Info.plist b/modules/BlueskyNSE/Info.plist
index c2dd7eda69..e9271925e0 100644
--- a/modules/BlueskyNSE/Info.plist
+++ b/modules/BlueskyNSE/Info.plist
@@ -8,6 +8,13 @@
com.apple.usernotifications.service
NSExtensionPrincipalClass
$(PRODUCT_MODULE_NAME).NotificationService
+ NSExtensionAttributes
+
+ IntentsSupported
+
+ INSendMessageIntent
+
+
MainAppScheme
bluesky
diff --git a/modules/BlueskyNSE/NotificationService.swift b/modules/BlueskyNSE/NotificationService.swift
index 481402890f..e6ebe8d043 100644
--- a/modules/BlueskyNSE/NotificationService.swift
+++ b/modules/BlueskyNSE/NotificationService.swift
@@ -1,5 +1,6 @@
import UserNotifications
import UIKit
+import Intents
let APP_GROUP = "group.app.bsky"
typealias ContentHandler = (UNNotificationContent) -> Void
@@ -40,17 +41,18 @@ class NotificationService: UNNotificationServiceExtension {
}
self.bestAttempt = bestAttempt
- if reason == "chat-message" {
+
+ if reason == "chat-message" || reason == "chat-reaction" {
mutateWithChatMessage(bestAttempt)
+ let finalContent = createCommunicationNotification(
+ from: bestAttempt,
+ userInfo: request.content.userInfo
+ )
+ contentHandler(finalContent)
} else {
mutateWithBadge(bestAttempt)
+ contentHandler(bestAttempt)
}
-
- // Any image downloading (or other network tasks) should be handled at the end
- // of this block. Otherwise, if there is a timeout and serviceExtensionTimeWillExpire
- // gets called, we might not have all the needed mutations completed in time.
-
- contentHandler(bestAttempt)
}
override func serviceExtensionTimeWillExpire() {
@@ -61,6 +63,81 @@ class NotificationService: UNNotificationServiceExtension {
contentHandler(bestAttempt)
}
+ // MARK: Communication Notification
+
+ func createCommunicationNotification(
+ from content: UNMutableNotificationContent,
+ userInfo: [AnyHashable: Any]
+ ) -> UNNotificationContent {
+ let senderDisplayName = userInfo["senderDisplayName"] as? String ?? "Unknown"
+ let convoId = userInfo["convoId"] as? String
+
+ var avatarImage: INImage? = nil
+ if let avatarUrlString = userInfo["senderAvatarUrl"] as? String {
+ avatarImage = downloadAvatarImage(from: avatarUrlString)
+ }
+
+ let senderHandle = INPersonHandle(value: nil, type: .unknown)
+ let sender = INPerson(
+ personHandle: senderHandle,
+ nameComponents: nil,
+ displayName: senderDisplayName,
+ image: avatarImage,
+ contactIdentifier: nil,
+ customIdentifier: nil
+ )
+
+ let intent = INSendMessageIntent(
+ recipients: nil,
+ outgoingMessageType: .outgoingMessageText,
+ content: content.body,
+ speakableGroupName: nil,
+ conversationIdentifier: convoId,
+ serviceName: nil,
+ sender: sender,
+ attachments: nil
+ )
+
+ let interaction = INInteraction(intent: intent, response: nil)
+ interaction.direction = .incoming
+ interaction.donate(completion: nil)
+
+ do {
+ return try content.updating(from: intent)
+ } catch {
+ return content
+ }
+ }
+
+ func downloadAvatarImage(from urlString: String) -> INImage? {
+ let thumbnailUrlString = urlString.replacingOccurrences(
+ of: "/img/avatar/",
+ with: "/img/avatar_thumbnail/"
+ )
+
+ guard let url = URL(string: thumbnailUrlString) else { return nil }
+
+ var request = URLRequest(url: url)
+ request.timeoutInterval = 5
+
+ var imageData: Data? = nil
+ let semaphore = DispatchSemaphore(value: 0)
+
+ let task = URLSession.shared.dataTask(with: request) { data, response, error in
+ if let data = data,
+ let httpResponse = response as? HTTPURLResponse,
+ httpResponse.statusCode == 200 {
+ imageData = data
+ }
+ semaphore.signal()
+ }
+ task.resume()
+ semaphore.wait()
+
+ guard let data = imageData else { return nil }
+ return INImage(imageData: data)
+ }
+
// MARK: Mutations
func mutateWithBadge(_ content: UNMutableNotificationContent) {
diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt
index 4f8a6b892a..fba23dfa0c 100644
--- a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt
+++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt
@@ -13,7 +13,7 @@ class BackgroundNotificationHandler(
return
}
- if (remoteMessage.data["reason"] == "chat-message") {
+ if (remoteMessage.data["reason"] == "chat-message" || remoteMessage.data["reason"] == "chat-reaction") {
mutateWithChatMessage(remoteMessage)
} else {
mutateWithOtherReason(remoteMessage)
diff --git a/src/lib/hooks/useNotificationHandler.ts b/src/lib/hooks/useNotificationHandler.ts
index 10651f62b8..60d6ebabfe 100644
--- a/src/lib/hooks/useNotificationHandler.ts
+++ b/src/lib/hooks/useNotificationHandler.ts
@@ -29,6 +29,7 @@ export type NotificationReason =
| 'reply'
| 'quote'
| 'chat-message'
+ | 'chat-reaction'
| 'starterpack-joined'
| 'like-via-repost'
| 'repost-via-repost'
@@ -44,7 +45,7 @@ export type NotificationReason =
export type NotificationPayload =
| undefined
| {
- reason: Exclude
+ reason: Exclude
uri: string
subject: string
recipientDid: string
@@ -55,6 +56,12 @@ export type NotificationPayload =
messageId: string
recipientDid: string
}
+ | {
+ reason: 'chat-reaction'
+ convoId: string
+ messageId: string
+ recipientDid: string
+ }
const DEFAULT_HANDLER_OPTIONS = {
shouldShowBanner: false,
@@ -192,8 +199,11 @@ export function useNotificationsHandler() {
const handleNotification = (payload?: NotificationPayload) => {
if (!payload) return
- if (payload.reason === 'chat-message') {
- logger.debug(`useNotificationsHandler: handling chat message`, {
+ if (
+ payload.reason === 'chat-message' ||
+ payload.reason === 'chat-reaction'
+ ) {
+ logger.debug(`useNotificationsHandler: handling chat notification`, {
payload,
})
@@ -270,7 +280,8 @@ export function useNotificationsHandler() {
logger.debug('useNotificationsHandler: incoming', {e, payload})
if (
- payload.reason === 'chat-message' &&
+ (payload.reason === 'chat-message' ||
+ payload.reason === 'chat-reaction') &&
payload.recipientDid === currentAccount?.did
) {
const shouldAlert = payload.convoId !== currentConvoId
@@ -341,7 +352,8 @@ export function useNotificationsHandler() {
// Whenever there's a stored payload, that means we had to switch accounts before handling the notification.
// Whenever currentAccount changes, we should try to handle it again.
if (
- storedAccountSwitchPayload?.reason === 'chat-message' &&
+ (storedAccountSwitchPayload?.reason === 'chat-message' ||
+ storedAccountSwitchPayload?.reason === 'chat-reaction') &&
currentAccount?.did === storedAccountSwitchPayload.recipientDid
) {
handleNotification(storedAccountSwitchPayload)
@@ -429,6 +441,7 @@ export function notificationToURL(payload: NotificationPayload): string | null {
return `/profile/${urip.host}`
}
case 'chat-message':
+ case 'chat-reaction':
// should be handled separately
return null
case 'verified':