Custom copy for notifications that contain mentions (#2050)

This commit is contained in:
Mauro
2023-11-09 19:12:11 +01:00
committed by GitHub
parent 019ca625ab
commit a4bc7b8e36
21 changed files with 139 additions and 90 deletions
@@ -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)
@@ -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.
@@ -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
@@ -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
+24 -1
View File
@@ -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") }
@@ -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@"
@@ -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...
@@ -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
@@ -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
@@ -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 getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirThrowableError: Error?
public var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirCallsCount = 0
public var getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirCalled: Bool {
return getMediaFileMediaSourceBodyMimeTypeUseCacheTempDirCallsCount > 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 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, tempDir: String?) throws -> MediaFileHandle {
if let error = getMediaFileMediaSourceBodyMimeTypeTempDirThrowableError {
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
@@ -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)
@@ -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)
}
}
@@ -1,19 +0,0 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
enum SharedUserDefaultsKeys: String {
case logLevel
}
@@ -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
}
}
}
@@ -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) {
@@ -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)
@@ -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,
@@ -114,4 +118,6 @@ struct EmptyNotificationItemProxy: NotificationItemProxyProtocol {
var notificationIdentifier: String { "" }
var roomJoinedMembers: Int { 0 }
var hasMention: Bool { false }
}
@@ -44,6 +44,8 @@ protocol NotificationItemProxyProtocol {
var isRoomDirect: Bool { get }
var isNoisy: Bool { get }
var hasMention: Bool { get }
}
extension NotificationItemProxyProtocol {
@@ -182,7 +182,7 @@ extension BackupState {
var keyBackupState: SecureBackupKeyBackupState {
switch self {
case .unknown:
return .unknown
return .disabled
case .creating:
return .enabling
case .enabling:
@@ -195,8 +195,6 @@ extension BackupState {
return .enabled
case .disabling:
return .disabling
case .disabled:
return .disabled
}
}
}
@@ -18,6 +18,7 @@ import Foundation
enum VoiceMessageMediaManagerError: Error {
case unsupportedMimeTye
case missingURL
}
private final class VoiceMessageConversionRequest {
@@ -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)