mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Custom copy for notifications that contain mentions (#2050)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user