Skip to content

Commit

Permalink
Custom copy for notifications that contain mentions (#2050)
Browse files Browse the repository at this point in the history
  • Loading branch information
Velin92 authored Nov 9, 2023
1 parent 019ca62 commit a4bc7b8
Show file tree
Hide file tree
Showing 21 changed files with 141 additions and 92 deletions.
38 changes: 19 additions & 19 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-rust-components-swift",
"state" : {
"revision" : "29870facdcf257e9cd79ee0eacd52b7425b92736",
"version" : "0.0.1-november23"
"revision" : "f647c08eb338ddc20a41429330f366b6bb97d879",
"version" : "0.0.2-november23"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,11 @@
"common_video" = "Video";
"common_voice_message" = "Voice message";
"common_waiting" = "Waiting…";
"common_waiting_for_decryption_key" = "Waiting for decryption key";
"common_waiting_for_decryption_key" = "Waiting for this message";
"common.report_a_problem" = "Report a problem";
"common_poll_end_confirmation" = "Are you sure you want to end this poll?";
"common_poll_summary" = "Poll: %1$@";
"common_verify_device" = "Verify device";
"confirm_recovery_key_banner_message" = "Your chat backup is currently out of sync. You need to confirm your recovery key to maintain access to your chat backup.";
"confirm_recovery_key_banner_title" = "Confirm your recovery key";
"crash_detection_dialog_content" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?";
Expand Down Expand Up @@ -217,6 +219,8 @@
"notification_invitation_action_join" = "Join";
"notification_invitation_action_reject" = "Reject";
"notification_invite_body" = "Invited you to chat";
"notification_mentioned_you_body" = "%1$@ mentioned you.\n%2$@";
"notification_mentioned_you_fallback_body" = "You have been mentioned.\n%1$@";
"notification_new_messages" = "New Messages";
"notification_reaction_body" = "Reacted with %1$@";
"notification_room_action_mark_as_read" = "Mark as read";
Expand Down Expand Up @@ -421,6 +425,8 @@
"screen_recovery_key_change_success" = "Recovery key changed";
"screen_recovery_key_change_title" = "Change recovery key?";
"screen_recovery_key_confirm_description" = "Enter your recovery key to confirm access to your chat backup.";
"screen_recovery_key_confirm_error_content" = "Please try again to confirm access to your chat backup.";
"screen_recovery_key_confirm_error_title" = "Incorrect recovery key";
"screen_recovery_key_confirm_key_description" = "Enter the 48 character code.";
"screen_recovery_key_confirm_key_placeholder" = "Enter...";
"screen_recovery_key_confirm_success" = "Recovery key confirmed";
Expand Down Expand Up @@ -518,6 +524,7 @@
"screen_session_verification_positive_button_canceled" = "Retry verification";
"screen_session_verification_positive_button_initial" = "I am ready";
"screen_session_verification_positive_button_verifying_ongoing" = "Waiting to match";
"screen_session_verification_ready_subtitle" = "Compare a unique set of emojis.";
"screen_session_verification_request_accepted_subtitle" = "Compare the unique emoji, ensuring they appear in the same order.";
"screen_session_verification_they_dont_match" = "They don’t match";
"screen_session_verification_they_match" = "They match";
Expand Down
8 changes: 5 additions & 3 deletions ElementX/Sources/Application/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ final class AppSettings {

/// UserDefaults to be used on reads and writes.
private static var store: UserDefaults! = UserDefaults(suiteName: suiteName)

static func reset() {
MXLog.warning("Resetting the AppSettings.")
store.removePersistentDomain(forName: suiteName)
Expand Down Expand Up @@ -181,6 +181,7 @@ final class AppSettings {

// MARK: - Analytics

#if !IS_NSE
#if DEBUG
/// The configuration to use for analytics during development. Set `isEnabled` to false to disable analytics in debug builds.
/// **Note:** Analytics are disabled by default for forks. If you are maintaining a fork, set custom configurations.
Expand All @@ -205,6 +206,7 @@ final class AppSettings {

@UserPreference(key: UserDefaultsKeys.timelineStyle, defaultValue: TimelineStyle.bubbles, storageType: .userDefaults(store))
var timelineStyle
#endif

@UserPreference(key: UserDefaultsKeys.shouldCollapseRoomStateEvents, defaultValue: true, storageType: .volatile)
var shouldCollapseRoomStateEvents
Expand Down Expand Up @@ -234,9 +236,9 @@ final class AppSettings {
/// Tag describing which set of device specific rules a pusher executes.
@UserPreference(key: UserDefaultsKeys.pusherProfileTag, storageType: .userDefaults(store))
var pusherProfileTag: String?

// MARK: - Other

// MARK: - Other

let permalinkBaseURL: URL = "https://matrix.to"

// MARK: - Logging
Expand Down
25 changes: 24 additions & 1 deletion ElementX/Sources/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -376,13 +376,15 @@ public enum L10n {
public static var commonVerificationCancelled: String { return L10n.tr("Localizable", "common_verification_cancelled") }
/// Verification complete
public static var commonVerificationComplete: String { return L10n.tr("Localizable", "common_verification_complete") }
/// Verify device
public static var commonVerifyDevice: String { return L10n.tr("Localizable", "common_verify_device") }
/// Video
public static var commonVideo: String { return L10n.tr("Localizable", "common_video") }
/// Voice message
public static var commonVoiceMessage: String { return L10n.tr("Localizable", "common_voice_message") }
/// Waiting…
public static var commonWaiting: String { return L10n.tr("Localizable", "common_waiting") }
/// Waiting for decryption key
/// Waiting for this message
public static var commonWaitingForDecryptionKey: String { return L10n.tr("Localizable", "common_waiting_for_decryption_key") }
/// Your chat backup is currently out of sync. You need to confirm your recovery key to maintain access to your chat backup.
public static var confirmRecoveryKeyBannerMessage: String { return L10n.tr("Localizable", "confirm_recovery_key_banner_message") }
Expand Down Expand Up @@ -506,6 +508,16 @@ public enum L10n {
}
/// Invited you to chat
public static var notificationInviteBody: String { return L10n.tr("Localizable", "notification_invite_body") }
/// %1$@ mentioned you.
/// %2$@
public static func notificationMentionedYouBody(_ p1: Any, _ p2: Any) -> String {
return L10n.tr("Localizable", "notification_mentioned_you_body", String(describing: p1), String(describing: p2))
}
/// You have been mentioned.
/// %1$@
public static func notificationMentionedYouFallbackBody(_ p1: Any) -> String {
return L10n.tr("Localizable", "notification_mentioned_you_fallback_body", String(describing: p1))
}
/// New Messages
public static var notificationNewMessages: String { return L10n.tr("Localizable", "notification_new_messages") }
/// Plural format key: "%#@COUNT@"
Expand Down Expand Up @@ -1020,6 +1032,10 @@ public enum L10n {
public static var screenRecoveryKeyChangeTitle: String { return L10n.tr("Localizable", "screen_recovery_key_change_title") }
/// Enter your recovery key to confirm access to your chat backup.
public static var screenRecoveryKeyConfirmDescription: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_description") }
/// Please try again to confirm access to your chat backup.
public static var screenRecoveryKeyConfirmErrorContent: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_error_content") }
/// Incorrect recovery key
public static var screenRecoveryKeyConfirmErrorTitle: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_error_title") }
/// Enter the 48 character code.
public static var screenRecoveryKeyConfirmKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_key_description") }
/// Enter...
Expand Down Expand Up @@ -1240,6 +1256,8 @@ public enum L10n {
public static var screenSessionVerificationPositiveButtonInitial: String { return L10n.tr("Localizable", "screen_session_verification_positive_button_initial") }
/// Waiting to match
public static var screenSessionVerificationPositiveButtonVerifyingOngoing: String { return L10n.tr("Localizable", "screen_session_verification_positive_button_verifying_ongoing") }
/// Compare a unique set of emojis.
public static var screenSessionVerificationReadySubtitle: String { return L10n.tr("Localizable", "screen_session_verification_ready_subtitle") }
/// Compare the unique emoji, ensuring they appear in the same order.
public static var screenSessionVerificationRequestAcceptedSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_request_accepted_subtitle") }
/// They don’t match
Expand Down Expand Up @@ -1547,6 +1565,11 @@ public enum L10n {
/// Edit poll
public static var editPoll: String { return L10n.tr("Localizable", "action.edit_poll") }
}

public enum Common {
/// Report a problem
public static var reportAProblem: String { return L10n.tr("Localizable", "common.report_a_problem") }
}
}
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
Expand Down
36 changes: 18 additions & 18 deletions ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,27 +234,27 @@ class SDKClientMock: SDKClientProtocol {
}
//MARK: - getMediaFile

public var getMediaFileMediaSourceBodyMimeTypeTempDirThrowableError: Error?
public var getMediaFileMediaSourceBodyMimeTypeTempDirCallsCount = 0
public var getMediaFileMediaSourceBodyMimeTypeTempDirCalled: Bool {
return getMediaFileMediaSourceBodyMimeTypeTempDirCallsCount > 0
}
public var getMediaFileMediaSourceBodyMimeTypeTempDirReceivedArguments: (mediaSource: MediaSource, body: String?, mimeType: String, tempDir: String?)?
public var getMediaFileMediaSourceBodyMimeTypeTempDirReceivedInvocations: [(mediaSource: MediaSource, body: String?, mimeType: String, tempDir: String?)] = []
public var getMediaFileMediaSourceBodyMimeTypeTempDirReturnValue: MediaFileHandle!
public var getMediaFileMediaSourceBodyMimeTypeTempDirClosure: ((MediaSource, String?, String, String?) throws -> MediaFileHandle)?

public func getMediaFile(mediaSource: MediaSource, body: String?, mimeType: String, tempDir: String?) throws -> MediaFileHandle {
if let error = getMediaFileMediaSourceBodyMimeTypeTempDirThrowableError {
public var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirThrowableError: Error?
public var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirCallsCount = 0
public var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirCalled: Bool {
return getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirCallsCount > 0
}
public var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirReceivedArguments: (mediaSource: MediaSource, body: String?, mimeType: String, useCache: Bool, tempDir: String?)?
public var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirReceivedInvocations: [(mediaSource: MediaSource, body: String?, mimeType: String, useCache: Bool, tempDir: String?)] = []
public var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirReturnValue: MediaFileHandle!
public var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirClosure: ((MediaSource, String?, String, Bool, String?) throws -> MediaFileHandle)?

public func getMediaFile(mediaSource: MediaSource, body: String?, mimeType: String, useCache: Bool, tempDir: String?) throws -> MediaFileHandle {
if let error = getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirThrowableError {
throw error
}
getMediaFileMediaSourceBodyMimeTypeTempDirCallsCount += 1
getMediaFileMediaSourceBodyMimeTypeTempDirReceivedArguments = (mediaSource: mediaSource, body: body, mimeType: mimeType, tempDir: tempDir)
getMediaFileMediaSourceBodyMimeTypeTempDirReceivedInvocations.append((mediaSource: mediaSource, body: body, mimeType: mimeType, tempDir: tempDir))
if let getMediaFileMediaSourceBodyMimeTypeTempDirClosure = getMediaFileMediaSourceBodyMimeTypeTempDirClosure {
return try getMediaFileMediaSourceBodyMimeTypeTempDirClosure(mediaSource, body, mimeType, tempDir)
getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirCallsCount += 1
getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirReceivedArguments = (mediaSource: mediaSource, body: body, mimeType: mimeType, useCache: useCache, tempDir: tempDir)
getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirReceivedInvocations.append((mediaSource: mediaSource, body: body, mimeType: mimeType, useCache: useCache, tempDir: tempDir))
if let getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirClosure = getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirClosure {
return try getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirClosure(mediaSource, body, mimeType, useCache, tempDir)
} else {
return getMediaFileMediaSourceBodyMimeTypeTempDirReturnValue
return getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirReturnValue
}
}
//MARK: - getMediaThumbnail
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,12 @@ extension UNMutableNotificationContent {
switch await mediaProvider.loadFileFromSource(mediaSource) {
case .success(let file):
do {
guard let url = file.url else {
MXLog.error("Couldn't add media attachment: URL is nil")
return self
}
let identifier = ProcessInfo.processInfo.globallyUniqueString
let newURL = try FileManager.default.copyFileToTemporaryDirectory(file: file.url, with: "\(identifier).\(file.url.pathExtension)")
let newURL = try FileManager.default.copyFileToTemporaryDirectory(file: url, with: "\(identifier).\(url.pathExtension)")
let attachment = try UNNotificationAttachment(identifier: identifier,
url: newURL,
options: nil)
Expand Down
4 changes: 3 additions & 1 deletion ElementX/Sources/Other/Pills/PlainMentionBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@ import Foundation
struct PlainMentionBuilder: MentionBuilderProtocol {
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) { }

func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String) { }
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String) {
attributedString.insert(NSAttributedString(string: "@"), at: range.location)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,14 @@ class MediaFileHandleProxy {
}

/// The media file's location on disk.
var url: URL {
URL(filePath: handle.path())
var url: URL? {
do {
let path = try handle.path()
return URL(filePath: path)
} catch {
MXLog.error("URL is missing for media file handle: \(error)")
return nil
}
}
}

Expand All @@ -60,6 +66,10 @@ extension MediaFileHandleProxy: Hashable {
///
/// This type allows for mocking but doesn't provide the automatic clean-up mechanism provided by the SDK.
private class UnmanagedMediaFileHandle: MediaFileHandleProtocol {
func persist(path: String) throws -> Bool {
false
}

let url: URL

init(url: URL) {
Expand Down
2 changes: 1 addition & 1 deletion ElementX/Sources/Services/Media/Provider/MediaLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ actor MediaLoader: MediaLoaderProtocol {

func loadMediaFileForSource(_ source: MediaSourceProxy, body: String?) async throws -> MediaFileHandleProxy {
let result = try await Task.dispatch(on: clientQueue) {
try self.client.getMediaFile(mediaSource: source.underlyingSource, body: body, mimeType: source.mimeType ?? "application/octet-stream", tempDir: nil)
try self.client.getMediaFile(mediaSource: source.underlyingSource, body: body, mimeType: source.mimeType ?? "application/octet-stream", useCache: true, tempDir: nil)
}

return MediaFileHandleProxy(handle: result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ struct NotificationItemProxy: NotificationItemProxyProtocol {
var isNoisy: Bool {
notificationItem.isNoisy ?? false
}

var hasMention: Bool {
notificationItem.hasMention ?? false
}

var senderAvatarMediaSource: MediaSourceProxy? {
if let senderAvatarURLString = notificationItem.senderInfo.avatarUrl,
Expand Down Expand Up @@ -114,4 +118,6 @@ struct EmptyNotificationItemProxy: NotificationItemProxyProtocol {
var notificationIdentifier: String { "" }

var roomJoinedMembers: Int { 0 }

var hasMention: Bool { false }
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ protocol NotificationItemProxyProtocol {
var isRoomDirect: Bool { get }

var isNoisy: Bool { get }

var hasMention: Bool { get }
}

extension NotificationItemProxyProtocol {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ extension BackupState {
var keyBackupState: SecureBackupKeyBackupState {
switch self {
case .unknown:
return .unknown
return .disabled
case .creating:
return .enabling
case .enabling:
Expand All @@ -195,8 +195,6 @@ extension BackupState {
return .enabled
case .disabling:
return .disabling
case .disabled:
return .disabled
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Foundation

enum VoiceMessageMediaManagerError: Error {
case unsupportedMimeTye
case missingURL
}

private final class VoiceMessageConversionRequest {
Expand Down Expand Up @@ -76,8 +77,11 @@ class VoiceMessageMediaManager: VoiceMessageMediaManagerProtocol {
}

// Convert from ogg
let convertedFileURL = URL.temporaryDirectory.appendingPathComponent(fileHandle.url.deletingPathExtension().lastPathComponent).appendingPathExtension(AudioConverterPreferredFileExtension.mpeg4aac.rawValue)
try audioConverter.convertToMPEG4AAC(sourceURL: fileHandle.url, destinationURL: convertedFileURL)
guard let url = fileHandle.url else {
throw VoiceMessageMediaManagerError.missingURL
}
let convertedFileURL = URL.temporaryDirectory.appendingPathComponent(url.deletingPathExtension().lastPathComponent).appendingPathExtension(AudioConverterPreferredFileExtension.mpeg4aac.rawValue)
try audioConverter.convertToMPEG4AAC(sourceURL: url, destinationURL: convertedFileURL)

// Cache the file and return the url
let result = voiceMessageCache.cache(mediaSource: source, using: convertedFileURL, move: true)
Expand Down
2 changes: 2 additions & 0 deletions ElementX/SupportingFiles/target.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ targets:
SWIFT_OBJC_BRIDGING_HEADER: ElementX/SupportingFiles/ElementX-Bridging-Header.h
SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h
PILLS_UT_TYPE_IDENTIFIER: $(BASE_BUNDLE_IDENTIFIER).pills
OTHER_SWIFT_FLAGS:
- "-DIS_MAIN_APP"

preBuildScripts:
- name: 🛠 SwiftGen
Expand Down
15 changes: 13 additions & 2 deletions NSE/Sources/NotificationContentBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,19 @@ struct NotificationContentBuilder {
private func processRoomMessage(notificationItem: NotificationItemProxyProtocol, messageType: MessageType, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
var notification = try await processCommonRoomMessage(notificationItem: notificationItem, mediaProvider: mediaProvider)

let senderDisplayName = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName
notification.body = String(messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: senderDisplayName, prefixWithSenderName: false).characters)
let displayName = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName
let message = String(messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName, prefixWithSenderName: false).characters)
let body: String
if notificationItem.hasMention {
if let senderDisplayName = notificationItem.senderDisplayName {
body = L10n.notificationMentionedYouBody(senderDisplayName, message)
} else {
body = L10n.notificationMentionedYouFallbackBody(message)
}
} else {
body = message
}
notification.body = body

switch messageType {
case .image(content: let content):
Expand Down
Loading

0 comments on commit a4bc7b8

Please sign in to comment.