From 412bc910659022806fe2f5b2f477b9df8c446e33 Mon Sep 17 00:00:00 2001 From: MinhDV Date: Thu, 30 Nov 2023 09:42:46 +0700 Subject: [PATCH 1/6] TW-1049 Save session to keychain --- lib/config/app_config.dart | 4 + .../keychain_sharing_manager.dart | 27 ++++++ .../keychain_sharing_restore_token.dart | 22 +++++ .../keychain_sharing_session.dart | 29 +++++++ .../flutter_hive_collections_database.dart | 83 ++++++++++++++++++- lib/utils/string_extension.dart | 7 ++ test/string_extension_test.dart | 8 ++ 7 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 lib/domain/keychain_sharing/keychain_sharing_manager.dart create mode 100644 lib/domain/keychain_sharing/keychain_sharing_restore_token.dart create mode 100644 lib/domain/keychain_sharing/keychain_sharing_session.dart diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index d077ae8557..c08f14268d 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -74,6 +74,10 @@ abstract class AppConfig { static const int thumbnailQuality = 70; static const int blurHashSize = 32; static const int imageQuality = 50; + static const String iOSKeychainSharingId = + 'KUT463DS29.com.linagora.ios.twake.shared'; + static const String iOSKeychainSharingAccount = + 'com.linagora.ios.twake.sessions'; static String? issueId; diff --git a/lib/domain/keychain_sharing/keychain_sharing_manager.dart b/lib/domain/keychain_sharing/keychain_sharing_manager.dart new file mode 100644 index 0000000000..f2bf4c1d98 --- /dev/null +++ b/lib/domain/keychain_sharing/keychain_sharing_manager.dart @@ -0,0 +1,27 @@ +import 'dart:convert'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/domain/keychain_sharing/keychain_sharing_restore_token.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class KeychainSharingManager { + static FlutterSecureStorage get _secureStorage => const FlutterSecureStorage( + iOptions: IOSOptions( + groupId: AppConfig.iOSKeychainSharingId, + accountName: AppConfig.iOSKeychainSharingAccount, + ), + ); + + static Future save(KeychainSharingRestoreToken token) => _secureStorage.write( + key: token.session.userId, + value: jsonEncode(token.toJson()), + ); + + static Future delete({required String? userId}) { + if (userId != null) { + return _secureStorage.delete(key: userId); + } else { + return _secureStorage.deleteAll(); + } + } +} diff --git a/lib/domain/keychain_sharing/keychain_sharing_restore_token.dart b/lib/domain/keychain_sharing/keychain_sharing_restore_token.dart new file mode 100644 index 0000000000..0c30d02a11 --- /dev/null +++ b/lib/domain/keychain_sharing/keychain_sharing_restore_token.dart @@ -0,0 +1,22 @@ +import 'package:fluffychat/domain/keychain_sharing/keychain_sharing_session.dart'; +import 'package:fluffychat/utils/string_extension.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'keychain_sharing_restore_token.g.dart'; + +@JsonSerializable() +class KeychainSharingRestoreToken { + final KeychainSharingSession session; + String? pusherNotificationClientIdentifier; + + KeychainSharingRestoreToken({ + required this.session, + }) { + pusherNotificationClientIdentifier = session.userId.sha256Hash; + } + + factory KeychainSharingRestoreToken.fromJson(Map json) => + _$KeychainSharingRestoreTokenFromJson(json); + + Map toJson() => _$KeychainSharingRestoreTokenToJson(this); +} diff --git a/lib/domain/keychain_sharing/keychain_sharing_session.dart b/lib/domain/keychain_sharing/keychain_sharing_session.dart new file mode 100644 index 0000000000..4290fd1be0 --- /dev/null +++ b/lib/domain/keychain_sharing/keychain_sharing_session.dart @@ -0,0 +1,29 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'keychain_sharing_session.g.dart'; + +@JsonSerializable() +class KeychainSharingSession { + String accessToken; + String? refreshToken; + String? oidcData; + String? slidingSyncProxy; + String userId; + String deviceId; + String homeserverUrl; + + KeychainSharingSession({ + required this.accessToken, + this.refreshToken, + this.oidcData, + this.slidingSyncProxy, + required this.userId, + required this.deviceId, + required this.homeserverUrl, + }); + + factory KeychainSharingSession.fromJson(Map json) => + _$KeychainSharingSessionFromJson(json); + + Map toJson() => _$KeychainSharingSessionToJson(this); +} diff --git a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart index 0c38c82e35..a91eeac942 100644 --- a/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart +++ b/lib/utils/matrix_sdk_extensions/flutter_hive_collections_database.dart @@ -1,8 +1,9 @@ import 'dart:convert'; import 'dart:io'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/utils/storage_directory_utils.dart'; +import 'package:fluffychat/domain/keychain_sharing/keychain_sharing_manager.dart'; +import 'package:fluffychat/domain/keychain_sharing/keychain_sharing_restore_token.dart'; +import 'package:fluffychat/domain/keychain_sharing/keychain_sharing_session.dart'; import 'package:flutter/foundation.dart' hide Key; import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -10,6 +11,9 @@ import 'package:hive/hive.dart'; import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/storage_directory_utils.dart'; + class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase { FlutterHiveCollectionsDatabase( String name, @@ -152,4 +156,79 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase { if (await file.exists() == false) return null; return file; } + + @override + Future updateClient( + String homeserverUrl, + String token, + String userId, + String? deviceId, + String? deviceName, + String? prevBatch, + String? olmAccount, + ) async { + if (PlatformInfos.isIOS) { + final restoreToken = KeychainSharingRestoreToken( + session: KeychainSharingSession( + accessToken: token, + userId: userId, + deviceId: deviceId ?? "", + homeserverUrl: homeserverUrl, + ), + ); + await KeychainSharingManager.save(restoreToken); + } + return super.updateClient( + homeserverUrl, + token, + userId, + deviceId, + deviceName, + prevBatch, + olmAccount, + ); + } + + @override + Future insertClient( + String name, + String homeserverUrl, + String token, + String userId, + String? deviceId, + String? deviceName, + String? prevBatch, + String? olmAccount, + ) async { + if (PlatformInfos.isIOS) { + final restoreToken = KeychainSharingRestoreToken( + session: KeychainSharingSession( + accessToken: token, + userId: userId, + deviceId: deviceId ?? "", + homeserverUrl: homeserverUrl, + ), + ); + await KeychainSharingManager.save(restoreToken); + } + return super.insertClient( + name, + homeserverUrl, + token, + userId, + deviceId, + deviceName, + prevBatch, + olmAccount, + ); + } + + @override + Future clear({bool supportDeleteCollections = false}) async { + if (PlatformInfos.isIOS) { + // TODO: Should pass userId here when support multiple accounts + await KeychainSharingManager.delete(userId: null); + } + return super.clear(supportDeleteCollections: supportDeleteCollections); + } } diff --git a/lib/utils/string_extension.dart b/lib/utils/string_extension.dart index 42292ca5b5..23d63d371f 100644 --- a/lib/utils/string_extension.dart +++ b/lib/utils/string_extension.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:collection/collection.dart'; import 'package:file_saver/file_saver.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -6,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:intl/intl.dart'; import 'package:matrix/matrix.dart'; +import 'package:crypto/crypto.dart'; extension StringCasingExtension on String { String removeDiacritics() { @@ -318,4 +321,8 @@ extension StringCasingExtension on String { String get urlSafeBase64 { return replaceAll('+', '-').replaceAll('/', '_'); } + + String get sha256Hash { + return sha256.convert(utf8.encode(this)).toString(); + } } diff --git a/test/string_extension_test.dart b/test/string_extension_test.dart index d4f1202142..765b6d46cc 100644 --- a/test/string_extension_test.dart +++ b/test/string_extension_test.dart @@ -400,4 +400,12 @@ void main() { expect(input.urlSafeBase64, equals(expectedOutput)); }); }); + + test('sha256Hash returns the correct SHA-256 hash', () { + const input = 'Hello, world!'; + const expectedOutput = + '315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3'; + final result = input.sha256Hash; + expect(result, equals(expectedOutput)); + }); } From 55ffbb41163f3dcc6bcce2da7e981ae40c82c8df Mon Sep 17 00:00:00 2001 From: MinhDV Date: Thu, 30 Nov 2023 09:47:37 +0700 Subject: [PATCH 2/6] TW-1049 Create NSE target --- ios/NSE/Info.plist | 19 + ios/NSE/NSE.entitlements | 14 + ios/Runner.xcodeproj/project.pbxproj | 700 +++++++++++++++++- .../xcshareddata/swiftpm/Package.resolved | 131 ++++ .../xcshareddata/xcschemes/NSE.xcscheme | 94 +++ .../xcshareddata/xcschemes/Runner.xcscheme | 8 +- .../xcshareddata/swiftpm/Package.resolved | 131 ++++ ios/Runner/AppDelegate.swift | 2 +- ios/Runner/Info.plist | 4 + ios/Runner/Runner.entitlements | 6 + scripts/patchs/ios-extension-debug.patch | 82 ++ 11 files changed, 1179 insertions(+), 12 deletions(-) create mode 100644 ios/NSE/Info.plist create mode 100644 ios/NSE/NSE.entitlements create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 ios/Runner.xcodeproj/xcshareddata/xcschemes/NSE.xcscheme create mode 100644 ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 scripts/patchs/ios-extension-debug.patch diff --git a/ios/NSE/Info.plist b/ios/NSE/Info.plist new file mode 100644 index 0000000000..367921692d --- /dev/null +++ b/ios/NSE/Info.plist @@ -0,0 +1,19 @@ + + + + + appGroupIdentifier + group.com.linagora.ios.twake + keychainAccessGroupIdentifier + KUT463DS29.com.linagora.ios.twake.shared + baseBundleIdentifier + com.linagora.ios.twake + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/ios/NSE/NSE.entitlements b/ios/NSE/NSE.entitlements new file mode 100644 index 0000000000..1c280b25fd --- /dev/null +++ b/ios/NSE/NSE.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.application-groups + + group.com.linagora.ios.twake + + keychain-access-groups + + $(AppIdentifierPrefix)com.linagora.ios.twake.shared + + + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 697bfe472f..4109807303 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -9,7 +9,82 @@ /* Begin PBXBuildFile section */ 0611A7F12A678C7700F180CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0611A7F42A678C7700F180CC /* Localizable.strings */; }; 0611A7F22A678C7700F180CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0611A7F42A678C7700F180CC /* Localizable.strings */; }; + 062DA3DD2B15813A007A963B /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA3DC2B15813A007A963B /* MatrixRustSDK */; }; + 062DA3E72B1585C2007A963B /* NotificationContentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3E62B1585C2007A963B /* NotificationContentBuilder.swift */; }; + 062DA4062B159903007A963B /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3E82B159902007A963B /* InfoPlistReader.swift */; }; + 062DA4072B159903007A963B /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3E92B159902007A963B /* Date.swift */; }; + 062DA4082B159903007A963B /* UserPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3EA2B159902007A963B /* UserPreference.swift */; }; + 062DA4092B159903007A963B /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3EB2B159902007A963B /* KeychainControllerProtocol.swift */; }; + 062DA40A2B159903007A963B /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3EC2B159902007A963B /* NotificationConstants.swift */; }; + 062DA40B2B159903007A963B /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3ED2B159902007A963B /* BackgroundTaskServiceProtocol.swift */; }; + 062DA40C2B159903007A963B /* RoomMessageEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3EE2B159902007A963B /* RoomMessageEventStringBuilder.swift */; }; + 062DA40D2B159903007A963B /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3EF2B159902007A963B /* AppSettings.swift */; }; + 062DA40E2B159903007A963B /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F02B159902007A963B /* RestorationToken.swift */; }; + 062DA40F2B159903007A963B /* PermalinkBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F12B159902007A963B /* PermalinkBuilder.swift */; }; + 062DA4102B159903007A963B /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F22B159902007A963B /* PlainMentionBuilder.swift */; }; + 062DA4112B159903007A963B /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F32B159903007A963B /* PlaceholderAvatarImage.swift */; }; + 062DA4122B159903007A963B /* NSRegularExpresion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F42B159903007A963B /* NSRegularExpresion.swift */; }; + 062DA4132B159903007A963B /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F52B159903007A963B /* AttributedString.swift */; }; + 062DA4142B159903007A963B /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F62B159903007A963B /* Bundle.swift */; }; + 062DA4152B159903007A963B /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F72B159903007A963B /* URL.swift */; }; + 062DA4162B159903007A963B /* LayoutDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F82B159903007A963B /* LayoutDirection.swift */; }; + 062DA4172B159903007A963B /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F92B159903007A963B /* ImageCache.swift */; }; + 062DA4182B159903007A963B /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3FA2B159903007A963B /* Task.swift */; }; + 062DA4192B159903007A963B /* UserAgentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3FB2B159903007A963B /* UserAgentBuilder.swift */; }; + 062DA41A2B159903007A963B /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3FC2B159903007A963B /* MatrixEntityRegex.swift */; }; + 062DA41B2B159903007A963B /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3FD2B159903007A963B /* String.swift */; }; + 062DA41C2B159903007A963B /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3FE2B159903007A963B /* BackgroundTaskProtocol.swift */; }; + 062DA41D2B159903007A963B /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3FF2B159903007A963B /* KeychainController.swift */; }; + 062DA41E2B159903007A963B /* TestablePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4002B159903007A963B /* TestablePreview.swift */; }; + 062DA41F2B159903007A963B /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4012B159903007A963B /* AvatarSize.swift */; }; + 062DA4202B159903007A963B /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4022B159903007A963B /* PillConstants.swift */; }; + 062DA4212B159903007A963B /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4032B159903007A963B /* FileManager.swift */; }; + 062DA4222B159903007A963B /* UNNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4042B159903007A963B /* UNNotificationContent.swift */; }; + 062DA4232B159903007A963B /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4052B159903007A963B /* UTType.swift */; }; + 062DA4262B159AE4007A963B /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA4252B159AE4007A963B /* DeviceKit */; }; + 062DA4292B159B3B007A963B /* Prefire in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA4282B159B3B007A963B /* Prefire */; }; + 062DA42C2B159B6E007A963B /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA42B2B159B6E007A963B /* Kingfisher */; }; + 062DA42F2B159C2D007A963B /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA42E2B159C2D007A963B /* KeychainAccess */; }; + 062DA4322B159C8A007A963B /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA4312B159C8A007A963B /* Compound */; }; + 062DA4352B159CCB007A963B /* Version in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA4342B159CCB007A963B /* Version */; }; + 062DA44B2B15A001007A963B /* MXLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4362B15A000007A963B /* MXLogger.swift */; }; + 062DA44D2B15A001007A963B /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4382B15A000007A963B /* Strings+Untranslated.swift */; }; + 062DA44E2B15A001007A963B /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4392B15A001007A963B /* Strings.swift */; }; + 062DA44F2B15A001007A963B /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA43A2B15A001007A963B /* Assets.swift */; }; + 062DA4502B15A001007A963B /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA43B2B15A001007A963B /* MediaProviderProtocol.swift */; }; + 062DA4512B15A001007A963B /* ImageProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA43C2B15A001007A963B /* ImageProviderProtocol.swift */; }; + 062DA4522B15A001007A963B /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA43D2B15A001007A963B /* MediaSourceProxy.swift */; }; + 062DA4532B15A001007A963B /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA43E2B15A001007A963B /* ElementXAttributeScope.swift */; }; + 062DA4542B15A001007A963B /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA43F2B15A001007A963B /* MediaProvider.swift */; }; + 062DA4552B15A001007A963B /* MediaFileHandleProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4402B15A001007A963B /* MediaFileHandleProxy.swift */; }; + 062DA4562B15A001007A963B /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4412B15A001007A963B /* MockMediaProvider.swift */; }; + 062DA4572B15A001007A963B /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4422B15A001007A963B /* NotificationItemProxy.swift */; }; + 062DA4582B15A001007A963B /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4432B15A001007A963B /* MediaLoader.swift */; }; + 062DA4592B15A001007A963B /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4442B15A001007A963B /* AttributedStringBuilder.swift */; }; + 062DA45A2B15A001007A963B /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4452B15A001007A963B /* MediaLoaderProtocol.swift */; }; + 062DA45B2B15A001007A963B /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4462B15A001007A963B /* MXLog.swift */; }; + 062DA45C2B15A001007A963B /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4472B15A001007A963B /* RustTracing.swift */; }; + 062DA45D2B15A001007A963B /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4482B15A001007A963B /* DTHTMLElement+AttributedStringBuilder.swift */; }; + 062DA45E2B15A001007A963B /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4492B15A001007A963B /* NotificationItemProxyProtocol.swift */; }; + 062DA45F2B15A001007A963B /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA44A2B15A001007A963B /* AttributedStringBuilderProtocol.swift */; }; + 062DA4622B15A03E007A963B /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA4612B15A03E007A963B /* DTCoreText */; }; + 062DA4652B15A06A007A963B /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA4642B15A06A007A963B /* LRUCache */; }; + 062DA4682B15A09C007A963B /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA4672B15A09C007A963B /* Collections */; }; + 062DA46A2B15A09C007A963B /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA4692B15A09C007A963B /* DequeModule */; }; + 062DA46C2B15A09C007A963B /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA46B2B15A09C007A963B /* OrderedCollections */; }; + 062DA4722B15A154007A963B /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA46D2B15A153007A963B /* UNNotificationRequest.swift */; }; + 062DA4732B15A154007A963B /* NSESettingsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA46E2B15A153007A963B /* NSESettingsProtocol.swift */; }; + 062DA4742B15A154007A963B /* DataProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA46F2B15A153007A963B /* DataProtectionManager.swift */; }; + 062DA4752B15A154007A963B /* NSELogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4702B15A153007A963B /* NSELogger.swift */; }; + 062DA4762B15A154007A963B /* NSEUserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4712B15A153007A963B /* NSEUserSession.swift */; }; + 0646E10A2B1084E00014B8DD /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0646E1092B1084E00014B8DD /* NotificationService.swift */; }; + 0646E10E2B1084E00014B8DD /* NSE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 0646E1072B1084E00014B8DD /* NSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 0646E1152B10A1160014B8DD /* KeychainSharingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0646E1142B10A1160014B8DD /* KeychainSharingData.swift */; }; + 0646E1172B10A3EB0014B8DD /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0646E1162B10A3EB0014B8DD /* Event.swift */; }; + 0646E1192B10A4EC0014B8DD /* MatrixHttpClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0646E1182B10A4EC0014B8DD /* MatrixHttpClient.swift */; }; + 06ED217C2B16D83000363DF5 /* KeychainSharingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0646E1142B10A1160014B8DD /* KeychainSharingData.swift */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 24AE4F7C04208EFEFE8D10A1 /* Pods_Twake_Share.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29F9CA3C68ACBADDD794AB3A /* Pods_Twake_Share.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 643D9A430DCE4893FC15438F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B16AB66899BBD65C9BFB2599 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; @@ -22,6 +97,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 0646E10C2B1084E00014B8DD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0646E1062B1084E00014B8DD; + remoteInfo = NSE; + }; C1005C4A261071B5002F4F32 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; @@ -49,6 +131,7 @@ dstSubfolderSpec = 13; files = ( C1005C4C261071B5002F4F32 /* Twake Share.appex in Embed App Extensions */, + 0646E10E2B1084E00014B8DD /* NSE.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -57,6 +140,69 @@ /* Begin PBXFileReference section */ 0611A7F32A678C7700F180CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 062DA3E62B1585C2007A963B /* NotificationContentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentBuilder.swift; sourceTree = ""; }; + 062DA3E82B159902007A963B /* InfoPlistReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoPlistReader.swift; sourceTree = ""; }; + 062DA3E92B159902007A963B /* Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; + 062DA3EA2B159902007A963B /* UserPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserPreference.swift; sourceTree = ""; }; + 062DA3EB2B159902007A963B /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = ""; }; + 062DA3EC2B159902007A963B /* NotificationConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = ""; }; + 062DA3ED2B159902007A963B /* BackgroundTaskServiceProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskServiceProtocol.swift; sourceTree = ""; }; + 062DA3EE2B159902007A963B /* RoomMessageEventStringBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomMessageEventStringBuilder.swift; sourceTree = ""; }; + 062DA3EF2B159902007A963B /* AppSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; + 062DA3F02B159902007A963B /* RestorationToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestorationToken.swift; sourceTree = ""; }; + 062DA3F12B159902007A963B /* PermalinkBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PermalinkBuilder.swift; sourceTree = ""; }; + 062DA3F22B159902007A963B /* PlainMentionBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlainMentionBuilder.swift; sourceTree = ""; }; + 062DA3F32B159903007A963B /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = ""; }; + 062DA3F42B159903007A963B /* NSRegularExpresion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSRegularExpresion.swift; sourceTree = ""; }; + 062DA3F52B159903007A963B /* AttributedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedString.swift; sourceTree = ""; }; + 062DA3F62B159903007A963B /* Bundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; + 062DA3F72B159903007A963B /* URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; + 062DA3F82B159903007A963B /* LayoutDirection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = ""; }; + 062DA3F92B159903007A963B /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; + 062DA3FA2B159903007A963B /* Task.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; + 062DA3FB2B159903007A963B /* UserAgentBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserAgentBuilder.swift; sourceTree = ""; }; + 062DA3FC2B159903007A963B /* MatrixEntityRegex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegex.swift; sourceTree = ""; }; + 062DA3FD2B159903007A963B /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; + 062DA3FE2B159903007A963B /* BackgroundTaskProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskProtocol.swift; sourceTree = ""; }; + 062DA3FF2B159903007A963B /* KeychainController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = ""; }; + 062DA4002B159903007A963B /* TestablePreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestablePreview.swift; sourceTree = ""; }; + 062DA4012B159903007A963B /* AvatarSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = ""; }; + 062DA4022B159903007A963B /* PillConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PillConstants.swift; sourceTree = ""; }; + 062DA4032B159903007A963B /* FileManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; + 062DA4042B159903007A963B /* UNNotificationContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UNNotificationContent.swift; sourceTree = ""; }; + 062DA4052B159903007A963B /* UTType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UTType.swift; sourceTree = ""; }; + 062DA4362B15A000007A963B /* MXLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MXLogger.swift; sourceTree = ""; }; + 062DA4382B15A000007A963B /* Strings+Untranslated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = ""; }; + 062DA4392B15A001007A963B /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; + 062DA43A2B15A001007A963B /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; + 062DA43B2B15A001007A963B /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; + 062DA43C2B15A001007A963B /* ImageProviderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageProviderProtocol.swift; sourceTree = ""; }; + 062DA43D2B15A001007A963B /* MediaSourceProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaSourceProxy.swift; sourceTree = ""; }; + 062DA43E2B15A001007A963B /* ElementXAttributeScope.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElementXAttributeScope.swift; sourceTree = ""; }; + 062DA43F2B15A001007A963B /* MediaProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; + 062DA4402B15A001007A963B /* MediaFileHandleProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaFileHandleProxy.swift; sourceTree = ""; }; + 062DA4412B15A001007A963B /* MockMediaProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = ""; }; + 062DA4422B15A001007A963B /* NotificationItemProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = ""; }; + 062DA4432B15A001007A963B /* MediaLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaLoader.swift; sourceTree = ""; }; + 062DA4442B15A001007A963B /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = ""; }; + 062DA4452B15A001007A963B /* MediaLoaderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaLoaderProtocol.swift; sourceTree = ""; }; + 062DA4462B15A001007A963B /* MXLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = ""; }; + 062DA4472B15A001007A963B /* RustTracing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RustTracing.swift; sourceTree = ""; }; + 062DA4482B15A001007A963B /* DTHTMLElement+AttributedStringBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DTHTMLElement+AttributedStringBuilder.swift"; sourceTree = ""; }; + 062DA4492B15A001007A963B /* NotificationItemProxyProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationItemProxyProtocol.swift; sourceTree = ""; }; + 062DA44A2B15A001007A963B /* AttributedStringBuilderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderProtocol.swift; sourceTree = ""; }; + 062DA46D2B15A153007A963B /* UNNotificationRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = ""; }; + 062DA46E2B15A153007A963B /* NSESettingsProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSESettingsProtocol.swift; sourceTree = ""; }; + 062DA46F2B15A153007A963B /* DataProtectionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataProtectionManager.swift; sourceTree = ""; }; + 062DA4702B15A153007A963B /* NSELogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = ""; }; + 062DA4712B15A153007A963B /* NSEUserSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSEUserSession.swift; sourceTree = ""; }; + 0646E1072B1084E00014B8DD /* NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NSE.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 0646E1092B1084E00014B8DD /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + 0646E10B2B1084E00014B8DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0646E1132B108CF90014B8DD /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = ""; }; + 0646E1142B10A1160014B8DD /* KeychainSharingData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainSharingData.swift; sourceTree = ""; }; + 0646E1162B10A3EB0014B8DD /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; + 0646E1182B10A4EC0014B8DD /* MatrixHttpClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixHttpClient.swift; sourceTree = ""; }; 06AAB3E12ADE390500E09F51 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; 06AAB3E22ADE39B400E09F51 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Main.strings; sourceTree = ""; }; 06AAB3E32ADE39B400E09F51 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/LaunchScreen.strings; sourceTree = ""; }; @@ -134,10 +280,13 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 1B6C59111A74FF0BB750A10A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 216064C73ECC2AECB9F0A1FA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 29F9CA3C68ACBADDD794AB3A /* Pods_Twake_Share.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Twake_Share.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3F66DD4265EB28DCDE497782 /* Pods-Twake Share.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Twake Share.debug.xcconfig"; path = "Target Support Files/Pods-Twake Share/Pods-Twake Share.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7BDA4FA73D97A9ACE3D3A884 /* Pods-Twake Share.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Twake Share.release.xcconfig"; path = "Target Support Files/Pods-Twake Share/Pods-Twake Share.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -152,10 +301,30 @@ C1005C53261072D4002F4F32 /* Twake Share.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Twake Share.entitlements"; sourceTree = ""; }; C149567B25C7274F00A16396 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; C149567D25C7276200A16396 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + CE1331EDBBE69F81528101C9 /* Pods-Twake Share.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Twake Share.profile.xcconfig"; path = "Target Support Files/Pods-Twake Share/Pods-Twake Share.profile.xcconfig"; sourceTree = ""; }; FDBA7311CF00074CB7786C33 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 0646E1042B1084E00014B8DD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 062DA4292B159B3B007A963B /* Prefire in Frameworks */, + 062DA4652B15A06A007A963B /* LRUCache in Frameworks */, + 062DA4622B15A03E007A963B /* DTCoreText in Frameworks */, + 062DA3DD2B15813A007A963B /* MatrixRustSDK in Frameworks */, + 062DA4682B15A09C007A963B /* Collections in Frameworks */, + 062DA46A2B15A09C007A963B /* DequeModule in Frameworks */, + 062DA42F2B159C2D007A963B /* KeychainAccess in Frameworks */, + 062DA4262B159AE4007A963B /* DeviceKit in Frameworks */, + 062DA4352B159CCB007A963B /* Version in Frameworks */, + 062DA4322B159C8A007A963B /* Compound in Frameworks */, + 062DA46C2B15A09C007A963B /* OrderedCollections in Frameworks */, + 062DA42C2B159B6E007A963B /* Kingfisher in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -168,16 +337,87 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 24AE4F7C04208EFEFE8D10A1 /* Pods_Twake_Share.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0646E1082B1084E00014B8DD /* NSE */ = { + isa = PBXGroup; + children = ( + 062DA46F2B15A153007A963B /* DataProtectionManager.swift */, + 062DA4702B15A153007A963B /* NSELogger.swift */, + 062DA46E2B15A153007A963B /* NSESettingsProtocol.swift */, + 062DA4712B15A153007A963B /* NSEUserSession.swift */, + 062DA46D2B15A153007A963B /* UNNotificationRequest.swift */, + 062DA43A2B15A001007A963B /* Assets.swift */, + 062DA4442B15A001007A963B /* AttributedStringBuilder.swift */, + 062DA44A2B15A001007A963B /* AttributedStringBuilderProtocol.swift */, + 062DA4482B15A001007A963B /* DTHTMLElement+AttributedStringBuilder.swift */, + 062DA43E2B15A001007A963B /* ElementXAttributeScope.swift */, + 062DA43C2B15A001007A963B /* ImageProviderProtocol.swift */, + 062DA4402B15A001007A963B /* MediaFileHandleProxy.swift */, + 062DA4432B15A001007A963B /* MediaLoader.swift */, + 062DA4452B15A001007A963B /* MediaLoaderProtocol.swift */, + 062DA43F2B15A001007A963B /* MediaProvider.swift */, + 062DA43B2B15A001007A963B /* MediaProviderProtocol.swift */, + 062DA43D2B15A001007A963B /* MediaSourceProxy.swift */, + 062DA4412B15A001007A963B /* MockMediaProvider.swift */, + 062DA4462B15A001007A963B /* MXLog.swift */, + 062DA4362B15A000007A963B /* MXLogger.swift */, + 062DA4422B15A001007A963B /* NotificationItemProxy.swift */, + 062DA4492B15A001007A963B /* NotificationItemProxyProtocol.swift */, + 062DA4472B15A001007A963B /* RustTracing.swift */, + 062DA4392B15A001007A963B /* Strings.swift */, + 062DA4382B15A000007A963B /* Strings+Untranslated.swift */, + 062DA3EF2B159902007A963B /* AppSettings.swift */, + 062DA3F52B159903007A963B /* AttributedString.swift */, + 062DA4012B159903007A963B /* AvatarSize.swift */, + 062DA3FE2B159903007A963B /* BackgroundTaskProtocol.swift */, + 062DA3ED2B159902007A963B /* BackgroundTaskServiceProtocol.swift */, + 062DA3F62B159903007A963B /* Bundle.swift */, + 062DA3E92B159902007A963B /* Date.swift */, + 062DA4032B159903007A963B /* FileManager.swift */, + 062DA3F92B159903007A963B /* ImageCache.swift */, + 062DA3E82B159902007A963B /* InfoPlistReader.swift */, + 062DA3FF2B159903007A963B /* KeychainController.swift */, + 062DA3EB2B159902007A963B /* KeychainControllerProtocol.swift */, + 062DA3F82B159903007A963B /* LayoutDirection.swift */, + 062DA3FC2B159903007A963B /* MatrixEntityRegex.swift */, + 062DA3EC2B159902007A963B /* NotificationConstants.swift */, + 062DA3F42B159903007A963B /* NSRegularExpresion.swift */, + 062DA3F12B159902007A963B /* PermalinkBuilder.swift */, + 062DA4022B159903007A963B /* PillConstants.swift */, + 062DA3F32B159903007A963B /* PlaceholderAvatarImage.swift */, + 062DA3F22B159902007A963B /* PlainMentionBuilder.swift */, + 062DA3F02B159902007A963B /* RestorationToken.swift */, + 062DA3EE2B159902007A963B /* RoomMessageEventStringBuilder.swift */, + 062DA3FD2B159903007A963B /* String.swift */, + 062DA3FA2B159903007A963B /* Task.swift */, + 062DA4002B159903007A963B /* TestablePreview.swift */, + 062DA4042B159903007A963B /* UNNotificationContent.swift */, + 062DA3F72B159903007A963B /* URL.swift */, + 062DA3FB2B159903007A963B /* UserAgentBuilder.swift */, + 062DA3EA2B159902007A963B /* UserPreference.swift */, + 062DA4052B159903007A963B /* UTType.swift */, + 0646E1132B108CF90014B8DD /* NSE.entitlements */, + 0646E1092B1084E00014B8DD /* NotificationService.swift */, + 0646E1142B10A1160014B8DD /* KeychainSharingData.swift */, + 0646E1162B10A3EB0014B8DD /* Event.swift */, + 0646E1182B10A4EC0014B8DD /* MatrixHttpClient.swift */, + 062DA3E62B1585C2007A963B /* NotificationContentBuilder.swift */, + 0646E10B2B1084E00014B8DD /* Info.plist */, + ); + path = NSE; + sourceTree = ""; + }; 17347C56C756794A41C124FE /* Frameworks */ = { isa = PBXGroup; children = ( B16AB66899BBD65C9BFB2599 /* Pods_Runner.framework */, + 29F9CA3C68ACBADDD794AB3A /* Pods_Twake_Share.framework */, ); name = Frameworks; sourceTree = ""; @@ -199,6 +439,7 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, C1005C43261071B5002F4F32 /* Twake Share */, + 0646E1082B1084E00014B8DD /* NSE */, 97C146EF1CF9000F007C117D /* Products */, E89DCAC000D371640E94E65B /* Pods */, 17347C56C756794A41C124FE /* Frameworks */, @@ -210,6 +451,7 @@ children = ( 97C146EE1CF9000F007C117D /* Runner.app */, C1005C42261071B5002F4F32 /* Twake Share.appex */, + 0646E1072B1084E00014B8DD /* NSE.appex */, ); name = Products; sourceTree = ""; @@ -248,6 +490,9 @@ 1B6C59111A74FF0BB750A10A /* Pods-Runner.debug.xcconfig */, FDBA7311CF00074CB7786C33 /* Pods-Runner.release.xcconfig */, 216064C73ECC2AECB9F0A1FA /* Pods-Runner.profile.xcconfig */, + 3F66DD4265EB28DCDE497782 /* Pods-Twake Share.debug.xcconfig */, + 7BDA4FA73D97A9ACE3D3A884 /* Pods-Twake Share.release.xcconfig */, + CE1331EDBBE69F81528101C9 /* Pods-Twake Share.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -255,6 +500,37 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 0646E1062B1084E00014B8DD /* NSE */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0646E10F2B1084E00014B8DD /* Build configuration list for PBXNativeTarget "NSE" */; + buildPhases = ( + 0646E1032B1084E00014B8DD /* Sources */, + 0646E1042B1084E00014B8DD /* Frameworks */, + 0646E1052B1084E00014B8DD /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = NSE; + packageProductDependencies = ( + 062DA3DC2B15813A007A963B /* MatrixRustSDK */, + 062DA4252B159AE4007A963B /* DeviceKit */, + 062DA4282B159B3B007A963B /* Prefire */, + 062DA42B2B159B6E007A963B /* Kingfisher */, + 062DA42E2B159C2D007A963B /* KeychainAccess */, + 062DA4312B159C8A007A963B /* Compound */, + 062DA4342B159CCB007A963B /* Version */, + 062DA4612B15A03E007A963B /* DTCoreText */, + 062DA4642B15A06A007A963B /* LRUCache */, + 062DA4672B15A09C007A963B /* Collections */, + 062DA4692B15A09C007A963B /* DequeModule */, + 062DA46B2B15A09C007A963B /* OrderedCollections */, + ); + productName = NSE; + productReference = 0646E1072B1084E00014B8DD /* NSE.appex */; + productType = "com.apple.product-type.app-extension"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; @@ -273,6 +549,7 @@ ); dependencies = ( C1005C4B261071B5002F4F32 /* PBXTargetDependency */, + 0646E10D2B1084E00014B8DD /* PBXTargetDependency */, ); name = Runner; productName = Runner; @@ -283,6 +560,7 @@ isa = PBXNativeTarget; buildConfigurationList = C1005C51261071B5002F4F32 /* Build configuration list for PBXNativeTarget "Twake Share" */; buildPhases = ( + 87B2A5119A5C77DC2D83CF2E /* [CP] Check Pods Manifest.lock */, C1005C3E261071B5002F4F32 /* Sources */, C1005C3F261071B5002F4F32 /* Frameworks */, C1005C40261071B5002F4F32 /* Resources */, @@ -305,10 +583,13 @@ KnownAssetTags = ( New, ); - LastSwiftUpdateCheck = 1240; + LastSwiftUpdateCheck = 1500; LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { + 0646E1062B1084E00014B8DD = { + CreatedOnToolsVersion = 15.0.1; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; @@ -352,17 +633,37 @@ "vi-VN", ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 062DA3DB2B1580B4007A963B /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */, + 062DA4242B159AE4007A963B /* XCRemoteSwiftPackageReference "DeviceKit" */, + 062DA4272B159B3B007A963B /* XCRemoteSwiftPackageReference "Prefire" */, + 062DA42A2B159B6E007A963B /* XCRemoteSwiftPackageReference "Kingfisher" */, + 062DA42D2B159C2D007A963B /* XCRemoteSwiftPackageReference "KeychainAccess" */, + 062DA4302B159C8A007A963B /* XCRemoteSwiftPackageReference "compound-ios" */, + 062DA4332B159CCB007A963B /* XCRemoteSwiftPackageReference "Version" */, + 062DA4602B15A03E007A963B /* XCRemoteSwiftPackageReference "DTCoreText" */, + 062DA4632B15A06A007A963B /* XCRemoteSwiftPackageReference "LRUCache" */, + 062DA4662B15A09C007A963B /* XCRemoteSwiftPackageReference "swift-collections" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, C1005C41261071B5002F4F32 /* Twake Share */, + 0646E1062B1084E00014B8DD /* NSE */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 0646E1052B1084E00014B8DD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -420,6 +721,28 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + 87B2A5119A5C77DC2D83CF2E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Twake Share-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -460,11 +783,79 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 0646E1032B1084E00014B8DD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 062DA4592B15A001007A963B /* AttributedStringBuilder.swift in Sources */, + 062DA4512B15A001007A963B /* ImageProviderProtocol.swift in Sources */, + 062DA40D2B159903007A963B /* AppSettings.swift in Sources */, + 062DA4232B159903007A963B /* UTType.swift in Sources */, + 062DA4762B15A154007A963B /* NSEUserSession.swift in Sources */, + 062DA4122B159903007A963B /* NSRegularExpresion.swift in Sources */, + 062DA4562B15A001007A963B /* MockMediaProvider.swift in Sources */, + 062DA4092B159903007A963B /* KeychainControllerProtocol.swift in Sources */, + 062DA41B2B159903007A963B /* String.swift in Sources */, + 062DA40E2B159903007A963B /* RestorationToken.swift in Sources */, + 062DA4722B15A154007A963B /* UNNotificationRequest.swift in Sources */, + 062DA4532B15A001007A963B /* ElementXAttributeScope.swift in Sources */, + 062DA4132B159903007A963B /* AttributedString.swift in Sources */, + 062DA44E2B15A001007A963B /* Strings.swift in Sources */, + 062DA4752B15A154007A963B /* NSELogger.swift in Sources */, + 062DA4502B15A001007A963B /* MediaProviderProtocol.swift in Sources */, + 062DA40F2B159903007A963B /* PermalinkBuilder.swift in Sources */, + 0646E1152B10A1160014B8DD /* KeychainSharingData.swift in Sources */, + 0646E1192B10A4EC0014B8DD /* MatrixHttpClient.swift in Sources */, + 062DA45D2B15A001007A963B /* DTHTMLElement+AttributedStringBuilder.swift in Sources */, + 062DA4072B159903007A963B /* Date.swift in Sources */, + 062DA4192B159903007A963B /* UserAgentBuilder.swift in Sources */, + 062DA41F2B159903007A963B /* AvatarSize.swift in Sources */, + 062DA40C2B159903007A963B /* RoomMessageEventStringBuilder.swift in Sources */, + 062DA3E72B1585C2007A963B /* NotificationContentBuilder.swift in Sources */, + 062DA4102B159903007A963B /* PlainMentionBuilder.swift in Sources */, + 062DA41E2B159903007A963B /* TestablePreview.swift in Sources */, + 062DA44F2B15A001007A963B /* Assets.swift in Sources */, + 0646E1172B10A3EB0014B8DD /* Event.swift in Sources */, + 062DA4542B15A001007A963B /* MediaProvider.swift in Sources */, + 062DA4742B15A154007A963B /* DataProtectionManager.swift in Sources */, + 062DA4732B15A154007A963B /* NSESettingsProtocol.swift in Sources */, + 062DA45E2B15A001007A963B /* NotificationItemProxyProtocol.swift in Sources */, + 062DA4582B15A001007A963B /* MediaLoader.swift in Sources */, + 062DA4142B159903007A963B /* Bundle.swift in Sources */, + 062DA4212B159903007A963B /* FileManager.swift in Sources */, + 062DA41C2B159903007A963B /* BackgroundTaskProtocol.swift in Sources */, + 062DA45F2B15A001007A963B /* AttributedStringBuilderProtocol.swift in Sources */, + 062DA4082B159903007A963B /* UserPreference.swift in Sources */, + 062DA4172B159903007A963B /* ImageCache.swift in Sources */, + 062DA4572B15A001007A963B /* NotificationItemProxy.swift in Sources */, + 062DA45C2B15A001007A963B /* RustTracing.swift in Sources */, + 062DA45B2B15A001007A963B /* MXLog.swift in Sources */, + 062DA4522B15A001007A963B /* MediaSourceProxy.swift in Sources */, + 062DA4182B159903007A963B /* Task.swift in Sources */, + 062DA41D2B159903007A963B /* KeychainController.swift in Sources */, + 062DA40B2B159903007A963B /* BackgroundTaskServiceProtocol.swift in Sources */, + 062DA44B2B15A001007A963B /* MXLogger.swift in Sources */, + 0646E10A2B1084E00014B8DD /* NotificationService.swift in Sources */, + 062DA4552B15A001007A963B /* MediaFileHandleProxy.swift in Sources */, + 062DA45A2B15A001007A963B /* MediaLoaderProtocol.swift in Sources */, + 062DA4152B159903007A963B /* URL.swift in Sources */, + 062DA40A2B159903007A963B /* NotificationConstants.swift in Sources */, + 062DA4112B159903007A963B /* PlaceholderAvatarImage.swift in Sources */, + 062DA4222B159903007A963B /* UNNotificationContent.swift in Sources */, + 062DA44D2B15A001007A963B /* Strings+Untranslated.swift in Sources */, + 062DA4202B159903007A963B /* PillConstants.swift in Sources */, + 062DA41A2B159903007A963B /* MatrixEntityRegex.swift in Sources */, + 062DA4062B159903007A963B /* InfoPlistReader.swift in Sources */, + 062DA4162B159903007A963B /* LayoutDirection.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 06ED217C2B16D83000363DF5 /* KeychainSharingData.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -480,6 +871,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 0646E10D2B1084E00014B8DD /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0646E1062B1084E00014B8DD /* NSE */; + targetProxy = 0646E10C2B1084E00014B8DD /* PBXContainerItemProxy */; + }; C1005C4B261071B5002F4F32 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = C1005C41261071B5002F4F32 /* Twake Share */; @@ -588,6 +984,138 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 0646E1102B1084E00014B8DD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = NSE/NSE.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KUT463DS29; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NSE/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NSE; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.twake.nse; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = twake.nse.development.profile; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 0646E1112B1084E00014B8DD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = NSE/NSE.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KUT463DS29; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NSE/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NSE; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.twake.nse; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = twake.nse.development.profile; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 0646E1122B1084E00014B8DD /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = NSE/NSE.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KUT463DS29; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NSE/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NSE; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.twake.nse; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = twake.nse.development.profile; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { @@ -648,7 +1176,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; CUSTOM_GROUP_ID = group.com.linagora.ios.twake; @@ -660,6 +1188,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -671,7 +1200,7 @@ MARKETING_VERSION = 0.32.1; PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.twake; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = twake.development.profile; + PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = twake.development.profile; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -797,7 +1326,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; CUSTOM_GROUP_ID = group.com.linagora.ios.twake; @@ -809,6 +1338,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -820,7 +1350,7 @@ MARKETING_VERSION = 0.32.1; PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.twake; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = twake.development.profile; + PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = twake.development.profile; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -850,6 +1380,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -871,6 +1402,7 @@ }; C1005C4E261071B5002F4F32 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 3F66DD4265EB28DCDE497782 /* Pods-Twake Share.debug.xcconfig */; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -911,6 +1443,7 @@ }; C1005C4F261071B5002F4F32 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 7BDA4FA73D97A9ACE3D3A884 /* Pods-Twake Share.release.xcconfig */; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -948,6 +1481,7 @@ }; C1005C50261071B5002F4F32 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = CE1331EDBBE69F81528101C9 /* Pods-Twake Share.profile.xcconfig */; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -986,6 +1520,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 0646E10F2B1084E00014B8DD /* Build configuration list for PBXNativeTarget "NSE" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0646E1102B1084E00014B8DD /* Debug */, + 0646E1112B1084E00014B8DD /* Release */, + 0646E1122B1084E00014B8DD /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1017,6 +1561,152 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 062DA3DB2B1580B4007A963B /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift"; + requirement = { + branch = main; + kind = branch; + }; + }; + 062DA4242B159AE4007A963B /* XCRemoteSwiftPackageReference "DeviceKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/devicekit/DeviceKit.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.1.0; + }; + }; + 062DA4272B159B3B007A963B /* XCRemoteSwiftPackageReference "Prefire" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/BarredEwe/Prefire"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.5.0; + }; + }; + 062DA42A2B159B6E007A963B /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/onevcat/Kingfisher"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 7.10.0; + }; + }; + 062DA42D2B159C2D007A963B /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess"; + requirement = { + branch = master; + kind = branch; + }; + }; + 062DA4302B159C8A007A963B /* XCRemoteSwiftPackageReference "compound-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/vector-im/compound-ios"; + requirement = { + branch = main; + kind = branch; + }; + }; + 062DA4332B159CCB007A963B /* XCRemoteSwiftPackageReference "Version" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/mxcl/Version"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.1; + }; + }; + 062DA4602B15A03E007A963B /* XCRemoteSwiftPackageReference "DTCoreText" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Cocoanetics/DTCoreText"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.6.27; + }; + }; + 062DA4632B15A06A007A963B /* XCRemoteSwiftPackageReference "LRUCache" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/nicklockwood/LRUCache"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.4; + }; + }; + 062DA4662B15A09C007A963B /* XCRemoteSwiftPackageReference "swift-collections" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-collections"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.5; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 062DA3DC2B15813A007A963B /* MatrixRustSDK */ = { + isa = XCSwiftPackageProductDependency; + package = 062DA3DB2B1580B4007A963B /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; + productName = MatrixRustSDK; + }; + 062DA4252B159AE4007A963B /* DeviceKit */ = { + isa = XCSwiftPackageProductDependency; + package = 062DA4242B159AE4007A963B /* XCRemoteSwiftPackageReference "DeviceKit" */; + productName = DeviceKit; + }; + 062DA4282B159B3B007A963B /* Prefire */ = { + isa = XCSwiftPackageProductDependency; + package = 062DA4272B159B3B007A963B /* XCRemoteSwiftPackageReference "Prefire" */; + productName = Prefire; + }; + 062DA42B2B159B6E007A963B /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = 062DA42A2B159B6E007A963B /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; + 062DA42E2B159C2D007A963B /* KeychainAccess */ = { + isa = XCSwiftPackageProductDependency; + package = 062DA42D2B159C2D007A963B /* XCRemoteSwiftPackageReference "KeychainAccess" */; + productName = KeychainAccess; + }; + 062DA4312B159C8A007A963B /* Compound */ = { + isa = XCSwiftPackageProductDependency; + package = 062DA4302B159C8A007A963B /* XCRemoteSwiftPackageReference "compound-ios" */; + productName = Compound; + }; + 062DA4342B159CCB007A963B /* Version */ = { + isa = XCSwiftPackageProductDependency; + package = 062DA4332B159CCB007A963B /* XCRemoteSwiftPackageReference "Version" */; + productName = Version; + }; + 062DA4612B15A03E007A963B /* DTCoreText */ = { + isa = XCSwiftPackageProductDependency; + package = 062DA4602B15A03E007A963B /* XCRemoteSwiftPackageReference "DTCoreText" */; + productName = DTCoreText; + }; + 062DA4642B15A06A007A963B /* LRUCache */ = { + isa = XCSwiftPackageProductDependency; + package = 062DA4632B15A06A007A963B /* XCRemoteSwiftPackageReference "LRUCache" */; + productName = LRUCache; + }; + 062DA4672B15A09C007A963B /* Collections */ = { + isa = XCSwiftPackageProductDependency; + package = 062DA4662B15A09C007A963B /* XCRemoteSwiftPackageReference "swift-collections" */; + productName = Collections; + }; + 062DA4692B15A09C007A963B /* DequeModule */ = { + isa = XCSwiftPackageProductDependency; + package = 062DA4662B15A09C007A963B /* XCRemoteSwiftPackageReference "swift-collections" */; + productName = DequeModule; + }; + 062DA46B2B15A09C007A963B /* OrderedCollections */ = { + isa = XCSwiftPackageProductDependency; + package = 062DA4662B15A09C007A963B /* XCRemoteSwiftPackageReference "swift-collections" */; + productName = OrderedCollections; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000000..ea30bef593 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,131 @@ +{ + "pins" : [ + { + "identity" : "compound-design-tokens", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vector-im/compound-design-tokens.git", + "state" : { + "revision" : "b603371c5e4ac798f4613a7388d2305100b31911", + "version" : "0.0.7" + } + }, + { + "identity" : "compound-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vector-im/compound-ios", + "state" : { + "branch" : "main", + "revision" : "11e9303709eb8f22feb4662feca35997d3ed79b5" + } + }, + { + "identity" : "devicekit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/devicekit/DeviceKit.git", + "state" : { + "revision" : "66837ecf1516e41fd4251bbb684dc4b1997f08ab", + "version" : "5.1.0" + } + }, + { + "identity" : "dtcoretext", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Cocoanetics/DTCoreText", + "state" : { + "revision" : "9d2d4d2296e5d2d852a7d3c592b817d913a5d020", + "version" : "1.6.27" + } + }, + { + "identity" : "dtfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Cocoanetics/DTFoundation.git", + "state" : { + "revision" : "76062513434421cb6c8a1ae1d4f8368a7ebc2da3", + "version" : "1.7.18" + } + }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess", + "state" : { + "branch" : "master", + "revision" : "e0c7eebc5a4465a3c4680764f26b7a61f567cdaf" + } + }, + { + "identity" : "kingfisher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Kingfisher", + "state" : { + "revision" : "277f1ab2c6664b19b4a412e32b094b201e2d5757", + "version" : "7.10.0" + } + }, + { + "identity" : "lrucache", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/LRUCache", + "state" : { + "revision" : "6d2b5246c9c98dcd498552bb22f08d55b12a8371", + "version" : "1.0.4" + } + }, + { + "identity" : "matrix-rust-components-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/matrix-org/matrix-rust-components-swift", + "state" : { + "branch" : "main", + "revision" : "aa1dd4fc587d4b4adf603fd7ffef1580c9955d0c" + } + }, + { + "identity" : "prefire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/BarredEwe/Prefire", + "state" : { + "revision" : "898a4a9f5d5eb0a0b07adb1a7c89daf0f068b129", + "version" : "1.5.0" + } + }, + { + "identity" : "sfsafesymbols", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SFSafeSymbols/SFSafeSymbols.git", + "state" : { + "revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c", + "version" : "4.1.1" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307", + "version" : "1.0.5" + } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", + "state" : { + "revision" : "121c146fe591b1320238d054ae35c81ffa45f45a", + "version" : "0.12.0" + } + }, + { + "identity" : "version", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mxcl/Version", + "state" : { + "revision" : "1fe824b80d89201652e7eca7c9252269a1d85e25", + "version" : "2.0.1" + } + } + ], + "version" : 2 +} diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/NSE.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/NSE.xcscheme new file mode 100644 index 0000000000..7ca8fae354 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/NSE.xcscheme @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index b52b2e698b..a6b826db27 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -27,8 +27,6 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - + + - - Void) { let userInfo = notification.request.content.userInfo twakeApnChannel?.invokeMethod("willPresent", arguments: userInfo) - let isRemoteNotification = userInfo["aps"] != nil + let isRemoteNotification = userInfo["event_id"] != nil // Hide remote noti when in foreground, decrypted noti will show by dart completionHandler(isRemoteNotification ? [] : [.alert, .badge, .sound]) } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 6f19754984..9591579c3e 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -86,6 +86,10 @@ Save photos/videos to device on Twake. UIApplicationSupportsIndirectInputEvents + NSUserActivityTypes + + INSendMessageIntent + UIBackgroundModes audio diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 87446575dd..701fdc723b 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -9,9 +9,15 @@ applinks:example.com webcredentials:example.com + com.apple.developer.usernotifications.communication + com.apple.security.application-groups group.com.linagora.ios.twake + keychain-access-groups + + $(AppIdentifierPrefix)com.linagora.ios.twake.shared + diff --git a/scripts/patchs/ios-extension-debug.patch b/scripts/patchs/ios-extension-debug.patch new file mode 100644 index 0000000000..1822641bbb --- /dev/null +++ b/scripts/patchs/ios-extension-debug.patch @@ -0,0 +1,82 @@ +diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift +index 6fe9d3c3..f93b6f65 100644 +--- a/ios/Runner/AppDelegate.swift ++++ b/ios/Runner/AppDelegate.swift +@@ -1,72 +1,8 @@ + import UIKit +-import Flutter +- +-let apnTokenKey = "apnToken" +- +-@UIApplicationMain +-@objc class AppDelegate: FlutterAppDelegate { +- var twakeApnChannel: FlutterMethodChannel? +- var initialNotiInfo: Any? +- override func application( +- _ application: UIApplication, +- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? +- ) -> Bool { +- twakeApnChannel = createApnChannel() +- initialNotiInfo = launchOptions?[.remoteNotification] +- +- GeneratedPluginRegistrant.register(with: self) +- if #available(iOS 10.0, *) { +- UNUserNotificationCenter.current().delegate = self +- } +- return super.application(application, didFinishLaunchingWithOptions: launchOptions) +- } +- +- func createApnChannel() -> FlutterMethodChannel { +- let controller : FlutterViewController = window?.rootViewController as! FlutterViewController +- let twakeApnChannel = FlutterMethodChannel( +- name: "twake_apn", +- binaryMessenger: controller.binaryMessenger) +- twakeApnChannel.setMethodCallHandler { [weak self ] call, result in +- switch call.method { +- case "getToken": +- result(UserDefaults.standard.string(forKey: apnTokenKey)) +- case "getInitialNoti": +- result(self?.initialNotiInfo) +- self?.initialNotiInfo = nil +- case "clearAll": +- UIApplication.shared.applicationIconBadgeNumber = 0 +- let center = UNUserNotificationCenter.current() +- center.removeAllDeliveredNotifications() +- center.removeAllPendingNotificationRequests() +- result(true) +- default: +- break +- } +- } +- return twakeApnChannel +- } +- +- override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { +- let token = deviceToken.base64EncodedString() +- // Save the token to use in the future +- UserDefaults.standard.set(token, forKey: apnTokenKey) +- } +- +- override func userNotificationCenter(_ center: UNUserNotificationCenter, +- willPresent notification: UNNotification, +- withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { +- let userInfo = notification.request.content.userInfo +- twakeApnChannel?.invokeMethod("willPresent", arguments: userInfo) +- let isRemoteNotification = userInfo["event_id"] != nil +- // Hide remote noti when in foreground, decrypted noti will show by dart +- completionHandler(isRemoteNotification ? [] : [.alert, .badge, .sound]) +- } +- +- override func userNotificationCenter(_ center: UNUserNotificationCenter, +- didReceive response: UNNotificationResponse, +- withCompletionHandler completionHandler: @escaping () -> Void) { +- let userInfo = response.notification.request.content.userInfo +- twakeApnChannel?.invokeMethod("didReceive", arguments: userInfo) +- completionHandler() ++@main ++class AppDelegate: UIResponder, UIApplicationDelegate { ++ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { ++ // Override point for customization after application launch. ++ return true + } + } From 5210020b1bba33549c75826b71e9fc403f025026 Mon Sep 17 00:00:00 2001 From: MinhDV Date: Thu, 30 Nov 2023 09:48:02 +0700 Subject: [PATCH 3/6] TW-1049 Copy element-x files --- ios/NSE/AttributedString.swift | 83 + ios/NSE/AvatarSize.swift | 110 ++ ios/NSE/BackgroundTaskProtocol.swift | 43 + ios/NSE/BackgroundTaskServiceProtocol.swift | 45 + ios/NSE/Bundle.swift | 57 + ios/NSE/Date.swift | 43 + ios/NSE/DesignKit/Package.resolved | 39 + ios/NSE/DesignKit/Package.swift | 31 + .../Buttons/ElementActionButtonStyle.swift | 91 ++ .../Buttons/ElementCapsuleButtonStyle.swift | 81 + .../Buttons/ElementGhostButtonStyle.swift | 89 ++ .../Sources/Colors/ElementColors.swift | 40 + .../Sources/Common/ElementControlSize.swift | 22 + .../Sources/Shapes/RoundedCornerShape.swift | 32 + .../TextFields/ElementTextFieldStyle.swift | 179 +++ ios/NSE/DesignKit/Tests/DesignKitTests.swift | 8 + ios/NSE/FileManager.swift | 69 + ios/NSE/Generated/Assets.swift | 237 +++ ios/NSE/Generated/Strings+Untranslated.swift | 55 + ios/NSE/Generated/Strings.swift | 1338 +++++++++++++++++ .../HTMLParsing/AttributedStringBuilder.swift | 282 ++++ .../AttributedStringBuilderProtocol.swift | 28 + ...THTMLElement+AttributedStringBuilder.swift | 76 + .../HTMLParsing/ElementXAttributeScope.swift | 69 + .../UIFont+AttributedStringBuilder.h | 27 + .../UIFont+AttributedStringBuilder.m | 83 + ios/NSE/ImageCache.swift | 32 + ios/NSE/Info.plist | 2 +- ios/NSE/InfoPlistReader.swift | 160 ++ ios/NSE/KeychainController.swift | 106 ++ ios/NSE/KeychainControllerProtocol.swift | 31 + ios/NSE/LayoutDirection.swift | 30 + ios/NSE/Logging/MXLog.swift | 197 +++ ios/NSE/Logging/MXLogger.swift | 335 +++++ ios/NSE/Logging/RustTracing.swift | 150 ++ ios/NSE/MatrixEntityRegex.swift | 90 ++ ios/NSE/NSRegularExpresion.swift | 40 + ios/NSE/NotificationConstants.swift | 38 + ios/NSE/PermalinkBuilder.swift | 148 ++ ios/NSE/PlaceholderAvatarImage.swift | 89 ++ ios/NSE/Provider/ImageProviderProtocol.swift | 35 + ios/NSE/Provider/MediaFileHandleProxy.swift | 68 + ios/NSE/Provider/MediaLoader.swift | 88 ++ ios/NSE/Provider/MediaLoaderProtocol.swift | 31 + ios/NSE/Provider/MediaProvider.swift | 126 ++ ios/NSE/Provider/MediaProviderProtocol.swift | 34 + ios/NSE/Provider/MediaSourceProxy.swift | 50 + ios/NSE/Provider/MockMediaProvider.swift | 53 + ios/NSE/Proxy/NotificationItemProxy.swift | 117 ++ .../Proxy/NotificationItemProxyProtocol.swift | 81 + ios/NSE/RestorationToken.swift | 63 + ios/NSE/RoomMessageEventStringBuilder.swift | 78 + ios/NSE/SharedUserDefaultsKeys.swift | 19 + .../Sources/NotificationContentBuilder.swift | 142 ++ .../NotificationServiceExtension.swift | 149 ++ .../Sources/Other/DataProtectionManager.swift | 45 + ios/NSE/Sources/Other/NSELogger.swift | 90 ++ ios/NSE/Sources/Other/NSESettings.swift | 24 + ios/NSE/Sources/Other/NSEUserSession.swift | 77 + .../Sources/Other/UNNotificationRequest.swift | 36 + ios/NSE/String.swift | 89 ++ ios/NSE/Task.swift | 63 + ios/NSE/TestablePreview.swift | 21 + ios/NSE/UNNotificationContent.swift | 252 ++++ ios/NSE/URL.swift | 81 + ios/NSE/UTType.swift | 35 + ios/NSE/UserAgentBuilder.swift | 63 + ios/NSE/UserPreference.swift | 207 +++ ios/Runner.xcodeproj/project.pbxproj | 346 +++-- .../xcshareddata/swiftpm/Package.resolved | 19 +- 70 files changed, 7126 insertions(+), 161 deletions(-) create mode 100644 ios/NSE/AttributedString.swift create mode 100644 ios/NSE/AvatarSize.swift create mode 100644 ios/NSE/BackgroundTaskProtocol.swift create mode 100644 ios/NSE/BackgroundTaskServiceProtocol.swift create mode 100644 ios/NSE/Bundle.swift create mode 100644 ios/NSE/Date.swift create mode 100644 ios/NSE/DesignKit/Package.resolved create mode 100644 ios/NSE/DesignKit/Package.swift create mode 100644 ios/NSE/DesignKit/Sources/Buttons/ElementActionButtonStyle.swift create mode 100644 ios/NSE/DesignKit/Sources/Buttons/ElementCapsuleButtonStyle.swift create mode 100644 ios/NSE/DesignKit/Sources/Buttons/ElementGhostButtonStyle.swift create mode 100644 ios/NSE/DesignKit/Sources/Colors/ElementColors.swift create mode 100644 ios/NSE/DesignKit/Sources/Common/ElementControlSize.swift create mode 100644 ios/NSE/DesignKit/Sources/Shapes/RoundedCornerShape.swift create mode 100644 ios/NSE/DesignKit/Sources/TextFields/ElementTextFieldStyle.swift create mode 100644 ios/NSE/DesignKit/Tests/DesignKitTests.swift create mode 100644 ios/NSE/FileManager.swift create mode 100644 ios/NSE/Generated/Assets.swift create mode 100644 ios/NSE/Generated/Strings+Untranslated.swift create mode 100644 ios/NSE/Generated/Strings.swift create mode 100644 ios/NSE/HTMLParsing/AttributedStringBuilder.swift create mode 100644 ios/NSE/HTMLParsing/AttributedStringBuilderProtocol.swift create mode 100644 ios/NSE/HTMLParsing/DTHTMLElement+AttributedStringBuilder.swift create mode 100644 ios/NSE/HTMLParsing/ElementXAttributeScope.swift create mode 100644 ios/NSE/HTMLParsing/UIFont+AttributedStringBuilder.h create mode 100644 ios/NSE/HTMLParsing/UIFont+AttributedStringBuilder.m create mode 100644 ios/NSE/ImageCache.swift create mode 100644 ios/NSE/InfoPlistReader.swift create mode 100644 ios/NSE/KeychainController.swift create mode 100644 ios/NSE/KeychainControllerProtocol.swift create mode 100644 ios/NSE/LayoutDirection.swift create mode 100644 ios/NSE/Logging/MXLog.swift create mode 100644 ios/NSE/Logging/MXLogger.swift create mode 100644 ios/NSE/Logging/RustTracing.swift create mode 100644 ios/NSE/MatrixEntityRegex.swift create mode 100644 ios/NSE/NSRegularExpresion.swift create mode 100644 ios/NSE/NotificationConstants.swift create mode 100644 ios/NSE/PermalinkBuilder.swift create mode 100644 ios/NSE/PlaceholderAvatarImage.swift create mode 100644 ios/NSE/Provider/ImageProviderProtocol.swift create mode 100644 ios/NSE/Provider/MediaFileHandleProxy.swift create mode 100644 ios/NSE/Provider/MediaLoader.swift create mode 100644 ios/NSE/Provider/MediaLoaderProtocol.swift create mode 100644 ios/NSE/Provider/MediaProvider.swift create mode 100644 ios/NSE/Provider/MediaProviderProtocol.swift create mode 100644 ios/NSE/Provider/MediaSourceProxy.swift create mode 100644 ios/NSE/Provider/MockMediaProvider.swift create mode 100644 ios/NSE/Proxy/NotificationItemProxy.swift create mode 100644 ios/NSE/Proxy/NotificationItemProxyProtocol.swift create mode 100644 ios/NSE/RestorationToken.swift create mode 100644 ios/NSE/RoomMessageEventStringBuilder.swift create mode 100644 ios/NSE/SharedUserDefaultsKeys.swift create mode 100644 ios/NSE/Sources/NotificationContentBuilder.swift create mode 100644 ios/NSE/Sources/NotificationServiceExtension.swift create mode 100644 ios/NSE/Sources/Other/DataProtectionManager.swift create mode 100644 ios/NSE/Sources/Other/NSELogger.swift create mode 100644 ios/NSE/Sources/Other/NSESettings.swift create mode 100644 ios/NSE/Sources/Other/NSEUserSession.swift create mode 100644 ios/NSE/Sources/Other/UNNotificationRequest.swift create mode 100644 ios/NSE/String.swift create mode 100644 ios/NSE/Task.swift create mode 100644 ios/NSE/TestablePreview.swift create mode 100644 ios/NSE/UNNotificationContent.swift create mode 100644 ios/NSE/URL.swift create mode 100644 ios/NSE/UTType.swift create mode 100644 ios/NSE/UserAgentBuilder.swift create mode 100644 ios/NSE/UserPreference.swift diff --git a/ios/NSE/AttributedString.swift b/ios/NSE/AttributedString.swift new file mode 100644 index 0000000000..f5027c7abc --- /dev/null +++ b/ios/NSE/AttributedString.swift @@ -0,0 +1,83 @@ +// +// 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. +// + +import Foundation + +extension AttributedString { + var formattedComponents: [AttributedStringBuilderComponent] { + runs[\.blockquote].map { value, range in + var attributedString = AttributedString(self[range]) + + // Remove trailing new lines if any + if attributedString.characters.last?.isNewline ?? false, + let range = attributedString.range(of: "\n", options: .backwards, locale: nil) { + attributedString.removeSubrange(range) + } + + let isBlockquote = value != nil + + return AttributedStringBuilderComponent(attributedString: attributedString, isBlockquote: isBlockquote) + } + } + + /// Replaces the specified placeholder with a string that links to the specified URL. + /// - Parameters: + /// - linkPlaceholder: The text in the string that will be replaced. Make sure this is unique within the string. + /// - string: The text for the link that will be substituted into the placeholder. + /// - url: The URL that the link should open. + mutating func replace(_ linkPlaceholder: String, with string: String, asLinkTo url: URL) { + // Replace the placeholder with a link. + var replacement = AttributedString(string) + replacement.link = url + replace(linkPlaceholder, with: replacement) + } + + /// Replaces the specified placeholder with the supplied attributed string. + /// - Parameters: + /// - placeholder: The text in the string that will be replaced. Make sure this is unique within the string. + /// - attributedString: The text for the link that will be substituted into the placeholder. + mutating func replace(_ placeholder: String, with replacement: AttributedString) { + guard let range = range(of: placeholder) else { + MXLog.failure("Failed to find the placeholder to be replaced.") + return + } + + // Replace the placeholder. + replaceSubrange(range, with: replacement) + } + + /// Returns a new attributed string, created by replacing any hard coded `UIFont` with + /// a simple presentation intent. This allows simple formatting to respond to Dynamic Type. + /// + /// Currently only supports regular and bold weights. + func replacingFontWithPresentationIntent() -> AttributedString { + var newValue = self + for run in newValue.runs { + guard let font = run.uiKit.font else { continue } + newValue[run.range].inlinePresentationIntent = font.fontDescriptor.symbolicTraits.contains(.traitBold) ? .stronglyEmphasized : nil + newValue[run.range].uiKit.font = nil + } + return newValue + } + + /// Makes the entire string bold by setting the presentation intent to strongly emphasized. + /// + /// In practice, this is rendered as semibold for smaller font sizes and just so happens to nicely + /// line up with the semibold → bold font switch used by compound. + mutating func bold() { + self[startIndex.. Void + +/// BackgroundTaskProtocol is the protocol describing a background task regardless of the platform used. +protocol BackgroundTaskProtocol: AnyObject { + /// Name of the background task for debug. + var name: String { get } + + /// `true` if the background task is currently running. + var isRunning: Bool { get } + + /// Flag indicating the background task is reusable. If reusable, `name` is the key to distinguish background tasks. + var isReusable: Bool { get } + + /// Elapsed time after the task started. In milliseconds. + var elapsedTime: TimeInterval { get } + + /// Expiration handler for the background task + var expirationHandler: BackgroundTaskExpirationHandler? { get } + + /// Method to be called when a task reused one more time. Should only be valid for reusable tasks. + func reuse() + + /// Stop the background task. Cannot be started anymore. For reusable tasks, should be called same number of times `reuse` called. + func stop() +} diff --git a/ios/NSE/BackgroundTaskServiceProtocol.swift b/ios/NSE/BackgroundTaskServiceProtocol.swift new file mode 100644 index 0000000000..568fcee14c --- /dev/null +++ b/ios/NSE/BackgroundTaskServiceProtocol.swift @@ -0,0 +1,45 @@ +// +// Copyright 2022 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. +// + +import Foundation + +@MainActor +protocol BackgroundTaskServiceProtocol { + func startBackgroundTask(withName name: String, + isReusable: Bool, + expirationHandler: (() -> Void)?) -> BackgroundTaskProtocol? +} + +extension BackgroundTaskServiceProtocol { + func startBackgroundTask(withName name: String) -> BackgroundTaskProtocol? { + startBackgroundTask(withName: name, + expirationHandler: nil) + } + + func startBackgroundTask(withName name: String, + isReusable: Bool) -> BackgroundTaskProtocol? { + startBackgroundTask(withName: name, + isReusable: isReusable, + expirationHandler: nil) + } + + func startBackgroundTask(withName name: String, + expirationHandler: (() -> Void)?) -> BackgroundTaskProtocol? { + startBackgroundTask(withName: name, + isReusable: false, + expirationHandler: expirationHandler) + } +} diff --git a/ios/NSE/Bundle.swift b/ios/NSE/Bundle.swift new file mode 100644 index 0000000000..9ddaa8b4a7 --- /dev/null +++ b/ios/NSE/Bundle.swift @@ -0,0 +1,57 @@ +// +// Copyright 2022 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. +// + +import Foundation + +public extension Bundle { + /// The top-level bundle that contains the entire app. + static var app: Bundle { + var bundle = Bundle.main + if bundle.bundleURL.pathExtension == "appex" { + // Peel off two directory levels - MY_APP.app/PlugIns/MY_APP_EXTENSION.appex + let url = bundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent() + if let otherBundle = Bundle(url: url) { + bundle = otherBundle + } + } + return bundle + } + + // MARK: - Localisation + + private static var cachedLocalizationBundles = [String: Bundle]() + + /// Get an lproj language bundle from the receiver bundle. + /// - Parameter language: The language to try to load. + /// - Returns: The lproj bundle if found otherwise nil. + static func lprojBundle(for language: String) -> Bundle? { + if let bundle = cachedLocalizationBundles[language] { + return bundle + } + + guard let lprojURL = Bundle.app.url(forResource: language, withExtension: "lproj") else { + return nil + } + + let bundle = Bundle(url: lprojURL) + cachedLocalizationBundles[language] = bundle + + return bundle + } + + /// Overrides `Bundle.app.preferredLocalizations` for testing translations. + static var overrideLocalizations: [String]? +} diff --git a/ios/NSE/Date.swift b/ios/NSE/Date.swift new file mode 100644 index 0000000000..1e292c39ec --- /dev/null +++ b/ios/NSE/Date.swift @@ -0,0 +1,43 @@ +// +// 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. +// + +import Foundation + +extension Date { + /// The date formatted with the minimal necessary units given how long ago it occurred. + func formattedMinimal() -> String { + let calendar = Calendar.current + + if calendar.isDateInToday(self) { + // Just the time if it was today. + return formatted(date: .omitted, time: .shortened) + } else if calendar.isDateInYesterday(self) { + // Simply "Yesterday" if it was yesterday. + return formatted(Date.RelativeFormatStyle(presentation: .named, capitalizationContext: .beginningOfSentence)) + } else if let sixDaysAgo = calendar.date(byAdding: .day, value: -6, to: calendar.startOfDay(for: .now)), + sixDaysAgo <= self { + // The named day if it was in the last 6 days. + return formatted(.dateTime.weekday(.wide)) + } else if let oneYearAgo = calendar.date(byAdding: .year, value: -1, to: .now), + oneYearAgo <= self { + // The day and month if it was in the past year + return formatted(.dateTime.day().month()) + } else { + // The day, month and year if it is any older. + return formatted(.dateTime.year().day().month()) + } + } +} diff --git a/ios/NSE/DesignKit/Package.resolved b/ios/NSE/DesignKit/Package.resolved new file mode 100644 index 0000000000..9466c8f8ad --- /dev/null +++ b/ios/NSE/DesignKit/Package.resolved @@ -0,0 +1,39 @@ +{ + "pins" : [ + { + "identity" : "compound-design-tokens", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vector-im/compound-design-tokens.git", + "state" : { + "revision" : "d9d1a792d8a124708c7e15becd359893ee9e9ea6" + } + }, + { + "identity" : "compound-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vector-im/compound-ios.git", + "state" : { + "revision" : "a6aec9a77bf008c86a296ba17d60005b5a8bfae4" + } + }, + { + "identity" : "element-design-tokens", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vector-im/element-design-tokens.git", + "state" : { + "revision" : "63e40f10b336c136d6d05f7967e4565e37d3d760", + "version" : "0.0.3" + } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", + "state" : { + "revision" : "5b3f3996c7a2a84d5f4ba0e03cd7d584154778f2", + "version" : "0.3.1" + } + } + ], + "version" : 2 +} diff --git a/ios/NSE/DesignKit/Package.swift b/ios/NSE/DesignKit/Package.swift new file mode 100644 index 0000000000..8124d5be1e --- /dev/null +++ b/ios/NSE/DesignKit/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "DesignKit", + platforms: [ + .iOS(.v16) + ], + products: [ + .library(name: "DesignKit", targets: ["DesignKit"]) + ], + dependencies: [ + .package(url: "https://github.com/vector-im/compound-ios", revision: "e8c097e545a06a2ef3036af33192a07c58fafd1b"), + .package(url: "https://github.com/vector-im/element-design-tokens", exact: "0.0.3"), + .package(url: "https://github.com/siteline/SwiftUI-Introspect", from: "0.9.0") + ], + targets: [ + .target(name: "DesignKit", + dependencies: [ + .product(name: "Compound", package: "compound-ios"), + .product(name: "DesignTokens", package: "element-design-tokens"), + .product(name: "SwiftUIIntrospect", package: "SwiftUI-Introspect") + ], + path: "Sources"), + .testTarget(name: "DesignKitTests", + dependencies: ["DesignKit"], + path: "Tests") + ] +) diff --git a/ios/NSE/DesignKit/Sources/Buttons/ElementActionButtonStyle.swift b/ios/NSE/DesignKit/Sources/Buttons/ElementActionButtonStyle.swift new file mode 100644 index 0000000000..4c062c40b3 --- /dev/null +++ b/ios/NSE/DesignKit/Sources/Buttons/ElementActionButtonStyle.swift @@ -0,0 +1,91 @@ +// +// Copyright 2022 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. +// + +import Compound +import DesignTokens +import SwiftUI + +public extension ButtonStyle where Self == ElementActionButtonStyle { + /// The CTA button style as defined in Compound. + /// - Parameter size: The control size to use. Defaults to regular. + /// - Parameter color: The color of the button's background. Defaults to the accent color. + static func elementAction(_ size: ElementControlSize = .regular, + color: Color = .compound.textActionPrimary) -> ElementActionButtonStyle { + ElementActionButtonStyle(size: size, color: color) + } +} + +public struct ElementActionButtonStyle: ButtonStyle { + @Environment(\.isEnabled) private var isEnabled + @Environment(\.colorScheme) private var colorScheme + + public var size: ElementControlSize + public var color: Color + + private var cornerRadius: CGFloat { size == .xLarge ? 14 : 8 } + private var verticalPadding: CGFloat { size == .xLarge ? 14 : 4 } + private var maxWidth: CGFloat? { size == .xLarge ? .infinity : nil } + + private var fontColor: Color { + Color.compound.textOnSolidPrimary + .opacity(colorScheme == .dark && !isEnabled ? 0.3 : 1.0) + } + + public init(size: ElementControlSize = .regular, color: Color = .compound.textActionPrimary) { + self.size = size + self.color = color + } + + public func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .padding(.horizontal, 12) + .padding(.vertical, verticalPadding) + .frame(maxWidth: maxWidth) + .foregroundColor(fontColor) + .font(.compound.bodyLGSemibold) + .background(Capsule() + .fill(color) + .opacity(backgroundOpacity(when: configuration.isPressed))) + } + + private func backgroundOpacity(when isPressed: Bool) -> CGFloat { + guard isEnabled else { return colorScheme == .dark ? 0.2 : 0.1 } + return isPressed ? 0.3 : 1.0 + } +} + +public struct ElementActionButtonStyle_Previews: PreviewProvider { + public static var previews: some View { + VStack { + Button("Enabled") { /* preview */ } + .buttonStyle(ElementActionButtonStyle()) + + Button("Disabled") { /* preview */ } + .buttonStyle(ElementActionButtonStyle()) + .disabled(true) + + Button { /* preview */ } label: { + Text("Clear BG") + .foregroundColor(.compound.textCriticalPrimary) + } + .buttonStyle(ElementActionButtonStyle(color: .clear)) + + Button("Red BG") { /* preview */ } + .buttonStyle(ElementActionButtonStyle(color: .compound.textCriticalPrimary)) + } + .padding() + } +} diff --git a/ios/NSE/DesignKit/Sources/Buttons/ElementCapsuleButtonStyle.swift b/ios/NSE/DesignKit/Sources/Buttons/ElementCapsuleButtonStyle.swift new file mode 100644 index 0000000000..38033c6c1b --- /dev/null +++ b/ios/NSE/DesignKit/Sources/Buttons/ElementCapsuleButtonStyle.swift @@ -0,0 +1,81 @@ +// +// Copyright 2022 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. +// + +import SwiftUI + +public extension ButtonStyle where Self == ElementCapsuleButtonStyle { + /// A button style that uses a capsule shape with a regular appearance. + static var elementCapsule: ElementCapsuleButtonStyle { + ElementCapsuleButtonStyle(isProminent: false) + } + + /// A button style that uses a capsule shape with a prominent appearance. + static var elementCapsuleProminent: ElementCapsuleButtonStyle { + ElementCapsuleButtonStyle(isProminent: true) + } +} + +public struct ElementCapsuleButtonStyle: ButtonStyle { + let isProminent: Bool + + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding(7) + .frame(maxWidth: .infinity) + .font(.compound.bodyLGSemibold) + .foregroundColor(fontColor) + .multilineTextAlignment(.center) + .background(background) + .opacity(configuration.isPressed ? 0.6 : 1) + .contentShape(Capsule()) + } + + @ViewBuilder + var background: some View { + if isProminent { + Capsule() + .foregroundColor(Color.compound.textActionPrimary) + } else { + Capsule() + .stroke(Color.compound.textActionPrimary) + } + } + + var fontColor: Color { + isProminent ? .compound.textOnSolidPrimary : .compound.textPrimary + } +} + +struct ElementCapsuleButtonStyle_Previews: PreviewProvider { + public static var previews: some View { + VStack { + Button("Enabled") { /* preview */ } + .buttonStyle(.elementCapsuleProminent) + + Button("Disabled") { /* preview */ } + .buttonStyle(.elementCapsuleProminent) + .disabled(true) + + Button("Enabled") { /* preview */ } + .buttonStyle(.elementCapsule) + + Button("Disabled") { /* preview */ } + .buttonStyle(.elementCapsule) + .disabled(true) + } + .padding() + } +} diff --git a/ios/NSE/DesignKit/Sources/Buttons/ElementGhostButtonStyle.swift b/ios/NSE/DesignKit/Sources/Buttons/ElementGhostButtonStyle.swift new file mode 100644 index 0000000000..9ce37ee32d --- /dev/null +++ b/ios/NSE/DesignKit/Sources/Buttons/ElementGhostButtonStyle.swift @@ -0,0 +1,89 @@ +// +// Copyright 2022 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. +// + +import Compound +import DesignTokens +import SwiftUI + +public extension ButtonStyle where Self == ElementGhostButtonStyle { + /// The Ghost button style as defined in Compound. + /// - Parameter size: The control size to use. Defaults to `regular`. + /// - Parameter color: The color of the label and border. Defaults to the accent color. + static func elementGhost(_ size: ElementControlSize = .regular, + color: Color = .compound.textActionAccent) -> ElementGhostButtonStyle { + ElementGhostButtonStyle(size: size, color: color) + } +} + +public struct ElementGhostButtonStyle: ButtonStyle { + @Environment(\.isEnabled) private var isEnabled + + public var size: ElementControlSize + public var color: Color + + private var verticalPadding: CGFloat { size == .xLarge ? 12 : 4 } + private var maxWidth: CGFloat? { size == .xLarge ? .infinity : nil } + + public init(size: ElementControlSize = .regular, color: Color = .compound.textActionAccent) { + self.size = size + self.color = color + } + + public func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .padding(.horizontal, 12) + .padding(.vertical, verticalPadding) + .frame(maxWidth: maxWidth) + .foregroundColor(color) + .font(.compound.bodySMSemibold) + .background(border) + .opacity(opacity(when: configuration.isPressed)) + } + + private var border: some View { + Capsule() + .strokeBorder() + .foregroundColor(color) + } + + private func opacity(when isPressed: Bool) -> CGFloat { + guard isEnabled else { return 0.5 } + return isPressed ? 0.6 : 1.0 + } +} + +public struct ElementGhostButtonStyle_Previews: PreviewProvider { + public static var previews: some View { + VStack { + Button("Enabled") { /* preview */ } + .buttonStyle(ElementGhostButtonStyle()) + + Button("Disabled") { /* preview */ } + .buttonStyle(ElementGhostButtonStyle()) + .disabled(true) + + Button("Red BG") { /* preview */ } + .buttonStyle(ElementGhostButtonStyle(color: .compound.textCriticalPrimary)) + + Button { /* preview */ } label: { + Text("Custom") + .foregroundColor(.compound.iconInfoPrimary) + } + .buttonStyle(ElementGhostButtonStyle(color: .compound.borderInfoSubtle)) + } + .padding() + } +} diff --git a/ios/NSE/DesignKit/Sources/Colors/ElementColors.swift b/ios/NSE/DesignKit/Sources/Colors/ElementColors.swift new file mode 100644 index 0000000000..0808fa8bc4 --- /dev/null +++ b/ios/NSE/DesignKit/Sources/Colors/ElementColors.swift @@ -0,0 +1,40 @@ +// +// Copyright 2022 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. +// + +import DesignTokens +import SwiftUI + +// MARK: SwiftUI + +public extension Color { + static let element = ElementColors() +} + +public struct ElementColors { + // MARK: - Legacy Compound + + private let colors = DesignTokens.CompoundColors() + + @available(swift, deprecated: 5.0, message: "Use textActionAccent/iconAccentTertiary from Compound.") + public var brand: Color { colors.accent } + + // MARK: - Temp + + /// The background colour of a row in a Form or grouped List. + /// + /// This colour will be removed once Compound form styles are used everywhere. + public var formRowBackground = Color.compound.bgCanvasDefaultLevel1 +} diff --git a/ios/NSE/DesignKit/Sources/Common/ElementControlSize.swift b/ios/NSE/DesignKit/Sources/Common/ElementControlSize.swift new file mode 100644 index 0000000000..5c43a27589 --- /dev/null +++ b/ios/NSE/DesignKit/Sources/Common/ElementControlSize.swift @@ -0,0 +1,22 @@ +// +// Copyright 2022 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. +// + +import Foundation + +public enum ElementControlSize { + case regular + case xLarge +} diff --git a/ios/NSE/DesignKit/Sources/Shapes/RoundedCornerShape.swift b/ios/NSE/DesignKit/Sources/Shapes/RoundedCornerShape.swift new file mode 100644 index 0000000000..6f363fb941 --- /dev/null +++ b/ios/NSE/DesignKit/Sources/Shapes/RoundedCornerShape.swift @@ -0,0 +1,32 @@ +// +// Copyright 2022 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. +// + +import SwiftUI + +public struct RoundedCornerShape: Shape { + let radius: CGFloat + let corners: UIRectCorner + + public init(radius: CGFloat, corners: UIRectCorner) { + self.radius = radius + self.corners = corners + } + + public func path(in rect: CGRect) -> Path { + let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) + return Path(path.cgPath) + } +} diff --git a/ios/NSE/DesignKit/Sources/TextFields/ElementTextFieldStyle.swift b/ios/NSE/DesignKit/Sources/TextFields/ElementTextFieldStyle.swift new file mode 100644 index 0000000000..08b5821753 --- /dev/null +++ b/ios/NSE/DesignKit/Sources/TextFields/ElementTextFieldStyle.swift @@ -0,0 +1,179 @@ +// +// Copyright 2022 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. +// + +import DesignTokens +import SwiftUI +import SwiftUIIntrospect + +public extension TextFieldStyle where Self == ElementTextFieldStyle { + static func elementInput(labelText: String? = nil, + footerText: String? = nil, + isError: Bool = false, + accessibilityIdentifier: String? = nil) -> ElementTextFieldStyle { + ElementTextFieldStyle(labelText: labelText.map(Text.init), + footerText: footerText.map(Text.init), + isError: isError, + accessibilityIdentifier: accessibilityIdentifier) + } + + @_disfavoredOverload + static func elementInput(labelText: Text? = nil, + footerText: Text? = nil, + isError: Bool = false, + accessibilityIdentifier: String? = nil) -> ElementTextFieldStyle { + ElementTextFieldStyle(labelText: labelText, + footerText: footerText, + isError: isError, + accessibilityIdentifier: accessibilityIdentifier) + } +} + +/// A bordered style of text input with a label and a footer +/// +/// As defined in: +/// https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=2039%3A26415 +public struct ElementTextFieldStyle: TextFieldStyle { + @Environment(\.isEnabled) private var isEnabled + @Environment(\.colorScheme) private var colorScheme + + @FocusState private var isFocused: Bool + public let labelText: Text? + public let footerText: Text? + public let isError: Bool + public let accessibilityIdentifier: String? + + /// The color of the text field's border. + private var borderColor: Color { + isError ? .compound.textCriticalPrimary : .compound._borderTextFieldFocused + } + + /// The width of the text field's border. + private var borderWidth: CGFloat { + isFocused || isError ? 1.0 : 0 + } + + private var accentColor: Color { + isError ? .compound.textCriticalPrimary : .compound.iconAccentTertiary + } + + /// The color of the text inside the text field. + private var textColor: Color { + isEnabled ? .compound.textPrimary : .compound.textDisabled + } + + /// The color of the text field's background. + private var backgroundColor: Color { + .compound.bgSubtleSecondary.opacity(isEnabled ? 1 : 0.5) + } + + /// The color of the placeholder text inside the text field. + private var placeholderColor: UIColor { + .compound.textPlaceholder + } + + /// The color of the label above the text field. + private var labelColor: Color { + isEnabled ? .compound.textPrimary : .compound.textDisabled + } + + /// The color of the footer label below the text field. + private var footerColor: Color { + isError ? .compound.textCriticalPrimary : .compound.textSecondary + } + + /// Creates the text field style configured as required. + /// - Parameters: + /// - labelText: The text shown in the label above the field. + /// - footerText: The text shown in the footer label below the field. + /// - isError: Whether or not the text field is currently in the error state. + public init(labelText: Text? = nil, footerText: Text? = nil, isError: Bool = false, accessibilityIdentifier: String? = nil) { + self.labelText = labelText + self.footerText = footerText + self.isError = isError + self.accessibilityIdentifier = accessibilityIdentifier + } + + public func _body(configuration: TextField<_Label>) -> some View { + let rectangle = RoundedRectangle(cornerRadius: 14.0) + + return VStack(alignment: .leading, spacing: 8) { + labelText + .font(.compound.bodySM) + .foregroundColor(labelColor) + .padding(.horizontal, 16) + + configuration + .focused($isFocused) + .font(.compound.bodyLG) + .foregroundColor(textColor) + .accentColor(accentColor) + .padding(.leading, 16.0) + .padding([.vertical, .trailing], 11.0) + .background { + ZStack { + backgroundColor + .clipShape(rectangle) + rectangle + .stroke(borderColor, lineWidth: borderWidth) + } + .onTapGesture { isFocused = true } // Set focus with taps outside of the text field + } + .introspect(.textField, on: .iOS(.v16)) { textField in + textField.clearButtonMode = .whileEditing + textField.attributedPlaceholder = NSAttributedString(string: textField.placeholder ?? "", + attributes: [NSAttributedString.Key.foregroundColor: placeholderColor]) + textField.accessibilityIdentifier = accessibilityIdentifier + } + + footerText + .tint(.compound.textLinkExternal) + .font(.compound.bodyXS) + .foregroundColor(footerColor) + .padding(.horizontal, 16) + } + } +} + +struct ElementTextFieldStyle_Previews: PreviewProvider { + public static var previews: some View { + ScrollView { + VStack(spacing: 20) { + // Plain text field. + TextField("Placeholder", text: .constant("")) + .textFieldStyle(.elementInput()) + TextField("Placeholder", text: .constant("Web")) + .textFieldStyle(.elementInput()) + TextField("Placeholder", text: .constant("Web")) + .textFieldStyle(.elementInput()) + .disabled(true) + TextField("Placeholder", text: .constant("Web")) + .textFieldStyle(.elementInput(isError: true)) + + // Text field with labels + TextField("Placeholder", text: .constant("")) + .textFieldStyle(.elementInput(labelText: "Label", footerText: "Footer")) + TextField("Placeholder", text: .constant("Input text")) + .textFieldStyle(.elementInput(labelText: "Title", footerText: "Footer")) + TextField("Placeholder", text: .constant("Bad text")) + .textFieldStyle(.elementInput(labelText: "Title", footerText: "Footer", isError: true)) + TextField("Placeholder", text: .constant("")) + .textFieldStyle(.elementInput(labelText: "Title", footerText: "Footer")) + .disabled(true) + } + .padding() + } + } +} diff --git a/ios/NSE/DesignKit/Tests/DesignKitTests.swift b/ios/NSE/DesignKit/Tests/DesignKitTests.swift new file mode 100644 index 0000000000..d2f41381c1 --- /dev/null +++ b/ios/NSE/DesignKit/Tests/DesignKitTests.swift @@ -0,0 +1,8 @@ +@testable import DesignKit +import XCTest + +final class DesignKitTests: XCTestCase { + func testExample() throws { + XCTAssert(true) + } +} diff --git a/ios/NSE/FileManager.swift b/ios/NSE/FileManager.swift new file mode 100644 index 0000000000..f99e57b51d --- /dev/null +++ b/ios/NSE/FileManager.swift @@ -0,0 +1,69 @@ +// +// Copyright 2022 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. +// + +import Foundation + +enum FileManagerError: Error { + case invalidFileSize +} + +extension FileManager { + func directoryExists(at url: URL) -> Bool { + var isDirectory: ObjCBool = false + guard fileExists(atPath: url.path(), isDirectory: &isDirectory) else { + return false + } + return isDirectory.boolValue + } + + func createDirectoryIfNeeded(at url: URL, withIntermediateDirectories: Bool = true) throws { + guard !directoryExists(at: url) else { + return + } + try createDirectory(at: url, withIntermediateDirectories: withIntermediateDirectories) + } + + func copyFileToTemporaryDirectory(file url: URL, with filename: String? = nil) throws -> URL { + let newURL = URL.temporaryDirectory.appendingPathComponent(filename ?? url.lastPathComponent) + + try? removeItem(at: newURL) + try copyItem(at: url, to: newURL) + + return newURL + } + + @discardableResult + func writeDataToTemporaryDirectory(data: Data, fileName: String) throws -> URL { + let newURL = URL.temporaryDirectory.appendingPathComponent(fileName) + + try data.write(to: newURL) + + return newURL + } + + /// Retrieve a file's disk size + /// - Parameter url: the file URL + /// - Returns: the size in bytes + func sizeForItem(at url: URL) throws -> Double { + let attributes = try attributesOfItem(atPath: url.path()) + + guard let size = attributes[FileAttributeKey.size] as? Double else { + throw FileManagerError.invalidFileSize + } + + return size + } +} diff --git a/ios/NSE/Generated/Assets.swift b/ios/NSE/Generated/Assets.swift new file mode 100644 index 0000000000..63ba6fd048 --- /dev/null +++ b/ios/NSE/Generated/Assets.swift @@ -0,0 +1,237 @@ +// swiftlint:disable all +// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen + +#if os(macOS) + import AppKit +#elseif os(iOS) + import UIKit +#elseif os(tvOS) || os(watchOS) + import UIKit +#endif +#if canImport(SwiftUI) + import SwiftUI +#endif + +// Deprecated typealiases +@available(*, deprecated, renamed: "ColorAsset.Color", message: "This typealias will be removed in SwiftGen 7.0") +internal typealias AssetColorTypeAlias = ColorAsset.Color +@available(*, deprecated, renamed: "ImageAsset.Image", message: "This typealias will be removed in SwiftGen 7.0") +internal typealias AssetImageTypeAlias = ImageAsset.Image + +// swiftlint:disable superfluous_disable_command file_length implicit_return + +// MARK: - Asset Catalogs + +// swiftlint:disable identifier_name line_length nesting type_body_length type_name +internal enum Asset { + internal enum Colors { + internal static let accentColor = ColorAsset(name: "colors/accent-color") + internal static let backgroundColor = ColorAsset(name: "colors/background-color") + internal static let grabber = ColorAsset(name: "colors/grabber") + } + internal enum Images { + internal static let appLogo = ImageAsset(name: "images/app-logo") + internal static let serverSelectionIcon = ImageAsset(name: "images/server-selection-icon") + internal static let closeCircle = ImageAsset(name: "images/close-circle") + internal static let addLocation = ImageAsset(name: "images/add-location") + internal static let attachment = ImageAsset(name: "images/attachment") + internal static let photosLibrary = ImageAsset(name: "images/photos-library") + internal static let takePhoto = ImageAsset(name: "images/take-photo") + internal static let textFormatting = ImageAsset(name: "images/text-formatting") + internal static let bold = ImageAsset(name: "images/bold") + internal static let bulletList = ImageAsset(name: "images/bullet-list") + internal static let closeRte = ImageAsset(name: "images/close-rte") + internal static let codeBlock = ImageAsset(name: "images/code-block") + internal static let composerAttachment = ImageAsset(name: "images/composer-attachment") + internal static let editing = ImageAsset(name: "images/editing") + internal static let indent = ImageAsset(name: "images/indent") + internal static let inlineCode = ImageAsset(name: "images/inline-code") + internal static let italic = ImageAsset(name: "images/italic") + internal static let link = ImageAsset(name: "images/link") + internal static let numberedList = ImageAsset(name: "images/numbered-list") + internal static let quote = ImageAsset(name: "images/quote") + internal static let sendMessage = ImageAsset(name: "images/send-message") + internal static let strikethrough = ImageAsset(name: "images/strikethrough") + internal static let textFormat = ImageAsset(name: "images/text-format") + internal static let underline = ImageAsset(name: "images/underline") + internal static let unindent = ImageAsset(name: "images/unindent") + internal static let decryptionError = ImageAsset(name: "images/decryption-error") + internal static let endedPoll = ImageAsset(name: "images/ended-poll") + internal static let compose = ImageAsset(name: "images/compose") + internal static let launchBackground = ImageAsset(name: "images/launch-background") + internal static let locationMarker = ImageAsset(name: "images/location-marker") + internal static let locationPin = ImageAsset(name: "images/location-pin") + internal static let locationPointerFull = ImageAsset(name: "images/location-pointer-full") + internal static let locationPointer = ImageAsset(name: "images/location-pointer") + internal static let addReaction = ImageAsset(name: "images/add-reaction") + internal static let copy = ImageAsset(name: "images/copy") + internal static let editOutline = ImageAsset(name: "images/edit-outline") + internal static let forward = ImageAsset(name: "images/forward") + internal static let reply = ImageAsset(name: "images/reply") + internal static let viewSource = ImageAsset(name: "images/view-source") + internal static let timelineEndedPoll = ImageAsset(name: "images/timeline-ended-poll") + internal static let timelinePollAttachment = ImageAsset(name: "images/timeline-poll-attachment") + internal static let timelinePoll = ImageAsset(name: "images/timeline-poll") + internal static let waitingGradient = ImageAsset(name: "images/waiting-gradient") + } +} +// swiftlint:enable identifier_name line_length nesting type_body_length type_name + +// MARK: - Implementation Details + +internal final class ColorAsset { + internal fileprivate(set) var name: String + + #if os(macOS) + internal typealias Color = NSColor + #elseif os(iOS) || os(tvOS) || os(watchOS) + internal typealias Color = UIColor + #endif + + @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) + internal private(set) lazy var color: Color = { + guard let color = Color(asset: self) else { + fatalError("Unable to load color asset named \(name).") + } + return color + }() + + #if os(iOS) || os(tvOS) + @available(iOS 11.0, tvOS 11.0, *) + internal func color(compatibleWith traitCollection: UITraitCollection) -> Color { + let bundle = BundleToken.bundle + guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else { + fatalError("Unable to load color asset named \(name).") + } + return color + } + #endif + + #if canImport(SwiftUI) + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + internal private(set) lazy var swiftUIColor: SwiftUI.Color = { + SwiftUI.Color(asset: self) + }() + #endif + + fileprivate init(name: String) { + self.name = name + } +} + +internal extension ColorAsset.Color { + @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) + convenience init?(asset: ColorAsset) { + let bundle = BundleToken.bundle + #if os(iOS) || os(tvOS) + self.init(named: asset.name, in: bundle, compatibleWith: nil) + #elseif os(macOS) + self.init(named: NSColor.Name(asset.name), bundle: bundle) + #elseif os(watchOS) + self.init(named: asset.name) + #endif + } +} + +#if canImport(SwiftUI) +@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) +internal extension SwiftUI.Color { + init(asset: ColorAsset) { + let bundle = BundleToken.bundle + self.init(asset.name, bundle: bundle) + } +} +#endif + +internal struct ImageAsset { + internal fileprivate(set) var name: String + + #if os(macOS) + internal typealias Image = NSImage + #elseif os(iOS) || os(tvOS) || os(watchOS) + internal typealias Image = UIImage + #endif + + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *) + internal var image: Image { + let bundle = BundleToken.bundle + #if os(iOS) || os(tvOS) + let image = Image(named: name, in: bundle, compatibleWith: nil) + #elseif os(macOS) + let name = NSImage.Name(self.name) + let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name) + #elseif os(watchOS) + let image = Image(named: name) + #endif + guard let result = image else { + fatalError("Unable to load image asset named \(name).") + } + return result + } + + #if os(iOS) || os(tvOS) + @available(iOS 8.0, tvOS 9.0, *) + internal func image(compatibleWith traitCollection: UITraitCollection) -> Image { + let bundle = BundleToken.bundle + guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else { + fatalError("Unable to load image asset named \(name).") + } + return result + } + #endif + + #if canImport(SwiftUI) + @available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) + internal var swiftUIImage: SwiftUI.Image { + SwiftUI.Image(asset: self) + } + #endif +} + +internal extension ImageAsset.Image { + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, *) + @available(macOS, deprecated, + message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") + convenience init?(asset: ImageAsset) { + #if os(iOS) || os(tvOS) + let bundle = BundleToken.bundle + self.init(named: asset.name, in: bundle, compatibleWith: nil) + #elseif os(macOS) + self.init(named: NSImage.Name(asset.name)) + #elseif os(watchOS) + self.init(named: asset.name) + #endif + } +} + +#if canImport(SwiftUI) +@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *) +internal extension SwiftUI.Image { + init(asset: ImageAsset) { + let bundle = BundleToken.bundle + self.init(asset.name, bundle: bundle) + } + + init(asset: ImageAsset, label: Text) { + let bundle = BundleToken.bundle + self.init(asset.name, bundle: bundle, label: label) + } + + init(decorative asset: ImageAsset) { + let bundle = BundleToken.bundle + self.init(decorative: asset.name, bundle: bundle) + } +} +#endif + +// swiftlint:disable convenience_type +private final class BundleToken { + static let bundle: Bundle = { + #if SWIFT_PACKAGE + return Bundle.module + #else + return Bundle(for: BundleToken.self) + #endif + }() +} +// swiftlint:enable convenience_type diff --git a/ios/NSE/Generated/Strings+Untranslated.swift b/ios/NSE/Generated/Strings+Untranslated.swift new file mode 100644 index 0000000000..2fb62c2fab --- /dev/null +++ b/ios/NSE/Generated/Strings+Untranslated.swift @@ -0,0 +1,55 @@ +// swiftlint:disable all +// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen + +import Foundation + +// swiftlint:disable superfluous_disable_command file_length implicit_return + +// MARK: - Strings + +// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces +public enum UntranslatedL10n { + /// Clear all data currently stored on this device? + /// Sign in again to access your account data and messages. + public static var softLogoutClearDataDialogContent: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_dialog_content") } + /// Clear data + public static var softLogoutClearDataDialogTitle: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_dialog_title") } + /// Warning: Your personal data (including encryption keys) is still stored on this device. + /// + /// Clear it if you’re finished using this device, or want to sign in to another account. + public static var softLogoutClearDataNotice: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_notice") } + /// Clear all data + public static var softLogoutClearDataSubmit: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_submit") } + /// Clear personal data + public static var softLogoutClearDataTitle: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_title") } + /// Sign in to recover encryption keys stored exclusively on this device. You need them to read all of your secure messages on any device. + public static var softLogoutSigninE2eWarningNotice: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_signin_e2e_warning_notice") } + /// Your homeserver (%1$s) admin has signed you out of your account %2$s (%3$s). + public static func softLogoutSigninNotice(_ p1: UnsafePointer, _ p2: UnsafePointer, _ p3: UnsafePointer) -> String { + return UntranslatedL10n.tr("Untranslated", "soft_logout_signin_notice", p1, p2, p3) + } + /// Sign in + public static var softLogoutSigninTitle: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_signin_title") } + /// Untranslated + public static var untranslated: String { return UntranslatedL10n.tr("Untranslated", "untranslated") } + /// Plural format key: "%#@VARIABLE@" + public static func untranslatedPlural(_ p1: Int) -> String { + return UntranslatedL10n.tr("Untranslated", "untranslated_plural", p1) + } +} +// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces + +// MARK: - Implementation Details + +extension UntranslatedL10n { + static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { + // No need to check languages, we always default to en for untranslated strings + guard let bundle = Bundle.lprojBundle(for: "en") else { return key } + let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "") + return String(format: format, locale: Locale(identifier: "en"), arguments: args) + } +} + +// swiftlint:enable all diff --git a/ios/NSE/Generated/Strings.swift b/ios/NSE/Generated/Strings.swift new file mode 100644 index 0000000000..c5777a1fed --- /dev/null +++ b/ios/NSE/Generated/Strings.swift @@ -0,0 +1,1338 @@ +// swiftlint:disable all +// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen + +import Foundation + +// swiftlint:disable superfluous_disable_command file_length implicit_return + +// MARK: - Strings + +// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces +public enum L10n { + /// Hide password + public static var a11yHidePassword: String { return L10n.tr("Localizable", "a11y_hide_password") } + /// Mentions only + public static var a11yNotificationsMentionsOnly: String { return L10n.tr("Localizable", "a11y_notifications_mentions_only") } + /// Muted + public static var a11yNotificationsMuted: String { return L10n.tr("Localizable", "a11y_notifications_muted") } + /// Poll + public static var a11yPoll: String { return L10n.tr("Localizable", "a11y_poll") } + /// Ended poll + public static var a11yPollEnd: String { return L10n.tr("Localizable", "a11y_poll_end") } + /// Send files + public static var a11ySendFiles: String { return L10n.tr("Localizable", "a11y_send_files") } + /// Show password + public static var a11yShowPassword: String { return L10n.tr("Localizable", "a11y_show_password") } + /// User menu + public static var a11yUserMenu: String { return L10n.tr("Localizable", "a11y_user_menu") } + /// Accept + public static var actionAccept: String { return L10n.tr("Localizable", "action_accept") } + /// Add to timeline + public static var actionAddToTimeline: String { return L10n.tr("Localizable", "action_add_to_timeline") } + /// Back + public static var actionBack: String { return L10n.tr("Localizable", "action_back") } + /// Cancel + public static var actionCancel: String { return L10n.tr("Localizable", "action_cancel") } + /// Choose photo + public static var actionChoosePhoto: String { return L10n.tr("Localizable", "action_choose_photo") } + /// Clear + public static var actionClear: String { return L10n.tr("Localizable", "action_clear") } + /// Close + public static var actionClose: String { return L10n.tr("Localizable", "action_close") } + /// Complete verification + public static var actionCompleteVerification: String { return L10n.tr("Localizable", "action_complete_verification") } + /// Confirm + public static var actionConfirm: String { return L10n.tr("Localizable", "action_confirm") } + /// Continue + public static var actionContinue: String { return L10n.tr("Localizable", "action_continue") } + /// Copy + public static var actionCopy: String { return L10n.tr("Localizable", "action_copy") } + /// Copy link + public static var actionCopyLink: String { return L10n.tr("Localizable", "action_copy_link") } + /// Copy link to message + public static var actionCopyLinkToMessage: String { return L10n.tr("Localizable", "action_copy_link_to_message") } + /// Create + public static var actionCreate: String { return L10n.tr("Localizable", "action_create") } + /// Create a room + public static var actionCreateARoom: String { return L10n.tr("Localizable", "action_create_a_room") } + /// Decline + public static var actionDecline: String { return L10n.tr("Localizable", "action_decline") } + /// Disable + public static var actionDisable: String { return L10n.tr("Localizable", "action_disable") } + /// Done + public static var actionDone: String { return L10n.tr("Localizable", "action_done") } + /// Edit + public static var actionEdit: String { return L10n.tr("Localizable", "action_edit") } + /// Enable + public static var actionEnable: String { return L10n.tr("Localizable", "action_enable") } + /// End poll + public static var actionEndPoll: String { return L10n.tr("Localizable", "action_end_poll") } + /// Forgot password? + public static var actionForgotPassword: String { return L10n.tr("Localizable", "action_forgot_password") } + /// Forward + public static var actionForward: String { return L10n.tr("Localizable", "action_forward") } + /// Invite + public static var actionInvite: String { return L10n.tr("Localizable", "action_invite") } + /// Invite friends + public static var actionInviteFriends: String { return L10n.tr("Localizable", "action_invite_friends") } + /// Invite friends to %1$@ + public static func actionInviteFriendsToApp(_ p1: Any) -> String { + return L10n.tr("Localizable", "action_invite_friends_to_app", String(describing: p1)) + } + /// Invite people to %1$@ + public static func actionInvitePeopleToApp(_ p1: Any) -> String { + return L10n.tr("Localizable", "action_invite_people_to_app", String(describing: p1)) + } + /// Invites + public static var actionInvitesList: String { return L10n.tr("Localizable", "action_invites_list") } + /// Learn more + public static var actionLearnMore: String { return L10n.tr("Localizable", "action_learn_more") } + /// Leave + public static var actionLeave: String { return L10n.tr("Localizable", "action_leave") } + /// Leave room + public static var actionLeaveRoom: String { return L10n.tr("Localizable", "action_leave_room") } + /// Manage account + public static var actionManageAccount: String { return L10n.tr("Localizable", "action_manage_account") } + /// Manage devices + public static var actionManageDevices: String { return L10n.tr("Localizable", "action_manage_devices") } + /// Next + public static var actionNext: String { return L10n.tr("Localizable", "action_next") } + /// No + public static var actionNo: String { return L10n.tr("Localizable", "action_no") } + /// Not now + public static var actionNotNow: String { return L10n.tr("Localizable", "action_not_now") } + /// OK + public static var actionOk: String { return L10n.tr("Localizable", "action_ok") } + /// Open settings + public static var actionOpenSettings: String { return L10n.tr("Localizable", "action_open_settings") } + /// Open with + public static var actionOpenWith: String { return L10n.tr("Localizable", "action_open_with") } + /// Quick reply + public static var actionQuickReply: String { return L10n.tr("Localizable", "action_quick_reply") } + /// Quote + public static var actionQuote: String { return L10n.tr("Localizable", "action_quote") } + /// React + public static var actionReact: String { return L10n.tr("Localizable", "action_react") } + /// Remove + public static var actionRemove: String { return L10n.tr("Localizable", "action_remove") } + /// Reply + public static var actionReply: String { return L10n.tr("Localizable", "action_reply") } + /// Reply in thread + public static var actionReplyInThread: String { return L10n.tr("Localizable", "action_reply_in_thread") } + /// Report bug + public static var actionReportBug: String { return L10n.tr("Localizable", "action_report_bug") } + /// Report Content + public static var actionReportContent: String { return L10n.tr("Localizable", "action_report_content") } + /// Retry + public static var actionRetry: String { return L10n.tr("Localizable", "action_retry") } + /// Retry decryption + public static var actionRetryDecryption: String { return L10n.tr("Localizable", "action_retry_decryption") } + /// Save + public static var actionSave: String { return L10n.tr("Localizable", "action_save") } + /// Search + public static var actionSearch: String { return L10n.tr("Localizable", "action_search") } + /// Send + public static var actionSend: String { return L10n.tr("Localizable", "action_send") } + /// Send message + public static var actionSendMessage: String { return L10n.tr("Localizable", "action_send_message") } + /// Share + public static var actionShare: String { return L10n.tr("Localizable", "action_share") } + /// Share link + public static var actionShareLink: String { return L10n.tr("Localizable", "action_share_link") } + /// Skip + public static var actionSkip: String { return L10n.tr("Localizable", "action_skip") } + /// Start + public static var actionStart: String { return L10n.tr("Localizable", "action_start") } + /// Start chat + public static var actionStartChat: String { return L10n.tr("Localizable", "action_start_chat") } + /// Start verification + public static var actionStartVerification: String { return L10n.tr("Localizable", "action_start_verification") } + /// Tap to load map + public static var actionStaticMapLoad: String { return L10n.tr("Localizable", "action_static_map_load") } + /// Take photo + public static var actionTakePhoto: String { return L10n.tr("Localizable", "action_take_photo") } + /// View Source + public static var actionViewSource: String { return L10n.tr("Localizable", "action_view_source") } + /// Yes + public static var actionYes: String { return L10n.tr("Localizable", "action_yes") } + /// About + public static var commonAbout: String { return L10n.tr("Localizable", "common_about") } + /// Acceptable use policy + public static var commonAcceptableUsePolicy: String { return L10n.tr("Localizable", "common_acceptable_use_policy") } + /// Advanced settings + public static var commonAdvancedSettings: String { return L10n.tr("Localizable", "common_advanced_settings") } + /// Analytics + public static var commonAnalytics: String { return L10n.tr("Localizable", "common_analytics") } + /// Audio + public static var commonAudio: String { return L10n.tr("Localizable", "common_audio") } + /// Bubbles + public static var commonBubbles: String { return L10n.tr("Localizable", "common_bubbles") } + /// Copyright + public static var commonCopyright: String { return L10n.tr("Localizable", "common_copyright") } + /// Creating room… + public static var commonCreatingRoom: String { return L10n.tr("Localizable", "common_creating_room") } + /// Left room + public static var commonCurrentUserLeftRoom: String { return L10n.tr("Localizable", "common_current_user_left_room") } + /// Decryption error + public static var commonDecryptionError: String { return L10n.tr("Localizable", "common_decryption_error") } + /// Developer options + public static var commonDeveloperOptions: String { return L10n.tr("Localizable", "common_developer_options") } + /// (edited) + public static var commonEditedSuffix: String { return L10n.tr("Localizable", "common_edited_suffix") } + /// Editing + public static var commonEditing: String { return L10n.tr("Localizable", "common_editing") } + /// * %1$@ %2$@ + public static func commonEmote(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "common_emote", String(describing: p1), String(describing: p2)) + } + /// Encryption enabled + public static var commonEncryptionEnabled: String { return L10n.tr("Localizable", "common_encryption_enabled") } + /// Error + public static var commonError: String { return L10n.tr("Localizable", "common_error") } + /// File + public static var commonFile: String { return L10n.tr("Localizable", "common_file") } + /// Forward message + public static var commonForwardMessage: String { return L10n.tr("Localizable", "common_forward_message") } + /// GIF + public static var commonGif: String { return L10n.tr("Localizable", "common_gif") } + /// Image + public static var commonImage: String { return L10n.tr("Localizable", "common_image") } + /// In reply to %1$@ + public static func commonInReplyTo(_ p1: Any) -> String { + return L10n.tr("Localizable", "common_in_reply_to", String(describing: p1)) + } + /// This Matrix ID can't be found, so the invite might not be received. + public static var commonInviteUnknownProfile: String { return L10n.tr("Localizable", "common_invite_unknown_profile") } + /// Leaving room + public static var commonLeavingRoom: String { return L10n.tr("Localizable", "common_leaving_room") } + /// Link copied to clipboard + public static var commonLinkCopiedToClipboard: String { return L10n.tr("Localizable", "common_link_copied_to_clipboard") } + /// Loading… + public static var commonLoading: String { return L10n.tr("Localizable", "common_loading") } + /// Plural format key: "%#@COUNT@" + public static func commonMemberCount(_ p1: Int) -> String { + return L10n.tr("Localizable", "common_member_count", p1) + } + /// Message + public static var commonMessage: String { return L10n.tr("Localizable", "common_message") } + /// Message layout + public static var commonMessageLayout: String { return L10n.tr("Localizable", "common_message_layout") } + /// Message removed + public static var commonMessageRemoved: String { return L10n.tr("Localizable", "common_message_removed") } + /// Modern + public static var commonModern: String { return L10n.tr("Localizable", "common_modern") } + /// Mute + public static var commonMute: String { return L10n.tr("Localizable", "common_mute") } + /// No results + public static var commonNoResults: String { return L10n.tr("Localizable", "common_no_results") } + /// Offline + public static var commonOffline: String { return L10n.tr("Localizable", "common_offline") } + /// Password + public static var commonPassword: String { return L10n.tr("Localizable", "common_password") } + /// People + public static var commonPeople: String { return L10n.tr("Localizable", "common_people") } + /// Permalink + public static var commonPermalink: String { return L10n.tr("Localizable", "common_permalink") } + /// Permission + public static var commonPermission: String { return L10n.tr("Localizable", "common_permission") } + /// Are you sure you want to end this poll? + public static var commonPollEndConfirmation: String { return L10n.tr("Localizable", "common_poll_end_confirmation") } + /// Poll: %1$@ + public static func commonPollSummary(_ p1: Any) -> String { + return L10n.tr("Localizable", "common_poll_summary", String(describing: p1)) + } + /// Total votes: %1$@ + public static func commonPollTotalVotes(_ p1: Any) -> String { + return L10n.tr("Localizable", "common_poll_total_votes", String(describing: p1)) + } + /// Results will show after the poll has ended + public static var commonPollUndisclosedText: String { return L10n.tr("Localizable", "common_poll_undisclosed_text") } + /// Plural format key: "%#@COUNT@" + public static func commonPollVotesCount(_ p1: Int) -> String { + return L10n.tr("Localizable", "common_poll_votes_count", p1) + } + /// Privacy policy + public static var commonPrivacyPolicy: String { return L10n.tr("Localizable", "common_privacy_policy") } + /// Reaction + public static var commonReaction: String { return L10n.tr("Localizable", "common_reaction") } + /// Reactions + public static var commonReactions: String { return L10n.tr("Localizable", "common_reactions") } + /// Refreshing… + public static var commonRefreshing: String { return L10n.tr("Localizable", "common_refreshing") } + /// Replying to %1$@ + public static func commonReplyingTo(_ p1: Any) -> String { + return L10n.tr("Localizable", "common_replying_to", String(describing: p1)) + } + /// Report a bug + public static var commonReportABug: String { return L10n.tr("Localizable", "common_report_a_bug") } + /// Report submitted + public static var commonReportSubmitted: String { return L10n.tr("Localizable", "common_report_submitted") } + /// Rich text editor + public static var commonRichTextEditor: String { return L10n.tr("Localizable", "common_rich_text_editor") } + /// Room name + public static var commonRoomName: String { return L10n.tr("Localizable", "common_room_name") } + /// e.g. your project name + public static var commonRoomNamePlaceholder: String { return L10n.tr("Localizable", "common_room_name_placeholder") } + /// Search for someone + public static var commonSearchForSomeone: String { return L10n.tr("Localizable", "common_search_for_someone") } + /// Search results + public static var commonSearchResults: String { return L10n.tr("Localizable", "common_search_results") } + /// Security + public static var commonSecurity: String { return L10n.tr("Localizable", "common_security") } + /// Select your server + public static var commonSelectYourServer: String { return L10n.tr("Localizable", "common_select_your_server") } + /// Sending… + public static var commonSending: String { return L10n.tr("Localizable", "common_sending") } + /// Server not supported + public static var commonServerNotSupported: String { return L10n.tr("Localizable", "common_server_not_supported") } + /// Server URL + public static var commonServerUrl: String { return L10n.tr("Localizable", "common_server_url") } + /// Settings + public static var commonSettings: String { return L10n.tr("Localizable", "common_settings") } + /// Shared location + public static var commonSharedLocation: String { return L10n.tr("Localizable", "common_shared_location") } + /// Starting chat… + public static var commonStartingChat: String { return L10n.tr("Localizable", "common_starting_chat") } + /// Sticker + public static var commonSticker: String { return L10n.tr("Localizable", "common_sticker") } + /// Success + public static var commonSuccess: String { return L10n.tr("Localizable", "common_success") } + /// Suggestions + public static var commonSuggestions: String { return L10n.tr("Localizable", "common_suggestions") } + /// Syncing + public static var commonSyncing: String { return L10n.tr("Localizable", "common_syncing") } + /// Text + public static var commonText: String { return L10n.tr("Localizable", "common_text") } + /// Third-party notices + public static var commonThirdPartyNotices: String { return L10n.tr("Localizable", "common_third_party_notices") } + /// Thread + public static var commonThread: String { return L10n.tr("Localizable", "common_thread") } + /// Topic + public static var commonTopic: String { return L10n.tr("Localizable", "common_topic") } + /// What is this room about? + public static var commonTopicPlaceholder: String { return L10n.tr("Localizable", "common_topic_placeholder") } + /// Unable to decrypt + public static var commonUnableToDecrypt: String { return L10n.tr("Localizable", "common_unable_to_decrypt") } + /// Invites couldn't be sent to one or more users. + public static var commonUnableToInviteMessage: String { return L10n.tr("Localizable", "common_unable_to_invite_message") } + /// Unable to send invite(s) + public static var commonUnableToInviteTitle: String { return L10n.tr("Localizable", "common_unable_to_invite_title") } + /// Unmute + public static var commonUnmute: String { return L10n.tr("Localizable", "common_unmute") } + /// Unsupported event + public static var commonUnsupportedEvent: String { return L10n.tr("Localizable", "common_unsupported_event") } + /// Username + public static var commonUsername: String { return L10n.tr("Localizable", "common_username") } + /// Verification cancelled + 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") } + /// Video + public static var commonVideo: String { return L10n.tr("Localizable", "common_video") } + /// Waiting… + public static var commonWaiting: String { return L10n.tr("Localizable", "common_waiting") } + /// %1$@ crashed the last time it was used. Would you like to share a crash report with us? + public static func crashDetectionDialogContent(_ p1: Any) -> String { + return L10n.tr("Localizable", "crash_detection_dialog_content", String(describing: p1)) + } + /// In order to let the application use the camera, please grant the permission in the system settings. + public static var dialogPermissionCamera: String { return L10n.tr("Localizable", "dialog_permission_camera") } + /// Please grant the permission in the system settings. + public static var dialogPermissionGeneric: String { return L10n.tr("Localizable", "dialog_permission_generic") } + /// In order to let the application use the microphone, please grant the permission in the system settings. + public static var dialogPermissionMicrophone: String { return L10n.tr("Localizable", "dialog_permission_microphone") } + /// In order to let the application display notifications, please grant the permission in the system settings. + public static var dialogPermissionNotification: String { return L10n.tr("Localizable", "dialog_permission_notification") } + /// Confirmation + public static var dialogTitleConfirmation: String { return L10n.tr("Localizable", "dialog_title_confirmation") } + /// Error + public static var dialogTitleError: String { return L10n.tr("Localizable", "dialog_title_error") } + /// Success + public static var dialogTitleSuccess: String { return L10n.tr("Localizable", "dialog_title_success") } + /// Warning + public static var dialogTitleWarning: String { return L10n.tr("Localizable", "dialog_title_warning") } + /// Activities + public static var emojiPickerCategoryActivity: String { return L10n.tr("Localizable", "emoji_picker_category_activity") } + /// Flags + public static var emojiPickerCategoryFlags: String { return L10n.tr("Localizable", "emoji_picker_category_flags") } + /// Food & Drink + public static var emojiPickerCategoryFoods: String { return L10n.tr("Localizable", "emoji_picker_category_foods") } + /// Animals & Nature + public static var emojiPickerCategoryNature: String { return L10n.tr("Localizable", "emoji_picker_category_nature") } + /// Objects + public static var emojiPickerCategoryObjects: String { return L10n.tr("Localizable", "emoji_picker_category_objects") } + /// Smileys & People + public static var emojiPickerCategoryPeople: String { return L10n.tr("Localizable", "emoji_picker_category_people") } + /// Travel & Places + public static var emojiPickerCategoryPlaces: String { return L10n.tr("Localizable", "emoji_picker_category_places") } + /// Symbols + public static var emojiPickerCategorySymbols: String { return L10n.tr("Localizable", "emoji_picker_category_symbols") } + /// Failed creating the permalink + public static var errorFailedCreatingThePermalink: String { return L10n.tr("Localizable", "error_failed_creating_the_permalink") } + /// %1$@ could not load the map. Please try again later. + public static func errorFailedLoadingMap(_ p1: Any) -> String { + return L10n.tr("Localizable", "error_failed_loading_map", String(describing: p1)) + } + /// Failed loading messages + public static var errorFailedLoadingMessages: String { return L10n.tr("Localizable", "error_failed_loading_messages") } + /// %1$@ could not access your location. Please try again later. + public static func errorFailedLocatingUser(_ p1: Any) -> String { + return L10n.tr("Localizable", "error_failed_locating_user", String(describing: p1)) + } + /// %1$@ does not have permission to access your location. You can enable access in Settings > Location + public static func errorMissingLocationAuthIos(_ p1: Any) -> String { + return L10n.tr("Localizable", "error_missing_location_auth_ios", String(describing: p1)) + } + /// No compatible app was found to handle this action. + public static var errorNoCompatibleAppFound: String { return L10n.tr("Localizable", "error_no_compatible_app_found") } + /// Some messages have not been sent + public static var errorSomeMessagesHaveNotBeenSent: String { return L10n.tr("Localizable", "error_some_messages_have_not_been_sent") } + /// Sorry, an error occurred + public static var errorUnknown: String { return L10n.tr("Localizable", "error_unknown") } + /// 🔐️ Join me on %1$@ + public static func inviteFriendsRichTitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "invite_friends_rich_title", String(describing: p1)) + } + /// Hey, talk to me on %1$@: %2$@ + public static func inviteFriendsText(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "invite_friends_text", String(describing: p1), String(describing: p2)) + } + /// Are you sure that you want to leave this room? You're the only person here. If you leave, no one will be able to join in the future, including you. + public static var leaveRoomAlertEmptySubtitle: String { return L10n.tr("Localizable", "leave_room_alert_empty_subtitle") } + /// Are you sure that you want to leave this room? This room is not public and you won't be able to rejoin without an invite. + public static var leaveRoomAlertPrivateSubtitle: String { return L10n.tr("Localizable", "leave_room_alert_private_subtitle") } + /// Are you sure that you want to leave the room? + public static var leaveRoomAlertSubtitle: String { return L10n.tr("Localizable", "leave_room_alert_subtitle") } + /// %1$@ iOS + public static func loginInitialDeviceNameIos(_ p1: Any) -> String { + return L10n.tr("Localizable", "login_initial_device_name_ios", String(describing: p1)) + } + /// Notification + public static var notification: String { return L10n.tr("Localizable", "Notification") } + /// Call + public static var notificationChannelCall: String { return L10n.tr("Localizable", "notification_channel_call") } + /// Listening for events + public static var notificationChannelListeningForEvents: String { return L10n.tr("Localizable", "notification_channel_listening_for_events") } + /// Noisy notifications + public static var notificationChannelNoisy: String { return L10n.tr("Localizable", "notification_channel_noisy") } + /// Silent notifications + public static var notificationChannelSilent: String { return L10n.tr("Localizable", "notification_channel_silent") } + /// Plural format key: "%#@COUNT@" + public static func notificationCompatSummaryLineForRoom(_ p1: Int) -> String { + return L10n.tr("Localizable", "notification_compat_summary_line_for_room", p1) + } + /// Plural format key: "%#@COUNT@" + public static func notificationCompatSummaryTitle(_ p1: Int) -> String { + return L10n.tr("Localizable", "notification_compat_summary_title", p1) + } + /// Notification + public static var notificationFallbackContent: String { return L10n.tr("Localizable", "notification_fallback_content") } + /// ** Failed to send - please open room + public static var notificationInlineReplyFailed: String { return L10n.tr("Localizable", "notification_inline_reply_failed") } + /// Join + public static var notificationInvitationActionJoin: String { return L10n.tr("Localizable", "notification_invitation_action_join") } + /// Reject + public static var notificationInvitationActionReject: String { return L10n.tr("Localizable", "notification_invitation_action_reject") } + /// Plural format key: "%#@COUNT@" + public static func notificationInvitations(_ p1: Int) -> String { + return L10n.tr("Localizable", "notification_invitations", p1) + } + /// Invited you to chat + public static var notificationInviteBody: String { return L10n.tr("Localizable", "notification_invite_body") } + /// New Messages + public static var notificationNewMessages: String { return L10n.tr("Localizable", "notification_new_messages") } + /// Plural format key: "%#@COUNT@" + public static func notificationNewMessagesForRoom(_ p1: Int) -> String { + return L10n.tr("Localizable", "notification_new_messages_for_room", p1) + } + /// Reacted with %1$@ + public static func notificationReactionBody(_ p1: Any) -> String { + return L10n.tr("Localizable", "notification_reaction_body", String(describing: p1)) + } + /// Mark as read + public static var notificationRoomActionMarkAsRead: String { return L10n.tr("Localizable", "notification_room_action_mark_as_read") } + /// Quick reply + public static var notificationRoomActionQuickReply: String { return L10n.tr("Localizable", "notification_room_action_quick_reply") } + /// Invited you to join the room + public static var notificationRoomInviteBody: String { return L10n.tr("Localizable", "notification_room_invite_body") } + /// Me + public static var notificationSenderMe: String { return L10n.tr("Localizable", "notification_sender_me") } + /// You are viewing the notification! Click me! + public static var notificationTestPushNotificationContent: String { return L10n.tr("Localizable", "notification_test_push_notification_content") } + /// %1$@: %2$@ + public static func notificationTickerTextDm(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "notification_ticker_text_dm", String(describing: p1), String(describing: p2)) + } + /// %1$@: %2$@ %3$@ + public static func notificationTickerTextGroup(_ p1: Any, _ p2: Any, _ p3: Any) -> String { + return L10n.tr("Localizable", "notification_ticker_text_group", String(describing: p1), String(describing: p2), String(describing: p3)) + } + /// Plural format key: "%#@COUNT@" + public static func notificationUnreadNotifiedMessages(_ p1: Int) -> String { + return L10n.tr("Localizable", "notification_unread_notified_messages", p1) + } + /// %1$@ and %2$@ + public static func notificationUnreadNotifiedMessagesAndInvitation(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "notification_unread_notified_messages_and_invitation", String(describing: p1), String(describing: p2)) + } + /// %1$@ in %2$@ + public static func notificationUnreadNotifiedMessagesInRoom(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "notification_unread_notified_messages_in_room", String(describing: p1), String(describing: p2)) + } + /// %1$@ in %2$@ and %3$@ + public static func notificationUnreadNotifiedMessagesInRoomAndInvitation(_ p1: Any, _ p2: Any, _ p3: Any) -> String { + return L10n.tr("Localizable", "notification_unread_notified_messages_in_room_and_invitation", String(describing: p1), String(describing: p2), String(describing: p3)) + } + /// Plural format key: "%#@COUNT@" + public static func notificationUnreadNotifiedMessagesInRoomRooms(_ p1: Int) -> String { + return L10n.tr("Localizable", "notification_unread_notified_messages_in_room_rooms", p1) + } + /// Rageshake to report bug + public static var preferenceRageshake: String { return L10n.tr("Localizable", "preference_rageshake") } + /// You seem to be shaking the phone in frustration. Would you like to open the bug report screen? + public static var rageshakeDetectionDialogContent: String { return L10n.tr("Localizable", "rageshake_detection_dialog_content") } + /// You seem to be shaking the phone in frustration. Would you like to open the bug report screen? + public static var rageshakeDialogContent: String { return L10n.tr("Localizable", "rageshake_dialog_content") } + /// This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages. + public static var reportContentExplanation: String { return L10n.tr("Localizable", "report_content_explanation") } + /// Reason for reporting this content + public static var reportContentHint: String { return L10n.tr("Localizable", "report_content_hint") } + /// Add attachment + public static var richTextEditorA11yAddAttachment: String { return L10n.tr("Localizable", "rich_text_editor_a11y_add_attachment") } + /// Toggle bullet list + public static var richTextEditorBulletList: String { return L10n.tr("Localizable", "rich_text_editor_bullet_list") } + /// Close formatting options + public static var richTextEditorCloseFormattingOptions: String { return L10n.tr("Localizable", "rich_text_editor_close_formatting_options") } + /// Toggle code block + public static var richTextEditorCodeBlock: String { return L10n.tr("Localizable", "rich_text_editor_code_block") } + /// Message… + public static var richTextEditorComposerPlaceholder: String { return L10n.tr("Localizable", "rich_text_editor_composer_placeholder") } + /// Create a link + public static var richTextEditorCreateLink: String { return L10n.tr("Localizable", "rich_text_editor_create_link") } + /// Edit link + public static var richTextEditorEditLink: String { return L10n.tr("Localizable", "rich_text_editor_edit_link") } + /// Apply bold format + public static var richTextEditorFormatBold: String { return L10n.tr("Localizable", "rich_text_editor_format_bold") } + /// Apply italic format + public static var richTextEditorFormatItalic: String { return L10n.tr("Localizable", "rich_text_editor_format_italic") } + /// Apply strikethrough format + public static var richTextEditorFormatStrikethrough: String { return L10n.tr("Localizable", "rich_text_editor_format_strikethrough") } + /// Apply underline format + public static var richTextEditorFormatUnderline: String { return L10n.tr("Localizable", "rich_text_editor_format_underline") } + /// Toggle full screen mode + public static var richTextEditorFullScreenToggle: String { return L10n.tr("Localizable", "rich_text_editor_full_screen_toggle") } + /// Indent + public static var richTextEditorIndent: String { return L10n.tr("Localizable", "rich_text_editor_indent") } + /// Apply inline code format + public static var richTextEditorInlineCode: String { return L10n.tr("Localizable", "rich_text_editor_inline_code") } + /// Set link + public static var richTextEditorLink: String { return L10n.tr("Localizable", "rich_text_editor_link") } + /// Toggle numbered list + public static var richTextEditorNumberedList: String { return L10n.tr("Localizable", "rich_text_editor_numbered_list") } + /// Open compose options + public static var richTextEditorOpenComposeOptions: String { return L10n.tr("Localizable", "rich_text_editor_open_compose_options") } + /// Toggle quote + public static var richTextEditorQuote: String { return L10n.tr("Localizable", "rich_text_editor_quote") } + /// Remove link + public static var richTextEditorRemoveLink: String { return L10n.tr("Localizable", "rich_text_editor_remove_link") } + /// Unindent + public static var richTextEditorUnindent: String { return L10n.tr("Localizable", "rich_text_editor_unindent") } + /// Link + public static var richTextEditorUrlPlaceholder: String { return L10n.tr("Localizable", "rich_text_editor_url_placeholder") } + /// This is the beginning of %1$@. + public static func roomTimelineBeginningOfRoom(_ p1: Any) -> String { + return L10n.tr("Localizable", "room_timeline_beginning_of_room", String(describing: p1)) + } + /// This is the beginning of this conversation. + public static var roomTimelineBeginningOfRoomNoName: String { return L10n.tr("Localizable", "room_timeline_beginning_of_room_no_name") } + /// New + public static var roomTimelineReadMarkerTitle: String { return L10n.tr("Localizable", "room_timeline_read_marker_title") } + /// Plural format key: "%#@COUNT@" + public static func roomTimelineStateChanges(_ p1: Int) -> String { + return L10n.tr("Localizable", "room_timeline_state_changes", p1) + } + /// Change account provider + public static var screenAccountProviderChange: String { return L10n.tr("Localizable", "screen_account_provider_change") } + /// Continue + public static var screenAccountProviderContinue: String { return L10n.tr("Localizable", "screen_account_provider_continue") } + /// Homeserver address + public static var screenAccountProviderFormHint: String { return L10n.tr("Localizable", "screen_account_provider_form_hint") } + /// Enter a search term or a domain address. + public static var screenAccountProviderFormNotice: String { return L10n.tr("Localizable", "screen_account_provider_form_notice") } + /// Search for a company, community, or private server. + public static var screenAccountProviderFormSubtitle: String { return L10n.tr("Localizable", "screen_account_provider_form_subtitle") } + /// Find an account provider + public static var screenAccountProviderFormTitle: String { return L10n.tr("Localizable", "screen_account_provider_form_title") } + /// This is where your conversations will live — just like you would use an email provider to keep your emails. + public static var screenAccountProviderSigninSubtitle: String { return L10n.tr("Localizable", "screen_account_provider_signin_subtitle") } + /// You’re about to sign in to %@ + public static func screenAccountProviderSigninTitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_account_provider_signin_title", String(describing: p1)) + } + /// This is where your conversations will live — just like you would use an email provider to keep your emails. + public static var screenAccountProviderSignupSubtitle: String { return L10n.tr("Localizable", "screen_account_provider_signup_subtitle") } + /// You’re about to create an account on %@ + public static func screenAccountProviderSignupTitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_account_provider_signup_title", String(describing: p1)) + } + /// We won't record or profile any personal data + public static var screenAnalyticsPromptDataUsage: String { return L10n.tr("Localizable", "screen_analytics_prompt_data_usage") } + /// Share anonymous usage data to help us identify issues. + public static var screenAnalyticsPromptHelpUsImprove: String { return L10n.tr("Localizable", "screen_analytics_prompt_help_us_improve") } + /// You can read all our terms %1$@. + public static func screenAnalyticsPromptReadTerms(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_analytics_prompt_read_terms", String(describing: p1)) + } + /// here + public static var screenAnalyticsPromptReadTermsContentLink: String { return L10n.tr("Localizable", "screen_analytics_prompt_read_terms_content_link") } + /// You can turn this off anytime + public static var screenAnalyticsPromptSettings: String { return L10n.tr("Localizable", "screen_analytics_prompt_settings") } + /// We won't share your data with third parties + public static var screenAnalyticsPromptThirdPartySharing: String { return L10n.tr("Localizable", "screen_analytics_prompt_third_party_sharing") } + /// Help improve %1$@ + public static func screenAnalyticsPromptTitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_analytics_prompt_title", String(describing: p1)) + } + /// Share anonymous usage data to help us identify issues. + public static var screenAnalyticsSettingsHelpUsImprove: String { return L10n.tr("Localizable", "screen_analytics_settings_help_us_improve") } + /// You can read all our terms %1$@. + public static func screenAnalyticsSettingsReadTerms(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_analytics_settings_read_terms", String(describing: p1)) + } + /// here + public static var screenAnalyticsSettingsReadTermsContentLink: String { return L10n.tr("Localizable", "screen_analytics_settings_read_terms_content_link") } + /// Share analytics data + public static var screenAnalyticsSettingsShareData: String { return L10n.tr("Localizable", "screen_analytics_settings_share_data") } + /// Attach screenshot + public static var screenBugReportAttachScreenshot: String { return L10n.tr("Localizable", "screen_bug_report_attach_screenshot") } + /// You may contact me if you have any follow up questions. + public static var screenBugReportContactMe: String { return L10n.tr("Localizable", "screen_bug_report_contact_me") } + /// Contact me + public static var screenBugReportContactMeTitle: String { return L10n.tr("Localizable", "screen_bug_report_contact_me_title") } + /// Edit screenshot + public static var screenBugReportEditScreenshot: String { return L10n.tr("Localizable", "screen_bug_report_edit_screenshot") } + /// Please describe the bug. What did you do? What did you expect to happen? What actually happened. Please go into as much detail as you can. + public static var screenBugReportEditorDescription: String { return L10n.tr("Localizable", "screen_bug_report_editor_description") } + /// Describe the bug… + public static var screenBugReportEditorPlaceholder: String { return L10n.tr("Localizable", "screen_bug_report_editor_placeholder") } + /// If possible, please write the description in English. + public static var screenBugReportEditorSupporting: String { return L10n.tr("Localizable", "screen_bug_report_editor_supporting") } + /// Send crash logs + public static var screenBugReportIncludeCrashLogs: String { return L10n.tr("Localizable", "screen_bug_report_include_crash_logs") } + /// Allow logs + public static var screenBugReportIncludeLogs: String { return L10n.tr("Localizable", "screen_bug_report_include_logs") } + /// Send screenshot + public static var screenBugReportIncludeScreenshot: String { return L10n.tr("Localizable", "screen_bug_report_include_screenshot") } + /// Logs will be included with your message to make sure that everything is working properly. To send your message without logs, turn off this setting. + public static var screenBugReportLogsDescription: String { return L10n.tr("Localizable", "screen_bug_report_logs_description") } + /// %1$@ crashed the last time it was used. Would you like to share a crash report with us? + public static func screenBugReportRashLogsAlertTitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_bug_report_rash_logs_alert_title", String(describing: p1)) + } + /// Matrix.org is a large, free server on the public Matrix network for secure, decentralised communication, run by the Matrix.org Foundation. + public static var screenChangeAccountProviderMatrixOrgSubtitle: String { return L10n.tr("Localizable", "screen_change_account_provider_matrix_org_subtitle") } + /// Other + public static var screenChangeAccountProviderOther: String { return L10n.tr("Localizable", "screen_change_account_provider_other") } + /// Use a different account provider, such as your own private server or a work account. + public static var screenChangeAccountProviderSubtitle: String { return L10n.tr("Localizable", "screen_change_account_provider_subtitle") } + /// Change account provider + public static var screenChangeAccountProviderTitle: String { return L10n.tr("Localizable", "screen_change_account_provider_title") } + /// We couldn't reach this homeserver. Please check that you have entered the homeserver URL correctly. If the URL is correct, contact your homeserver administrator for further help. + public static var screenChangeServerErrorInvalidHomeserver: String { return L10n.tr("Localizable", "screen_change_server_error_invalid_homeserver") } + /// This server currently doesn’t support sliding sync. + public static var screenChangeServerErrorNoSlidingSyncMessage: String { return L10n.tr("Localizable", "screen_change_server_error_no_sliding_sync_message") } + /// Homeserver URL + public static var screenChangeServerFormHeader: String { return L10n.tr("Localizable", "screen_change_server_form_header") } + /// You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$@ + public static func screenChangeServerFormNotice(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_change_server_form_notice", String(describing: p1)) + } + /// Continue + public static var screenChangeServerSubmit: String { return L10n.tr("Localizable", "screen_change_server_submit") } + /// What is the address of your server? + public static var screenChangeServerSubtitle: String { return L10n.tr("Localizable", "screen_change_server_subtitle") } + /// Select your server + public static var screenChangeServerTitle: String { return L10n.tr("Localizable", "screen_change_server_title") } + /// Add option + public static var screenCreatePollAddOptionBtn: String { return L10n.tr("Localizable", "screen_create_poll_add_option_btn") } + /// Show results only after poll ends + public static var screenCreatePollAnonymousDesc: String { return L10n.tr("Localizable", "screen_create_poll_anonymous_desc") } + /// Hide votes + public static var screenCreatePollAnonymousHeadline: String { return L10n.tr("Localizable", "screen_create_poll_anonymous_headline") } + /// Option %1$d + public static func screenCreatePollAnswerHint(_ p1: Int) -> String { + return L10n.tr("Localizable", "screen_create_poll_answer_hint", p1) + } + /// Are you sure you want to discard this poll? + public static var screenCreatePollDiscardConfirmation: String { return L10n.tr("Localizable", "screen_create_poll_discard_confirmation") } + /// Discard Poll + public static var screenCreatePollDiscardConfirmationTitle: String { return L10n.tr("Localizable", "screen_create_poll_discard_confirmation_title") } + /// Question or topic + public static var screenCreatePollQuestionDesc: String { return L10n.tr("Localizable", "screen_create_poll_question_desc") } + /// What is the poll about? + public static var screenCreatePollQuestionHint: String { return L10n.tr("Localizable", "screen_create_poll_question_hint") } + /// Create Poll + public static var screenCreatePollTitle: String { return L10n.tr("Localizable", "screen_create_poll_title") } + /// New room + public static var screenCreateRoomActionCreateRoom: String { return L10n.tr("Localizable", "screen_create_room_action_create_room") } + /// Invite friends to Element + public static var screenCreateRoomActionInvitePeople: String { return L10n.tr("Localizable", "screen_create_room_action_invite_people") } + /// Invite people + public static var screenCreateRoomAddPeopleTitle: String { return L10n.tr("Localizable", "screen_create_room_add_people_title") } + /// An error occurred when creating the room + public static var screenCreateRoomErrorCreatingRoom: String { return L10n.tr("Localizable", "screen_create_room_error_creating_room") } + /// Messages in this room are encrypted. Encryption can’t be disabled afterwards. + public static var screenCreateRoomPrivateOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_private_option_description") } + /// Private room (invite only) + public static var screenCreateRoomPrivateOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_private_option_title") } + /// Messages are not encrypted and anyone can read them. You can enable encryption at a later date. + public static var screenCreateRoomPublicOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_public_option_description") } + /// Public room (anyone) + public static var screenCreateRoomPublicOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_public_option_title") } + /// Room name + public static var screenCreateRoomRoomNameLabel: String { return L10n.tr("Localizable", "screen_create_room_room_name_label") } + /// Create a room + public static var screenCreateRoomTitle: String { return L10n.tr("Localizable", "screen_create_room_title") } + /// Topic (optional) + public static var screenCreateRoomTopicLabel: String { return L10n.tr("Localizable", "screen_create_room_topic_label") } + /// Block + public static var screenDmDetailsBlockAlertAction: String { return L10n.tr("Localizable", "screen_dm_details_block_alert_action") } + /// Blocked users won't be able to send you messages and all their messages will be hidden. You can unblock them anytime. + public static var screenDmDetailsBlockAlertDescription: String { return L10n.tr("Localizable", "screen_dm_details_block_alert_description") } + /// Block user + public static var screenDmDetailsBlockUser: String { return L10n.tr("Localizable", "screen_dm_details_block_user") } + /// Unblock + public static var screenDmDetailsUnblockAlertAction: String { return L10n.tr("Localizable", "screen_dm_details_unblock_alert_action") } + /// You'll be able to see all messages from them again. + public static var screenDmDetailsUnblockAlertDescription: String { return L10n.tr("Localizable", "screen_dm_details_unblock_alert_description") } + /// Unblock user + public static var screenDmDetailsUnblockUser: String { return L10n.tr("Localizable", "screen_dm_details_unblock_user") } + /// Display name + public static var screenEditProfileDisplayName: String { return L10n.tr("Localizable", "screen_edit_profile_display_name") } + /// Your display name + public static var screenEditProfileDisplayNamePlaceholder: String { return L10n.tr("Localizable", "screen_edit_profile_display_name_placeholder") } + /// An unknown error was encountered and the information couldn't be changed. + public static var screenEditProfileError: String { return L10n.tr("Localizable", "screen_edit_profile_error") } + /// Unable to update profile + public static var screenEditProfileErrorTitle: String { return L10n.tr("Localizable", "screen_edit_profile_error_title") } + /// Edit profile + public static var screenEditProfileTitle: String { return L10n.tr("Localizable", "screen_edit_profile_title") } + /// Updating profile… + public static var screenEditProfileUpdatingDetails: String { return L10n.tr("Localizable", "screen_edit_profile_updating_details") } + /// Are you sure you want to decline the invitation to join %1$@? + public static func screenInvitesDeclineChatMessage(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_invites_decline_chat_message", String(describing: p1)) + } + /// Decline invite + public static var screenInvitesDeclineChatTitle: String { return L10n.tr("Localizable", "screen_invites_decline_chat_title") } + /// Are you sure you want to decline this private chat with %1$@? + public static func screenInvitesDeclineDirectChatMessage(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_invites_decline_direct_chat_message", String(describing: p1)) + } + /// Decline chat + public static var screenInvitesDeclineDirectChatTitle: String { return L10n.tr("Localizable", "screen_invites_decline_direct_chat_title") } + /// No Invites + public static var screenInvitesEmptyList: String { return L10n.tr("Localizable", "screen_invites_empty_list") } + /// %1$@ (%2$@) invited you + public static func screenInvitesInvitedYou(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "screen_invites_invited_you", String(describing: p1), String(describing: p2)) + } + /// This account has been deactivated. + public static var screenLoginErrorDeactivatedAccount: String { return L10n.tr("Localizable", "screen_login_error_deactivated_account") } + /// Incorrect username and/or password + public static var screenLoginErrorInvalidCredentials: String { return L10n.tr("Localizable", "screen_login_error_invalid_credentials") } + /// This is not a valid user identifier. Expected format: ‘@user:homeserver.org’ + public static var screenLoginErrorInvalidUserId: String { return L10n.tr("Localizable", "screen_login_error_invalid_user_id") } + /// The selected homeserver doesn't support password or OIDC login. Please contact your admin or choose another homeserver. + public static var screenLoginErrorUnsupportedAuthentication: String { return L10n.tr("Localizable", "screen_login_error_unsupported_authentication") } + /// Enter your details + public static var screenLoginFormHeader: String { return L10n.tr("Localizable", "screen_login_form_header") } + /// Password + public static var screenLoginPasswordHint: String { return L10n.tr("Localizable", "screen_login_password_hint") } + /// Continue + public static var screenLoginSubmit: String { return L10n.tr("Localizable", "screen_login_submit") } + /// Matrix is an open network for secure, decentralised communication. + public static var screenLoginSubtitle: String { return L10n.tr("Localizable", "screen_login_subtitle") } + /// Welcome back! + public static var screenLoginTitle: String { return L10n.tr("Localizable", "screen_login_title") } + /// Sign in to %1$@ + public static func screenLoginTitleWithHomeserver(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_login_title_with_homeserver", String(describing: p1)) + } + /// Username + public static var screenLoginUsernameHint: String { return L10n.tr("Localizable", "screen_login_username_hint") } + /// Failed selecting media, please try again. + public static var screenMediaPickerErrorFailedSelection: String { return L10n.tr("Localizable", "screen_media_picker_error_failed_selection") } + /// Failed processing media to upload, please try again. + public static var screenMediaUploadPreviewErrorFailedProcessing: String { return L10n.tr("Localizable", "screen_media_upload_preview_error_failed_processing") } + /// Failed uploading media, please try again. + public static var screenMediaUploadPreviewErrorFailedSending: String { return L10n.tr("Localizable", "screen_media_upload_preview_error_failed_sending") } + /// This is a one time process, thanks for waiting. + public static var screenMigrationMessage: String { return L10n.tr("Localizable", "screen_migration_message") } + /// Setting up your account. + public static var screenMigrationTitle: String { return L10n.tr("Localizable", "screen_migration_title") } + /// You can change your settings later. + public static var screenNotificationOptinSubtitle: String { return L10n.tr("Localizable", "screen_notification_optin_subtitle") } + /// Allow notifications and never miss a message + public static var screenNotificationOptinTitle: String { return L10n.tr("Localizable", "screen_notification_optin_title") } + /// Additional settings + public static var screenNotificationSettingsAdditionalSettingsSectionTitle: String { return L10n.tr("Localizable", "screen_notification_settings_additional_settings_section_title") } + /// Audio and video calls + public static var screenNotificationSettingsCallsLabel: String { return L10n.tr("Localizable", "screen_notification_settings_calls_label") } + /// Configuration mismatch + public static var screenNotificationSettingsConfigurationMismatch: String { return L10n.tr("Localizable", "screen_notification_settings_configuration_mismatch") } + /// We’ve simplified Notifications Settings to make options easier to find. Some custom settings you’ve chosen in the past are not shown here, but they’re still active. + /// + /// If you proceed, some of your settings may change. + public static var screenNotificationSettingsConfigurationMismatchDescription: String { return L10n.tr("Localizable", "screen_notification_settings_configuration_mismatch_description") } + /// Direct chats + public static var screenNotificationSettingsDirectChats: String { return L10n.tr("Localizable", "screen_notification_settings_direct_chats") } + /// Custom setting per chat + public static var screenNotificationSettingsEditCustomSettingsSectionTitle: String { return L10n.tr("Localizable", "screen_notification_settings_edit_custom_settings_section_title") } + /// An error occurred while updating the notification setting. + public static var screenNotificationSettingsEditFailedUpdatingDefaultMode: String { return L10n.tr("Localizable", "screen_notification_settings_edit_failed_updating_default_mode") } + /// All messages + public static var screenNotificationSettingsEditModeAllMessages: String { return L10n.tr("Localizable", "screen_notification_settings_edit_mode_all_messages") } + /// Mentions and Keywords only + public static var screenNotificationSettingsEditModeMentionsAndKeywords: String { return L10n.tr("Localizable", "screen_notification_settings_edit_mode_mentions_and_keywords") } + /// On direct chats, notify me for + public static var screenNotificationSettingsEditScreenDirectSectionHeader: String { return L10n.tr("Localizable", "screen_notification_settings_edit_screen_direct_section_header") } + /// On group chats, notify me for + public static var screenNotificationSettingsEditScreenGroupSectionHeader: String { return L10n.tr("Localizable", "screen_notification_settings_edit_screen_group_section_header") } + /// Enable notifications on this device + public static var screenNotificationSettingsEnableNotifications: String { return L10n.tr("Localizable", "screen_notification_settings_enable_notifications") } + /// The configuration has not been corrected, please try again. + public static var screenNotificationSettingsFailedFixingConfiguration: String { return L10n.tr("Localizable", "screen_notification_settings_failed_fixing_configuration") } + /// Group chats + public static var screenNotificationSettingsGroupChats: String { return L10n.tr("Localizable", "screen_notification_settings_group_chats") } + /// Mentions + public static var screenNotificationSettingsMentionsSectionTitle: String { return L10n.tr("Localizable", "screen_notification_settings_mentions_section_title") } + /// All + public static var screenNotificationSettingsModeAll: String { return L10n.tr("Localizable", "screen_notification_settings_mode_all") } + /// Mentions + public static var screenNotificationSettingsModeMentions: String { return L10n.tr("Localizable", "screen_notification_settings_mode_mentions") } + /// Notify me for + public static var screenNotificationSettingsNotificationSectionTitle: String { return L10n.tr("Localizable", "screen_notification_settings_notification_section_title") } + /// Notify me on @room + public static var screenNotificationSettingsRoomMentionLabel: String { return L10n.tr("Localizable", "screen_notification_settings_room_mention_label") } + /// To receive notifications, please change your %1$@. + public static func screenNotificationSettingsSystemNotificationsActionRequired(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_notification_settings_system_notifications_action_required", String(describing: p1)) + } + /// system settings + public static var screenNotificationSettingsSystemNotificationsActionRequiredContentLink: String { return L10n.tr("Localizable", "screen_notification_settings_system_notifications_action_required_content_link") } + /// System notifications turned off + public static var screenNotificationSettingsSystemNotificationsTurnedOff: String { return L10n.tr("Localizable", "screen_notification_settings_system_notifications_turned_off") } + /// Notifications + public static var screenNotificationSettingsTitle: String { return L10n.tr("Localizable", "screen_notification_settings_title") } + /// Sign in manually + public static var screenOnboardingSignInManually: String { return L10n.tr("Localizable", "screen_onboarding_sign_in_manually") } + /// Sign in with QR code + public static var screenOnboardingSignInWithQrCode: String { return L10n.tr("Localizable", "screen_onboarding_sign_in_with_qr_code") } + /// Create account + public static var screenOnboardingSignUp: String { return L10n.tr("Localizable", "screen_onboarding_sign_up") } + /// Communicate and collaborate securely + public static var screenOnboardingSubtitle: String { return L10n.tr("Localizable", "screen_onboarding_subtitle") } + /// Welcome to the fastest Element ever. Supercharged for speed and simplicity. + public static var screenOnboardingWelcomeMessage: String { return L10n.tr("Localizable", "screen_onboarding_welcome_message") } + /// Welcome to %1$@. Supercharged, for speed and simplicity. + public static func screenOnboardingWelcomeSubtitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_onboarding_welcome_subtitle", String(describing: p1)) + } + /// Be in your element + public static var screenOnboardingWelcomeTitle: String { return L10n.tr("Localizable", "screen_onboarding_welcome_title") } + /// Block user + public static var screenReportContentBlockUser: String { return L10n.tr("Localizable", "screen_report_content_block_user") } + /// Check if you want to hide all current and future messages from this user + public static var screenReportContentBlockUserHint: String { return L10n.tr("Localizable", "screen_report_content_block_user_hint") } + /// Camera + public static var screenRoomAttachmentSourceCamera: String { return L10n.tr("Localizable", "screen_room_attachment_source_camera") } + /// Take photo + public static var screenRoomAttachmentSourceCameraPhoto: String { return L10n.tr("Localizable", "screen_room_attachment_source_camera_photo") } + /// Record video + public static var screenRoomAttachmentSourceCameraVideo: String { return L10n.tr("Localizable", "screen_room_attachment_source_camera_video") } + /// Attachment + public static var screenRoomAttachmentSourceFiles: String { return L10n.tr("Localizable", "screen_room_attachment_source_files") } + /// Photo & Video Library + public static var screenRoomAttachmentSourceGallery: String { return L10n.tr("Localizable", "screen_room_attachment_source_gallery") } + /// Location + public static var screenRoomAttachmentSourceLocation: String { return L10n.tr("Localizable", "screen_room_attachment_source_location") } + /// Poll + public static var screenRoomAttachmentSourcePoll: String { return L10n.tr("Localizable", "screen_room_attachment_source_poll") } + /// Text Formatting + public static var screenRoomAttachmentTextFormatting: String { return L10n.tr("Localizable", "screen_room_attachment_text_formatting") } + /// Add topic + public static var screenRoomDetailsAddTopicTitle: String { return L10n.tr("Localizable", "screen_room_details_add_topic_title") } + /// Already a member + public static var screenRoomDetailsAlreadyAMember: String { return L10n.tr("Localizable", "screen_room_details_already_a_member") } + /// Already invited + public static var screenRoomDetailsAlreadyInvited: String { return L10n.tr("Localizable", "screen_room_details_already_invited") } + /// Edit Room + public static var screenRoomDetailsEditRoomTitle: String { return L10n.tr("Localizable", "screen_room_details_edit_room_title") } + /// There was an unknown error and the information couldn't be changed. + public static var screenRoomDetailsEditionError: String { return L10n.tr("Localizable", "screen_room_details_edition_error") } + /// Unable to update room + public static var screenRoomDetailsEditionErrorTitle: String { return L10n.tr("Localizable", "screen_room_details_edition_error_title") } + /// Messages are secured with locks. Only you and the recipients have the unique keys to unlock them. + public static var screenRoomDetailsEncryptionEnabledSubtitle: String { return L10n.tr("Localizable", "screen_room_details_encryption_enabled_subtitle") } + /// Message encryption enabled + public static var screenRoomDetailsEncryptionEnabledTitle: String { return L10n.tr("Localizable", "screen_room_details_encryption_enabled_title") } + /// An error occurred when loading notification settings. + public static var screenRoomDetailsErrorLoadingNotificationSettings: String { return L10n.tr("Localizable", "screen_room_details_error_loading_notification_settings") } + /// Failed muting this room, please try again. + public static var screenRoomDetailsErrorMuting: String { return L10n.tr("Localizable", "screen_room_details_error_muting") } + /// Failed unmuting this room, please try again. + public static var screenRoomDetailsErrorUnmuting: String { return L10n.tr("Localizable", "screen_room_details_error_unmuting") } + /// Invite people + public static var screenRoomDetailsInvitePeopleTitle: String { return L10n.tr("Localizable", "screen_room_details_invite_people_title") } + /// Leave room + public static var screenRoomDetailsLeaveRoomTitle: String { return L10n.tr("Localizable", "screen_room_details_leave_room_title") } + /// Custom + public static var screenRoomDetailsNotificationModeCustom: String { return L10n.tr("Localizable", "screen_room_details_notification_mode_custom") } + /// Default + public static var screenRoomDetailsNotificationModeDefault: String { return L10n.tr("Localizable", "screen_room_details_notification_mode_default") } + /// Notifications + public static var screenRoomDetailsNotificationTitle: String { return L10n.tr("Localizable", "screen_room_details_notification_title") } + /// People + public static var screenRoomDetailsPeopleTitle: String { return L10n.tr("Localizable", "screen_room_details_people_title") } + /// Room name + public static var screenRoomDetailsRoomNameLabel: String { return L10n.tr("Localizable", "screen_room_details_room_name_label") } + /// Security + public static var screenRoomDetailsSecurityTitle: String { return L10n.tr("Localizable", "screen_room_details_security_title") } + /// Share room + public static var screenRoomDetailsShareRoomTitle: String { return L10n.tr("Localizable", "screen_room_details_share_room_title") } + /// Topic + public static var screenRoomDetailsTopicTitle: String { return L10n.tr("Localizable", "screen_room_details_topic_title") } + /// Updating room… + public static var screenRoomDetailsUpdatingRoom: String { return L10n.tr("Localizable", "screen_room_details_updating_room") } + /// Message history is currently unavailable in this room + public static var screenRoomEncryptedHistoryBanner: String { return L10n.tr("Localizable", "screen_room_encrypted_history_banner") } + /// Failed processing media to upload, please try again. + public static var screenRoomErrorFailedProcessingMedia: String { return L10n.tr("Localizable", "screen_room_error_failed_processing_media") } + /// Could not retrieve user details + public static var screenRoomErrorFailedRetrievingUserDetails: String { return L10n.tr("Localizable", "screen_room_error_failed_retrieving_user_details") } + /// Would you like to invite them back? + public static var screenRoomInviteAgainAlertMessage: String { return L10n.tr("Localizable", "screen_room_invite_again_alert_message") } + /// You are alone in this chat + public static var screenRoomInviteAgainAlertTitle: String { return L10n.tr("Localizable", "screen_room_invite_again_alert_title") } + /// Block + public static var screenRoomMemberDetailsBlockAlertAction: String { return L10n.tr("Localizable", "screen_room_member_details_block_alert_action") } + /// Blocked users won't be able to send you messages and all their messages will be hidden. You can unblock them anytime. + public static var screenRoomMemberDetailsBlockAlertDescription: String { return L10n.tr("Localizable", "screen_room_member_details_block_alert_description") } + /// Block user + public static var screenRoomMemberDetailsBlockUser: String { return L10n.tr("Localizable", "screen_room_member_details_block_user") } + /// Unblock + public static var screenRoomMemberDetailsUnblockAlertAction: String { return L10n.tr("Localizable", "screen_room_member_details_unblock_alert_action") } + /// You'll be able to see all messages from them again. + public static var screenRoomMemberDetailsUnblockAlertDescription: String { return L10n.tr("Localizable", "screen_room_member_details_unblock_alert_description") } + /// Unblock user + public static var screenRoomMemberDetailsUnblockUser: String { return L10n.tr("Localizable", "screen_room_member_details_unblock_user") } + /// Plural format key: "%#@COUNT@" + public static func screenRoomMemberListHeaderTitle(_ p1: Int) -> String { + return L10n.tr("Localizable", "screen_room_member_list_header_title", p1) + } + /// Pending + public static var screenRoomMemberListPendingHeaderTitle: String { return L10n.tr("Localizable", "screen_room_member_list_pending_header_title") } + /// Room members + public static var screenRoomMemberListRoomMembersHeaderTitle: String { return L10n.tr("Localizable", "screen_room_member_list_room_members_header_title") } + /// Message copied + public static var screenRoomMessageCopied: String { return L10n.tr("Localizable", "screen_room_message_copied") } + /// You do not have permission to post to this room + public static var screenRoomNoPermissionToPost: String { return L10n.tr("Localizable", "screen_room_no_permission_to_post") } + /// Allow custom setting + public static var screenRoomNotificationSettingsAllowCustom: String { return L10n.tr("Localizable", "screen_room_notification_settings_allow_custom") } + /// Turning this on will override your default setting + public static var screenRoomNotificationSettingsAllowCustomFootnote: String { return L10n.tr("Localizable", "screen_room_notification_settings_allow_custom_footnote") } + /// Notify me in this chat for + public static var screenRoomNotificationSettingsCustomSettingsTitle: String { return L10n.tr("Localizable", "screen_room_notification_settings_custom_settings_title") } + /// You can change it in your %1$@. + public static func screenRoomNotificationSettingsDefaultSettingFootnote(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_room_notification_settings_default_setting_footnote", String(describing: p1)) + } + /// global settings + public static var screenRoomNotificationSettingsDefaultSettingFootnoteContentLink: String { return L10n.tr("Localizable", "screen_room_notification_settings_default_setting_footnote_content_link") } + /// Default setting + public static var screenRoomNotificationSettingsDefaultSettingTitle: String { return L10n.tr("Localizable", "screen_room_notification_settings_default_setting_title") } + /// Remove custom setting + public static var screenRoomNotificationSettingsEditRemoveSetting: String { return L10n.tr("Localizable", "screen_room_notification_settings_edit_remove_setting") } + /// An error occurred while loading notification settings. + public static var screenRoomNotificationSettingsErrorLoadingSettings: String { return L10n.tr("Localizable", "screen_room_notification_settings_error_loading_settings") } + /// Failed restoring the default mode, please try again. + public static var screenRoomNotificationSettingsErrorRestoringDefault: String { return L10n.tr("Localizable", "screen_room_notification_settings_error_restoring_default") } + /// Failed setting the mode, please try again. + public static var screenRoomNotificationSettingsErrorSettingMode: String { return L10n.tr("Localizable", "screen_room_notification_settings_error_setting_mode") } + /// All messages + public static var screenRoomNotificationSettingsModeAllMessages: String { return L10n.tr("Localizable", "screen_room_notification_settings_mode_all_messages") } + /// Mentions and Keywords only + public static var screenRoomNotificationSettingsModeMentionsAndKeywords: String { return L10n.tr("Localizable", "screen_room_notification_settings_mode_mentions_and_keywords") } + /// In this room, notify me for + public static var screenRoomNotificationSettingsRoomCustomSettingsTitle: String { return L10n.tr("Localizable", "screen_room_notification_settings_room_custom_settings_title") } + /// Show less + public static var screenRoomReactionsShowLess: String { return L10n.tr("Localizable", "screen_room_reactions_show_less") } + /// Show more + public static var screenRoomReactionsShowMore: String { return L10n.tr("Localizable", "screen_room_reactions_show_more") } + /// Remove + public static var screenRoomRetrySendMenuRemoveAction: String { return L10n.tr("Localizable", "screen_room_retry_send_menu_remove_action") } + /// Send again + public static var screenRoomRetrySendMenuSendAgainAction: String { return L10n.tr("Localizable", "screen_room_retry_send_menu_send_again_action") } + /// Your message failed to send + public static var screenRoomRetrySendMenuTitle: String { return L10n.tr("Localizable", "screen_room_retry_send_menu_title") } + /// Add emoji + public static var screenRoomTimelineAddReaction: String { return L10n.tr("Localizable", "screen_room_timeline_add_reaction") } + /// Show less + public static var screenRoomTimelineLessReactions: String { return L10n.tr("Localizable", "screen_room_timeline_less_reactions") } + /// Create a new conversation or room + public static var screenRoomlistA11yCreateMessage: String { return L10n.tr("Localizable", "screen_roomlist_a11y_create_message") } + /// Get started by messaging someone. + public static var screenRoomlistEmptyMessage: String { return L10n.tr("Localizable", "screen_roomlist_empty_message") } + /// No chats yet. + public static var screenRoomlistEmptyTitle: String { return L10n.tr("Localizable", "screen_roomlist_empty_title") } + /// All Chats + public static var screenRoomlistMainSpaceTitle: String { return L10n.tr("Localizable", "screen_roomlist_main_space_title") } + /// Change account provider + public static var screenServerConfirmationChangeServer: String { return L10n.tr("Localizable", "screen_server_confirmation_change_server") } + /// A private server for Element employees. + public static var screenServerConfirmationMessageLoginElementDotIo: String { return L10n.tr("Localizable", "screen_server_confirmation_message_login_element_dot_io") } + /// Matrix is an open network for secure, decentralised communication. + public static var screenServerConfirmationMessageLoginMatrixDotOrg: String { return L10n.tr("Localizable", "screen_server_confirmation_message_login_matrix_dot_org") } + /// This is where your conversations will live — just like you would use an email provider to keep your emails. + public static var screenServerConfirmationMessageRegister: String { return L10n.tr("Localizable", "screen_server_confirmation_message_register") } + /// You’re about to sign in to %1$@ + public static func screenServerConfirmationTitleLogin(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_server_confirmation_title_login", String(describing: p1)) + } + /// You’re about to create an account on %1$@ + public static func screenServerConfirmationTitleRegister(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_server_confirmation_title_register", String(describing: p1)) + } + /// Something doesn’t seem right. Either the request timed out or the request was denied. + public static var screenSessionVerificationCancelledSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_cancelled_subtitle") } + /// Verification cancelled + public static var screenSessionVerificationCancelledTitle: String { return L10n.tr("Localizable", "screen_session_verification_cancelled_title") } + /// Confirm that the emojis below match those shown on your other session. + public static var screenSessionVerificationCompareEmojisSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_compare_emojis_subtitle") } + /// Compare emojis + public static var screenSessionVerificationCompareEmojisTitle: String { return L10n.tr("Localizable", "screen_session_verification_compare_emojis_title") } + /// Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted. + public static var screenSessionVerificationCompleteSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_complete_subtitle") } + /// Prove it’s you in order to access your encrypted message history. + public static var screenSessionVerificationOpenExistingSessionSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_open_existing_session_subtitle") } + /// Open an existing session + public static var screenSessionVerificationOpenExistingSessionTitle: String { return L10n.tr("Localizable", "screen_session_verification_open_existing_session_title") } + /// Retry verification + public static var screenSessionVerificationPositiveButtonCanceled: String { return L10n.tr("Localizable", "screen_session_verification_positive_button_canceled") } + /// I am ready + public static var screenSessionVerificationPositiveButtonInitial: String { return L10n.tr("Localizable", "screen_session_verification_positive_button_initial") } + /// Start + public static var screenSessionVerificationPositiveButtonReady: String { return L10n.tr("Localizable", "screen_session_verification_positive_button_ready") } + /// Waiting to match + public static var screenSessionVerificationPositiveButtonVerifyingOngoing: String { return L10n.tr("Localizable", "screen_session_verification_positive_button_verifying_ongoing") } + /// 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 + public static var screenSessionVerificationTheyDontMatch: String { return L10n.tr("Localizable", "screen_session_verification_they_dont_match") } + /// They match + public static var screenSessionVerificationTheyMatch: String { return L10n.tr("Localizable", "screen_session_verification_they_match") } + /// Accept the request to start the verification process in your other session to continue. + public static var screenSessionVerificationWaitingToAcceptSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_waiting_to_accept_subtitle") } + /// Waiting to accept request + public static var screenSessionVerificationWaitingToAcceptTitle: String { return L10n.tr("Localizable", "screen_session_verification_waiting_to_accept_title") } + /// Account and devices + public static var screenSettingsOidcAccount: String { return L10n.tr("Localizable", "screen_settings_oidc_account") } + /// Share location + public static var screenShareLocationTitle: String { return L10n.tr("Localizable", "screen_share_location_title") } + /// Share my location + public static var screenShareMyLocationAction: String { return L10n.tr("Localizable", "screen_share_my_location_action") } + /// Open in Apple Maps + public static var screenShareOpenAppleMaps: String { return L10n.tr("Localizable", "screen_share_open_apple_maps") } + /// Open in Google Maps + public static var screenShareOpenGoogleMaps: String { return L10n.tr("Localizable", "screen_share_open_google_maps") } + /// Open in OpenStreetMap + public static var screenShareOpenOsmMaps: String { return L10n.tr("Localizable", "screen_share_open_osm_maps") } + /// Share this location + public static var screenShareThisLocationAction: String { return L10n.tr("Localizable", "screen_share_this_location_action") } + /// Are you sure you want to sign out? + public static var screenSignoutConfirmationDialogContent: String { return L10n.tr("Localizable", "screen_signout_confirmation_dialog_content") } + /// Sign out + public static var screenSignoutConfirmationDialogSubmit: String { return L10n.tr("Localizable", "screen_signout_confirmation_dialog_submit") } + /// Sign out + public static var screenSignoutConfirmationDialogTitle: String { return L10n.tr("Localizable", "screen_signout_confirmation_dialog_title") } + /// Signing out… + public static var screenSignoutInProgressDialogContent: String { return L10n.tr("Localizable", "screen_signout_in_progress_dialog_content") } + /// Sign out + public static var screenSignoutPreferenceItem: String { return L10n.tr("Localizable", "screen_signout_preference_item") } + /// An error occurred when trying to start a chat + public static var screenStartChatErrorStartingChat: String { return L10n.tr("Localizable", "screen_start_chat_error_starting_chat") } + /// Location + public static var screenViewLocationTitle: String { return L10n.tr("Localizable", "screen_view_location_title") } + /// There's a high demand for %1$@ on %2$@ at the moment. Come back to the app in a few days and try again. + /// + /// Thanks for your patience! + public static func screenWaitlistMessage(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "screen_waitlist_message", String(describing: p1), String(describing: p2)) + } + /// Welcome to %1$@! + public static func screenWaitlistMessageSuccess(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_waitlist_message_success", String(describing: p1)) + } + /// You’re almost there. + public static var screenWaitlistTitle: String { return L10n.tr("Localizable", "screen_waitlist_title") } + /// You're in. + public static var screenWaitlistTitleSuccess: String { return L10n.tr("Localizable", "screen_waitlist_title_success") } + /// Calls, polls, search and more will be added later this year. + public static var screenWelcomeBullet1: String { return L10n.tr("Localizable", "screen_welcome_bullet_1") } + /// Message history for encrypted rooms won’t be available in this update. + public static var screenWelcomeBullet2: String { return L10n.tr("Localizable", "screen_welcome_bullet_2") } + /// We’d love to hear from you, let us know what you think via the settings page. + public static var screenWelcomeBullet3: String { return L10n.tr("Localizable", "screen_welcome_bullet_3") } + /// Let's go! + public static var screenWelcomeButton: String { return L10n.tr("Localizable", "screen_welcome_button") } + /// Here’s what you need to know: + public static var screenWelcomeSubtitle: String { return L10n.tr("Localizable", "screen_welcome_subtitle") } + /// Welcome to %1$@! + public static func screenWelcomeTitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_welcome_title", String(describing: p1)) + } + /// Looks like you’re using a new device. Verify with another device to access your encrypted messages moving forwards. + public static var sessionVerificationBannerMessage: String { return L10n.tr("Localizable", "session_verification_banner_message") } + /// Verify it’s you + public static var sessionVerificationBannerTitle: String { return L10n.tr("Localizable", "session_verification_banner_title") } + /// Rageshake + public static var settingsRageshake: String { return L10n.tr("Localizable", "settings_rageshake") } + /// Detection threshold + public static var settingsRageshakeDetectionThreshold: String { return L10n.tr("Localizable", "settings_rageshake_detection_threshold") } + /// General + public static var settingsTitleGeneral: String { return L10n.tr("Localizable", "settings_title_general") } + /// Version: %1$@ (%2$@) + public static func settingsVersionNumber(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "settings_version_number", String(describing: p1), String(describing: p2)) + } + /// (avatar was changed too) + public static var stateEventAvatarChangedToo: String { return L10n.tr("Localizable", "state_event_avatar_changed_too") } + /// %1$@ changed their avatar + public static func stateEventAvatarUrlChanged(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_avatar_url_changed", String(describing: p1)) + } + /// You changed your avatar + public static var stateEventAvatarUrlChangedByYou: String { return L10n.tr("Localizable", "state_event_avatar_url_changed_by_you") } + /// %1$@ changed their display name from %2$@ to %3$@ + public static func stateEventDisplayNameChangedFrom(_ p1: Any, _ p2: Any, _ p3: Any) -> String { + return L10n.tr("Localizable", "state_event_display_name_changed_from", String(describing: p1), String(describing: p2), String(describing: p3)) + } + /// You changed your display name from %1$@ to %2$@ + public static func stateEventDisplayNameChangedFromByYou(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "state_event_display_name_changed_from_by_you", String(describing: p1), String(describing: p2)) + } + /// %1$@ removed their display name (it was %2$@) + public static func stateEventDisplayNameRemoved(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "state_event_display_name_removed", String(describing: p1), String(describing: p2)) + } + /// You removed your display name (it was %1$@) + public static func stateEventDisplayNameRemovedByYou(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_display_name_removed_by_you", String(describing: p1)) + } + /// %1$@ set their display name to %2$@ + public static func stateEventDisplayNameSet(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "state_event_display_name_set", String(describing: p1), String(describing: p2)) + } + /// You set your display name to %1$@ + public static func stateEventDisplayNameSetByYou(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_display_name_set_by_you", String(describing: p1)) + } + /// %1$@ changed the room avatar + public static func stateEventRoomAvatarChanged(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_avatar_changed", String(describing: p1)) + } + /// You changed the room avatar + public static var stateEventRoomAvatarChangedByYou: String { return L10n.tr("Localizable", "state_event_room_avatar_changed_by_you") } + /// %1$@ removed the room avatar + public static func stateEventRoomAvatarRemoved(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_avatar_removed", String(describing: p1)) + } + /// You removed the room avatar + public static var stateEventRoomAvatarRemovedByYou: String { return L10n.tr("Localizable", "state_event_room_avatar_removed_by_you") } + /// %1$@ banned %2$@ + public static func stateEventRoomBan(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "state_event_room_ban", String(describing: p1), String(describing: p2)) + } + /// You banned %1$@ + public static func stateEventRoomBanByYou(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_ban_by_you", String(describing: p1)) + } + /// %1$@ created the room + public static func stateEventRoomCreated(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_created", String(describing: p1)) + } + /// You created the room + public static var stateEventRoomCreatedByYou: String { return L10n.tr("Localizable", "state_event_room_created_by_you") } + /// %1$@ invited %2$@ + public static func stateEventRoomInvite(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "state_event_room_invite", String(describing: p1), String(describing: p2)) + } + /// %1$@ accepted the invite + public static func stateEventRoomInviteAccepted(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_invite_accepted", String(describing: p1)) + } + /// You accepted the invite + public static var stateEventRoomInviteAcceptedByYou: String { return L10n.tr("Localizable", "state_event_room_invite_accepted_by_you") } + /// You invited %1$@ + public static func stateEventRoomInviteByYou(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_invite_by_you", String(describing: p1)) + } + /// %1$@ invited you + public static func stateEventRoomInviteYou(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_invite_you", String(describing: p1)) + } + /// %1$@ joined the room + public static func stateEventRoomJoin(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_join", String(describing: p1)) + } + /// You joined the room + public static var stateEventRoomJoinByYou: String { return L10n.tr("Localizable", "state_event_room_join_by_you") } + /// %1$@ requested to join + public static func stateEventRoomKnock(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_knock", String(describing: p1)) + } + /// %1$@ allowed %2$@ to join + public static func stateEventRoomKnockAccepted(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "state_event_room_knock_accepted", String(describing: p1), String(describing: p2)) + } + /// %1$@ allowed you to join + public static func stateEventRoomKnockAcceptedByYou(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_knock_accepted_by_you", String(describing: p1)) + } + /// You requested to join + public static var stateEventRoomKnockByYou: String { return L10n.tr("Localizable", "state_event_room_knock_by_you") } + /// %1$@ rejected %2$@'s request to join + public static func stateEventRoomKnockDenied(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "state_event_room_knock_denied", String(describing: p1), String(describing: p2)) + } + /// You rejected %1$@'s request to join + public static func stateEventRoomKnockDeniedByYou(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_knock_denied_by_you", String(describing: p1)) + } + /// %1$@ rejected your request to join + public static func stateEventRoomKnockDeniedYou(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_knock_denied_you", String(describing: p1)) + } + /// %1$@ is no longer interested in joining + public static func stateEventRoomKnockRetracted(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_knock_retracted", String(describing: p1)) + } + /// You cancelled your request to join + public static var stateEventRoomKnockRetractedByYou: String { return L10n.tr("Localizable", "state_event_room_knock_retracted_by_you") } + /// %1$@ left the room + public static func stateEventRoomLeave(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_leave", String(describing: p1)) + } + /// You left the room + public static var stateEventRoomLeaveByYou: String { return L10n.tr("Localizable", "state_event_room_leave_by_you") } + /// %1$@ changed the room name to: %2$@ + public static func stateEventRoomNameChanged(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "state_event_room_name_changed", String(describing: p1), String(describing: p2)) + } + /// You changed the room name to: %1$@ + public static func stateEventRoomNameChangedByYou(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_name_changed_by_you", String(describing: p1)) + } + /// %1$@ removed the room name + public static func stateEventRoomNameRemoved(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_name_removed", String(describing: p1)) + } + /// You removed the room name + public static var stateEventRoomNameRemovedByYou: String { return L10n.tr("Localizable", "state_event_room_name_removed_by_you") } + /// %1$@ rejected the invitation + public static func stateEventRoomReject(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_reject", String(describing: p1)) + } + /// You rejected the invitation + public static var stateEventRoomRejectByYou: String { return L10n.tr("Localizable", "state_event_room_reject_by_you") } + /// %1$@ removed %2$@ + public static func stateEventRoomRemove(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "state_event_room_remove", String(describing: p1), String(describing: p2)) + } + /// You removed %1$@ + public static func stateEventRoomRemoveByYou(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_remove_by_you", String(describing: p1)) + } + /// %1$@ sent an invitation to %2$@ to join the room + public static func stateEventRoomThirdPartyInvite(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "state_event_room_third_party_invite", String(describing: p1), String(describing: p2)) + } + /// You sent an invitation to %1$@ to join the room + public static func stateEventRoomThirdPartyInviteByYou(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_third_party_invite_by_you", String(describing: p1)) + } + /// %1$@ revoked the invitation for %2$@ to join the room + public static func stateEventRoomThirdPartyRevokedInvite(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "state_event_room_third_party_revoked_invite", String(describing: p1), String(describing: p2)) + } + /// You revoked the invitation for %1$@ to join the room + public static func stateEventRoomThirdPartyRevokedInviteByYou(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_third_party_revoked_invite_by_you", String(describing: p1)) + } + /// %1$@ changed the topic to: %2$@ + public static func stateEventRoomTopicChanged(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "state_event_room_topic_changed", String(describing: p1), String(describing: p2)) + } + /// You changed the topic to: %1$@ + public static func stateEventRoomTopicChangedByYou(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_topic_changed_by_you", String(describing: p1)) + } + /// %1$@ removed the room topic + public static func stateEventRoomTopicRemoved(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_topic_removed", String(describing: p1)) + } + /// You removed the room topic + public static var stateEventRoomTopicRemovedByYou: String { return L10n.tr("Localizable", "state_event_room_topic_removed_by_you") } + /// %1$@ unbanned %2$@ + public static func stateEventRoomUnban(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "state_event_room_unban", String(describing: p1), String(describing: p2)) + } + /// You unbanned %1$@ + public static func stateEventRoomUnbanByYou(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_unban_by_you", String(describing: p1)) + } + /// %1$@ made an unknown change to their membership + public static func stateEventRoomUnknownMembershipChange(_ p1: Any) -> String { + return L10n.tr("Localizable", "state_event_room_unknown_membership_change", String(describing: p1)) + } + /// en + public static var testLanguageIdentifier: String { return L10n.tr("Localizable", "test_language_identifier") } + /// en + public static var testUntranslatedDefaultLanguageIdentifier: String { return L10n.tr("Localizable", "test_untranslated_default_language_identifier") } + + public enum Action { + /// Edit poll + public static var editPoll: String { return L10n.tr("Localizable", "action.edit_poll") } + } +} +// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces + +// MARK: - Implementation Details + +extension L10n { + static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { + // Use preferredLocalizations to get a language that is in the bundle and the user's preferred list of languages. + let languages = Bundle.overrideLocalizations ?? Bundle.app.preferredLocalizations + + for language in languages { + if let translation = trIn(language, table, key, args) { + return translation + } + } + return Bundle.app.developmentLocalization.flatMap { trIn($0, table, key, args) } ?? key + } + + private static func trIn(_ language: String, _ table: String, _ key: String, _ args: CVarArg...) -> String? { + guard let bundle = Bundle.lprojBundle(for: language) else { return nil } + let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "") + let translation = String(format: format, locale: Locale(identifier: language), arguments: args) + guard translation != key else { return nil } + return translation + } +} + +// swiftlint:enable all diff --git a/ios/NSE/HTMLParsing/AttributedStringBuilder.swift b/ios/NSE/HTMLParsing/AttributedStringBuilder.swift new file mode 100644 index 0000000000..d0c7a9146b --- /dev/null +++ b/ios/NSE/HTMLParsing/AttributedStringBuilder.swift @@ -0,0 +1,282 @@ +// +// Copyright 2022 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. +// + +import DTCoreText +import Foundation +import LRUCache + +struct AttributedStringBuilder: AttributedStringBuilderProtocol { + private let temporaryBlockquoteMarkingColor = UIColor.magenta + private let temporaryCodeBlockMarkingColor = UIColor.cyan + private let linkColor = UIColor.blue + private let permalinkBaseURL: URL + + private static var cache = LRUCache(countLimit: 1000) + + static func invalidateCache() { + cache.removeAllValues() + } + + init(permalinkBaseURL: URL) { + self.permalinkBaseURL = permalinkBaseURL + } + + func fromPlain(_ string: String?) -> AttributedString? { + guard let string else { + return nil + } + + if let cached = Self.cache.value(forKey: string) { + return cached + } + + let mutableAttributedString = NSMutableAttributedString(string: string) + addLinks(mutableAttributedString) + removeLinkColors(mutableAttributedString) + + let result = try? AttributedString(mutableAttributedString, including: \.elementX) + Self.cache.setValue(result, forKey: string) + return result + } + + // Do not use the default HTML renderer of NSAttributedString because this method + // runs on the UI thread which we want to avoid because renderHTMLString is called + // most of the time from a background thread. + // Use DTCoreText HTML renderer instead. + // Using DTCoreText, which renders static string, helps to avoid code injection attacks + // that could happen with the default HTML renderer of NSAttributedString which is a + // webview. + func fromHTML(_ htmlString: String?) -> AttributedString? { + guard let htmlString else { + return nil + } + + if let cached = Self.cache.value(forKey: htmlString) { + return cached + } + + guard let data = htmlString.data(using: .utf8) else { + return nil + } + + let defaultFont = UIFont.preferredFont(forTextStyle: .body) + + let parsingOptions: [String: Any] = [ + DTUseiOS6Attributes: true, + DTDefaultFontFamily: defaultFont.familyName, + DTDefaultFontName: defaultFont.fontName, + DTDefaultFontSize: defaultFont.pointSize, + DTDefaultStyleSheet: DTCSSStylesheet(styleBlock: defaultCSS) as Any + ] + + guard let builder = DTHTMLAttributedStringBuilder(html: data, options: parsingOptions, documentAttributes: nil) else { + return nil + } + + builder.willFlushCallback = { element in + element?.sanitize(font: defaultFont) + } + + guard let attributedString = builder.generatedAttributedString() else { + return nil + } + + let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString) + removeDefaultForegroundColor(mutableAttributedString) + addLinks(mutableAttributedString) + detectPermalinks(mutableAttributedString) + removeLinkColors(mutableAttributedString) + replaceMarkedBlockquotes(mutableAttributedString) + replaceMarkedCodeBlocks(mutableAttributedString) + removeDTCoreTextArtifacts(mutableAttributedString) + + let result = try? AttributedString(mutableAttributedString, including: \.elementX) + Self.cache.setValue(result, forKey: htmlString) + return result + } + + // MARK: - Private + + private func replaceMarkedBlockquotes(_ attributedString: NSMutableAttributedString) { + // According to blockquotes in the string, DTCoreText can apply 2 policies: + // - define a `DTTextBlocksAttribute` attribute on a
block + // - or, just define a `NSBackgroundColorAttributeName` attribute + attributedString.enumerateAttribute(.DTTextBlocks, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in + guard let value = value as? NSArray, + let dtTextBlock = value.firstObject as? DTTextBlock, + dtTextBlock.backgroundColor == temporaryBlockquoteMarkingColor else { + return + } + + attributedString.addAttribute(.MatrixBlockquote, value: true, range: range) + } + + attributedString.enumerateAttribute(.backgroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in + guard let value = value as? UIColor, + value == temporaryBlockquoteMarkingColor else { + return + } + + attributedString.removeAttribute(.backgroundColor, range: range) + attributedString.addAttribute(.MatrixBlockquote, value: true, range: range) + } + } + + func replaceMarkedCodeBlocks(_ attributedString: NSMutableAttributedString) { + attributedString.enumerateAttribute(.backgroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in + if let value = value as? UIColor, + value == temporaryCodeBlockMarkingColor { + attributedString.addAttribute(.backgroundColor, value: UIColor(.compound._bgCodeBlock) as Any, range: range) + } + } + } + + private func removeDTCoreTextArtifacts(_ attributedString: NSMutableAttributedString) { + guard attributedString.length > 0 else { + return + } + + // DTCoreText adds a newline at the end of plain text ( https://github.com/Cocoanetics/DTCoreText/issues/779 ) + // or after a blockquote section. + // Trim trailing whitespace and newlines in the string content + while (attributedString.string as NSString).hasSuffixCharacter(from: .whitespacesAndNewlines) { + attributedString.deleteCharacters(in: .init(location: attributedString.length - 1, length: 1)) + } + } + + private func addLinks(_ attributedString: NSMutableAttributedString) { + let string = attributedString.string + + var matches = MatrixEntityRegex.userIdentifierRegex.matches(in: string, options: []) + matches.append(contentsOf: MatrixEntityRegex.roomIdentifierRegex.matches(in: string, options: [])) + // As of right now we do not handle event id links in any way so there is no need to add them as links + // matches.append(contentsOf: MatrixEntityRegex.eventIdentifierRegex.matches(in: string, options: [])) + matches.append(contentsOf: MatrixEntityRegex.roomAliasRegex.matches(in: string, options: [])) + + let linkMatches = MatrixEntityRegex.linkRegex.matches(in: string, options: []) + matches.append(contentsOf: linkMatches) + guard matches.count > 0 else { + return + } + + // Sort the links by length so the longest one always takes priority + matches.sorted { $0.range.length > $1.range.length }.forEach { match in + guard let matchRange = Range(match.range, in: string) else { + return + } + + var hasLink = false + attributedString.enumerateAttribute(.link, in: match.range, options: []) { value, _, stop in + if value != nil { + hasLink = true + stop.pointee = true + } + } + + if hasLink { + return + } + + var link = String(string[matchRange]) + + if linkMatches.contains(match), !link.contains("://") { + link.insert(contentsOf: "https://", at: link.startIndex) + } + + attributedString.addAttribute(.link, value: link as Any, range: match.range) + } + } + + private func detectPermalinks(_ attributedString: NSMutableAttributedString) { + attributedString.enumerateAttribute(.link, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in + if value != nil { + if let url = value as? URL { + switch PermalinkBuilder.detectPermalink(in: url, baseURL: permalinkBaseURL) { + case .userIdentifier(let identifier): + attributedString.addAttributes([.MatrixUserID: identifier], range: range) + case .roomIdentifier(let identifier): + attributedString.addAttributes([.MatrixRoomID: identifier], range: range) + case .roomAlias(let alias): + attributedString.addAttributes([.MatrixRoomAlias: alias], range: range) + case .event(let roomIdentifier, let eventIdentifier): + attributedString.addAttributes([.MatrixEventID: EventIDAttributeValue(roomID: roomIdentifier, eventID: eventIdentifier)], range: range) + case .none: + break + } + } + } + } + } + + private func removeDefaultForegroundColor(_ attributedString: NSMutableAttributedString) { + attributedString.enumerateAttribute(.foregroundColor, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in + if value as? UIColor == UIColor.black { + attributedString.removeAttribute(.foregroundColor, range: range) + } + } + } + + private func removeLinkColors(_ attributedString: NSMutableAttributedString) { + attributedString.enumerateAttribute(.link, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in + if value != nil { + attributedString.removeAttribute(.foregroundColor, range: range) + } + } + } + + private var defaultCSS: String { + """ + blockquote { + background: \(temporaryBlockquoteMarkingColor.toHexString()); + display: block; + } + pre,code { + background-color: \(temporaryCodeBlockMarkingColor.toHexString()); + display: inline; + white-space: pre; + font-size: 0.9em; + -coretext-fontname: .AppleSystemUIFontMonospaced-Regular; + } + h1,h2,h3 { + font-size: 1.2em; + } + """ + } +} + +extension UIColor { + func toHexString() -> String { + var red: CGFloat = 0.0 + var green: CGFloat = 0.0 + var blue: CGFloat = 0.0 + var alpha: CGFloat = 0.0 + + getRed(&red, green: &green, blue: &blue, alpha: &alpha) + + let rgb = Int(red * 255) << 16 | Int(green * 255) << 8 | Int(blue * 255) << 0 + + return NSString(format: "#%06x", rgb) as String + } +} + +extension NSAttributedString.Key { + static let DTTextBlocks: NSAttributedString.Key = .init(rawValue: DTTextBlocksAttribute) + static let MatrixBlockquote: NSAttributedString.Key = .init(rawValue: BlockquoteAttribute.name) + static let MatrixUserID: NSAttributedString.Key = .init(rawValue: UserIDAttribute.name) + static let MatrixRoomID: NSAttributedString.Key = .init(rawValue: RoomIDAttribute.name) + static let MatrixRoomAlias: NSAttributedString.Key = .init(rawValue: RoomAliasAttribute.name) + static let MatrixEventID: NSAttributedString.Key = .init(rawValue: EventIDAttribute.name) +} diff --git a/ios/NSE/HTMLParsing/AttributedStringBuilderProtocol.swift b/ios/NSE/HTMLParsing/AttributedStringBuilderProtocol.swift new file mode 100644 index 0000000000..1c244265c7 --- /dev/null +++ b/ios/NSE/HTMLParsing/AttributedStringBuilderProtocol.swift @@ -0,0 +1,28 @@ +// +// Copyright 2022 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. +// + +import Foundation + +struct AttributedStringBuilderComponent: Hashable { + let attributedString: AttributedString + let isBlockquote: Bool +} + +protocol AttributedStringBuilderProtocol { + func fromPlain(_ string: String?) -> AttributedString? + + func fromHTML(_ htmlString: String?) -> AttributedString? +} diff --git a/ios/NSE/HTMLParsing/DTHTMLElement+AttributedStringBuilder.swift b/ios/NSE/HTMLParsing/DTHTMLElement+AttributedStringBuilder.swift new file mode 100644 index 0000000000..7b7d7da71b --- /dev/null +++ b/ios/NSE/HTMLParsing/DTHTMLElement+AttributedStringBuilder.swift @@ -0,0 +1,76 @@ +// +// Copyright 2022 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. +// + +import DTCoreText +import Foundation + +public extension DTHTMLElement { + /// Sanitize the element using the given parameters. + /// - Parameters: + /// - font: The default font to use when resetting the content of any unsupported tags. + @objc func sanitize(font: UIFont) { + if let name, !Self.allowedHTMLTags.contains(name) { + // This is an unsupported tag. + // Remove any attachments to fix rendering. + textAttachment = nil + + // If the element has plain text content show that, + // otherwise prevent the tag from displaying. + if let stringContent = attributedString()?.string, + !stringContent.isEmpty, + let element = DTTextHTMLElement(name: nil, attributes: nil) { + element.setText(stringContent) + removeAllChildNodes() + addChildNode(element) + + if let parent = parent() { + element.inheritAttributes(from: parent) + } else { + fontDescriptor = DTCoreTextFontDescriptor() + fontDescriptor.fontFamily = font.familyName + fontDescriptor.fontName = font.fontName + fontDescriptor.pointSize = font.pointSize + paragraphStyle = DTCoreTextParagraphStyle.default() + + element.inheritAttributes(from: self) + } + element.interpretAttributes() + + } else if let parent = parent() { + parent.removeChildNode(self) + } else { + didOutput = true + } + + } else { + // This element is a supported tag, but it may contain children that aren't, + // so santize all child nodes to ensure correct tags. + if let childNodes = childNodes as? [DTHTMLElement] { + childNodes.forEach { $0.sanitize(font: font) } + } + } + } + + private static var allowedHTMLTags = { + ["font", // custom to matrix for IRC-style font coloring + "del", // for markdown + "body", // added internally by DTCoreText + "mx-reply", + "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "p", "a", "ul", "ol", + "nl", "li", "b", "i", "u", "strong", "em", "strike", "code", "hr", "br", "div", + "table", "thead", "caption", "tbody", "tr", "th", "td", "pre"] + }() +} diff --git a/ios/NSE/HTMLParsing/ElementXAttributeScope.swift b/ios/NSE/HTMLParsing/ElementXAttributeScope.swift new file mode 100644 index 0000000000..a14a927012 --- /dev/null +++ b/ios/NSE/HTMLParsing/ElementXAttributeScope.swift @@ -0,0 +1,69 @@ +// +// Copyright 2022 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. +// + +import Foundation + +enum BlockquoteAttribute: AttributedStringKey { + typealias Value = Bool + public static var name = "MXBlockquoteAttribute" +} + +enum UserIDAttribute: AttributedStringKey { + typealias Value = String + public static var name = "MXUserIDAttribute" +} + +enum RoomIDAttribute: AttributedStringKey { + typealias Value = String + public static var name = "MXRoomIDAttribute" +} + +enum RoomAliasAttribute: AttributedStringKey { + typealias Value = String + public static var name = "MXRoomAliasAttribute" +} + +struct EventIDAttributeValue: Hashable { + let roomID: String + let eventID: String +} + +enum EventIDAttribute: AttributedStringKey { + typealias Value = EventIDAttributeValue + public static var name = "MXEventIDAttribute" +} + +extension AttributeScopes { + struct ElementXAttributes: AttributeScope { + let blockquote: BlockquoteAttribute + + let userID: UserIDAttribute + let roomID: RoomIDAttribute + let roomAlias: RoomAliasAttribute + let eventID: EventIDAttribute + + let swiftUI: SwiftUIAttributes + let uiKit: UIKitAttributes + } + + var elementX: ElementXAttributes.Type { ElementXAttributes.self } +} + +extension AttributeDynamicLookup { + subscript(dynamicMember keyPath: KeyPath) -> T { + self[T.self] + } +} diff --git a/ios/NSE/HTMLParsing/UIFont+AttributedStringBuilder.h b/ios/NSE/HTMLParsing/UIFont+AttributedStringBuilder.h new file mode 100644 index 0000000000..3c574509c8 --- /dev/null +++ b/ios/NSE/HTMLParsing/UIFont+AttributedStringBuilder.h @@ -0,0 +1,27 @@ +// +// Copyright 2022 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. +// + +@import UIKit; + +NS_ASSUME_NONNULL_BEGIN + +@interface UIFont(DTCoreTextFix) + +// Fix DTCoreText iOS 13 issue (https://github.com/Cocoanetics/DTCoreText/issues/1168) + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/NSE/HTMLParsing/UIFont+AttributedStringBuilder.m b/ios/NSE/HTMLParsing/UIFont+AttributedStringBuilder.m new file mode 100644 index 0000000000..9c24400ac0 --- /dev/null +++ b/ios/NSE/HTMLParsing/UIFont+AttributedStringBuilder.m @@ -0,0 +1,83 @@ +// +// Copyright 2022 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. +// + +#import "UIFont+AttributedStringBuilder.h" + +@import UIKit; +@import CoreText; +@import ObjectiveC; + +#pragma mark - UIFont DTCoreText fix + +@interface UIFont (vc_DTCoreTextFix) + ++ (UIFont *)vc_fixedFontWithCTFont:(CTFontRef)ctFont; + +@end + +@implementation UIFont (vc_DTCoreTextFix) + ++ (UIFont *)vc_fixedFontWithCTFont:(CTFontRef)ctFont { + NSString *fontName = (__bridge_transfer NSString *)CTFontCopyName(ctFont, kCTFontPostScriptNameKey); + + CGFloat fontSize = CTFontGetSize(ctFont); + UIFont *font = [UIFont fontWithName:fontName size:fontSize]; + + // On iOS 13+ "TimesNewRomanPSMT" will be used instead of "SFUI" + // In case of "Times New Roman" fallback, use system font and reuse UIFontDescriptorSymbolicTraits. + if ([font.familyName.lowercaseString containsString:@"times"]) { + UIFontDescriptorSymbolicTraits symbolicTraits = (UIFontDescriptorSymbolicTraits)CTFontGetSymbolicTraits(ctFont); + + // Start with the body text style and update it to keep consistent line spacing with plain text messages. + UIFontDescriptor *fontDescriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleBody]; + fontDescriptor = [fontDescriptor fontDescriptorWithSize:fontSize]; + fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits]; + + font = [UIFont fontWithDescriptor:fontDescriptor size:fontSize]; + } + + + return font; +} + +@end + +#pragma mark - Implementation + +@implementation UIFont(DTCoreTextFix) + +// DTCoreText iOS 13 fix. See issue and comment here: https://github.com/Cocoanetics/DTCoreText/issues/1168#issuecomment-583541514 +// Also see https://github.com/Cocoanetics/DTCoreText/pull/1245 for a possible future solution ++ (void)load +{ + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + Class originalClass = object_getClass([UIFont class]); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wundeclared-selector" + SEL originalSelector = @selector(fontWithCTFont:); // DTCoreText method we're overriding + SEL ourSelector = @selector(vc_fixedFontWithCTFont:); // Use custom implementation +#pragma clang diagnostic pop + + Method originalMethod = class_getClassMethod(originalClass, originalSelector); + Method swizzledMethod = class_getClassMethod(originalClass, ourSelector); + + method_exchangeImplementations(originalMethod, swizzledMethod); + }); +} + +@end diff --git a/ios/NSE/ImageCache.swift b/ios/NSE/ImageCache.swift new file mode 100644 index 0000000000..7423826ef4 --- /dev/null +++ b/ios/NSE/ImageCache.swift @@ -0,0 +1,32 @@ +// +// Copyright 2022 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. +// + +import Foundation +import Kingfisher + +extension ImageCache { + static var onlyInMemory: ImageCache { + let result = ImageCache.default + result.diskStorage.config.sizeLimit = 1 + return result + } + + static var onlyOnDisk: ImageCache { + let result = ImageCache.default + result.memoryStorage.config.totalCostLimit = 1 + return result + } +} diff --git a/ios/NSE/Info.plist b/ios/NSE/Info.plist index 367921692d..8a0da37143 100644 --- a/ios/NSE/Info.plist +++ b/ios/NSE/Info.plist @@ -13,7 +13,7 @@ NSExtensionPointIdentifier com.apple.usernotifications.service NSExtensionPrincipalClass - $(PRODUCT_MODULE_NAME).NotificationService + $(PRODUCT_MODULE_NAME).NotificationServiceExtension diff --git a/ios/NSE/InfoPlistReader.swift b/ios/NSE/InfoPlistReader.swift new file mode 100644 index 0000000000..7fb50e6202 --- /dev/null +++ b/ios/NSE/InfoPlistReader.swift @@ -0,0 +1,160 @@ +// +// Copyright 2022 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. +// + +import Foundation + +struct InfoPlistReader { + private enum Keys { + static let appGroupIdentifier = "appGroupIdentifier" + static let baseBundleIdentifier = "baseBundleIdentifier" + static let keychainAccessGroupIdentifier = "keychainAccessGroupIdentifier" + static let bundleShortVersion = "CFBundleShortVersionString" + static let bundleDisplayName = "CFBundleDisplayName" + static let mapLibreAPIKey = "mapLibreAPIKey" + static let utExportedTypeDeclarationsKey = "UTExportedTypeDeclarations" + static let utTypeIdentifierKey = "UTTypeIdentifier" + static let utDescriptionKey = "UTTypeDescription" + + static let otlpTracingURL = "otlpTracingURL" + static let otlpTracingUsername = "otlpTracingUsername" + static let otlpTracingPassword = "otlpTracingPassword" + + static let bundleURLTypes = "CFBundleURLTypes" + static let bundleURLName = "CFBundleURLName" + static let bundleURLSchemes = "CFBundleURLSchemes" + } + + private enum Values { + static let mentionPills = "Mention Pills" + } + + /// Info.plist reader on the bundle object that contains the current executable. + static let main = InfoPlistReader(bundle: .main) + + /// Info.plist reader on the bundle object that contains the main app executable. + static let app = InfoPlistReader(bundle: .app) + + private let bundle: Bundle + + /// Initializer + /// - Parameter bundle: bundle to read values from + init(bundle: Bundle) { + self.bundle = bundle + } + + /// App group identifier set in Info.plist of the target + var appGroupIdentifier: String { + infoPlistValue(forKey: Keys.appGroupIdentifier) + } + + /// Base bundle identifier set in Info.plist of the target + var baseBundleIdentifier: String { + infoPlistValue(forKey: Keys.baseBundleIdentifier) + } + + /// Keychain access group identifier set in Info.plist of the target + var keychainAccessGroupIdentifier: String { + infoPlistValue(forKey: Keys.keychainAccessGroupIdentifier) + } + + /// Bundle executable of the target + var bundleExecutable: String { + infoPlistValue(forKey: kCFBundleExecutableKey as String) + } + + /// Bundle identifier of the target + var bundleIdentifier: String { + infoPlistValue(forKey: kCFBundleIdentifierKey as String) + } + + /// Bundle short version string of the target + var bundleShortVersionString: String { + infoPlistValue(forKey: Keys.bundleShortVersion) + } + + /// Bundle version of the target + var bundleVersion: String { + infoPlistValue(forKey: kCFBundleVersionKey as String) + } + + /// Bundle display name of the target + var bundleDisplayName: String { + infoPlistValue(forKey: Keys.bundleDisplayName) + } + + // MARK: - MapLibre + + var mapLibreAPIKey: String { + infoPlistValue(forKey: Keys.mapLibreAPIKey) + } + + // MARK: - OTLP Tracing + + var otlpTracingURL: String { + infoPlistValue(forKey: Keys.otlpTracingURL) + } + + var otlpTracingUsername: String { + infoPlistValue(forKey: Keys.otlpTracingUsername) + } + + var otlpTracingPassword: String { + infoPlistValue(forKey: Keys.otlpTracingPassword) + } + + // MARK: - Custom App Scheme + + var appScheme: String { + customSchemeForName("Application") + } + + var elementCallScheme: String { + customSchemeForName("Element Call") + } + + // MARK: - Mention Pills + + /// Mention Pills UTType + var pillsUTType: String { + let exportedTypes: [[String: Any]] = infoPlistValue(forKey: Keys.utExportedTypeDeclarationsKey) + guard let mentionPills = exportedTypes.first(where: { $0[Keys.utDescriptionKey] as? String == Values.mentionPills }), + let utType = mentionPills[Keys.utTypeIdentifierKey] as? String else { + fatalError("Add properly \(Values.mentionPills) exported type into your target's Info.plst") + } + return utType + } + + // MARK: - Private + + private func infoPlistValue(forKey key: String) -> T { + guard let result = bundle.object(forInfoDictionaryKey: key) as? T else { + fatalError("Add \(key) into your target's Info.plst") + } + return result + } + + private func customSchemeForName(_ name: String) -> String { + let urlTypes: [[String: Any]] = infoPlistValue(forKey: Keys.bundleURLTypes) + + guard let urlType = urlTypes.first(where: { $0[Keys.bundleURLName] as? String == name }), + let urlSchemes = urlType[Keys.bundleURLSchemes] as? [String], + let scheme = urlSchemes.first else { + fatalError("Invalid custom application scheme configuration") + } + + return scheme + } +} diff --git a/ios/NSE/KeychainController.swift b/ios/NSE/KeychainController.swift new file mode 100644 index 0000000000..f1545b123d --- /dev/null +++ b/ios/NSE/KeychainController.swift @@ -0,0 +1,106 @@ +// +// Copyright 2022 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. +// + +import Foundation +import KeychainAccess +import MatrixRustSDK + +enum KeychainControllerService: String { + case sessions + case tests + + var identifier: String { + InfoPlistReader.main.baseBundleIdentifier + "." + rawValue + } +} + +class KeychainController: KeychainControllerProtocol { + private let keychain: Keychain + + init(service: KeychainControllerService, + accessGroup: String) { + keychain = Keychain(service: service.identifier, + accessGroup: accessGroup) + } + + func setRestorationToken(_ restorationToken: RestorationToken, forUsername username: String) { + do { + let tokenData = try JSONEncoder().encode(restorationToken) + try keychain.set(tokenData, key: username) + } catch { + MXLog.error("Failed storing user restore token with error: \(error)") + } + } + + func restorationTokenForUsername(_ username: String) -> RestorationToken? { + do { + guard let tokenData = try keychain.getData(username) else { + return nil + } + + return try JSONDecoder().decode(RestorationToken.self, from: tokenData) + } catch { + MXLog.error("Failed retrieving user restore token") + return nil + } + } + + func restorationTokens() -> [KeychainCredentials] { + keychain.allKeys().compactMap { username in + guard let restorationToken = restorationTokenForUsername(username) else { + return nil + } + + return KeychainCredentials(userID: username, restorationToken: restorationToken) + } + } + + func removeRestorationTokenForUsername(_ username: String) { + MXLog.warning("Removing restoration token for user: \(username).") + + do { + try keychain.remove(username) + } catch { + MXLog.error("Failed removing restore token with error: \(error)") + } + } + + func removeAllRestorationTokens() { + MXLog.warning("Removing all user restoration tokens.") + + do { + try keychain.removeAll() + } catch { + MXLog.error("Failed removing all tokens") + } + } + + // MARK: - ClientSessionDelegate + + func retrieveSessionFromKeychain(userId: String) throws -> Session { + MXLog.info("Retrieving an updated Session from the keychain.") + guard let session = restorationTokenForUsername(userId)?.session else { + throw ClientError.Generic(msg: "Failed to find RestorationToken in the Keychain.") + } + return session + } + + func saveSessionInKeychain(session: Session) { + MXLog.info("Saving session changes in the keychain.") + let restorationToken = RestorationToken(session: session) + setRestorationToken(restorationToken, forUsername: session.userId) + } +} diff --git a/ios/NSE/KeychainControllerProtocol.swift b/ios/NSE/KeychainControllerProtocol.swift new file mode 100644 index 0000000000..51570ce2a2 --- /dev/null +++ b/ios/NSE/KeychainControllerProtocol.swift @@ -0,0 +1,31 @@ +// +// Copyright 2022 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. +// + +import Foundation +import MatrixRustSDK + +struct KeychainCredentials { + let userID: String + let restorationToken: RestorationToken +} + +protocol KeychainControllerProtocol: ClientSessionDelegate { + func setRestorationToken(_ restorationToken: RestorationToken, forUsername: String) + func restorationTokenForUsername(_ username: String) -> RestorationToken? + func restorationTokens() -> [KeychainCredentials] + func removeRestorationTokenForUsername(_ username: String) + func removeAllRestorationTokens() +} diff --git a/ios/NSE/LayoutDirection.swift b/ios/NSE/LayoutDirection.swift new file mode 100644 index 0000000000..53a4a33b87 --- /dev/null +++ b/ios/NSE/LayoutDirection.swift @@ -0,0 +1,30 @@ +// +// 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. +// + +import SwiftUI + +extension LayoutDirection { + var isolateLayoutUnicodeString: String { + switch self { + case .leftToRight: + return "\u{2066}" + case .rightToLeft: + return "\u{2067}" + default: + return "" + } + } +} diff --git a/ios/NSE/Logging/MXLog.swift b/ios/NSE/Logging/MXLog.swift new file mode 100644 index 0000000000..6b0e236d52 --- /dev/null +++ b/ios/NSE/Logging/MXLog.swift @@ -0,0 +1,197 @@ +// +// Copyright 2021 The Matrix.org Foundation C.I.C +// +// 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. +// + +import Foundation +import MatrixRustSDK + +/** + Logging utility that provies multiple logging levels as well as file output and rolling. + Its purpose is to provide a common entry for customizing logging and should be used throughout the code. + */ +enum MXLog { + private enum Constants { + static let target = "elementx" + + // Avoid redirecting NSLogs to files if we are attached to a debugger. + static let redirectToFiles = isatty(STDERR_FILENO) == 0 + + /// the maximum number of log files to use before rolling. `10` by default. + static let maxLogFileCount: UInt = 10 + + /// the maximum total space to use for log files in bytes. `100MB` by default. + static let logFilesSizeLimit: UInt = 100 * 1024 * 1024 // 100MB + } + + // Rust side crashes if invoking setupTracing multiple times + private static var didConfigureOnce = false + + private static var rootSpan: Span! + private static var target: String! + + static func configure(target: String? = nil, + logLevel: TracingConfiguration.LogLevel, + otlpConfiguration: OTLPConfiguration? = nil, + redirectToFiles: Bool = Constants.redirectToFiles, + maxLogFileCount: UInt = Constants.maxLogFileCount, + logFileSizeLimit: UInt = Constants.logFilesSizeLimit) { + guard didConfigureOnce == false else { + if let target { + MXLogger.setSubLogName(target) + } + + // SubLogName needs to be set before calling configure in order to be applied + MXLogger.configure(redirectToFiles: redirectToFiles, + maxLogFileCount: maxLogFileCount, + logFileSizeLimit: logFileSizeLimit) + return + } + + setupTracing(configuration: .init(logLevel: logLevel), otlpConfiguration: otlpConfiguration) + + if let target { + self.target = target + MXLogger.setSubLogName(target) + } else { + self.target = Constants.target + } + + rootSpan = Span(file: #file, line: #line, level: .info, target: self.target, name: "root") + + rootSpan.enter() + + MXLogger.configure(redirectToFiles: redirectToFiles, + maxLogFileCount: maxLogFileCount, + logFileSizeLimit: logFileSizeLimit) + + didConfigureOnce = true + } + + static func createSpan(_ name: String, + file: String = #file, + function: String = #function, + line: Int = #line, + column: Int = #column) -> Span { + createSpan(name, level: .info, file: file, function: function, line: line, column: column) + } + + static func verbose(_ message: Any, + file: String = #file, + function: String = #function, + line: Int = #line, + column: Int = #column, + context: Any? = nil) { + log(message, level: .trace, file: file, function: function, line: line, column: column, context: context) + } + + static func debug(_ message: Any, + file: String = #file, + function: String = #function, + line: Int = #line, + column: Int = #column, + context: Any? = nil) { + log(message, level: .debug, file: file, function: function, line: line, column: column, context: context) + } + + static func info(_ message: Any, + file: String = #file, + function: String = #function, + line: Int = #line, + column: Int = #column, + context: Any? = nil) { + log(message, level: .info, file: file, function: function, line: line, column: column, context: context) + } + + static func warning(_ message: Any, + file: String = #file, + function: String = #function, + line: Int = #line, + column: Int = #column, + context: Any? = nil) { + log(message, level: .warn, file: file, function: function, line: line, column: column, context: context) + } + + /// Log error with additional details + /// + /// - Parameters: + /// - message: Description of the error without any variables (this is to improve error aggregations by type) + /// - context: Additional context-dependent details about the issue + static func error(_ message: Any, + file: String = #file, + function: String = #function, + line: Int = #line, + column: Int = #column, + context: Any? = nil) { + log(message, level: .error, file: file, function: function, line: line, column: column, context: context) + } + + /// Log failure with additional details + /// + /// A failure is any type of programming error which should never occur in production. In `DEBUG` configuration + /// any failure will raise `assertionFailure` + /// + /// - Parameters: + /// - message: Description of the error without any variables (this is to improve error aggregations by type) + /// - context: Additional context-dependent details about the issue + static func failure(_ message: Any, + file: String = #file, + function: String = #function, + line: Int = #line, + column: Int = #column, + context: Any? = nil) { + log(message, level: .error, file: file, function: function, line: line, column: column, context: context) + + #if DEBUG + assertionFailure("\(message)") + #endif + } + + // MARK: - Private + + private static func createSpan(_ name: String, + level: LogLevel, + file: String = #file, + function: String = #function, + line: Int = #line, + column: Int = #column) -> Span { + guard didConfigureOnce else { + fatalError() + } + + if Span.current().isNone() { + rootSpan.enter() + } + + return Span(file: file, line: UInt32(line), level: level, target: target, name: name) + } + + private static func log(_ message: Any, + level: LogLevel, + file: String = #file, + function: String = #function, + line: Int = #line, + column: Int = #column, + context: Any? = nil) { + guard didConfigureOnce else { + return + } + + if Span.current().isNone() { + rootSpan.enter() + } + + logEvent(file: (file as NSString).lastPathComponent, line: UInt32(line), level: level, target: target, message: "\(message)") + } +} diff --git a/ios/NSE/Logging/MXLogger.swift b/ios/NSE/Logging/MXLogger.swift new file mode 100644 index 0000000000..eccbf53a1a --- /dev/null +++ b/ios/NSE/Logging/MXLogger.swift @@ -0,0 +1,335 @@ +// +// Copyright 2022 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. +// + +import UIKit + +/// The `MXLogger` tool redirects NSLog output into a fixed pool of files. +/// Another log file is used every time `MXLogger redirectNSLog(toFiles: true)` +/// is called. The pool contains 3 files. +/// +/// `MXLogger` can track and log uncaught exceptions or crashes. +class MXLogger { + /// stderr so it can be restored. + static var stderrSave: Int32 = 0 + + private enum Constants { + /// The filename used for the crash log. + static let crashLogFileName = "crash.log" + } + + /// Redirect NSLog output to MXLogger files. + /// + /// It is advised to condition this redirection in `#if (!isatty(STDERR_FILENO))` block to enable + /// it only when the device is not attached to the debugger. + /// + /// - Parameters: + /// - redirectToFiles: `true` to enable the redirection. + /// - maxLogFileCount: number of files to keep (default is 10). + /// - logFileSizeLimit: size limit of log files in bytes. 0 means no limitation, the default value for other methods + static func configure(redirectToFiles: Bool, + maxLogFileCount: UInt, + logFileSizeLimit: UInt) { + if redirectToFiles { + var tempLog = "" + + // Do a circular buffer based on X files + for index in (0...(maxLogFileCount - 2)).reversed() { + rotateLog(at: index, tempLog: &tempLog) + } + + // Save stderr so it can be restored. + stderrSave = dup(STDERR_FILENO) + + let nsLogURL = logURL(for: "console\(subLogName).log") + freopen((nsLogURL as NSURL).fileSystemRepresentation, "w+", stderr) + + MXLog.info("redirectNSLogToFiles: true") + if !tempLog.isEmpty { + // We can now log into files + MXLog.info(tempLog) + } + + removeExtraFiles(from: maxLogFileCount) + + if logFileSizeLimit > 0 { + removeFiles(after: logFileSizeLimit) + } + } else if stderrSave > 0 { + // Flush before restoring stderr + fflush(stderr) + + // Now restore stderr, so new output goes to console. + dup2(stderrSave, STDERR_FILENO) + close(stderrSave) + } + } + + private static func rotateLog(at index: UInt, tempLog: inout String) { + let fileManager = FileManager.default + + let currentURL: URL + let newURL: URL + + if index == 0 { + currentURL = logURL(for: String("console\(subLogName).log")) + newURL = logURL(for: String("console\(subLogName).1.log")) + } else { + currentURL = logURL(for: String("console\(subLogName).\(index).log")) + newURL = logURL(for: String("console\(subLogName).\(index + 1).log")) + } + + guard fileManager.fileExists(atPath: currentURL.path()) else { return } + + if fileManager.fileExists(atPath: newURL.path()) { + // Temp log + tempLog.append("removeItemAt: \(newURL)\n") + + do { + try fileManager.removeItem(at: newURL) + } catch { + tempLog.append("removeItemAt: \(newURL). Error: \(error)\n") + } + } + + // Temp log + tempLog.append("moveItemAt: \(currentURL) to: \(newURL)\n") + + do { + try fileManager.moveItem(at: currentURL, to: newURL) + } catch { + tempLog.append("moveItemAt: \(currentURL) to: \(newURL). Error: \(error)\n") + } + } + + private static func logURL(for fileName: String) -> URL { + MXLogger.logsFolderURL.appending(path: fileName) + } + + /// Delete all log files. + static func deleteLogFiles() { + let fileManager = FileManager.default + for logFileURL in logFiles { + try? fileManager.removeItem(at: logFileURL) + } + } + + /// The list of all log file URLs, sorted chronologically. + static var logFiles: [URL] { + var logFiles = [(url: URL, modificationDate: Date)]() + + let fileManager = FileManager.default + let enumerator = fileManager.enumerator(at: logsFolderURL, includingPropertiesForKeys: [.contentModificationDateKey]) + + // Find all *.log files and their modification dates. + while let logURL = enumerator?.nextObject() as? URL { + guard let resourceValues = try? logURL.resourceValues(forKeys: [.contentModificationDateKey]), + let modificationDate = resourceValues.contentModificationDate + else { continue } + + if logURL.pathExtension == "log" { + logFiles.append((logURL, modificationDate)) + } + } + + let sortedFiles = logFiles.sorted { $0.modificationDate > $1.modificationDate }.map(\.url) + + MXLog.info("logFiles: \(sortedFiles.map(\.lastPathComponent))") + + return sortedFiles + } + + // MARK: - Exceptions and crashes + + /// Exceptions uncaught by try catch block are handled here + static func handleUncaughtException(_ exception: NSException) { + MXLogger.logCrashes(false) + + // Extract running app information + let app = InfoPlistReader.main.bundleExecutable + let appId = InfoPlistReader.main.bundleIdentifier + let appVersion = "\(InfoPlistReader.main.bundleShortVersionString) (r\(InfoPlistReader.main.bundleVersion))" + + // Build the crash log + let model = UIDevice.current.model + let version = UIDevice.current.systemVersion + + let backtrace = exception.callStackSymbols + let description = String(format: "%.0f - %@\n%@\nApplication: %@ (%@)\nApplication version: %@\nBuild: %@\n%@ %@\n\nMain thread: %@\n%@\n", + Date.now.timeIntervalSince1970, + NSDate(), + exception.description, + app, appId, + appVersion, + buildVersion ?? "Unknown", + model, version, + Thread.isMainThread ? "true" : "false", + backtrace) + + // Write to the crash log file + MXLogger.deleteCrashLog() + let crashLog = crashLogURL + try? description.write(to: crashLog, atomically: false, encoding: .utf8) + + MXLog.error("handleUncaughtException", context: ["description": description]) + } + + // Signals emitted by the app are handled here + private static func handleSignal(_ signalValue: Int32) { + // Throw a custom Objective-C exception + // The Objective-C runtime will then be able to build a readable call stack in handleUncaughtException + withVaList([signalValue]) { NSException.raise(.init("Signal detected"), format: "Signal detected: %d", arguments: $0) } + } + + /// Make `MXLogger` catch and log unmanaged exceptions or application crashes. + /// + /// When such error happens, `MXLogger` stores the application stack trace into a file + /// just before the application leaves. The path of this file is provided by `MXLogger.crashLog`. + /// + /// - Parameter enabled: `true` to enable the catch. + static func logCrashes(_ enabled: Bool) { + if enabled { + // Handle not managed exceptions by ourselves + NSSetUncaughtExceptionHandler { exception in + MXLogger.handleUncaughtException(exception) + } + + // Register signal event (seg fault & cie) + signal(SIGABRT) { MXLogger.handleSignal($0) } + signal(SIGILL) { MXLogger.handleSignal($0) } + signal(SIGSEGV) { MXLogger.handleSignal($0) } + signal(SIGFPE) { MXLogger.handleSignal($0) } + signal(SIGBUS) { MXLogger.handleSignal($0) } + signal(SIGABRT) { MXLogger.handleSignal($0) } + } else { + // Disable crash handling + NSSetUncaughtExceptionHandler(nil) + signal(SIGABRT, SIG_DFL) + signal(SIGILL, SIG_DFL) + signal(SIGSEGV, SIG_DFL) + signal(SIGFPE, SIG_DFL) + signal(SIGBUS, SIG_DFL) + } + } + + /// Set the app build version. + /// It will be reported in crash report. + static var buildVersion: String? + + /// Set a sub name for namespacing log files. + /// + /// A sub name must be set when running from an app extension because extensions can + /// run in parallel to the app. + /// It must be called before `redirectNSLog(toFiles)`. + /// + /// - Parameter name: the subname for log files. Files will be named as `console-[subLogName].log` + /// Default is nil. + static func setSubLogName(_ name: String) { + if name.isEmpty { + subLogName = "" + } else { + subLogName = "-\(name)" + } + } + + private static var subLogName = "" + + /// The URL used for a crash log file. + static var crashLogURL: URL { + MXLogger.logsFolderURL.appending(path: Constants.crashLogFileName) + } + + /// The URL of the file containing the last application crash if one exists or `nil` if there is none. + /// + /// Only one crash log is stored at a time. The best moment for the app to handle it is the + /// at its next startup. + static var crashLog: URL? { + let crashLogURL = MXLogger.crashLogURL + let fileManager = FileManager.default + guard fileManager.fileExists(atPath: crashLogURL.path()) else { return nil } + + return crashLogURL + } + + /// Delete the crash log file. + static func deleteCrashLog() { + let crashLog = MXLogger.crashLogURL + let fileManager = FileManager.default + + if fileManager.fileExists(atPath: crashLog.path()) { + try? fileManager.removeItem(at: crashLog) + } + } + + // MARK: - Private + + /// The folder where logs are stored + private static var logsFolderURL: URL { + .appGroupContainerDirectory + } + + /// If `self.redirectNSLog(toFiles:numberOfFiles:)` is called with a lower numberOfFiles we need to do some cleanup. + private static func removeExtraFiles(from count: UInt) { + let fileManager = FileManager.default + + for index in count... { + let fileName = "console\(subLogName).\(index).log" + let logFile = logURL(for: fileName) + + if fileManager.fileExists(atPath: logFile.path()) { + try? fileManager.removeItem(at: logFile) + MXLog.info("removeExtraFilesFromCount: \(count). removeItemAt: \(logFile)\n") + } else { + break + } + } + } + + /// If `redirectNSLog(toFiles:sizeLimit:)` is called with a size limit, we may need to do some cleanup. + private static func removeFiles(after sizeLimit: UInt) { + var logSize: UInt = 0 + var indexExceedingSizeLimit: Int? + let fileManager = FileManager.default + + // Start from console.1.log. Do not consider console.log. It should be almost empty + for index in 1... { + let fileName = "console\(subLogName).\(index).log" + let logFile = logURL(for: fileName) + + if fileManager.fileExists(atPath: logFile.path()) { + if let attributes = try? fileManager.attributesOfItem(atPath: logFile.path()), let fileSize = attributes[.size] as? UInt { + logSize += fileSize + } + + if logSize >= sizeLimit { + indexExceedingSizeLimit = index + break + } + } else { + break + } + } + + let logSizeString = logSize.formatted(.byteCount(style: .binary)) + let sizeLimitString = sizeLimit.formatted(.byteCount(style: .binary)) + + if let indexExceedingSizeLimit { + MXLog.info("removeFilesAfterSizeLimit: Remove files from index \(indexExceedingSizeLimit) because logs are too large (\(logSizeString) for a limit of \(sizeLimitString)\n") + removeExtraFiles(from: UInt(indexExceedingSizeLimit)) + } else { + MXLog.info("removeFilesAfterSizeLimit: No need: \(logSizeString) for a limit of \(sizeLimitString)\n") + } + } +} diff --git a/ios/NSE/Logging/RustTracing.swift b/ios/NSE/Logging/RustTracing.swift new file mode 100644 index 0000000000..7774ffc08f --- /dev/null +++ b/ios/NSE/Logging/RustTracing.swift @@ -0,0 +1,150 @@ +// +// Copyright 2022 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. +// + +import Collections +import MatrixRustSDK + +struct OTLPConfiguration { + let url: String + let username: String + let password: String +} + +// This exposes the full Rust side tracing subscriber filter for more flexibility. +// We can filter by level, crate and even file. See more details here: +// https://docs.rs/tracing-subscriber/0.2.7/tracing_subscriber/filter/struct.EnvFilter.html#examples +struct TracingConfiguration { + enum LogLevel: Codable, Hashable { + case error, warn, info, debug, trace + case custom(String) + + var title: String { + switch self { + case .error: + return "Error" + case .warn: + return "Warning" + case .info: + return "Info" + case .debug: + return "Debug" + case .trace: + return "Trace" + case .custom: + return "Custom" + } + } + + fileprivate var rawValue: String { + switch self { + case .error: + return "error" + case .warn: + return "warn" + case .info: + return "info" + case .debug: + return "debug" + case .trace: + return "trace" + case .custom(let filter): + return filter + } + } + } + + enum Target: String { + case common = "" + + case elementx + + case hyper, matrix_sdk_ffi, matrix_sdk_crypto + + case matrix_sdk_client = "matrix_sdk::client" + case matrix_sdk_oidc = "matrix_sdk::oidc" + case matrix_sdk_http_client = "matrix_sdk::http_client" + case matrix_sdk_sliding_sync = "matrix_sdk::sliding_sync" + case matrix_sdk_base_sliding_sync = "matrix_sdk_base::sliding_sync" + case matrix_sdk_ui_timeline = "matrix_sdk_ui::timeline" + } + + static let targets: OrderedDictionary = [ + .common: .info, + .elementx: .info, + .hyper: .warn, + .matrix_sdk_ffi: .info, + .matrix_sdk_client: .trace, + .matrix_sdk_crypto: .info, + .matrix_sdk_oidc: .trace, + .matrix_sdk_http_client: .info, + .matrix_sdk_sliding_sync: .info, + .matrix_sdk_base_sliding_sync: .info, + .matrix_sdk_ui_timeline: .info + ] + + let filter: String + + /// Sets the same log level for all Targets + /// - Parameter logLevel: the desired log level + /// - Returns: a custom tracing configuration + init(logLevel: LogLevel) { + if case let .custom(filter) = logLevel { + self.filter = filter + return + } + + let overrides = Self.targets.keys.reduce(into: [Target: LogLevel]()) { partialResult, target in + // Keep the defaults here + let ignoredTargets: [Target] = [.common, .matrix_sdk_ffi, .hyper, .matrix_sdk_client, .matrix_sdk_oidc] + if ignoredTargets.contains(target) { + return + } + + partialResult[target] = logLevel + } + + var newTargets = Self.targets + for (target, logLevel) in overrides { + newTargets.updateValue(logLevel, forKey: target) + } + + let components = newTargets.map { (target: Target, logLevel: LogLevel) in + guard !target.rawValue.isEmpty else { + return logLevel.rawValue + } + + return "\(target.rawValue)=\(logLevel.rawValue)" + } + + filter = components.joined(separator: ",") + } +} + +func setupTracing(configuration: TracingConfiguration, otlpConfiguration: OTLPConfiguration?) { + if let otlpConfiguration { + setupOtlpTracing(config: .init(clientName: "ElementX-iOS", + user: otlpConfiguration.username, + password: otlpConfiguration.password, + otlpEndpoint: otlpConfiguration.url, + filter: configuration.filter, + writeToStdoutOrSystem: true, + writeToFiles: nil)) + } else { + setupTracing(config: .init(filter: configuration.filter, + writeToStdoutOrSystem: true, + writeToFiles: nil)) + } +} diff --git a/ios/NSE/MatrixEntityRegex.swift b/ios/NSE/MatrixEntityRegex.swift new file mode 100644 index 0000000000..2f9e28f900 --- /dev/null +++ b/ios/NSE/MatrixEntityRegex.swift @@ -0,0 +1,90 @@ +// +// Copyright 2022 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. +// + +import Foundation + +// https://spec.matrix.org/latest/appendices/#identifier-grammar +enum MatrixEntityRegex: String { + case homeserver + case userId + case roomAlias + case roomId + case eventId + + var rawValue: String { + switch self { + case .homeserver: + return "[A-Z0-9]+((\\.|\\-)[A-Z0-9]+){0,}(:[0-9]{2,5})?" + case .userId: + return "@[\\x21-\\x39\\x3B-\\x7F]+:" + MatrixEntityRegex.homeserver.rawValue + case .roomAlias: + return "#[A-Z0-9._%#@=+-]+:" + MatrixEntityRegex.homeserver.rawValue + case .roomId: + return "![A-Z0-9]+:" + MatrixEntityRegex.homeserver.rawValue + case .eventId: + return "\\$[a-z0-9_\\-\\/]+(:[a-z0-9]+\\.[a-z0-9]+)?" + } + } + + // swiftlint:disable force_try + static var homeserverRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.homeserver.rawValue, options: .caseInsensitive) + static var userIdentifierRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.userId.rawValue, options: .caseInsensitive) + static var roomAliasRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.roomAlias.rawValue, options: .caseInsensitive) + static var roomIdentifierRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.roomId.rawValue, options: .caseInsensitive) + static var eventIdentifierRegex = try! NSRegularExpression(pattern: MatrixEntityRegex.eventId.rawValue, options: .caseInsensitive) + static var linkRegex = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + // swiftlint:enable force_try + + static func isMatrixHomeserver(_ homeserver: String) -> Bool { + guard let match = homeserverRegex.firstMatch(in: homeserver) else { + return false + } + + return match.range.length == homeserver.count + } + + static func isMatrixUserIdentifier(_ identifier: String) -> Bool { + guard let match = userIdentifierRegex.firstMatch(in: identifier) else { + return false + } + + return match.range.length == identifier.count + } + + static func isMatrixRoomAlias(_ alias: String) -> Bool { + guard let match = roomAliasRegex.firstMatch(in: alias) else { + return false + } + + return match.range.length == alias.count + } + + static func isMatrixRoomIdentifier(_ identifier: String) -> Bool { + guard let match = roomIdentifierRegex.firstMatch(in: identifier) else { + return false + } + + return match.range.length == identifier.count + } + + static func isMatrixEventIdentifier(_ identifier: String) -> Bool { + guard let match = eventIdentifierRegex.firstMatch(in: identifier) else { + return false + } + + return match.range.length == identifier.count + } +} diff --git a/ios/NSE/NSRegularExpresion.swift b/ios/NSE/NSRegularExpresion.swift new file mode 100644 index 0000000000..8d7fd38f60 --- /dev/null +++ b/ios/NSE/NSRegularExpresion.swift @@ -0,0 +1,40 @@ +// +// 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. +// + +import Foundation + +/// NSRegularExpressions work internally on NSStrings, we need to be careful how we build the ranges for extended grapheme clusters https://stackoverflow.com/a/27880748/730924 +extension NSRegularExpression { + func enumerateMatches(in string: String, options: NSRegularExpression.MatchingOptions = [], using block: (NSTextCheckingResult?, NSRegularExpression.MatchingFlags, UnsafeMutablePointer) -> Void) { + enumerateMatches(in: string, options: options, range: .init(location: 0, length: (string as NSString).length), using: block) + } + + func matches(in string: String, options: NSRegularExpression.MatchingOptions = []) -> [NSTextCheckingResult] { + matches(in: string, options: options, range: .init(location: 0, length: (string as NSString).length)) + } + + func numberOfMatches(in string: String, options: NSRegularExpression.MatchingOptions = []) -> Int { + numberOfMatches(in: string, options: options, range: .init(location: 0, length: (string as NSString).length)) + } + + func firstMatch(in string: String, options: NSRegularExpression.MatchingOptions = []) -> NSTextCheckingResult? { + firstMatch(in: string, options: options, range: .init(location: 0, length: (string as NSString).length)) + } + + func rangeOfFirstMatch(in string: String, options: NSRegularExpression.MatchingOptions = []) -> NSRange { + rangeOfFirstMatch(in: string, options: options, range: .init(location: 0, length: (string as NSString).length)) + } +} diff --git a/ios/NSE/NotificationConstants.swift b/ios/NSE/NotificationConstants.swift new file mode 100644 index 0000000000..0694b8f58c --- /dev/null +++ b/ios/NSE/NotificationConstants.swift @@ -0,0 +1,38 @@ +// +// Copyright 2022 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. +// + +import Foundation + +enum NotificationConstants { + enum UserInfoKey { + static let roomIdentifier = "room_id" + static let eventIdentifier = "event_id" + static let unreadCount = "unread_count" + static let pusherNotificationClientIdentifier = "pusher_notification_client_identifier" + static let receiverIdentifier = "receiver_id" + static let notificationIdentifier = "notification_identifier" + } + + enum Category { + static let discard = "discard" + static let message = "message" + static let invite = "invite" + } + + enum Action { + static let inlineReply = "inline-reply" + } +} diff --git a/ios/NSE/PermalinkBuilder.swift b/ios/NSE/PermalinkBuilder.swift new file mode 100644 index 0000000000..c24fae0606 --- /dev/null +++ b/ios/NSE/PermalinkBuilder.swift @@ -0,0 +1,148 @@ +// +// Copyright 2022 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. +// + +import Foundation + +enum PermalinkBuilderError: Error { + case invalidUserIdentifier + case invalidRoomIdentifier + case invalidRoomAlias + case invalidEventIdentifier + case failedConstructingURL + case failedAddingPercentEncoding +} + +enum PermalinkType: Equatable { + case userIdentifier(String) + case roomIdentifier(String) + case roomAlias(String) + case event(roomIdentifier: String, eventIdentifier: String) +} + +enum PermalinkBuilder { + private static var uriComponentCharacterSet: CharacterSet = { + var charset = CharacterSet.alphanumerics + charset.insert(charactersIn: "-_.!~*'()") + return charset + }() + + static func detectPermalink(in url: URL, baseURL: URL) -> PermalinkType? { + guard url.absoluteString.hasPrefix(baseURL.absoluteString) else { + return nil + } + + guard let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) else { + return nil + } + + guard var fragment = urlComponents.fragment else { + return nil + } + + if fragment.hasPrefix("/") { + fragment = String(fragment.dropFirst(1)) + } + + if let userIdentifierRange = MatrixEntityRegex.userIdentifierRegex.firstMatch(in: fragment)?.range { + return .userIdentifier((fragment as NSString).substring(with: userIdentifierRange)) + } + + if let roomAliasRange = MatrixEntityRegex.roomAliasRegex.firstMatch(in: fragment)?.range { + return .roomAlias((fragment as NSString).substring(with: roomAliasRange)) + } + + if let roomIdentifierRange = MatrixEntityRegex.roomIdentifierRegex.firstMatch(in: fragment)?.range { + let roomIdentifier = (fragment as NSString).substring(with: roomIdentifierRange) + + if let eventIdentifierRange = MatrixEntityRegex.eventIdentifierRegex.firstMatch(in: fragment)?.range { + let eventIdentifier = (fragment as NSString).substring(with: eventIdentifierRange) + return .event(roomIdentifier: roomIdentifier, eventIdentifier: eventIdentifier) + } + + return .roomIdentifier(roomIdentifier) + } + + return nil + } + + static func permalinkTo(userIdentifier: String, baseURL: URL) throws -> URL { + guard MatrixEntityRegex.isMatrixUserIdentifier(userIdentifier) else { + throw PermalinkBuilderError.invalidUserIdentifier + } + + let urlString = "\(baseURL)/#/\(userIdentifier)" + + guard let url = URL(string: urlString) else { + throw PermalinkBuilderError.failedConstructingURL + } + + return url + } + + static func permalinkTo(roomIdentifier: String, baseURL: URL) throws -> URL { + guard MatrixEntityRegex.isMatrixRoomIdentifier(roomIdentifier) else { + throw PermalinkBuilderError.invalidRoomIdentifier + } + + return try permalinkTo(roomIdentifierOrAlias: roomIdentifier, baseURL: baseURL) + } + + static func permalinkTo(roomAlias: String, baseURL: URL) throws -> URL { + guard MatrixEntityRegex.isMatrixRoomAlias(roomAlias) else { + throw PermalinkBuilderError.invalidRoomAlias + } + + return try permalinkTo(roomIdentifierOrAlias: roomAlias, baseURL: baseURL) + } + + static func permalinkTo(eventIdentifier: String, roomIdentifier: String, baseURL: URL) throws -> URL { + guard MatrixEntityRegex.isMatrixEventIdentifier(eventIdentifier) else { + throw PermalinkBuilderError.invalidEventIdentifier + } + guard MatrixEntityRegex.isMatrixRoomIdentifier(roomIdentifier) else { + throw PermalinkBuilderError.invalidRoomIdentifier + } + + guard let roomId = roomIdentifier.addingPercentEncoding(withAllowedCharacters: uriComponentCharacterSet), + let eventId = eventIdentifier.addingPercentEncoding(withAllowedCharacters: uriComponentCharacterSet) else { + throw PermalinkBuilderError.failedAddingPercentEncoding + } + + let urlString = "\(baseURL)/#/\(roomId)/\(eventId)" + + guard let url = URL(string: urlString) else { + throw PermalinkBuilderError.failedConstructingURL + } + + return url + } + + // MARK: - Private + + private static func permalinkTo(roomIdentifierOrAlias: String, baseURL: URL) throws -> URL { + guard let identifier = roomIdentifierOrAlias.addingPercentEncoding(withAllowedCharacters: uriComponentCharacterSet) else { + throw PermalinkBuilderError.failedAddingPercentEncoding + } + + let urlString = "\(baseURL)/#/\(identifier)" + + guard let url = URL(string: urlString) else { + throw PermalinkBuilderError.failedConstructingURL + } + + return url + } +} diff --git a/ios/NSE/PlaceholderAvatarImage.swift b/ios/NSE/PlaceholderAvatarImage.swift new file mode 100644 index 0000000000..8aed5699b5 --- /dev/null +++ b/ios/NSE/PlaceholderAvatarImage.swift @@ -0,0 +1,89 @@ +// +// Copyright 2022 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. +// + +import SwiftUI + +import Compound +import DesignKit + +struct PlaceholderAvatarImage: View { + @Environment(\.redactionReasons) private var redactionReasons + + private let textForImage: String + private let contentID: String? + + var body: some View { + GeometryReader { geometry in + ZStack(alignment: .center) { + backgroundColor + + // This text's frame doesn't look right when redacted + if redactionReasons != .placeholder { + Text(textForImage) + .foregroundColor(avatarColor?.foreground ?? .white) + .font(.system(size: geometry.size.width * 0.5625, weight: .semibold)) + .minimumScaleFactor(0.001) + .frame(alignment: .center) + } + } + } + .aspectRatio(1, contentMode: .fill) + } + + init(name: String?, contentID: String?) { + let baseName = name ?? contentID?.trimmingCharacters(in: .punctuationCharacters) + textForImage = baseName?.first?.uppercased() ?? "" + self.contentID = contentID + } + + private var backgroundColor: Color { + if redactionReasons.contains(.placeholder) { + return Color(.systemGray4) // matches the default text redaction + } + + return avatarColor?.background ?? .compound.iconPrimary + } + + private var avatarColor: AvatarColor? { + guard let contentID else { + return nil + } + + return Color.compound.avatarColor(for: contentID) + } +} + +struct PlaceholderAvatarImage_Previews: PreviewProvider, TestablePreview { + static var previews: some View { + VStack(spacing: 75) { + PlaceholderAvatarImage(name: "Xavier", contentID: "@userid1:matrix.org") + .clipShape(Circle()) + .frame(width: 150, height: 100) + + PlaceholderAvatarImage(name: "@*~AmazingName~*@", contentID: "@userid2:matrix.org") + .clipShape(Circle()) + .frame(width: 150, height: 100) + + PlaceholderAvatarImage(name: nil, contentID: "@userid3:matrix.org") + .clipShape(Circle()) + .frame(width: 150, height: 100) + + PlaceholderAvatarImage(name: nil, contentID: "@fooserid:matrix.org") + .clipShape(Circle()) + .frame(width: 30, height: 30) + } + } +} diff --git a/ios/NSE/Provider/ImageProviderProtocol.swift b/ios/NSE/Provider/ImageProviderProtocol.swift new file mode 100644 index 0000000000..578e830390 --- /dev/null +++ b/ios/NSE/Provider/ImageProviderProtocol.swift @@ -0,0 +1,35 @@ +// +// 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. +// + +import UIKit + +protocol ImageProviderProtocol { + func imageFromSource(_ source: MediaSourceProxy?, size: CGSize?) -> UIImage? + + func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result + + func loadImageDataFromSource(_ source: MediaSourceProxy) async -> Result +} + +extension ImageProviderProtocol { + func imageFromSource(_ source: MediaSourceProxy?) -> UIImage? { + imageFromSource(source, size: nil) + } + + func loadImageFromSource(_ source: MediaSourceProxy) async -> Result { + await loadImageFromSource(source, size: nil) + } +} diff --git a/ios/NSE/Provider/MediaFileHandleProxy.swift b/ios/NSE/Provider/MediaFileHandleProxy.swift new file mode 100644 index 0000000000..5926413709 --- /dev/null +++ b/ios/NSE/Provider/MediaFileHandleProxy.swift @@ -0,0 +1,68 @@ +// +// 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. +// + +import Foundation +import MatrixRustSDK + +/// A wrapper around Rust's `MediaFileHandle` type that provides us with a +/// media file that is stored unencrypted in a temporary location for previewing. +class MediaFileHandleProxy { + /// The underlying handle for the file. + private let handle: MediaFileHandleProtocol + + /// Creates a new instance from the Rust type. + init(handle: MediaFileHandleProtocol) { + self.handle = handle + } + + /// Creates an unmanaged instance (for mocking etc), using a raw `URL` + /// + /// A media file created from a URL won't have the automatic clean-up mechanism + /// that is provided by the SDK's `MediaFileHandle`. + static func unmanaged(url: URL) -> MediaFileHandleProxy { + MediaFileHandleProxy(handle: UnmanagedMediaFileHandle(url: url)) + } + + /// The media file's location on disk. + var url: URL { + URL(filePath: handle.path()) + } +} + +// MARK: - Hashable + +extension MediaFileHandleProxy: Hashable { + static func == (lhs: MediaFileHandleProxy, rhs: MediaFileHandleProxy) -> Bool { + lhs.url == rhs.url + } + + func hash(into hasher: inout Hasher) { + hasher.combine(url) + } +} + +// MARK: - + +/// An unmanaged file handle that can be created direct from a URL. +/// +/// This type allows for mocking but doesn't provide the automatic clean-up mechanism provided by the SDK. +private struct UnmanagedMediaFileHandle: MediaFileHandleProtocol { + let url: URL + + func path() -> String { + url.path() + } +} diff --git a/ios/NSE/Provider/MediaLoader.swift b/ios/NSE/Provider/MediaLoader.swift new file mode 100644 index 0000000000..3d98d0739e --- /dev/null +++ b/ios/NSE/Provider/MediaLoader.swift @@ -0,0 +1,88 @@ +// +// Copyright 2022 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. +// + +import Combine +import Foundation +import MatrixRustSDK +import UIKit + +private final class MediaRequest { + var continuations: [CheckedContinuation] = [] +} + +actor MediaLoader: MediaLoaderProtocol { + private let client: ClientProtocol + private let clientQueue: DispatchQueue + private var ongoingRequests = [MediaSourceProxy: MediaRequest]() + + init(client: ClientProtocol, + clientQueue: DispatchQueue = .global()) { + self.client = client + self.clientQueue = clientQueue + } + + func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data { + try await enqueueLoadMediaRequest(forSource: source) { + try self.client.getMediaContent(mediaSource: source.underlyingSource) + } + } + + func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data { + try await enqueueLoadMediaRequest(forSource: source) { + try self.client.getMediaThumbnail(mediaSource: source.underlyingSource, width: UInt64(width), height: UInt64(height)) + } + } + + 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) + } + + return MediaFileHandleProxy(handle: result) + } + + // MARK: - Private + + private func enqueueLoadMediaRequest(forSource source: MediaSourceProxy, operation: @escaping () throws -> [UInt8]) async throws -> Data { + if let ongoingRequest = ongoingRequests[source] { + return try await withCheckedThrowingContinuation { continuation in + ongoingRequest.continuations.append(continuation) + } + } + + let ongoingRequest = MediaRequest() + ongoingRequests[source] = ongoingRequest + + defer { + ongoingRequests[source] = nil + } + + do { + let result = try await Task.dispatch(on: clientQueue) { + let bytes = try operation() + return Data(bytes: bytes, count: bytes.count) + } + + ongoingRequest.continuations.forEach { $0.resume(returning: result) } + + return result + + } catch { + ongoingRequest.continuations.forEach { $0.resume(throwing: error) } + throw error + } + } +} diff --git a/ios/NSE/Provider/MediaLoaderProtocol.swift b/ios/NSE/Provider/MediaLoaderProtocol.swift new file mode 100644 index 0000000000..df57bac301 --- /dev/null +++ b/ios/NSE/Provider/MediaLoaderProtocol.swift @@ -0,0 +1,31 @@ +// +// Copyright 2022 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. +// + +import Foundation + +protocol MediaLoaderProtocol { + func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data + + func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data + + func loadMediaFileForSource(_ source: MediaSourceProxy, body: String?) async throws -> MediaFileHandleProxy +} + +extension MediaLoaderProtocol { + func loadMediaFileForSource(_ source: MediaSourceProxy) async throws -> MediaFileHandleProxy { + try await loadMediaFileForSource(source, body: nil) + } +} diff --git a/ios/NSE/Provider/MediaProvider.swift b/ios/NSE/Provider/MediaProvider.swift new file mode 100644 index 0000000000..d3e655ca16 --- /dev/null +++ b/ios/NSE/Provider/MediaProvider.swift @@ -0,0 +1,126 @@ +// +// Copyright 2022 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. +// + +import Kingfisher +import UIKit + +struct MediaProvider: MediaProviderProtocol { + private let mediaLoader: MediaLoaderProtocol + private let imageCache: Kingfisher.ImageCache + private let backgroundTaskService: BackgroundTaskServiceProtocol? + + init(mediaLoader: MediaLoaderProtocol, + imageCache: Kingfisher.ImageCache, + backgroundTaskService: BackgroundTaskServiceProtocol?) { + self.mediaLoader = mediaLoader + self.imageCache = imageCache + self.backgroundTaskService = backgroundTaskService + } + + // MARK: Images + + func imageFromSource(_ source: MediaSourceProxy?, size: CGSize?) -> UIImage? { + guard let url = source?.url else { + return nil + } + let cacheKey = cacheKeyForURL(url, size: size) + return imageCache.retrieveImageInMemoryCache(forKey: cacheKey, options: nil) + } + + func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result { + if let image = imageFromSource(source, size: size) { + return .success(image) + } + + let loadImageBgTask = await backgroundTaskService?.startBackgroundTask(withName: "LoadImage: \(source.url.hashValue)") + defer { + loadImageBgTask?.stop() + } + + let cacheKey = cacheKeyForURL(source.url, size: size) + + if case let .success(cacheResult) = await imageCache.retrieveImage(forKey: cacheKey), + let image = cacheResult.image { + return .success(image) + } + + do { + let imageData: Data + if let size { + imageData = try await mediaLoader.loadMediaThumbnailForSource(source, width: UInt(size.width), height: UInt(size.height)) + } else { + imageData = try await mediaLoader.loadMediaContentForSource(source) + } + + guard let image = UIImage(data: imageData) else { + MXLog.error("Invalid image data") + return .failure(.invalidImageData) + } + + imageCache.store(image, forKey: cacheKey) + + return .success(image) + } catch { + MXLog.error("Failed retrieving image with error: \(error)") + return .failure(.failedRetrievingImage) + } + } + + func loadImageDataFromSource(_ source: MediaSourceProxy) async -> Result { + do { + let imageData = try await mediaLoader.loadMediaContentForSource(source) + return .success(imageData) + } catch { + MXLog.error("Failed retrieving image with error: \(error)") + return .failure(.failedRetrievingImage) + } + } + + // MARK: Files + + func loadFileFromSource(_ source: MediaSourceProxy, body: String?) async -> Result { + let loadFileBgTask = await backgroundTaskService?.startBackgroundTask(withName: "LoadFile: \(source.url.hashValue)") + defer { loadFileBgTask?.stop() } + + do { + let file = try await mediaLoader.loadMediaFileForSource(source, body: body) + return .success(file) + } catch { + MXLog.error("Failed retrieving file with error: \(error)") + return .failure(.failedRetrievingFile) + } + } + + // MARK: - Private + + private func cacheKeyForURL(_ url: URL, size: CGSize?) -> String { + if let size { + return "\(url.absoluteString){\(size.width),\(size.height)}" + } else { + return url.absoluteString + } + } +} + +private extension ImageCache { + func retrieveImage(forKey key: String) async -> Result { + await withCheckedContinuation { continuation in + retrieveImage(forKey: key) { result in + continuation.resume(returning: result) + } + } + } +} diff --git a/ios/NSE/Provider/MediaProviderProtocol.swift b/ios/NSE/Provider/MediaProviderProtocol.swift new file mode 100644 index 0000000000..a2762a91e1 --- /dev/null +++ b/ios/NSE/Provider/MediaProviderProtocol.swift @@ -0,0 +1,34 @@ +// +// Copyright 2022 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. +// + +import Foundation +import UIKit + +enum MediaProviderError: Error { + case failedRetrievingImage + case failedRetrievingFile + case invalidImageData +} + +protocol MediaProviderProtocol: ImageProviderProtocol { + func loadFileFromSource(_ source: MediaSourceProxy, body: String?) async -> Result +} + +extension MediaProviderProtocol { + func loadFileFromSource(_ source: MediaSourceProxy) async -> Result { + await loadFileFromSource(source, body: nil) + } +} diff --git a/ios/NSE/Provider/MediaSourceProxy.swift b/ios/NSE/Provider/MediaSourceProxy.swift new file mode 100644 index 0000000000..17cc69399c --- /dev/null +++ b/ios/NSE/Provider/MediaSourceProxy.swift @@ -0,0 +1,50 @@ +// +// Copyright 2022 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. +// + +import Foundation +import MatrixRustSDK + +struct MediaSourceProxy: Hashable { + /// The media source provided by Rust. + let underlyingSource: MediaSource + /// The media's mime type, used when loading the media's file. + /// This is optional when loading images and thumbnails in memory. + let mimeType: String? + + let url: URL! + + init(source: MediaSource, mimeType: String?) { + underlyingSource = source + url = URL(string: underlyingSource.url()) + self.mimeType = mimeType + } + + init(url: URL, mimeType: String?) { + underlyingSource = mediaSourceFromUrl(url: url.absoluteString) + self.url = URL(string: underlyingSource.url()) + self.mimeType = mimeType + } + + // MARK: - Hashable + + public static func == (lhs: MediaSourceProxy, rhs: MediaSourceProxy) -> Bool { + lhs.url == rhs.url + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(url) + } +} diff --git a/ios/NSE/Provider/MockMediaProvider.swift b/ios/NSE/Provider/MockMediaProvider.swift new file mode 100644 index 0000000000..fb5778e837 --- /dev/null +++ b/ios/NSE/Provider/MockMediaProvider.swift @@ -0,0 +1,53 @@ +// +// Copyright 2022 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. +// + +import Foundation +import UIKit + +struct MockMediaProvider: MediaProviderProtocol { + func imageFromSource(_ source: MediaSourceProxy?, size: CGSize?) -> UIImage? { + guard source != nil else { + return nil + } + + if source?.url == .picturesDirectory { + return Asset.Images.appLogo.image + } + + return UIImage(systemName: "photo") + } + + func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result { + guard let image = UIImage(systemName: "photo") else { + fatalError() + } + + return .success(image) + } + + func loadImageDataFromSource(_ source: MediaSourceProxy) async -> Result { + guard let image = UIImage(systemName: "photo"), + let data = image.pngData() else { + fatalError() + } + + return .success(data) + } + + func loadFileFromSource(_ source: MediaSourceProxy, body: String?) async -> Result { + .failure(.failedRetrievingFile) + } +} diff --git a/ios/NSE/Proxy/NotificationItemProxy.swift b/ios/NSE/Proxy/NotificationItemProxy.swift new file mode 100644 index 0000000000..cbda78c959 --- /dev/null +++ b/ios/NSE/Proxy/NotificationItemProxy.swift @@ -0,0 +1,117 @@ +// +// Copyright 2022 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. +// + +import Foundation +import MatrixRustSDK +import UserNotifications + +struct NotificationItemProxy: NotificationItemProxyProtocol { + let notificationItem: NotificationItem + let eventID: String + let receiverID: String + let roomID: String + + var event: NotificationEvent? { + notificationItem.event + } + + var senderDisplayName: String? { + notificationItem.senderInfo.displayName + } + + var senderID: String { + switch notificationItem.event { + case .timeline(let event): + return event.senderId() + case .invite(let senderID): + return senderID + } + } + + var roomDisplayName: String { + notificationItem.roomInfo.displayName + } + + var roomCanonicalAlias: String? { + notificationItem.roomInfo.canonicalAlias + } + + var isRoomDirect: Bool { + notificationItem.roomInfo.isDirect + } + + var roomJoinedMembers: Int { + Int(notificationItem.roomInfo.joinedMembersCount) + } + + var isNoisy: Bool { + notificationItem.isNoisy ?? false + } + + var senderAvatarMediaSource: MediaSourceProxy? { + if let senderAvatarURLString = notificationItem.senderInfo.avatarUrl, + let senderAvatarURL = URL(string: senderAvatarURLString) { + return MediaSourceProxy(url: senderAvatarURL, mimeType: nil) + } + return nil + } + + var roomAvatarMediaSource: MediaSourceProxy? { + if let roomAvatarURLString = notificationItem.roomInfo.avatarUrl, + let roomAvatarURL = URL(string: roomAvatarURLString) { + return MediaSourceProxy(url: roomAvatarURL, mimeType: nil) + } + return nil + } +} + +struct EmptyNotificationItemProxy: NotificationItemProxyProtocol { + let eventID: String + + var event: NotificationEvent? { + nil + } + + let roomID: String + + let receiverID: String + + var senderID: String { "" } + + var senderDisplayName: String? { nil } + + var senderAvatarURL: String? { nil } + + var roomDisplayName: String { "" } + + var roomCanonicalAlias: String? { nil } + + var roomAvatarURL: String? { nil } + + var isNoisy: Bool { false } + + var isRoomDirect: Bool { false } + + var isRoomEncrypted: Bool? { nil } + + var senderAvatarMediaSource: MediaSourceProxy? { nil } + + var roomAvatarMediaSource: MediaSourceProxy? { nil } + + var notificationIdentifier: String { "" } + + var roomJoinedMembers: Int { 0 } +} diff --git a/ios/NSE/Proxy/NotificationItemProxyProtocol.swift b/ios/NSE/Proxy/NotificationItemProxyProtocol.swift new file mode 100644 index 0000000000..9f1ca8d419 --- /dev/null +++ b/ios/NSE/Proxy/NotificationItemProxyProtocol.swift @@ -0,0 +1,81 @@ +// +// Copyright 2022 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. +// + +import Foundation +import MatrixRustSDK +import UserNotifications + +protocol NotificationItemProxyProtocol { + var event: NotificationEvent? { get } + + var eventID: String { get } + + var senderID: String { get } + + var roomID: String { get } + + var receiverID: String { get } + + var senderDisplayName: String? { get } + + var senderAvatarMediaSource: MediaSourceProxy? { get } + + var roomDisplayName: String { get } + + var roomCanonicalAlias: String? { get } + + var roomAvatarMediaSource: MediaSourceProxy? { get } + + var roomJoinedMembers: Int { get } + + var isRoomDirect: Bool { get } + + var isNoisy: Bool { get } +} + +extension NotificationItemProxyProtocol { + var isDM: Bool { + isRoomDirect && roomJoinedMembers <= 2 + } + + var hasMedia: Bool { + if (isDM && senderAvatarMediaSource != nil) || + (!isDM && roomAvatarMediaSource != nil) { + return true + } + switch event { + case .invite, .none: + return false + case .timeline(let event): + switch try? event.eventType() { + case .state, .none: + return false + case let .messageLike(content): + switch content { + case let .roomMessage(messageType, _): + switch messageType { + case .image, .video, .audio: + return true + default: + return false + } + default: + return false + } + } + } + } +} diff --git a/ios/NSE/RestorationToken.swift b/ios/NSE/RestorationToken.swift new file mode 100644 index 0000000000..4c5bf28fd4 --- /dev/null +++ b/ios/NSE/RestorationToken.swift @@ -0,0 +1,63 @@ +// +// Copyright 2022 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. +// + +import CryptoKit +import Foundation + +import MatrixRustSDK + +struct RestorationToken: Codable, Equatable { + let session: MatrixRustSDK.Session + let pusherNotificationClientIdentifier: String? + + init(session: MatrixRustSDK.Session) { + self.session = session + if let data = session.userId.data(using: .utf8) { + let digest = SHA256.hash(data: data) + pusherNotificationClientIdentifier = digest.compactMap { String(format: "%02x", $0) }.joined() + } else { + pusherNotificationClientIdentifier = nil + } + } +} + +extension MatrixRustSDK.Session: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self = try .init(accessToken: container.decode(String.self, forKey: .accessToken), + refreshToken: container.decodeIfPresent(String.self, forKey: .refreshToken), + userId: container.decode(String.self, forKey: .userId), + deviceId: container.decode(String.self, forKey: .deviceId), + homeserverUrl: container.decode(String.self, forKey: .homeserverUrl), + oidcData: container.decodeIfPresent(String.self, forKey: .oidcData), + slidingSyncProxy: container.decode(String.self, forKey: .slidingSyncProxy)) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(accessToken, forKey: .accessToken) + try container.encode(refreshToken, forKey: .refreshToken) + try container.encode(userId, forKey: .userId) + try container.encode(deviceId, forKey: .deviceId) + try container.encode(homeserverUrl, forKey: .homeserverUrl) + try container.encode(oidcData, forKey: .oidcData) + try container.encode(slidingSyncProxy, forKey: .slidingSyncProxy) + } + + enum CodingKeys: String, CodingKey { + case accessToken, refreshToken, userId, deviceId, homeserverUrl, oidcData, slidingSyncProxy + } +} diff --git a/ios/NSE/RoomMessageEventStringBuilder.swift b/ios/NSE/RoomMessageEventStringBuilder.swift new file mode 100644 index 0000000000..945083b9c3 --- /dev/null +++ b/ios/NSE/RoomMessageEventStringBuilder.swift @@ -0,0 +1,78 @@ +// +// 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. +// + +import Foundation +import MatrixRustSDK + +struct RoomMessageEventStringBuilder { + let attributedStringBuilder: AttributedStringBuilderProtocol + + func buildAttributedString(for messageType: MessageType, senderDisplayName: String, prefixWithSenderName: Bool) -> AttributedString { + let message: String + switch messageType { + // Message types that don't need a prefix. + case .emote(content: let content): + if let attributedMessage = attributedMessageFrom(formattedBody: content.formatted) { + return AttributedString(L10n.commonEmote(senderDisplayName, String(attributedMessage.characters))) + } else { + return AttributedString(L10n.commonEmote(senderDisplayName, content.body)) + } + // Message types that should be prefixed with the sender's name. + case .audio: + message = L10n.commonAudio + case .image: + message = L10n.commonImage + case .video: + message = L10n.commonVideo + case .file: + message = L10n.commonFile + case .location: + message = L10n.commonSharedLocation + case .notice(content: let content): + if let attributedMessage = attributedMessageFrom(formattedBody: content.formatted) { + message = String(attributedMessage.characters) + } else { + message = content.body + } + case .text(content: let content): + if let attributedMessage = attributedMessageFrom(formattedBody: content.formatted) { + message = String(attributedMessage.characters) + } else { + message = content.body + } + } + + if prefixWithSenderName { + return prefix(message, with: senderDisplayName) + } else { + return AttributedString(message) + } + } + + private func prefix(_ eventSummary: String, with senderDisplayName: String) -> AttributedString { + let attributedEventSummary = AttributedString(eventSummary.trimmingCharacters(in: .whitespacesAndNewlines)) + + var attributedSenderDisplayName = AttributedString(senderDisplayName) + attributedSenderDisplayName.bold() + + // Don't include the message body in the markdown otherwise it makes tappable links. + return attributedSenderDisplayName + ": " + attributedEventSummary + } + + private func attributedMessageFrom(formattedBody: FormattedBody?) -> AttributedString? { + formattedBody.flatMap { attributedStringBuilder.fromHTML($0.body) } + } +} diff --git a/ios/NSE/SharedUserDefaultsKeys.swift b/ios/NSE/SharedUserDefaultsKeys.swift new file mode 100644 index 0000000000..a941785b4a --- /dev/null +++ b/ios/NSE/SharedUserDefaultsKeys.swift @@ -0,0 +1,19 @@ +// +// 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 filterNotificationsByPushRulesEnabled +} diff --git a/ios/NSE/Sources/NotificationContentBuilder.swift b/ios/NSE/Sources/NotificationContentBuilder.swift new file mode 100644 index 0000000000..85648b58e4 --- /dev/null +++ b/ios/NSE/Sources/NotificationContentBuilder.swift @@ -0,0 +1,142 @@ +// +// 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. +// + +import Foundation +import MatrixRustSDK +import UserNotifications + +struct NotificationContentBuilder { + let messageEventStringBuilder: RoomMessageEventStringBuilder + + /// Process the given notification item proxy + /// - Parameters: + /// - notificationItem: The notification item + /// - mediaProvider: Media provider to process also media. May be passed nil to ignore media operations. + /// - Returns: A notification content object if the notification should be displayed. Otherwise nil. + func content(for notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { + switch notificationItem.event { + case .none: + return processEmpty(notificationItem: notificationItem) + case .invite: + return try await processInvited(notificationItem: notificationItem, mediaProvider: mediaProvider) + case .timeline(let event): + switch try? event.eventType() { + case let .messageLike(content): + switch content { + case .roomMessage(let messageType, _): + return try await processRoomMessage(notificationItem: notificationItem, messageType: messageType, mediaProvider: mediaProvider) + default: + return processEmpty(notificationItem: notificationItem) + } + default: + return processEmpty(notificationItem: notificationItem) + } + } + } + + // MARK: - Private + + func baseMutableContent(for notificationItem: NotificationItemProxyProtocol) -> UNMutableNotificationContent { + let notification = UNMutableNotificationContent() + notification.receiverID = notificationItem.receiverID + notification.roomID = notificationItem.roomID + notification.eventID = notificationItem.eventID + notification.sound = notificationItem.isNoisy ? UNNotificationSound(named: UNNotificationSoundName(rawValue: "message.caf")) : nil + // So that the UI groups notification that are received for the same room but also for the same user + // Removing the @ fixes an iOS bug where the notification crashes if the mute button is tapped + notification.threadIdentifier = "\(notificationItem.receiverID)\(notificationItem.roomID)".replacingOccurrences(of: "@", with: "") + return notification + } + + private func processEmpty(notificationItem: NotificationItemProxyProtocol) -> UNMutableNotificationContent { + let notification = baseMutableContent(for: notificationItem) + notification.title = InfoPlistReader(bundle: .app).bundleDisplayName + notification.body = L10n.notification + notification.categoryIdentifier = NotificationConstants.Category.message + return notification + } + + private func processInvited(notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { + var notification = baseMutableContent(for: notificationItem) + + notification.categoryIdentifier = NotificationConstants.Category.invite + + let body: String + if !notificationItem.isDM { + body = L10n.notificationRoomInviteBody + } else { + body = L10n.notificationInviteBody + } + + notification = try await notification.addSenderIcon(using: mediaProvider, + senderID: notificationItem.senderID, + senderName: notificationItem.senderDisplayName ?? notificationItem.roomDisplayName, + icon: icon(for: notificationItem)) + notification.body = body + + return notification + } + + 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) + + switch messageType { + case .image(content: let content): + notification = await notification.addMediaAttachment(using: mediaProvider, + mediaSource: .init(source: content.source, + mimeType: content.info?.mimetype)) + case .audio(content: let content): + notification = await notification.addMediaAttachment(using: mediaProvider, + mediaSource: .init(source: content.source, + mimeType: content.info?.mimetype)) + case .video(content: let content): + notification = await notification.addMediaAttachment(using: mediaProvider, + mediaSource: .init(source: content.source, + mimeType: content.info?.mimetype)) + default: + break + } + + return notification + } + + private func processCommonRoomMessage(notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { + var notification = baseMutableContent(for: notificationItem) + notification.title = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName + if notification.title != notificationItem.roomDisplayName { + notification.subtitle = notificationItem.roomDisplayName + } + notification.categoryIdentifier = NotificationConstants.Category.message + + notification = try await notification.addSenderIcon(using: mediaProvider, + senderID: notificationItem.senderID, + senderName: notificationItem.senderDisplayName ?? notificationItem.roomDisplayName, + icon: icon(for: notificationItem)) + return notification + } + + func icon(for notificationItem: NotificationItemProxyProtocol) -> NotificationIcon { + if notificationItem.isDM { + return NotificationIcon(mediaSource: notificationItem.senderAvatarMediaSource, groupInfo: nil) + } else { + return NotificationIcon(mediaSource: notificationItem.roomAvatarMediaSource, + groupInfo: .init(name: notificationItem.roomDisplayName, id: notificationItem.roomID)) + } + } +} diff --git a/ios/NSE/Sources/NotificationServiceExtension.swift b/ios/NSE/Sources/NotificationServiceExtension.swift new file mode 100644 index 0000000000..979b95b326 --- /dev/null +++ b/ios/NSE/Sources/NotificationServiceExtension.swift @@ -0,0 +1,149 @@ +// +// Copyright 2022 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. +// + +import Intents +import MatrixRustSDK +import UserNotifications + +class NotificationServiceExtension: UNNotificationServiceExtension { + private let settings = NSESettings() + private let notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: .homeDirectory))) + private lazy var keychainController = KeychainController(service: .sessions, + accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) + private var handler: ((UNNotificationContent) -> Void)? + private var modifiedContent: UNMutableNotificationContent? + private var userSession: NSEUserSession? + + override func didReceive(_ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + guard !DataProtectionManager.isDeviceLockedAfterReboot(containerURL: URL.appGroupContainerDirectory), + let roomId = request.roomId, + let eventId = request.eventId, + let clientID = request.pusherNotificationClientIdentifier, + let credentials = keychainController.restorationTokens().first(where: { $0.restorationToken.pusherNotificationClientIdentifier == clientID }) else { + // We cannot process this notification, it might be due to one of these: + // - Device rebooted and locked + // - Not a Matrix notification + // - User is not signed in + // - NotificationID could not be resolved + return contentHandler(request.content) + } + + handler = contentHandler + modifiedContent = request.content.mutableCopy() as? UNMutableNotificationContent + + NSELogger.configure() + + NSELogger.logMemory(with: tag) + + MXLog.info("\(tag) #########################################") + MXLog.info("\(tag) Payload came: \(request.content.userInfo)") + + Task { + await run(with: credentials, + roomId: roomId, + eventId: eventId, + unreadCount: request.unreadCount) + } + } + + override func serviceExtensionTimeWillExpire() { + // Called just before the extension will be terminated by the system. + // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. + MXLog.warning("\(tag) serviceExtensionTimeWillExpire") + notify() + } + + private func run(with credentials: KeychainCredentials, + roomId: String, + eventId: String, + unreadCount: Int?) async { + MXLog.info("\(tag) run with roomId: \(roomId), eventId: \(eventId)") + + do { + let userSession = try NSEUserSession(credentials: credentials, clientSessionDelegate: keychainController) + self.userSession = userSession + + guard let itemProxy = await userSession.notificationItemProxy(roomID: roomId, eventID: eventId) else { + MXLog.info("\(tag) no notification for the event, discard") + return discard() + } + + // After the first processing, update the modified content + modifiedContent = try await notificationContentBuilder.content(for: itemProxy, mediaProvider: nil) + + guard itemProxy.hasMedia else { + MXLog.info("\(tag) no media needed") + + // We've processed the item and no media operations needed, so no need to go further + return notify() + } + + MXLog.info("\(tag) process with media") + + // There is some media to load, process it again + if let latestContent = try? await notificationContentBuilder.content(for: itemProxy, mediaProvider: userSession.mediaProvider) { + // Processing finished, hopefully with some media + modifiedContent = latestContent + } + // We still notify, but without the media attachment if it fails to load + + // Finally update the app badge + if let unreadCount { + modifiedContent?.badge = NSNumber(value: unreadCount) + } + + return notify() + } catch { + MXLog.error("NSE run error: \(error)") + return discard() + } + } + + private func notify() { + MXLog.info("\(tag) notify") + + guard let modifiedContent else { + MXLog.info("\(tag) notify: no modified content") + return discard() + } + + handler?(modifiedContent) + cleanUp() + } + + private func discard() { + MXLog.info("\(tag) discard") + + handler?(UNMutableNotificationContent()) + cleanUp() + } + + private var tag: String { + "[NSE][\(Unmanaged.passUnretained(self).toOpaque())][\(Unmanaged.passUnretained(Thread.current).toOpaque())]" + } + + private func cleanUp() { + handler = nil + modifiedContent = nil + } + + deinit { + cleanUp() + NSELogger.logMemory(with: tag) + MXLog.info("\(tag) deinit") + } +} diff --git a/ios/NSE/Sources/Other/DataProtectionManager.swift b/ios/NSE/Sources/Other/DataProtectionManager.swift new file mode 100644 index 0000000000..6a519aa0a0 --- /dev/null +++ b/ios/NSE/Sources/Other/DataProtectionManager.swift @@ -0,0 +1,45 @@ +// +// Copyright 2022 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. +// + +import Foundation + +final class DataProtectionManager { + /// Detects after reboot, before unlocked state. Does this by trying to write a file to the filesystem (to the Caches directory) and read it back. + /// - Parameter containerURL: Container url to write the file. + /// - Returns: true if the state detected + static func isDeviceLockedAfterReboot(containerURL: URL) -> Bool { + let dummyString = ProcessInfo.processInfo.globallyUniqueString + guard let dummyData = dummyString.data(using: .utf8) else { + return true + } + + do { + // add a unique filename + let url = containerURL.appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString) + + try dummyData.write(to: url, options: .completeFileProtectionUntilFirstUserAuthentication) + let readData = try Data(contentsOf: url) + let readString = String(data: readData, encoding: .utf8) + try FileManager.default.removeItem(at: url) + if readString != dummyString { + return true + } + } catch { + return true + } + return false + } +} diff --git a/ios/NSE/Sources/Other/NSELogger.swift b/ios/NSE/Sources/Other/NSELogger.swift new file mode 100644 index 0000000000..d51f263d5c --- /dev/null +++ b/ios/NSE/Sources/Other/NSELogger.swift @@ -0,0 +1,90 @@ +// +// Copyright 2022 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. +// + +import Foundation +import MatrixRustSDK + +class NSELogger { + private static var isConfigured = false + + /// Memory formatter, uses exact 2 fraction digits and no grouping + private static var numberFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.alwaysShowsDecimalSeparator = true + formatter.decimalSeparator = "." + formatter.groupingSeparator = "" + formatter.maximumFractionDigits = 2 + formatter.minimumFractionDigits = 2 + return formatter + } + + private static var formattedMemoryAvailable: String { + let freeBytes = os_proc_available_memory() + let freeMB = Double(freeBytes) / 1024 / 1024 + guard let formattedStr = numberFormatter.string(from: NSNumber(value: freeMB)) else { + return "" + } + return "\(formattedStr) MB" + } + + /// Details: https://developer.apple.com/forums/thread/105088 + /// - Returns: Current memory footprint + private static var memoryFootprint: Float? { + // The `TASK_VM_INFO_COUNT` and `TASK_VM_INFO_REV1_COUNT` macros are too + // complex for the Swift C importer, so we have to define them ourselves. + let TASK_VM_INFO_COUNT = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) + guard let offset = MemoryLayout.offset(of: \task_vm_info_data_t.min_address) else { + return nil + } + let TASK_VM_INFO_REV1_COUNT = mach_msg_type_number_t(offset / MemoryLayout.size) + var info = task_vm_info_data_t() + var count = TASK_VM_INFO_COUNT + let kr = withUnsafeMutablePointer(to: &info) { infoPtr in + infoPtr.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { intPtr in + task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), intPtr, &count) + } + } + guard kr == KERN_SUCCESS, count >= TASK_VM_INFO_REV1_COUNT else { + return nil + } + + return Float(info.phys_footprint) + } + + /// Formatted memory footprint for debugging purposes + /// - Returns: Memory footprint in MBs as a readable string + public static var formattedMemoryFootprint: String { + let usedBytes = UInt64(memoryFootprint ?? 0) + let usedMB = Double(usedBytes) / 1024 / 1024 + guard let formattedStr = numberFormatter.string(from: NSNumber(value: usedMB)) else { + return "" + } + return "\(formattedStr) MB" + } + + static func configure() { + guard !isConfigured else { + return + } + isConfigured = true + + MXLog.configure(target: "nse", logLevel: .info) + } + + static func logMemory(with tag: String) { + MXLog.info("\(tag) Memory: footprint: \(formattedMemoryFootprint) - available: \(formattedMemoryAvailable)") + } +} diff --git a/ios/NSE/Sources/Other/NSESettings.swift b/ios/NSE/Sources/Other/NSESettings.swift new file mode 100644 index 0000000000..d0dd2cbb82 --- /dev/null +++ b/ios/NSE/Sources/Other/NSESettings.swift @@ -0,0 +1,24 @@ +// +// 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. +// + +import Foundation + +final class NSESettings { + private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier + + /// UserDefaults to be used on reads and writes. + private static var store: UserDefaults! = UserDefaults(suiteName: suiteName) +} diff --git a/ios/NSE/Sources/Other/NSEUserSession.swift b/ios/NSE/Sources/Other/NSEUserSession.swift new file mode 100644 index 0000000000..77161df90e --- /dev/null +++ b/ios/NSE/Sources/Other/NSEUserSession.swift @@ -0,0 +1,77 @@ +// +// 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. +// + +import Foundation +import MatrixRustSDK + +final class NSEUserSession { + private let baseClient: Client + private let notificationClient: NotificationClient + private let userID: String + private(set) lazy var mediaProvider: MediaProviderProtocol = MediaProvider(mediaLoader: MediaLoader(client: baseClient), + imageCache: .onlyOnDisk, + backgroundTaskService: nil) + + init(credentials: KeychainCredentials, clientSessionDelegate: ClientSessionDelegate) throws { + userID = credentials.userID + baseClient = try ClientBuilder() + .basePath(path: URL.sessionsBaseDirectory.path) + .username(username: credentials.userID) + .userAgent(userAgent: UserAgentBuilder.makeASCIIUserAgent()) + .enableCrossProcessRefreshLock(processId: InfoPlistReader.main.bundleIdentifier, + sessionDelegate: clientSessionDelegate) + .build() + + baseClient.setDelegate(delegate: ClientDelegateWrapper()) + try baseClient.restoreSession(session: credentials.restorationToken.session) + + notificationClient = try baseClient + .notificationClient(processSetup: .multipleProcesses) + .filterByPushRules() + .finish() + } + + func notificationItemProxy(roomID: String, eventID: String) async -> NotificationItemProxyProtocol? { + await Task.dispatch(on: .global()) { + do { + let notification = try self.notificationClient.getNotification(roomId: roomID, eventId: eventID) + + guard let notification else { + return nil + } + return NotificationItemProxy(notificationItem: notification, + eventID: eventID, + receiverID: self.userID, + roomID: roomID) + } catch { + MXLog.error("NSE: Could not get notification's content creating an empty notification instead, error: \(error)") + return EmptyNotificationItemProxy(eventID: eventID, roomID: roomID, receiverID: self.userID) + } + } + } +} + +private class ClientDelegateWrapper: ClientDelegate { + // MARK: - ClientDelegate + + func didReceiveAuthError(isSoftLogout: Bool) { + MXLog.error("Received authentication error, the NSE can't handle this.") + } + + func didRefreshTokens() { + MXLog.info("Delegating session updates to the ClientSessionDelegate.") + } +} diff --git a/ios/NSE/Sources/Other/UNNotificationRequest.swift b/ios/NSE/Sources/Other/UNNotificationRequest.swift new file mode 100644 index 0000000000..24c7116bb0 --- /dev/null +++ b/ios/NSE/Sources/Other/UNNotificationRequest.swift @@ -0,0 +1,36 @@ +// +// Copyright 2022 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. +// + +import Foundation +import UserNotifications + +extension UNNotificationRequest { + var roomId: String? { + content.userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String + } + + var eventId: String? { + content.userInfo[NotificationConstants.UserInfoKey.eventIdentifier] as? String + } + + var unreadCount: Int? { + content.userInfo[NotificationConstants.UserInfoKey.unreadCount] as? Int + } + + var pusherNotificationClientIdentifier: String? { + content.userInfo[NotificationConstants.UserInfoKey.pusherNotificationClientIdentifier] as? String + } +} diff --git a/ios/NSE/String.swift b/ios/NSE/String.swift new file mode 100644 index 0000000000..d319ab6664 --- /dev/null +++ b/ios/NSE/String.swift @@ -0,0 +1,89 @@ +// +// Copyright 2022 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. +// + +import SwiftUI + +extension String { + /// Returns the string as an `AttributedString` with the specified character tinted in a different color. + /// - Parameters: + /// - character: The character to be tinted. + /// - color: The color to tint the character. Defaults to the accent color. + /// - Returns: An `AttributedString`. + func tinting(_ character: Character, color: Color = .accentColor) -> AttributedString { + var string = AttributedString(self) + let characterView = string.characters + for index in characterView.indices where characterView[index] == character { + string[index.. String? { + guard !isASCII else { + return self + } + guard !canBeConverted(to: .ascii) else { + return nil + } + let mutableString = NSMutableString(string: self) + guard CFStringTransform(mutableString, nil, "Any-Latin; Latin-ASCII; [:^ASCII:] Remove" as CFString, false) else { + return nil + } + return mutableString.trimmingCharacters(in: .whitespaces) + } +} + +extension String { + static func generateBreakableWhitespaceEnd(whitespaceCount: Int, layoutDirection: LayoutDirection) -> String { + guard whitespaceCount > 0 else { + return "" + } + + var whiteSpaces = layoutDirection.isolateLayoutUnicodeString + + // fixed size whitespace of size 1/3 em per character + whiteSpaces += String(repeating: "\u{2004}", count: whitespaceCount) + + // braille whitespace, which is non breakable but makes previous whitespaces breakable + return whiteSpaces + "\u{2800}" + } +} + +extension String { + func ellipsize(length: Int) -> String { + guard count > length else { + return self + } + return "\(prefix(length))…" + } +} diff --git a/ios/NSE/Task.swift b/ios/NSE/Task.swift new file mode 100644 index 0000000000..99f73bbe21 --- /dev/null +++ b/ios/NSE/Task.swift @@ -0,0 +1,63 @@ +// +// Copyright 2022 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. +// + +import Foundation + +public extension Task where Success == Never, Failure == Never { + /// Dispatches the given closure onto the given queue, wrapped within + /// a continuation to make it non-blocking and awaitable. + /// + /// Use this method to `await` blocking calls to the SDK from a `Task`. + /// + /// - Parameters: + /// - queue: The queue to run the closure on. + /// - function: A string identifying the declaration that is the notional + /// source for the continuation, used to identify the continuation in + /// runtime diagnostics related to misuse of this continuation. + /// - body: A sendable closure. Use of sendable won't work as it isn't + /// async, but is added to enforce actor semantics. + static func dispatch(on queue: DispatchQueue, function: String = #function, _ body: @escaping @Sendable () -> T) async -> T { + await withCheckedContinuation(function: function) { continuation in + queue.async { + continuation.resume(returning: body()) + } + } + } + + /// Dispatches the given throwing closure onto the given queue, wrapped within + /// a continuation to make it non-blocking and awaitable. + /// + /// Use this method to `await` blocking calls to the SDK from a `Task`. + /// + /// - Parameters: + /// - queue: The queue to run the closure on. + /// - function: A string identifying the declaration that is the notional + /// source for the continuation, used to identify the continuation in + /// runtime diagnostics related to misuse of this continuation. + /// - body: A sendable closure. Use of sendable won't work as it isn't + /// async, but is added to enforce actor semantics. + static func dispatch(on queue: DispatchQueue, function: String = #function, _ body: @escaping @Sendable () throws -> T) async throws -> T { + try await withCheckedThrowingContinuation(function: function) { continuation in + queue.async { + do { + try continuation.resume(returning: body()) + } catch { + continuation.resume(throwing: error) + } + } + } + } +} diff --git a/ios/NSE/TestablePreview.swift b/ios/NSE/TestablePreview.swift new file mode 100644 index 0000000000..e138b9cd3e --- /dev/null +++ b/ios/NSE/TestablePreview.swift @@ -0,0 +1,21 @@ +// +// 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. +// + +import SwiftUI + +import Prefire + +protocol TestablePreview: PrefireProvider { } diff --git a/ios/NSE/UNNotificationContent.swift b/ios/NSE/UNNotificationContent.swift new file mode 100644 index 0000000000..149fae7354 --- /dev/null +++ b/ios/NSE/UNNotificationContent.swift @@ -0,0 +1,252 @@ +// +// 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. +// + +import Foundation +import Intents +import SwiftUI +import UserNotifications + +import Version + +struct NotificationIcon { + struct GroupInfo { + let name: String + let id: String + } + + let mediaSource: MediaSourceProxy? + // Required as the key to set images for groups + let groupInfo: GroupInfo? + + var shouldDisplayAsGroup: Bool { + groupInfo != nil + } +} + +extension UNNotificationContent { + @objc var receiverID: String? { + userInfo[NotificationConstants.UserInfoKey.receiverIdentifier] as? String + } + + @objc var roomID: String? { + userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String + } + + @objc var eventID: String? { + userInfo[NotificationConstants.UserInfoKey.eventIdentifier] as? String + } +} + +extension UNMutableNotificationContent { + override var receiverID: String? { + get { + userInfo[NotificationConstants.UserInfoKey.receiverIdentifier] as? String + } + set { + userInfo[NotificationConstants.UserInfoKey.receiverIdentifier] = newValue + } + } + + override var roomID: String? { + get { + userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String + } + set { + userInfo[NotificationConstants.UserInfoKey.roomIdentifier] = newValue + } + } + + override var eventID: String? { + get { + userInfo[NotificationConstants.UserInfoKey.eventIdentifier] as? String + } + set { + userInfo[NotificationConstants.UserInfoKey.eventIdentifier] = newValue + } + } + + func addMediaAttachment(using mediaProvider: MediaProviderProtocol?, + mediaSource: MediaSourceProxy) async -> UNMutableNotificationContent { + guard let mediaProvider else { + return self + } + switch await mediaProvider.loadFileFromSource(mediaSource) { + case .success(let file): + do { + let identifier = ProcessInfo.processInfo.globallyUniqueString + let newURL = try FileManager.default.copyFileToTemporaryDirectory(file: file.url, with: "\(identifier).\(file.url.pathExtension)") + let attachment = try UNNotificationAttachment(identifier: identifier, + url: newURL, + options: nil) + attachments.append(attachment) + } catch { + MXLog.error("Couldn't add media attachment:: \(error)") + return self + } + case .failure(let error): + MXLog.error("Couldn't load the file for media attachment: \(error)") + } + + return self + } + + func addSenderIcon(using mediaProvider: MediaProviderProtocol?, + senderID: String, + senderName: String, + icon: NotificationIcon) async throws -> UNMutableNotificationContent { + // We display the placeholder only if... + var needsPlaceholder = false + + var fetchedImage: INImage? + let image: INImage + if let mediaSource = icon.mediaSource { + switch await mediaProvider?.loadImageDataFromSource(mediaSource) { + case .success(let data): + fetchedImage = INImage(imageData: data) + case .failure(let error): + MXLog.error("Couldn't add sender icon: \(error)") + // ...The provider failed to fetch + needsPlaceholder = true + case .none: + break + } + } else { + // ...There is no media + needsPlaceholder = true + } + + if let fetchedImage { + image = fetchedImage + } else if needsPlaceholder, + let data = await getPlaceholderAvatarImageData(name: icon.groupInfo?.name ?? senderName, + id: icon.groupInfo?.id ?? senderID) { + image = INImage(imageData: data) + } else { + image = INImage(named: "") + } + + let senderHandle = INPersonHandle(value: senderID, type: .unknown) + let sender = INPerson(personHandle: senderHandle, + nameComponents: nil, + displayName: senderName, + image: !icon.shouldDisplayAsGroup ? image : nil, + contactIdentifier: nil, + customIdentifier: nil) + + // These are required to show the group name as subtitle + var speakableGroupName: INSpeakableString? + var recipients: [INPerson]? + if let groupInfo = icon.groupInfo { + let meHandle = INPersonHandle(value: receiverID, type: .unknown) + let me = INPerson(personHandle: meHandle, nameComponents: nil, displayName: nil, image: nil, contactIdentifier: nil, customIdentifier: nil, isMe: true) + speakableGroupName = INSpeakableString(spokenPhrase: groupInfo.name) + recipients = [sender, me] + } + + let intent = INSendMessageIntent(recipients: recipients, + outgoingMessageType: .outgoingMessageText, + content: nil, + speakableGroupName: speakableGroupName, + conversationIdentifier: roomID, + serviceName: nil, + sender: sender, + attachments: nil) + if speakableGroupName != nil { + intent.setImage(image, forParameterNamed: \.speakableGroupName) + } + + // Use the intent to initialize the interaction. + let interaction = INInteraction(intent: intent, response: nil) + + // Interaction direction is incoming because the user is + // receiving this message. + interaction.direction = .incoming + + // Donate the interaction before updating notification content. + try await interaction.donate() + // Update notification content before displaying the + // communication notification. + let updatedContent = try updating(from: intent) + + // swiftlint:disable:next force_cast + return updatedContent.mutableCopy() as! UNMutableNotificationContent + } + + private func getPlaceholderAvatarImageData(name: String, id: String) async -> Data? { + // The version value is used in case the design of the placeholder is updated to force a replacement + let isIOS17Available = isIOS17Available() + let prefix = "notification_placeholder\(isIOS17Available ? "V3" : "V2")" + let fileName = "\(prefix)_\(name)_\(id).png" + if let data = try? Data(contentsOf: URL.temporaryDirectory.appendingPathComponent(fileName)) { + MXLog.info("Found existing notification icon placeholder") + return data + } + + MXLog.info("Generating notification icon placeholder") + let image = PlaceholderAvatarImage(name: name, + contentID: id) + .clipShape(Circle()) + .frame(width: 50, height: 50) + let renderer = await ImageRenderer(content: image) + guard let image = await renderer.uiImage else { + MXLog.info("Generating notification icon placeholder failed") + return nil + } + + let data: Data? + // On simulator and macOS the image is rendered correctly + // But on other devices before iOS 17 is rendered upside down so we need to flip it + #if targetEnvironment(simulator) + data = image.pngData() + #else + if ProcessInfo.processInfo.isiOSAppOnMac || isIOS17Available { + data = image.pngData() + } else { + data = image.flippedVertically().pngData() + } + #endif + + if let data { + do { + // cache image data + try FileManager.default.writeDataToTemporaryDirectory(data: data, fileName: fileName) + } catch { + MXLog.error("Could not store placeholder image") + return data + } + } + return data + } + + private func isIOS17Available() -> Bool { + guard let version = Version(UIDevice.current.systemVersion) else { + return false + } + + return version.major >= 17 + } +} + +private extension UIImage { + func flippedVertically() -> UIImage { + let format = UIGraphicsImageRendererFormat() + format.scale = scale + return UIGraphicsImageRenderer(size: size, format: format).image { context in + context.cgContext.concatenate(CGAffineTransform(scaleX: 1, y: -1)) + self.draw(at: CGPoint(x: 0, y: -size.height)) + } + } +} diff --git a/ios/NSE/URL.swift b/ios/NSE/URL.swift new file mode 100644 index 0000000000..bd8fa98759 --- /dev/null +++ b/ios/NSE/URL.swift @@ -0,0 +1,81 @@ +// +// Copyright 2022 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. +// + +import Foundation + +extension URL: ExpressibleByStringLiteral { + public init(stringLiteral value: StaticString) { + guard let url = URL(string: "\(value)") else { + fatalError("The static string used to create this URL is invalid") + } + + self = url + } + + /// The URL of the primary app group container. + static var appGroupContainerDirectory: URL { + guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: InfoPlistReader.main.appGroupIdentifier) else { + MXLog.error("Application Group unavailable, falling back to the application folder") + // Browserstack doesn't properly handle AppGroup entitlements so this fails, presumably because of the resigning happening on their side + // Try using the normal app folder instead of the app group + // https://www.browserstack.com/docs/app-automate/appium/troubleshooting/entitlements-error + + return URL.applicationSupportDirectory.deletingLastPathComponent().deletingLastPathComponent() + } + + return url + } + + /// The base directory where all session data is stored. + static var sessionsBaseDirectory: URL { + let applicationSupportSessionsURL = applicationSupportBaseDirectory.appendingPathComponent("Sessions", isDirectory: true) + + try? FileManager.default.createDirectoryIfNeeded(at: applicationSupportSessionsURL) + + return applicationSupportSessionsURL + } + + /// The base directory where all cache is stored. + static var cacheBaseDirectory: URL { + let url = appGroupContainerDirectory + .appendingPathComponent("Library", isDirectory: true) + .appendingPathComponent("Caches", isDirectory: true) + + try? FileManager.default.createDirectoryIfNeeded(at: url) + + return url + } + + /// The base directory where all application support data is stored. + static var applicationSupportBaseDirectory: URL { + var url = appGroupContainerDirectory + .appendingPathComponent("Library", isDirectory: true) + .appendingPathComponent("Application Support", isDirectory: true) + .appendingPathComponent(InfoPlistReader.main.baseBundleIdentifier, isDirectory: true) + + try? FileManager.default.createDirectoryIfNeeded(at: url) + + do { + var resourceValues = URLResourceValues() + resourceValues.isExcludedFromBackup = true + try url.setResourceValues(resourceValues) + } catch { + MXLog.error("Failed excluding Application Support from backups") + } + + return url + } +} diff --git a/ios/NSE/UTType.swift b/ios/NSE/UTType.swift new file mode 100644 index 0000000000..b92e825e2d --- /dev/null +++ b/ios/NSE/UTType.swift @@ -0,0 +1,35 @@ +// +// 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. +// + +import UniformTypeIdentifiers + +extension UTType { + /// Creates a type based on an optional mime type, falling back to a filename when this type is missing or unknown. + init?(mimeType: String?, fallbackFilename: String) { + guard let mimeType, let type = UTType(mimeType: mimeType) else { + self.init(filename: fallbackFilename) + return + } + self = type + } + + /// Creates a type based on a filename. + private init?(filename: String) { + let components = filename.split(separator: ".") + guard components.count > 1, let filenameExtension = components.last else { return nil } + self.init(filenameExtension: String(filenameExtension)) + } +} diff --git a/ios/NSE/UserAgentBuilder.swift b/ios/NSE/UserAgentBuilder.swift new file mode 100644 index 0000000000..aacf7ed398 --- /dev/null +++ b/ios/NSE/UserAgentBuilder.swift @@ -0,0 +1,63 @@ +// +// Copyright 2022 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. +// + +import Foundation +import UIKit + +#if !os(OSX) +import DeviceKit +#endif + +final class UserAgentBuilder { + class func makeASCIIUserAgent() -> String { + makeUserAgent()?.asciified() ?? "unknown" + } + + private class func makeUserAgent() -> String? { + let clientName = InfoPlistReader.app.bundleDisplayName + let clientVersion = InfoPlistReader.app.bundleShortVersionString + + #if os(iOS) + return String(format: "%@/%@ (%@; iOS %@; Scale/%0.2f)", + clientName, + clientVersion, + Device.current.safeDescription, + UIDevice.current.systemVersion, + UIScreen.main.scale) + #elseif os(tvOS) + return String(format: "%@/%@ (%@; tvOS %@; Scale/%0.2f)", + clientName, + clientVersion, + Device.current.safeDescription, + UIDevice.current.systemVersion, + UIScreen.main.scale) + #elseif os(watchOS) + return String(format: "%@/%@ (%@; watchOS %@; Scale/%0.2f)", + clientName, + clientVersion, + Device.current.safeDescription, + WKInterfaceDevice.current.systemVersion, + WKInterfaceDevice.currentDevice.screenScale) + #elseif os(OSX) + return String(format: "%@/%@ (Mac; Mac OS X %@)", + clientName, + clientVersion, + NSProcessInfo.processInfo.operatingSystemVersionString) + #else + return nil + #endif + } +} diff --git a/ios/NSE/UserPreference.swift b/ios/NSE/UserPreference.swift new file mode 100644 index 0000000000..f53bcaa9fc --- /dev/null +++ b/ios/NSE/UserPreference.swift @@ -0,0 +1,207 @@ +// +// 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. +// + +import Combine +import Foundation + +/// Property wrapper that allows to store data into a keyed storage. +/// It also exposes a Combine publisher for listening to value changes. +/// The publisher isn't supposed to skip consecutive duplicates if any, +/// there is no concept of Equatable at this level. +@propertyWrapper +final class UserPreference { + private let key: String + private var keyedStorage: any KeyedStorage + private let defaultValue: T + private let subject: PassthroughSubject = .init() + + init(key: String, defaultValue: T, keyedStorage: any KeyedStorage) { + self.key = key + self.defaultValue = defaultValue + self.keyedStorage = keyedStorage + } + + var wrappedValue: T { + get { + keyedStorage[key] ?? defaultValue + } + set { + keyedStorage[key] = newValue + subject.send(wrappedValue) + } + } + + var projectedValue: AnyPublisher { + subject + .prepend(wrappedValue) + .eraseToAnyPublisher() + } +} + +// MARK: - UserPreference convenience initializers + +extension UserPreference { + enum StorageType { + case userDefaults(UserDefaults = .standard) + case volatile + } + + convenience init(key: String, defaultValue: T, storageType: StorageType) { + let storage: any KeyedStorage + + switch storageType { + case .userDefaults(let userDefaults): + storage = UserDefaultsStorage(userDefaults: userDefaults) + case .volatile: + storage = [String: T]() + } + + self.init(key: key, defaultValue: defaultValue, keyedStorage: storage) + } + + convenience init(key: R, defaultValue: T, storageType: StorageType) where R.RawValue == String { + self.init(key: key.rawValue, defaultValue: defaultValue, storageType: storageType) + } + + /// Convenience initializer that also immediatelly stores the provided initialValue. + /// The initial value is stored every time the app is launched. + /// And will override any existing values. + /// + /// - Parameters: + /// - key: the raw representable key used to store the value, needs conform also to String + /// - initialValue: the initial value that will be stored when the app is launched, the initialValue is also used as defaultValue + /// - storageType: the storage type where the wrappedValue will be stored. + convenience init(key: R, initialValue: T, storageType: StorageType) where R.RawValue == String { + self.init(key: key, defaultValue: initialValue, storageType: storageType) + wrappedValue = initialValue + } + + convenience init(key: String, storageType: StorageType) where T: ExpressibleByNilLiteral { + self.init(key: key, defaultValue: nil, storageType: storageType) + } + + convenience init(key: R, storageType: StorageType) where R: RawRepresentable, R.RawValue == String, T: ExpressibleByNilLiteral { + self.init(key: key.rawValue, storageType: storageType) + } +} + +// MARK: - Storage + +protocol KeyedStorage { + associatedtype Value: Codable + + subscript(key: String) -> Value? { get set } +} + +/// An implementation of KeyedStorage on the UserDefaults. +/// +/// When used with a `Value` that conforms to `PlistRepresentable` the Codable encode/decode +/// phase is skipped, and values are stored natively in the plist. +final class UserDefaultsStorage: KeyedStorage { + private let userDefaults: UserDefaults + + init(userDefaults: UserDefaults) { + self.userDefaults = userDefaults + } + + subscript(key: String) -> Value? { + get { + let value: Value? + if Value.self is PlistRepresentable.Type { + value = decodePlistRepresentableValue(for: key) + } else { + value = decodeValue(for: key) + } + + return value + } + set { + if Value.self is PlistRepresentable.Type { + encodePlistRepresentable(value: newValue, for: key) + } else { + encode(value: newValue, for: key) + } + } + } + + private func decodeValue(for key: String) -> Value? { + userDefaults + .data(forKey: key) + .flatMap { + try? JSONDecoder().decode(Value.self, from: $0) + } + } + + private func decodePlistRepresentableValue(for key: String) -> Value? { + userDefaults.object(forKey: key) as? Value + } + + private func encode(value: Value?, for key: String) { + // Detects correctly double optionals like this: String?? = .some(nil) + if value.isNil { + userDefaults.removeObject(forKey: key) + } else { + let encodedValue = try? JSONEncoder().encode(value) + userDefaults.setValue(encodedValue, forKey: key) + } + } + + private func encodePlistRepresentable(value: Value?, for key: String) { + // Detects correctly double optionals like this: String?? = .some(nil) + if value.isNil { + userDefaults.removeObject(forKey: key) + } else { + userDefaults.set(value, forKey: key) + } + } +} + +private protocol Nullable { + var isNil: Bool { get } +} + +extension Optional: Nullable { + var isNil: Bool { + switch self { + case .none: + return true + case .some(let nullable as Nullable): + return nullable.isNil + case .some: + return false + } + } +} + +extension Dictionary: KeyedStorage where Key == String, Value: Codable { } + +// MARK: - PlistRepresentable + +/// A protocol to mark types as being plist compliant. +/// UserDefaultsStorage uses this protocol to avoid to encode/decode with Codable plist compliant values. +protocol PlistRepresentable { } + +extension Bool: PlistRepresentable { } +extension String: PlistRepresentable { } +extension Int: PlistRepresentable { } +extension Float: PlistRepresentable { } +extension Double: PlistRepresentable { } +extension Date: PlistRepresentable { } +extension Data: PlistRepresentable { } + +extension Array: PlistRepresentable where Element: PlistRepresentable { } +extension Dictionary: PlistRepresentable where Key == String, Value: PlistRepresentable { } +extension Optional: PlistRepresentable where Wrapped: PlistRepresentable { } diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 4109807303..5c71e9e9d7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,14 +3,43 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ 0611A7F12A678C7700F180CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0611A7F42A678C7700F180CC /* Localizable.strings */; }; 0611A7F22A678C7700F180CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0611A7F42A678C7700F180CC /* Localizable.strings */; }; + 061C71742B2183DE0087684C /* NSEUserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71502B2183DE0087684C /* NSEUserSession.swift */; }; + 061C71752B2183DE0087684C /* NSESettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71512B2183DE0087684C /* NSESettings.swift */; }; + 061C71762B2183DE0087684C /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71522B2183DE0087684C /* UNNotificationRequest.swift */; }; + 061C71772B2183DE0087684C /* DataProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71532B2183DE0087684C /* DataProtectionManager.swift */; }; + 061C71782B2183DE0087684C /* NSELogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71542B2183DE0087684C /* NSELogger.swift */; }; + 061C71792B2183DE0087684C /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71552B2183DE0087684C /* NotificationServiceExtension.swift */; }; + 061C717A2B2183DE0087684C /* NotificationContentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71562B2183DE0087684C /* NotificationContentBuilder.swift */; }; + 061C717C2B2183DE0087684C /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71592B2183DE0087684C /* ElementXAttributeScope.swift */; }; + 061C717D2B2183DE0087684C /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C715A2B2183DE0087684C /* AttributedStringBuilderProtocol.swift */; }; + 061C717E2B2183DE0087684C /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C715C2B2183DE0087684C /* AttributedStringBuilder.swift */; }; + 061C717F2B2183DE0087684C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C715D2B2183DE0087684C /* DTHTMLElement+AttributedStringBuilder.swift */; }; + 061C71802B2183DE0087684C /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 061C715E2B2183DE0087684C /* UIFont+AttributedStringBuilder.m */; }; + 061C71812B2183DE0087684C /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71602B2183DE0087684C /* MXLog.swift */; }; + 061C71822B2183DE0087684C /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71612B2183DE0087684C /* RustTracing.swift */; }; + 061C71832B2183DE0087684C /* MXLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71622B2183DE0087684C /* MXLogger.swift */; }; + 061C71842B2183DE0087684C /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71642B2183DE0087684C /* Strings.swift */; }; + 061C71852B2183DE0087684C /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71652B2183DE0087684C /* Assets.swift */; }; + 061C71862B2183DE0087684C /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71662B2183DE0087684C /* Strings+Untranslated.swift */; }; + 061C71872B2183DE0087684C /* MediaFileHandleProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71682B2183DE0087684C /* MediaFileHandleProxy.swift */; }; + 061C71882B2183DE0087684C /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71692B2183DE0087684C /* MediaProviderProtocol.swift */; }; + 061C71892B2183DE0087684C /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C716A2B2183DE0087684C /* MediaProvider.swift */; }; + 061C718A2B2183DE0087684C /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C716B2B2183DE0087684C /* MediaLoader.swift */; }; + 061C718B2B2183DE0087684C /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C716C2B2183DE0087684C /* MediaLoaderProtocol.swift */; }; + 061C718C2B2183DE0087684C /* ImageProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C716D2B2183DE0087684C /* ImageProviderProtocol.swift */; }; + 061C718D2B2183DE0087684C /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C716E2B2183DE0087684C /* MediaSourceProxy.swift */; }; + 061C718E2B2183DE0087684C /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C716F2B2183DE0087684C /* MockMediaProvider.swift */; }; + 061C718F2B2183DE0087684C /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71712B2183DE0087684C /* NotificationItemProxyProtocol.swift */; }; + 061C71902B2183DE0087684C /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71722B2183DE0087684C /* NotificationItemProxy.swift */; }; + 061C71912B2183DE0087684C /* SharedUserDefaultsKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061C71732B2183DE0087684C /* SharedUserDefaultsKeys.swift */; }; + 061C71952B21847C0087684C /* DesignKit in Frameworks */ = {isa = PBXBuildFile; productRef = 061C71942B21847C0087684C /* DesignKit */; }; 062DA3DD2B15813A007A963B /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA3DC2B15813A007A963B /* MatrixRustSDK */; }; - 062DA3E72B1585C2007A963B /* NotificationContentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3E62B1585C2007A963B /* NotificationContentBuilder.swift */; }; 062DA4062B159903007A963B /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3E82B159902007A963B /* InfoPlistReader.swift */; }; 062DA4072B159903007A963B /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3E92B159902007A963B /* Date.swift */; }; 062DA4082B159903007A963B /* UserPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3EA2B159902007A963B /* UserPreference.swift */; }; @@ -18,10 +47,8 @@ 062DA40A2B159903007A963B /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3EC2B159902007A963B /* NotificationConstants.swift */; }; 062DA40B2B159903007A963B /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3ED2B159902007A963B /* BackgroundTaskServiceProtocol.swift */; }; 062DA40C2B159903007A963B /* RoomMessageEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3EE2B159902007A963B /* RoomMessageEventStringBuilder.swift */; }; - 062DA40D2B159903007A963B /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3EF2B159902007A963B /* AppSettings.swift */; }; 062DA40E2B159903007A963B /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F02B159902007A963B /* RestorationToken.swift */; }; 062DA40F2B159903007A963B /* PermalinkBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F12B159902007A963B /* PermalinkBuilder.swift */; }; - 062DA4102B159903007A963B /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F22B159902007A963B /* PlainMentionBuilder.swift */; }; 062DA4112B159903007A963B /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F32B159903007A963B /* PlaceholderAvatarImage.swift */; }; 062DA4122B159903007A963B /* NSRegularExpresion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F42B159903007A963B /* NSRegularExpresion.swift */; }; 062DA4132B159903007A963B /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3F52B159903007A963B /* AttributedString.swift */; }; @@ -37,7 +64,6 @@ 062DA41D2B159903007A963B /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA3FF2B159903007A963B /* KeychainController.swift */; }; 062DA41E2B159903007A963B /* TestablePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4002B159903007A963B /* TestablePreview.swift */; }; 062DA41F2B159903007A963B /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4012B159903007A963B /* AvatarSize.swift */; }; - 062DA4202B159903007A963B /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4022B159903007A963B /* PillConstants.swift */; }; 062DA4212B159903007A963B /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4032B159903007A963B /* FileManager.swift */; }; 062DA4222B159903007A963B /* UNNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4042B159903007A963B /* UNNotificationContent.swift */; }; 062DA4232B159903007A963B /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4052B159903007A963B /* UTType.swift */; }; @@ -45,44 +71,13 @@ 062DA4292B159B3B007A963B /* Prefire in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA4282B159B3B007A963B /* Prefire */; }; 062DA42C2B159B6E007A963B /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA42B2B159B6E007A963B /* Kingfisher */; }; 062DA42F2B159C2D007A963B /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA42E2B159C2D007A963B /* KeychainAccess */; }; - 062DA4322B159C8A007A963B /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA4312B159C8A007A963B /* Compound */; }; 062DA4352B159CCB007A963B /* Version in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA4342B159CCB007A963B /* Version */; }; - 062DA44B2B15A001007A963B /* MXLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4362B15A000007A963B /* MXLogger.swift */; }; - 062DA44D2B15A001007A963B /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4382B15A000007A963B /* Strings+Untranslated.swift */; }; - 062DA44E2B15A001007A963B /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4392B15A001007A963B /* Strings.swift */; }; - 062DA44F2B15A001007A963B /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA43A2B15A001007A963B /* Assets.swift */; }; - 062DA4502B15A001007A963B /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA43B2B15A001007A963B /* MediaProviderProtocol.swift */; }; - 062DA4512B15A001007A963B /* ImageProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA43C2B15A001007A963B /* ImageProviderProtocol.swift */; }; - 062DA4522B15A001007A963B /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA43D2B15A001007A963B /* MediaSourceProxy.swift */; }; - 062DA4532B15A001007A963B /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA43E2B15A001007A963B /* ElementXAttributeScope.swift */; }; - 062DA4542B15A001007A963B /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA43F2B15A001007A963B /* MediaProvider.swift */; }; - 062DA4552B15A001007A963B /* MediaFileHandleProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4402B15A001007A963B /* MediaFileHandleProxy.swift */; }; - 062DA4562B15A001007A963B /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4412B15A001007A963B /* MockMediaProvider.swift */; }; - 062DA4572B15A001007A963B /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4422B15A001007A963B /* NotificationItemProxy.swift */; }; - 062DA4582B15A001007A963B /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4432B15A001007A963B /* MediaLoader.swift */; }; - 062DA4592B15A001007A963B /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4442B15A001007A963B /* AttributedStringBuilder.swift */; }; - 062DA45A2B15A001007A963B /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4452B15A001007A963B /* MediaLoaderProtocol.swift */; }; - 062DA45B2B15A001007A963B /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4462B15A001007A963B /* MXLog.swift */; }; - 062DA45C2B15A001007A963B /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4472B15A001007A963B /* RustTracing.swift */; }; - 062DA45D2B15A001007A963B /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4482B15A001007A963B /* DTHTMLElement+AttributedStringBuilder.swift */; }; - 062DA45E2B15A001007A963B /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4492B15A001007A963B /* NotificationItemProxyProtocol.swift */; }; - 062DA45F2B15A001007A963B /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA44A2B15A001007A963B /* AttributedStringBuilderProtocol.swift */; }; 062DA4622B15A03E007A963B /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA4612B15A03E007A963B /* DTCoreText */; }; 062DA4652B15A06A007A963B /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA4642B15A06A007A963B /* LRUCache */; }; 062DA4682B15A09C007A963B /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA4672B15A09C007A963B /* Collections */; }; 062DA46A2B15A09C007A963B /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA4692B15A09C007A963B /* DequeModule */; }; 062DA46C2B15A09C007A963B /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = 062DA46B2B15A09C007A963B /* OrderedCollections */; }; - 062DA4722B15A154007A963B /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA46D2B15A153007A963B /* UNNotificationRequest.swift */; }; - 062DA4732B15A154007A963B /* NSESettingsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA46E2B15A153007A963B /* NSESettingsProtocol.swift */; }; - 062DA4742B15A154007A963B /* DataProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA46F2B15A153007A963B /* DataProtectionManager.swift */; }; - 062DA4752B15A154007A963B /* NSELogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4702B15A153007A963B /* NSELogger.swift */; }; - 062DA4762B15A154007A963B /* NSEUserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062DA4712B15A153007A963B /* NSEUserSession.swift */; }; - 0646E10A2B1084E00014B8DD /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0646E1092B1084E00014B8DD /* NotificationService.swift */; }; 0646E10E2B1084E00014B8DD /* NSE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 0646E1072B1084E00014B8DD /* NSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 0646E1152B10A1160014B8DD /* KeychainSharingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0646E1142B10A1160014B8DD /* KeychainSharingData.swift */; }; - 0646E1172B10A3EB0014B8DD /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0646E1162B10A3EB0014B8DD /* Event.swift */; }; - 0646E1192B10A4EC0014B8DD /* MatrixHttpClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0646E1182B10A4EC0014B8DD /* MatrixHttpClient.swift */; }; - 06ED217C2B16D83000363DF5 /* KeychainSharingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0646E1142B10A1160014B8DD /* KeychainSharingData.swift */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 24AE4F7C04208EFEFE8D10A1 /* Pods_Twake_Share.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29F9CA3C68ACBADDD794AB3A /* Pods_Twake_Share.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; @@ -140,7 +135,36 @@ /* Begin PBXFileReference section */ 0611A7F32A678C7700F180CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - 062DA3E62B1585C2007A963B /* NotificationContentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentBuilder.swift; sourceTree = ""; }; + 061C71502B2183DE0087684C /* NSEUserSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSEUserSession.swift; sourceTree = ""; }; + 061C71512B2183DE0087684C /* NSESettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSESettings.swift; sourceTree = ""; }; + 061C71522B2183DE0087684C /* UNNotificationRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = ""; }; + 061C71532B2183DE0087684C /* DataProtectionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataProtectionManager.swift; sourceTree = ""; }; + 061C71542B2183DE0087684C /* NSELogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = ""; }; + 061C71552B2183DE0087684C /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; + 061C71562B2183DE0087684C /* NotificationContentBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationContentBuilder.swift; sourceTree = ""; }; + 061C71592B2183DE0087684C /* ElementXAttributeScope.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElementXAttributeScope.swift; sourceTree = ""; }; + 061C715A2B2183DE0087684C /* AttributedStringBuilderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderProtocol.swift; sourceTree = ""; }; + 061C715B2B2183DE0087684C /* UIFont+AttributedStringBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIFont+AttributedStringBuilder.h"; sourceTree = ""; }; + 061C715C2B2183DE0087684C /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = ""; }; + 061C715D2B2183DE0087684C /* DTHTMLElement+AttributedStringBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DTHTMLElement+AttributedStringBuilder.swift"; sourceTree = ""; }; + 061C715E2B2183DE0087684C /* UIFont+AttributedStringBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIFont+AttributedStringBuilder.m"; sourceTree = ""; }; + 061C71602B2183DE0087684C /* MXLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = ""; }; + 061C71612B2183DE0087684C /* RustTracing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RustTracing.swift; sourceTree = ""; }; + 061C71622B2183DE0087684C /* MXLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MXLogger.swift; sourceTree = ""; }; + 061C71642B2183DE0087684C /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; + 061C71652B2183DE0087684C /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; + 061C71662B2183DE0087684C /* Strings+Untranslated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = ""; }; + 061C71682B2183DE0087684C /* MediaFileHandleProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaFileHandleProxy.swift; sourceTree = ""; }; + 061C71692B2183DE0087684C /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; + 061C716A2B2183DE0087684C /* MediaProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; + 061C716B2B2183DE0087684C /* MediaLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaLoader.swift; sourceTree = ""; }; + 061C716C2B2183DE0087684C /* MediaLoaderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaLoaderProtocol.swift; sourceTree = ""; }; + 061C716D2B2183DE0087684C /* ImageProviderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageProviderProtocol.swift; sourceTree = ""; }; + 061C716E2B2183DE0087684C /* MediaSourceProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaSourceProxy.swift; sourceTree = ""; }; + 061C716F2B2183DE0087684C /* MockMediaProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = ""; }; + 061C71712B2183DE0087684C /* NotificationItemProxyProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationItemProxyProtocol.swift; sourceTree = ""; }; + 061C71722B2183DE0087684C /* NotificationItemProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = ""; }; + 061C71732B2183DE0087684C /* SharedUserDefaultsKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharedUserDefaultsKeys.swift; sourceTree = ""; }; 062DA3E82B159902007A963B /* InfoPlistReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoPlistReader.swift; sourceTree = ""; }; 062DA3E92B159902007A963B /* Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; 062DA3EA2B159902007A963B /* UserPreference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserPreference.swift; sourceTree = ""; }; @@ -148,10 +172,8 @@ 062DA3EC2B159902007A963B /* NotificationConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = ""; }; 062DA3ED2B159902007A963B /* BackgroundTaskServiceProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskServiceProtocol.swift; sourceTree = ""; }; 062DA3EE2B159902007A963B /* RoomMessageEventStringBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomMessageEventStringBuilder.swift; sourceTree = ""; }; - 062DA3EF2B159902007A963B /* AppSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; 062DA3F02B159902007A963B /* RestorationToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestorationToken.swift; sourceTree = ""; }; 062DA3F12B159902007A963B /* PermalinkBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PermalinkBuilder.swift; sourceTree = ""; }; - 062DA3F22B159902007A963B /* PlainMentionBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlainMentionBuilder.swift; sourceTree = ""; }; 062DA3F32B159903007A963B /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = ""; }; 062DA3F42B159903007A963B /* NSRegularExpresion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSRegularExpresion.swift; sourceTree = ""; }; 062DA3F52B159903007A963B /* AttributedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedString.swift; sourceTree = ""; }; @@ -167,42 +189,12 @@ 062DA3FF2B159903007A963B /* KeychainController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = ""; }; 062DA4002B159903007A963B /* TestablePreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestablePreview.swift; sourceTree = ""; }; 062DA4012B159903007A963B /* AvatarSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = ""; }; - 062DA4022B159903007A963B /* PillConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PillConstants.swift; sourceTree = ""; }; 062DA4032B159903007A963B /* FileManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; 062DA4042B159903007A963B /* UNNotificationContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UNNotificationContent.swift; sourceTree = ""; }; 062DA4052B159903007A963B /* UTType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UTType.swift; sourceTree = ""; }; - 062DA4362B15A000007A963B /* MXLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MXLogger.swift; sourceTree = ""; }; - 062DA4382B15A000007A963B /* Strings+Untranslated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = ""; }; - 062DA4392B15A001007A963B /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; - 062DA43A2B15A001007A963B /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; - 062DA43B2B15A001007A963B /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; - 062DA43C2B15A001007A963B /* ImageProviderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageProviderProtocol.swift; sourceTree = ""; }; - 062DA43D2B15A001007A963B /* MediaSourceProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaSourceProxy.swift; sourceTree = ""; }; - 062DA43E2B15A001007A963B /* ElementXAttributeScope.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElementXAttributeScope.swift; sourceTree = ""; }; - 062DA43F2B15A001007A963B /* MediaProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; - 062DA4402B15A001007A963B /* MediaFileHandleProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaFileHandleProxy.swift; sourceTree = ""; }; - 062DA4412B15A001007A963B /* MockMediaProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = ""; }; - 062DA4422B15A001007A963B /* NotificationItemProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = ""; }; - 062DA4432B15A001007A963B /* MediaLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaLoader.swift; sourceTree = ""; }; - 062DA4442B15A001007A963B /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = ""; }; - 062DA4452B15A001007A963B /* MediaLoaderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaLoaderProtocol.swift; sourceTree = ""; }; - 062DA4462B15A001007A963B /* MXLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = ""; }; - 062DA4472B15A001007A963B /* RustTracing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RustTracing.swift; sourceTree = ""; }; - 062DA4482B15A001007A963B /* DTHTMLElement+AttributedStringBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DTHTMLElement+AttributedStringBuilder.swift"; sourceTree = ""; }; - 062DA4492B15A001007A963B /* NotificationItemProxyProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationItemProxyProtocol.swift; sourceTree = ""; }; - 062DA44A2B15A001007A963B /* AttributedStringBuilderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderProtocol.swift; sourceTree = ""; }; - 062DA46D2B15A153007A963B /* UNNotificationRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = ""; }; - 062DA46E2B15A153007A963B /* NSESettingsProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSESettingsProtocol.swift; sourceTree = ""; }; - 062DA46F2B15A153007A963B /* DataProtectionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataProtectionManager.swift; sourceTree = ""; }; - 062DA4702B15A153007A963B /* NSELogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = ""; }; - 062DA4712B15A153007A963B /* NSEUserSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSEUserSession.swift; sourceTree = ""; }; 0646E1072B1084E00014B8DD /* NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NSE.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - 0646E1092B1084E00014B8DD /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 0646E10B2B1084E00014B8DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 0646E1132B108CF90014B8DD /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = ""; }; - 0646E1142B10A1160014B8DD /* KeychainSharingData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainSharingData.swift; sourceTree = ""; }; - 0646E1162B10A3EB0014B8DD /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; - 0646E1182B10A4EC0014B8DD /* MatrixHttpClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixHttpClient.swift; sourceTree = ""; }; 06AAB3E12ADE390500E09F51 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; 06AAB3E22ADE39B400E09F51 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Main.strings; sourceTree = ""; }; 06AAB3E32ADE39B400E09F51 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/LaunchScreen.strings; sourceTree = ""; }; @@ -319,9 +311,9 @@ 062DA42F2B159C2D007A963B /* KeychainAccess in Frameworks */, 062DA4262B159AE4007A963B /* DeviceKit in Frameworks */, 062DA4352B159CCB007A963B /* Version in Frameworks */, - 062DA4322B159C8A007A963B /* Compound in Frameworks */, 062DA46C2B15A09C007A963B /* OrderedCollections in Frameworks */, 062DA42C2B159B6E007A963B /* Kingfisher in Frameworks */, + 061C71952B21847C0087684C /* DesignKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -344,35 +336,95 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 061C714E2B2183DE0087684C /* Sources */ = { + isa = PBXGroup; + children = ( + 061C714F2B2183DE0087684C /* Other */, + 061C71552B2183DE0087684C /* NotificationServiceExtension.swift */, + 061C71562B2183DE0087684C /* NotificationContentBuilder.swift */, + ); + path = Sources; + sourceTree = ""; + }; + 061C714F2B2183DE0087684C /* Other */ = { + isa = PBXGroup; + children = ( + 061C71502B2183DE0087684C /* NSEUserSession.swift */, + 061C71512B2183DE0087684C /* NSESettings.swift */, + 061C71522B2183DE0087684C /* UNNotificationRequest.swift */, + 061C71532B2183DE0087684C /* DataProtectionManager.swift */, + 061C71542B2183DE0087684C /* NSELogger.swift */, + ); + path = Other; + sourceTree = ""; + }; + 061C71582B2183DE0087684C /* HTMLParsing */ = { + isa = PBXGroup; + children = ( + 061C71592B2183DE0087684C /* ElementXAttributeScope.swift */, + 061C715A2B2183DE0087684C /* AttributedStringBuilderProtocol.swift */, + 061C715B2B2183DE0087684C /* UIFont+AttributedStringBuilder.h */, + 061C715C2B2183DE0087684C /* AttributedStringBuilder.swift */, + 061C715D2B2183DE0087684C /* DTHTMLElement+AttributedStringBuilder.swift */, + 061C715E2B2183DE0087684C /* UIFont+AttributedStringBuilder.m */, + ); + path = HTMLParsing; + sourceTree = ""; + }; + 061C715F2B2183DE0087684C /* Logging */ = { + isa = PBXGroup; + children = ( + 061C71602B2183DE0087684C /* MXLog.swift */, + 061C71612B2183DE0087684C /* RustTracing.swift */, + 061C71622B2183DE0087684C /* MXLogger.swift */, + ); + path = Logging; + sourceTree = ""; + }; + 061C71632B2183DE0087684C /* Generated */ = { + isa = PBXGroup; + children = ( + 061C71642B2183DE0087684C /* Strings.swift */, + 061C71652B2183DE0087684C /* Assets.swift */, + 061C71662B2183DE0087684C /* Strings+Untranslated.swift */, + ); + path = Generated; + sourceTree = ""; + }; + 061C71672B2183DE0087684C /* Provider */ = { + isa = PBXGroup; + children = ( + 061C71682B2183DE0087684C /* MediaFileHandleProxy.swift */, + 061C71692B2183DE0087684C /* MediaProviderProtocol.swift */, + 061C716A2B2183DE0087684C /* MediaProvider.swift */, + 061C716B2B2183DE0087684C /* MediaLoader.swift */, + 061C716C2B2183DE0087684C /* MediaLoaderProtocol.swift */, + 061C716D2B2183DE0087684C /* ImageProviderProtocol.swift */, + 061C716E2B2183DE0087684C /* MediaSourceProxy.swift */, + 061C716F2B2183DE0087684C /* MockMediaProvider.swift */, + ); + path = Provider; + sourceTree = ""; + }; + 061C71702B2183DE0087684C /* Proxy */ = { + isa = PBXGroup; + children = ( + 061C71712B2183DE0087684C /* NotificationItemProxyProtocol.swift */, + 061C71722B2183DE0087684C /* NotificationItemProxy.swift */, + ); + path = Proxy; + sourceTree = ""; + }; 0646E1082B1084E00014B8DD /* NSE */ = { isa = PBXGroup; children = ( - 062DA46F2B15A153007A963B /* DataProtectionManager.swift */, - 062DA4702B15A153007A963B /* NSELogger.swift */, - 062DA46E2B15A153007A963B /* NSESettingsProtocol.swift */, - 062DA4712B15A153007A963B /* NSEUserSession.swift */, - 062DA46D2B15A153007A963B /* UNNotificationRequest.swift */, - 062DA43A2B15A001007A963B /* Assets.swift */, - 062DA4442B15A001007A963B /* AttributedStringBuilder.swift */, - 062DA44A2B15A001007A963B /* AttributedStringBuilderProtocol.swift */, - 062DA4482B15A001007A963B /* DTHTMLElement+AttributedStringBuilder.swift */, - 062DA43E2B15A001007A963B /* ElementXAttributeScope.swift */, - 062DA43C2B15A001007A963B /* ImageProviderProtocol.swift */, - 062DA4402B15A001007A963B /* MediaFileHandleProxy.swift */, - 062DA4432B15A001007A963B /* MediaLoader.swift */, - 062DA4452B15A001007A963B /* MediaLoaderProtocol.swift */, - 062DA43F2B15A001007A963B /* MediaProvider.swift */, - 062DA43B2B15A001007A963B /* MediaProviderProtocol.swift */, - 062DA43D2B15A001007A963B /* MediaSourceProxy.swift */, - 062DA4412B15A001007A963B /* MockMediaProvider.swift */, - 062DA4462B15A001007A963B /* MXLog.swift */, - 062DA4362B15A000007A963B /* MXLogger.swift */, - 062DA4422B15A001007A963B /* NotificationItemProxy.swift */, - 062DA4492B15A001007A963B /* NotificationItemProxyProtocol.swift */, - 062DA4472B15A001007A963B /* RustTracing.swift */, - 062DA4392B15A001007A963B /* Strings.swift */, - 062DA4382B15A000007A963B /* Strings+Untranslated.swift */, - 062DA3EF2B159902007A963B /* AppSettings.swift */, + 061C71632B2183DE0087684C /* Generated */, + 061C71582B2183DE0087684C /* HTMLParsing */, + 061C715F2B2183DE0087684C /* Logging */, + 061C71672B2183DE0087684C /* Provider */, + 061C71702B2183DE0087684C /* Proxy */, + 061C714E2B2183DE0087684C /* Sources */, + 061C71732B2183DE0087684C /* SharedUserDefaultsKeys.swift */, 062DA3F52B159903007A963B /* AttributedString.swift */, 062DA4012B159903007A963B /* AvatarSize.swift */, 062DA3FE2B159903007A963B /* BackgroundTaskProtocol.swift */, @@ -389,9 +441,7 @@ 062DA3EC2B159902007A963B /* NotificationConstants.swift */, 062DA3F42B159903007A963B /* NSRegularExpresion.swift */, 062DA3F12B159902007A963B /* PermalinkBuilder.swift */, - 062DA4022B159903007A963B /* PillConstants.swift */, 062DA3F32B159903007A963B /* PlaceholderAvatarImage.swift */, - 062DA3F22B159902007A963B /* PlainMentionBuilder.swift */, 062DA3F02B159902007A963B /* RestorationToken.swift */, 062DA3EE2B159902007A963B /* RoomMessageEventStringBuilder.swift */, 062DA3FD2B159903007A963B /* String.swift */, @@ -403,11 +453,6 @@ 062DA3EA2B159902007A963B /* UserPreference.swift */, 062DA4052B159903007A963B /* UTType.swift */, 0646E1132B108CF90014B8DD /* NSE.entitlements */, - 0646E1092B1084E00014B8DD /* NotificationService.swift */, - 0646E1142B10A1160014B8DD /* KeychainSharingData.swift */, - 0646E1162B10A3EB0014B8DD /* Event.swift */, - 0646E1182B10A4EC0014B8DD /* MatrixHttpClient.swift */, - 062DA3E62B1585C2007A963B /* NotificationContentBuilder.swift */, 0646E10B2B1084E00014B8DD /* Info.plist */, ); path = NSE; @@ -519,13 +564,13 @@ 062DA4282B159B3B007A963B /* Prefire */, 062DA42B2B159B6E007A963B /* Kingfisher */, 062DA42E2B159C2D007A963B /* KeychainAccess */, - 062DA4312B159C8A007A963B /* Compound */, 062DA4342B159CCB007A963B /* Version */, 062DA4612B15A03E007A963B /* DTCoreText */, 062DA4642B15A06A007A963B /* LRUCache */, 062DA4672B15A09C007A963B /* Collections */, 062DA4692B15A09C007A963B /* DequeModule */, 062DA46B2B15A09C007A963B /* OrderedCollections */, + 061C71942B21847C0087684C /* DesignKit */, ); productName = NSE; productReference = 0646E1072B1084E00014B8DD /* NSE.appex */; @@ -639,11 +684,11 @@ 062DA4272B159B3B007A963B /* XCRemoteSwiftPackageReference "Prefire" */, 062DA42A2B159B6E007A963B /* XCRemoteSwiftPackageReference "Kingfisher" */, 062DA42D2B159C2D007A963B /* XCRemoteSwiftPackageReference "KeychainAccess" */, - 062DA4302B159C8A007A963B /* XCRemoteSwiftPackageReference "compound-ios" */, 062DA4332B159CCB007A963B /* XCRemoteSwiftPackageReference "Version" */, 062DA4602B15A03E007A963B /* XCRemoteSwiftPackageReference "DTCoreText" */, 062DA4632B15A06A007A963B /* XCRemoteSwiftPackageReference "LRUCache" */, 062DA4662B15A09C007A963B /* XCRemoteSwiftPackageReference "swift-collections" */, + 061C71932B21847C0087684C /* XCLocalSwiftPackageReference "NSE/DesignKit" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -787,64 +832,60 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 062DA4592B15A001007A963B /* AttributedStringBuilder.swift in Sources */, - 062DA4512B15A001007A963B /* ImageProviderProtocol.swift in Sources */, - 062DA40D2B159903007A963B /* AppSettings.swift in Sources */, 062DA4232B159903007A963B /* UTType.swift in Sources */, - 062DA4762B15A154007A963B /* NSEUserSession.swift in Sources */, 062DA4122B159903007A963B /* NSRegularExpresion.swift in Sources */, - 062DA4562B15A001007A963B /* MockMediaProvider.swift in Sources */, 062DA4092B159903007A963B /* KeychainControllerProtocol.swift in Sources */, + 061C718D2B2183DE0087684C /* MediaSourceProxy.swift in Sources */, + 061C71882B2183DE0087684C /* MediaProviderProtocol.swift in Sources */, 062DA41B2B159903007A963B /* String.swift in Sources */, + 061C71902B2183DE0087684C /* NotificationItemProxy.swift in Sources */, 062DA40E2B159903007A963B /* RestorationToken.swift in Sources */, - 062DA4722B15A154007A963B /* UNNotificationRequest.swift in Sources */, - 062DA4532B15A001007A963B /* ElementXAttributeScope.swift in Sources */, + 061C71792B2183DE0087684C /* NotificationServiceExtension.swift in Sources */, + 061C717E2B2183DE0087684C /* AttributedStringBuilder.swift in Sources */, 062DA4132B159903007A963B /* AttributedString.swift in Sources */, - 062DA44E2B15A001007A963B /* Strings.swift in Sources */, - 062DA4752B15A154007A963B /* NSELogger.swift in Sources */, - 062DA4502B15A001007A963B /* MediaProviderProtocol.swift in Sources */, + 061C71852B2183DE0087684C /* Assets.swift in Sources */, 062DA40F2B159903007A963B /* PermalinkBuilder.swift in Sources */, - 0646E1152B10A1160014B8DD /* KeychainSharingData.swift in Sources */, - 0646E1192B10A4EC0014B8DD /* MatrixHttpClient.swift in Sources */, - 062DA45D2B15A001007A963B /* DTHTMLElement+AttributedStringBuilder.swift in Sources */, 062DA4072B159903007A963B /* Date.swift in Sources */, + 061C71742B2183DE0087684C /* NSEUserSession.swift in Sources */, 062DA4192B159903007A963B /* UserAgentBuilder.swift in Sources */, 062DA41F2B159903007A963B /* AvatarSize.swift in Sources */, + 061C71892B2183DE0087684C /* MediaProvider.swift in Sources */, 062DA40C2B159903007A963B /* RoomMessageEventStringBuilder.swift in Sources */, - 062DA3E72B1585C2007A963B /* NotificationContentBuilder.swift in Sources */, - 062DA4102B159903007A963B /* PlainMentionBuilder.swift in Sources */, 062DA41E2B159903007A963B /* TestablePreview.swift in Sources */, - 062DA44F2B15A001007A963B /* Assets.swift in Sources */, - 0646E1172B10A3EB0014B8DD /* Event.swift in Sources */, - 062DA4542B15A001007A963B /* MediaProvider.swift in Sources */, - 062DA4742B15A154007A963B /* DataProtectionManager.swift in Sources */, - 062DA4732B15A154007A963B /* NSESettingsProtocol.swift in Sources */, - 062DA45E2B15A001007A963B /* NotificationItemProxyProtocol.swift in Sources */, - 062DA4582B15A001007A963B /* MediaLoader.swift in Sources */, 062DA4142B159903007A963B /* Bundle.swift in Sources */, 062DA4212B159903007A963B /* FileManager.swift in Sources */, + 061C718E2B2183DE0087684C /* MockMediaProvider.swift in Sources */, + 061C71772B2183DE0087684C /* DataProtectionManager.swift in Sources */, + 061C71912B2183DE0087684C /* SharedUserDefaultsKeys.swift in Sources */, + 061C71782B2183DE0087684C /* NSELogger.swift in Sources */, + 061C717F2B2183DE0087684C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */, + 061C71862B2183DE0087684C /* Strings+Untranslated.swift in Sources */, 062DA41C2B159903007A963B /* BackgroundTaskProtocol.swift in Sources */, - 062DA45F2B15A001007A963B /* AttributedStringBuilderProtocol.swift in Sources */, + 061C718F2B2183DE0087684C /* NotificationItemProxyProtocol.swift in Sources */, + 061C717C2B2183DE0087684C /* ElementXAttributeScope.swift in Sources */, 062DA4082B159903007A963B /* UserPreference.swift in Sources */, + 061C717A2B2183DE0087684C /* NotificationContentBuilder.swift in Sources */, 062DA4172B159903007A963B /* ImageCache.swift in Sources */, - 062DA4572B15A001007A963B /* NotificationItemProxy.swift in Sources */, - 062DA45C2B15A001007A963B /* RustTracing.swift in Sources */, - 062DA45B2B15A001007A963B /* MXLog.swift in Sources */, - 062DA4522B15A001007A963B /* MediaSourceProxy.swift in Sources */, 062DA4182B159903007A963B /* Task.swift in Sources */, + 061C71802B2183DE0087684C /* UIFont+AttributedStringBuilder.m in Sources */, + 061C71762B2183DE0087684C /* UNNotificationRequest.swift in Sources */, + 061C717D2B2183DE0087684C /* AttributedStringBuilderProtocol.swift in Sources */, + 061C71812B2183DE0087684C /* MXLog.swift in Sources */, + 061C71832B2183DE0087684C /* MXLogger.swift in Sources */, + 061C71752B2183DE0087684C /* NSESettings.swift in Sources */, 062DA41D2B159903007A963B /* KeychainController.swift in Sources */, 062DA40B2B159903007A963B /* BackgroundTaskServiceProtocol.swift in Sources */, - 062DA44B2B15A001007A963B /* MXLogger.swift in Sources */, - 0646E10A2B1084E00014B8DD /* NotificationService.swift in Sources */, - 062DA4552B15A001007A963B /* MediaFileHandleProxy.swift in Sources */, - 062DA45A2B15A001007A963B /* MediaLoaderProtocol.swift in Sources */, + 061C71842B2183DE0087684C /* Strings.swift in Sources */, + 061C718B2B2183DE0087684C /* MediaLoaderProtocol.swift in Sources */, 062DA4152B159903007A963B /* URL.swift in Sources */, 062DA40A2B159903007A963B /* NotificationConstants.swift in Sources */, 062DA4112B159903007A963B /* PlaceholderAvatarImage.swift in Sources */, + 061C71872B2183DE0087684C /* MediaFileHandleProxy.swift in Sources */, 062DA4222B159903007A963B /* UNNotificationContent.swift in Sources */, - 062DA44D2B15A001007A963B /* Strings+Untranslated.swift in Sources */, - 062DA4202B159903007A963B /* PillConstants.swift in Sources */, + 061C718C2B2183DE0087684C /* ImageProviderProtocol.swift in Sources */, + 061C71822B2183DE0087684C /* RustTracing.swift in Sources */, 062DA41A2B159903007A963B /* MatrixEntityRegex.swift in Sources */, + 061C718A2B2183DE0087684C /* MediaLoader.swift in Sources */, 062DA4062B159903007A963B /* InfoPlistReader.swift in Sources */, 062DA4162B159903007A963B /* LayoutDirection.swift in Sources */, ); @@ -855,7 +896,6 @@ buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 06ED217C2B16D83000363DF5 /* KeychainSharingData.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1562,13 +1602,20 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 061C71932B21847C0087684C /* XCLocalSwiftPackageReference "NSE/DesignKit" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = NSE/DesignKit; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCRemoteSwiftPackageReference section */ 062DA3DB2B1580B4007A963B /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift"; requirement = { - branch = main; - kind = branch; + kind = exactVersion; + version = 1.1.20; }; }; 062DA4242B159AE4007A963B /* XCRemoteSwiftPackageReference "DeviceKit" */ = { @@ -1603,14 +1650,6 @@ kind = branch; }; }; - 062DA4302B159C8A007A963B /* XCRemoteSwiftPackageReference "compound-ios" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/vector-im/compound-ios"; - requirement = { - branch = main; - kind = branch; - }; - }; 062DA4332B159CCB007A963B /* XCRemoteSwiftPackageReference "Version" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/mxcl/Version"; @@ -1646,6 +1685,10 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 061C71942B21847C0087684C /* DesignKit */ = { + isa = XCSwiftPackageProductDependency; + productName = DesignKit; + }; 062DA3DC2B15813A007A963B /* MatrixRustSDK */ = { isa = XCSwiftPackageProductDependency; package = 062DA3DB2B1580B4007A963B /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; @@ -1671,11 +1714,6 @@ package = 062DA42D2B159C2D007A963B /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; - 062DA4312B159C8A007A963B /* Compound */ = { - isa = XCSwiftPackageProductDependency; - package = 062DA4302B159C8A007A963B /* XCRemoteSwiftPackageReference "compound-ios" */; - productName = Compound; - }; 062DA4342B159CCB007A963B /* Version */ = { isa = XCSwiftPackageProductDependency; package = 062DA4332B159CCB007A963B /* XCRemoteSwiftPackageReference "Version" */; diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9beedfb70c..f33ac28748 100644 --- a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vector-im/compound-design-tokens.git", "state" : { - "revision" : "b603371c5e4ac798f4613a7388d2305100b31911", - "version" : "0.0.7" + "revision" : "387d2b7211f07761c67e849c59414a1bb803defa" } }, { @@ -14,8 +13,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vector-im/compound-ios", "state" : { - "branch" : "main", - "revision" : "0fcc7ecae868f7015789275580c4be458ad4f546" + "revision" : "e8c097e545a06a2ef3036af33192a07c58fafd1b" } }, { @@ -45,6 +43,15 @@ "version" : "1.7.18" } }, + { + "identity" : "element-design-tokens", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vector-im/element-design-tokens", + "state" : { + "revision" : "63e40f10b336c136d6d05f7967e4565e37d3d760", + "version" : "0.0.3" + } + }, { "identity" : "keychainaccess", "kind" : "remoteSourceControl", @@ -77,8 +84,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-rust-components-swift", "state" : { - "branch" : "main", - "revision" : "42274cc2414e675b246432b037e7fa82b587fd97" + "revision" : "5f3a195f6a461b4d40a8a705a3af8235711f12f5", + "version" : "1.1.20" } }, { From af8bf56f857354f60c641d8aba20108899c73a28 Mon Sep 17 00:00:00 2001 From: MinhDV Date: Thu, 30 Nov 2023 09:48:35 +0700 Subject: [PATCH 4/6] TW-1049 Support localizations --- ios/Runner/en.lproj/Localizable.strings | 678 ++++++++++++++++++ ios/Runner/en.lproj/Localizable.stringsdict | 230 ++++++ ios/Runner/fr.lproj/Localizable.strings | 668 +++++++++++++++++ ios/Runner/fr.lproj/Localizable.stringsdict | 230 ++++++ ios/Runner/ru-RU.lproj/Localizable.strings | 670 ++++++++++++++++- .../ru-RU.lproj/Localizable.stringsdict | 258 +++++++ 6 files changed, 2733 insertions(+), 1 deletion(-) create mode 100644 ios/Runner/en.lproj/Localizable.stringsdict create mode 100644 ios/Runner/fr.lproj/Localizable.stringsdict create mode 100644 ios/Runner/ru-RU.lproj/Localizable.stringsdict diff --git a/ios/Runner/en.lproj/Localizable.strings b/ios/Runner/en.lproj/Localizable.strings index 18d97c2da4..e8b58d7159 100644 --- a/ios/Runner/en.lproj/Localizable.strings +++ b/ios/Runner/en.lproj/Localizable.strings @@ -1 +1,679 @@ "newMessageInTwake" = "You have 1 encrypted message"; +"Notification" = "Notification"; +"a11y_delete" = "Delete"; +"a11y_hide_password" = "Hide password"; +"a11y_jump_to_bottom" = "Jump to bottom"; +"a11y_notifications_mentions_only" = "Mentions only"; +"a11y_notifications_muted" = "Muted"; +"a11y_page_n" = "Page %1$d"; +"a11y_pause" = "Pause"; +"a11y_pin_field" = "PIN field"; +"a11y_play" = "Play"; +"a11y_poll" = "Poll"; +"a11y_poll_end" = "Ended poll"; +"a11y_react_with" = "React with %1$@"; +"a11y_react_with_other_emojis" = "React with other emojis"; +"a11y_read_receipts_multiple" = "Read by %1$@ and %2$@"; +"a11y_read_receipts_single" = "Read by %1$@"; +"a11y_read_receipts_tap_to_show_all" = "Tap to show all"; +"a11y_remove_reaction_with" = "Remove reaction with %1$@"; +"a11y_send_files" = "Send files"; +"a11y_show_password" = "Show password"; +"a11y_start_call" = "Start a call"; +"a11y_user_menu" = "User menu"; +"a11y_voice_message_record" = "Record voice message."; +"a11y_voice_message_stop_recording" = "Stop recording"; +"action_accept" = "Accept"; +"action_add_to_timeline" = "Add to timeline"; +"action_back" = "Back"; +"action_cancel" = "Cancel"; +"action_choose_photo" = "Choose photo"; +"action_clear" = "Clear"; +"action_close" = "Close"; +"action_complete_verification" = "Complete verification"; +"action_confirm" = "Confirm"; +"action_continue" = "Continue"; +"action_copy" = "Copy"; +"action_copy_link" = "Copy link"; +"action_copy_link_to_message" = "Copy link to message"; +"action_create" = "Create"; +"action_create_a_room" = "Create a room"; +"action_decline" = "Decline"; +"action_delete_poll" = "Delete Poll"; +"action_disable" = "Disable"; +"action_done" = "Done"; +"action_edit" = "Edit"; +"action_edit_poll" = "Edit poll"; +"action_enable" = "Enable"; +"action_end_poll" = "End poll"; +"action_enter_pin" = "Enter PIN"; +"action_forgot_password" = "Forgot password?"; +"action_forward" = "Forward"; +"action_invite" = "Invite"; +"action_invite_friends" = "Invite friends"; +"action_invite_friends_to_app" = "Invite friends to %1$@"; +"action_invite_people_to_app" = "Invite people to %1$@"; +"action_invites_list" = "Invites"; +"action_join" = "Join"; +"action_learn_more" = "Learn more"; +"action_leave" = "Leave"; +"action_leave_room" = "Leave room"; +"action_manage_account" = "Manage account"; +"action_manage_devices" = "Manage devices"; +"action_next" = "Next"; +"action_no" = "No"; +"action_not_now" = "Not now"; +"action_ok" = "OK"; +"action_open_settings" = "Settings"; +"action_open_with" = "Open with"; +"action_quick_reply" = "Quick reply"; +"action_quote" = "Quote"; +"action_react" = "React"; +"action_remove" = "Remove"; +"action_reply" = "Reply"; +"action_reply_in_thread" = "Reply in thread"; +"action_report_bug" = "Report bug"; +"action_report_content" = "Report content"; +"action_retry" = "Retry"; +"action_retry_decryption" = "Retry decryption"; +"action_save" = "Save"; +"action_search" = "Search"; +"action_send" = "Send"; +"action_send_message" = "Send message"; +"action_share" = "Share"; +"action_share_link" = "Share link"; +"action_sign_in_again" = "Sign in again"; +"action_signout" = "Sign out"; +"action_signout_anyway" = "Sign out anyway"; +"action_skip" = "Skip"; +"action_start" = "Start"; +"action_start_chat" = "Start chat"; +"action_start_verification" = "Start verification"; +"action_static_map_load" = "Tap to load map"; +"action_take_photo" = "Take photo"; +"action_tap_for_options" = "Tap for options"; +"action_try_again" = "Try again"; +"action_view_source" = "View source"; +"action_yes" = "Yes"; +"common_about" = "About"; +"common_acceptable_use_policy" = "Acceptable use policy"; +"common_advanced_settings" = "Advanced settings"; +"common_analytics" = "Analytics"; +"common_appearance" = "Appearance"; +"common_audio" = "Audio"; +"common_bubbles" = "Bubbles"; +"common_chat_backup" = "Chat backup"; +"common_copyright" = "Copyright"; +"common_creating_room" = "Creating room…"; +"common_current_user_left_room" = "Left room"; +"common_dark" = "Dark"; +"common_decryption_error" = "Decryption error"; +"common_developer_options" = "Developer options"; +"common_edited_suffix" = "(edited)"; +"common_editing" = "Editing"; +"common_emote" = "* %1$@ %2$@"; +"common_encryption_enabled" = "Encryption enabled"; +"common_enter_your_pin" = "Enter your PIN"; +"common_error" = "Error"; +"common_everyone" = "Everyone"; +"common_face_id_ios" = "Face ID"; +"common_file" = "File"; +"common_forward_message" = "Forward message"; +"common_gif" = "GIF"; +"common_image" = "Image"; +"common_in_reply_to" = "In reply to %1$@"; +"common_invite_unknown_profile" = "This Matrix ID can't be found, so the invite might not be received."; +"common_leaving_room" = "Leaving room"; +"common_light" = "Light"; +"common_link_copied_to_clipboard" = "Link copied to clipboard"; +"common_loading" = "Loading…"; +"common_message" = "Message"; +"common_message_actions" = "Message actions"; +"common_message_layout" = "Message layout"; +"common_message_removed" = "Message removed"; +"common_modern" = "Modern"; +"common_mute" = "Mute"; +"common_no_results" = "No results"; +"common_offline" = "Offline"; +"common_optic_id_ios" = "Optic ID"; +"common_password" = "Password"; +"common_people" = "People"; +"common_permalink" = "Permalink"; +"common_permission" = "Permission"; +"common_poll_total_votes" = "Total votes: %1$@"; +"common_poll_undisclosed_text" = "Results will show after the poll has ended"; +"common_privacy_policy" = "Privacy policy"; +"common_reaction" = "Reaction"; +"common_reactions" = "Reactions"; +"common_recovery_key" = "Recovery key"; +"common_refreshing" = "Refreshing…"; +"common_replying_to" = "Replying to %1$@"; +"common_report_a_bug" = "Report a bug"; +"common_report_a_problem" = "Report a problem"; +"common_report_submitted" = "Report submitted"; +"common_rich_text_editor" = "Rich text editor"; +"common_room" = "Room"; +"common_room_name" = "Room name"; +"common_room_name_placeholder" = "e.g. your project name"; +"common_screen_lock" = "Screen lock"; +"common_search_for_someone" = "Search for someone"; +"common_search_results" = "Search results"; +"common_security" = "Security"; +"common_seen_by" = "Seen by"; +"common_sending" = "Sending…"; +"common_sending_failed" = "Sending failed"; +"common_sent" = "Sent"; +"common_server_not_supported" = "Server not supported"; +"common_server_url" = "Server URL"; +"common_settings" = "Settings"; +"common_shared_location" = "Shared location"; +"common_signing_out" = "Signing out"; +"common_starting_chat" = "Starting chat…"; +"common_sticker" = "Sticker"; +"common_success" = "Success"; +"common_suggestions" = "Suggestions"; +"common_syncing" = "Syncing"; +"common_system" = "System"; +"common_text" = "Text"; +"common_third_party_notices" = "Third-party notices"; +"common_thread" = "Thread"; +"common_topic" = "Topic"; +"common_topic_placeholder" = "What is this room about?"; +"common_touch_id_ios" = "Touch ID"; +"common_unable_to_decrypt" = "Unable to decrypt"; +"common_unable_to_invite_message" = "Invites couldn't be sent to one or more users."; +"common_unable_to_invite_title" = "Unable to send invite(s)"; +"common_unlock" = "Unlock"; +"common_unmute" = "Unmute"; +"common_unsupported_event" = "Unsupported event"; +"common_username" = "Username"; +"common_verification_cancelled" = "Verification cancelled"; +"common_verification_complete" = "Verification complete"; +"common_video" = "Video"; +"common_voice_message" = "Voice message"; +"common_waiting" = "Waiting…"; +"common_waiting_for_decryption_key" = "Waiting for this message"; +"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?"; +"dialog_permission_camera" = "In order to let the application use the camera, please grant the permission in the system settings."; +"dialog_permission_generic" = "Please grant the permission in the system settings."; +"dialog_permission_location_description_ios" = "Grant access in Settings -> Location."; +"dialog_permission_location_title_ios" = "%1$@ does not have access to your location."; +"dialog_permission_microphone" = "In order to let the application use the microphone, please grant the permission in the system settings."; +"dialog_permission_microphone_description_ios" = "Grant access so you can record and send messages with audio."; +"dialog_permission_microphone_title_ios" = "%1$@ needs permission to access your microphone."; +"dialog_permission_notification" = "In order to let the application display notifications, please grant the permission in the system settings."; +"dialog_title_confirmation" = "Confirmation"; +"dialog_title_warning" = "Warning"; +"emoji_picker_category_activity" = "Activities"; +"emoji_picker_category_flags" = "Flags"; +"emoji_picker_category_foods" = "Food & Drink"; +"emoji_picker_category_nature" = "Animals & Nature"; +"emoji_picker_category_objects" = "Objects"; +"emoji_picker_category_people" = "Smileys & People"; +"emoji_picker_category_places" = "Travel & Places"; +"emoji_picker_category_symbols" = "Symbols"; +"error_failed_creating_the_permalink" = "Failed creating the permalink"; +"error_failed_loading_map" = "%1$@ could not load the map. Please try again later."; +"error_failed_loading_messages" = "Failed loading messages"; +"error_failed_locating_user" = "%1$@ could not access your location. Please try again later."; +"error_failed_uploading_voice_message" = "Failed to upload your voice message."; +"error_no_compatible_app_found" = "No compatible app was found to handle this action."; +"error_some_messages_have_not_been_sent" = "Some messages have not been sent"; +"error_unknown" = "Sorry, an error occurred"; +"invite_friends_rich_title" = "🔐️ Join me on %1$@"; +"invite_friends_text" = "Hey, talk to me on %1$@: %2$@"; +"leave_room_alert_empty_subtitle" = "Are you sure that you want to leave this room? You're the only person here. If you leave, no one will be able to join in the future, including you."; +"leave_room_alert_private_subtitle" = "Are you sure that you want to leave this room? This room is not public and you won't be able to rejoin without an invite."; +"leave_room_alert_subtitle" = "Are you sure that you want to leave the room?"; +"login_initial_device_name_ios" = "%1$@ iOS"; +"notification_channel_call" = "Call"; +"notification_channel_listening_for_events" = "Listening for events"; +"notification_channel_noisy" = "Noisy notifications"; +"notification_channel_silent" = "Silent notifications"; +"notification_inline_reply_failed" = "** Failed to send - please open room"; +"notification_invitation_action_join" = "Join"; +"notification_invitation_action_reject" = "Reject"; +"notification_invite_body" = "Invited you to chat"; +"notification_mentioned_you_body" = "Mentioned you: %1$@"; +"notification_new_messages" = "New Messages"; +"notification_reaction_body" = "Reacted with %1$@"; +"notification_room_action_mark_as_read" = "Mark as read"; +"notification_room_invite_body" = "Invited you to join the room"; +"notification_sender_me" = "Me"; +"notification_test_push_notification_content" = "You are viewing the notification! Click me!"; +"notification_ticker_text_dm" = "%1$@: %2$@"; +"notification_ticker_text_group" = "%1$@: %2$@ %3$@"; +"notification_unread_notified_messages_and_invitation" = "%1$@ and %2$@"; +"notification_unread_notified_messages_in_room" = "%1$@ in %2$@"; +"notification_unread_notified_messages_in_room_and_invitation" = "%1$@ in %2$@ and %3$@"; +"preference_rageshake" = "Rageshake to report bug"; +"rageshake_detection_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"; +"report_content_explanation" = "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages."; +"report_content_hint" = "Reason for reporting this content"; +"rich_text_editor_bullet_list" = "Toggle bullet list"; +"rich_text_editor_close_formatting_options" = "Close formatting options"; +"rich_text_editor_code_block" = "Toggle code block"; +"rich_text_editor_composer_placeholder" = "Message…"; +"rich_text_editor_create_link" = "Create a link"; +"rich_text_editor_edit_link" = "Edit link"; +"rich_text_editor_format_bold" = "Apply bold format"; +"rich_text_editor_format_italic" = "Apply italic format"; +"rich_text_editor_format_strikethrough" = "Apply strikethrough format"; +"rich_text_editor_format_underline" = "Apply underline format"; +"rich_text_editor_full_screen_toggle" = "Toggle full screen mode"; +"rich_text_editor_indent" = "Indent"; +"rich_text_editor_inline_code" = "Apply inline code format"; +"rich_text_editor_link" = "Set link"; +"rich_text_editor_numbered_list" = "Toggle numbered list"; +"rich_text_editor_open_compose_options" = "Open compose options"; +"rich_text_editor_quote" = "Toggle quote"; +"rich_text_editor_remove_link" = "Remove link"; +"rich_text_editor_unindent" = "Unindent"; +"rich_text_editor_url_placeholder" = "Link"; +"rich_text_editor_a11y_add_attachment" = "Add attachment"; +"room_timeline_beginning_of_room" = "This is the beginning of %1$@."; +"room_timeline_beginning_of_room_no_name" = "This is the beginning of this conversation."; +"room_timeline_read_marker_title" = "New"; +"screen_advanced_settings_element_call_base_url" = "Custom Element Call base URL"; +"screen_advanced_settings_element_call_base_url_description" = "Set a custom base URL for Element Call."; +"screen_advanced_settings_element_call_base_url_validation_error" = "Invalid URL, please make sure you include the protocol (http/https) and the correct address."; +"screen_room_mentions_at_room_subtitle" = "Notify the whole room"; +"screen_account_provider_change" = "Change account provider"; +"screen_account_provider_form_hint" = "Homeserver address"; +"screen_account_provider_form_notice" = "Enter a search term or a domain address."; +"screen_account_provider_form_subtitle" = "Search for a company, community, or private server."; +"screen_account_provider_form_title" = "Find an account provider"; +"screen_account_provider_signin_subtitle" = "This is where your conversations will live — just like you would use an email provider to keep your emails."; +"screen_account_provider_signin_title" = "You’re about to sign in to %@"; +"screen_account_provider_signup_subtitle" = "This is where your conversations will live — just like you would use an email provider to keep your emails."; +"screen_account_provider_signup_title" = "You’re about to create an account on %@"; +"screen_advanced_settings_developer_mode" = "Developer mode"; +"screen_advanced_settings_developer_mode_description" = "Enable to have access to features and functionality for developers."; +"screen_advanced_settings_rich_text_editor_description" = "Disable the rich text editor to type Markdown manually."; +"screen_advanced_settings_view_source_description" = "Enable option to view message source in the timeline."; +"screen_analytics_prompt_data_usage" = "We won't record or profile any personal data"; +"screen_analytics_prompt_help_us_improve" = "Share anonymous usage data to help us identify issues."; +"screen_analytics_prompt_read_terms" = "You can read all our terms %1$@."; +"screen_analytics_prompt_read_terms_content_link" = "here"; +"screen_analytics_prompt_settings" = "You can turn this off anytime"; +"screen_analytics_prompt_third_party_sharing" = "We won't share your data with third parties"; +"screen_analytics_prompt_title" = "Help improve %1$@"; +"screen_analytics_settings_share_data" = "Share analytics data"; +"screen_app_lock_biometric_authentication" = "biometric authentication"; +"screen_app_lock_biometric_unlock" = "biometric unlock"; +"screen_app_lock_biometric_unlock_reason_ios" = "Authentication is needed to access your app"; +"screen_app_lock_forgot_pin" = "Forgot PIN?"; +"screen_app_lock_settings_change_pin" = "Change PIN code"; +"screen_app_lock_settings_enable_biometric_unlock" = "Allow biometric unlock"; +"screen_app_lock_settings_enable_face_id_ios" = "Allow Face ID"; +"screen_app_lock_settings_enable_optic_id_ios" = "Allow Optic ID"; +"screen_app_lock_settings_enable_touch_id_ios" = "Allow Touch ID"; +"screen_app_lock_settings_remove_pin" = "Remove PIN"; +"screen_app_lock_settings_remove_pin_alert_message" = "Are you sure you want to remove PIN?"; +"screen_app_lock_settings_remove_pin_alert_title" = "Remove PIN?"; +"screen_app_lock_setup_biometric_unlock_allow_title" = "Allow %1$@"; +"screen_app_lock_setup_biometric_unlock_skip" = "I’d rather use PIN"; +"screen_app_lock_setup_biometric_unlock_subtitle" = "Save yourself some time and use %1$@ to unlock the app each time"; +"screen_app_lock_setup_choose_pin" = "Choose PIN"; +"screen_app_lock_setup_confirm_pin" = "Confirm PIN"; +"screen_app_lock_setup_pin_blacklisted_dialog_content" = "You cannot choose this as your PIN code for security reasons"; +"screen_app_lock_setup_pin_blacklisted_dialog_title" = "Choose a different PIN"; +"screen_app_lock_setup_pin_context" = "Lock %1$@ to add extra security to your chats.\n\nChoose something memorable. If you forget this PIN, you will be logged out of the app."; +"screen_app_lock_setup_pin_mismatch_dialog_content" = "Please enter the same PIN twice"; +"screen_app_lock_setup_pin_mismatch_dialog_title" = "PINs don't match"; +"screen_app_lock_signout_alert_message" = "You’ll need to re-login and create a new PIN to proceed"; +"screen_app_lock_signout_alert_title" = "You are being signed out"; +"screen_bug_report_attach_screenshot" = "Attach screenshot"; +"screen_bug_report_contact_me" = "You may contact me if you have any follow up questions."; +"screen_bug_report_contact_me_title" = "Contact me"; +"screen_bug_report_edit_screenshot" = "Edit screenshot"; +"screen_bug_report_editor_description" = "Please describe the problem. What did you do? What did you expect to happen? What actually happened. Please go into as much detail as you can."; +"screen_bug_report_editor_placeholder" = "Describe the problem…"; +"screen_bug_report_editor_supporting" = "If possible, please write the description in English."; +"screen_bug_report_include_crash_logs" = "Send crash logs"; +"screen_bug_report_include_logs" = "Allow logs"; +"screen_bug_report_include_screenshot" = "Send screenshot"; +"screen_bug_report_logs_description" = "Logs will be included with your message to make sure that everything is working properly. To send your message without logs, turn off this setting."; +"screen_change_account_provider_matrix_org_subtitle" = "Matrix.org is a large, free server on the public Matrix network for secure, decentralised communication, run by the Matrix.org Foundation."; +"screen_change_account_provider_other" = "Other"; +"screen_change_account_provider_subtitle" = "Use a different account provider, such as your own private server or a work account."; +"screen_change_account_provider_title" = "Change account provider"; +"screen_change_server_error_invalid_homeserver" = "We couldn't reach this homeserver. Please check that you have entered the homeserver URL correctly. If the URL is correct, contact your homeserver administrator for further help."; +"screen_change_server_error_no_sliding_sync_message" = "This server currently doesn’t support sliding sync."; +"screen_change_server_form_header" = "Homeserver URL"; +"screen_change_server_form_notice" = "You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$@"; +"screen_change_server_subtitle" = "What is the address of your server?"; +"screen_change_server_title" = "Select your server"; +"screen_chat_backup_key_backup_action_disable" = "Turn off backup"; +"screen_chat_backup_key_backup_action_enable" = "Turn on backup"; +"screen_chat_backup_key_backup_description" = "Backup ensures that you don't lose your message history. %1$@."; +"screen_chat_backup_key_backup_title" = "Backup"; +"screen_chat_backup_recovery_action_change" = "Change recovery key"; +"screen_chat_backup_recovery_action_confirm" = "Confirm recovery key"; +"screen_chat_backup_recovery_action_confirm_description" = "Your chat backup is currently out of sync."; +"screen_chat_backup_recovery_action_setup" = "Set up recovery"; +"screen_chat_backup_recovery_action_setup_description" = "Get access to your encrypted messages if you lose all your devices or are signed out of %1$@ everywhere."; +"screen_create_poll_add_option_btn" = "Add option"; +"screen_create_poll_anonymous_desc" = "Show results only after poll ends"; +"screen_create_poll_anonymous_headline" = "Hide votes"; +"screen_create_poll_answer_hint" = "Option %1$d"; +"screen_create_poll_cancel_confirmation_content_ios" = "Your changes won’t be saved"; +"screen_create_poll_cancel_confirmation_title_ios" = "Cancel Poll"; +"screen_create_poll_question_desc" = "Question or topic"; +"screen_create_poll_question_hint" = "What is the poll about?"; +"screen_create_poll_title" = "Create Poll"; +"screen_create_room_action_create_room" = "New room"; +"screen_create_room_action_invite_people" = "Invite friends to Element"; +"screen_create_room_add_people_title" = "Invite people"; +"screen_create_room_error_creating_room" = "An error occurred when creating the room"; +"screen_create_room_private_option_description" = "Messages in this room are encrypted. Encryption can’t be disabled afterwards."; +"screen_create_room_private_option_title" = "Private room (invite only)"; +"screen_create_room_public_option_description" = "Messages are not encrypted and anyone can read them. You can enable encryption at a later date."; +"screen_create_room_public_option_title" = "Public room (anyone)"; +"screen_create_room_room_name_label" = "Room name"; +"screen_create_room_topic_label" = "Topic (optional)"; +"screen_edit_poll_delete_confirmation" = "Are you sure you want to delete this poll?"; +"screen_edit_poll_delete_confirmation_title" = "Delete Poll"; +"screen_edit_poll_title" = "Edit poll"; +"screen_edit_profile_display_name" = "Display name"; +"screen_edit_profile_display_name_placeholder" = "Your display name"; +"screen_edit_profile_error" = "An unknown error was encountered and the information couldn't be changed."; +"screen_edit_profile_error_title" = "Unable to update profile"; +"screen_edit_profile_title" = "Edit profile"; +"screen_edit_profile_updating_details" = "Updating profile…"; +"screen_invites_decline_chat_message" = "Are you sure you want to decline the invitation to join %1$@?"; +"screen_invites_decline_chat_title" = "Decline invite"; +"screen_invites_decline_direct_chat_message" = "Are you sure you want to decline this private chat with %1$@?"; +"screen_invites_decline_direct_chat_title" = "Decline chat"; +"screen_invites_empty_list" = "No Invites"; +"screen_invites_invited_you" = "%1$@ (%2$@) invited you"; +"screen_key_backup_disable_confirmation_action_turn_off" = "Turn off"; +"screen_key_backup_disable_confirmation_description" = "You will lose your encrypted messages if you are signed out of all devices."; +"screen_key_backup_disable_confirmation_title" = "Are you sure you want to turn off backup?"; +"screen_key_backup_disable_description" = "Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:"; +"screen_key_backup_disable_description_point_1" = "Not have encrypted message history on new devices"; +"screen_key_backup_disable_description_point_2" = "Lose access to your encrypted messages if you are signed out of %1$@ everywhere"; +"screen_key_backup_disable_title" = "Are you sure you want to turn off backup?"; +"screen_login_error_deactivated_account" = "This account has been deactivated."; +"screen_login_error_invalid_credentials" = "Incorrect username and/or password"; +"screen_login_error_invalid_user_id" = "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’"; +"screen_login_error_unsupported_authentication" = "The selected homeserver doesn't support password or OIDC login. Please contact your admin or choose another homeserver."; +"screen_login_form_header" = "Enter your details"; +"screen_login_title" = "Welcome back!"; +"screen_login_title_with_homeserver" = "Sign in to %1$@"; +"screen_media_picker_error_failed_selection" = "Failed selecting media, please try again."; +"screen_media_upload_preview_error_failed_processing" = "Failed processing media to upload, please try again."; +"screen_media_upload_preview_error_failed_sending" = "Failed uploading media, please try again."; +"screen_migration_message" = "This is a one time process, thanks for waiting."; +"screen_migration_title" = "Setting up your account."; +"screen_notification_optin_subtitle" = "You can change your settings later."; +"screen_notification_optin_title" = "Allow notifications and never miss a message"; +"screen_notification_settings_additional_settings_section_title" = "Additional settings"; +"screen_notification_settings_calls_label" = "Audio and video calls"; +"screen_notification_settings_configuration_mismatch" = "Configuration mismatch"; +"screen_notification_settings_configuration_mismatch_description" = "We’ve simplified Notifications Settings to make options easier to find. Some custom settings you’ve chosen in the past are not shown here, but they’re still active.\n\nIf you proceed, some of your settings may change."; +"screen_notification_settings_direct_chats" = "Direct chats"; +"screen_notification_settings_edit_custom_settings_section_title" = "Custom setting per chat"; +"screen_notification_settings_edit_failed_updating_default_mode" = "An error occurred while updating the notification setting."; +"screen_notification_settings_edit_mode_all_messages" = "All messages"; +"screen_notification_settings_edit_mode_mentions_and_keywords" = "Mentions and Keywords only"; +"screen_notification_settings_edit_screen_direct_section_header" = "On direct chats, notify me for"; +"screen_notification_settings_edit_screen_group_section_header" = "On group chats, notify me for"; +"screen_notification_settings_enable_notifications" = "Enable notifications on this device"; +"screen_notification_settings_failed_fixing_configuration" = "The configuration has not been corrected, please try again."; +"screen_notification_settings_group_chats" = "Group chats"; +"screen_notification_settings_mentions_only_disclaimer" = "Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms."; +"screen_notification_settings_mentions_section_title" = "Mentions"; +"screen_notification_settings_mode_all" = "All"; +"screen_notification_settings_mode_mentions" = "Mentions"; +"screen_notification_settings_notification_section_title" = "Notify me for"; +"screen_notification_settings_room_mention_label" = "Notify me on @room"; +"screen_notification_settings_system_notifications_action_required" = "To receive notifications, please change your %1$@."; +"screen_notification_settings_system_notifications_action_required_content_link" = "system settings"; +"screen_notification_settings_system_notifications_turned_off" = "System notifications turned off"; +"screen_notification_settings_title" = "Notifications"; +"screen_onboarding_sign_in_manually" = "Sign in manually"; +"screen_onboarding_sign_in_with_qr_code" = "Sign in with QR code"; +"screen_onboarding_sign_up" = "Create account"; +"screen_onboarding_welcome_message" = "Welcome to the fastest Element ever. Supercharged for speed and simplicity."; +"screen_onboarding_welcome_subtitle" = "Welcome to %1$@. Supercharged, for speed and simplicity."; +"screen_onboarding_welcome_title" = "Be in your element"; +"screen_recovery_key_change_description" = "Get a new recovery key if you've lost your existing one. After changing your recovery key, your old one will no longer work."; +"screen_recovery_key_change_generate_key" = "Generate a new recovery key"; +"screen_recovery_key_change_generate_key_description" = "Make sure you can store your recovery key somewhere safe"; +"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"; +"screen_recovery_key_confirm_title" = "Confirm your recovery key"; +"screen_recovery_key_copied_to_clipboard" = "Copied recovery key"; +"screen_recovery_key_generating_key" = "Generating…"; +"screen_recovery_key_save_action" = "Save recovery key"; +"screen_recovery_key_save_description" = "Write down your recovery key somewhere safe or save it in a password manager."; +"screen_recovery_key_save_key_description" = "Tap to copy recovery key"; +"screen_recovery_key_save_title" = "Save your recovery key"; +"screen_recovery_key_setup_confirmation_description" = "You will not be able to access your new recovery key after this step."; +"screen_recovery_key_setup_confirmation_title" = "Have you saved your recovery key?"; +"screen_recovery_key_setup_description" = "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’."; +"screen_recovery_key_setup_generate_key" = "Generate your recovery key"; +"screen_recovery_key_setup_generate_key_description" = "Make sure you can store your recovery key somewhere safe"; +"screen_recovery_key_setup_success" = "Recovery setup successful"; +"screen_recovery_key_setup_title" = "Set up recovery"; +"screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user"; +"screen_room_attachment_source_camera" = "Camera"; +"screen_room_attachment_source_camera_photo" = "Take photo"; +"screen_room_attachment_source_camera_video" = "Record video"; +"screen_room_attachment_source_files" = "Attachment"; +"screen_room_attachment_source_gallery" = "Photo & Video Library"; +"screen_room_attachment_source_location" = "Location"; +"screen_room_attachment_source_poll" = "Poll"; +"screen_room_attachment_text_formatting" = "Text Formatting"; +"screen_room_details_add_topic_title" = "Add topic"; +"screen_room_details_already_a_member" = "Already a member"; +"screen_room_details_already_invited" = "Already invited"; +"screen_room_details_edit_room_title" = "Edit Room"; +"screen_room_details_edition_error" = "There was an unknown error and the information couldn't be changed."; +"screen_room_details_edition_error_title" = "Unable to update room"; +"screen_room_details_encryption_enabled_subtitle" = "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."; +"screen_room_details_encryption_enabled_title" = "Message encryption enabled"; +"screen_room_details_error_loading_notification_settings" = "An error occurred when loading notification settings."; +"screen_room_details_error_muting" = "Failed muting this room, please try again."; +"screen_room_details_error_unmuting" = "Failed unmuting this room, please try again."; +"screen_room_details_invite_people_title" = "Invite people"; +"screen_room_details_notification_mode_custom" = "Custom"; +"screen_room_details_notification_mode_default" = "Default"; +"screen_room_details_notification_title" = "Notifications"; +"screen_room_details_room_name_label" = "Room name"; +"screen_room_details_share_room_title" = "Share room"; +"screen_room_details_updating_room" = "Updating room…"; +"screen_room_encrypted_history_banner" = "Message history is currently unavailable."; +"screen_room_encrypted_history_banner_unverified" = "Message history is unavailable in this room. Verify this device to see your message history."; +"screen_room_error_failed_retrieving_user_details" = "Could not retrieve user details"; +"screen_room_invite_again_alert_message" = "Would you like to invite them back?"; +"screen_room_invite_again_alert_title" = "You are alone in this chat"; +"screen_room_member_details_block_alert_action" = "Block"; +"screen_room_member_details_block_alert_description" = "Blocked users won't be able to send you messages and all their messages will be hidden. You can unblock them anytime."; +"screen_room_member_details_block_user" = "Block user"; +"screen_room_member_details_unblock_alert_action" = "Unblock"; +"screen_room_member_details_unblock_alert_description" = "You'll be able to see all messages from them again."; +"screen_room_member_details_unblock_user" = "Unblock user"; +"screen_room_member_list_pending_header_title" = "Pending"; +"screen_room_member_list_room_members_header_title" = "Room members"; +"screen_room_message_copied" = "Message copied"; +"screen_room_no_permission_to_post" = "You do not have permission to post to this room"; +"screen_room_notification_settings_allow_custom" = "Allow custom setting"; +"screen_room_notification_settings_allow_custom_footnote" = "Turning this on will override your default setting"; +"screen_room_notification_settings_custom_settings_title" = "Notify me in this chat for"; +"screen_room_notification_settings_default_setting_footnote" = "You can change it in your %1$@."; +"screen_room_notification_settings_default_setting_footnote_content_link" = "global settings"; +"screen_room_notification_settings_default_setting_title" = "Default setting"; +"screen_room_notification_settings_edit_remove_setting" = "Remove custom setting"; +"screen_room_notification_settings_error_loading_settings" = "An error occurred while loading notification settings."; +"screen_room_notification_settings_error_restoring_default" = "Failed restoring the default mode, please try again."; +"screen_room_notification_settings_error_setting_mode" = "Failed setting the mode, please try again."; +"screen_room_notification_settings_mentions_only_disclaimer" = "Your homeserver does not support this option in encrypted rooms, you won't get notified in this room."; +"screen_room_notification_settings_mode_all_messages" = "All messages"; +"screen_room_notification_settings_room_custom_settings_title" = "In this room, notify me for"; +"screen_room_reactions_show_less" = "Show less"; +"screen_room_reactions_show_more" = "Show more"; +"screen_room_retry_send_menu_send_again_action" = "Send again"; +"screen_room_retry_send_menu_title" = "Your message failed to send"; +"screen_room_timeline_add_reaction" = "Add emoji"; +"screen_room_timeline_less_reactions" = "Show less"; +"screen_room_voice_message_tooltip" = "Hold to record"; +"screen_roomlist_a11y_create_message" = "Create a new conversation or room"; +"screen_roomlist_empty_message" = "Get started by messaging someone."; +"screen_roomlist_empty_title" = "No chats yet."; +"screen_roomlist_main_space_title" = "All Chats"; +"screen_server_confirmation_change_server" = "Change account provider"; +"screen_server_confirmation_message_login_element_dot_io" = "A private server for Element employees."; +"screen_server_confirmation_message_login_matrix_dot_org" = "Matrix is an open network for secure, decentralised communication."; +"screen_server_confirmation_message_register" = "This is where your conversations will live — just like you would use an email provider to keep your emails."; +"screen_server_confirmation_title_login" = "You’re about to sign in to %1$@"; +"screen_server_confirmation_title_register" = "You’re about to create an account on %1$@"; +"screen_session_verification_cancelled_subtitle" = "Something doesn’t seem right. Either the request timed out or the request was denied."; +"screen_session_verification_compare_emojis_subtitle" = "Confirm that the emojis below match those shown on your other session."; +"screen_session_verification_compare_emojis_title" = "Compare emojis"; +"screen_session_verification_complete_subtitle" = "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted."; +"screen_session_verification_open_existing_session_subtitle" = "Prove it’s you in order to access your encrypted message history."; +"screen_session_verification_open_existing_session_title" = "Open an existing session"; +"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"; +"screen_session_verification_waiting_to_accept_subtitle" = "Accept the request to start the verification process in your other session to continue."; +"screen_session_verification_waiting_to_accept_title" = "Waiting to accept request"; +"screen_share_location_title" = "Share location"; +"screen_share_my_location_action" = "Share my location"; +"screen_share_open_apple_maps" = "Open in Apple Maps"; +"screen_share_open_google_maps" = "Open in Google Maps"; +"screen_share_open_osm_maps" = "Open in OpenStreetMap"; +"screen_share_this_location_action" = "Share this location"; +"screen_signed_out_reason_1" = "You’ve changed your password on another session"; +"screen_signed_out_reason_2" = "You have deleted the session from another session"; +"screen_signed_out_reason_3" = "Your server’s administrator has invalidated your access"; +"screen_signed_out_subtitle" = "You might have been signed out for one of the reasons listed below. Please sign in again to continue using %@."; +"screen_signed_out_title" = "You’re signed out"; +"screen_signout_confirmation_dialog_content" = "Are you sure you want to sign out?"; +"screen_signout_in_progress_dialog_content" = "Signing out…"; +"screen_signout_key_backup_disabled_subtitle" = "You are about to sign out of your last session. If you sign out now, you will lose access to your encrypted messages."; +"screen_signout_key_backup_disabled_title" = "You have turned off backup"; +"screen_signout_key_backup_offline_subtitle" = "Your keys were still being backed up when you went offline. Reconnect so that your keys can be backed up before signing out."; +"screen_signout_key_backup_offline_title" = "Your keys are still being backed up"; +"screen_signout_key_backup_ongoing_subtitle" = "Please wait for this to complete before signing out."; +"screen_signout_key_backup_ongoing_title" = "Your keys are still being backed up"; +"screen_signout_recovery_disabled_subtitle" = "You are about to sign out of your last session. If you sign out now, you'll lose access to your encrypted messages."; +"screen_signout_recovery_disabled_title" = "Recovery not set up"; +"screen_signout_save_recovery_key_subtitle" = "You are about to sign out of your last session. If you sign out now, you might lose access to your encrypted messages."; +"screen_signout_save_recovery_key_title" = "Have you saved your recovery key?"; +"screen_start_chat_error_starting_chat" = "An error occurred when trying to start a chat"; +"screen_view_location_title" = "Location"; +"screen_waitlist_message" = "There's a high demand for %1$@ on %2$@ at the moment. Come back to the app in a few days and try again.\n\nThanks for your patience!"; +"screen_waitlist_title" = "You’re almost there."; +"screen_waitlist_title_success" = "You're in."; +"screen_welcome_bullet_1" = "Calls, polls, search and more will be added later this year."; +"screen_welcome_bullet_2" = "Message history for encrypted rooms isn’t available yet."; +"screen_welcome_bullet_3" = "We’d love to hear from you, let us know what you think via the settings page."; +"screen_welcome_button" = "Let's go!"; +"screen_welcome_subtitle" = "Here’s what you need to know:"; +"screen_welcome_title" = "Welcome to %1$@!"; +"session_verification_banner_message" = "Looks like you’re using a new device. Verify with another device to access your encrypted messages."; +"session_verification_banner_title" = "Verify it’s you"; +"settings_rageshake" = "Rageshake"; +"settings_rageshake_detection_threshold" = "Detection threshold"; +"settings_version_number" = "Version: %1$@ (%2$@)"; +"state_event_avatar_changed_too" = "(avatar was changed too)"; +"state_event_avatar_url_changed" = "%1$@ changed their avatar"; +"state_event_avatar_url_changed_by_you" = "You changed your avatar"; +"state_event_display_name_changed_from" = "%1$@ changed their display name from %2$@ to %3$@"; +"state_event_display_name_changed_from_by_you" = "You changed your display name from %1$@ to %2$@"; +"state_event_display_name_removed" = "%1$@ removed their display name (it was %2$@)"; +"state_event_display_name_removed_by_you" = "You removed your display name (it was %1$@)"; +"state_event_display_name_set" = "%1$@ set their display name to %2$@"; +"state_event_display_name_set_by_you" = "You set your display name to %1$@"; +"state_event_room_avatar_changed" = "%1$@ changed the room avatar"; +"state_event_room_avatar_changed_by_you" = "You changed the room avatar"; +"state_event_room_avatar_removed" = "%1$@ removed the room avatar"; +"state_event_room_avatar_removed_by_you" = "You removed the room avatar"; +"state_event_room_ban" = "%1$@ banned %2$@"; +"state_event_room_ban_by_you" = "You banned %1$@"; +"state_event_room_created" = "%1$@ created the room"; +"state_event_room_created_by_you" = "You created the room"; +"state_event_room_invite" = "%1$@ invited %2$@"; +"state_event_room_invite_accepted" = "%1$@ accepted the invite"; +"state_event_room_invite_accepted_by_you" = "You accepted the invite"; +"state_event_room_invite_by_you" = "You invited %1$@"; +"state_event_room_invite_you" = "%1$@ invited you"; +"state_event_room_join" = "%1$@ joined the room"; +"state_event_room_join_by_you" = "You joined the room"; +"state_event_room_knock" = "%1$@ requested to join"; +"state_event_room_knock_accepted" = "%1$@ allowed %2$@ to join"; +"state_event_room_knock_accepted_by_you" = "%1$@ allowed you to join"; +"state_event_room_knock_by_you" = "You requested to join"; +"state_event_room_knock_denied" = "%1$@ rejected %2$@'s request to join"; +"state_event_room_knock_denied_by_you" = "You rejected %1$@'s request to join"; +"state_event_room_knock_denied_you" = "%1$@ rejected your request to join"; +"state_event_room_knock_retracted" = "%1$@ is no longer interested in joining"; +"state_event_room_knock_retracted_by_you" = "You cancelled your request to join"; +"state_event_room_leave" = "%1$@ left the room"; +"state_event_room_leave_by_you" = "You left the room"; +"state_event_room_name_changed" = "%1$@ changed the room name to: %2$@"; +"state_event_room_name_changed_by_you" = "You changed the room name to: %1$@"; +"state_event_room_name_removed" = "%1$@ removed the room name"; +"state_event_room_name_removed_by_you" = "You removed the room name"; +"state_event_room_reject" = "%1$@ rejected the invitation"; +"state_event_room_reject_by_you" = "You rejected the invitation"; +"state_event_room_remove" = "%1$@ removed %2$@"; +"state_event_room_remove_by_you" = "You removed %1$@"; +"state_event_room_third_party_invite" = "%1$@ sent an invitation to %2$@ to join the room"; +"state_event_room_third_party_invite_by_you" = "You sent an invitation to %1$@ to join the room"; +"state_event_room_third_party_revoked_invite" = "%1$@ revoked the invitation for %2$@ to join the room"; +"state_event_room_third_party_revoked_invite_by_you" = "You revoked the invitation for %1$@ to join the room"; +"state_event_room_topic_changed" = "%1$@ changed the topic to: %2$@"; +"state_event_room_topic_changed_by_you" = "You changed the topic to: %1$@"; +"state_event_room_topic_removed" = "%1$@ removed the room topic"; +"state_event_room_topic_removed_by_you" = "You removed the room topic"; +"state_event_room_unban" = "%1$@ unbanned %2$@"; +"state_event_room_unban_by_you" = "You unbanned %1$@"; +"state_event_room_unknown_membership_change" = "%1$@ made an unknown change to their membership"; +"test_language_identifier" = "en"; +"test_untranslated_default_language_identifier" = "en"; +"dialog_title_error" = "Error"; +"dialog_title_success" = "Success"; +"notification_fallback_content" = "Notification"; +"notification_room_action_quick_reply" = "Quick reply"; +"screen_room_mentions_at_room_title" = "Everyone"; +"screen_analytics_settings_help_us_improve" = "Share anonymous usage data to help us identify issues."; +"screen_analytics_settings_read_terms" = "You can read all our terms %1$@."; +"screen_analytics_settings_read_terms_content_link" = "here"; +"screen_bug_report_rash_logs_alert_title" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?"; +"screen_create_room_title" = "Create a room"; +"screen_dm_details_block_alert_action" = "Block"; +"screen_dm_details_block_alert_description" = "Blocked users won't be able to send you messages and all their messages will be hidden. You can unblock them anytime."; +"screen_dm_details_block_user" = "Block user"; +"screen_dm_details_unblock_alert_action" = "Unblock"; +"screen_dm_details_unblock_alert_description" = "You'll be able to see all messages from them again."; +"screen_dm_details_unblock_user" = "Unblock user"; +"screen_login_subtitle" = "Matrix is an open network for secure, decentralised communication."; +"screen_report_content_block_user" = "Block user"; +"screen_room_details_leave_room_title" = "Leave room"; +"screen_room_details_security_title" = "Security"; +"screen_room_details_topic_title" = "Topic"; +"screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again."; +"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only"; +"screen_signout_confirmation_dialog_submit" = "Sign out"; +"screen_signout_confirmation_dialog_title" = "Sign out"; +"screen_signout_preference_item" = "Sign out"; +"screen_waitlist_message_success" = "Welcome to %1$@!"; diff --git a/ios/Runner/en.lproj/Localizable.stringsdict b/ios/Runner/en.lproj/Localizable.stringsdict new file mode 100644 index 0000000000..65f2937f77 --- /dev/null +++ b/ios/Runner/en.lproj/Localizable.stringsdict @@ -0,0 +1,230 @@ + + + + + a11y_digits_entered + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d digit entered + other + %1$d digits entered + + + a11y_read_receipts_multiple_with_others + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Read by %1$@ and %2$d other + other + Read by %1$@ and %2$d others + + + common_member_count + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d member + other + %1$d members + + + common_poll_votes_count + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d vote + other + %d votes + + + notification_compat_summary_line_for_room + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@: %2$d message + other + %1$@: %2$d messages + + + notification_compat_summary_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d notification + other + %d notifications + + + notification_invitations + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d invitation + other + %d invitations + + + notification_new_messages_for_room + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d new message + other + %d new messages + + + notification_unread_notified_messages + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d unread notified message + other + %d unread notified messages + + + notification_unread_notified_messages_in_room_rooms + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d room + other + %d rooms + + + room_timeline_state_changes + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d room change + other + %1$d room changes + + + screen_app_lock_subtitle + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + You have %1$d attempt to unlock + other + You have %1$d attempts to unlock + + + screen_app_lock_subtitle_wrong_pin + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Wrong PIN. You have %1$d more chance + other + Wrong PIN. You have %1$d more chances + + + screen_room_member_list_header_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d person + other + %1$d people + + + + \ No newline at end of file diff --git a/ios/Runner/fr.lproj/Localizable.strings b/ios/Runner/fr.lproj/Localizable.strings index debec9ea4c..3a23c212cd 100644 --- a/ios/Runner/fr.lproj/Localizable.strings +++ b/ios/Runner/fr.lproj/Localizable.strings @@ -1 +1,669 @@ "newMessageInTwake" = "Vous avez un message chiffré"; +"Notification" = "Notification"; +"a11y_delete" = "Supprimer"; +"a11y_hide_password" = "Masquer le mot de passe"; +"a11y_notifications_mentions_only" = "Mentions uniquement"; +"a11y_notifications_muted" = "En sourdine"; +"a11y_pause" = "Pause"; +"a11y_pin_field" = "Code PIN"; +"a11y_play" = "Lecture"; +"a11y_poll" = "Sondage"; +"a11y_poll_end" = "Sondage terminé"; +"a11y_read_receipts_single" = "Read by %1$@"; +"a11y_send_files" = "Envoyer des fichiers"; +"a11y_show_password" = "Afficher le mot de passe"; +"a11y_start_call" = "Démarrer un appel"; +"a11y_user_menu" = "Menu utilisateur"; +"a11y_voice_message_record" = "Enregistrer un message vocal. Taper deux fois et maintenir pour enregistrer. Relâcher pour stopper l’enregistrement."; +"a11y_voice_message_stop_recording" = "Stop recording"; +"a11y.read_receipts_multiple" = "Read by %1$@ and %2$@"; +"action_accept" = "Accepter"; +"action_add_to_timeline" = "Ajouter à la discussion"; +"action_back" = "Retour"; +"action_cancel" = "Annuler"; +"action_choose_photo" = "Choisissez une photo"; +"action_clear" = "Effacer"; +"action_close" = "Fermer"; +"action_complete_verification" = "Terminer la vérification"; +"action_confirm" = "Confirmer"; +"action_continue" = "Continuer"; +"action_copy" = "Copier"; +"action_copy_link" = "Copier le lien"; +"action_copy_link_to_message" = "Copier le lien vers le message"; +"action_create" = "Créer"; +"action_create_a_room" = "Créer un salon"; +"action_decline" = "Refuser"; +"action_disable" = "Désactiver"; +"action_done" = "Terminé"; +"action_edit" = "Modifier"; +"action_enable" = "Activer"; +"action_end_poll" = "Terminer le sondage"; +"action_enter_pin" = "Saisir le code PIN"; +"action_forgot_password" = "Mot de passe oublié ?"; +"action_forward" = "Transférer"; +"action_invite" = "Inviter"; +"action_invite_friends" = "Inviter des amis"; +"action_invite_friends_to_app" = "Inviter des amis à %1$@"; +"action_invite_people_to_app" = "Invitez des personnes à %1$@"; +"action_invites_list" = "Invitations"; +"action_join" = "Rejoindre"; +"action_learn_more" = "En savoir plus"; +"action_leave" = "Quitter"; +"action_leave_room" = "Quitter le salon"; +"action_manage_account" = "Gérer le compte"; +"action_manage_devices" = "Gérez les sessions"; +"action_next" = "Suivant"; +"action_no" = "Non"; +"action_not_now" = "Pas maintenant"; +"action_ok" = "OK"; +"action_open_settings" = "Ouvrir les paramètres"; +"action_open_with" = "Ouvrir avec"; +"action_quick_reply" = "Réponse rapide"; +"action_quote" = "Citer"; +"action_react" = "Réagissez"; +"action_remove" = "Supprimer"; +"action_reply" = "Répondre"; +"action_reply_in_thread" = "Répondre dans le fil de discussion"; +"action_report_bug" = "Signaler un problème"; +"action_report_content" = "Signaler le contenu"; +"action_retry" = "Réessayer"; +"action_retry_decryption" = "Réessayer le déchiffrement"; +"action_save" = "Enregistrer"; +"action_search" = "Rechercher"; +"action_send" = "Envoyer"; +"action_send_message" = "Envoyer un message"; +"action_share" = "Partager"; +"action_share_link" = "Partager le lien"; +"action_sign_in_again" = "Se connecter à nouveau"; +"action_signout" = "Se déconnecter"; +"action_signout_anyway" = "Se déconnecter quand même"; +"action_skip" = "Passer"; +"action_start" = "Démarrer"; +"action_start_chat" = "Démarrer une discussion"; +"action_start_verification" = "Commencer la vérification"; +"action_static_map_load" = "Cliquez pour charger la carte"; +"action_take_photo" = "Prendre une photo"; +"action_tap_for_options" = "Tap for options"; +"action_try_again" = "Essayer à nouveau"; +"action_view_source" = "Afficher la source"; +"action_yes" = "Oui"; +"action.edit_poll" = "Modifier le sondage"; +"common_about" = "À propos"; +"common_acceptable_use_policy" = "Politique d’utilisation acceptable"; +"common_advanced_settings" = "Paramètres avancés"; +"common_analytics" = "Statistiques d’utilisation"; +"common_appearance" = "Appearance"; +"common_audio" = "Audio"; +"common_bubbles" = "Bulles"; +"common_chat_backup" = "Sauvegarde des discussions"; +"common_copyright" = "Droits d’auteur"; +"common_creating_room" = "Création du salon…"; +"common_current_user_left_room" = "Quitter le salon"; +"common_dark" = "Dark"; +"common_decryption_error" = "Erreur de déchiffrement"; +"common_developer_options" = "Options pour les développeurs"; +"common_edited_suffix" = "(modifié)"; +"common_editing" = "Édition"; +"common_emote" = "* %1$@ %2$@"; +"common_encryption_enabled" = "Chiffrement activé"; +"common_enter_your_pin" = "Saisissez votre code PIN"; +"common_error" = "Erreur"; +"common_everyone" = "Tout le monde"; +"common_face_id_ios" = "Face ID"; +"common_file" = "Fichier"; +"common_forward_message" = "Transférer le message"; +"common_gif" = "GIF"; +"common_image" = "Image"; +"common_in_reply_to" = "En réponse à %1$@"; +"common_invite_unknown_profile" = "Cet identifiant Matrix est introuvable, il est donc possible que l’invitation ne soit pas reçue."; +"common_leaving_room" = "Quitter le salon…"; +"common_light" = "Light"; +"common_link_copied_to_clipboard" = "Lien copié dans le presse-papiers"; +"common_loading" = "Chargement…"; +"common_message" = "Message"; +"common_message_actions" = "Message actions"; +"common_message_layout" = "Mode d’affichage des messages"; +"common_message_removed" = "Message supprimé"; +"common_modern" = "Moderne"; +"common_mute" = "Mettre en sourdine"; +"common_no_results" = "Aucun résultat"; +"common_offline" = "Hors ligne"; +"common_optic_id_ios" = "Optic ID"; +"common_password" = "Mot de passe"; +"common_people" = "Personnes"; +"common_permalink" = "Permalien"; +"common_permission" = "Autorisation"; +"common_poll_total_votes" = "Nombre total de votes : %1$@"; +"common_poll_undisclosed_text" = "Les résultats s’afficheront une fois le sondage terminé"; +"common_privacy_policy" = "Politique de confidentialité"; +"common_reaction" = "Réaction"; +"common_reactions" = "Réactions"; +"common_recovery_key" = "Clé de récupération"; +"common_refreshing" = "Actualisation…"; +"common_replying_to" = "En réponse à %1$@"; +"common_report_a_bug" = "Signaler un problème"; +"common_report_a_problem" = "Report a problem"; +"common_report_submitted" = "Rapport soumis"; +"common_rich_text_editor" = "Éditeur de texte enrichi"; +"common_room" = "Salon"; +"common_room_name" = "Nom du salon"; +"common_room_name_placeholder" = "par exemple, le nom de votre projet"; +"common_screen_lock" = "Verrouillage d’écran"; +"common_search_for_someone" = "Rechercher quelqu’un"; +"common_search_results" = "Résultats de la recherche"; +"common_security" = "Sécurité"; +"common_seen_by" = "Seen by"; +"common_sending" = "Envoi en cours…"; +"common_sending_failed" = "Sending failed"; +"common_sent" = "Sent"; +"common_server_not_supported" = "Serveur non pris en charge"; +"common_server_url" = "URL du serveur"; +"common_settings" = "Paramètres"; +"common_shared_location" = "Position partagée"; +"common_signing_out" = "Déconnexion"; +"common_starting_chat" = "Création de la discussion..."; +"common_sticker" = "Autocollant"; +"common_success" = "Succès"; +"common_suggestions" = "Suggestions"; +"common_syncing" = "Synchronisation"; +"common_system" = "System"; +"common_text" = "Texte"; +"common_third_party_notices" = "Avis de tiers"; +"common_thread" = "Fil de discussion"; +"common_topic" = "Sujet"; +"common_topic_placeholder" = "De quoi s’agit-il dans ce salon ?"; +"common_touch_id_ios" = "Touch ID"; +"common_unable_to_decrypt" = "Échec de déchiffrement"; +"common_unable_to_invite_message" = "Les invitations n’ont pas pu être envoyées à un ou plusieurs utilisateurs."; +"common_unable_to_invite_title" = "Impossible d’envoyer une ou plusieurs invitations"; +"common_unlock" = "Déverrouillage"; +"common_unmute" = "Annuler la sourdine"; +"common_unsupported_event" = "Événement non pris en charge"; +"common_username" = "Nom d’utilisateur"; +"common_verification_cancelled" = "Vérification annulée"; +"common_verification_complete" = "Vérification terminée"; +"common_video" = "Vidéo"; +"common_voice_message" = "Message vocal"; +"common_waiting" = "En attente..."; +"common_waiting_for_decryption_key" = "En attente de la clé de déchiffrement"; +"common_poll_end_confirmation" = "Êtes-vous sûr de vouloir mettre fin à ce sondage ?"; +"common_poll_summary" = "Sondage : %1$@"; +"common_verify_device" = "Verify device"; +"confirm_recovery_key_banner_message" = "La sauvegarde des conversations est désynchronisée. Vous devez confirmer la clé de récupération pour accéder à votre historique."; +"confirm_recovery_key_banner_title" = "Confirmer votre clé de récupération"; +"crash_detection_dialog_content" = "%1$@ s’est arrêté la dernière fois qu’il a été utilisé. Souhaitez-vous partager un rapport d’incident avec nous ?"; +"dialog_permission_camera" = "Pour permettre à l’application d’utiliser l’appareil photo, veuillez accorder l’autorisation dans les paramètres du système."; +"dialog_permission_generic" = "Veuillez accorder l’autorisation dans les paramètres du système."; +"dialog_permission_location_description_ios" = "Autorisez l’accès dans Paramètres / Localisation"; +"dialog_permission_location_title_ios" = "%1$@ n’a pas accès à votre position"; +"dialog_permission_microphone" = "Pour permettre à l’application d’utiliser le microphone, veuillez accorder l’autorisation dans les paramètres du système."; +"dialog_permission_microphone_description_ios" = "Donnez la permission afin de pouvoir enregistrer et envoyer des messages vocaux"; +"dialog_permission_microphone_title_ios" = "%1$@ a besoin de votre autorisation pour utiliser le microphone."; +"dialog_permission_notification" = "Pour permettre à l’application d’afficher les notifications, veuillez accorder l’autorisation dans les paramètres du système."; +"dialog_title_confirmation" = "Confirmation"; +"dialog_title_warning" = "Attention"; +"emoji_picker_category_activity" = "Activités"; +"emoji_picker_category_flags" = "Drapeaux"; +"emoji_picker_category_foods" = "Nourriture et boissons"; +"emoji_picker_category_nature" = "Animaux et nature"; +"emoji_picker_category_objects" = "Objets"; +"emoji_picker_category_people" = "Émoticônes et personnes"; +"emoji_picker_category_places" = "Voyages & lieux"; +"emoji_picker_category_symbols" = "Symboles"; +"error_failed_creating_the_permalink" = "Échec de la création du permalien"; +"error_failed_loading_map" = "%1$@ n’a pas pu charger la carte. Veuillez réessayer ultérieurement."; +"error_failed_loading_messages" = "Échec du chargement des messages"; +"error_failed_locating_user" = "%1$@ n’a pas pu accéder à votre position. Veuillez réessayer ultérieurement."; +"error_failed_uploading_voice_message" = "Échec lors de l’envoi du message vocal."; +"error_no_compatible_app_found" = "Aucune application compatible n’a été trouvée pour gérer cette action."; +"error_some_messages_have_not_been_sent" = "Certains messages n’ont pas été envoyés"; +"error_unknown" = "Désolé, une erreur s’est produite"; +"invite_friends_rich_title" = "🔐️ Rejoignez-moi sur %1$@"; +"invite_friends_text" = "Salut, parle-moi sur %1$@ : %2$@"; +"leave_room_alert_empty_subtitle" = "Êtes-vous sûr de vouloir quitter ce salon ? Vous êtes la seule personne ici. Si vous partez, personne ne pourra rejoindre le salon à l’avenir, y compris vous."; +"leave_room_alert_private_subtitle" = "Êtes-vous sûr de vouloir quitter ce salon ? Ce salon n’est pas public et vous ne pourrez pas le rejoindre sans invitation."; +"leave_room_alert_subtitle" = "Êtes-vous sûr de vouloir quitter le salon ?"; +"login_initial_device_name_ios" = "%1$@ iOS"; +"notification_channel_call" = "Appel"; +"notification_channel_listening_for_events" = "À l’écoute des événements"; +"notification_channel_noisy" = "Notifications bruyantes"; +"notification_channel_silent" = "Notifications silencieuses"; +"notification_inline_reply_failed" = "** Échec de l’envoi - veuillez ouvrir le salon"; +"notification_invitation_action_join" = "Rejoindre"; +"notification_invitation_action_reject" = "Rejeter"; +"notification_invite_body" = "Vous a invité(e) à discuter"; +"notification_mentioned_you_body" = "%1$@ mentioned you.\n%2$@"; +"notification_mentioned_you_fallback_body" = "You have been mentioned.\n%1$@"; +"notification_new_messages" = "Nouveaux messages"; +"notification_reaction_body" = "A réagi avec %1$@"; +"notification_room_action_mark_as_read" = "Marquer comme lu"; +"notification_room_invite_body" = "Vous a invité(e) à rejoindre le salon"; +"notification_sender_me" = "Moi"; +"notification_test_push_notification_content" = "Vous êtes en train de voir la notification ! Cliquez-moi !"; +"notification_ticker_text_dm" = "%1$@ : %2$@"; +"notification_ticker_text_group" = "%1$@ : %2$@ %3$@"; +"notification_unread_notified_messages_and_invitation" = "%1$@ et %2$@"; +"notification_unread_notified_messages_in_room" = "%1$@ dans %2$@"; +"notification_unread_notified_messages_in_room_and_invitation" = "%1$@ dans %2$@ et %3$@"; +"preference_rageshake" = "Rageshake pour signaler un problème"; +"rageshake_detection_dialog_content" = "Vous semblez secouez votre téléphone avec frustration. Souhaitez-vous ouvrir le formulaire pour reporter un problème?"; +"report_content_explanation" = "Ce message sera signalé à l’administrateur de votre serveur d’accueil. Il ne pourra lire aucun message chiffré."; +"report_content_hint" = "Raison du signalement de ce contenu"; +"rich_text_editor_bullet_list" = "Afficher une liste à puces"; +"rich_text_editor_close_formatting_options" = "Fermer les options de formatage"; +"rich_text_editor_code_block" = "Afficher le bloc de code"; +"rich_text_editor_composer_placeholder" = "Message..."; +"rich_text_editor_create_link" = "Créer un lien"; +"rich_text_editor_edit_link" = "Modifier le lien"; +"rich_text_editor_format_bold" = "Appliquer le format gras"; +"rich_text_editor_format_italic" = "Appliquer le format italique"; +"rich_text_editor_format_strikethrough" = "Appliquer le format barré"; +"rich_text_editor_format_underline" = "Appliquer le format souligné"; +"rich_text_editor_full_screen_toggle" = "Activer/désactiver le mode plein écran"; +"rich_text_editor_indent" = "Décaler vers la droite"; +"rich_text_editor_inline_code" = "Appliquer le formatage de code en ligne"; +"rich_text_editor_link" = "Définir un lien"; +"rich_text_editor_numbered_list" = "Afficher une liste numérotée"; +"rich_text_editor_open_compose_options" = "Ouvrir les options de rédaction"; +"rich_text_editor_quote" = "Afficher/masquer la citation"; +"rich_text_editor_remove_link" = "Supprimer le lien"; +"rich_text_editor_unindent" = "Décaler vers la gauche"; +"rich_text_editor_url_placeholder" = "Lien"; +"rich_text_editor_a11y_add_attachment" = "Ajouter une pièce jointe"; +"room_timeline_beginning_of_room" = "Ceci est le début de %1$@."; +"room_timeline_beginning_of_room_no_name" = "Ceci est le début de cette conversation."; +"room_timeline_read_marker_title" = "Nouveau"; +"screen_advanced_settings_element_call_base_url" = "URL de base pour Element Call personnalisée"; +"screen_advanced_settings_element_call_base_url_description" = "Configurer une URL de base pour Element Call."; +"screen_advanced_settings_element_call_base_url_validation_error" = "URL invalide, assurez-vous d’inclure le protocol (http/https) et l’adresse correcte."; +"screen_room_mentions_at_room_subtitle" = "Notifier tout le salon"; +"screen_account_provider_change" = "Changer de fournisseur de compte"; +"screen_account_provider_form_hint" = "Adresse du serveur d’accueil"; +"screen_account_provider_form_notice" = "Entrez un terme de recherche ou une adresse de domaine."; +"screen_account_provider_form_subtitle" = "Recherchez une entreprise, une communauté ou un serveur privé."; +"screen_account_provider_form_title" = "Trouver un fournisseur de comptes"; +"screen_account_provider_signin_subtitle" = "C’est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails."; +"screen_account_provider_signin_title" = "Vous êtes sur le point de vous connecter à %@"; +"screen_account_provider_signup_subtitle" = "C’est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails."; +"screen_account_provider_signup_title" = "Vous êtes sur le point de créer un compte sur %@"; +"screen_advanced_settings_developer_mode" = "Mode développeur"; +"screen_advanced_settings_developer_mode_description" = "Activer pour pouvoir accéder aux fonctionnalités destinées aux développeurs."; +"screen_advanced_settings_rich_text_editor_description" = "Désactivez l’éditeur de texte enrichi pour saisir manuellement du Markdown."; +"screen_analytics_prompt_data_usage" = "Nous n’enregistrerons ni ne profilerons aucune donnée personnelle"; +"screen_analytics_prompt_help_us_improve" = "Partagez des données d’utilisation anonymes pour nous aider à identifier les problèmes."; +"screen_analytics_prompt_read_terms" = "Vous pouvez lire toutes nos conditions %1$@."; +"screen_analytics_prompt_read_terms_content_link" = "ici"; +"screen_analytics_prompt_settings" = "Vous pouvez le désactiver à tout moment"; +"screen_analytics_prompt_third_party_sharing" = "Nous ne partagerons pas vos données avec des tiers"; +"screen_analytics_prompt_title" = "Aidez à améliorer %1$@"; +"screen_analytics_settings_share_data" = "Partagez des données de statistiques d’utilisation"; +"screen_app_lock_biometric_authentication" = "Authentification biométrique"; +"screen_app_lock_biometric_unlock" = "Déverrouillage biométrique"; +"screen_app_lock_biometric_unlock_reason_ios" = "Veuillez vous authentification pour accéder à l’application"; +"screen_app_lock_forgot_pin" = "Code PIN oublié?"; +"screen_app_lock_settings_change_pin" = "Modifier le code PIN"; +"screen_app_lock_settings_enable_biometric_unlock" = "Autoriser le déverrouillage biométrique"; +"screen_app_lock_settings_enable_face_id_ios" = "Autoriser Face ID"; +"screen_app_lock_settings_enable_optic_id_ios" = "Autoriser Optic ID"; +"screen_app_lock_settings_enable_touch_id_ios" = "Autoriser Touch ID"; +"screen_app_lock_settings_remove_pin" = "Supprimer le code PIN"; +"screen_app_lock_settings_remove_pin_alert_message" = "Êtes-vous certain de vouloir supprimer le code PIN?"; +"screen_app_lock_settings_remove_pin_alert_title" = "Supprimer le code PIN?"; +"screen_app_lock_setup_biometric_unlock_allow_title" = "Autoriser %1$@"; +"screen_app_lock_setup_biometric_unlock_skip" = "Je préfère utiliser le code PIN"; +"screen_app_lock_setup_biometric_unlock_subtitle" = "Gagnez du temps en utilisant %1$@ pour déverrouiller l’application à chaque fois."; +"screen_app_lock_setup_choose_pin" = "Choisissez un code PIN"; +"screen_app_lock_setup_confirm_pin" = "Confirmer le code PIN"; +"screen_app_lock_setup_pin_blacklisted_dialog_content" = "Vous ne pouvez pas choisir ce code PIN pour des raisons de sécurité"; +"screen_app_lock_setup_pin_blacklisted_dialog_title" = "Choisissez un code PIN différent"; +"screen_app_lock_setup_pin_context" = "Verrouillez %1$@ pour ajouter une sécurité supplémentaire à vos discussions. Choisissez un code facile à retenir. Si vous oubliez le code PIN, vous serez déconnecté."; +"screen_app_lock_setup_pin_mismatch_dialog_content" = "Veuillez saisir le même code PIN deux fois"; +"screen_app_lock_setup_pin_mismatch_dialog_title" = "Les codes PIN ne correspondent pas"; +"screen_app_lock_signout_alert_message" = "Pour continuer, vous devrez vous connecter à nouveau et créer un nouveau code PIN."; +"screen_app_lock_signout_alert_title" = "Vous êtes en train de vous déconnecter"; +"screen_bug_report_attach_screenshot" = "Joindre une capture d’écran"; +"screen_bug_report_contact_me" = "Vous pouvez me contacter si vous avez des questions complémentaires."; +"screen_bug_report_contact_me_title" = "Contactez-moi"; +"screen_bug_report_edit_screenshot" = "Modifier la capture d’écran"; +"screen_bug_report_editor_description" = "S’il vous plait, veuillez décrire le problème. Qu’avez-vous fait ? À quoi vous attendiez-vous ? Que s’est-il réellement passé ? Veuillez ajouter le plus de détails possible."; +"screen_bug_report_editor_placeholder" = "Décrire le problème"; +"screen_bug_report_editor_supporting" = "Si possible, veuillez rédiger la description en anglais."; +"screen_bug_report_include_crash_logs" = "Envoyer des journaux d’incident"; +"screen_bug_report_include_logs" = "Autoriser à inclure les journaux techniques"; +"screen_bug_report_include_screenshot" = "Envoyer une capture d’écran"; +"screen_bug_report_logs_description" = "Pour vérifier que les choses fonctionnent comme prévu, des journaux techniques seront envoyés avec votre message. Pour ne pas envoyer ces journaux, désactivez ce paramètre."; +"screen_change_account_provider_matrix_org_subtitle" = "Matrix.org est un grand serveur gratuit sur le réseau public Matrix pour une communication sécurisée et décentralisée, géré par la Fondation Matrix.org."; +"screen_change_account_provider_other" = "Autres"; +"screen_change_account_provider_subtitle" = "Utilisez un autre fournisseur de compte, tel que votre propre serveur privé ou un serveur professionnel."; +"screen_change_account_provider_title" = "Changer de fournisseur de compte"; +"screen_change_server_error_invalid_homeserver" = "Nous n’avons pas pu atteindre ce serveur d’accueil. Vérifiez que vous avez correctement saisi l’URL du serveur d’accueil. Si l’URL est correcte, contactez l’administrateur de votre serveur d’accueil pour obtenir de l’aide."; +"screen_change_server_error_no_sliding_sync_message" = "Ce serveur ne prend actuellement pas en charge la synchronisation glissante."; +"screen_change_server_form_header" = "URL du serveur d’accueil"; +"screen_change_server_form_notice" = "Vous ne pouvez vous connecter qu’à un serveur existant qui prend en charge le sliding sync. L’administrateur de votre serveur d’accueil devra le configurer. %1$@"; +"screen_change_server_subtitle" = "Quelle est l’adresse de votre serveur ?"; +"screen_change_server_title" = "Choisissez votre serveur"; +"screen_chat_backup_key_backup_action_disable" = "Désactiver la sauvegarde"; +"screen_chat_backup_key_backup_action_enable" = "Activer la sauvegarde"; +"screen_chat_backup_key_backup_description" = "La sauvegarde assure que vous ne perdiez pas l’historique des discussions. %1$@."; +"screen_chat_backup_key_backup_title" = "Sauvegarde"; +"screen_chat_backup_recovery_action_change" = "Changer la clé de récupération"; +"screen_chat_backup_recovery_action_confirm" = "Confirmer la clé de récupération"; +"screen_chat_backup_recovery_action_confirm_description" = "La sauvegarde des discussions est désynchronisée."; +"screen_chat_backup_recovery_action_setup" = "Configurer la récupération"; +"screen_chat_backup_recovery_action_setup_description" = "Accédez à vos messages chiffrés si vous perdez tous vos appareils ou que vous êtes déconnectés de %1$@ partout."; +"screen_create_poll_add_option_btn" = "Ajouter une option"; +"screen_create_poll_anonymous_desc" = "Afficher les résultats uniquement après la fin du sondage"; +"screen_create_poll_anonymous_headline" = "Masquer les votes"; +"screen_create_poll_answer_hint" = "Option %1$d"; +"screen_create_poll_discard_confirmation" = "Êtes-vous sûr de vouloir supprimer ce sondage ?"; +"screen_create_poll_discard_confirmation_title" = "Supprimer le sondage"; +"screen_create_poll_question_desc" = "Question ou sujet"; +"screen_create_poll_question_hint" = "Quel est le sujet du sondage ?"; +"screen_create_poll_title" = "Créer un sondage"; +"screen_create_room_action_create_room" = "Nouveau salon"; +"screen_create_room_action_invite_people" = "Inviter des amis sur Element"; +"screen_create_room_add_people_title" = "Inviter des personnes"; +"screen_create_room_error_creating_room" = "Une erreur s’est produite lors de la création du salon"; +"screen_create_room_private_option_description" = "Les messages dans ce salon sont chiffrés. Le chiffrement ne pourra pas être désactivé par la suite."; +"screen_create_room_private_option_title" = "Salon privé (sur invitation seulement)"; +"screen_create_room_public_option_description" = "Les messages ne sont pas chiffrés et n’importe qui peut les lire. Vous pouvez activer le chiffrement ultérieurement."; +"screen_create_room_public_option_title" = "Salon public (tout le monde)"; +"screen_create_room_room_name_label" = "Nom du salon"; +"screen_create_room_topic_label" = "Sujet (facultatif)"; +"screen_edit_profile_display_name" = "Pseudonyme"; +"screen_edit_profile_display_name_placeholder" = "Votre pseudonyme"; +"screen_edit_profile_error" = "Une erreur inconnue s’est produite et les informations n’ont pas pu être modifiées."; +"screen_edit_profile_error_title" = "Impossible de mettre à jour le profil"; +"screen_edit_profile_title" = "Modifier le profil"; +"screen_edit_profile_updating_details" = "Mise à jour du profil..."; +"screen_invites_decline_chat_message" = "Êtes-vous sûr de vouloir décliner l’invitation à rejoindre %1$@ ?"; +"screen_invites_decline_chat_title" = "Refuser l’invitation"; +"screen_invites_decline_direct_chat_message" = "Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$@ ?"; +"screen_invites_decline_direct_chat_title" = "Refuser l’invitation"; +"screen_invites_empty_list" = "Aucune invitation"; +"screen_invites_invited_you" = "%1$@ (%2$@) vous a invité(e)"; +"screen_key_backup_disable_confirmation_action_turn_off" = "Désactiver"; +"screen_key_backup_disable_confirmation_description" = "Vous perdrez vos messages chiffrés si vous vous déconnectez de toutes vos sessions."; +"screen_key_backup_disable_confirmation_title" = "Êtes-vous certain de vouloir désactiver la sauvegarde?"; +"screen_key_backup_disable_description" = "Désactiver la sauvegarde supprimera votre clé de récupération actuelle et désactivera d’autres mesures de sécurité. Dans ce cas, vous:"; +"screen_key_backup_disable_description_point_1" = "Pas d’accès à l’historique des discussions chiffrées sur vos nouveaux appareils"; +"screen_key_backup_disable_description_point_2" = "Perte de l’accès à vos messages chiffrés si vous êtes déconnectés de %1$@ partout"; +"screen_key_backup_disable_title" = "Êtes-vous certain de vouloir désactiver la sauvegarde?"; +"screen_login_error_deactivated_account" = "Ce compte a été désactivé."; +"screen_login_error_invalid_credentials" = "Nom d’utilisateur et/ou mot de passe incorrects"; +"screen_login_error_invalid_user_id" = "Il ne s’agit pas d’un identifiant utilisateur valide. Format attendu : « @user:homeserver.org »"; +"screen_login_error_unsupported_authentication" = "Le serveur d’accueil sélectionné ne prend pas en charge le mot de passe ou la connexion OIDC. Contactez votre administrateur ou choisissez un autre serveur d’accueil."; +"screen_login_form_header" = "Saisissez vos identifiants"; +"screen_login_title" = "Content de vous revoir !"; +"screen_login_title_with_homeserver" = "Connectez-vous à %1$@"; +"screen_media_picker_error_failed_selection" = "Échec de la sélection du média, veuillez réessayer."; +"screen_media_upload_preview_error_failed_processing" = "Échec du traitement des médias à télécharger, veuillez réessayer."; +"screen_media_upload_preview_error_failed_sending" = "Échec du téléchargement du média, veuillez réessayer."; +"screen_migration_message" = "Il s’agit d’une opération ponctuelle, merci d’attendre quelques instants."; +"screen_migration_title" = "Configuration de votre compte."; +"screen_notification_optin_subtitle" = "Vous pourrez modifier vos paramètres ultérieurement."; +"screen_notification_optin_title" = "Autorisez les notifications et ne manquez aucun message"; +"screen_notification_settings_additional_settings_section_title" = "Réglages supplémentaires"; +"screen_notification_settings_calls_label" = "Appels audio et vidéo"; +"screen_notification_settings_configuration_mismatch" = "Incompatibilité de configuration"; +"screen_notification_settings_configuration_mismatch_description" = "Nous avons simplifié les paramètres des notifications pour que les options soient plus faciles à trouver.\n\nCertains paramètres personnalisés que vous avez choisis par le passé ne sont pas affichés ici, mais ils sont toujours actifs.\n\nSi vous continuez, il est possible que certains de vos paramètres soient modifiés."; +"screen_notification_settings_direct_chats" = "Discussions directes"; +"screen_notification_settings_edit_custom_settings_section_title" = "Paramétrage personnalisé par salon"; +"screen_notification_settings_edit_failed_updating_default_mode" = "Une erreur s’est produite lors de la mise à jour du paramètre de notification."; +"screen_notification_settings_edit_mode_all_messages" = "Tous les messages"; +"screen_notification_settings_edit_mode_mentions_and_keywords" = "Mentions et mots clés uniquement"; +"screen_notification_settings_edit_screen_direct_section_header" = "Sur les discussions directes, prévenez-moi pour"; +"screen_notification_settings_edit_screen_group_section_header" = "Lors de discussions de groupe, prévenez-moi pour"; +"screen_notification_settings_enable_notifications" = "Activer les notifications sur cet appareil"; +"screen_notification_settings_failed_fixing_configuration" = "La configuration n’a pas été corrigée, veuillez réessayer."; +"screen_notification_settings_group_chats" = "Discussions de groupe"; +"screen_notification_settings_mentions_only_disclaimer" = "Your homeserver does not support this option in encrypted rooms, you may not get notified in some rooms."; +"screen_notification_settings_mentions_section_title" = "Mentions"; +"screen_notification_settings_mode_all" = "Tous"; +"screen_notification_settings_mode_mentions" = "Mentions"; +"screen_notification_settings_notification_section_title" = "Prévenez-moi pour"; +"screen_notification_settings_room_mention_label" = "Prévenez-moi si un message contient \"@room\""; +"screen_notification_settings_system_notifications_action_required" = "Pour recevoir des notifications, veuillez modifier votre %1$@."; +"screen_notification_settings_system_notifications_action_required_content_link" = "paramètres du système"; +"screen_notification_settings_system_notifications_turned_off" = "Les notifications du système sont désactivées"; +"screen_notification_settings_title" = "Notifications"; +"screen_onboarding_sign_in_manually" = "Se connecter manuellement"; +"screen_onboarding_sign_in_with_qr_code" = "Se connecter avec un QR code"; +"screen_onboarding_sign_up" = "Créer un compte"; +"screen_onboarding_welcome_message" = "Bienvenue dans l’Element le plus rapide de tous les temps. Boosté pour plus de rapidité et de simplicité."; +"screen_onboarding_welcome_subtitle" = "Bienvenue sur %1$@. Boosté, pour plus de rapidité et de simplicité."; +"screen_onboarding_welcome_title" = "Soyez dans votre Element"; +"screen_recovery_key_change_description" = "Obtenez une nouvelle clé de récupération dans le cas où vous avez oublié l’ancienne. Après le changement, l’ancienne clé ne sera plus utilisable."; +"screen_recovery_key_change_generate_key" = "Générer une nouvelle clé"; +"screen_recovery_key_change_generate_key_description" = "Assurez-vous de conserver la clé dans un endroit sûr."; +"screen_recovery_key_change_success" = "Clé de récupération modifée"; +"screen_recovery_key_change_title" = "Changer la clé de récupération?"; +"screen_recovery_key_confirm_description" = "Saisissez votre clé de récupération pour accéder à l’historique de vos discussions."; +"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" = "Saisissez la clé à 48 caractères."; +"screen_recovery_key_confirm_key_placeholder" = "Saisissez la clé ici…"; +"screen_recovery_key_confirm_success" = "Clé de récupération confirmée"; +"screen_recovery_key_confirm_title" = "Confirmez votre clé de récupération"; +"screen_recovery_key_copied_to_clipboard" = "Clé de récupération copiée"; +"screen_recovery_key_generating_key" = "Génération…"; +"screen_recovery_key_save_action" = "Enregistrer la clé"; +"screen_recovery_key_save_description" = "Recopier votre clé de récupération dans un endroit sécurisé ou enregistrer la dans un manager de mot de passe."; +"screen_recovery_key_save_key_description" = "Taper pour copier la clé"; +"screen_recovery_key_save_title" = "Sauvegarder la clé"; +"screen_recovery_key_setup_confirmation_description" = "La clé ne pourra plus être affichée après cette étape."; +"screen_recovery_key_setup_confirmation_title" = "Avez-vous sauvegardé votre clé de récupération?"; +"screen_recovery_key_setup_description" = "Votre sauvegarde est protégée par votre clé de récupération. Si vous avez besoin d’une nouvelle clé après la configuration, vous pourrez en créer une nouvelle en cliquant sur \"Changer la clé de récupération\""; +"screen_recovery_key_setup_generate_key" = "Générer la clé de récupération"; +"screen_recovery_key_setup_generate_key_description" = "Assurez-vous de pouvoir enregistrer votre clé dans un endroit sécurisé."; +"screen_recovery_key_setup_success" = "Sauvegarde mise en place avec succès"; +"screen_recovery_key_setup_title" = "Configurer la sauvegarde"; +"screen_report_content_block_user_hint" = "Cochez si vous souhaitez masquer tous les messages actuels et futurs de cet utilisateur."; +"screen_room_attachment_source_camera" = "Appareil photo"; +"screen_room_attachment_source_camera_photo" = "Prendre une photo"; +"screen_room_attachment_source_camera_video" = "Enregistrer une vidéo"; +"screen_room_attachment_source_files" = "Pièce jointe"; +"screen_room_attachment_source_gallery" = "Gallerie Photo et Vidéo"; +"screen_room_attachment_source_location" = "Position"; +"screen_room_attachment_source_poll" = "Sondage"; +"screen_room_attachment_text_formatting" = "Formatage du texte"; +"screen_room_details_add_topic_title" = "Ajouter un sujet"; +"screen_room_details_already_a_member" = "Déjà membre"; +"screen_room_details_already_invited" = "Déjà invité(e)"; +"screen_room_details_edit_room_title" = "Modifier le salon"; +"screen_room_details_edition_error" = "Une erreur inconnue s’est produite et les informations n’ont pas pu être modifiées."; +"screen_room_details_edition_error_title" = "Impossible de mettre à jour le salon"; +"screen_room_details_encryption_enabled_subtitle" = "Les messages sont sécurisés par des clés de chiffrement. Seuls vous et les destinataires possédez les clés uniques pour les déverrouiller."; +"screen_room_details_encryption_enabled_title" = "Chiffrement des messages activé"; +"screen_room_details_error_loading_notification_settings" = "Une erreur s’est produite lors du chargement des paramètres de notification."; +"screen_room_details_error_muting" = "Échec de la mise en sourdine de ce salon, veuillez réessayer."; +"screen_room_details_error_unmuting" = "Échec de la désactivation de la mise en sourdine de ce salon, veuillez réessayer."; +"screen_room_details_invite_people_title" = "Inviter des personnes"; +"screen_room_details_notification_mode_custom" = "Personnalisé"; +"screen_room_details_notification_mode_default" = "Défaut"; +"screen_room_details_notification_title" = "Notifications"; +"screen_room_details_room_name_label" = "Nom du salon"; +"screen_room_details_share_room_title" = "Partager le salon"; +"screen_room_details_updating_room" = "Mise à jour du salon…"; +"screen_room_encrypted_history_banner" = "L’historique des messages n’est actuellement pas disponible dans ce salon"; +"screen_room_encrypted_history_banner_unverified" = "L’historique de la discussion n’est pas disponible. Vérifiez cette session pour accéder à l’historique."; +"screen_room_error_failed_retrieving_user_details" = "Impossible de récupérer les détails de l’utilisateur"; +"screen_room_invite_again_alert_message" = "Souhaitez-vous inviter l’ancien membre à revenir ?"; +"screen_room_invite_again_alert_title" = "Vous êtes seul dans ce salon"; +"screen_room_member_details_block_alert_action" = "Bloquer"; +"screen_room_member_details_block_alert_description" = "Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez les débloquer à tout moment."; +"screen_room_member_details_block_user" = "Bloquer l’utilisateur"; +"screen_room_member_details_unblock_alert_action" = "Débloquer"; +"screen_room_member_details_unblock_alert_description" = "Vous pourrez à nouveau voir tous ses messages."; +"screen_room_member_details_unblock_user" = "Débloquer l’utilisateur"; +"screen_room_member_list_pending_header_title" = "En attente"; +"screen_room_member_list_room_members_header_title" = "Membres du salon"; +"screen_room_message_copied" = "Message copié"; +"screen_room_no_permission_to_post" = "Vous n’êtes pas autorisé à publier dans ce salon"; +"screen_room_notification_settings_allow_custom" = "Autoriser les paramètres personnalisés"; +"screen_room_notification_settings_allow_custom_footnote" = "L’activation de cette option annulera votre paramètre par défaut"; +"screen_room_notification_settings_custom_settings_title" = "Prévenez-moi dans ce salon pour"; +"screen_room_notification_settings_default_setting_footnote" = "Vous pouvez le modifier dans votre %1$@."; +"screen_room_notification_settings_default_setting_footnote_content_link" = "paramètres globaux"; +"screen_room_notification_settings_default_setting_title" = "Paramètre par défaut"; +"screen_room_notification_settings_edit_remove_setting" = "Supprimer le paramètre personnalisé"; +"screen_room_notification_settings_error_loading_settings" = "Une erreur s’est produite lors du chargement des paramètres de notification."; +"screen_room_notification_settings_error_restoring_default" = "Échec de la restauration du mode par défaut, veuillez réessayer."; +"screen_room_notification_settings_error_setting_mode" = "Échec de la configuration du mode, veuillez réessayer."; +"screen_room_notification_settings_mentions_only_disclaimer" = "Your homeserver does not support this option in encrypted rooms, you won't get notified in this room."; +"screen_room_notification_settings_mode_all_messages" = "Tous les messages"; +"screen_room_notification_settings_room_custom_settings_title" = "Dans ce salon, prévenez-moi pour"; +"screen_room_reactions_show_less" = "Afficher moins"; +"screen_room_reactions_show_more" = "Afficher plus"; +"screen_room_retry_send_menu_send_again_action" = "Envoyer à nouveau"; +"screen_room_retry_send_menu_title" = "Votre message n’a pas pu être envoyé"; +"screen_room_timeline_add_reaction" = "Ajouter un émoji"; +"screen_room_timeline_less_reactions" = "Afficher moins"; +"screen_room_voice_message_tooltip" = "Maintenir pour enregistrer"; +"screen_roomlist_a11y_create_message" = "Créer une nouvelle discussion ou un nouveau salon"; +"screen_roomlist_empty_message" = "Commencez par envoyer un message à quelqu’un."; +"screen_roomlist_empty_title" = "Aucune discussion pour le moment."; +"screen_roomlist_main_space_title" = "Conversations"; +"screen_server_confirmation_change_server" = "Changer de fournisseur de compte"; +"screen_server_confirmation_message_login_element_dot_io" = "Un serveur privé pour les employés d’Element."; +"screen_server_confirmation_message_login_matrix_dot_org" = "Matrix est un réseau ouvert pour une communication sécurisée et décentralisée."; +"screen_server_confirmation_message_register" = "C’est ici que vos conversations seront enregistrées, comme vous le feriez avec un fournisseur de messagerie pour conserver vos e-mails."; +"screen_server_confirmation_title_login" = "Vous êtes sur le point de vous connecter à %1$@"; +"screen_server_confirmation_title_register" = "Vous êtes sur le point de créer un compte sur %1$@"; +"screen_session_verification_cancelled_subtitle" = "Quelque chose ne va pas. Soit la demande a expiré, soit elle a été refusée."; +"screen_session_verification_compare_emojis_subtitle" = "Confirmez que les emojis ci-dessous correspondent à ceux affichés sur votre autre session."; +"screen_session_verification_compare_emojis_title" = "Comparez les émojis"; +"screen_session_verification_complete_subtitle" = "Votre nouvelle session est désormais vérifiée. Elle a accès à vos messages chiffrés et les autres utilisateurs la verront identifiée comme fiable."; +"screen_session_verification_open_existing_session_subtitle" = "Prouvez qu’il s’agit bien de vous pour accéder à l’historique de vos messages chiffrés."; +"screen_session_verification_open_existing_session_title" = "Ouvrir une session existante"; +"screen_session_verification_positive_button_canceled" = "Réessayer la vérification"; +"screen_session_verification_positive_button_initial" = "Je suis prêt.e"; +"screen_session_verification_positive_button_verifying_ongoing" = "En attente de correspondance"; +"screen_session_verification_ready_subtitle" = "Compare a unique set of emojis."; +"screen_session_verification_request_accepted_subtitle" = "Comparez les emoji uniques en veillant à ce qu’ils apparaissent dans le même ordre."; +"screen_session_verification_they_dont_match" = "Ils ne correspondent pas"; +"screen_session_verification_they_match" = "Ils correspondent"; +"screen_session_verification_waiting_to_accept_subtitle" = "Pour continuer, acceptez la demande de lancement de la procédure de vérification dans votre autre session."; +"screen_session_verification_waiting_to_accept_title" = "En attente d’acceptation de la demande"; +"screen_share_location_title" = "Partage de position"; +"screen_share_my_location_action" = "Partager ma position"; +"screen_share_open_apple_maps" = "Ouvrir dans Apple Maps"; +"screen_share_open_google_maps" = "Ouvrir dans Google Maps"; +"screen_share_open_osm_maps" = "Ouvrir dans OpenStreetMap"; +"screen_share_this_location_action" = "Partager cet position"; +"screen_signed_out_reason_1" = "Le mot de passe de votre compte a été modifié sur un autre appareil"; +"screen_signed_out_reason_2" = "Cette session a été supprimée depuis un autre appareil"; +"screen_signed_out_reason_3" = "L’administrateur de votre serveur a révoqué votre accès."; +"screen_signed_out_subtitle" = "La déconnexion peut être due à une des raisons ci-dessous. Veuillez vous connecter à nouveau pour continuer à utiliser %1$@."; +"screen_signed_out_title" = "Vous avez été déconnecté"; +"screen_signout_confirmation_dialog_content" = "Êtes-vous sûr de vouloir vous déconnecter ?"; +"screen_signout_in_progress_dialog_content" = "Déconnexion..."; +"screen_signout_key_backup_disabled_subtitle" = "Vous êtes en train de vous déconnecter de votre dernière session. Si vous vous déconnectez maintenant, vous perdrez l’accès à l’historique de vos discussions chiffrées."; +"screen_signout_key_backup_disabled_title" = "Vous avez désactivé la sauvegarde"; +"screen_signout_key_backup_offline_subtitle" = "Vos clés étaient en cours de sauvegarde lorsque vous avez perdu la connexion au réseau. Il faudrait rétablir cette connexion afin de pouvoir terminer la sauvegarde avant de vous déconnecter."; +"screen_signout_key_backup_offline_title" = "Vos clés sont en cours de sauvegarde"; +"screen_signout_key_backup_ongoing_subtitle" = "Veuillez attendre que cela se termine avant de vous déconnecter."; +"screen_signout_key_backup_ongoing_title" = "Vos clés sont en cours de sauvegarde"; +"screen_signout_recovery_disabled_subtitle" = "Vous êtes sur le point de vous déconnecter de votre dernier appareil. Si vous le faites maintenant, vous perdrez l’accès à l’historique de vos messages."; +"screen_signout_recovery_disabled_title" = "La récupération n’est pas configurée."; +"screen_signout_save_recovery_key_subtitle" = "Vous êtes sur le point de vous déconnecter de votre dernière session. Si vous le faites maintenant, vous perdrez l’accès à l’historique de vos discussions chiffrées."; +"screen_signout_save_recovery_key_title" = "Avez-vous sauvegardé votre clé de récupération?"; +"screen_start_chat_error_starting_chat" = "Une erreur s’est produite lors de la tentative de création de la discussion"; +"screen_view_location_title" = "Position"; +"screen_waitlist_message" = "Il y a une forte demande pour %1$@ sur %2$@ à l’heure actuelle. Revenez sur l’application dans quelques jours et réessayez. \n\nMerci pour votre patience !"; +"screen_waitlist_title" = "Vous y êtes presque."; +"screen_waitlist_title_success" = "Vous y êtes."; +"screen_welcome_bullet_1" = "Les appels, les sondages, les recherches et plus encore seront ajoutés plus tard cette année."; +"screen_welcome_bullet_2" = "L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour."; +"screen_welcome_bullet_3" = "N’hésitez pas à nous faire part de vos commentaires via l’écran des paramètres."; +"screen_welcome_button" = "C’est parti !"; +"screen_welcome_subtitle" = "Voici ce que vous devez savoir :"; +"screen_welcome_title" = "Bienvenue dans %1$@ !"; +"session_verification_banner_message" = "Il semblerait que vous utilisiez un nouvel appareil. Vérifiez la session avec un autre de vos appareils pour accéder à vos messages chiffrés."; +"session_verification_banner_title" = "Vérifier que c’est bien vous"; +"settings_rageshake" = "Rageshake"; +"settings_rageshake_detection_threshold" = "Seuil de détection"; +"settings_version_number" = "Version : %1$@ ( %2$@ )"; +"state_event_avatar_changed_too" = "(l’avatar a aussi été modifié)"; +"state_event_avatar_url_changed" = "%1$@ a changé son avatar"; +"state_event_avatar_url_changed_by_you" = "Vous avez changé d’avatar"; +"state_event_display_name_changed_from" = "%1$@ a changé son pseudonyme de %2$@ à %3$@"; +"state_event_display_name_changed_from_by_you" = "Vous avez changé votre pseudonyme de %1$@ à %2$@"; +"state_event_display_name_removed" = "%1$@ a supprimé son pseudonyme (c’était %2$@)"; +"state_event_display_name_removed_by_you" = "Vous avez supprimé votre pseudonyme (c’était %1$@)"; +"state_event_display_name_set" = "%1$@ a défini son pseudonyme en tant que %2$@"; +"state_event_display_name_set_by_you" = "Vous avez défini votre pseudonyme comme %1$@"; +"state_event_room_avatar_changed" = "%1$@ a changé l’avatar du salon"; +"state_event_room_avatar_changed_by_you" = "Vous avez changé l’avatar du salon"; +"state_event_room_avatar_removed" = "%1$@ a supprimé l’avatar du salon"; +"state_event_room_avatar_removed_by_you" = "Vous avez supprimé l’avatar du salon"; +"state_event_room_ban" = "%1$@ a banni %2$@"; +"state_event_room_ban_by_you" = "Vous avez banni %1$@"; +"state_event_room_created" = "%1$@ a créé le salon"; +"state_event_room_created_by_you" = "Vous avez créé le salon"; +"state_event_room_invite" = "%1$@ a invité %2$@"; +"state_event_room_invite_accepted" = "%1$@ a accepté l’invitation"; +"state_event_room_invite_accepted_by_you" = "Vous avez accepté l’invitation"; +"state_event_room_invite_by_you" = "Vous avez invité %1$@"; +"state_event_room_invite_you" = "%1$@ vous a invité(e)"; +"state_event_room_join" = "%1$@ a rejoint le salon"; +"state_event_room_join_by_you" = "Vous avez rejoint le salon"; +"state_event_room_knock" = "%1$@ a demandé à rejoindre"; +"state_event_room_knock_accepted" = "%1$@ a autorisé %2$@ à rejoindre"; +"state_event_room_knock_accepted_by_you" = "%1$@ vous a autorisé à rejoindre"; +"state_event_room_knock_by_you" = "Vous avez demandé à rejoindre"; +"state_event_room_knock_denied" = "%1$@ a rejeté la demande de %2$@ pour rejoindre"; +"state_event_room_knock_denied_by_you" = "Vous avez rejeté la demande de %1$@ pour rejoindre"; +"state_event_room_knock_denied_you" = "%1$@ a rejeté votre demande pour rejoindre"; +"state_event_room_knock_retracted" = "%1$@ n’est plus intéressé à rejoindre"; +"state_event_room_knock_retracted_by_you" = "Vous avez annulé votre demande d’adhésion"; +"state_event_room_leave" = "%1$@ a quitté le salon"; +"state_event_room_leave_by_you" = "Vous avez quitté le salon"; +"state_event_room_name_changed" = "%1$@ a changé le nom du salon en : %2$@"; +"state_event_room_name_changed_by_you" = "Vous avez changé le nom du salon en : %1$@"; +"state_event_room_name_removed" = "%1$@ a supprimé le nom du salon"; +"state_event_room_name_removed_by_you" = "Vous avez supprimé le nom du salon"; +"state_event_room_reject" = "%1$@ a rejeté l’invitation"; +"state_event_room_reject_by_you" = "Vous avez refusé l’invitation"; +"state_event_room_remove" = "%1$@ a supprimé %2$@"; +"state_event_room_remove_by_you" = "Vous avez supprimé %1$@"; +"state_event_room_third_party_invite" = "%1$@ a envoyé une invitation à %2$@ à rejoindre le salon"; +"state_event_room_third_party_invite_by_you" = "Vous avez envoyé une invitation à %1$@ pour rejoindre le salon"; +"state_event_room_third_party_revoked_invite" = "%1$@ a révoqué l’invitation de %2$@ à rejoindre le salon"; +"state_event_room_third_party_revoked_invite_by_you" = "Vous avez révoqué l’invitation de %1$@ à rejoindre le salon"; +"state_event_room_topic_changed" = "%1$@ a changé le sujet pour : %2$@"; +"state_event_room_topic_changed_by_you" = "Vous avez changé le sujet pour : %1$@"; +"state_event_room_topic_removed" = "%1$@ a supprimé le sujet du salon"; +"state_event_room_topic_removed_by_you" = "Vous avez supprimé le sujet du salon"; +"state_event_room_unban" = "%1$@ a débanni %2$@"; +"state_event_room_unban_by_you" = "Vous avez débanni %1$@"; +"state_event_room_unknown_membership_change" = "%1$@ a effectué un changement inconnu à son adhésion"; +"test_language_identifier" = "Ang."; +"test_untranslated_default_language_identifier" = "en"; +"dialog_title_error" = "Erreur"; +"dialog_title_success" = "Succès"; +"notification_fallback_content" = "Notification"; +"notification_room_action_quick_reply" = "Réponse rapide"; +"screen_room_mentions_at_room_title" = "Tout le monde"; +"screen_analytics_settings_help_us_improve" = "Partagez des données d’utilisation anonymes pour nous aider à identifier les problèmes."; +"screen_analytics_settings_read_terms" = "Vous pouvez lire toutes nos conditions %1$@."; +"screen_analytics_settings_read_terms_content_link" = "ici"; +"screen_bug_report_rash_logs_alert_title" = "%1$@ s’est arrêté la dernière fois qu’il a été utilisé. Souhaitez-vous partager un rapport d’incident avec nous ?"; +"screen_create_room_title" = "Créer un salon"; +"screen_dm_details_block_alert_action" = "Bloquer"; +"screen_dm_details_block_alert_description" = "Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez les débloquer à tout moment."; +"screen_dm_details_block_user" = "Bloquer l’utilisateur"; +"screen_dm_details_unblock_alert_action" = "Débloquer"; +"screen_dm_details_unblock_alert_description" = "Vous pourrez à nouveau voir tous ses messages."; +"screen_dm_details_unblock_user" = "Débloquer l’utilisateur"; +"screen_login_subtitle" = "Matrix est un réseau ouvert pour une communication sécurisée et décentralisée."; +"screen_report_content_block_user" = "Bloquer l’utilisateur"; +"screen_room_details_leave_room_title" = "Quitter le salon"; +"screen_room_details_security_title" = "Sécurité"; +"screen_room_details_topic_title" = "Sujet"; +"screen_room_error_failed_processing_media" = "Échec du traitement des médias à télécharger, veuillez réessayer."; +"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions et mots clés uniquement"; +"screen_signout_confirmation_dialog_submit" = "Se déconnecter"; +"screen_signout_confirmation_dialog_title" = "Se déconnecter"; +"screen_signout_preference_item" = "Se déconnecter"; +"screen_waitlist_message_success" = "Bienvenue dans %1$@ !"; diff --git a/ios/Runner/fr.lproj/Localizable.stringsdict b/ios/Runner/fr.lproj/Localizable.stringsdict new file mode 100644 index 0000000000..643a151e0b --- /dev/null +++ b/ios/Runner/fr.lproj/Localizable.stringsdict @@ -0,0 +1,230 @@ + + + + + a11y_digits_entered + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d chiffre saisi + other + %1$d chiffres saisis + + + a11y_read_receipts_multiple_with_others + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Read by %1$@ and %2$d other + other + Read by %1$@ and %2$d others + + + common_member_count + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d membre + other + %1$d membres + + + common_poll_votes_count + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d vote + other + %d votes + + + notification_compat_summary_line_for_room + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@ : %2$d message + other + %1$@ : %2$d messages + + + notification_compat_summary_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d notification + other + %d notifications + + + notification_invitations + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d invitation + other + %d invitations + + + notification_new_messages_for_room + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d nouveau message + other + %d nouveaux messages + + + notification_unread_notified_messages + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d message notifié non lu + other + %d messages notifiés non lus + + + notification_unread_notified_messages_in_room_rooms + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d salon + other + %d salons + + + room_timeline_state_changes + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d changement dans le salon + other + %1$d changements dans le salon + + + screen_app_lock_subtitle + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Il reste %1$d tentative pour déverrouiller + other + Il reste %1$d tentatives pour déverrouiller + + + screen_app_lock_subtitle_wrong_pin + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Code PIN incorrect. Il reste %1$d tentative + other + Code PIN incorrect. Il reste %1$d tentatives + + + screen_room_member_list_header_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d personne + other + %1$d personnes + + + + \ No newline at end of file diff --git a/ios/Runner/ru-RU.lproj/Localizable.strings b/ios/Runner/ru-RU.lproj/Localizable.strings index 18d97c2da4..b2545d384b 100644 --- a/ios/Runner/ru-RU.lproj/Localizable.strings +++ b/ios/Runner/ru-RU.lproj/Localizable.strings @@ -1 +1,669 @@ -"newMessageInTwake" = "You have 1 encrypted message"; +"newMessageInTwake" = "У вас одно зашифрованное сообщение"; +"Notification" = "Уведомление"; +"a11y_delete" = "Удалить"; +"a11y_hide_password" = "Скрыть пароль"; +"a11y_notifications_mentions_only" = "Только упоминания"; +"a11y_notifications_muted" = "Звук отключен"; +"a11y_pause" = "Приостановить"; +"a11y_pin_field" = "Поле PIN-кода"; +"a11y_play" = "Воспроизвести"; +"a11y_poll" = "Опрос"; +"a11y_poll_end" = "Опрос завершен"; +"a11y_read_receipts_single" = "Прочитано %1$@"; +"a11y_send_files" = "Отправить файлы"; +"a11y_show_password" = "Показать пароль"; +"a11y_start_call" = "Начать звонок"; +"a11y_user_menu" = "Меню пользователя"; +"a11y_voice_message_record" = "Записать голосовое сообщение."; +"a11y_voice_message_stop_recording" = "Остановить запись"; +"a11y.read_receipts_multiple" = "Прочитано %1$@ и %2$@"; +"action_accept" = "Разрешить"; +"action_add_to_timeline" = "Добавить в хронологию"; +"action_back" = "Назад"; +"action_cancel" = "Отмена"; +"action_choose_photo" = "Выбрать фото"; +"action_clear" = "Очистить"; +"action_close" = "Закрыть"; +"action_complete_verification" = "Полная проверка"; +"action_confirm" = "Подтвердить"; +"action_continue" = "Продолжить"; +"action_copy" = "Копировать"; +"action_copy_link" = "Скопировать ссылку"; +"action_copy_link_to_message" = "Скопировать ссылку в сообщение"; +"action_create" = "Создать"; +"action_create_a_room" = "Создать комнату"; +"action_decline" = "Отклонить"; +"action_disable" = "Отключить"; +"action_done" = "Готово"; +"action_edit" = "Редактировать"; +"action_enable" = "Включить"; +"action_end_poll" = "Завершить опрос"; +"action_enter_pin" = "Введите PIN-код"; +"action_forgot_password" = "Забыли пароль?"; +"action_forward" = "Переслать"; +"action_invite" = "Пригласить"; +"action_invite_friends" = "Пригласить друзей"; +"action_invite_friends_to_app" = "Пригласить друзей в %1$@"; +"action_invite_people_to_app" = "Пригласите пользователей в %1$@"; +"action_invites_list" = "Приглашения"; +"action_join" = "Присоединиться"; +"action_learn_more" = "Подробнее"; +"action_leave" = "Выйти"; +"action_leave_room" = "Покинуть комнату"; +"action_manage_account" = "Настройки аккаунта"; +"action_manage_devices" = "Управление устройствами"; +"action_next" = "Далее"; +"action_no" = "Нет"; +"action_not_now" = "Не сейчас"; +"action_ok" = "Ок"; +"action_open_settings" = "Открыть настройки"; +"action_open_with" = "Открыть с помощью"; +"action_quick_reply" = "Быстрый ответ"; +"action_quote" = "Цитата"; +"action_react" = "Реакция"; +"action_remove" = "Удалить"; +"action_reply" = "Ответить"; +"action_reply_in_thread" = "Ответить в теме"; +"action_report_bug" = "Сообщить об ошибке"; +"action_report_content" = "Пожаловаться на содержание"; +"action_retry" = "Повторить"; +"action_retry_decryption" = "Повторите расшифровку"; +"action_save" = "Сохранить"; +"action_search" = "Поиск"; +"action_send" = "Отправить"; +"action_send_message" = "Отправить сообщение"; +"action_share" = "Поделиться"; +"action_share_link" = "Поделиться ссылкой"; +"action_sign_in_again" = "Повторите вход"; +"action_signout" = "Выйти"; +"action_signout_anyway" = "Все равно выйти"; +"action_skip" = "Пропустить"; +"action_start" = "Начать"; +"action_start_chat" = "Начать чат"; +"action_start_verification" = "Начать подтверждение"; +"action_static_map_load" = "Нажмите, чтобы загрузить карту"; +"action_take_photo" = "Сделать фото"; +"action_tap_for_options" = "Нажмите для просмотра вариантов"; +"action_try_again" = "Повторить попытку"; +"action_view_source" = "Показать источник"; +"action_yes" = "Да"; +"action.edit_poll" = "Редактировать опрос"; +"common_about" = "О приложении"; +"common_acceptable_use_policy" = "Политика допустимого использования"; +"common_advanced_settings" = "Дополнительные параметры"; +"common_analytics" = "Аналитика"; +"common_appearance" = "Оформление"; +"common_audio" = "Аудио"; +"common_bubbles" = "Пузыри"; +"common_chat_backup" = "Резервная копия чатов"; +"common_copyright" = "Авторское право"; +"common_creating_room" = "Создание комнаты…"; +"common_current_user_left_room" = "Покинул комнату"; +"common_dark" = "Темная"; +"common_decryption_error" = "Ошибка расшифровки"; +"common_developer_options" = "Для разработчика"; +"common_edited_suffix" = "(изменено)"; +"common_editing" = "Редактирование"; +"common_emote" = "%1$@%2$@"; +"common_encryption_enabled" = "Шифрование включено"; +"common_enter_your_pin" = "Введите свой PIN-код"; +"common_error" = "Ошибка"; +"common_everyone" = "Для всех"; +"common_face_id_ios" = "Face ID"; +"common_file" = "Файл"; +"common_forward_message" = "Переслать сообщение"; +"common_gif" = "GIF"; +"common_image" = "Изображения"; +"common_in_reply_to" = "В ответ на %1$@"; +"common_invite_unknown_profile" = "Идентификатор Matrix ID не найден, приглашение может быть не получено."; +"common_leaving_room" = "Покинуть комнату"; +"common_light" = "Светлая"; +"common_link_copied_to_clipboard" = "Ссылка скопирована в буфер обмена"; +"common_loading" = "Загрузка…"; +"common_message" = "Сообщение"; +"common_message_actions" = "Действия с сообщением"; +"common_message_layout" = "Оформление сообщений"; +"common_message_removed" = "Сообщение удалено"; +"common_modern" = "Современный"; +"common_mute" = "Без звука"; +"common_no_results" = "Ничего не найдено"; +"common_offline" = "Не в сети"; +"common_optic_id_ios" = "Оптический идентификатор"; +"common_password" = "Пароль"; +"common_people" = "Пользователи"; +"common_permalink" = "Постоянная ссылка"; +"common_permission" = "Разрешение"; +"common_poll_total_votes" = "Всего голосов: %1$@"; +"common_poll_undisclosed_text" = "Результаты будут показаны после завершения опроса"; +"common_privacy_policy" = "Политика конфиденциальности"; +"common_reaction" = "Реакция"; +"common_reactions" = "Реакции"; +"common_recovery_key" = "Ключ восстановления"; +"common_refreshing" = "Обновление…"; +"common_replying_to" = "Отвечает на %1$@"; +"common_report_a_bug" = "Сообщить об ошибке"; +"common_report_a_problem" = "Сообщить о проблеме"; +"common_report_submitted" = "Отчет отправлен"; +"common_rich_text_editor" = "Редактор форматированного текста"; +"common_room" = "Комната"; +"common_room_name" = "Название комнаты"; +"common_room_name_placeholder" = "например, название вашего проекта"; +"common_screen_lock" = "Блокировка экрана"; +"common_search_for_someone" = "Поиск человека"; +"common_search_results" = "Результаты поиска"; +"common_security" = "Безопасность"; +"common_seen_by" = "Просмотрено"; +"common_sending" = "Отправка…"; +"common_sending_failed" = "Сбой отправки"; +"common_sent" = "Отправлено"; +"common_server_not_supported" = "Сервер не поддерживается"; +"common_server_url" = "Адрес сервера"; +"common_settings" = "Настройки"; +"common_shared_location" = "Делится местонахождением"; +"common_signing_out" = "Выход…"; +"common_starting_chat" = "Начало чата…"; +"common_sticker" = "Стикер"; +"common_success" = "Успешно"; +"common_suggestions" = "Рекомендации"; +"common_syncing" = "Синхронизация"; +"common_system" = "Системная"; +"common_text" = "Текст"; +"common_third_party_notices" = "Уведомление о третьей стороне"; +"common_thread" = "Обсуждение"; +"common_topic" = "Тема"; +"common_topic_placeholder" = "О чем эта комната?"; +"common_touch_id_ios" = "Touch ID"; +"common_unable_to_decrypt" = "Невозможно расшифровать"; +"common_unable_to_invite_message" = "Не удалось отправить приглашения одному или нескольким пользователям."; +"common_unable_to_invite_title" = "Не удалось отправить приглашение(я)"; +"common_unlock" = "Разблокировать"; +"common_unmute" = "Включить звук"; +"common_unsupported_event" = "Неподдерживаемое событие"; +"common_username" = "Имя пользователя"; +"common_verification_cancelled" = "Проверка отменена"; +"common_verification_complete" = "Проверка завершена"; +"common_video" = "Видео"; +"common_voice_message" = "Голосовое сообщение"; +"common_waiting" = "Ожидание…"; +"common_waiting_for_decryption_key" = "Ожидание ключа расшифровки"; +"common_poll_end_confirmation" = "Вы действительно хотите завершить данный опрос?"; +"common_poll_summary" = "Опрос: %1$@"; +"common_verify_device" = "Подтверждение устройства"; +"confirm_recovery_key_banner_message" = "В настоящее время резервная копия вашего чата не синхронизирована. Требуется подтвердить вашим ключом восстановления, чтобы сохранить доступ к резервной копии чата."; +"confirm_recovery_key_banner_title" = "Подтвердите ключ восстановления"; +"crash_detection_dialog_content" = "При последнем использовании %1$@ произошел сбой. Хотите поделиться отчетом о сбое?"; +"dialog_permission_camera" = "Чтобы приложение могло использовать камеру, предоставьте разрешение в системных настройках."; +"dialog_permission_generic" = "Пожалуйста, предоставьте разрешение в системных настройках."; +"dialog_permission_location_description_ios" = "Предоставьте доступ в меню «Настройки» -> «Местоположение»."; +"dialog_permission_location_title_ios" = "%1$@ не имеет доступа к вашему местоположению."; +"dialog_permission_microphone" = "Чтобы приложение могло использовать микрофон, предоставьте разрешение в системных настройках."; +"dialog_permission_microphone_description_ios" = "Предоставьте доступ, чтобы вы могли записывать и отправлять сообщения со звуком."; +"dialog_permission_microphone_title_ios" = "%1$@ требуется разрешение на доступ к микрофону."; +"dialog_permission_notification" = "Чтобы приложение отображало уведомления, предоставьте разрешение в системных настройках."; +"dialog_title_confirmation" = "Подтверждение"; +"dialog_title_warning" = "Предупреждение"; +"emoji_picker_category_activity" = "Деятельность"; +"emoji_picker_category_flags" = "Флаги"; +"emoji_picker_category_foods" = "Еда и напитки"; +"emoji_picker_category_nature" = "Животные и природа"; +"emoji_picker_category_objects" = "Объекты"; +"emoji_picker_category_people" = "Смайлы и люди"; +"emoji_picker_category_places" = "Путешествия и места"; +"emoji_picker_category_symbols" = "Символы"; +"error_failed_creating_the_permalink" = "Не удалось создать постоянную ссылку"; +"error_failed_loading_map" = "Не удалось загрузить карту %1$@. Пожалуйста, повторите попытку позже."; +"error_failed_loading_messages" = "Не удалось загрузить сообщения"; +"error_failed_locating_user" = "%1$@ не удалось получить доступ к вашему местоположению. Пожалуйста, повторите попытку позже."; +"error_failed_uploading_voice_message" = "Не удалось загрузить голосовое сообщение."; +"error_no_compatible_app_found" = "Не найдено совместимое приложение для обработки этого действия."; +"error_some_messages_have_not_been_sent" = "Некоторые сообщения не были отправлены"; +"error_unknown" = "Извините, произошла ошибка"; +"invite_friends_rich_title" = "🔐️ Присоединяйтесь ко мне в %1$@"; +"invite_friends_text" = "Привет, поговори со мной по %1$@: %2$@"; +"leave_room_alert_empty_subtitle" = "Вы уверены, что хотите покинуть эту комнату? Вы здесь единственный человек. Если вы уйдете, никто не сможет присоединиться в будущем, включая вас."; +"leave_room_alert_private_subtitle" = "Вы уверены, что хотите покинуть эту комнату? Эта комната не является публичной, и Вы не сможете присоединиться к ней без приглашения."; +"leave_room_alert_subtitle" = "Вы уверены, что хотите покинуть комнату?"; +"login_initial_device_name_ios" = "%1$@ iOS"; +"notification_channel_call" = "Позвонить"; +"notification_channel_listening_for_events" = "Прослушивание событий"; +"notification_channel_noisy" = "Шумные уведомления"; +"notification_channel_silent" = "Бесшумные уведомления"; +"notification_inline_reply_failed" = "** Не удалось отправить - пожалуйста, откройте комнату"; +"notification_invitation_action_join" = "Присоединиться"; +"notification_invitation_action_reject" = "Отклонить"; +"notification_invite_body" = "Пригласил вас в чат"; +"notification_mentioned_you_body" = "%1$@ упомянул вас.\n%2$@"; +"notification_mentioned_you_fallback_body" = "Вас уже упомянули.\n%1$@"; +"notification_new_messages" = "Новые сообщения"; +"notification_reaction_body" = "Отреагировал на %1$@"; +"notification_room_action_mark_as_read" = "Отметить как прочитанное"; +"notification_room_invite_body" = "Пригласил вас в комнату"; +"notification_sender_me" = "Я"; +"notification_test_push_notification_content" = "Вы просматриваете уведомление! Нажмите на меня!"; +"notification_ticker_text_dm" = "%1$@: %2$@"; +"notification_ticker_text_group" = "%1$@: %2$@ %3$@"; +"notification_unread_notified_messages_and_invitation" = "%1$@ и %2$@"; +"notification_unread_notified_messages_in_room" = "%1$@ в %2$@"; +"notification_unread_notified_messages_in_room_and_invitation" = "%1$@ в %2$@ и %3$@"; +"preference_rageshake" = "Rageshake сообщит об ошибке"; +"rageshake_detection_dialog_content" = "Похоже, что вы трясете телефон. Хотите открыть экран сообщения об ошибке?"; +"report_content_explanation" = "Это сообщение будет передано администратору вашего домашнего сервера. Они не смогут прочитать зашифрованные сообщения."; +"report_content_hint" = "Причина, по которой вы пожаловались на этот контент"; +"rich_text_editor_bullet_list" = "Переключить список маркеров"; +"rich_text_editor_close_formatting_options" = "Закрыть параметры форматирования"; +"rich_text_editor_code_block" = "Переключить блок кода"; +"rich_text_editor_composer_placeholder" = "Сообщение"; +"rich_text_editor_create_link" = "Создать ссылку"; +"rich_text_editor_edit_link" = "Редактировать ссылку"; +"rich_text_editor_format_bold" = "Применить жирный шрифт"; +"rich_text_editor_format_italic" = "Применить курсивный формат"; +"rich_text_editor_format_strikethrough" = "Применить формат зачеркивания"; +"rich_text_editor_format_underline" = "Применить формат подчеркивания"; +"rich_text_editor_full_screen_toggle" = "Переключение полноэкранного режима"; +"rich_text_editor_indent" = "Отступ"; +"rich_text_editor_inline_code" = "Применить встроенный формат кода"; +"rich_text_editor_link" = "Установить ссылку"; +"rich_text_editor_numbered_list" = "Переключить нумерованный список"; +"rich_text_editor_open_compose_options" = "Открыть параметры компоновки"; +"rich_text_editor_quote" = "Переключить цитату"; +"rich_text_editor_remove_link" = "Удалить ссылку"; +"rich_text_editor_unindent" = "Без отступа"; +"rich_text_editor_url_placeholder" = "Ссылка"; +"rich_text_editor_a11y_add_attachment" = "Прикрепить файл"; +"room_timeline_beginning_of_room" = "Это начало %1$@."; +"room_timeline_beginning_of_room_no_name" = "Это начало разговора."; +"room_timeline_read_marker_title" = "Новый"; +"screen_advanced_settings_element_call_base_url" = "Базовый URL сервера звонков Element"; +"screen_advanced_settings_element_call_base_url_description" = "Задайте свой сервер Element Call."; +"screen_advanced_settings_element_call_base_url_validation_error" = "Адрес указан неверно, удостоверьтесь, что вы указали протокол (http/https) и правильный адрес."; +"screen_room_mentions_at_room_subtitle" = "Уведомить всю комнату"; +"screen_account_provider_change" = "Переключить аккаунт"; +"screen_account_provider_form_hint" = "Адрес домашнего сервера"; +"screen_account_provider_form_notice" = "Введите поисковый запрос или адрес домена."; +"screen_account_provider_form_subtitle" = "Поиск компании, сообщества или частного сервера."; +"screen_account_provider_form_title" = "Поиск сервера учетной записи"; +"screen_account_provider_signin_subtitle" = "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."; +"screen_account_provider_signin_title" = "Вы собираетесь войти в %@"; +"screen_account_provider_signup_subtitle" = "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."; +"screen_account_provider_signup_title" = "Вы собираетесь создать учетную запись на %@"; +"screen_advanced_settings_developer_mode" = "Режим разработчика"; +"screen_advanced_settings_developer_mode_description" = "Предоставьте разработчикам доступ к функциям и функциональным возможностям."; +"screen_advanced_settings_rich_text_editor_description" = "Отключить редактор форматированного текста и включить Markdown."; +"screen_analytics_prompt_data_usage" = "Мы не будем записывать или профилировать какие-либо персональные данные"; +"screen_analytics_prompt_help_us_improve" = "Предоставлять анонимные данные об использовании, чтобы помочь нам выявить проблемы."; +"screen_analytics_prompt_read_terms" = "Вы можете ознакомиться со всеми нашими условиями %1$@."; +"screen_analytics_prompt_read_terms_content_link" = "здесь"; +"screen_analytics_prompt_settings" = "Вы можете отключить эту функцию в любое время"; +"screen_analytics_prompt_third_party_sharing" = "Мы не будем передавать ваши данные третьим лицам"; +"screen_analytics_prompt_title" = "Помогите улучшить %1$@"; +"screen_analytics_settings_share_data" = "Делитесь данными аналитики"; +"screen_app_lock_biometric_authentication" = "биометрическая идентификация"; +"screen_app_lock_biometric_unlock" = "биометрическая разблокировать"; +"screen_app_lock_biometric_unlock_reason_ios" = "Для доступа к приложению необходима аутентификация"; +"screen_app_lock_forgot_pin" = "Забыли PIN-код?"; +"screen_app_lock_settings_change_pin" = "Измените PIN-код"; +"screen_app_lock_settings_enable_biometric_unlock" = "Разрешить биометрическую разблокировать"; +"screen_app_lock_settings_enable_face_id_ios" = "Разрешить Face ID"; +"screen_app_lock_settings_enable_optic_id_ios" = "Разрешить Optic ID"; +"screen_app_lock_settings_enable_touch_id_ios" = "Разрешить Touch ID"; +"screen_app_lock_settings_remove_pin" = "Удалить PIN-код"; +"screen_app_lock_settings_remove_pin_alert_message" = "Вы действительно хотите удалить PIN-код?"; +"screen_app_lock_settings_remove_pin_alert_title" = "Удалить PIN-код?"; +"screen_app_lock_setup_biometric_unlock_allow_title" = "Разрешить %1$@"; +"screen_app_lock_setup_biometric_unlock_skip" = "Я бы предпочел использовать PIN-код"; +"screen_app_lock_setup_biometric_unlock_subtitle" = "Сэкономьте время и используйте %1$@ для разблокировки приложения"; +"screen_app_lock_setup_choose_pin" = "Выберите PIN-код"; +"screen_app_lock_setup_confirm_pin" = "Подтвердите PIN-код"; +"screen_app_lock_setup_pin_blacklisted_dialog_content" = "Из соображений безопасности вы не можешь выбрать это в качестве PIN-кода"; +"screen_app_lock_setup_pin_blacklisted_dialog_title" = "Выберите другой PIN-код"; +"screen_app_lock_setup_pin_context" = "Заблокируйте %1$@, чтобы повысить безопасность ваших чатов.\n\nВыберите что-нибудь незабываемое. Если вы забудете этот PIN-код, вы выйдете из приложения."; +"screen_app_lock_setup_pin_mismatch_dialog_content" = "Повторите PIN-код"; +"screen_app_lock_setup_pin_mismatch_dialog_title" = "PIN-коды не совпадают"; +"screen_app_lock_signout_alert_message" = "Чтобы продолжить, вам необходимо повторно войти в систему и создать новый PIN-код"; +"screen_app_lock_signout_alert_title" = "Вы выходите из системы"; +"screen_bug_report_attach_screenshot" = "Приложить снимок экрана"; +"screen_bug_report_contact_me" = "Вы можете связаться со мной, если у Вас возникнут какие-либо дополнительные вопросы."; +"screen_bug_report_contact_me_title" = "Связаться со мной"; +"screen_bug_report_edit_screenshot" = "Редактировать снимок экрана"; +"screen_bug_report_editor_description" = "Пожалуйста, опишите ошибку. Что вы сделали? Какое поведение вы ожидали? Что произошло на самом деле. Пожалуйста, опишите все как можно подробнее."; +"screen_bug_report_editor_placeholder" = "Опишите проблему…"; +"screen_bug_report_editor_supporting" = "Если возможно, пожалуйста, напишите описание на английском языке."; +"screen_bug_report_include_crash_logs" = "Отправка журналов сбоев"; +"screen_bug_report_include_logs" = "Разрешить ведение журналов"; +"screen_bug_report_include_screenshot" = "Отправить снимок экрана"; +"screen_bug_report_logs_description" = "Чтобы убедиться, что все работает правильно, в сообщение будут включены журналы. Чтобы отправить сообщение без журналов, отключите эту настройку."; +"screen_change_account_provider_matrix_org_subtitle" = "Matrix.org — это большой бесплатный сервер в общедоступной сети Matrix для безопасной децентрализованной связи, управляемый Matrix.org Foundation."; +"screen_change_account_provider_other" = "Другое"; +"screen_change_account_provider_subtitle" = "Используйте другого поставщика учетных записей, например, собственный частный сервер или рабочую учетную запись."; +"screen_change_account_provider_title" = "Сменить поставщика учетной записи"; +"screen_change_server_error_invalid_homeserver" = "Нам не удалось связаться с этим домашним сервером. Убедитесь, что вы правильно ввели URL-адрес домашнего сервера. Если URL-адрес указан правильно, обратитесь к администратору домашнего сервера за дополнительной помощью."; +"screen_change_server_error_no_sliding_sync_message" = "К сожалению данный сервер не поддерживает sliding sync."; +"screen_change_server_form_header" = "URL-адрес домашнего сервера"; +"screen_change_server_form_notice" = "Вы можете подключиться только к существующему серверу, поддерживающему sliding sync. Администратору домашнего сервера потребуется настроить его. %1$@"; +"screen_change_server_subtitle" = "Какой адрес у вашего сервера?"; +"screen_change_server_title" = "Выберите свой сервер"; +"screen_chat_backup_key_backup_action_disable" = "Отключить резервное копирование"; +"screen_chat_backup_key_backup_action_enable" = "Включить резервное копирование"; +"screen_chat_backup_key_backup_description" = "Резервное копирование гарантирует, что вы не потеряете историю сообщений. %1$@."; +"screen_chat_backup_key_backup_title" = "Резервное копирование"; +"screen_chat_backup_recovery_action_change" = "Изменить ключ восстановления"; +"screen_chat_backup_recovery_action_confirm" = "Подтвердить ключ восстановления"; +"screen_chat_backup_recovery_action_confirm_description" = "Резервная копия чата в настоящее время не синхронизирована."; +"screen_chat_backup_recovery_action_setup" = "Настроить восстановление"; +"screen_chat_backup_recovery_action_setup_description" = "Получите доступ к зашифрованным сообщениям, если вы потеряете все свои устройства или выйдете из системы %1$@ отовсюду."; +"screen_create_poll_add_option_btn" = "Добавить опцию"; +"screen_create_poll_anonymous_desc" = "Показывать результаты только после окончания опроса"; +"screen_create_poll_anonymous_headline" = "Анонимный опрос"; +"screen_create_poll_answer_hint" = "Настройка %1$d"; +"screen_create_poll_discard_confirmation" = "Вы действительно хотите отменить этот опрос?"; +"screen_create_poll_discard_confirmation_title" = "Отменить опрос"; +"screen_create_poll_question_desc" = "Вопрос или тема"; +"screen_create_poll_question_hint" = "Тема опроса?"; +"screen_create_poll_title" = "Создать опрос"; +"screen_create_room_action_create_room" = "Новая комната"; +"screen_create_room_action_invite_people" = "Пригласите друзей в Element"; +"screen_create_room_add_people_title" = "Пригласить людей"; +"screen_create_room_error_creating_room" = "Произошла ошибка при создании комнаты"; +"screen_create_room_private_option_description" = "Сообщения в этой комнате зашифрованы. Отключить шифрование впоследствии невозможно."; +"screen_create_room_private_option_title" = "Приватная комната (только по приглашению)"; +"screen_create_room_public_option_description" = "Сообщения не зашифрованы, и каждый может их прочитать. Вы можете включить шифрование позже."; +"screen_create_room_public_option_title" = "Публичная комната (любой)"; +"screen_create_room_room_name_label" = "Название комнаты"; +"screen_create_room_topic_label" = "Тема (необязательно)"; +"screen_edit_profile_display_name" = "Отображаемое имя"; +"screen_edit_profile_display_name_placeholder" = "Ваше отображаемое имя"; +"screen_edit_profile_error" = "Произошла неизвестная ошибка, изменить информацию не удалось."; +"screen_edit_profile_error_title" = "Невозможно обновить профиль"; +"screen_edit_profile_title" = "Редактировать профиль"; +"screen_edit_profile_updating_details" = "Обновление профиля…"; +"screen_invites_decline_chat_message" = "Вы уверены, что хотите отклонить приглашение в %1$@?"; +"screen_invites_decline_chat_title" = "Отклонить приглашение"; +"screen_invites_decline_direct_chat_message" = "Вы уверены, что хотите отказаться от приватного общения с %1$@?"; +"screen_invites_decline_direct_chat_title" = "Отклонить чат"; +"screen_invites_empty_list" = "Нет приглашений"; +"screen_invites_invited_you" = "%1$@ (%2$@) пригласил вас"; +"screen_key_backup_disable_confirmation_action_turn_off" = "Выключить"; +"screen_key_backup_disable_confirmation_description" = "Вы потеряете зашифрованные сообщения, если выйдете из всех устройств."; +"screen_key_backup_disable_confirmation_title" = "Вы действительно хотите отключить резервное копирование?"; +"screen_key_backup_disable_description" = "Отключение резервного копирования удалит текущую резервную копию ключа шифрования и отключит другие функции безопасности. В этом случае вы выполните следующие действия:"; +"screen_key_backup_disable_description_point_1" = "Нет зашифрованной истории сообщений на новых устройствах"; +"screen_key_backup_disable_description_point_2" = "Потерять доступ к зашифрованным сообщениям, если вы вышли из %1$@ любой точки мира"; +"screen_key_backup_disable_title" = "Вы действительно хотите отключить резервное копирование?"; +"screen_login_error_deactivated_account" = "Данная учетная запись была деактивирована."; +"screen_login_error_invalid_credentials" = "Неверное имя пользователя и/или пароль"; +"screen_login_error_invalid_user_id" = "Это не корректный идентификатор пользователя. Ожидаемый формат: '@user:homeserver.org'"; +"screen_login_error_unsupported_authentication" = "Выбранный домашний сервер не поддерживает пароль или логин OIDC. Пожалуйста, свяжитесь с администратором или выберите другой домашний сервер."; +"screen_login_form_header" = "Введите сведения о себе"; +"screen_login_title" = "Рады видеть вас снова!"; +"screen_login_title_with_homeserver" = "Войти в %1$@"; +"screen_media_picker_error_failed_selection" = "Не удалось выбрать носитель, попробуйте еще раз."; +"screen_media_upload_preview_error_failed_processing" = "Не удалось обработать медиафайл для загрузки, попробуйте еще раз."; +"screen_media_upload_preview_error_failed_sending" = "Не удалось загрузить медиафайлы, попробуйте еще раз."; +"screen_migration_message" = "Это одноразовый процесс, спасибо, что подождали."; +"screen_migration_title" = "Настройка учетной записи."; +"screen_notification_optin_subtitle" = "Вы можете изменить настройки позже."; +"screen_notification_optin_title" = "Разрешите уведомления и никогда не пропустите сообщение"; +"screen_notification_settings_additional_settings_section_title" = "Дополнительные параметры"; +"screen_notification_settings_calls_label" = "Аудио и видео звонки"; +"screen_notification_settings_configuration_mismatch" = "Несоответствие конфигурации"; +"screen_notification_settings_configuration_mismatch_description" = "Мы упростили настройки уведомлений, чтобы упростить поиск опций. Некоторые пользовательские настройки, выбранные вами ранее, не отображаются в данном меню, но они все еще активны. \n\nЕсли вы продолжите, некоторые настройки могут быть изменены."; +"screen_notification_settings_direct_chats" = "Прямые чаты"; +"screen_notification_settings_edit_custom_settings_section_title" = "Индивидуальные настройки для каждого чата"; +"screen_notification_settings_edit_failed_updating_default_mode" = "При обновлении настроек уведомления произошла ошибка."; +"screen_notification_settings_edit_mode_all_messages" = "Все сообщения"; +"screen_notification_settings_edit_mode_mentions_and_keywords" = "Только упоминания и ключевые слова"; +"screen_notification_settings_edit_screen_direct_section_header" = "Уведомлять меня в личных чатах"; +"screen_notification_settings_edit_screen_group_section_header" = "Уведомлять меня в групповых чатах"; +"screen_notification_settings_enable_notifications" = "Включить уведомления на данном устройстве"; +"screen_notification_settings_failed_fixing_configuration" = "Конфигурация не была исправлена, попробуйте еще раз."; +"screen_notification_settings_group_chats" = "Групповые чаты"; +"screen_notification_settings_mentions_only_disclaimer" = "Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, в некоторых комнатах вы можете не получать уведомления."; +"screen_notification_settings_mentions_section_title" = "Упоминания"; +"screen_notification_settings_mode_all" = "Все"; +"screen_notification_settings_mode_mentions" = "Упоминания"; +"screen_notification_settings_notification_section_title" = "Уведомить меня"; +"screen_notification_settings_room_mention_label" = "Уведомить меня в @room"; +"screen_notification_settings_system_notifications_action_required" = "Чтобы получать уведомления, измените свой %1$@."; +"screen_notification_settings_system_notifications_action_required_content_link" = "настройки системы"; +"screen_notification_settings_system_notifications_turned_off" = "Системные уведомления выключены"; +"screen_notification_settings_title" = "Уведомления"; +"screen_onboarding_sign_in_manually" = "Вход в систему вручную"; +"screen_onboarding_sign_in_with_qr_code" = "Войти с помощью QR-кода"; +"screen_onboarding_sign_up" = "Создать учетную запись"; +"screen_onboarding_welcome_message" = "Добро пожаловать в самый быстрый Element. Преимущество в скорости и простоте."; +"screen_onboarding_welcome_subtitle" = "Добро пожаловать в %1$@. Supercharged — это скорость и простота."; +"screen_onboarding_welcome_title" = "Будь c element"; +"screen_recovery_key_change_description" = "Получите новый ключ восстановления, если вы потеряли существующий. После смены ключа восстановления старый ключ больше не будет работать."; +"screen_recovery_key_change_generate_key" = "Создать новый ключ восстановления"; +"screen_recovery_key_change_generate_key_description" = "Убедитесь, что вы можете хранить ключ восстановления в безопасном месте"; +"screen_recovery_key_change_success" = "Ключ восстановления изменен"; +"screen_recovery_key_change_title" = "Изменить ключ восстановления?"; +"screen_recovery_key_confirm_description" = "Введите ключ восстановления, чтобы подтвердить доступ к резервной копии чата."; +"screen_recovery_key_confirm_error_content" = "Пожалуйста, попробуйте еще раз, чтобы подтвердить доступ к резервной копии чата."; +"screen_recovery_key_confirm_error_title" = "Неверный ключ восстановления"; +"screen_recovery_key_confirm_key_description" = "Введите 48 значный код."; +"screen_recovery_key_confirm_key_placeholder" = "Вход..."; +"screen_recovery_key_confirm_success" = "Ключ восстановления подтвержден"; +"screen_recovery_key_confirm_title" = "Подтвердите ключ восстановления"; +"screen_recovery_key_copied_to_clipboard" = "Ключ восстановления скопирован"; +"screen_recovery_key_generating_key" = "Генерация…"; +"screen_recovery_key_save_action" = "Сохранить ключ восстановления"; +"screen_recovery_key_save_description" = "Запишите ключ восстановления в безопасном месте или сохраните его в менеджере паролей."; +"screen_recovery_key_save_key_description" = "Нажмите, чтобы скопировать ключ восстановления"; +"screen_recovery_key_save_title" = "Сохраните ключ восстановления"; +"screen_recovery_key_setup_confirmation_description" = "После этого шага вы не сможете получить доступ к новому ключу восстановления."; +"screen_recovery_key_setup_confirmation_title" = "Вы сохранили ключ восстановления?"; +"screen_recovery_key_setup_description" = "Резервная копия чата защищена ключом восстановления. Если после настройки вам понадобится новый ключ восстановления, вы можете создать его заново, выбрав «Изменить ключ восстановления»."; +"screen_recovery_key_setup_generate_key" = "Сгенерируйте свой ключ восстановления"; +"screen_recovery_key_setup_generate_key_description" = "Убедитесь, что вы можете хранить ключ восстановления в безопасном месте"; +"screen_recovery_key_setup_success" = "Настройка восстановления выполнена успешно"; +"screen_recovery_key_setup_title" = "Настроить восстановление"; +"screen_report_content_block_user_hint" = "Отметьте, хотите ли вы скрыть все текущие и будущие сообщения от этого пользователя"; +"screen_room_attachment_source_camera" = "Камера"; +"screen_room_attachment_source_camera_photo" = "Сделать фото"; +"screen_room_attachment_source_camera_video" = "Записать видео"; +"screen_room_attachment_source_files" = "Вложение"; +"screen_room_attachment_source_gallery" = "Фото и видео"; +"screen_room_attachment_source_location" = "Местоположение"; +"screen_room_attachment_source_poll" = "Опрос"; +"screen_room_attachment_text_formatting" = "Форматирование текста"; +"screen_room_details_add_topic_title" = "Добавить тему"; +"screen_room_details_already_a_member" = "Уже зарегистрирован"; +"screen_room_details_already_invited" = "Уже приглашены"; +"screen_room_details_edit_room_title" = "Редактировать комнату"; +"screen_room_details_edition_error" = "Произошла неизвестная ошибка, и информацию нельзя было изменить."; +"screen_room_details_edition_error_title" = "Не удалось обновить комнату"; +"screen_room_details_encryption_enabled_subtitle" = "Сообщения зашифрованы. Только у вас и у получателей есть уникальные ключи для их разблокировки."; +"screen_room_details_encryption_enabled_title" = "Шифрование сообщений включено"; +"screen_room_details_error_loading_notification_settings" = "При загрузке настроек уведомлений произошла ошибка."; +"screen_room_details_error_muting" = "Не удалось отключить звук в этой комнате, попробуйте еще раз."; +"screen_room_details_error_unmuting" = "Не удалось включить звук в эту комнату, попробуйте еще раз."; +"screen_room_details_invite_people_title" = "Пригласить участника"; +"screen_room_details_notification_mode_custom" = "Пользовательский"; +"screen_room_details_notification_mode_default" = "По умолчанию"; +"screen_room_details_notification_title" = "Уведомления"; +"screen_room_details_room_name_label" = "Название комнаты"; +"screen_room_details_share_room_title" = "Поделиться комнатой"; +"screen_room_details_updating_room" = "Обновление комнаты…"; +"screen_room_encrypted_history_banner" = "В настоящее время история сообщений недоступна в этой комнате."; +"screen_room_encrypted_history_banner_unverified" = "История сообщений в этой комнате недоступна. Проверьте это устройство, чтобы увидеть историю сообщений."; +"screen_room_error_failed_retrieving_user_details" = "Не удалось получить данные о пользователе"; +"screen_room_invite_again_alert_message" = "Хотите пригласить их снова?"; +"screen_room_invite_again_alert_title" = "Вы одни в этой комнате"; +"screen_room_member_details_block_alert_action" = "Заблокировать"; +"screen_room_member_details_block_alert_description" = "Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время."; +"screen_room_member_details_block_user" = "Заблокировать пользователя"; +"screen_room_member_details_unblock_alert_action" = "Разблокировать"; +"screen_room_member_details_unblock_alert_description" = "Вы снова сможете увидеть все сообщения."; +"screen_room_member_details_unblock_user" = "Разблокировать пользователя"; +"screen_room_member_list_pending_header_title" = "В ожидании"; +"screen_room_member_list_room_members_header_title" = "Участники комнаты"; +"screen_room_message_copied" = "Сообщение скопировано"; +"screen_room_no_permission_to_post" = "У вас нет разрешения публиковать сообщения в этой комнате"; +"screen_room_notification_settings_allow_custom" = "Разрешить пользовательские настройки"; +"screen_room_notification_settings_allow_custom_footnote" = "Включение этого параметра отменяет настройки по умолчанию"; +"screen_room_notification_settings_custom_settings_title" = "Уведомить меня в этом чате"; +"screen_room_notification_settings_default_setting_footnote" = "Вы можете изменить его в своем %1$@."; +"screen_room_notification_settings_default_setting_footnote_content_link" = "основные настройки"; +"screen_room_notification_settings_default_setting_title" = "Настройка по умолчанию"; +"screen_room_notification_settings_edit_remove_setting" = "Удалить пользовательскую настройку"; +"screen_room_notification_settings_error_loading_settings" = "Произошла ошибка при загрузке настроек уведомлений."; +"screen_room_notification_settings_error_restoring_default" = "Не удалось восстановить режим по умолчанию, попробуйте еще раз."; +"screen_room_notification_settings_error_setting_mode" = "Не удалось настроить режим, попробуйте еще раз."; +"screen_room_notification_settings_mentions_only_disclaimer" = "Ваш домашний сервер не поддерживает эту опцию в зашифрованных комнатах, вы не будете получать уведомления в этой комнате."; +"screen_room_notification_settings_mode_all_messages" = "Все сообщения"; +"screen_room_notification_settings_room_custom_settings_title" = "В этой комнате уведомить меня о"; +"screen_room_reactions_show_less" = "Показать меньше"; +"screen_room_reactions_show_more" = "Показать больше"; +"screen_room_retry_send_menu_send_again_action" = "Отправить снова"; +"screen_room_retry_send_menu_title" = "Не удалось отправить ваше сообщение"; +"screen_room_timeline_add_reaction" = "Добавить эмодзи"; +"screen_room_timeline_less_reactions" = "Показать меньше"; +"screen_room_voice_message_tooltip" = "Удерживайте для записи"; +"screen_roomlist_a11y_create_message" = "Создайте новую беседу или комнату"; +"screen_roomlist_empty_message" = "Начните переписку с отправки сообщения."; +"screen_roomlist_empty_title" = "Пока нет доступных чатов."; +"screen_roomlist_main_space_title" = "Все чаты"; +"screen_server_confirmation_change_server" = "Сменить учетную запись"; +"screen_server_confirmation_message_login_element_dot_io" = "Частный сервер для сотрудников Element."; +"screen_server_confirmation_message_login_matrix_dot_org" = "Matrix — это открытая сеть для безопасной децентрализованной связи."; +"screen_server_confirmation_message_register" = "Здесь будут храниться ваши разговоры - точно так же, как вы используете почтового провайдера для хранения своих писем."; +"screen_server_confirmation_title_login" = "Вы собираетесь войти в %1$@"; +"screen_server_confirmation_title_register" = "Вы собираетесь создать учетную запись на %1$@"; +"screen_session_verification_cancelled_subtitle" = "Кажется, что-то не так. Время ожидания запроса истекло, либо запрос был отклонен."; +"screen_session_verification_compare_emojis_subtitle" = "Убедитесь, что приведенные ниже смайлики совпадают со смайликами, показанными во время другого сеанса."; +"screen_session_verification_compare_emojis_title" = "Сравните емодзи"; +"screen_session_verification_complete_subtitle" = "Ваш новый сеанс подтвержден. У него есть доступ к вашим зашифрованным сообщениям, и другие пользователи увидят его как доверенное."; +"screen_session_verification_open_existing_session_subtitle" = "Чтобы получить доступ к зашифрованной истории сообщений, докажите, что это вы."; +"screen_session_verification_open_existing_session_title" = "Открыть существующий сеанс"; +"screen_session_verification_positive_button_canceled" = "Повторить проверку"; +"screen_session_verification_positive_button_initial" = "Я готов"; +"screen_session_verification_positive_button_verifying_ongoing" = "Ожидание соответствия"; +"screen_session_verification_ready_subtitle" = "Сравните уникальный набор эмодзи."; +"screen_session_verification_request_accepted_subtitle" = "Сравните уникальные смайлики, убедившись, что они расположены в том же порядке."; +"screen_session_verification_they_dont_match" = "Они не совпадают"; +"screen_session_verification_they_match" = "Они совпадают"; +"screen_session_verification_waiting_to_accept_subtitle" = "Для продолжения работы примите запрос на запуск процесса проверки в другом сеансе."; +"screen_session_verification_waiting_to_accept_title" = "Ожидание принятия запроса"; +"screen_share_location_title" = "Поделиться местоположением"; +"screen_share_my_location_action" = "Поделиться моим местоположением"; +"screen_share_open_apple_maps" = "Открыть в Apple Maps"; +"screen_share_open_google_maps" = "Открыть в Google Maps"; +"screen_share_open_osm_maps" = "Открыть в OpenStreetMap"; +"screen_share_this_location_action" = "Поделиться этим местоположением"; +"screen_signed_out_reason_1" = "Вы изменили свой пароль в другой сессии"; +"screen_signed_out_reason_2" = "Вы удалили сессию из другой сессии"; +"screen_signed_out_reason_3" = "Администратор вашего сервера аннулировал ваш доступ"; +"screen_signed_out_subtitle" = "Возможно, вы вышли из системы по одной из причин, перечисленных ниже. Пожалуйста, войдите в систему еще раз, чтобы продолжить использование %@."; +"screen_signed_out_title" = "Вы вышли из системы"; +"screen_signout_confirmation_dialog_content" = "Вы уверены, что вы хотите выйти?"; +"screen_signout_in_progress_dialog_content" = "Выполняется выход…"; +"screen_signout_key_backup_disabled_subtitle" = "Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы потеряете доступ к зашифрованным сообщениям."; +"screen_signout_key_backup_disabled_title" = "Вы отключили резервное копирование"; +"screen_signout_key_backup_offline_subtitle" = "Когда вы перешли в автономный режим, резервное копирование ваших ключей продолжалось. Повторно подключитесь, чтобы перед выходом из системы можно было создать резервную копию ключей."; +"screen_signout_key_backup_offline_title" = "Резервное копирование ключей все еще продолжается"; +"screen_signout_key_backup_ongoing_subtitle" = "Пожалуйста, дождитесь завершения процесса, прежде чем выходить из системы."; +"screen_signout_key_backup_ongoing_title" = "Резервное копирование ключей все еще продолжается"; +"screen_signout_recovery_disabled_subtitle" = "Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы потеряете доступ к зашифрованным сообщениям."; +"screen_signout_recovery_disabled_title" = "Восстановление не настроено"; +"screen_signout_save_recovery_key_subtitle" = "Вы собираетесь выйти из последнего сеанса. Если вы выйдете из системы сейчас, вы можете потерять доступ к зашифрованным сообщениям."; +"screen_signout_save_recovery_key_title" = "Вы сохранили свой ключ восстановления?"; +"screen_start_chat_error_starting_chat" = "Произошла ошибка при попытке открытия комнаты"; +"screen_view_location_title" = "Местоположение"; +"screen_waitlist_message" = "В настоящее время существует высокий спрос на %1$@ на %2$@. Вернитесь в приложение через несколько дней и попробуйте снова.\n\nСпасибо за терпение!"; +"screen_waitlist_title" = "Почти готово."; +"screen_waitlist_title_success" = "Вы зарегистрированы."; +"screen_welcome_bullet_1" = "Звонки, опросы, поиск и многое другое будут добавлены позже в этом году."; +"screen_welcome_bullet_2" = "История сообщений для зашифрованных комнат в этом обновлении будет недоступна."; +"screen_welcome_bullet_3" = "Мы будем рады услышать ваше мнение, сообщите нам об этом через страницу настроек."; +"screen_welcome_button" = "Поехали!"; +"screen_welcome_subtitle" = "Вот что вам необходимо знать:"; +"screen_welcome_title" = "Добро пожаловать в %1$@!"; +"session_verification_banner_message" = "Похоже, вы используете новое устройство. Чтобы получить доступ к зашифрованным сообщениям в дальнейшем, проверьте их на другом устройстве."; +"session_verification_banner_title" = "Подтвердите, что это вы"; +"settings_rageshake" = "Rageshake"; +"settings_rageshake_detection_threshold" = "Порог обнаружения"; +"settings_version_number" = "Версия: %1$@ (%2$@)"; +"state_event_avatar_changed_too" = "(аватар тоже был изменен)"; +"state_event_avatar_url_changed" = "%1$@ сменили свой аватар"; +"state_event_avatar_url_changed_by_you" = "Вы сменили аватар"; +"state_event_display_name_changed_from" = "%1$@ изменил свое отображаемое имя с %2$@ на %3$@"; +"state_event_display_name_changed_from_by_you" = "Вы изменили свое отображаемое имя с %1$@ на %2$@"; +"state_event_display_name_removed" = "%1$@ удалил свое отображаемое имя (оно было %2$@)"; +"state_event_display_name_removed_by_you" = "Вы удалили свое отображаемое имя (оно было %1$@)"; +"state_event_display_name_set" = "%1$@ установили свое отображаемое имя на %2$@"; +"state_event_display_name_set_by_you" = "Вы установили отображаемое имя на %1$@"; +"state_event_room_avatar_changed" = "%1$@ изменил аватар комнаты"; +"state_event_room_avatar_changed_by_you" = "Вы изменили аватар комнаты"; +"state_event_room_avatar_removed" = "%1$@ удалил аватар комнаты"; +"state_event_room_avatar_removed_by_you" = "Вы удалили аватар комнаты"; +"state_event_room_ban" = "%1$@ заблокирован %2$@"; +"state_event_room_ban_by_you" = "Вы заблокировали %1$@"; +"state_event_room_created" = "%1$@ создал комнату"; +"state_event_room_created_by_you" = "Вы создали комнату"; +"state_event_room_invite" = "%1$@ пригласил %2$@"; +"state_event_room_invite_accepted" = "%1$@ принял приглашение"; +"state_event_room_invite_accepted_by_you" = "Вы приняли приглашение"; +"state_event_room_invite_by_you" = "Вы пригласили %1$@"; +"state_event_room_invite_you" = "Пользователь %1$@ пригласил вас"; +"state_event_room_join" = "%1$@ присоединился к комнате"; +"state_event_room_join_by_you" = "Вы вошли в комнату"; +"state_event_room_knock" = "%1$@ запросил присоединение"; +"state_event_room_knock_accepted" = "%1$@ разрешил %2$@ присоединиться"; +"state_event_room_knock_accepted_by_you" = "%1$@ разрешил вам присоединиться"; +"state_event_room_knock_by_you" = "Вы запросили присоединение"; +"state_event_room_knock_denied" = "%1$@ отклонил запрос %2$@ на присоединение"; +"state_event_room_knock_denied_by_you" = "Вы отклонили запрос %1$@ на присоединение"; +"state_event_room_knock_denied_you" = "%1$@ отклонил ваш запрос на присоединение"; +"state_event_room_knock_retracted" = "%1$@ больше не заинтересован в присоединении"; +"state_event_room_knock_retracted_by_you" = "Вы отменили запрос на присоединение"; +"state_event_room_leave" = "%1$@ покинул комнату"; +"state_event_room_leave_by_you" = "Вы вышли из комнаты"; +"state_event_room_name_changed" = "%1$@ изменил название комнаты на: %2$@"; +"state_event_room_name_changed_by_you" = "Вы изменили название комнаты на: %1$@"; +"state_event_room_name_removed" = "%1$@ удалил название комнаты"; +"state_event_room_name_removed_by_you" = "Вы удалили название комнаты"; +"state_event_room_reject" = "%1$@ отклонил приглашение"; +"state_event_room_reject_by_you" = "Вы отклонили приглашение"; +"state_event_room_remove" = "%1$@ удалил %2$@"; +"state_event_room_remove_by_you" = "Вы удалили %1$@"; +"state_event_room_third_party_invite" = "%1$@ отправила приглашение %2$@ присоединиться к комнате"; +"state_event_room_third_party_invite_by_you" = "Вы отправили приглашение присоединиться к комнате %1$@"; +"state_event_room_third_party_revoked_invite" = "%1$@ отозвал приглашение %2$@ присоединиться к комнате"; +"state_event_room_third_party_revoked_invite_by_you" = "Вы отозвали приглашение %1$@ присоединиться к комнате"; +"state_event_room_topic_changed" = "%1$@ изменил тему на: %2$@"; +"state_event_room_topic_changed_by_you" = "Вы изменили тему на: %1$@"; +"state_event_room_topic_removed" = "%1$@ удалил тему комнаты"; +"state_event_room_topic_removed_by_you" = "Вы удалили тему комнаты"; +"state_event_room_unban" = "%1$@ разблокирован %2$@"; +"state_event_room_unban_by_you" = "Вы разблокировали %1$@"; +"state_event_room_unknown_membership_change" = "%1$@ внес неизвестное изменение в составе"; +"test_language_identifier" = "en"; +"test_untranslated_default_language_identifier" = "en"; +"dialog_title_error" = "Ошибка"; +"dialog_title_success" = "Успешно"; +"notification_fallback_content" = "Уведомление"; +"notification_room_action_quick_reply" = "Быстрый ответ"; +"screen_room_mentions_at_room_title" = "Для всех"; +"screen_analytics_settings_help_us_improve" = "Предоставлять анонимные данные об использовании, чтобы помочь нам выявить проблемы."; +"screen_analytics_settings_read_terms" = "Вы можете ознакомиться со всеми нашими условиями %1$@."; +"screen_analytics_settings_read_terms_content_link" = "здесь"; +"screen_bug_report_rash_logs_alert_title" = "При последнем использовании %1$@ произошел сбой. Хотите поделиться отчетом о сбое?"; +"screen_create_room_title" = "Создать комнату"; +"screen_dm_details_block_alert_action" = "Заблокировать"; +"screen_dm_details_block_alert_description" = "Заблокированные пользователи не смогут отправлять вам сообщения, а все их сообщения будут скрыты. Вы можете разблокировать их в любое время."; +"screen_dm_details_block_user" = "Заблокировать пользователя"; +"screen_dm_details_unblock_alert_action" = "Разблокировать"; +"screen_dm_details_unblock_alert_description" = "Вы снова сможете увидеть все сообщения."; +"screen_dm_details_unblock_user" = "Разблокировать пользователя"; +"screen_login_subtitle" = "Matrix — это открытая сеть для безопасной децентрализованной связи."; +"screen_report_content_block_user" = "Заблокировать пользователя"; +"screen_room_details_leave_room_title" = "Покинуть комнату"; +"screen_room_details_security_title" = "Безопасность"; +"screen_room_details_topic_title" = "Тема"; +"screen_room_error_failed_processing_media" = "Не удалось обработать медиафайл для загрузки, попробуйте еще раз."; +"screen_room_notification_settings_mode_mentions_and_keywords" = "Только упоминания и ключевые слова"; +"screen_signout_confirmation_dialog_submit" = "Выйти"; +"screen_signout_confirmation_dialog_title" = "Выйти"; +"screen_signout_preference_item" = "Выйти"; +"screen_waitlist_message_success" = "Добро пожаловать в %1$@!"; diff --git a/ios/Runner/ru-RU.lproj/Localizable.stringsdict b/ios/Runner/ru-RU.lproj/Localizable.stringsdict new file mode 100644 index 0000000000..a5ccc5d6ed --- /dev/null +++ b/ios/Runner/ru-RU.lproj/Localizable.stringsdict @@ -0,0 +1,258 @@ + + + + + a11y_digits_entered + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Введена цифра %1$d + few + Ведено %1$d цифр + many + Введено много цифр + + + a11y_read_receipts_multiple_with_others + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Прочитано %1$@ и %2$d другим + few + Прочитано %1$@ и %2$d другими + many + Прочитано %1$@ и %2$d другими + + + common_member_count + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d участник + few + %1$d участников + many + %1$d участников + + + common_poll_votes_count + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d голос + few + %d голоса + many + %d голосов + + + notification_compat_summary_line_for_room + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$@: %2$d сообщение + few + %1$@: %2$d сообщения + many + %1$@: %2$d сообщений + + + notification_compat_summary_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d уведомление + few + %d уведомления + many + %d уведомлений + + + notification_invitations + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d приглашение + few + %d приглашения + many + %d приглашений + + + notification_new_messages_for_room + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d новое сообщение + few + %d новых сообщения + many + %d новых сообщений + + + notification_unread_notified_messages + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d непрочитанное уведомление + few + %d непрочитанных уведомления + many + %d непрочитанных уведомлений + + + notification_unread_notified_messages_in_room_rooms + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %d комната + few + %d комнаты + many + %d комнат + + + room_timeline_state_changes + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d изменение в комнате + few + %1$d изменения в комнате + many + %1$d изменений в комнате + + + screen_app_lock_subtitle + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Вы попытались разблокировать %1$d раз + few + Вы попытались разблокировать %1$d раз + many + Вы попытались разблокировать много раз + + + screen_app_lock_subtitle_wrong_pin + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Неверный PIN-код. У вас остался %1$d шанс + few + Неверный PIN-код. У вас остался %1$d шансов + many + Неверный PIN-код. У вас остался %1$d шанса + + + screen_room_member_list_header_title + + NSStringLocalizedFormatKey + %#@COUNT@ + COUNT + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + %1$d пользователь + few + %1$d пользователя + many + %1$d пользователей + + + + \ No newline at end of file From 838b98e70463905e8c27d868817844ecb5bf53a5 Mon Sep 17 00:00:00 2001 From: MinhDV Date: Thu, 30 Nov 2023 09:48:55 +0700 Subject: [PATCH 5/6] TW-1049 Support multi account --- lib/presentation/extensions/client_extension.dart | 3 +++ lib/utils/background_push.dart | 3 +++ 2 files changed, 6 insertions(+) diff --git a/lib/presentation/extensions/client_extension.dart b/lib/presentation/extensions/client_extension.dart index 0c813ebcf6..be42139cba 100644 --- a/lib/presentation/extensions/client_extension.dart +++ b/lib/presentation/extensions/client_extension.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; +import 'package:fluffychat/utils/string_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; @@ -24,4 +25,6 @@ extension ClientExtension on Client { } String mxid(BuildContext context) => userID ?? L10n.of(context)!.user; + + String? get pusherNotificationClientIdentifier => userID?.sha256Hash; } diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index 43771f3619..2855546ff8 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -23,6 +23,7 @@ import 'dart:io'; import 'package:fcm_shared_isolate/fcm_shared_isolate.dart'; import 'package:fluffychat/domain/model/extensions/push/push_notification_extension.dart'; +import 'package:fluffychat/presentation/extensions/client_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; import 'package:fluffychat/utils/push_helper.dart'; import 'package:fluffychat/widgets/twake_app.dart'; @@ -205,6 +206,8 @@ class BackgroundPush { "loc-args": [], }, }, + "pusher_notification_client_identifier": + client.pusherNotificationClientIdentifier, }, } : {}, From bbe176b5ef00683d76eb63887a3b5253bcd21f0a Mon Sep 17 00:00:00 2001 From: MinhDV Date: Wed, 6 Dec 2023 10:16:02 +0700 Subject: [PATCH 6/6] TW-1049 Write ADR --- docs/adr/0012-improve-ios-notification.md | 73 ++ ios/NSE/RestorationToken.swift | 2 +- scripts/copy-nse/.gitignore | 3 + scripts/copy-nse/README.MD | 16 + scripts/copy-nse/index.js | 69 ++ scripts/copy-nse/package-lock.json | 868 ++++++++++++++++++++++ scripts/copy-nse/package.json | 17 + scripts/patchs/element-x-nse-fix.patch | 13 + 8 files changed, 1060 insertions(+), 1 deletion(-) create mode 100644 docs/adr/0012-improve-ios-notification.md create mode 100644 scripts/copy-nse/.gitignore create mode 100644 scripts/copy-nse/README.MD create mode 100644 scripts/copy-nse/index.js create mode 100644 scripts/copy-nse/package-lock.json create mode 100644 scripts/copy-nse/package.json create mode 100644 scripts/patchs/element-x-nse-fix.patch diff --git a/docs/adr/0012-improve-ios-notification.md b/docs/adr/0012-improve-ios-notification.md new file mode 100644 index 0000000000..4aac3f5ee0 --- /dev/null +++ b/docs/adr/0012-improve-ios-notification.md @@ -0,0 +1,73 @@ +# 12. Improve iOS notification + +Date: 2023-12-06 + +## Status + +Accepted + +## Context +The motivation behind this decision is the inability to execute Dart code for decrypting notifications when the iOS app is running in the background. This limitation is different from Android or Web, where Dart code can easily run in such scenarios. + +## Decision +We have decided to make the following changes to address the mentioned issue: + +1. **Notification Service Extension (NSE):** + - Utilize the [Notification Service Extension](https://developer.apple.com/documentation/usernotifications/modifying_content_in_newly_delivered_notifications) provided by Apple to modify the content of notifications. + +2. **MatrixRustSDK Integration:** + - Integrate MatrixRustSDK to decrypt messages within the Notification Service Extension. The MatrixRustSDK version and other dependencies should be synchronized with Element X to avoid unexpected errors. Reference the synchronization from [Element X project.yml](https://github.com/vector-im/element-x-ios/blob/main/project.yml#L46). + +3. **Automated Script for Source Code Copy:** + - Develop a script written in Node.js to automate the process of copying the entire source code related to the Notification Service Extension from Element X, a known version. Detail at [scripts/copy-nse/README.MD](../../scripts/copy-nse/README.MD) + +4. **Dart Code Adjustment:** + - Modify Dart code to ensure compatibility with the Notification Service Extension from Element X. + +5. **Debugging Patch with AppDelegate:** + - Add the patch `scripts/patchs/ios-extension-debug.patch` to address debugging issues. This patch removes FlutterAppDelegate and suggests using AppDelegate instead for smoother debugging during Notification Service Extension development. + +6. **Add pusher_notification_client_identifier to Default Payload:** + - Extend the default notification payload by adding the `pusher_notification_client_identifier` field. This supports multi-account scenarios, where each notification will have a `client_identifier` to identify the user for decryption. For now it is SHA256 of userId + +7. **Keychain Access Groups:** + - Utilize `keychain-access-groups` to enable data exchange between the Notification Service Extension (NSE) and the main app by using a shared Keychain Access Group. This ensures security and safe data transmission (See details at [link](https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps/)). + +8. **INSendMessageIntent:** + - Use `INSendMessageIntent` to support displaying profile pictures in notifications. Instead of using regular notifications, integrating `INSendMessageIntent` ensures that the sender's image is displayed correctly in the notification (See details at [link](https://stackoverflow.com/questions/68198452/ios-15-communication-notification-picture-not-shown)). + +9. **iOS Version Support Below 16:** + - Communicate that Element X's Notification Service Extension supports iOS 16 and above. Therefore, Twake NSE will also have a similar requirement. Users with iOS versions below 16 can still install Twake, but they won't be able to decrypt notifications. + +## Consequences +Implementing these decisions has the following consequences: + +1. **Notification Discrepancy:** + - Notifications will behave differently in the background and foreground because they are handled by Swift in the background and Dart in the foreground. + +2. **Integration Challenges:** + - There might be challenges in seamlessly integrating Notification Service Extension with MatrixRustSDK and adjusting it to fit the current source code. The MatrixRustSDK version and dependencies should be synchronized with Element X to avoid unexpected errors. + +3. **Automated Source Code Copy:** + - Introducing an automated Node.js script streamlines the process of copying the source code from Element X, enhancing efficiency and reducing manual errors during this task. + +4. **Message Decryption Research:** + - Further research is required to successfully decrypt encrypted messages. This introduces a potential risk as it may involve understanding specific nuances when integrating MatrixRustSDK with Notification Service Extension. + +5. **Maintenance Overhead:** + - Copying source code from Element X may create maintenance overhead, as any updates or changes in Element X's source code need manual integration and synchronization. + +6. **Improved Debugging:** + - The debugging patch aims to optimize the debugging experience by addressing issues related to FlutterAppDelegate in the context of Notification Service Extension. + +7. **Multi-Account Support:** + - Multi-account support is achieved by adding `pusher_notification_client_identifier` to the default notification payload. + +8. **Security with Keychain Access Groups:** + - Ensures secure data transmission between Notification Service Extension and the main app using Keychain Access Groups. + +9. **Profile Picture Display Enhancement:** + - The use of `INSendMessageIntent` enhances the user experience by ensuring the correct display of sender profile pictures in notifications. + +10. **iOS 16 Requirement:** + - Communicates that Element X's Notification Service Extension requires iOS 16 or later, and Twake NSE has a similar requirement. Users below iOS 16 can install Twake but won't decrypt notifications. diff --git a/ios/NSE/RestorationToken.swift b/ios/NSE/RestorationToken.swift index 4c5bf28fd4..353abdc4d6 100644 --- a/ios/NSE/RestorationToken.swift +++ b/ios/NSE/RestorationToken.swift @@ -43,7 +43,7 @@ extension MatrixRustSDK.Session: Codable { deviceId: container.decode(String.self, forKey: .deviceId), homeserverUrl: container.decode(String.self, forKey: .homeserverUrl), oidcData: container.decodeIfPresent(String.self, forKey: .oidcData), - slidingSyncProxy: container.decode(String.self, forKey: .slidingSyncProxy)) + slidingSyncProxy: container.decodeIfPresent(String.self, forKey: .slidingSyncProxy)) } public func encode(to encoder: Encoder) throws { diff --git a/scripts/copy-nse/.gitignore b/scripts/copy-nse/.gitignore new file mode 100644 index 0000000000..6bc8ea8708 --- /dev/null +++ b/scripts/copy-nse/.gitignore @@ -0,0 +1,3 @@ +/node_modules +/element-x-ios-main +/main.zip \ No newline at end of file diff --git a/scripts/copy-nse/README.MD b/scripts/copy-nse/README.MD new file mode 100644 index 0000000000..385355fd08 --- /dev/null +++ b/scripts/copy-nse/README.MD @@ -0,0 +1,16 @@ +# Description +- Script to copy all Notification Extension Service resource from Element X to Twake + +# Steps +- Download Element X repository as zip and unzip +- Copy source code from NSE +- Apply patch: It is fix nullable slidingProxy when try decode from Keychain +- Copy DesignKit: It is local dependencies required by NSE +- Clean up: Delete zip file and unzip folder + +# Setup +``` +cd scripts/copy-nse +npm install +node index.js +``` \ No newline at end of file diff --git a/scripts/copy-nse/index.js b/scripts/copy-nse/index.js new file mode 100644 index 0000000000..f1777e09b7 --- /dev/null +++ b/scripts/copy-nse/index.js @@ -0,0 +1,69 @@ +const repoName = 'element-x-ios'; +const href = `https://github.com/vector-im/${repoName}/archive`; +const zipFile = 'main.zip'; + +const source = `${href}/${zipFile}`; + +const extractEntryTo = `${repoName}-main/`; + +const outputDir = `./`; + +const readYamlFile = require('read-yaml-file') +const fs = require('fs'); +const request = require('request'); +const admZip = require('adm-zip'); +const { exec } = require("child_process"); + +const main = async () => { + try { + console.log('start downloading') + await download(source, zipFile) + console.log('finished downloading'); + + var zip = new admZip(zipFile); + console.log('start unzip'); + zip.extractEntryTo(extractEntryTo, outputDir, true, true); + console.log('finished unzip'); + + const dirname = `${outputDir}${extractEntryTo}NSE/SupportingFiles` + const data = await readYamlFile(`${dirname}/target.yml`) + const sources = data.targets.NSE.sources.map(source => source.path) + + console.log('copying NSE files') + const ignoreSources = ['../SupportingFiles'] + + sources + .filter(source => !ignoreSources.includes(source)) + .forEach(source => { + fs.cpSync(`${dirname}/${source}`, `../../ios/NSE/${source.split('/').pop()}`, { recursive: true }) + }) + + console.log('apply patch') + exec('cd ../../ && git apply scripts/patchs/element-x-nse-fix.patch') + + console.log('copying DesignKit files') + fs.cpSync(`${outputDir}${extractEntryTo}DesignKit`, `../../ios/NSE/DesignKit`, { recursive: true }); + + console.log('clean up') + fs.unlinkSync(zipFile) + fs.rmSync(extractEntryTo, { recursive: true }) + + console.log('done') + } catch (error) { + console.log(error); + } +} + +const download = (url, output) => new Promise((resolve, reject) => { + request + .get(url) + .on('error', function (error) { + reject(error) + }) + .pipe(fs.createWriteStream(output)) + .on('finish', function () { + resolve() + }); +}) + +main() \ No newline at end of file diff --git a/scripts/copy-nse/package-lock.json b/scripts/copy-nse/package-lock.json new file mode 100644 index 0000000000..b1b4feacaf --- /dev/null +++ b/scripts/copy-nse/package-lock.json @@ -0,0 +1,868 @@ +{ + "name": "copy-nse", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "copy-nse", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "adm-zip": "^0.5.10", + "curlrequest": "^1.0.1", + "read-yaml-file": "^2.1.0", + "request": "^2.88.2" + } + }, + "node_modules/adm-zip": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", + "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "node_modules/curlrequest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/curlrequest/-/curlrequest-1.0.1.tgz", + "integrity": "sha512-SUWXgCtSqkFOBUfyDZ2M6xZmYIxy+9PSO5hGD/TsOnyToXe6/uDFZMOzEY1SlGfMWbK7/pyheJ2UbiLQ/BUAsQ==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/read-yaml-file": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-2.1.0.tgz", + "integrity": "sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==", + "dependencies": { + "js-yaml": "^4.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=10.13" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } + }, + "dependencies": { + "adm-zip": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", + "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==" + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" + }, + "aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "curlrequest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/curlrequest/-/curlrequest-1.0.1.tgz", + "integrity": "sha512-SUWXgCtSqkFOBUfyDZ2M6xZmYIxy+9PSO5hGD/TsOnyToXe6/uDFZMOzEY1SlGfMWbK7/pyheJ2UbiLQ/BUAsQ==" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, + "qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" + }, + "read-yaml-file": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-2.1.0.tgz", + "integrity": "sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==", + "requires": { + "js-yaml": "^4.0.0", + "strip-bom": "^4.0.0" + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==" + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } + } +} diff --git a/scripts/copy-nse/package.json b/scripts/copy-nse/package.json new file mode 100644 index 0000000000..51f0d7f60b --- /dev/null +++ b/scripts/copy-nse/package.json @@ -0,0 +1,17 @@ +{ + "name": "copy-nse", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "adm-zip": "^0.5.10", + "curlrequest": "^1.0.1", + "read-yaml-file": "^2.1.0", + "request": "^2.88.2" + } +} diff --git a/scripts/patchs/element-x-nse-fix.patch b/scripts/patchs/element-x-nse-fix.patch new file mode 100644 index 0000000000..bd1cc50893 --- /dev/null +++ b/scripts/patchs/element-x-nse-fix.patch @@ -0,0 +1,13 @@ +diff --git a/ios/NSE/RestorationToken.swift b/ios/NSE/RestorationToken.swift +index 4c5bf28f..353abdc4 100644 +--- a/ios/NSE/RestorationToken.swift ++++ b/ios/NSE/RestorationToken.swift +@@ -43,7 +43,7 @@ extension MatrixRustSDK.Session: Codable { + deviceId: container.decode(String.self, forKey: .deviceId), + homeserverUrl: container.decode(String.self, forKey: .homeserverUrl), + oidcData: container.decodeIfPresent(String.self, forKey: .oidcData), +- slidingSyncProxy: container.decode(String.self, forKey: .slidingSyncProxy)) ++ slidingSyncProxy: container.decodeIfPresent(String.self, forKey: .slidingSyncProxy)) + } + + public func encode(to encoder: Encoder) throws {