diff --git a/Riot/AppDelegate.m b/Riot/AppDelegate.m index b8782185c..a9d651a71 100644 --- a/Riot/AppDelegate.m +++ b/Riot/AppDelegate.m @@ -177,6 +177,12 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN */ NSMutableDictionary *> *eventsToNotify; + /** + Cache for payloads received with incoming push notifications. + The key is the event id. The value, the payload. + */ + NSMutableDictionary *incomingPushPayloads; + /** Currently displayed "Call not supported" alert. */ @@ -395,6 +401,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN callEventsListeners = [NSMutableDictionary dictionary]; notificationListenerBlocks = [NSMutableDictionary dictionary]; eventsToNotify = [NSMutableDictionary dictionary]; + incomingPushPayloads = [NSMutableDictionary dictionary]; // To simplify navigation into the app, we retrieve here the main navigation controller and the tab bar controller. UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController; @@ -539,6 +546,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN { [array removeAllObjects]; } + [incomingPushPayloads removeAllObjects]; // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. @@ -1085,6 +1093,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN if (roomId.length) { // TODO retrieve the right matrix session + // We can use the "user_id" value in notification.userInfo //************** // Patch consider the first session which knows the room id @@ -1165,6 +1174,9 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN { [array addObject:eventId]; } + + // Cache payload for further usage + incomingPushPayloads[eventId] = payload.dictionaryPayload; } else { @@ -1187,9 +1199,12 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN { NSLog(@"[AppDelegate][Push] launchBackgroundSync"); __weak typeof(self) weakSelf = self; + + NSMutableArray *incomingPushEventIds = self.incomingPushEventIds[@(account.mxSession.hash)]; + NSMutableArray *incomingPushEventIdsCopy = [incomingPushEventIds copy]; // Flush all the pending push notifications for this session. - [self.incomingPushEventIds[@(account.mxSession.hash)] removeAllObjects]; + [incomingPushEventIds removeAllObjects]; [account backgroundSync:20000 success:^{ @@ -1210,8 +1225,14 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN } failure:^(NSError *error) { - NSLog(@"[AppDelegate][Push] launchBackgroundSync: the background sync fails"); - + NSLog(@"[AppDelegate][Push] launchBackgroundSync: the background sync failed. Error: %@ (%@). incomingPushEventIdsCopy: %@ - self.incomingPushEventIds: %@", error.domain, @(error.code), incomingPushEventIdsCopy, incomingPushEventIds); + + // Trigger limited local notifications when the sync with HS fails + [self handleLimitedLocalNotifications:account.mxSession events:incomingPushEventIdsCopy]; + + // Update app icon badge number + [self refreshApplicationIconBadgeNumber]; + }]; } } @@ -1238,6 +1259,13 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN NSString *roomId = eventDict[@"room_id"]; BOOL checkReadEvent = YES; MXEvent *event; + + // Ignore event already notified to the user + if ([self displayedLocalNotificationForEvent:eventId andUser:account.mxCredentials.userId type:nil]) + { + NSLog(@"[AppDelegate][Push] handleLocalNotificationsForAccount: Skip event already displayed in a notification. Event id: %@", eventId); + continue; + } if (eventId && roomId) { @@ -1249,7 +1277,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // Ignore redacted event. if (event.isRedactedEvent) { - NSLog(@"[AppDelegate][Push] handleLocalNotificationsForAccount: Skip redacted event"); + NSLog(@"[AppDelegate][Push] handleLocalNotificationsForAccount: Skip redacted event. Event id: %@", event.eventId); continue; } @@ -1259,7 +1287,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // Ignore call invite when callkit is active. if (isCallKitActive) { - NSLog(@"[AppDelegate][Push] handleLocalNotificationsForAccount: Skip call event"); + NSLog(@"[AppDelegate][Push] handleLocalNotificationsForAccount: Skip call event. Event id: %@", event.eventId); continue; } else @@ -1285,7 +1313,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN MXEvent *readReceiptEvent = [account.mxSession.store eventWithEventId:readReceipt.eventId inRoom:roomId]; if (event.originServerTs <= readReceiptEvent.originServerTs) { - NSLog(@"[AppDelegate][Push] handleLocalNotificationsForAccount: Skip already read event"); + NSLog(@"[AppDelegate][Push] handleLocalNotificationsForAccount: Skip already read event. Event id: %@", event.eventId); continue; } } @@ -1303,7 +1331,12 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN UILocalNotification *eventNotification = [[UILocalNotification alloc] init]; eventNotification.alertBody = notificationBody; - eventNotification.userInfo = @{ @"room_id" : event.roomId }; + eventNotification.userInfo = @{ + @"type": @"full", + @"room_id": event.roomId, + @"event_id": event.eventId, + @"user_id": account.mxCredentials.userId + }; // Set sound name based on the value provided in action of MXPushRule for (MXPushRuleAction *action in rule.actions) @@ -1444,6 +1477,127 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN return notificationBody; } +/** + Display "limited" notifications for events the app was not able to get data + (because of /sync failure). + + In this situation, we are only able to display "You received a message in %@". + + @param mxSession the matrix session where the /sync failed. + @param events the list of events id we did not get data. + */ +- (void)handleLimitedLocalNotifications:(MXSession*)mxSession events:(NSArray *)events +{ + NSString *userId = mxSession.matrixRestClient.credentials.userId; + + NSLog(@"[AppDelegate][Push] handleLocalNotificationsForFailedSync: %@", userId); + NSLog(@"[AppDelegate][Push] handleLocalNotificationsForFailedSync: eventsToNotify: %@", eventsToNotify[@(mxSession.hash)]); + NSLog(@"[AppDelegate][Push] handleLocalNotificationsForFailedSync: incomingPushEventIds: %@", self.incomingPushEventIds[@(mxSession.hash)]); + NSLog(@"[AppDelegate][Push] handleLocalNotificationsForFailedSync: events: %@", events); + + if (!events.count) + { + return; + } + + for (NSString *eventId in events) + { + // Ignore event already notified to the user + if ([self displayedLocalNotificationForEvent:eventId andUser:userId type:nil]) + { + NSLog(@"[AppDelegate][Push] handleLocalNotificationsForAccount: Skip event already displayed in a notification. Event id: %@", eventId); + continue; + } + + // Build notification user info + NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ + @"type": @"limited", + @"event_id": eventId, + @"user_id": userId + }]; + + // Add the room_id so that user will open the room when tapping on the notif + NSDictionary *payload = incomingPushPayloads[eventId]; + NSString *roomId = payload[@"room_id"]; + if (roomId) + { + userInfo[@"room_id"] = roomId; + } + else + { + NSLog(@"[AppDelegate][Push] handleLocalNotificationsForFailedSync: room_id is missing for event %@ in payload %@", eventId, payload); + } + + UILocalNotification *localNotificationForFailedSync = [[UILocalNotification alloc] init]; + localNotificationForFailedSync.userInfo = userInfo; + localNotificationForFailedSync.alertBody = [self limitedNotificationBodyForEvent:eventId inMatrixSession:mxSession]; + + NSLog(@"[AppDelegate][Push] handleLocalNotificationsForFailedSync: Display notification for event %@", eventId); + [[UIApplication sharedApplication] scheduleLocalNotification:localNotificationForFailedSync]; + } +} + +/** + Build the body for the "limited" notification to display to the user. + + @param eventId the id of the event the app failed to get data. + @param mxSession the matrix session where the /sync failed. + @return the string to display in the local notification. + */ +- (nullable NSString *)limitedNotificationBodyForEvent:(NSString *)eventId inMatrixSession:(MXSession*)mxSession +{ + NSString *notificationBody; + + NSString *roomDisplayName; + + NSDictionary *payload = incomingPushPayloads[eventId]; + NSString *roomId = payload[@"room_id"]; + if (roomId) + { + MXRoomSummary *roomSummary = [mxSession roomSummaryWithRoomId:roomId]; + if (roomSummary) + { + roomDisplayName = roomSummary.displayname; + } + } + + if (roomDisplayName.length) + { + [NSString stringWithFormat:NSLocalizedString(@"SINGLE_UNREAD_IN_ROOM", nil), roomDisplayName]; + } + else + { + [NSString stringWithFormat:NSLocalizedString(@"SINGLE_UNREAD", nil), roomDisplayName]; + } + + return notificationBody; +} + +/** + Return the already displayed notification for an event. + + @param eventId the id of the event attached to the notification to find. + @param userId the id of the user attached to the notification to find. + @param type the type of notification. @"full" or @"limited". nil for any type. + @return the local notification if any. + */ +- (UILocalNotification*)displayedLocalNotificationForEvent:(NSString*)eventId andUser:(NSString*)userId type:(NSString*)type +{ + UILocalNotification *limitedLocalNotification; + for (UILocalNotification *localNotification in [[UIApplication sharedApplication] scheduledLocalNotifications]) + { + if ([localNotification.userInfo[@"event_id"] isEqualToString:eventId] + && [localNotification.userInfo[@"user_id"] isEqualToString:userId] + && (!type || [localNotification.userInfo[@"type"] isEqualToString:type])) + { + limitedLocalNotification = localNotification; + break; + } + } + + return limitedLocalNotification; +} + - (void)refreshApplicationIconBadgeNumber { // Consider the total number of missed discussions including the invites. @@ -1985,10 +2139,24 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // Check whether some push notifications are pending for this session. if (self.incomingPushEventIds[@(mxSession.hash)].count) { - NSLog(@"[AppDelegate][Push] relaunch a background sync for the kMXSessionStateDidChangeNotification pending incoming push"); + NSLog(@"[AppDelegate][Push] relaunch a background sync for %tu kMXSessionStateDidChangeNotification pending incoming pushes", self.incomingPushEventIds[@(mxSession.hash)].count); [self launchBackgroundSync]; } } + else if (mxSession.state == MXSessionStateInitialSyncFailed) + { + // Display failure sync notifications for pending events if any + if (self.incomingPushEventIds[@(mxSession.hash)].count) + { + NSLog(@"[AppDelegate][Push] initial sync failed with %tu pending incoming pushes", self.incomingPushEventIds[@(mxSession.hash)].count); + + // Trigger limited local notifications when the sync with HS fails + [self handleLimitedLocalNotifications:mxSession events:self.incomingPushEventIds[@(mxSession.hash)]]; + + // Update app icon badge number + [self refreshApplicationIconBadgeNumber]; + } + } } else if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive) { diff --git a/Riot/Assets/en.lproj/Localizable.strings b/Riot/Assets/en.lproj/Localizable.strings index 2c7af440a..33e5d39f4 100644 --- a/Riot/Assets/en.lproj/Localizable.strings +++ b/Riot/Assets/en.lproj/Localizable.strings @@ -44,6 +44,12 @@ /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ posted a picture %@ in %@"; +/* A single unread message in a room */ +"SINGLE_UNREAD_IN_ROOM" = "You received a message in %@"; + +/* A single unread message */ +"SINGLE_UNREAD" = "You received a message"; + /** Coalesced messages **/ /* Multiple unread messages in a room */