From 05469207d73819c7604223009ef73236e89c29b8 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Thu, 30 Mar 2023 09:26:56 -0300 Subject: [PATCH] Fix iOS Push notification crashes (#7237) --- app/init/launch.ts | 5 +++-- app/init/push_notifications.ts | 6 ++---- app/screens/in_app_notification/index.tsx | 8 ++++---- .../Gekidou/Networking/Network+Users.swift | 4 ++-- .../PushNotification+ProfileImage.swift | 9 ++++++--- .../PushNotification/PushNotification.swift | 14 +++++++++----- .../Gekidou/Storage/Database+Mentions.swift | 13 +++++++------ .../NotificationService.swift | 17 +++++++++-------- 8 files changed, 42 insertions(+), 34 deletions(-) diff --git a/app/init/launch.ts b/app/init/launch.ts index 3f48028d8..f5f6ebd4b 100644 --- a/app/init/launch.ts +++ b/app/init/launch.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import Emm from '@mattermost/react-native-emm'; -import {Alert, DeviceEventEmitter, Linking, Platform} from 'react-native'; +import {Alert, AppState, DeviceEventEmitter, Linking, Platform} from 'react-native'; import {Notifications} from 'react-native-notifications'; import {appEntry, pushNotificationEntry, upgradeEntry} from '@actions/remote/entry'; @@ -45,7 +45,8 @@ export const initialLaunch = async () => { return launchAppFromNotification(convertToNotificationData(notification!), true); } - return launchApp({launchType: Launch.Normal, coldStart: notification ? tapped : true}); + const coldStart = notification ? (tapped || AppState.currentState === 'active') : true; + return launchApp({launchType: Launch.Normal, coldStart}); }; const launchAppFromDeepLink = async (deepLinkUrl: string, coldStart = false) => { diff --git a/app/init/push_notifications.ts b/app/init/push_notifications.ts index ec37f9e89..5b974c2ca 100644 --- a/app/init/push_notifications.ts +++ b/app/init/push_notifications.ts @@ -25,7 +25,7 @@ import NativeNotifications from '@notifications'; import {getServerDisplayName} from '@queries/app/servers'; import {getCurrentChannelId} from '@queries/servers/system'; import {getIsCRTEnabled, getThreadById} from '@queries/servers/thread'; -import {dismissOverlay, showOverlay} from '@screens/navigation'; +import {showOverlay} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; import NavigationStore from '@store/navigation_store'; import {isBetaApp} from '@utils/general'; @@ -139,6 +139,7 @@ class PushNotifications { const condition3 = isInThreadScreen && !isSameThreadNotification; if (condition1 || condition2 || condition3) { + // Dismiss the screen if it's already visible or else it blocks the navigation DeviceEventEmitter.emit(Navigation.NAVIGATION_SHOW_OVERLAY); const screen = Screens.IN_APP_NOTIFICATION; @@ -148,9 +149,6 @@ class PushNotifications { serverUrl, }; - // Dismiss the screen if it's already visible or else it blocks the navigation - await dismissOverlay(screen); - showOverlay(screen, passProps); } } diff --git a/app/screens/in_app_notification/index.tsx b/app/screens/in_app_notification/index.tsx index 1e2c2e5c2..d69cb63ef 100644 --- a/app/screens/in_app_notification/index.tsx +++ b/app/screens/in_app_notification/index.tsx @@ -30,6 +30,7 @@ type InAppNotificationProps = { } const AUTO_DISMISS_TIME_MILLIS = 5000; +const noInsets = {top: 0, bottom: 0, left: 0, right: 0}; const styles = StyleSheet.create({ container: { @@ -69,11 +70,10 @@ const InAppNotification = ({componentId, serverName, serverUrl, notification}: I const dismissTimerRef = useRef(null); const initial = useSharedValue(-130); const isTablet = useIsTablet(); - let insets = {top: 0}; - if (Platform.OS === 'ios') { + let insets = useSafeAreaInsets(); + if (Platform.OS === 'android') { // on Android we disable the safe area provider as it conflicts with the gesture system - // eslint-disable-next-line react-hooks/rules-of-hooks - insets = useSafeAreaInsets(); + insets = noInsets; } const tapped = useRef(false); diff --git a/ios/Gekidou/Sources/Gekidou/Networking/Network+Users.swift b/ios/Gekidou/Sources/Gekidou/Networking/Network+Users.swift index a77282245..4d7f0b96f 100644 --- a/ios/Gekidou/Sources/Gekidou/Networking/Network+Users.swift +++ b/ios/Gekidou/Sources/Gekidou/Networking/Network+Users.swift @@ -15,7 +15,7 @@ extension Network { // remove existing users in the database users = [User]() let storedUserIds = Database.default.queryUsers(byIds: userIds, forServerUrl: serverUrl) - if !(userIds.filter{ !storedUserIds.contains($0) }).isEmpty { + if !(userIds.subtracting(storedUserIds)).isEmpty { group.enter() DispatchQueue.global(qos: .default).async { self.fetchUsers(byIds: Array(userIds), forServerUrl: serverUrl) {data, response, error in @@ -29,7 +29,7 @@ extension Network { } let storedUsernames = Database.default.queryUsers(byUsernames: usernames, forServerUrl: serverUrl) - if !(usernames.filter{ !storedUsernames.contains($0) }).isEmpty { + if !(usernames.subtracting(storedUsernames)).isEmpty { group.enter() DispatchQueue.global(qos: .default).async { self.fetchUsers(byUsernames: Array(usernames), forServerUrl: serverUrl) {data, response, error in diff --git a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+ProfileImage.swift b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+ProfileImage.swift index c11672e50..2842ba9f9 100644 --- a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+ProfileImage.swift +++ b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification+ProfileImage.swift @@ -6,16 +6,19 @@ extension PushNotification { var updatedAt: Double = 0 func processResponse(data: Data?, response: URLResponse?, error: Error?) { if let httpResponse = response as? HTTPURLResponse { - if (httpResponse.statusCode == 200 && error == nil) { + let statusCode = httpResponse.statusCode + let errorMessage = error?.localizedDescription ?? "" + if (statusCode == 200 && error == nil) { ImageCache.default.insertImage(data, for: senderId, updatedAt: updatedAt, forServer: serverUrl) completionHandler(data) } else { os_log( OSLogType.default, "Mattermost Notifications: Request for profile image failed with status %{public}@ and error %{public}@", - httpResponse.statusCode, - (error?.localizedDescription ?? "") + String(statusCode), + errorMessage ) + completionHandler(nil) } } } diff --git a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification.swift b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification.swift index aefbc389f..4e67f2c46 100644 --- a/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification.swift +++ b/ios/Gekidou/Sources/Gekidou/PushNotification/PushNotification.swift @@ -73,16 +73,19 @@ public class PushNotification: NSObject { let url = Network.default.buildApiUrl(ackNotification.serverUrl, endpoint) Network.default.request( url, withMethod: "POST", withBody: jsonData, - andHeaders: headers, forServerUrl: ackNotification.serverUrl) { data, response, error in - if (error != nil && ackNotification.isIdLoaded) { - let backoffInSeconds = self.fibonacciBackoffsInSeconds[self.retryIndex] - self.retryIndex += 1 + andHeaders: headers, forServerUrl: ackNotification.serverUrl) {[weak self] data, response, error in + if error != nil && ackNotification.isIdLoaded, + let fibonacciBackoffsInSeconds = self?.fibonacciBackoffsInSeconds, + let retryIndex = self?.retryIndex, + fibonacciBackoffsInSeconds.count > retryIndex { + let backoffInSeconds = fibonacciBackoffsInSeconds[retryIndex] + self?.retryIndex += 1 DispatchQueue.main.asyncAfter(deadline: .now() + backoffInSeconds, execute: {[weak self] in os_log( OSLogType.default, "Mattermost Notifications: receipt retrieval failed. Retry %{public}@", - String(describing: self?.retryIndex) + String(retryIndex) ) self?.postNotificationReceiptWithRetry(ackNotification, completionHandler: completionHandler) }) @@ -94,6 +97,7 @@ public class PushNotification: NSObject { } } catch { os_log(OSLogType.default, "Mattermost Notifications: receipt failed %{public}@", error.localizedDescription) + completionHandler(nil) } } diff --git a/ios/Gekidou/Sources/Gekidou/Storage/Database+Mentions.swift b/ios/Gekidou/Sources/Gekidou/Storage/Database+Mentions.swift index 635f4b200..d0e4f3bee 100644 --- a/ios/Gekidou/Sources/Gekidou/Storage/Database+Mentions.swift +++ b/ios/Gekidou/Sources/Gekidou/Storage/Database+Mentions.swift @@ -19,7 +19,7 @@ extension Database { SELECT SUM(my.mentions_count) \ FROM MyChannel my \ INNER JOIN MyChannelSettings mys ON mys.id=my.id \ - INNER JOIN Channel c ON c.id=my.id \ + INNER JOIN Channel c INDEXED BY sqlite_autoindex_Channel_1 ON c.id=my.id \ WHERE c.delete_at = 0 AND mys.notify_props NOT LIKE '%"mark_unread":"mention"%' """ let mentions = try? db.prepare(stmtString).scalar() as? Double @@ -28,11 +28,12 @@ extension Database { public func getThreadMentions(_ db: Connection) -> Int { let stmtString = """ - SELECT SUM(unread_mentions) \ - FROM Thread t - INNER JOIN Post p ON t.id=p.id \ - INNER JOIN Channel c ON p.channel_id=c.id - WHERE c.delete_at = 0 + SELECT SUM(t.unread_mentions) \ + FROM Thread t \ + INNER JOIN Post p INDEXED BY Post_channel_id ON t.id=p.id \ + INNER JOIN Channel c ON p.channel_id=c.id \ + INNER JOIN MyChannelSettings mys ON mys.id=c.id \ + WHERE c.delete_at = 0 AND mys.notify_props NOT LIKE '%"mark_unread":"mention"%' """ let mentions = try? db.prepare(stmtString).scalar() as? Double return Int(mentions ?? 0) diff --git a/ios/NotificationService/NotificationService.swift b/ios/NotificationService/NotificationService.swift index 8ec1f5d77..abb06f6f4 100644 --- a/ios/NotificationService/NotificationService.swift +++ b/ios/NotificationService/NotificationService.swift @@ -27,7 +27,7 @@ class NotificationService: UNNotificationServiceExtension { }) } else { bestAttemptContent.badge = Gekidou.Database.default.getTotalMentions() as NSNumber - os_log(OSLogType.default, "Mattermost Notifications: app in the foreground, no data processed. Will call sendMessageIntent") + os_log(OSLogType.default, "Mattermost Notifications: app in use, no data processed. Will call sendMessageIntent") self?.sendMessageIntent() } return @@ -55,10 +55,8 @@ class NotificationService: UNNotificationServiceExtension { if #available(iOSApplicationExtension 15.0, *) { let overrideUsername = notification.userInfo["override_username"] as? String let senderId = notification.userInfo["sender_id"] as? String - let sender = overrideUsername ?? senderId - guard let serverUrl = notification.userInfo["server_url"] as? String, - let sender = sender + guard let serverUrl = notification.userInfo["server_url"] as? String else { os_log(OSLogType.default, "Mattermost Notifications: No intent created. will call contentHandler to present notification") self.contentHandler?(notification) @@ -66,10 +64,13 @@ class NotificationService: UNNotificationServiceExtension { } let overrideIconUrl = notification.userInfo["override_icon_url"] as? String - os_log(OSLogType.default, "Mattermost Notifications: Fetching profile Image in server %{public}@ for sender %{public}@", serverUrl, sender) - - PushNotification.default.fetchProfileImageSync(serverUrl, senderId: sender, overrideIconUrl: overrideIconUrl) {[weak self] data in - self?.sendMessageIntentCompletion(data) + os_log(OSLogType.default, "Mattermost Notifications: Fetching profile Image in server %{public}@ for sender %{public}@", serverUrl, senderId ?? overrideUsername ?? "no sender is set") + if senderId != nil || overrideIconUrl != nil { + PushNotification.default.fetchProfileImageSync(serverUrl, senderId: senderId ?? "", overrideIconUrl: overrideIconUrl) {[weak self] data in + self?.sendMessageIntentCompletion(data) + } + } else { + self.sendMessageIntentCompletion(nil) } } }