From 47a037f3203556b91a23dc19c599920af17a6d2c Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 2 Aug 2021 16:06:55 +0100 Subject: [PATCH 01/18] Add notice message type. Change an if to a switch to match the one below. --- RiotNSE/NotificationService.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 85bdde3d5..cf6ceb539 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -364,21 +364,23 @@ class NotificationService: UNNotificationServiceExtension { if roomDisplayName != nil && roomDisplayName != eventSenderName { notificationTitle = NSString.localizedUserNotificationString(forKey: "MSG_FROM_USER_IN_ROOM_TITLE", arguments: [eventSenderName as Any, roomDisplayName as Any]) - if msgType == kMXMessageTypeText { + switch msgType { + case kMXMessageTypeText, kMXMessageTypeNotice: notificationBody = messageContent - } else if msgType == kMXMessageTypeEmote { + case kMXMessageTypeEmote: notificationBody = NSString.localizedUserNotificationString(forKey: "ACTION_FROM_USER", arguments: [eventSenderName as Any, messageContent as Any]) - } else if msgType == kMXMessageTypeImage { + case kMXMessageTypeImage: notificationBody = NSString.localizedUserNotificationString(forKey: "IMAGE_FROM_USER", arguments: [eventSenderName as Any, messageContent as Any]) - } else { + default: // Encrypted messages falls here notificationBody = NSString.localizedUserNotificationString(forKey: "MESSAGE", arguments: []) + break } } else { notificationTitle = eventSenderName switch msgType { - case kMXMessageTypeText: + case kMXMessageTypeText, kMXMessageTypeNotice: notificationBody = messageContent break case kMXMessageTypeEmote: From 3f5491a850514f164de23e9303fed98e2b12f877 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 3 Aug 2021 09:38:53 +0100 Subject: [PATCH 02/18] Always use a message event's body instead of "Message". Add the room's title to bestAttemptContents. --- RiotNSE/NotificationService.swift | 57 ++++++++++++------------------- 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index cf6ceb539..95035f0d3 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -189,8 +189,10 @@ class NotificationService: UNNotificationServiceExtension { guard let roomSummary = NotificationService.backgroundSyncService.roomSummary(forRoomId: roomId) else { return } guard let roomDisplayName = roomSummary.displayname else { return } if roomSummary.isDirect == true { + bestAttemptContents[eventId]?.title = roomDisplayName bestAttemptContents[eventId]?.body = NSString.localizedUserNotificationString(forKey: "MESSAGE_FROM_X", arguments: [roomDisplayName as Any]) } else { + bestAttemptContents[eventId]?.title = roomDisplayName bestAttemptContents[eventId]?.body = NSString.localizedUserNotificationString(forKey: "MESSAGE_IN_X", arguments: [roomDisplayName as Any]) } } @@ -326,6 +328,7 @@ class NotificationService: UNNotificationServiceExtension { } else { MXLog.debug("[NotificationService] notificationContent: Do not attempt to send a VoIP push, there is not enough time to process it.") } + #warning("Handle .roomEncrypted separately.") case .roomMessage, .roomEncrypted: if isRoomMentionsOnly { // A local notification will be displayed only for highlighted notification. @@ -345,6 +348,7 @@ class NotificationService: UNNotificationServiceExtension { } if !isHighlighted { + #warning("In practice, this only hides the notification's content. An empty notification may be less useful in this instance?") // Ignore this notif. MXLog.debug("[NotificationService] notificationContentForEvent: Ignore non highlighted notif in mentions only room") onComplete(nil) @@ -352,50 +356,31 @@ class NotificationService: UNNotificationServiceExtension { } } - var msgType = event.content["msgtype"] as? String + let msgType = event.content["msgtype"] as? String let messageContent = event.content["body"] as? String - if event.isEncrypted && !self.showDecryptedContentInNotifications { - // Hide the content - msgType = nil - } - // Display the room name only if it is different than the sender name if roomDisplayName != nil && roomDisplayName != eventSenderName { notificationTitle = NSString.localizedUserNotificationString(forKey: "MSG_FROM_USER_IN_ROOM_TITLE", arguments: [eventSenderName as Any, roomDisplayName as Any]) - - switch msgType { - case kMXMessageTypeText, kMXMessageTypeNotice: - notificationBody = messageContent - case kMXMessageTypeEmote: - notificationBody = NSString.localizedUserNotificationString(forKey: "ACTION_FROM_USER", arguments: [eventSenderName as Any, messageContent as Any]) - case kMXMessageTypeImage: - notificationBody = NSString.localizedUserNotificationString(forKey: "IMAGE_FROM_USER", arguments: [eventSenderName as Any, messageContent as Any]) - default: - // Encrypted messages falls here - notificationBody = NSString.localizedUserNotificationString(forKey: "MESSAGE", arguments: []) - break - } } else { notificationTitle = eventSenderName - - switch msgType { - case kMXMessageTypeText, kMXMessageTypeNotice: - notificationBody = messageContent - break - case kMXMessageTypeEmote: - notificationBody = NSString.localizedUserNotificationString(forKey: "ACTION_FROM_USER", arguments: [eventSenderName as Any, messageContent as Any]) - break - case kMXMessageTypeImage: - notificationBody = NSString.localizedUserNotificationString(forKey: "IMAGE_FROM_USER", arguments: [eventSenderName as Any, messageContent as Any]) - break - default: - // Encrypted messages falls here - notificationBody = NSString.localizedUserNotificationString(forKey: "MESSAGE", arguments: []) - break - } } - break + + if event.isEncrypted && !self.showDecryptedContentInNotifications { + // Hide the content + notificationBody = NSString.localizedUserNotificationString(forKey: "MESSAGE", arguments: []) + break + } + + switch msgType { + case kMXMessageTypeEmote: + notificationBody = NSString.localizedUserNotificationString(forKey: "ACTION_FROM_USER", arguments: [eventSenderName as Any, messageContent as Any]) + case kMXMessageTypeImage: + notificationBody = NSString.localizedUserNotificationString(forKey: "IMAGE_FROM_USER", arguments: [eventSenderName as Any, messageContent as Any]) + default: + // All other message types such as text, notice etc + notificationBody = messageContent + } case .roomMember: if roomDisplayName != nil && roomDisplayName != eventSenderName { notificationBody = NSString.localizedUserNotificationString(forKey: "USER_INVITE_TO_NAMED_ROOM", arguments: [eventSenderName as Any, roomDisplayName as Any]) From 06041c8c2b751c56015c5cf43b715444e85174a0 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 3 Aug 2021 14:55:06 +0100 Subject: [PATCH 03/18] Add fallback notification for reactions. Add fallback notifications for member displayname and avatar changes. --- Riot/Assets/en.lproj/Localizable.strings | 16 +++++++ RiotNSE/NotificationService.swift | 58 ++++++++++++++++++------ 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/Riot/Assets/en.lproj/Localizable.strings b/Riot/Assets/en.lproj/Localizable.strings index 6541338bd..535f6abcc 100644 --- a/Riot/Assets/en.lproj/Localizable.strings +++ b/Riot/Assets/en.lproj/Localizable.strings @@ -92,6 +92,11 @@ /* Look, stuff's happened, alright? Just open the app. */ "MSGS_IN_TWO_PLUS_ROOMS" = "%@ new messages in %@, %@ and others"; +/** Reactions **/ + +/* A user has reacted to a message */ +"REACTION_FROM_USER" = "%@ sent a reaction" + /** Invites **/ /* A user has invited you to a chat */ @@ -103,6 +108,17 @@ /* A user has invited you to a named room */ "USER_INVITE_TO_NAMED_ROOM" = "%@ has invited you to %@"; +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ changed their name to %@"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ changed their avatar"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ updated their details"; + /** Calls **/ /* Incoming one-to-one voice call */ diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 95035f0d3..2f40a2c55 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -359,12 +359,7 @@ class NotificationService: UNNotificationServiceExtension { let msgType = event.content["msgtype"] as? String let messageContent = event.content["body"] as? String - // Display the room name only if it is different than the sender name - if roomDisplayName != nil && roomDisplayName != eventSenderName { - notificationTitle = NSString.localizedUserNotificationString(forKey: "MSG_FROM_USER_IN_ROOM_TITLE", arguments: [eventSenderName as Any, roomDisplayName as Any]) - } else { - notificationTitle = eventSenderName - } + notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) if event.isEncrypted && !self.showDecryptedContentInNotifications { // Hide the content @@ -382,19 +377,38 @@ class NotificationService: UNNotificationServiceExtension { notificationBody = messageContent } case .roomMember: - if roomDisplayName != nil && roomDisplayName != eventSenderName { - notificationBody = NSString.localizedUserNotificationString(forKey: "USER_INVITE_TO_NAMED_ROOM", arguments: [eventSenderName as Any, roomDisplayName as Any]) + // If the user is already joined, display updated displayname/avatar events. + if NotificationService.backgroundSyncService.roomSummary(forRoomId: roomId)?.membership == .join { + notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) + + if event.content["membership"] as? String == "join", + let prevContent = event.prevContent, + prevContent["membership"] as? String == "join" { + if let displayname = event.content["displayname"] as? String, + let oldDisplayname = prevContent["displayname"] as? String, + displayname != oldDisplayname { + notificationBody = NSString.localizedUserNotificationString(forKey: "USER_UPDATED_DISPLAYNAME", arguments: [oldDisplayname, displayname]) + } else { + notificationBody = NSString.localizedUserNotificationString(forKey: "USER_UPDATED_AVATAR", arguments: [eventSenderName]) + } + } else { + #warning("Join and leave?") + notificationBody = NSString.localizedUserNotificationString(forKey: "USER_MEMBERSHIP_UPDATED", arguments: [eventSenderName]) + } + // Otherwise treat the notification as an invite. } else { - notificationBody = NSString.localizedUserNotificationString(forKey: "USER_INVITE_TO_CHAT", arguments: [eventSenderName as Any]) + if roomDisplayName != nil && roomDisplayName != eventSenderName { + notificationBody = NSString.localizedUserNotificationString(forKey: "USER_INVITE_TO_NAMED_ROOM", arguments: [eventSenderName, roomDisplayName as Any]) + } else { + notificationBody = NSString.localizedUserNotificationString(forKey: "USER_INVITE_TO_CHAT", arguments: [eventSenderName]) + } } case .sticker: - if roomDisplayName != nil && roomDisplayName != eventSenderName { - notificationTitle = NSString.localizedUserNotificationString(forKey: "MSG_FROM_USER_IN_ROOM_TITLE", arguments: [eventSenderName as Any, roomDisplayName as Any]) - } else { - notificationTitle = eventSenderName - } - + notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) notificationBody = NSString.localizedUserNotificationString(forKey: "STICKER_FROM_USER", arguments: [eventSenderName as Any]) + case .reaction: + notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) + notificationBody = NSString.localizedUserNotificationString(forKey: "REACTION_FROM_USER", arguments: [eventSenderName]) case .custom: if (event.type == kWidgetMatrixEventTypeString || event.type == kWidgetModularEventTypeString), let type = event.content?["type"] as? String, @@ -444,6 +458,20 @@ class NotificationService: UNNotificationServiceExtension { }) } + /// Returns the default title for message notifications. + /// - Parameters: + /// - eventSenderName: The displayname of the sender. + /// - roomDisplayName: The displayname of the room the message was sent in. + /// - Returns: A string to be used for the notification's title. + private func messageTitle(for eventSenderName: String, in roomDisplayName: String?) -> String { + // Display the room name only if it is different than the sender name + if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName { + return NSString.localizedUserNotificationString(forKey: "MSG_FROM_USER_IN_ROOM_TITLE", arguments: [eventSenderName, roomDisplayName]) + } else { + return eventSenderName + } + } + /// Get the context of an event. /// - Parameters: /// - event: the event From 74a9205c7246bc478f972ca91ca7135d3c315899 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 4 Aug 2021 14:18:18 +0100 Subject: [PATCH 04/18] Use the body text "Notification" instead of message as the event may not be a message. --- Riot/Assets/en.lproj/Localizable.strings | 8 +++++++- RiotNSE/NotificationService.swift | 24 ++++++++++++------------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Riot/Assets/en.lproj/Localizable.strings b/Riot/Assets/en.lproj/Localizable.strings index 535f6abcc..3bc9a5c30 100644 --- a/Riot/Assets/en.lproj/Localizable.strings +++ b/Riot/Assets/en.lproj/Localizable.strings @@ -14,6 +14,12 @@ limitations under the License. */ +/** General **/ + +"NOTIFICATION" = "Notification"; + +/** Titles **/ + /* Message title for a specific person in a named room */ "MSG_FROM_USER_IN_ROOM_TITLE" = "%@ in %@"; @@ -95,7 +101,7 @@ /** Reactions **/ /* A user has reacted to a message */ -"REACTION_FROM_USER" = "%@ sent a reaction" +"REACTION_FROM_USER" = "%@ sent a reaction"; /** Invites **/ diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 2f40a2c55..065fff78f 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -108,6 +108,9 @@ class NotificationService: UNNotificationServiceExtension { // no need to check before, if it's nil, the badge will remain unchanged content.badge = userInfo["unread_count"] as? NSNumber + // update the fallback body to read "Notification" which aligns with the system body when previews are hidden. + content.body = NSString.localizedUserNotificationString(forKey: "NOTIFICATION", arguments: []) + bestAttemptContents[eventId] = content contentHandlers[eventId] = contentHandler @@ -186,15 +189,13 @@ class NotificationService: UNNotificationServiceExtension { MXLog.debug("[NotificationService] preprocessPayload: Do not preprocess because app protection is set") return } + + // If a room summary is available, use the displayname for the best attempt title. guard let roomSummary = NotificationService.backgroundSyncService.roomSummary(forRoomId: roomId) else { return } guard let roomDisplayName = roomSummary.displayname else { return } - if roomSummary.isDirect == true { - bestAttemptContents[eventId]?.title = roomDisplayName - bestAttemptContents[eventId]?.body = NSString.localizedUserNotificationString(forKey: "MESSAGE_FROM_X", arguments: [roomDisplayName as Any]) - } else { - bestAttemptContents[eventId]?.title = roomDisplayName - bestAttemptContents[eventId]?.body = NSString.localizedUserNotificationString(forKey: "MESSAGE_IN_X", arguments: [roomDisplayName as Any]) - } + bestAttemptContents[eventId]?.title = roomDisplayName + + // At this stage we don't know the message type, so leave the body as set in didReceive. } private func fetchEvent(withEventId eventId: String, roomId: String, allowSync: Bool = true) { @@ -242,10 +243,10 @@ class NotificationService: UNNotificationServiceExtension { isUnwantedNotification = true } - // modify the best attempt content, to be able to use in future - self.bestAttemptContents[event.eventId] = content - if self.ongoingVoIPPushRequests[event.eventId] == true { + // modify the best attempt content, to be able to use in the future + self.bestAttemptContents[event.eventId] = content + // There is an ongoing VoIP Push request for this event, wait for it to be completed. // When it completes, it'll continue with the bestAttemptContent. return @@ -377,7 +378,7 @@ class NotificationService: UNNotificationServiceExtension { notificationBody = messageContent } case .roomMember: - // If the user is already joined, display updated displayname/avatar events. + // If the current user is already joined, display updated displayname/avatar events. if NotificationService.backgroundSyncService.roomSummary(forRoomId: roomId)?.membership == .join { notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) @@ -392,7 +393,6 @@ class NotificationService: UNNotificationServiceExtension { notificationBody = NSString.localizedUserNotificationString(forKey: "USER_UPDATED_AVATAR", arguments: [eventSenderName]) } } else { - #warning("Join and leave?") notificationBody = NSString.localizedUserNotificationString(forKey: "USER_MEMBERSHIP_UPDATED", arguments: [eventSenderName]) } // Otherwise treat the notification as an invite. From 1e10839fe452cb8903de8265e7a381bef6675613 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 4 Aug 2021 14:34:23 +0100 Subject: [PATCH 05/18] Use the notification fallback content when an event couldn't be decrypted. --- RiotNSE/NotificationService.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 065fff78f..a48473b6e 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -329,8 +329,10 @@ class NotificationService: UNNotificationServiceExtension { } else { MXLog.debug("[NotificationService] notificationContent: Do not attempt to send a VoIP push, there is not enough time to process it.") } - #warning("Handle .roomEncrypted separately.") - case .roomMessage, .roomEncrypted: + case .roomEncrypted: + // If unable to decrypt the event, use the fallback. + break + case .roomMessage: if isRoomMentionsOnly { // A local notification will be displayed only for highlighted notification. var isHighlighted = false From f090cc9db753987b70d1b628fd72e96196da5bb1 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 4 Aug 2021 15:25:16 +0100 Subject: [PATCH 06/18] Strip the original message from reply notifications. --- Riot/Assets/en.lproj/Localizable.strings | 7 ++++++ RiotNSE/NotificationService.swift | 27 +++++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/en.lproj/Localizable.strings b/Riot/Assets/en.lproj/Localizable.strings index 3bc9a5c30..908bff02b 100644 --- a/Riot/Assets/en.lproj/Localizable.strings +++ b/Riot/Assets/en.lproj/Localizable.strings @@ -73,8 +73,15 @@ /* New message indicator on a room */ "MESSAGE_IN_X" = "Message in %@"; +/* New message with hidden content due to PIN enabled */ "MESSAGE_PROTECTED" = "New Message"; +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER" = "%@ replied"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ replied in %@"; + /** Coalesced messages **/ /* Multiple unread messages in a room */ diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index a48473b6e..b2e73b9e2 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -361,8 +361,13 @@ class NotificationService: UNNotificationServiceExtension { let msgType = event.content["msgtype"] as? String let messageContent = event.content["body"] as? String + let isReply = event.isReply() - notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) + if isReply { + notificationTitle = self.replyTitle(for: eventSenderName, in: roomDisplayName) + } else { + notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) + } if event.isEncrypted && !self.showDecryptedContentInNotifications { // Hide the content @@ -375,9 +380,16 @@ class NotificationService: UNNotificationServiceExtension { notificationBody = NSString.localizedUserNotificationString(forKey: "ACTION_FROM_USER", arguments: [eventSenderName as Any, messageContent as Any]) case kMXMessageTypeImage: notificationBody = NSString.localizedUserNotificationString(forKey: "IMAGE_FROM_USER", arguments: [eventSenderName as Any, messageContent as Any]) + + // All other message types such as text, notice etc default: - // All other message types such as text, notice etc - notificationBody = messageContent + if event.isReply() { + let parser = MXReplyEventParser() + let replyParts = parser.parse(event) + notificationBody = replyParts.bodyParts.replyText + } else { + notificationBody = messageContent + } } case .roomMember: // If the current user is already joined, display updated displayname/avatar events. @@ -474,6 +486,15 @@ class NotificationService: UNNotificationServiceExtension { } } + private func replyTitle(for eventSenderName: String, in roomDisplayName: String?) -> String { + // Display the room name only if it is different than the sender name + if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName { + return NSString.localizedUserNotificationString(forKey: "REPLY_FROM_USER_IN_ROOM_TITLE", arguments: [eventSenderName, roomDisplayName]) + } else { + return NSString.localizedUserNotificationString(forKey: "REPLY_FROM_USER", arguments: [eventSenderName]) + } + } + /// Get the context of an event. /// - Parameters: /// - event: the event From 6b53ba15e791438f4518cd26e6484c4da6213c24 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 4 Aug 2021 15:31:22 +0100 Subject: [PATCH 07/18] Move strings. --- Riot/Assets/en.lproj/Localizable.strings | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Riot/Assets/en.lproj/Localizable.strings b/Riot/Assets/en.lproj/Localizable.strings index 908bff02b..b4ccee26d 100644 --- a/Riot/Assets/en.lproj/Localizable.strings +++ b/Riot/Assets/en.lproj/Localizable.strings @@ -23,6 +23,12 @@ /* Message title for a specific person in a named room */ "MSG_FROM_USER_IN_ROOM_TITLE" = "%@ in %@"; +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ replied"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ replied in %@"; + /** Single, end-to-end encrypted messages (ie. we don't know what they say) */ /* New message from a specific person, not referencing a room */ @@ -76,12 +82,6 @@ /* New message with hidden content due to PIN enabled */ "MESSAGE_PROTECTED" = "New Message"; -/* New message reply from a specific person, not referencing a room. */ -"REPLY_FROM_USER" = "%@ replied"; - -/* New message reply from a specific person in a named room. */ -"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ replied in %@"; - /** Coalesced messages **/ /* Multiple unread messages in a room */ From 00231feb49d25ffed40506567d2b010d711f6da5 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 4 Aug 2021 16:08:33 +0100 Subject: [PATCH 08/18] Add notification body tweaks for video, audio and file messages. --- Riot/Assets/en.lproj/Localizable.strings | 15 ++++++++++++--- RiotNSE/NotificationService.swift | 12 +++++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Riot/Assets/en.lproj/Localizable.strings b/Riot/Assets/en.lproj/Localizable.strings index b4ccee26d..ee2b62a28 100644 --- a/Riot/Assets/en.lproj/Localizable.strings +++ b/Riot/Assets/en.lproj/Localizable.strings @@ -51,14 +51,23 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; -/** Image Messages **/ +/** Media Messages **/ -/* New action message from a specific person, not referencing a room. */ +/* New image message from a specific person, not referencing a room. */ "IMAGE_FROM_USER" = "%@ sent a picture %@"; -/* New action message from a specific person in a named room. */ +/* New image message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ posted a picture %@ in %@"; +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ sent a video %@"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ sent an audio file %@"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ sent a file %@"; + /* A single unread message in a room */ "SINGLE_UNREAD_IN_ROOM" = "You received a message in %@"; diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index b2e73b9e2..d821ab8ae 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -377,11 +377,17 @@ class NotificationService: UNNotificationServiceExtension { switch msgType { case kMXMessageTypeEmote: - notificationBody = NSString.localizedUserNotificationString(forKey: "ACTION_FROM_USER", arguments: [eventSenderName as Any, messageContent as Any]) + notificationBody = NSString.localizedUserNotificationString(forKey: "ACTION_FROM_USER", arguments: [eventSenderName, messageContent as Any]) case kMXMessageTypeImage: - notificationBody = NSString.localizedUserNotificationString(forKey: "IMAGE_FROM_USER", arguments: [eventSenderName as Any, messageContent as Any]) + notificationBody = NSString.localizedUserNotificationString(forKey: "IMAGE_FROM_USER", arguments: [eventSenderName, messageContent as Any]) + case kMXMessageTypeVideo: + notificationBody = NSString.localizedUserNotificationString(forKey: "VIDEO_FROM_USER", arguments: [eventSenderName, messageContent as Any]) + case kMXMessageTypeAudio: + notificationBody = NSString.localizedUserNotificationString(forKey: "AUDIO_FROM_USER", arguments: [eventSenderName, messageContent as Any]) + case kMXMessageTypeFile: + notificationBody = NSString.localizedUserNotificationString(forKey: "FILE_FROM_USER", arguments: [eventSenderName, messageContent as Any]) - // All other message types such as text, notice etc + // All other message types such as text, notice, server notice etc default: if event.isReply() { let parser = MXReplyEventParser() From 853a10bf4b1c22c74a7477f43483518b84aa2321 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 4 Aug 2021 16:14:02 +0100 Subject: [PATCH 09/18] Add voice message notification body. --- Riot/Assets/en.lproj/Localizable.strings | 3 +++ RiotNSE/NotificationService.swift | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/en.lproj/Localizable.strings b/Riot/Assets/en.lproj/Localizable.strings index ee2b62a28..c668ee02c 100644 --- a/Riot/Assets/en.lproj/Localizable.strings +++ b/Riot/Assets/en.lproj/Localizable.strings @@ -65,6 +65,9 @@ /* New audio message from a specific person, not referencing a room. */ "AUDIO_FROM_USER" = "%@ sent an audio file %@"; +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ sent a voice message"; + /* New file message from a specific person, not referencing a room. */ "FILE_FROM_USER" = "%@ sent a file %@"; diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index d821ab8ae..b053fe113 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -383,7 +383,11 @@ class NotificationService: UNNotificationServiceExtension { case kMXMessageTypeVideo: notificationBody = NSString.localizedUserNotificationString(forKey: "VIDEO_FROM_USER", arguments: [eventSenderName, messageContent as Any]) case kMXMessageTypeAudio: - notificationBody = NSString.localizedUserNotificationString(forKey: "AUDIO_FROM_USER", arguments: [eventSenderName, messageContent as Any]) + if event.isVoiceMessage() { + notificationBody = NSString.localizedUserNotificationString(forKey: "VOICE_MESSAGE_FROM_USER", arguments: [eventSenderName]) + } else { + notificationBody = NSString.localizedUserNotificationString(forKey: "AUDIO_FROM_USER", arguments: [eventSenderName, messageContent as Any]) + } case kMXMessageTypeFile: notificationBody = NSString.localizedUserNotificationString(forKey: "FILE_FROM_USER", arguments: [eventSenderName, messageContent as Any]) From 89b5146efde2d25828596d6c0b37efe696ff09ec Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 4 Aug 2021 17:04:42 +0100 Subject: [PATCH 10/18] Update CHANGES.rst. --- CHANGES.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index c7d5e0920..bb6150123 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,10 +5,12 @@ Changes to be released in next version * 🙌 Improvements - * + * Notifications: Replies now hide the referenced content (#4660). + * Notifications: Show the body of all message event types (#4653). + * Notifications: Replace "Message" fallback with "Notification" as the event may not be a message. 🐛 Bugfix - * + * Notifications: Name/Avatar changes were displayed as invites (#4659). ⚠️ API Changes * From 0a926295ee65d1e6a026a03df9e8c8bb6877f4a1 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 10 Aug 2021 10:28:30 +0100 Subject: [PATCH 11/18] Revert "Update CHANGES.rst." This reverts commit 89b5146efde2d25828596d6c0b37efe696ff09ec. --- CHANGES.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index bb6150123..c7d5e0920 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,12 +5,10 @@ Changes to be released in next version * 🙌 Improvements - * Notifications: Replies now hide the referenced content (#4660). - * Notifications: Show the body of all message event types (#4653). - * Notifications: Replace "Message" fallback with "Notification" as the event may not be a message. + * 🐛 Bugfix - * Notifications: Name/Avatar changes were displayed as invites (#4659). + * ⚠️ API Changes * From eb336cf578c2e65bfb942385bb702ac5e4ccb2e1 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 10 Aug 2021 10:31:34 +0100 Subject: [PATCH 12/18] Add changelog entries. --- changelog.d/4132.change | 1 + changelog.d/4653.change | 1 + changelog.d/4660.change | 1 + 3 files changed, 3 insertions(+) create mode 100644 changelog.d/4132.change create mode 100644 changelog.d/4653.change create mode 100644 changelog.d/4660.change diff --git a/changelog.d/4132.change b/changelog.d/4132.change new file mode 100644 index 000000000..eb78d8c42 --- /dev/null +++ b/changelog.d/4132.change @@ -0,0 +1 @@ +Notifications: Replace "Message" fallback with "Notification" as the event may not be a message. \ No newline at end of file diff --git a/changelog.d/4653.change b/changelog.d/4653.change new file mode 100644 index 000000000..9d54243ee --- /dev/null +++ b/changelog.d/4653.change @@ -0,0 +1 @@ +Notifications: Show the body of all message event types. \ No newline at end of file diff --git a/changelog.d/4660.change b/changelog.d/4660.change new file mode 100644 index 000000000..b6f66573e --- /dev/null +++ b/changelog.d/4660.change @@ -0,0 +1 @@ +Notifications: Replies now hide the referenced content. \ No newline at end of file From d93a4994a7957d783a4a6761f502ef61e92e4869 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 10 Aug 2021 10:45:33 +0100 Subject: [PATCH 13/18] Fix localisation key. --- RiotNSE/NotificationService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index b053fe113..7543472bc 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -501,7 +501,7 @@ class NotificationService: UNNotificationServiceExtension { if let roomDisplayName = roomDisplayName, roomDisplayName != eventSenderName { return NSString.localizedUserNotificationString(forKey: "REPLY_FROM_USER_IN_ROOM_TITLE", arguments: [eventSenderName, roomDisplayName]) } else { - return NSString.localizedUserNotificationString(forKey: "REPLY_FROM_USER", arguments: [eventSenderName]) + return NSString.localizedUserNotificationString(forKey: "REPLY_FROM_USER_TITLE", arguments: [eventSenderName]) } } From d6dc900a9b14c8197fe6ee66a394dfbb39b6aec3 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 12 Aug 2021 09:35:30 +0100 Subject: [PATCH 14/18] Update the value of notificationBodyLocalizationKey to "NOTIFICATION". --- Riot/Modules/Application/LegacyAppDelegate.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index a6df6cb48..7fb2ae40f 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1910,6 +1910,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Add observer on settings changes. [[MXKAppSettings standardAppSettings] addObserver:self forKeyPath:@"showAllEventsInRoomHistory" options:0 context:nil]; + // Override the default notification content. This must happen before the account manager prepares the session. + MXKAppSettings.standardAppSettings.notificationBodyLocalizationKey = @"NOTIFICATION"; + // Prepare account manager MXKAccountManager *accountManager = [MXKAccountManager sharedManager]; From 2cf5c1317553e5b60e0c6235106a5c3d2359351e Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 17 Aug 2021 15:38:01 +0100 Subject: [PATCH 15/18] Update strings from feedback. --- Riot/Assets/en.lproj/Localizable.strings | 6 +++--- RiotNSE/NotificationService.swift | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Riot/Assets/en.lproj/Localizable.strings b/Riot/Assets/en.lproj/Localizable.strings index c668ee02c..b8576fcc3 100644 --- a/Riot/Assets/en.lproj/Localizable.strings +++ b/Riot/Assets/en.lproj/Localizable.strings @@ -54,13 +54,13 @@ /** Media Messages **/ /* New image message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ sent a picture %@"; +"PICTURE_FROM_USER" = "%@ sent a picture"; /* New image message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ posted a picture %@ in %@"; /* New video message from a specific person, not referencing a room. */ -"VIDEO_FROM_USER" = "%@ sent a video %@"; +"VIDEO_FROM_USER" = "%@ sent a video"; /* New audio message from a specific person, not referencing a room. */ "AUDIO_FROM_USER" = "%@ sent an audio file %@"; @@ -142,7 +142,7 @@ "USER_UPDATED_AVATAR" = "%@ changed their avatar"; /* A user's membership has updated in an unknown way */ -"USER_MEMBERSHIP_UPDATED" = "%@ updated their details"; +"USER_MEMBERSHIP_UPDATED" = "%@ updated their profile"; /** Calls **/ diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 7543472bc..391d32079 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -379,9 +379,9 @@ class NotificationService: UNNotificationServiceExtension { case kMXMessageTypeEmote: notificationBody = NSString.localizedUserNotificationString(forKey: "ACTION_FROM_USER", arguments: [eventSenderName, messageContent as Any]) case kMXMessageTypeImage: - notificationBody = NSString.localizedUserNotificationString(forKey: "IMAGE_FROM_USER", arguments: [eventSenderName, messageContent as Any]) + notificationBody = NSString.localizedUserNotificationString(forKey: "PICTURE_FROM_USER", arguments: [eventSenderName]) case kMXMessageTypeVideo: - notificationBody = NSString.localizedUserNotificationString(forKey: "VIDEO_FROM_USER", arguments: [eventSenderName, messageContent as Any]) + notificationBody = NSString.localizedUserNotificationString(forKey: "VIDEO_FROM_USER", arguments: [eventSenderName]) case kMXMessageTypeAudio: if event.isVoiceMessage() { notificationBody = NSString.localizedUserNotificationString(forKey: "VOICE_MESSAGE_FROM_USER", arguments: [eventSenderName]) @@ -406,17 +406,24 @@ class NotificationService: UNNotificationServiceExtension { if NotificationService.backgroundSyncService.roomSummary(forRoomId: roomId)?.membership == .join { notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) + // If the sender's membership is join and hasn't changed. if event.content["membership"] as? String == "join", - let prevContent = event.prevContent, - prevContent["membership"] as? String == "join" { + let previousContent = event.prevContent, + previousContent["membership"] as? String == "join" { + + // Check for display name changes if let displayname = event.content["displayname"] as? String, - let oldDisplayname = prevContent["displayname"] as? String, + let oldDisplayname = previousContent["displayname"] as? String, displayname != oldDisplayname { + notificationBody = NSString.localizedUserNotificationString(forKey: "USER_UPDATED_DISPLAYNAME", arguments: [oldDisplayname, displayname]) } else { + // If the display name hasn't changed, handle as an avatar change. notificationBody = NSString.localizedUserNotificationString(forKey: "USER_UPDATED_AVATAR", arguments: [eventSenderName]) } } else { + // No known reports of having reached this situation for a membership notification + // So use a generic membership updated fallback. notificationBody = NSString.localizedUserNotificationString(forKey: "USER_MEMBERSHIP_UPDATED", arguments: [eventSenderName]) } // Otherwise treat the notification as an invite. From 2de87e775c74282f9dab1fca5d236280a5e61e60 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 17 Aug 2021 15:51:18 +0100 Subject: [PATCH 16/18] Add reaction content to notifications when available. --- Riot/Assets/en.lproj/Localizable.strings | 7 +++++-- RiotNSE/NotificationService.swift | 8 +++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/en.lproj/Localizable.strings b/Riot/Assets/en.lproj/Localizable.strings index b8576fcc3..3601873d5 100644 --- a/Riot/Assets/en.lproj/Localizable.strings +++ b/Riot/Assets/en.lproj/Localizable.strings @@ -119,8 +119,11 @@ /** Reactions **/ -/* A user has reacted to a message */ -"REACTION_FROM_USER" = "%@ sent a reaction"; +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ reacted %@"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ sent a reaction"; /** Invites **/ diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 391d32079..ecb2864bc 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -439,7 +439,13 @@ class NotificationService: UNNotificationServiceExtension { notificationBody = NSString.localizedUserNotificationString(forKey: "STICKER_FROM_USER", arguments: [eventSenderName as Any]) case .reaction: notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) - notificationBody = NSString.localizedUserNotificationString(forKey: "REACTION_FROM_USER", arguments: [eventSenderName]) + if let reactionKey = event.relatesTo?.key { + // Try to show the reaction key in the notification. + notificationBody = NSString.localizedUserNotificationString(forKey: "REACTION_FROM_USER", arguments: [eventSenderName, reactionKey]) + } else { + // Otherwise show a generic reaction. + notificationBody = NSString.localizedUserNotificationString(forKey: "GENERIC_REACTION_FROM_USER", arguments: [eventSenderName]) + } case .custom: if (event.type == kWidgetMatrixEventTypeString || event.type == kWidgetModularEventTypeString), let type = event.content?["type"] as? String, From f3ca6b2253c968073fd3d96dd8f0d1e9b27e2841 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 17 Aug 2021 15:55:35 +0100 Subject: [PATCH 17/18] Add a few extra comments. --- RiotNSE/NotificationService.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index ecb2864bc..7c1368eb3 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -403,6 +403,7 @@ class NotificationService: UNNotificationServiceExtension { } case .roomMember: // If the current user is already joined, display updated displayname/avatar events. + // This is an unexpected path, but has been seen in some circumstances. if NotificationService.backgroundSyncService.roomSummary(forRoomId: roomId)?.membership == .join { notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) @@ -427,6 +428,7 @@ class NotificationService: UNNotificationServiceExtension { notificationBody = NSString.localizedUserNotificationString(forKey: "USER_MEMBERSHIP_UPDATED", arguments: [eventSenderName]) } // Otherwise treat the notification as an invite. + // This is the expected notification content for a membership event. } else { if roomDisplayName != nil && roomDisplayName != eventSenderName { notificationBody = NSString.localizedUserNotificationString(forKey: "USER_INVITE_TO_NAMED_ROOM", arguments: [eventSenderName, roomDisplayName as Any]) @@ -434,9 +436,12 @@ class NotificationService: UNNotificationServiceExtension { notificationBody = NSString.localizedUserNotificationString(forKey: "USER_INVITE_TO_CHAT", arguments: [eventSenderName]) } } + case .sticker: notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) notificationBody = NSString.localizedUserNotificationString(forKey: "STICKER_FROM_USER", arguments: [eventSenderName as Any]) + + // Reactions are unexpected notification types, but have been seen in some circumstances. case .reaction: notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) if let reactionKey = event.relatesTo?.key { @@ -446,6 +451,7 @@ class NotificationService: UNNotificationServiceExtension { // Otherwise show a generic reaction. notificationBody = NSString.localizedUserNotificationString(forKey: "GENERIC_REACTION_FROM_USER", arguments: [eventSenderName]) } + case .custom: if (event.type == kWidgetMatrixEventTypeString || event.type == kWidgetModularEventTypeString), let type = event.content?["type"] as? String, From 3329c169e0555f368bb2869f4f959ac5cc3f3133 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 18 Aug 2021 12:32:32 +0100 Subject: [PATCH 18/18] Update from feedback. Set notificationBodyLocalizationKey in CommonConfiguration. Remove redundant customisation. Use MXRoomMemberEventContent. --- Config/CommonConfiguration.swift | 3 +++ Riot/Assets/en.lproj/Localizable.strings | 3 +++ Riot/Modules/Application/LegacyAppDelegate.m | 3 --- RiotNSE/NotificationService.swift | 25 +++++++++++--------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index 1583fdc78..e582f546a 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -41,6 +41,9 @@ class CommonConfiguration: NSObject, Configurable { // Enable lazy loading settings.syncWithLazyLoadOfRoomMembers = true + // Customize the default notification content + settings.notificationBodyLocalizationKey = "NOTIFICATION" + settings.messageDetailsAllowSharing = BuildSettings.messageDetailsAllowShare settings.messageDetailsAllowSaving = BuildSettings.messageDetailsAllowSave settings.messageDetailsAllowCopyingMedia = BuildSettings.messageDetailsAllowCopyMedia diff --git a/Riot/Assets/en.lproj/Localizable.strings b/Riot/Assets/en.lproj/Localizable.strings index 3601873d5..914bb11e2 100644 --- a/Riot/Assets/en.lproj/Localizable.strings +++ b/Riot/Assets/en.lproj/Localizable.strings @@ -141,6 +141,9 @@ /* A user has change their name to a new name */ "USER_UPDATED_DISPLAYNAME" = "%@ changed their name to %@"; +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ changed their name"; + /* A user has change their avatar */ "USER_UPDATED_AVATAR" = "%@ changed their avatar"; diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 7fb2ae40f..a6df6cb48 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1910,9 +1910,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Add observer on settings changes. [[MXKAppSettings standardAppSettings] addObserver:self forKeyPath:@"showAllEventsInRoomHistory" options:0 context:nil]; - // Override the default notification content. This must happen before the account manager prepares the session. - MXKAppSettings.standardAppSettings.notificationBodyLocalizationKey = @"NOTIFICATION"; - // Prepare account manager MXKAccountManager *accountManager = [MXKAccountManager sharedManager]; diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 7c1368eb3..0b2ff4758 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -108,9 +108,6 @@ class NotificationService: UNNotificationServiceExtension { // no need to check before, if it's nil, the badge will remain unchanged content.badge = userInfo["unread_count"] as? NSNumber - // update the fallback body to read "Notification" which aligns with the system body when previews are hidden. - content.body = NSString.localizedUserNotificationString(forKey: "NOTIFICATION", arguments: []) - bestAttemptContents[eventId] = content contentHandlers[eventId] = contentHandler @@ -408,16 +405,22 @@ class NotificationService: UNNotificationServiceExtension { notificationTitle = self.messageTitle(for: eventSenderName, in: roomDisplayName) // If the sender's membership is join and hasn't changed. - if event.content["membership"] as? String == "join", - let previousContent = event.prevContent, - previousContent["membership"] as? String == "join" { + if let newContent = MXRoomMemberEventContent(fromJSON: event.content), + let prevContentDict = event.prevContent, + let oldContent = MXRoomMemberEventContent(fromJSON: prevContentDict), + newContent.membership == kMXMembershipStringJoin, + oldContent.membership == kMXMembershipStringJoin { // Check for display name changes - if let displayname = event.content["displayname"] as? String, - let oldDisplayname = previousContent["displayname"] as? String, - displayname != oldDisplayname { - - notificationBody = NSString.localizedUserNotificationString(forKey: "USER_UPDATED_DISPLAYNAME", arguments: [oldDisplayname, displayname]) + if newContent.displayname != oldContent.displayname { + // If there was a change, use the sender's userID if one was blank and show the change. + if let oldDisplayname = oldContent.displayname ?? event.sender, + let displayname = newContent.displayname ?? event.sender { + notificationBody = NSString.localizedUserNotificationString(forKey: "USER_UPDATED_DISPLAYNAME", arguments: [oldDisplayname, displayname]) + } else { + // Should never be reached as the event should always have a sender. + notificationBody = NSString.localizedUserNotificationString(forKey: "GENERIC_USER_UPDATED_DISPLAYNAME", arguments: [eventSenderName]) + } } else { // If the display name hasn't changed, handle as an avatar change. notificationBody = NSString.localizedUserNotificationString(forKey: "USER_UPDATED_AVATAR", arguments: [eventSenderName])