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 6541338bd..914bb11e2 100644 --- a/Riot/Assets/en.lproj/Localizable.strings +++ b/Riot/Assets/en.lproj/Localizable.strings @@ -14,9 +14,21 @@ 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 %@"; +/* 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 */ @@ -39,14 +51,26 @@ /* 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. */ -"IMAGE_FROM_USER" = "%@ sent a picture %@"; +/* New image message from a specific person, not referencing a room. */ +"PICTURE_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 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 %@"; + /* A single unread message in a room */ "SINGLE_UNREAD_IN_ROOM" = "You received a message in %@"; @@ -67,6 +91,7 @@ /* New message indicator on a room */ "MESSAGE_IN_X" = "Message in %@"; +/* New message with hidden content due to PIN enabled */ "MESSAGE_PROTECTED" = "New Message"; /** Coalesced messages **/ @@ -92,6 +117,14 @@ /* 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, 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 **/ /* A user has invited you to a chat */ @@ -103,6 +136,20 @@ /* 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 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"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ updated their profile"; + /** Calls **/ /* Incoming one-to-one voice call */ diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 680cd7848..e568cf7a8 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4014,7 +4014,7 @@ internal enum VectorL10n { internal static var settingsAdd3pidPasswordMessage: String { return VectorL10n.tr("Vector", "settings_add_3pid_password_message") } - /// Add email adress + /// Add email address internal static var settingsAdd3pidPasswordTitleEmail: String { return VectorL10n.tr("Vector", "settings_add_3pid_password_title_email") } diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 725db7ad1..faff89229 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -2383,11 +2383,16 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } else if (bubbleData.attachment.type == MXKAttachmentTypeVoiceMessage) { - if (bubbleData.isPaginationFirstBubble) { + if (bubbleData.isPaginationFirstBubble) + { cellViewClass = VoiceMessageWithPaginationTitleBubbleCell.class; - } else if (bubbleData.shouldHideSenderInformation) { + } + else if (bubbleData.shouldHideSenderInformation) + { cellViewClass = VoiceMessageWithoutSenderInfoBubbleCell.class; - } else { + } + else + { cellViewClass = VoiceMessageBubbleCell.class; } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift index be0479777..cf3cd8415 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackController.swift @@ -164,8 +164,6 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess } self.state = .stopped - self.loading = true - self.samples = [] updateUI() let requiredNumberOfSamples = playbackView.getRequiredNumberOfSamples() diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 85bdde3d5..0b2ff4758 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -186,13 +186,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]?.body = NSString.localizedUserNotificationString(forKey: "MESSAGE_FROM_X", arguments: [roomDisplayName as Any]) - } else { - 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) { @@ -240,10 +240,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 @@ -326,7 +326,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.") } - 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 @@ -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,62 +356,105 @@ class NotificationService: UNNotificationServiceExtension { } } - var msgType = event.content["msgtype"] as? String + let msgType = event.content["msgtype"] as? String let messageContent = event.content["body"] as? String + let isReply = event.isReply() + + 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 - msgType = nil + notificationBody = NSString.localizedUserNotificationString(forKey: "MESSAGE", arguments: []) + break } - // 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]) - - if msgType == kMXMessageTypeText { - notificationBody = messageContent - } else if msgType == kMXMessageTypeEmote { - notificationBody = NSString.localizedUserNotificationString(forKey: "ACTION_FROM_USER", arguments: [eventSenderName as Any, messageContent as Any]) - } else if msgType == kMXMessageTypeImage { - notificationBody = NSString.localizedUserNotificationString(forKey: "IMAGE_FROM_USER", arguments: [eventSenderName as Any, messageContent as Any]) + switch msgType { + case kMXMessageTypeEmote: + notificationBody = NSString.localizedUserNotificationString(forKey: "ACTION_FROM_USER", arguments: [eventSenderName, messageContent as Any]) + case kMXMessageTypeImage: + notificationBody = NSString.localizedUserNotificationString(forKey: "PICTURE_FROM_USER", arguments: [eventSenderName]) + case kMXMessageTypeVideo: + notificationBody = NSString.localizedUserNotificationString(forKey: "VIDEO_FROM_USER", arguments: [eventSenderName]) + case kMXMessageTypeAudio: + if event.isVoiceMessage() { + notificationBody = NSString.localizedUserNotificationString(forKey: "VOICE_MESSAGE_FROM_USER", arguments: [eventSenderName]) } else { - // Encrypted messages falls here - notificationBody = NSString.localizedUserNotificationString(forKey: "MESSAGE", arguments: []) + notificationBody = NSString.localizedUserNotificationString(forKey: "AUDIO_FROM_USER", arguments: [eventSenderName, messageContent as Any]) } - } else { - notificationTitle = eventSenderName - - switch msgType { - case kMXMessageTypeText: - 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 + case kMXMessageTypeFile: + notificationBody = NSString.localizedUserNotificationString(forKey: "FILE_FROM_USER", arguments: [eventSenderName, messageContent as Any]) + + // All other message types such as text, notice, server notice etc + default: + if event.isReply() { + let parser = MXReplyEventParser() + let replyParts = parser.parse(event) + notificationBody = replyParts.bodyParts.replyText + } else { + notificationBody = messageContent } } - break 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 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) + + // If the sender's membership is join and hasn't changed. + 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 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]) + } + } 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. + // This is the expected notification content for a membership event. } else { - notificationBody = NSString.localizedUserNotificationString(forKey: "USER_INVITE_TO_CHAT", arguments: [eventSenderName as Any]) - } - 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 + 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: + 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 { + // 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, @@ -457,6 +504,29 @@ 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 + } + } + + 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_TITLE", arguments: [eventSenderName]) + } + } + /// Get the context of an event. /// - Parameters: /// - event: the event 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 diff --git a/changelog.d/4714.bugfix b/changelog.d/4714.bugfix new file mode 100644 index 000000000..35496bfbe --- /dev/null +++ b/changelog.d/4714.bugfix @@ -0,0 +1 @@ +Fixed flickering voice message cells while being sent. \ No newline at end of file diff --git a/changelog.d/4726.bugfix b/changelog.d/4726.bugfix new file mode 100644 index 000000000..4f8feade5 --- /dev/null +++ b/changelog.d/4726.bugfix @@ -0,0 +1 @@ +Fastfile: Update build number in AppVersion.xcconfig instead of AppIdentifiers.xcconfig. \ No newline at end of file