From c68c853d2f94d4b4746b639a68b9b6704936805f Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 14 Dec 2021 15:02:19 +0000 Subject: [PATCH 001/109] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index febe520e8a..b213c8623b 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.6.11 -CURRENT_PROJECT_VERSION = 1.6.11 +MARKETING_VERSION = 1.6.12 +CURRENT_PROJECT_VERSION = 1.6.12 From d494c68f0f983f02fe7da7c27a1a9068403f4ced Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 14 Dec 2021 18:33:37 +0100 Subject: [PATCH 002/109] RoomVC: Fix left room label memory management. --- .../MatrixKit/Controllers/MXKRoomViewController.m | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m index 382373a923..f29b59ee75 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m @@ -1031,14 +1031,16 @@ - (void)leaveRoomOnEvent:(MXEvent*)event roomDataSource = nil; // Add reason label - _leftRoomReasonLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 5, self.view.frame.size.width - 20, 70)]; - _leftRoomReasonLabel.numberOfLines = 0; - _leftRoomReasonLabel.text = reason; - _leftRoomReasonLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth; + UILabel *leftRoomReasonLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 5, self.view.frame.size.width - 20, 70)]; + leftRoomReasonLabel.numberOfLines = 0; + leftRoomReasonLabel.text = reason; + leftRoomReasonLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth; _bubblesTableView.tableHeaderView = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 80)]; - [_bubblesTableView.tableHeaderView addSubview:_leftRoomReasonLabel]; + [_bubblesTableView.tableHeaderView addSubview:leftRoomReasonLabel]; [_bubblesTableView reloadData]; + _leftRoomReasonLabel = leftRoomReasonLabel; + [self updateViewControllerAppearanceOnRoomDataSourceState]; } From 0ef26cccccb76e810d859ab5633d4885aadf5fa8 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 15 Dec 2021 11:07:41 +0100 Subject: [PATCH 003/109] Add changes --- changelog.d/5311.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5311.bugfix diff --git a/changelog.d/5311.bugfix b/changelog.d/5311.bugfix new file mode 100644 index 0000000000..cfd5998e51 --- /dev/null +++ b/changelog.d/5311.bugfix @@ -0,0 +1 @@ +RoomVC: Fix left room reason label memory management. \ No newline at end of file From 00eddb7a3a16c9795c5b120313a0ca644e65a6e2 Mon Sep 17 00:00:00 2001 From: artevaeckt Date: Tue, 14 Dec 2021 22:13:13 +0000 Subject: [PATCH 004/109] Translated using Weblate (German) Currently translated at 98.1% (433 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/de/ --- .../Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings index fd15a50c40..05095f81df 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings @@ -492,8 +492,8 @@ "call_consulting_with_user" = "Bei %@ anfragen"; "microphone_access_not_granted_for_voice_message" = "%@ fehlt die Berechtigung, für Sprachnachrichten auf das Mikrofon zuzugreifen"; "message_reply_to_sender_sent_a_voice_message" = "hat eine Sprachnachricht gesendet."; -"attachment_size_prompt_title" = "Größe zum Senden"; +"attachment_size_prompt_title" = "Zum Senden die Größe auswählen"; "attachment_large_with_resolution" = "Groß %@ (~%@)"; "attachment_medium_with_resolution" = "Mittel %@ (~%@)"; "attachment_small_with_resolution" = "Klein %@ (~%@)"; -"attachment_size_prompt_message" = "Du kannst dies in den Einstellungen ausschalten."; +"attachment_size_prompt_message" = "Dies kannst du in den Einstellungen abschalten."; From c93daef4b23a214014e0e08a79728ec4d29c7922 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Tue, 14 Dec 2021 12:41:07 +0000 Subject: [PATCH 005/109] Translated using Weblate (Albanian) Currently translated at 99.7% (440 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/sq/ --- .../Assets/MatrixKitAssets.bundle/sq.lproj/MatrixKit.strings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sq.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sq.lproj/MatrixKit.strings index 48323eea3c..c00e170a46 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sq.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sq.lproj/MatrixKit.strings @@ -475,3 +475,5 @@ "auth_username_in_use" = "Emër përdoruesi i përdorur"; "auth_invalid_user_name" = "Emër i pavlefshëm përdoruesi"; "rename" = "Riemërtojeni"; +"attachment_unsupported_preview_message" = "Ky lloj kartele nuk mbulohet."; +"attachment_unsupported_preview_title" = "S’arrihet të bëhet paraparje"; From 7507ef97d7d6fb15be698a0246a1b32ef807f2eb Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 14 Dec 2021 15:01:21 +0000 Subject: [PATCH 006/109] Translated using Weblate (Ukrainian) Currently translated at 97.7% (431 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/uk/ --- .../uk.lproj/MatrixKit.strings | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings index f9ca88a636..26aef08f32 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings @@ -532,3 +532,17 @@ "attachment_size_prompt_title" = "Підтвердити розмір, щоб надіслати"; "room_no_power_to_create_conference_call" = "Вам потрібен дозвіл, щоб надсилати запрошення, щоб розпочати конференцію в цій кімнаті"; "room_event_encryption_verify_message" = "Щоб переконатися, що цьому сеансу можна довіряти, зв’яжіться з його власником іншим способом (наприклад, особисто чи телефоном) і запитайте його, чи збігається ключ, який вони бачать у налаштуваннях користувача для цього сеансу, з ключем нижче:\n\nНазва сеансу: %@\nID сеансу: %@\nКлюч сеансу: %@\n\nЯкщо він збігається, натисніть кнопку підтвердження внизу. Якщо ні, значить хтось інший перехоплює цей сеанс, і ви, ймовірно, хочете натиснути кнопку чорного списку.\n\nУ майбутньому цей процес перевірки буде ускладнено."; +"call_more_actions_hold" = "Утримувати"; +"call_holded" = "Ви утримуєте виклик"; +"call_remote_holded" = "%@ утримує виклик"; + +// Login Screen +"login_error_already_logged_in" = "Вже ввійшли"; +"message_unsaved_changes" = "Ви маєте незбережені зміни. Якщо вийдете, їх буде скасовано."; +"attachment_unsupported_preview_message" = "Цей тип файлу не підтримується."; +"attachment_unsupported_preview_title" = "Не вдалося показати попередній перегляд"; +"attachment_e2e_keys_file_prompt" = "Цей файл містить ключі шифрування, експортовані з клієнта Matrix.\nБажаєте переглянути вміст файлу або імпортувати з нього ключі?"; +"room_member_power_level_prompt" = "Ви не зможете скасувати цю зміну, оскільки користувач отримає ті ж повноваження, що й ви.\nВи впевнені?"; +"room_error_timeline_event_not_found" = "Застосунок намагався завантажити певну точку стрічки у цій кімнаті, але не зміг її знайти"; +"room_error_timeline_event_not_found_title" = "Не вдалося завантажити позицію стрічки"; +"room_error_cannot_load_timeline" = "Не вдалося завантажити стрічку"; From f499e9298dbca413ea0c651b2faa73acdd65c9ab Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Tue, 14 Dec 2021 23:48:36 +0000 Subject: [PATCH 007/109] Translated using Weblate (Slovak) Currently translated at 100.0% (48 of 48 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/sk/ --- Riot/Assets/sk.lproj/Localizable.strings | 90 ++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/Riot/Assets/sk.lproj/Localizable.strings b/Riot/Assets/sk.lproj/Localizable.strings index 950c96d369..0ba5126ceb 100644 --- a/Riot/Assets/sk.lproj/Localizable.strings +++ b/Riot/Assets/sk.lproj/Localizable.strings @@ -75,3 +75,93 @@ /** General **/ "NOTIFICATION" = "Oznámenia"; + +/* New message from a specific person in a named room */ +"MSG_FROM_USER_IN_ROOM" = "%@ napísal v %@"; + +/** Key verification **/ + +"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@ žiada o overenie"; + +/* Group call from user, CallKit caller name */ +"GROUP_CALL_FROM_USER" = "%@ (Skupinový hovor)"; + +/* A user added a Jitsi call to a room */ +"GROUP_CALL_STARTED" = "Začal sa skupinový hovor"; + +/* Incoming named video conference invite from a specific person */ +"VIDEO_CONF_NAMED_FROM_USER" = "Skupinový videohovor od používateľa %@: '%@'"; + +/* Incoming named voice conference invite from a specific person */ +"VOICE_CONF_NAMED_FROM_USER" = "Skupinový hovor od používateľa %@: '%@'"; + +/* Incoming unnamed video conference invite from a specific person */ +"VIDEO_CONF_FROM_USER" = "Skupinový videohovor od používateľa %@"; + +/* Incoming unnamed voice conference invite from a specific person */ +"VOICE_CONF_FROM_USER" = "Skupinový hovor od používateľa %@"; + +/** Calls **/ + +/* Incoming one-to-one voice call */ +"VOICE_CALL_FROM_USER" = "Hovor od používateľa %@"; + +/* Incoming one-to-one video call */ +"VIDEO_CALL_FROM_USER" = "Videohovor od používateľa %@"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ aktualizoval/a svoj profil"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ zmenil/a svoj obrázok"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ zmenil/a svoje meno"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ zmenil/a svoje meno na %@"; + +/* A user has invited you to a named room */ +"USER_INVITE_TO_NAMED_ROOM" = "%@ vás pozval/a do %@"; + +/* A user has invited you to an (unamed) group chat */ +"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ vás pozval/a na skupinovú konverzáciu"; + +/** Invites **/ + +/* A user has invited you to a chat */ +"USER_INVITE_TO_CHAT" = "%@ vás pozval/a na konverzáciu"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ poslal/a reakciu"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ reagoval/a %@"; + +/* Look, stuff's happened, alright? Just open the app. */ +"MSGS_IN_TWO_PLUS_ROOMS" = "%@ nových správ v %@, %@ a ďalších"; + +/* Multiple messages in two rooms */ +"MSGS_IN_TWO_ROOMS" = "%@ nových správ v %@ a %@"; + +/* Multiple unread messages from two plus people (ie. for 4+ people: 'others' replaces the third person) */ +"MSGS_FROM_TWO_PLUS_USERS" = "%@ nových správ od %@, %@ a ďalších"; + +/* Sticker from a specific person, not referencing a room. */ +"STICKER_FROM_USER" = "%@ poslal/a nálepku"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ poslal/a súbor %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ poslal/a zvukovú správu"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ poslal/a zvukový súbor %@"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ odpovedal/a v %@"; From b9efd87ef7f22f9a002a9a478bf5cff8b77109d7 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 16 Dec 2021 16:29:07 +0200 Subject: [PATCH 008/109] Fixed SwiftUI UI tests not finding the right state to tap if not already displayed on screen. --- .../Modules/Common/Mock/MockAppScreens.swift | 8 ++--- .../Modules/Common/Mock/MockScreenState.swift | 33 ++++++------------- .../Modules/Common/Mock/ScreenList.swift | 3 +- .../Modules/Common/Mock/ScreenStateInfo.swift | 4 +-- .../Modules/Common/Mock/StateRenderer.swift | 2 +- .../Common/Test/UI/MockScreenTest.swift | 11 +++---- .../Common/Test/UI/XCUIApplication+Riot.swift | 30 +++++++++++++++++ .../Test/UI/PollEditFormUITests.swift | 2 +- .../Test/UI/PollTimelineUITests.swift | 4 +-- 9 files changed, 54 insertions(+), 43 deletions(-) create mode 100644 RiotSwiftUI/Modules/Common/Test/UI/XCUIApplication+Riot.swift diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index bceefcd268..78535b31a5 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -20,12 +20,12 @@ import Foundation @available(iOS 14.0, *) enum MockAppScreens { static let appScreens: [MockScreenState.Type] = [ - MockTemplateUserProfileScreenState.self, - MockTemplateRoomListScreenState.self, - MockTemplateRoomChatScreenState.self, MockUserSuggestionScreenState.self, MockPollEditFormScreenState.self, - MockPollTimelineScreenState.self + MockPollTimelineScreenState.self, + MockTemplateUserProfileScreenState.self, + MockTemplateRoomListScreenState.self, + MockTemplateRoomChatScreenState.self ] } diff --git a/RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift b/RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift index 4bf0df19dd..d4ac2a9e77 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift @@ -22,7 +22,6 @@ protocol MockScreenState { static var screenStates: [MockScreenState] { get } var screenType: Any.Type { get } var screenView: ([Any], AnyView) { get } - var stateTitle: String { get } } @available(iOS 14.0, *) @@ -33,44 +32,32 @@ extension MockScreenState { let depsAndViews = screenStates.map(\.screenView) let deps = depsAndViews.map({ $0.0 }) let views = depsAndViews.map({ $0.1 }) - let stateTitles = screenStates.map(\.stateTitle) - let fullScreenTitles = screenStates.map(\.fullScreenTitle) + let titles = screenStates.map(\.title) var states = [ScreenStateInfo]() for i in 0.. String { + String(describing: type).components(separatedBy: .punctuationCharacters).filter { $0.count > 0}.last! } - - /// A title to represent the screen and it's screen state - var fullScreenTitle: String { - "\(screenName): \(stateTitle)" - } - } @available(iOS 14.0, *) diff --git a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift index ce9f09f353..5e29d179be 100644 --- a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift +++ b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift @@ -33,8 +33,7 @@ struct ScreenList: View { ForEach(0.. Date: Wed, 27 Oct 2021 14:51:46 +0100 Subject: [PATCH 009/109] Begin migration from Matomo to PostHog Add CocoaPods-Keys. --- .gitignore | 3 + Config/BuildSettings.swift | 3 +- Gemfile | 1 + Gemfile.lock | 9 + Podfile | 10 +- Riot/Assets/en.lproj/Vector.strings | 8 +- Riot/Assets/third_party_licenses.html | 28 +++ Riot/Generated/Strings.swift | 24 ++- Riot/Managers/Analytics/Analytics.h | 65 ------- Riot/Managers/Analytics/Analytics.m | 162 ------------------ Riot/Managers/Analytics/Analytics.swift | 145 ++++++++++++++++ .../Analytics/AnalyticsSettings.swift | 70 ++++++++ .../Analytics/PHGPostHogConfiguration.swift | 24 +++ Riot/Managers/Settings/RiotSettings.swift | 22 ++- Riot/Modules/Application/LegacyAppDelegate.h | 1 - Riot/Modules/Application/LegacyAppDelegate.m | 16 +- .../AuthenticationViewController.m | 4 +- .../Common/Recents/RecentsViewController.m | 2 +- .../Communities/GroupsViewController.m | 2 +- .../Home/GroupHomeViewController.m | 2 +- .../Members/GroupParticipantsViewController.m | 2 +- .../Rooms/GroupRoomsViewController.m | 2 +- .../TabDetail/GroupDetailsViewController.m | 2 +- .../Contacts/ContactsTableViewController.m | 2 +- .../Details/ContactDetailsViewController.m | 2 +- .../Files/HomeFilesSearchViewController.m | 2 +- .../HomeMessagesSearchViewController.m | 2 +- .../Rooms/DirectoryViewController.m | 2 +- .../UnifiedSearchViewController.m | 2 +- .../Library/MediaAlbumContentViewController.m | 2 +- .../MediaPicker/MediaPickerViewController.m | 2 +- .../Attachements/AttachmentsViewController.m | 2 +- .../Detail/RoomMemberDetailsViewController.m | 2 +- .../Members/RoomParticipantsViewController.m | 2 +- Riot/Modules/Room/RoomViewController.m | 6 +- .../Files/RoomFilesSearchViewController.m | 2 +- .../RoomMessagesSearchViewController.m | 2 +- .../Room/Search/RoomSearchViewController.m | 2 +- .../Settings/RoomSettingsViewController.m | 2 +- .../DirectoryServerPickerViewController.m | 2 +- .../Modal/ServiceTermsModalCoordinator.swift | 6 +- .../DeactivateAccountViewController.m | 2 +- .../Language/LanguagePickerViewController.m | 2 +- .../CountryPickerViewController.m | 2 +- .../ManageSessionViewController.m | 2 +- .../Security/SecurityViewController.m | 2 +- .../Modules/Settings/SettingsViewController.m | 31 ++-- Riot/Modules/TabBar/MasterTabBarController.m | 70 ++++---- .../UserDevices/UsersDevicesViewController.m | 2 +- 49 files changed, 423 insertions(+), 341 deletions(-) delete mode 100644 Riot/Managers/Analytics/Analytics.h delete mode 100644 Riot/Managers/Analytics/Analytics.m create mode 100644 Riot/Managers/Analytics/Analytics.swift create mode 100644 Riot/Managers/Analytics/AnalyticsSettings.swift create mode 100644 Riot/Managers/Analytics/PHGPostHogConfiguration.swift diff --git a/.gitignore b/.gitignore index 695d4cd613..f4e7a1f10f 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,9 @@ vendor/ # Pods/ +# Never commit auto-generated secrets even if pods are checked in +Pods/CocoaPodsKeys/ + ## Ignore project files as we generate them with xcodegen (https://github.com/yonaskolb/XcodeGen) *.xcodeproj *.xcworkspace diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 525870b519..4be53baeac 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -165,7 +165,8 @@ final class BuildSettings: NSObject { static let roomsAllowToJoinPublicRooms: Bool = true // MARK: - Analytics - static let analyticsServerUrl = URL(string: "https://piwik.riot.im/piwik.php") + #warning("Testing environment.") + static let analyticsHost = "https://posthog-poc.lab.element.dev" static let analyticsAppId = "14" diff --git a/Gemfile b/Gemfile index 53efbaf921..ea061a17e6 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,7 @@ source "https://rubygems.org" gem "xcode-install" gem "fastlane" gem "cocoapods", '~>1.11.2' +gem "cocoapods-keys" plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index a8674758c3..c014fde7c4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,6 +3,9 @@ GEM specs: CFPropertyList (3.0.4) rexml + RubyInline (3.12.5) + ZenTest (~> 4.3) + ZenTest (4.12.0) activesupport (6.1.4.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) @@ -64,6 +67,9 @@ GEM typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) cocoapods-downloader (1.5.1) + cocoapods-keys (2.2.1) + dotenv + osx_keychain cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -225,6 +231,8 @@ GEM netrc (0.11.0) optparse (0.1.1) os (1.1.1) + osx_keychain (1.0.2) + RubyInline (~> 3) plist (3.6.0) public_suffix (4.0.6) rake (13.0.6) @@ -292,6 +300,7 @@ PLATFORMS DEPENDENCIES cocoapods (~> 1.11.2) + cocoapods-keys fastlane fastlane-plugin-diawi fastlane-plugin-versioning diff --git a/Podfile b/Podfile index 92a7bdad04..d9925aa6dd 100644 --- a/Podfile +++ b/Podfile @@ -3,7 +3,7 @@ source 'https://cdn.cocoapods.org/' # Uncomment this line to define a global platform for your project platform :ios, '12.1' -# Use frameforks to allow usage of pod written in Swift (like PiwikTracker) +# Use frameworks to allow usage of pods written in Swift use_frameworks! # Different flavours of pods to MatrixSDK. Can be one of: @@ -67,8 +67,8 @@ abstract_target 'RiotPods' do pod 'KeychainAccess', '~> 4.2.2' pod 'WeakDictionary', '~> 2.0' - # Piwik for analytics - pod 'MatomoTracker', '~> 7.4.1' + # PostHog for analytics + pod 'PostHog', '~> 1.4.2' # Remove warnings from "bad" pods pod 'OLMKit', :inhibit_warnings => true @@ -127,6 +127,10 @@ abstract_target 'RiotPods' do end +plugin 'cocoapods-keys', { + :project => "Riot", + :keys => ["PostHog"] +} post_install do |installer| installer.pods_project.targets.each do |target| diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 768862c3bd..e2d6c1b3f1 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -577,7 +577,7 @@ Tap the + to start adding people."; "settings_term_conditions" = "Terms & Conditions"; "settings_privacy_policy" = "Privacy Policy"; "settings_third_party_notices" = "Third-party Notices"; -"settings_send_crash_report" = "Send anon crash & usage data"; +"settings_analytics_and_crash_data" = "Send crash and analytics data"; "settings_enable_rageshake" = "Rage shake to report bug"; "settings_clear_cache" = "Clear cache"; @@ -945,8 +945,10 @@ Tap the + to start adding people."; "no_voip_title" = "Incoming call"; "no_voip" = "%@ is calling you but %@ does not support calls yet.\nYou can ignore this notification and answer the call from another device or you can reject it."; -// Crash report -"google_analytics_use_prompt" = "Would you like to help improve %@ by automatically reporting anonymous crash reports and usage data?"; +// Analytics +"analytics_prompt_title" = "Help us improve %@"; +"analytics_prompt_new_user" = "Would you like to help improve %@ by automatically reporting crash reports and usage data?\n\nWe don't record or profile any personal data, and we don't share anything with any third parties."; +"analytics_prompt_posthog_upgrade" = "To allow us to understand how people use multiple devices, we've enhanced our analytics data to include a randomly generated identifier associated with your account that will be shared across your devices.\n\nWe don't record or profile any personal data, and we don't share anything with any third parties.\n\nYou previously agreed to send anonymous usage data to %@ - is this still okay?"; // Crypto "e2e_enabling_on_app_update" = "%@ now supports end-to-end encryption but you need to log in again to enable it.\n\nYou can do it now or later from the application settings."; diff --git a/Riot/Assets/third_party_licenses.html b/Riot/Assets/third_party_licenses.html index b615810b26..ee1a8d284e 100644 --- a/Riot/Assets/third_party_licenses.html +++ b/Riot/Assets/third_party_licenses.html @@ -1897,6 +1897,34 @@ SOFTWARE.

+
  • + PostHog iOS (https://github.com/PostHog/posthog-ios) +

    + The MIT License (MIT) +

    + Copyright (c) 2020 PostHog (part of Hiberly Inc) +

    + Copyright (c) 2016 Segment.io, Inc. +

    + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: +

    + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. +

    + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +

    +
  • diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 4ab7572a3f..df6ce4e940 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -31,6 +31,18 @@ public class VectorL10n: NSObject { public static func activeCallDetails(_ p1: String) -> String { return VectorL10n.tr("Vector", "active_call_details", p1) } + /// Would you like to help improve %@ by automatically reporting crash reports and usage data?\n\nWe don't record or profile any personal data, and we don't share anything with any third parties. + public static func analyticsPromptNewUser(_ p1: String) -> String { + return VectorL10n.tr("Vector", "analytics_prompt_new_user", p1) + } + /// To allow us to understand how people use multiple devices, we've enhanced our analytics data to include a randomly generated identifier associated with your account that will be shared across your devices.\n\nWe don't record or profile any personal data, and we don't share anything with any third parties.\n\nYou previously agreed to send anonymous usage data to %@ - is this still okay? + public static func analyticsPromptPosthogUpgrade(_ p1: String) -> String { + return VectorL10n.tr("Vector", "analytics_prompt_posthog_upgrade", p1) + } + /// Help us improve %@ + public static func analyticsPromptTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "analytics_prompt_title", p1) + } /// Please review and accept the policies of this homeserver: public static var authAcceptPolicies: String { return VectorL10n.tr("Vector", "auth_accept_policies") @@ -1439,10 +1451,6 @@ public class VectorL10n: NSObject { public static var gdprConsentNotGivenAlertReviewNowAction: String { return VectorL10n.tr("Vector", "gdpr_consent_not_given_alert_review_now_action") } - /// Would you like to help improve %@ by automatically reporting anonymous crash reports and usage data? - public static func googleAnalyticsUsePrompt(_ p1: String) -> String { - return VectorL10n.tr("Vector", "google_analytics_use_prompt", p1) - } /// Home public static var groupDetailsHome: String { return VectorL10n.tr("Vector", "group_details_home") @@ -4227,6 +4235,10 @@ public class VectorL10n: NSObject { public static var settingsAdvanced: String { return VectorL10n.tr("Vector", "settings_advanced") } + /// Send crash and analytics data + public static var settingsAnalyticsAndCrashData: String { + return VectorL10n.tr("Vector", "settings_analytics_and_crash_data") + } /// Call invitations public static var settingsCallInvitations: String { return VectorL10n.tr("Vector", "settings_call_invitations") @@ -4755,10 +4767,6 @@ public class VectorL10n: NSObject { public static var settingsSecurity: String { return VectorL10n.tr("Vector", "settings_security") } - /// Send anon crash & usage data - public static var settingsSendCrashReport: String { - return VectorL10n.tr("Vector", "settings_send_crash_report") - } /// SENDING IMAGES AND VIDEOS public static var settingsSendingMedia: String { return VectorL10n.tr("Vector", "settings_sending_media") diff --git a/Riot/Managers/Analytics/Analytics.h b/Riot/Managers/Analytics/Analytics.h deleted file mode 100644 index 5ad851929e..0000000000 --- a/Riot/Managers/Analytics/Analytics.h +++ /dev/null @@ -1,65 +0,0 @@ -/* - Copyright 2018 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 - -#import - - -// Metrics related to notifications -FOUNDATION_EXPORT NSString *const AnalyticsNoficationsCategory; -FOUNDATION_EXPORT NSString *const AnalyticsNoficationsTimeToDisplayContent; -/** - The analytics value for accept/decline of the identity server's terms. - */ -FOUNDATION_EXPORT NSString *const AnalyticsContactsIdentityServerAccepted; - - -/** - `Analytics` sends analytics to an analytics tool. - */ -@interface Analytics : NSObject - -/** - Returns the shared Analytics manager. - - @return the shared Analytics manager. - */ -+ (instancetype)sharedInstance; - -/** - Start doing analytics if the settings `enableCrashReport` is enabled. - */ -- (void)start; - -/** - Stop doing analytics. - */ -- (void)stop; - -/** - Track a screen display. - - @param screenName the name of the displayed screen. - */ -- (void)trackScreen:(NSString*)screenName; - -/** - Flush analytics data. - */ -- (void)dispatch; - -@end diff --git a/Riot/Managers/Analytics/Analytics.m b/Riot/Managers/Analytics/Analytics.m deleted file mode 100644 index 6bf7269b83..0000000000 --- a/Riot/Managers/Analytics/Analytics.m +++ /dev/null @@ -1,162 +0,0 @@ -/* - Copyright 2018 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 "Analytics.h" - -#import "GeneratedInterface-Swift.h" - - -NSString *const AnalyticsNoficationsCategory = @"notifications"; -NSString *const AnalyticsNoficationsTimeToDisplayContent = @"timelineDisplay"; -NSString *const AnalyticsContactsIdentityServerAccepted = @"identityServerAccepted"; - - -// Duration data will be visible under the Piwik category called "Performance". -// Other values will be visible in "Metrics". -// Some Matomo screenshots are available at https://github.com/vector-im/element-ios/pull/3789. -NSString *const kAnalyticsPerformanceCategory = @"Performance"; -NSString *const kAnalyticsMetricsCategory = @"Metrics"; - - -@import MatomoTracker; - -@interface Analytics () -{ - MatomoTracker *matomoTracker; -} - -@end - -@implementation Analytics - -+ (instancetype)sharedInstance -{ - static Analytics *sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - sharedInstance = [[Analytics alloc] init]; - }); - - return sharedInstance; -} - -- (instancetype)init -{ - self = [super init]; - if (self) - { - matomoTracker = [[MatomoTracker alloc] initWithSiteId:BuildSettings.analyticsAppId - baseURL:BuildSettings.analyticsServerUrl - userAgent:@"iOSMatomoTracker"]; - [self migrateFromFourPointFourSharedInstance]; - } - return self; -} - -- (void)migrateFromFourPointFourSharedInstance -{ - if ([[NSUserDefaults standardUserDefaults] boolForKey:@"migratedFromFourPointFourSharedInstance"]) return; - [matomoTracker copyFromOldSharedInstance]; - [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"migratedFromFourPointFourSharedInstance"]; -} - -- (void)start -{ - // Check whether the user has enabled the sending of crash reports. - if (RiotSettings.shared.enableCrashReport) - { - matomoTracker.isOptedOut = NO; - - [matomoTracker setCustomVariableWithIndex:1 name:@"App Platform" value:@"iOS Platform"]; - [matomoTracker setCustomVariableWithIndex:2 name:@"App Version" value:[AppDelegate theDelegate].appVersion]; - - // The language is either the one selected by the user within the app - // or, else, the one configured by the OS - NSString *language = [NSBundle mxk_language] ? [NSBundle mxk_language] : [[NSBundle mainBundle] preferredLocalizations][0]; - [matomoTracker setCustomVariableWithIndex:4 name:@"Chosen Language" value:language]; - - MXKAccount* account = [MXKAccountManager sharedManager].activeAccounts.firstObject; - if (account) - { - [matomoTracker setCustomVariableWithIndex:7 name:@"Homeserver URL" value:account.mxCredentials.homeServer]; - [matomoTracker setCustomVariableWithIndex:8 name:@"Identity Server URL" value:account.identityServerURL]; - } - - // TODO: We should also track device and os version - // But that needs to be decided for all platforms - - // Catch and log crashes - [MXLogger logCrashes:YES]; - [MXLogger setBuildVersion:[AppDelegate theDelegate].build]; - -#ifdef DEBUG - // Disable analytics in debug as it pollutes stats - matomoTracker.isOptedOut = YES; -#endif - } - else - { - MXLogDebug(@"[AppDelegate] The user decided to not send analytics"); - matomoTracker.isOptedOut = YES; - [MXLogger logCrashes:NO]; - } -} - -- (void)stop -{ - matomoTracker.isOptedOut = YES; - [MXLogger logCrashes:NO]; -} - -- (void)trackScreen:(NSString *)screenName -{ - // Use the same pattern as Android - NSString *appName = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"]; - NSString *appVersion = [AppDelegate theDelegate].appVersion; - - [matomoTracker trackWithView:@[@"ios", appName, appVersion, screenName] - url:nil]; -} - -- (void)dispatch -{ - [matomoTracker dispatch]; -} - -#pragma mark - MXAnalyticsDelegate - -- (void)trackDuration:(NSTimeInterval)seconds category:(NSString*)category name:(NSString*)name -{ - // Report time in ms to make figures look better in Matomo - NSNumber *value = @(seconds * 1000); - [matomoTracker trackWithEventWithCategory:kAnalyticsPerformanceCategory - action:category - name:name - number:value - url:nil]; -} - -- (void)trackValue:(NSNumber*)value category:(NSString*)category name:(NSString*)name -{ - [matomoTracker trackWithEventWithCategory:kAnalyticsMetricsCategory - action:category - name:name - number:value - url:nil]; -} - -@end diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift new file mode 100644 index 0000000000..d1e2388dbf --- /dev/null +++ b/Riot/Managers/Analytics/Analytics.swift @@ -0,0 +1,145 @@ +// +// Copyright 2021 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 PostHog + +@objcMembers class Analytics: NSObject { + + // MARK: - Properties + + static let shared = Analytics() + + private(set) var isRunning = false + + private var postHog: PHGPostHog? + + // MARK: - Public + + func shouldShowPseudonymousAnalyticsPrompt(for session: MXSession) -> Bool { + return AnalyticsSettings(session: session).showPseudonymousAnalyticsPrompt + } + + func optIn(with session: MXSession?) { + guard let session = session else { return } + + var settings = AnalyticsSettings(session: session) + settings.generateIDIfMissing() + settings.pseudonymousAnalyticsOptIn = true + settings.showPseudonymousAnalyticsPrompt = false + + session.setAccountData(settings.dictionary, forType: AnalyticsSettings.eventType) { + MXLog.debug("[Analytics] Successfully updated analytics settings in account data.") + } failure: { error in + MXLog.error("[Analytics] Failed to update analytics settings.") + } + } + + func optOut(with session: MXSession) { + var settings = AnalyticsSettings(session: session) + settings.id = nil + settings.pseudonymousAnalyticsOptIn = false + settings.showPseudonymousAnalyticsPrompt = false + + session.setAccountData(settings.dictionary, forType: AnalyticsSettings.eventType, success: nil) { error in + MXLog.error("[Analytics] Failed to update analytics settings.") + } + } + + private func start(with pseudonymousID: String) { + guard !isRunning else { return } + + postHog = PHGPostHog(configuration: PHGPostHogConfiguration.standard) + postHog?.enable() + isRunning = true + MXLog.debug("[Analytics] Started.") + + if !RiotSettings.shared.hasPseudonymousAnalyticsIdentified { + postHog?.identify(pseudonymousID) + MXLog.debug("[Analytics] Identified.") + RiotSettings.shared.hasPseudonymousAnalyticsIdentified = true + } + + postHog?.capture("analyticsDidStart") + forceUpload() + } + + func reset() { + guard isRunning else { return } + + postHog?.disable() + isRunning = false + MXLog.debug("[Analytics] Stopped.") + + postHog?.reset() + RiotSettings.shared.hasPseudonymousAnalyticsIdentified = false + + postHog = nil + } + + func forceUpload() { + postHog?.flush() + } + + func log(event: String) { + postHog?.capture(event) + } +} + + +// MARK: - Legacy compatibility +extension Analytics { + #warning("Use enums instead") + static let NotificationsCategory = "notifications" + static let NotificationsTimeToDisplayContent = "timelineDisplay" + static let ContactsIdentityServerAccepted = "identityServerAccepted" + static let PerformanceCategory = "Performance" + static let MetricsCategory = "Metrics" + + @objc func trackScreen(_ screenName: String) { +// postHog?.capture("screen:\(screenName)") + } +} + +extension Analytics: MXAnalyticsDelegate { + var settingsEventType: String { AnalyticsSettings.eventType } + + func handleSettingsEvent(_ event: [AnyHashable: Any]) { + guard event["type"] as? String == AnalyticsSettings.eventType, + let content = event["content"] as? [AnyHashable: Any] + else { + MXLog.error("[Analytics] handleSettingsEvent: invalid event") + return + } + + let settings = AnalyticsSettings(dictionary: content) + + if !settings.showPseudonymousAnalyticsPrompt, + settings.pseudonymousAnalyticsOptIn == true, + let id = settings.id { + start(with: id) + } else { + reset() + } + } + + @objc func trackDuration(_ seconds: TimeInterval, category: String, name: String) { +// postHog?.capture("\(category):\(name)", properties: ["duration": seconds]) + } + + @objc func trackValue(_ value: NSNumber, category: String, name: String) { +// postHog?.capture("\(category):\(name)", properties: ["value": value]) + } +} diff --git a/Riot/Managers/Analytics/AnalyticsSettings.swift b/Riot/Managers/Analytics/AnalyticsSettings.swift new file mode 100644 index 0000000000..d85cfc8b21 --- /dev/null +++ b/Riot/Managers/Analytics/AnalyticsSettings.swift @@ -0,0 +1,70 @@ +// +// Copyright 2021 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 AnalyticsSettings { + static let eventType = "im.vector.analytics" + + private enum Constants { + static let idKey = "id" + static let optInKey = "pseudonymousAnalyticsOptIn" + static let showPromptKey = "showPseudonymousAnalyticsPrompt" + } + + /// A randomly generated analytics token for this user. + /// This is suggested to be a 128-bit hex encoded string. + var id: String? + + /// Boolean indicating whether the user has opted in. + /// If nil, the user hasn't yet given consent either way + var pseudonymousAnalyticsOptIn: Bool? + + /// Boolean indicating whether to show the analytics opt-in prompt. + var showPseudonymousAnalyticsPrompt: Bool + + mutating func generateIDIfMissing() { + guard id == nil else { return } + + // Generate a 32 character analytics ID containing the characters 0-f. + id = [UInt8](repeating: 0, count: 16) + .map { _ in String(format: "%02x", UInt8.random(in: 0...UInt8.max)) } + .joined() + } +} + +extension AnalyticsSettings { + init(dictionary: Dictionary?) { + self.id = dictionary?[Constants.idKey] as? String + self.pseudonymousAnalyticsOptIn = dictionary?[Constants.optInKey] as? Bool + self.showPseudonymousAnalyticsPrompt = dictionary?[Constants.showPromptKey] as? Bool ?? true + } + + var dictionary: Dictionary { + var dictionary = [AnyHashable: Any]() + dictionary[Constants.idKey] = id + dictionary[Constants.optInKey] = pseudonymousAnalyticsOptIn + dictionary[Constants.showPromptKey] = showPseudonymousAnalyticsPrompt + + return dictionary + } +} + +extension AnalyticsSettings { + init(session: MXSession) { + self.init(dictionary: session.accountData.accountData(forEventType: AnalyticsSettings.eventType)) + } +} diff --git a/Riot/Managers/Analytics/PHGPostHogConfiguration.swift b/Riot/Managers/Analytics/PHGPostHogConfiguration.swift new file mode 100644 index 0000000000..a9a0ba590a --- /dev/null +++ b/Riot/Managers/Analytics/PHGPostHogConfiguration.swift @@ -0,0 +1,24 @@ +// +// Copyright 2021 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 PostHog +import Keys + +extension PHGPostHogConfiguration { + static var standard: PHGPostHogConfiguration { + PHGPostHogConfiguration(apiKey: RiotKeys().postHog, host: BuildSettings.analyticsHost) + } +} diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 5b31329d43..3413dbdaf3 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -23,7 +23,8 @@ final class RiotSettings: NSObject { // MARK: - Constants public enum UserDefaultsKeys { - static let enableCrashReport = "enableCrashReport" + static let enableAnalytics = "enableAnalytics" + static let matomoAnalytics = "enableCrashReport" static let notificationsShowDecryptedContent = "showDecryptedContent" static let allowStunServerFallback = "allowStunServerFallback" static let pinRoomsWithMissedNotificationsOnHome = "pinRoomsWithMissedNotif" @@ -100,13 +101,22 @@ final class RiotSettings: NSObject { // MARK: Other - /// Indicate if `enableCrashReport` settings has been set once. - var isEnableCrashReportHasBeenSetOnce: Bool { - return RiotSettings.defaults.object(forKey: UserDefaultsKeys.enableCrashReport) != nil + /// Whether the user has both seen the Matomo analytics prompt and declined it. + /// This is used to prevent users who previously opted out from being asked again. + var hasSeenAndDeclinedMatomoAnalytics: Bool { + RiotSettings.defaults.object(forKey: UserDefaultsKeys.matomoAnalytics) != nil && !RiotSettings.defaults.bool(forKey: UserDefaultsKeys.matomoAnalytics) } - @UserDefault(key: UserDefaultsKeys.enableCrashReport, defaultValue: false, storage: defaults) - var enableCrashReport + /// Whether the user previously accepted the Matomo analytics prompt. + /// This allows these users to be shown a different prompt to explain the changes. + var hasAcceptedMatomoAnalytics: Bool { + RiotSettings.defaults.bool(forKey: UserDefaultsKeys.matomoAnalytics) + } + + #warning("Rename me!") + /// Indicates if the device has already called identify for this session to PostHog. + @UserDefault(key: "hasPseudonymousAnalyticsIdentified", defaultValue: false, storage: defaults) + var hasPseudonymousAnalyticsIdentified @UserDefault(key: "enableRageShake", defaultValue: false, storage: defaults) var enableRageShake diff --git a/Riot/Modules/Application/LegacyAppDelegate.h b/Riot/Modules/Application/LegacyAppDelegate.h index 72dc4f61e0..40e3c23771 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.h +++ b/Riot/Modules/Application/LegacyAppDelegate.h @@ -22,7 +22,6 @@ #import "JitsiViewController.h" #import "RageShakeManager.h" -#import "Analytics.h" #import "ThemeService.h" #import "UniversalLink.h" diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 180cfe27ad..9de053ba90 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -433,16 +433,14 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( _isAppForeground = NO; _handleSelfVerificationRequest = YES; - // Configure our analytics. It will indeed start if the option is enabled - Analytics *analytics = [Analytics sharedInstance]; + // Configure our analytics. It will start automatically if the option is enabled + Analytics *analytics = Analytics.shared; [MXSDKOptions sharedInstance].analyticsDelegate = analytics; - [DecryptionFailureTracker sharedInstance].delegate = [Analytics sharedInstance]; + [DecryptionFailureTracker sharedInstance].delegate = analytics; MXBaseProfiler *profiler = [MXBaseProfiler new]; profiler.analytics = analytics; [MXSDKOptions sharedInstance].profiler = profiler; - - [analytics start]; self.localAuthenticationService = [[LocalAuthenticationService alloc] initWithPinCodePreferences:[PinCodePreferences shared]]; @@ -587,7 +585,7 @@ - (void)applicationDidEnterBackground:(UIApplication *)application // Analytics: Force to send the pending actions [[DecryptionFailureTracker sharedInstance] dispatch]; - [[Analytics sharedInstance] dispatch]; + [Analytics.shared forceUpload]; } - (void)applicationWillEnterForeground:(UIApplication *)application @@ -648,7 +646,8 @@ - (void)afterAppUnlockedByPin:(UIApplication *)application MXLogDebug(@"[AppDelegate] afterAppUnlockedByPin"); // Check if there is crash log to send - if (RiotSettings.shared.enableCrashReport) +#warning Is this technically analytics or is it mixing the two up? + if (Analytics.shared.isRunning) { [self checkExceptionToReport]; } @@ -2225,6 +2224,9 @@ - (void)logoutSendingRequestServer:(BOOL)sendLogoutServerRequest // Reset push notification store [self.pushNotificationStore reset]; + // Reset analytics + [Analytics.shared reset]; + #ifdef MX_CALL_STACK_ENDPOINT // Erase all created certificates and private keys by MXEndpointCallStack for (MXKAccount *account in MXKAccountManager.sharedManager.accounts) diff --git a/Riot/Modules/Authentication/AuthenticationViewController.m b/Riot/Modules/Authentication/AuthenticationViewController.m index 188e33268b..a698c9badc 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/AuthenticationViewController.m @@ -311,7 +311,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"Authentication"]; + [Analytics.shared trackScreen:@"Authentication"]; [_keyboardAvoider startAvoiding]; } @@ -330,7 +330,7 @@ - (void)viewDidAppear:(BOOL)animated return; } - // Verify that the app does not show the authentification screean whereas + // Verify that the app does not show the authentication screen whereas // the user has already logged in. // This bug rarely happens (https://github.com/vector-im/riot-ios/issues/1643) // but it invites the user to log in again. They will then lose all their diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index b8f0d0a80b..922c304630 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -260,7 +260,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:_screenName]; + [Analytics.shared trackScreen:_screenName]; // Reset back user interactions self.userInteractionEnabled = YES; diff --git a/Riot/Modules/Communities/GroupsViewController.m b/Riot/Modules/Communities/GroupsViewController.m index 91a8813524..cd722d3234 100644 --- a/Riot/Modules/Communities/GroupsViewController.m +++ b/Riot/Modules/Communities/GroupsViewController.m @@ -204,7 +204,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"Groups"]; + [Analytics.shared trackScreen:@"Groups"]; // Deselect the current selected row, it will be restored on viewDidAppear (if any) NSIndexPath *indexPath = [self.groupsTableView indexPathForSelectedRow]; diff --git a/Riot/Modules/Communities/Home/GroupHomeViewController.m b/Riot/Modules/Communities/Home/GroupHomeViewController.m index 018c15eb9c..12d240f649 100644 --- a/Riot/Modules/Communities/Home/GroupHomeViewController.m +++ b/Riot/Modules/Communities/Home/GroupHomeViewController.m @@ -206,7 +206,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"GroupDetailsHome"]; + [Analytics.shared trackScreen:@"GroupDetailsHome"]; // Release the potential pushed view controller [self releasePushedViewController]; diff --git a/Riot/Modules/Communities/Members/GroupParticipantsViewController.m b/Riot/Modules/Communities/Members/GroupParticipantsViewController.m index 7be31d2555..f8a0508c4b 100644 --- a/Riot/Modules/Communities/Members/GroupParticipantsViewController.m +++ b/Riot/Modules/Communities/Members/GroupParticipantsViewController.m @@ -220,7 +220,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"GroupDetailsPeople"]; + [Analytics.shared trackScreen:@"GroupDetailsPeople"]; // Release the potential pushed view controller [self releasePushedViewController]; diff --git a/Riot/Modules/Communities/Rooms/GroupRoomsViewController.m b/Riot/Modules/Communities/Rooms/GroupRoomsViewController.m index cabf6cdde8..a51a762f1e 100644 --- a/Riot/Modules/Communities/Rooms/GroupRoomsViewController.m +++ b/Riot/Modules/Communities/Rooms/GroupRoomsViewController.m @@ -184,7 +184,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"GroupDetailsRooms"]; + [Analytics.shared trackScreen:@"GroupDetailsRooms"]; // Release the potential pushed view controller [self releasePushedViewController]; diff --git a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m index 28e7e64de9..96660bcc63 100644 --- a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m +++ b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m @@ -138,7 +138,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"GroupDetails"]; + [Analytics.shared trackScreen:@"GroupDetails"]; } - (void)viewWillDisappear:(BOOL)animated diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index 1a8bec148e..ab59604f67 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -161,7 +161,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:_screenName]; + [Analytics.shared trackScreen:_screenName]; MXWeakify(self); diff --git a/Riot/Modules/Contacts/Details/ContactDetailsViewController.m b/Riot/Modules/Contacts/Details/ContactDetailsViewController.m index 3d332dbaa2..a42ee92253 100644 --- a/Riot/Modules/Contacts/Details/ContactDetailsViewController.m +++ b/Riot/Modules/Contacts/Details/ContactDetailsViewController.m @@ -234,7 +234,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"ContactDetails"]; + [Analytics.shared trackScreen:@"ContactDetails"]; // Hide the bottom border of the navigation bar to display the expander header [self hideNavigationBarBorder:YES]; diff --git a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m index 63035f7261..c67f9e3f90 100644 --- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m @@ -110,7 +110,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"FilesGlobalSearch"]; + [Analytics.shared trackScreen:@"FilesGlobalSearch"]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionDidLeaveRoomNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionNewRoomNotification object:nil]; diff --git a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m index 00fd9d20d1..5a439b6bb8 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m @@ -117,7 +117,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"MessagesGlobalSearch"]; + [Analytics.shared trackScreen:@"MessagesGlobalSearch"]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionDidLeaveRoomNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionNewRoomNotification object:nil]; diff --git a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m index 47300a956f..f62b5beec2 100644 --- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m +++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m @@ -108,7 +108,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"Directory"]; + [Analytics.shared trackScreen:@"Directory"]; // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { diff --git a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m index bd0a07b3f6..d2f044854d 100644 --- a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m +++ b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m @@ -145,7 +145,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"UnifiedSearch"]; + [Analytics.shared trackScreen:@"UnifiedSearch"]; // Let's child display the loading not the home view controller if (self.activityIndicator) diff --git a/Riot/Modules/MediaPicker/Library/MediaAlbumContentViewController.m b/Riot/Modules/MediaPicker/Library/MediaAlbumContentViewController.m index b87015e597..f847f5c6dd 100644 --- a/Riot/Modules/MediaPicker/Library/MediaAlbumContentViewController.m +++ b/Riot/Modules/MediaPicker/Library/MediaAlbumContentViewController.m @@ -165,7 +165,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"MediaAlbumContent"]; + [Analytics.shared trackScreen:@"MediaAlbumContent"]; self.navigationItem.title = _assetsCollection.localizedTitle; diff --git a/Riot/Modules/MediaPicker/MediaPickerViewController.m b/Riot/Modules/MediaPicker/MediaPickerViewController.m index 92261789cb..9aa20846b6 100644 --- a/Riot/Modules/MediaPicker/MediaPickerViewController.m +++ b/Riot/Modules/MediaPicker/MediaPickerViewController.m @@ -214,7 +214,7 @@ - (void)viewWillAppear:(BOOL)animated [self userInterfaceThemeDidChange]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"MediaPicker"]; + [Analytics.shared trackScreen:@"MediaPicker"]; if (!userAlbumsQueue) { diff --git a/Riot/Modules/Room/Attachements/AttachmentsViewController.m b/Riot/Modules/Room/Attachements/AttachmentsViewController.m index 596c6a3b54..556539a6a8 100644 --- a/Riot/Modules/Room/Attachements/AttachmentsViewController.m +++ b/Riot/Modules/Room/Attachements/AttachmentsViewController.m @@ -78,7 +78,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"AttachmentsViewer"]; + [Analytics.shared trackScreen:@"AttachmentsViewer"]; } - (void)destroy diff --git a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m index d1888c6d7b..0d68d9b423 100644 --- a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m +++ b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m @@ -240,7 +240,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"RoomMemberDetails"]; + [Analytics.shared trackScreen:@"RoomMemberDetails"]; [self userInterfaceThemeDidChange]; diff --git a/Riot/Modules/Room/Members/RoomParticipantsViewController.m b/Riot/Modules/Room/Members/RoomParticipantsViewController.m index 1776b87758..737a3d1be6 100644 --- a/Riot/Modules/Room/Members/RoomParticipantsViewController.m +++ b/Riot/Modules/Room/Members/RoomParticipantsViewController.m @@ -247,7 +247,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"RoomParticipants"]; + [Analytics.shared trackScreen:@"RoomParticipants"]; // Refresh display [self refreshTableView]; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 46c83de523..5fb98c36ea 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -568,7 +568,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"ChatRoom"]; + [Analytics.shared trackScreen:@"ChatRoom"]; // Refresh the room title view [self refreshRoomTitle]; @@ -610,8 +610,8 @@ - (void)viewWillAppear:(BOOL)animated [self.roomDataSource reload]; [LegacyAppDelegate theDelegate].lastNavigatedRoomIdFromPush = nil; - notificationTaskProfile = [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:AnalyticsNoficationsTimeToDisplayContent - category:AnalyticsNoficationsCategory]; + notificationTaskProfile = [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:Analytics.NotificationsTimeToDisplayContent + category:Analytics.NotificationsCategory]; } } diff --git a/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m b/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m index 0e14b53eeb..17bfde94ab 100644 --- a/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m +++ b/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m @@ -111,7 +111,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"RoomFilesSearch"]; + [Analytics.shared trackScreen:@"RoomFilesSearch"]; // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { diff --git a/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m b/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m index 4e93f01d6d..13ec858ad1 100644 --- a/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m +++ b/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m @@ -112,7 +112,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"RoomMessagesSearch"]; + [Analytics.shared trackScreen:@"RoomMessagesSearch"]; // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { diff --git a/Riot/Modules/Room/Search/RoomSearchViewController.m b/Riot/Modules/Room/Search/RoomSearchViewController.m index 2a09832633..f73ada35fc 100644 --- a/Riot/Modules/Room/Search/RoomSearchViewController.m +++ b/Riot/Modules/Room/Search/RoomSearchViewController.m @@ -108,7 +108,7 @@ - (void)viewWillAppear:(BOOL)animated } // Screen tracking - [[Analytics sharedInstance] trackScreen:@"RoomsSearch"]; + [Analytics.shared trackScreen:@"RoomsSearch"]; // Enable the search field by default at the screen opening if (self.searchBarHidden) diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.m b/Riot/Modules/Room/Settings/RoomSettingsViewController.m index 7ca017b57a..de36a4035e 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m @@ -313,7 +313,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"RoomSettings"]; + [Analytics.shared trackScreen:@"RoomSettings"]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didUpdateRules:) name:kMXNotificationCenterDidUpdateRules object:nil]; diff --git a/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m b/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m index d042146738..c3a7c752ce 100644 --- a/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m +++ b/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m @@ -146,7 +146,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"DirectoryServerPicker"]; + [Analytics.shared trackScreen:@"DirectoryServerPicker"]; // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { diff --git a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift index 4b05e230ce..d4d7b90dcc 100644 --- a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift +++ b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift @@ -107,7 +107,7 @@ extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelega func serviceTermsModalScreenCoordinatorDidAccept(_ coordinator: ServiceTermsModalScreenCoordinatorType) { if serviceTerms.serviceType == MXServiceTypeIdentityService { - Analytics.sharedInstance().trackValue(1, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted) + Analytics.shared.trackValue(1, category: MXKAnalyticsCategory.contacts.rawValue, name: Analytics.ContactsIdentityServerAccepted) } self.delegate?.serviceTermsModalCoordinatorDidAccept(self) @@ -119,7 +119,7 @@ extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelega func serviceTermsModalScreenCoordinatorDidDecline(_ coordinator: ServiceTermsModalScreenCoordinatorType) { if serviceTerms.serviceType == MXServiceTypeIdentityService { - Analytics.sharedInstance().trackValue(1, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted) + Analytics.shared.trackValue(1, category: MXKAnalyticsCategory.contacts.rawValue, name: Analytics.ContactsIdentityServerAccepted) disableIdentityServer() } @@ -131,7 +131,7 @@ extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelega extension ServiceTermsModalCoordinator: UIAdaptivePresentationControllerDelegate { func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { if serviceTerms.serviceType == MXServiceTypeIdentityService { - Analytics.sharedInstance().trackValue(0, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted) + Analytics.shared.trackValue(0, category: MXKAnalyticsCategory.contacts.rawValue, name: Analytics.ContactsIdentityServerAccepted) } self.delegate?.serviceTermsModalCoordinatorDidDismissInteractively(self) diff --git a/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m b/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m index 3e4b4297e7..b617fe4651 100644 --- a/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m +++ b/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m @@ -97,7 +97,7 @@ - (void)viewWillAppear:(BOOL)animated [self userInterfaceThemeDidChange]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"DeactivateAccount"]; + [Analytics.shared trackScreen:@"DeactivateAccount"]; } - (void)viewDidLayoutSubviews diff --git a/Riot/Modules/Settings/Language/LanguagePickerViewController.m b/Riot/Modules/Settings/Language/LanguagePickerViewController.m index aac54c0768..c93b88ae19 100644 --- a/Riot/Modules/Settings/Language/LanguagePickerViewController.m +++ b/Riot/Modules/Settings/Language/LanguagePickerViewController.m @@ -111,7 +111,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"CountryPicker"]; + [Analytics.shared trackScreen:@"CountryPicker"]; } - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath; diff --git a/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m b/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m index 0ab2fe0d82..8842d7e87c 100644 --- a/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m +++ b/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m @@ -100,7 +100,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"CountryPicker"]; + [Analytics.shared trackScreen:@"CountryPicker"]; } - (void)destroy diff --git a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m index b9b9ac474a..02eac7c51b 100644 --- a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m +++ b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m @@ -162,7 +162,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"ManageSession"]; + [Analytics.shared trackScreen:@"ManageSession"]; // Release the potential pushed view controller [self releasePushedViewController]; diff --git a/Riot/Modules/Settings/Security/SecurityViewController.m b/Riot/Modules/Settings/Security/SecurityViewController.m index 5c6d93aab8..9b61048a6f 100644 --- a/Riot/Modules/Settings/Security/SecurityViewController.m +++ b/Riot/Modules/Settings/Security/SecurityViewController.m @@ -251,7 +251,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"Security"]; + [Analytics.shared trackScreen:@"Security"]; // Release the potential pushed view controller [self releasePushedViewController]; diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 1993ab9f7f..6978f15e8b 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -778,7 +778,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"Settings"]; + [Analytics.shared trackScreen:@"Settings"]; // Refresh display [self refreshSettings]; @@ -2251,11 +2251,11 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N { MXKTableViewCellWithLabelAndSwitch* sendCrashReportCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; - sendCrashReportCell.mxkLabel.text = [VectorL10n settingsSendCrashReport]; - sendCrashReportCell.mxkSwitch.on = RiotSettings.shared.enableCrashReport; + sendCrashReportCell.mxkLabel.text = VectorL10n.settingsAnalyticsAndCrashData; + sendCrashReportCell.mxkSwitch.on = Analytics.shared.isRunning; sendCrashReportCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; sendCrashReportCell.mxkSwitch.enabled = YES; - [sendCrashReportCell.mxkSwitch addTarget:self action:@selector(toggleSendCrashReport:) forControlEvents:UIControlEventTouchUpInside]; + [sendCrashReportCell.mxkSwitch addTarget:self action:@selector(toggleAnalytics:) forControlEvents:UIControlEventTouchUpInside]; cell = sendCrashReportCell; } @@ -3115,27 +3115,20 @@ - (void)toggleEnableURLPreviews:(UISwitch *)sender [[MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession] reset]; } -- (void)toggleSendCrashReport:(id)sender +- (void)toggleAnalytics:(UISwitch *)sender { - BOOL enable = RiotSettings.shared.enableCrashReport; - if (enable) + if (sender.isOn) { - MXLogDebug(@"[SettingsViewController] disable automatic crash report and analytics sending"); - - RiotSettings.shared.enableCrashReport = NO; - - [[Analytics sharedInstance] stop]; - - // Remove potential crash file. - [MXLogger deleteCrashLog]; + MXLogDebug(@"[SettingsViewController] enable automatic crash report and analytics sending"); + [Analytics.shared optInWith:self.mainSession]; } else { - MXLogDebug(@"[SettingsViewController] enable automatic crash report and analytics sending"); - - RiotSettings.shared.enableCrashReport = YES; + MXLogDebug(@"[SettingsViewController] disable automatic crash report and analytics sending"); + [Analytics.shared optOutWith:self.mainSession]; - [[Analytics sharedInstance] start]; + // Remove potential crash file. + [MXLogger deleteCrashLog]; } } diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index 4a4751d830..db0b2d77e7 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -196,11 +196,15 @@ - (void)viewDidAppear:(BOOL)animated if (!authIsShown) { - // Check whether the user has been already prompted to send crash reports. - // (Check whether 'enableCrashReport' flag has been set once) - if (!RiotSettings.shared.isEnableCrashReportHasBeenSetOnce) + // Check whether the user should be prompted to send analytics. + MXSession *mxSession = self.mxSessions.firstObject; + if (mxSession && [Analytics.shared shouldShowPseudonymousAnalyticsPromptFor:mxSession]) { - [self promptUserBeforeUsingAnalytics]; + // We don't need to prompt users who previously declined the old analytics. + if (!RiotSettings.shared.hasSeenAndDeclinedMatomoAnalytics) + { + [self promptUserBeforeUsingAnalytics]; + } } [self refreshTabBarBadges]; @@ -923,45 +927,51 @@ - (NSInteger)indexOfTabItemWithTag:(NSUInteger)tag - (void)promptUserBeforeUsingAnalytics { - MXLogDebug(@"[MasterTabBarController]: Invite the user to send crash reports"); + MXLogDebug(@"[MasterTabBarController]: Invite the user to send analytics"); - __weak typeof(self) weakSelf = self; + MXSession *mxSession = self.mxSessions.firstObject; + + if (!mxSession) + { + MXLogError(@"[MasterTabBarController]: Failed to prompt for Analytics due to missing MXSession."); + return; + } + + MXWeakify(self); [currentAlert dismissViewControllerAnimated:NO completion:nil]; - NSString *appDisplayName = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"]; + NSString *title = [VectorL10n analyticsPromptTitle:AppInfo.current.displayName]; + NSString *message; + if (RiotSettings.shared.hasAcceptedMatomoAnalytics) + { + message = [VectorL10n analyticsPromptPosthogUpgrade:AppInfo.current.displayName]; + } + else + { + message = [VectorL10n analyticsPromptNewUser:AppInfo.current.displayName]; + } - currentAlert = [UIAlertController alertControllerWithTitle:[VectorL10n googleAnalyticsUsePrompt:appDisplayName] message:nil preferredStyle:UIAlertControllerStyleAlert]; + currentAlert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; [currentAlert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n no] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - - RiotSettings.shared.enableCrashReport = NO; - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - } - - }]]; + + MXStrongifyAndReturnIfNil(self); + [Analytics.shared optOutWith:mxSession]; + self->currentAlert = nil; + + }]]; [currentAlert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n yes] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - - RiotSettings.shared.enableCrashReport = YES; - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - } - - [[Analytics sharedInstance] start]; - - }]]; + + MXStrongifyAndReturnIfNil(self); + [Analytics.shared optInWith:mxSession]; + self->currentAlert = nil; + }]]; [currentAlert mxk_setAccessibilityIdentifier: @"HomeVCUseAnalyticsAlert"]; [self presentViewController:currentAlert animated:YES completion:nil]; diff --git a/Riot/Modules/UserDevices/UsersDevicesViewController.m b/Riot/Modules/UserDevices/UsersDevicesViewController.m index 5f83b5bc59..5371b3100d 100644 --- a/Riot/Modules/UserDevices/UsersDevicesViewController.m +++ b/Riot/Modules/UserDevices/UsersDevicesViewController.m @@ -121,7 +121,7 @@ - (void)viewWillAppear:(BOOL)animated [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"UnknowDevices"]; + [Analytics.shared trackScreen:@"UnknownDevices"]; [self.tableView reloadData]; } From 2b80c0437af5f1f51a496e05a7eed4e3ecab2378 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 19 Nov 2021 10:00:24 +0000 Subject: [PATCH 010/109] Don't read analytics opt in status from account data. Update PostHog to 1.4.3. Add tests for prompt type. --- Podfile | 2 +- Podfile.lock | 53 +++++++---- Riot/Managers/Analytics/Analytics.swift | 95 +++++++++---------- .../Analytics/AnalyticsSettings.swift | 21 ++-- .../Analytics/PHGPostHogConfiguration.swift | 5 +- Riot/Managers/Settings/RiotSettings.swift | 17 +++- Riot/Modules/Application/LegacyAppDelegate.m | 4 +- .../Modules/Settings/SettingsViewController.m | 4 +- Riot/Modules/TabBar/MasterTabBarController.m | 13 +-- RiotTests/AnalyticsTests.swift | 71 ++++++++++++++ 10 files changed, 184 insertions(+), 101 deletions(-) create mode 100644 RiotTests/AnalyticsTests.swift diff --git a/Podfile b/Podfile index d9925aa6dd..97bb4efee8 100644 --- a/Podfile +++ b/Podfile @@ -68,7 +68,7 @@ abstract_target 'RiotPods' do pod 'WeakDictionary', '~> 2.0' # PostHog for analytics - pod 'PostHog', '~> 1.4.2' + pod 'PostHog', '~> 1.4.3' # Remove warnings from "bad" pods pod 'OLMKit', :inhibit_warnings => true diff --git a/Podfile.lock b/Podfile.lock index 3f62ea7534..0722e15bc6 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -48,6 +48,7 @@ PODS: - Introspect (0.1.3) - JitsiMeetSDK (3.10.2) - KeychainAccess (4.2.2) + - Keys (1.0.1) - KituraContracts (1.2.1): - LoggerAPI (~> 1.7) - KTCenterFlowLayout (1.3.1) @@ -56,19 +57,29 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatomoTracker (7.4.1): - - MatomoTracker/Core (= 7.4.1) - - MatomoTracker/Core (7.4.1) - - MatrixSDK (0.20.15): - - MatrixSDK/Core (= 0.20.15) - - MatrixSDK/Core (0.20.15): + - MatrixKit (0.16.10): + - Down (~> 0.11.0) + - DTCoreText (~> 1.6.25) + - HPGrowingTextView (~> 1.1) + - libPhoneNumber-iOS (~> 0.9.13) + - MatrixKit/Core (= 0.16.10) + - MatrixSDK (= 0.20.10) + - MatrixKit/Core (0.16.10): + - Down (~> 0.11.0) + - DTCoreText (~> 1.6.25) + - HPGrowingTextView (~> 1.1) + - libPhoneNumber-iOS (~> 0.9.13) + - MatrixSDK (= 0.20.10) + - MatrixSDK (0.20.10): + - MatrixSDK/Core (= 0.20.10) + - MatrixSDK/Core (0.20.10): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - OLMKit (~> 3.2.5) - Realm (= 10.16.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.20.15): + - MatrixSDK/JingleCallStack (0.20.10): - JitsiMeetSDK (= 3.10.2) - MatrixSDK/Core - OLMKit (3.2.5): @@ -76,6 +87,7 @@ PODS: - OLMKit/olmcpp (= 3.2.5) - OLMKit/olmc (3.2.5) - OLMKit/olmcpp (3.2.5) + - PostHog (1.4.3) - ReadMoreTextView (3.0.1) - Realm (10.16.0): - Realm/Headers (= 10.16.0) @@ -104,22 +116,20 @@ PODS: DEPENDENCIES: - DGCollectionViewLeftAlignFlowLayout (~> 1.0.4) - - Down (~> 0.11.0) - DSWaveformImage (~> 6.1.1) - - DTCoreText (~> 1.6.25) - ffmpeg-kit-ios-audio (~> 4.5) - FLEX (~> 4.5.0) - FlowCommoniOS (~> 1.12.0) - GBDeviceInfo (~> 6.6.0) - - HPGrowingTextView (~> 1.1) - Introspect (~> 0.1) - KeychainAccess (~> 4.2.2) + - Keys (from `Pods/CocoaPodsKeys`) - KTCenterFlowLayout (~> 1.3.1) - - libPhoneNumber-iOS (~> 0.9.13) - - MatomoTracker (~> 7.4.1) - - MatrixSDK (= 0.20.15) - - MatrixSDK/JingleCallStack (= 0.20.15) + - MatrixKit (= 0.16.10) + - MatrixSDK + - MatrixSDK/JingleCallStack - OLMKit + - PostHog (~> 1.4.3) - ReadMoreTextView (~> 3.0.1) - Reusable (~> 4.1) - SideMenu (~> 6.5) @@ -157,9 +167,10 @@ SPEC REPOS: - libPhoneNumber-iOS - LoggerAPI - Logging - - MatomoTracker + - MatrixKit - MatrixSDK - OLMKit + - PostHog - ReadMoreTextView - Realm - Reusable @@ -173,6 +184,10 @@ SPEC REPOS: - zxcvbn-ios - ZXingObjC +EXTERNAL SOURCES: + Keys: + :path: Pods/CocoaPodsKeys + SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24 @@ -192,15 +207,17 @@ SPEC CHECKSUMS: Introspect: 2be020f30f084ada52bb4387fff83fa52c5c400e JitsiMeetSDK: 2f118fa770f23e518f3560fc224fae3ac7062223 KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51 + Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 KituraContracts: e845e60dc8627ad0a76fa55ef20a45451d8f830b KTCenterFlowLayout: 6e02b50ab2bd865025ae82fe266ed13b6d9eaf97 libbase58: 7c040313537b8c44b6e2d15586af8e21f7354efd libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatomoTracker: 24a846c9d3aa76933183fe9d47fd62c9efa863fb - MatrixSDK: 2f4d3aacb1c53e2785f0be71d24b8e62e5c5c056 + MatrixKit: c3f0bb056ceeb015e2f1688543ac4dbcf88bef2f + MatrixSDK: 0e2ed8fc6f004cac4b4ab46f038a86fe49ce4007 OLMKit: 9fb4799c4a044dd2c06bda31ec31a12191ad30b5 + PostHog: 066d1528a2d8b9217c1815872702567ed4e80c1c ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d Realm: b6027801398f3743fc222f096faa85281b506e6c Reusable: 6bae6a5e8aa793c9c441db0213c863a64bce9136 @@ -214,6 +231,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 989bcc8b1857dc64a9b810ddaf4446903adbe162 +PODFILE CHECKSUM: e86a58a6bea003fc10d9bcb721be494b06fb8a64 COCOAPODS: 1.11.2 diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index d1e2388dbf..91625a9ba5 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -22,58 +22,70 @@ import PostHog static let shared = Analytics() - private(set) var isRunning = false - private var postHog: PHGPostHog? - // MARK: - Public + private(set) var isRunning = false - func shouldShowPseudonymousAnalyticsPrompt(for session: MXSession) -> Bool { - return AnalyticsSettings(session: session).showPseudonymousAnalyticsPrompt + var shouldShowAnalyticsPrompt: Bool { + // Show an analytics prompt when the user hasn't seen the PostHog prompt before + // so long as they haven't previously declined the Matomo analytics prompt. + !RiotSettings.shared.hasSeenAnalyticsPrompt && !RiotSettings.shared.hasDeclinedMatomoAnalytics } + var promptShouldDisplayUpgradeMessage: Bool { + // Show an analytics prompt when the user hasn't seen the PostHog prompt before + // so long as they haven't previously declined the Matomo analytics prompt. + RiotSettings.shared.hasAcceptedMatomoAnalytics + } + + // MARK: - Public + func optIn(with session: MXSession?) { guard let session = session else { return } + RiotSettings.shared.enableAnalytics = true var settings = AnalyticsSettings(session: session) - settings.generateIDIfMissing() - settings.pseudonymousAnalyticsOptIn = true - settings.showPseudonymousAnalyticsPrompt = false - session.setAccountData(settings.dictionary, forType: AnalyticsSettings.eventType) { - MXLog.debug("[Analytics] Successfully updated analytics settings in account data.") - } failure: { error in - MXLog.error("[Analytics] Failed to update analytics settings.") + if settings.id == nil { + settings.generateID() + + session.setAccountData(settings.dictionary, forType: AnalyticsSettings.eventType) { + MXLog.debug("[Analytics] Successfully updated analytics settings in account data.") + } failure: { error in + MXLog.error("[Analytics] Failed to update analytics settings.") + } } - } - - func optOut(with session: MXSession) { - var settings = AnalyticsSettings(session: session) - settings.id = nil - settings.pseudonymousAnalyticsOptIn = false - settings.showPseudonymousAnalyticsPrompt = false - session.setAccountData(settings.dictionary, forType: AnalyticsSettings.eventType, success: nil) { error in - MXLog.error("[Analytics] Failed to update analytics settings.") + startIfEnabled() + + if !RiotSettings.shared.isIdentifiedForAnalytics { + identify(with: settings) } } - private func start(with pseudonymousID: String) { - guard !isRunning else { return } + func optOut() { + RiotSettings.shared.enableAnalytics = false + reset() + } + + func startIfEnabled() { + guard RiotSettings.shared.enableAnalytics, !isRunning else { return } postHog = PHGPostHog(configuration: PHGPostHogConfiguration.standard) postHog?.enable() isRunning = true MXLog.debug("[Analytics] Started.") - - if !RiotSettings.shared.hasPseudonymousAnalyticsIdentified { - postHog?.identify(pseudonymousID) - MXLog.debug("[Analytics] Identified.") - RiotSettings.shared.hasPseudonymousAnalyticsIdentified = true + } + + private func identify(with settings: AnalyticsSettings) { + guard let id = settings.id else { + MXLog.warning("[Analytics] identify(with:) called before an ID has been generated.") + return } - postHog?.capture("analyticsDidStart") - forceUpload() + postHog?.identify(id) + MXLog.debug("[Analytics] Identified.") + RiotSettings.shared.isIdentifiedForAnalytics = true } func reset() { @@ -84,7 +96,7 @@ import PostHog MXLog.debug("[Analytics] Stopped.") postHog?.reset() - RiotSettings.shared.hasPseudonymousAnalyticsIdentified = false + RiotSettings.shared.isIdentifiedForAnalytics = false postHog = nil } @@ -114,27 +126,6 @@ extension Analytics { } extension Analytics: MXAnalyticsDelegate { - var settingsEventType: String { AnalyticsSettings.eventType } - - func handleSettingsEvent(_ event: [AnyHashable: Any]) { - guard event["type"] as? String == AnalyticsSettings.eventType, - let content = event["content"] as? [AnyHashable: Any] - else { - MXLog.error("[Analytics] handleSettingsEvent: invalid event") - return - } - - let settings = AnalyticsSettings(dictionary: content) - - if !settings.showPseudonymousAnalyticsPrompt, - settings.pseudonymousAnalyticsOptIn == true, - let id = settings.id { - start(with: id) - } else { - reset() - } - } - @objc func trackDuration(_ seconds: TimeInterval, category: String, name: String) { // postHog?.capture("\(category):\(name)", properties: ["duration": seconds]) } diff --git a/Riot/Managers/Analytics/AnalyticsSettings.swift b/Riot/Managers/Analytics/AnalyticsSettings.swift index d85cfc8b21..2b26c17b43 100644 --- a/Riot/Managers/Analytics/AnalyticsSettings.swift +++ b/Riot/Managers/Analytics/AnalyticsSettings.swift @@ -21,22 +21,19 @@ struct AnalyticsSettings { private enum Constants { static let idKey = "id" - static let optInKey = "pseudonymousAnalyticsOptIn" - static let showPromptKey = "showPseudonymousAnalyticsPrompt" + static let webOptInKey = "pseudonymousAnalyticsOptIn" } /// A randomly generated analytics token for this user. /// This is suggested to be a 128-bit hex encoded string. var id: String? - /// Boolean indicating whether the user has opted in. - /// If nil, the user hasn't yet given consent either way - var pseudonymousAnalyticsOptIn: Bool? + /// Unused on iOS but necessary to load the value in case opt in was declined on web, + /// but accepted on iOS. Otherwise generating an ID would wipe out the existing value. + private var webOptIn: Bool? - /// Boolean indicating whether to show the analytics opt-in prompt. - var showPseudonymousAnalyticsPrompt: Bool - - mutating func generateIDIfMissing() { + /// Generate a new random analytics ID. This method has no effect if an ID already exists. + mutating func generateID() { guard id == nil else { return } // Generate a 32 character analytics ID containing the characters 0-f. @@ -49,15 +46,13 @@ struct AnalyticsSettings { extension AnalyticsSettings { init(dictionary: Dictionary?) { self.id = dictionary?[Constants.idKey] as? String - self.pseudonymousAnalyticsOptIn = dictionary?[Constants.optInKey] as? Bool - self.showPseudonymousAnalyticsPrompt = dictionary?[Constants.showPromptKey] as? Bool ?? true + self.webOptIn = dictionary?[Constants.webOptInKey] as? Bool } var dictionary: Dictionary { var dictionary = [AnyHashable: Any]() dictionary[Constants.idKey] = id - dictionary[Constants.optInKey] = pseudonymousAnalyticsOptIn - dictionary[Constants.showPromptKey] = showPseudonymousAnalyticsPrompt + dictionary[Constants.webOptInKey] = webOptIn return dictionary } diff --git a/Riot/Managers/Analytics/PHGPostHogConfiguration.swift b/Riot/Managers/Analytics/PHGPostHogConfiguration.swift index a9a0ba590a..04f17290dd 100644 --- a/Riot/Managers/Analytics/PHGPostHogConfiguration.swift +++ b/Riot/Managers/Analytics/PHGPostHogConfiguration.swift @@ -19,6 +19,9 @@ import Keys extension PHGPostHogConfiguration { static var standard: PHGPostHogConfiguration { - PHGPostHogConfiguration(apiKey: RiotKeys().postHog, host: BuildSettings.analyticsHost) + let configuration = PHGPostHogConfiguration(apiKey: RiotKeys().postHog, host: BuildSettings.analyticsHost) + configuration.shouldSendDeviceID = false + + return configuration } } diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 3413dbdaf3..13a9a7b676 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -103,7 +103,13 @@ final class RiotSettings: NSObject { /// Whether the user has both seen the Matomo analytics prompt and declined it. /// This is used to prevent users who previously opted out from being asked again. - var hasSeenAndDeclinedMatomoAnalytics: Bool { + var hasSeenAnalyticsPrompt: Bool { + RiotSettings.defaults.object(forKey: UserDefaultsKeys.enableAnalytics) != nil + } + + /// Whether the user has both seen the Matomo analytics prompt and declined it. + /// This is used to prevent users who previously opted out from being asked again. + var hasDeclinedMatomoAnalytics: Bool { RiotSettings.defaults.object(forKey: UserDefaultsKeys.matomoAnalytics) != nil && !RiotSettings.defaults.bool(forKey: UserDefaultsKeys.matomoAnalytics) } @@ -113,10 +119,13 @@ final class RiotSettings: NSObject { RiotSettings.defaults.bool(forKey: UserDefaultsKeys.matomoAnalytics) } - #warning("Rename me!") /// Indicates if the device has already called identify for this session to PostHog. - @UserDefault(key: "hasPseudonymousAnalyticsIdentified", defaultValue: false, storage: defaults) - var hasPseudonymousAnalyticsIdentified + @UserDefault(key: UserDefaultsKeys.enableAnalytics, defaultValue: false, storage: defaults) + var enableAnalytics + + /// Indicates if the device has already called identify for this session to PostHog. + @UserDefault(key: "isIdentifiedForAnalytics", defaultValue: false, storage: defaults) + var isIdentifiedForAnalytics @UserDefault(key: "enableRageShake", defaultValue: false, storage: defaults) var enableRageShake diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 9de053ba90..a85fe1e2bf 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -433,7 +433,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( _isAppForeground = NO; _handleSelfVerificationRequest = YES; - // Configure our analytics. It will start automatically if the option is enabled + // Configure our analytics. It will start if the option is enabled Analytics *analytics = Analytics.shared; [MXSDKOptions sharedInstance].analyticsDelegate = analytics; [DecryptionFailureTracker sharedInstance].delegate = analytics; @@ -441,6 +441,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( MXBaseProfiler *profiler = [MXBaseProfiler new]; profiler.analytics = analytics; [MXSDKOptions sharedInstance].profiler = profiler; + + [analytics startIfEnabled]; self.localAuthenticationService = [[LocalAuthenticationService alloc] initWithPinCodePreferences:[PinCodePreferences shared]]; diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 6978f15e8b..3042664113 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -2252,7 +2252,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N MXKTableViewCellWithLabelAndSwitch* sendCrashReportCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; sendCrashReportCell.mxkLabel.text = VectorL10n.settingsAnalyticsAndCrashData; - sendCrashReportCell.mxkSwitch.on = Analytics.shared.isRunning; + sendCrashReportCell.mxkSwitch.on = RiotSettings.shared.enableAnalytics; sendCrashReportCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; sendCrashReportCell.mxkSwitch.enabled = YES; [sendCrashReportCell.mxkSwitch addTarget:self action:@selector(toggleAnalytics:) forControlEvents:UIControlEventTouchUpInside]; @@ -3125,7 +3125,7 @@ - (void)toggleAnalytics:(UISwitch *)sender else { MXLogDebug(@"[SettingsViewController] disable automatic crash report and analytics sending"); - [Analytics.shared optOutWith:self.mainSession]; + [Analytics.shared optOut]; // Remove potential crash file. [MXLogger deleteCrashLog]; diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index db0b2d77e7..61329d157b 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -197,14 +197,9 @@ - (void)viewDidAppear:(BOOL)animated if (!authIsShown) { // Check whether the user should be prompted to send analytics. - MXSession *mxSession = self.mxSessions.firstObject; - if (mxSession && [Analytics.shared shouldShowPseudonymousAnalyticsPromptFor:mxSession]) + if (Analytics.shared.shouldShowAnalyticsPrompt) { - // We don't need to prompt users who previously declined the old analytics. - if (!RiotSettings.shared.hasSeenAndDeclinedMatomoAnalytics) - { - [self promptUserBeforeUsingAnalytics]; - } + [self promptUserBeforeUsingAnalytics]; } [self refreshTabBarBadges]; @@ -943,7 +938,7 @@ - (void)promptUserBeforeUsingAnalytics NSString *title = [VectorL10n analyticsPromptTitle:AppInfo.current.displayName]; NSString *message; - if (RiotSettings.shared.hasAcceptedMatomoAnalytics) + if (Analytics.shared.promptShouldDisplayUpgradeMessage) { message = [VectorL10n analyticsPromptPosthogUpgrade:AppInfo.current.displayName]; } @@ -959,7 +954,7 @@ - (void)promptUserBeforeUsingAnalytics handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); - [Analytics.shared optOutWith:mxSession]; + [Analytics.shared optOut]; self->currentAlert = nil; }]]; diff --git a/RiotTests/AnalyticsTests.swift b/RiotTests/AnalyticsTests.swift new file mode 100644 index 0000000000..b31cbd6c5f --- /dev/null +++ b/RiotTests/AnalyticsTests.swift @@ -0,0 +1,71 @@ +// +// Copyright 2021 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 XCTest +@testable import Riot + +class AnalyticsTests: XCTestCase { + func testAnalyticsPromptNewUser() { + // Given a fresh install of the app (with neither PostHog nor Matomo analytics having been set). + RiotSettings.defaults.removeObject(forKey: RiotSettings.UserDefaultsKeys.enableAnalytics) + RiotSettings.defaults.removeObject(forKey: RiotSettings.UserDefaultsKeys.matomoAnalytics) + + // When the user is prompted for analytics. + let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt + let displayUpgradeMessage = Analytics.shared.promptShouldDisplayUpgradeMessage + + // Then the regular prompt should be shown. + XCTAssertTrue(showPrompt, "A prompt should be shown when for a new user") + XCTAssertFalse(displayUpgradeMessage, "The prompt should not ask about upgrading from Matomo") + } + + func testAnalyticsPromptUpgradeFromMatomo() { + // Given an existing install of the app where the user previously accepted Matomo analytics + RiotSettings.defaults.removeObject(forKey: RiotSettings.UserDefaultsKeys.enableAnalytics) + RiotSettings.defaults.set(true, forKey: RiotSettings.UserDefaultsKeys.matomoAnalytics) + + // When the user is prompted for analytics + let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt + let displayUpgradeMessage = Analytics.shared.promptShouldDisplayUpgradeMessage + + // Then an upgrade prompt should be shown. + XCTAssertTrue(showPrompt, "A prompt should be shown when for a new user") + XCTAssertTrue(displayUpgradeMessage, "The prompt should not ask about upgrading from Matomo") + } + + func testAnalyticsPromptUserDeclinedMatomo() { + // Given an existing install of the app where the user previously declined Matomo analytics + RiotSettings.defaults.removeObject(forKey: RiotSettings.UserDefaultsKeys.enableAnalytics) + RiotSettings.defaults.set(false, forKey: RiotSettings.UserDefaultsKeys.matomoAnalytics) + + // When the user is prompted for analytics + let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt + + // Then no prompt should be shown. + XCTAssertFalse(showPrompt, "A prompt should be shown when for a new user") + } + + func testAnalyticsPromptUserAcceptedPostHog() { + // Given an existing install of the app where the user previously accepted PostHog + RiotSettings.defaults.set(true, forKey: RiotSettings.UserDefaultsKeys.enableAnalytics) + + // When the user is prompted for analytics + let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt + + // Then no prompt should be shown. + XCTAssertFalse(showPrompt, "A prompt should be shown when for a new user") + } +} From 63e9dcde5560b393c152fcae3abf319dd860ed83 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 19 Nov 2021 16:32:39 +0000 Subject: [PATCH 011/109] Add missed MXLogger calls in Analytics. --- Riot/Managers/Analytics/Analytics.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index 91625a9ba5..3acf02df76 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -75,6 +75,10 @@ import PostHog postHog?.enable() isRunning = true MXLog.debug("[Analytics] Started.") + + // Catch and log crashes + MXLogger.logCrashes(true) + MXLogger.setBuildVersion(AppDelegate.theDelegate().build) } private func identify(with settings: AnalyticsSettings) { @@ -99,6 +103,8 @@ import PostHog RiotSettings.shared.isIdentifiedForAnalytics = false postHog = nil + + MXLogger.logCrashes(false) } func forceUpload() { From 50dea9843bcae7c1e2332ca30eff716f18edf6dc Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 22 Nov 2021 14:28:09 +0000 Subject: [PATCH 012/109] Update to PostHog 1.4.4 --- Podfile | 2 +- Podfile.lock | 8 ++++---- Riot/Managers/Analytics/Analytics.swift | 4 +--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Podfile b/Podfile index 97bb4efee8..fa5354524f 100644 --- a/Podfile +++ b/Podfile @@ -68,7 +68,7 @@ abstract_target 'RiotPods' do pod 'WeakDictionary', '~> 2.0' # PostHog for analytics - pod 'PostHog', '~> 1.4.3' + pod 'PostHog', '~> 1.4.4' # Remove warnings from "bad" pods pod 'OLMKit', :inhibit_warnings => true diff --git a/Podfile.lock b/Podfile.lock index 0722e15bc6..223232453b 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -87,7 +87,7 @@ PODS: - OLMKit/olmcpp (= 3.2.5) - OLMKit/olmc (3.2.5) - OLMKit/olmcpp (3.2.5) - - PostHog (1.4.3) + - PostHog (1.4.4) - ReadMoreTextView (3.0.1) - Realm (10.16.0): - Realm/Headers (= 10.16.0) @@ -129,7 +129,7 @@ DEPENDENCIES: - MatrixSDK - MatrixSDK/JingleCallStack - OLMKit - - PostHog (~> 1.4.3) + - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) - Reusable (~> 4.1) - SideMenu (~> 6.5) @@ -217,7 +217,7 @@ SPEC CHECKSUMS: MatrixKit: c3f0bb056ceeb015e2f1688543ac4dbcf88bef2f MatrixSDK: 0e2ed8fc6f004cac4b4ab46f038a86fe49ce4007 OLMKit: 9fb4799c4a044dd2c06bda31ec31a12191ad30b5 - PostHog: 066d1528a2d8b9217c1815872702567ed4e80c1c + PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d Realm: b6027801398f3743fc222f096faa85281b506e6c Reusable: 6bae6a5e8aa793c9c441db0213c863a64bce9136 @@ -231,6 +231,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: e86a58a6bea003fc10d9bcb721be494b06fb8a64 +PODFILE CHECKSUM: 6d24497e38de7332dbb2c2ff21ad7ed8090c81de COCOAPODS: 1.11.2 diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index 3acf02df76..d1c36a3912 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -24,7 +24,7 @@ import PostHog private var postHog: PHGPostHog? - private(set) var isRunning = false + var isRunning: Bool { postHog?.enabled ?? false } var shouldShowAnalyticsPrompt: Bool { // Show an analytics prompt when the user hasn't seen the PostHog prompt before @@ -73,7 +73,6 @@ import PostHog postHog = PHGPostHog(configuration: PHGPostHogConfiguration.standard) postHog?.enable() - isRunning = true MXLog.debug("[Analytics] Started.") // Catch and log crashes @@ -96,7 +95,6 @@ import PostHog guard isRunning else { return } postHog?.disable() - isRunning = false MXLog.debug("[Analytics] Stopped.") postHog?.reset() From 23555b00fd4f77db03817b65a1f165f26d9f1411 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 24 Nov 2021 10:43:22 +0000 Subject: [PATCH 013/109] Add specific methods to track analytics and test generated event types. --- Riot/Managers/Analytics/Analytics.swift | 61 ++++++++++----- Riot/Managers/Analytics/DecryptionFailure.h | 14 ++-- Riot/Managers/Analytics/DecryptionFailure.m | 7 -- .../Analytics/DecryptionFailureTracker.h | 3 +- .../Analytics/DecryptionFailureTracker.m | 17 +++-- .../Analytics/DictionaryConvertable.swift | 43 +++++++++++ Riot/Managers/Analytics/EventExtensions.swift | 74 +++++++++++++++++++ .../Analytics/Generated/GeneratedEvents.swift | 45 +++++++++++ Riot/Modules/Application/LegacyAppDelegate.m | 2 +- Riot/Modules/Room/RoomViewController.m | 6 +- .../Modal/ServiceTermsModalCoordinator.swift | 6 +- Riot/SupportingFiles/Riot-Bridging-Header.h | 1 + 12 files changed, 232 insertions(+), 47 deletions(-) create mode 100644 Riot/Managers/Analytics/DictionaryConvertable.swift create mode 100644 Riot/Managers/Analytics/EventExtensions.swift create mode 100644 Riot/Managers/Analytics/Generated/GeneratedEvents.swift diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index d1c36a3912..ba6a55ec3e 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -112,29 +112,56 @@ import PostHog func log(event: String) { postHog?.capture(event) } -} - - -// MARK: - Legacy compatibility -extension Analytics { - #warning("Use enums instead") - static let NotificationsCategory = "notifications" - static let NotificationsTimeToDisplayContent = "timelineDisplay" - static let ContactsIdentityServerAccepted = "identityServerAccepted" - static let PerformanceCategory = "Performance" - static let MetricsCategory = "Metrics" - - @objc func trackScreen(_ screenName: String) { + + func trackScreen(_ screenName: String) { // postHog?.capture("screen:\(screenName)") } + + func trackE2EEError(_ reason: DecryptionFailureReason, count: Int) { + for _ in 0.. delegate; +@property (nonatomic, weak) Analytics *delegate; /** Report an event unable to decrypt. diff --git a/Riot/Managers/Analytics/DecryptionFailureTracker.m b/Riot/Managers/Analytics/DecryptionFailureTracker.m index 56521eb585..d34ba00178 100644 --- a/Riot/Managers/Analytics/DecryptionFailureTracker.m +++ b/Riot/Managers/Analytics/DecryptionFailureTracker.m @@ -15,6 +15,7 @@ */ #import "DecryptionFailureTracker.h" +#import "GeneratedInterface-Swift.h" // Call `checkFailures` every `CHECK_INTERVAL` @@ -97,20 +98,20 @@ - (void)reportUnableToDecryptErrorForEvent:(MXEvent *)event withRoomState:(MXRoo switch (event.decryptionError.code) { case MXDecryptingErrorUnknownInboundSessionIdCode: - decryptionFailure.reason = DecryptionFailureReason.olmKeysNotSent; + decryptionFailure.reason = DecryptionFailureReasonOlmKeysNotSent; break; case MXDecryptingErrorOlmCode: - decryptionFailure.reason = DecryptionFailureReason.olmIndexError; + decryptionFailure.reason = DecryptionFailureReasonOlmIndexError; break; case MXDecryptingErrorEncryptionNotEnabledCode: case MXDecryptingErrorUnableToDecryptCode: - decryptionFailure.reason = DecryptionFailureReason.unexpected; + decryptionFailure.reason = DecryptionFailureReasonUnexpected; break; default: - decryptionFailure.reason = DecryptionFailureReason.unspecified; + decryptionFailure.reason = DecryptionFailureReasonUnspecified; break; } @@ -152,17 +153,17 @@ - (void)checkFailures if (failuresToTrack.count) { // Sort failures by error reason - NSMutableDictionary *failuresCounts = [NSMutableDictionary dictionary]; + NSMutableDictionary *failuresCounts = [NSMutableDictionary dictionary]; for (DecryptionFailure *failure in failuresToTrack) { - failuresCounts[failure.reason] = @(failuresCounts[failure.reason].unsignedIntegerValue + 1); + failuresCounts[@(failure.reason)] = @(failuresCounts[@(failure.reason)].unsignedIntegerValue + 1); } MXLogDebug(@"[DecryptionFailureTracker] trackFailures: %@", failuresCounts); - for (NSString *reason in failuresCounts) + for (NSNumber *reason in failuresCounts) { - [_delegate trackValue:failuresCounts[reason] category:kDecryptionFailureTrackerAnalyticsCategory name:reason]; + [self.delegate trackE2EEError:reason.integerValue count:failuresCounts[reason].integerValue]; } } } diff --git a/Riot/Managers/Analytics/DictionaryConvertable.swift b/Riot/Managers/Analytics/DictionaryConvertable.swift new file mode 100644 index 0000000000..c8bc925b4a --- /dev/null +++ b/Riot/Managers/Analytics/DictionaryConvertable.swift @@ -0,0 +1,43 @@ +// +// Copyright 2021 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 DictionaryConvertible { + var dictionary: [String: Any] { get } +} + +extension DictionaryConvertible { + var dictionary: [String: Any] { + let mirror = Mirror(reflecting: self) + let dict: [String: Any] = Dictionary(uniqueKeysWithValues: mirror.children + .compactMap { (label: String?, value: Any) in + guard let label = label else { return nil } + + if let value = value as? NSCoding { + return (label, value) + } + + if let value = value as? CustomStringConvertible { + return (label, value.description) + } + + return nil + }) + + return dict + } +} diff --git a/Riot/Managers/Analytics/EventExtensions.swift b/Riot/Managers/Analytics/EventExtensions.swift new file mode 100644 index 0000000000..718c71ebad --- /dev/null +++ b/Riot/Managers/Analytics/EventExtensions.swift @@ -0,0 +1,74 @@ +// +// Copyright 2021 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 + +// MARK: -Events + +extension AnalyticsEvent.Error: DictionaryConvertible { } +extension AnalyticsEvent.CallStarted: DictionaryConvertible { } +extension AnalyticsEvent.CallEnded: DictionaryConvertible { } +extension AnalyticsEvent.CallError: DictionaryConvertible { } + +// MARK: - Enums + +extension AnalyticsEvent.ErrorDomain: CustomStringConvertible { + var description: String { rawValue } +} + +extension AnalyticsEvent.ErrorName: CustomStringConvertible { + var description: String { rawValue } +} + +// MARK: - Helpers + +extension __MXCallHangupReason { + var errorName: AnalyticsEvent.ErrorName { + switch self { + case .userHangup: + return .VoipUserHangup + case .inviteTimeout: + return .VoipInviteTimeout + case .iceFailed: + return .VoipIceFailed + case .iceTimeout: + return .VoipIceTimeout + case .userMediaFailed: + return .VoipUserMediaFailed + case .unknownError: + return .UnknownError + default: + return .UnknownError + } + } +} + +extension DecryptionFailureReason { + var errorName: AnalyticsEvent.ErrorName { + switch self { + case .unspecified: + return .OlmUnspecifiedError + case .olmKeysNotSent: + return .OlmKeysNotSentError + case .olmIndexError: + return .OlmIndexError + case .unexpected: + return .UnknownError + default: + return .UnknownError + } + } +} diff --git a/Riot/Managers/Analytics/Generated/GeneratedEvents.swift b/Riot/Managers/Analytics/Generated/GeneratedEvents.swift new file mode 100644 index 0000000000..11100e091e --- /dev/null +++ b/Riot/Managers/Analytics/Generated/GeneratedEvents.swift @@ -0,0 +1,45 @@ +import Foundation + +struct AnalyticsEvent { + struct Error { + let domain: ErrorDomain + let name: ErrorName + let context: String? + } + + enum ErrorDomain: String { + case E2EE + case VOIP + } + + enum ErrorName: String { + case UnknownError + case OlmIndexError + case OlmKeysNotSentError + case OlmUnspecifiedError + case VoipUserHangup + case VoipIceFailed + case VoipInviteTimeout + case VoipIceTimeout + case VoipUserMediaFailed + } + + struct CallStarted { + let placed: Bool + let isVideo: Bool + let numParticipants: Int + } + + struct CallEnded { + let placed: Bool + let isVideo: Bool + let durationMs: Int + let numParticipants: Int + } + + struct CallError { + let placed: Bool + let isVideo: Bool + let numParticipants: Int + } +} diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index a85fe1e2bf..4357bec9b0 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -2395,7 +2395,7 @@ - (void)showLaunchAnimation launchAnimationContainerView = launchLoadingView; [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:kMXAnalyticsStartupLaunchScreen - category:kMXAnalyticsStartupCategory]; + category:kMXAnalyticsStartupCategory]; } } diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 5fb98c36ea..bcc330c61f 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -134,6 +134,8 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNotification"; NSNotificationName const RoomGroupCallTileTappedNotification = @"RoomGroupCallTileTappedNotification"; +NSString * const RoomAnalyticsNotificationsCategory = @"notifications"; +NSString * const RoomAnalyticsNotificationsTimeToDisplayContent = @"timelineDisplay"; const NSTimeInterval kResizeComposerAnimationDuration = .05; @interface RoomViewController () Date: Wed, 24 Nov 2021 11:07:34 +0000 Subject: [PATCH 014/109] Remove cocoapods-keys. Use UUID for analytics. Make configuration optional. --- Config/BuildSettings.swift | 6 ++++-- Gemfile | 1 - Gemfile.lock | 11 +---------- Podfile | 5 ----- Podfile.lock | 9 +-------- Riot/Managers/Analytics/Analytics.swift | 5 ++++- Riot/Managers/Analytics/AnalyticsSettings.swift | 6 +----- Riot/Managers/Analytics/PHGPostHogConfiguration.swift | 7 ++++--- 8 files changed, 15 insertions(+), 35 deletions(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 4be53baeac..c9ab63d851 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -166,8 +166,10 @@ final class BuildSettings: NSObject { // MARK: - Analytics #warning("Testing environment.") - static let analyticsHost = "https://posthog-poc.lab.element.dev" - static let analyticsAppId = "14" + // Optional host for PostHog analytics. Set to nil to disable analytics. + static let analyticsHost: String? = "https://posthog-poc.lab.element.dev" + // Public key for submitting analytics. Set to nil to disable analytics. + static let analyticsKey: String? = "rs-pJjsYJTuAkXJfhaMmPUNBhWliDyTKLOOxike6ck8" // MARK: - Bug report diff --git a/Gemfile b/Gemfile index ea061a17e6..53efbaf921 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,6 @@ source "https://rubygems.org" gem "xcode-install" gem "fastlane" gem "cocoapods", '~>1.11.2' -gem "cocoapods-keys" plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index c014fde7c4..ca9078e41f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,9 +3,6 @@ GEM specs: CFPropertyList (3.0.4) rexml - RubyInline (3.12.5) - ZenTest (~> 4.3) - ZenTest (4.12.0) activesupport (6.1.4.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) @@ -67,9 +64,6 @@ GEM typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) cocoapods-downloader (1.5.1) - cocoapods-keys (2.2.1) - dotenv - osx_keychain cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -231,8 +225,6 @@ GEM netrc (0.11.0) optparse (0.1.1) os (1.1.1) - osx_keychain (1.0.2) - RubyInline (~> 3) plist (3.6.0) public_suffix (4.0.6) rake (13.0.6) @@ -300,7 +292,6 @@ PLATFORMS DEPENDENCIES cocoapods (~> 1.11.2) - cocoapods-keys fastlane fastlane-plugin-diawi fastlane-plugin-versioning @@ -308,4 +299,4 @@ DEPENDENCIES xcode-install BUNDLED WITH - 2.2.28 + 2.2.31 diff --git a/Podfile b/Podfile index fa5354524f..f908e38841 100644 --- a/Podfile +++ b/Podfile @@ -127,11 +127,6 @@ abstract_target 'RiotPods' do end -plugin 'cocoapods-keys', { - :project => "Riot", - :keys => ["PostHog"] -} - post_install do |installer| installer.pods_project.targets.each do |target| diff --git a/Podfile.lock b/Podfile.lock index 223232453b..21da4f99d7 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -48,7 +48,6 @@ PODS: - Introspect (0.1.3) - JitsiMeetSDK (3.10.2) - KeychainAccess (4.2.2) - - Keys (1.0.1) - KituraContracts (1.2.1): - LoggerAPI (~> 1.7) - KTCenterFlowLayout (1.3.1) @@ -123,7 +122,6 @@ DEPENDENCIES: - GBDeviceInfo (~> 6.6.0) - Introspect (~> 0.1) - KeychainAccess (~> 4.2.2) - - Keys (from `Pods/CocoaPodsKeys`) - KTCenterFlowLayout (~> 1.3.1) - MatrixKit (= 0.16.10) - MatrixSDK @@ -184,10 +182,6 @@ SPEC REPOS: - zxcvbn-ios - ZXingObjC -EXTERNAL SOURCES: - Keys: - :path: Pods/CocoaPodsKeys - SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24 @@ -207,7 +201,6 @@ SPEC CHECKSUMS: Introspect: 2be020f30f084ada52bb4387fff83fa52c5c400e JitsiMeetSDK: 2f118fa770f23e518f3560fc224fae3ac7062223 KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51 - Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 KituraContracts: e845e60dc8627ad0a76fa55ef20a45451d8f830b KTCenterFlowLayout: 6e02b50ab2bd865025ae82fe266ed13b6d9eaf97 libbase58: 7c040313537b8c44b6e2d15586af8e21f7354efd @@ -231,6 +224,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 6d24497e38de7332dbb2c2ff21ad7ed8090c81de +PODFILE CHECKSUM: 06d0fee10c99dee2531993f8cb2e54ec04f0752b COCOAPODS: 1.11.2 diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index ba6a55ec3e..6b0bbb7b47 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -71,7 +71,10 @@ import PostHog func startIfEnabled() { guard RiotSettings.shared.enableAnalytics, !isRunning else { return } - postHog = PHGPostHog(configuration: PHGPostHogConfiguration.standard) + // Ensures that analytics are configured BuildSettings + guard let configuration = PHGPostHogConfiguration.standard else { return } + + postHog = PHGPostHog(configuration: configuration) postHog?.enable() MXLog.debug("[Analytics] Started.") diff --git a/Riot/Managers/Analytics/AnalyticsSettings.swift b/Riot/Managers/Analytics/AnalyticsSettings.swift index 2b26c17b43..67927cacee 100644 --- a/Riot/Managers/Analytics/AnalyticsSettings.swift +++ b/Riot/Managers/Analytics/AnalyticsSettings.swift @@ -35,11 +35,7 @@ struct AnalyticsSettings { /// Generate a new random analytics ID. This method has no effect if an ID already exists. mutating func generateID() { guard id == nil else { return } - - // Generate a 32 character analytics ID containing the characters 0-f. - id = [UInt8](repeating: 0, count: 16) - .map { _ in String(format: "%02x", UInt8.random(in: 0...UInt8.max)) } - .joined() + id = UUID().uuidString } } diff --git a/Riot/Managers/Analytics/PHGPostHogConfiguration.swift b/Riot/Managers/Analytics/PHGPostHogConfiguration.swift index 04f17290dd..c02b85c30b 100644 --- a/Riot/Managers/Analytics/PHGPostHogConfiguration.swift +++ b/Riot/Managers/Analytics/PHGPostHogConfiguration.swift @@ -15,11 +15,12 @@ // import PostHog -import Keys extension PHGPostHogConfiguration { - static var standard: PHGPostHogConfiguration { - let configuration = PHGPostHogConfiguration(apiKey: RiotKeys().postHog, host: BuildSettings.analyticsHost) + static var standard: PHGPostHogConfiguration? { + guard let apiKey = BuildSettings.analyticsKey, let host = BuildSettings.analyticsHost else { return nil } + + let configuration = PHGPostHogConfiguration(apiKey: apiKey, host: host) configuration.shouldSendDeviceID = false return configuration From 7bdc146b8297a48fded20eca22939ab7d7887aad Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 24 Nov 2021 18:04:54 +0000 Subject: [PATCH 015/109] Use matrix-analytics-events generated stubs (locally for now). Track screens, removing any that aren't part of the schema. --- Podfile | 1 + Podfile.lock | 9 ++- Riot/Managers/Analytics/Analytics.swift | 47 +++++++++------- Riot/Managers/Analytics/AnalyticsScreen.swift | 44 +++++++++++++++ .../Analytics/DictionaryConvertable.swift | 31 +++++----- Riot/Managers/Analytics/EventExtensions.swift | 56 +++++++++++-------- .../Analytics/Generated/GeneratedEvents.swift | 45 --------------- .../AuthenticationViewController.m | 3 - .../Common/Recents/RecentsViewController.h | 5 -- .../Common/Recents/RecentsViewController.m | 5 +- .../Communities/GroupsViewController.m | 2 +- .../Home/GroupHomeViewController.m | 2 +- .../Members/GroupParticipantsViewController.m | 3 - .../Rooms/GroupRoomsViewController.m | 3 - .../TabDetail/GroupDetailsViewController.m | 3 - .../Contacts/ContactsTableViewController.h | 5 -- .../Contacts/ContactsTableViewController.m | 5 -- .../Details/ContactDetailsViewController.m | 3 - .../Favorites/FavouritesViewController.m | 2 - .../Files/HomeFilesSearchViewController.m | 3 - .../HomeMessagesSearchViewController.m | 3 - .../Rooms/DirectoryViewController.m | 2 +- .../UnifiedSearchViewController.m | 4 -- Riot/Modules/Home/HomeViewController.m | 5 +- .../Library/MediaAlbumContentViewController.m | 3 - .../MediaPicker/MediaPickerViewController.m | 3 - Riot/Modules/People/PeopleViewController.m | 2 - .../Attachements/AttachmentsViewController.m | 8 --- .../Detail/RoomMemberDetailsViewController.m | 2 +- .../Members/RoomParticipantsViewController.m | 3 - Riot/Modules/Room/RoomViewController.m | 2 +- .../Files/RoomFilesSearchViewController.m | 3 - .../RoomMessagesSearchViewController.m | 3 - .../Room/Search/RoomSearchViewController.m | 3 - .../Settings/RoomSettingsViewController.m | 3 - .../DirectoryServerPickerViewController.m | 3 - Riot/Modules/Rooms/RoomsViewController.m | 7 --- .../DeactivateAccountViewController.m | 3 - .../Language/LanguagePickerViewController.m | 8 --- .../CountryPickerViewController.m | 8 --- .../ManageSessionViewController.m | 3 - .../Security/SecurityViewController.m | 3 - .../Modules/Settings/SettingsViewController.m | 3 - .../StartChat/StartChatViewController.m | 2 - .../UserDevices/UsersDevicesViewController.m | 3 - 45 files changed, 137 insertions(+), 232 deletions(-) create mode 100644 Riot/Managers/Analytics/AnalyticsScreen.swift delete mode 100644 Riot/Managers/Analytics/Generated/GeneratedEvents.swift diff --git a/Podfile b/Podfile index f908e38841..9e9c5451c5 100644 --- a/Podfile +++ b/Podfile @@ -69,6 +69,7 @@ abstract_target 'RiotPods' do # PostHog for analytics pod 'PostHog', '~> 1.4.4' + pod 'AnalyticsEvents', :path => '../matrix-analytics-events/AnalyticsEvents.podspec' # Remove warnings from "bad" pods pod 'OLMKit', :inhibit_warnings => true diff --git a/Podfile.lock b/Podfile.lock index 21da4f99d7..f356c62a33 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -14,6 +14,7 @@ PODS: - AFNetworking/Serialization (4.0.1) - AFNetworking/UIKit (4.0.1): - AFNetworking/NSURLSession + - AnalyticsEvents (0.1.0) - BlueCryptor (1.0.32) - BlueECC (1.2.5) - BlueRSA (1.0.200) @@ -114,6 +115,7 @@ PODS: - ZXingObjC/All (3.6.5) DEPENDENCIES: + - AnalyticsEvents (from `../matrix-analytics-events/AnalyticsEvents.podspec`) - DGCollectionViewLeftAlignFlowLayout (~> 1.0.4) - DSWaveformImage (~> 6.1.1) - ffmpeg-kit-ios-audio (~> 4.5) @@ -182,8 +184,13 @@ SPEC REPOS: - zxcvbn-ios - ZXingObjC +EXTERNAL SOURCES: + AnalyticsEvents: + :path: "../matrix-analytics-events/AnalyticsEvents.podspec" + SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce + AnalyticsEvents: 5d210d99ddf18f3c81116e5c98f6d9f159598f80 BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24 BlueECC: 0d18e93347d3ec6d41416de21c1ffa4d4cd3c2cc BlueRSA: dfeef51db96bcc4edec654956c1581adbda4e6a3 @@ -224,6 +231,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 06d0fee10c99dee2531993f8cb2e54ec04f0752b +PODFILE CHECKSUM: 1a5c7e918ee799655f370ad9fae8cd457b8d1ca1 COCOAPODS: 1.11.2 diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index 6b0bbb7b47..358ce63e0f 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -15,6 +15,7 @@ // import PostHog +import AnalyticsEvents @objcMembers class Analytics: NSObject { @@ -112,18 +113,19 @@ import PostHog postHog?.flush() } - func log(event: String) { - postHog?.capture(event) + private func capture(event: DictionaryConvertible, named eventName: String) { + postHog?.capture(eventName, properties: event.dictionary) } - func trackScreen(_ screenName: String) { -// postHog?.capture("screen:\(screenName)") + func trackScreen(_ screen: AnalyticsScreen) { + let event = AnalyticsEventScreen(durationMs: nil, eventName: .screen, screenName: screen.screenName) + capture(event: event, named: event.eventName.rawValue) } func trackE2EEError(_ reason: DecryptionFailureReason, count: Int) { for _ in 0.. Date: Thu, 25 Nov 2021 18:28:32 +0000 Subject: [PATCH 016/109] Update MXAnalyticsDelegate --- Riot/Managers/Analytics/Analytics.swift | 29 ++++++++++--------- .../Common/Recents/RecentsViewController.m | 3 -- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index 358ce63e0f..493b270ac1 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -119,7 +119,8 @@ import AnalyticsEvents func trackScreen(_ screen: AnalyticsScreen) { let event = AnalyticsEventScreen(durationMs: nil, eventName: .screen, screenName: screen.screenName) - capture(event: event, named: event.eventName.rawValue) + // Screen capture differs compared to event capture. + postHog?.screen(event.screenName.rawValue, properties: event.dictionary) } func trackE2EEError(_ reason: DecryptionFailureReason, count: Int) { @@ -138,30 +139,30 @@ import AnalyticsEvents extension Analytics: MXAnalyticsDelegate { func trackDuration(_ seconds: TimeInterval, category: String, name: String) { } - func trackCallStarted(_ call: MXCall) { + func trackCallStarted(withVideo isVideo: Bool, numberOfParticipants: Int, incoming isIncoming: Bool) { let event = AnalyticsEventCallStarted(eventName: .callStarted, - isVideo: call.isVideoCall, - numParticipants: Int(call.room.summary.membersCount.joined), - placed: !call.isIncoming) + isVideo: isVideo, + numParticipants: numberOfParticipants, + placed: !isIncoming) capture(event: event, named: event.eventName.rawValue) } - func trackCallEnded(_ call: MXCall) { - let event = AnalyticsEventCallEnded(durationMs: Int(call.duration), + func trackCallEnded(withDuration duration: Int, video isVideo: Bool, numberOfParticipants: Int, incoming isIncoming: Bool) { + let event = AnalyticsEventCallEnded(durationMs: duration, eventName: .callEnded, - isVideo: call.isVideoCall, - numParticipants: Int(call.room.summary.membersCount.joined), - placed: !call.isIncoming) + isVideo: isVideo, + numParticipants: numberOfParticipants, + placed: !isIncoming) capture(event: event, named: event.eventName.rawValue) } - func trackCallError(_ call: MXCall, with reason: __MXCallHangupReason) { + func trackCallError(with reason: __MXCallHangupReason, video isVideo: Bool, numberOfParticipants: Int, incoming isIncoming: Bool) { let callEvent = AnalyticsEventCallError(eventName: .callError, - isVideo: call.isVideoCall, - numParticipants: Int(call.room.summary.membersCount.joined), - placed: !call.isIncoming) + isVideo: isVideo, + numParticipants: numberOfParticipants, + placed: !isIncoming) let event = AnalyticsEventError(context: nil, domain: .voip, eventName: .error, name: reason.errorName) diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index d738277493..a7a38937f2 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -256,9 +256,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - // Screen tracking - [Analytics.shared trackScreen:AnalyticsScreenHome]; - // Reset back user interactions self.userInteractionEnabled = YES; From 1ce338742952570eeb3a77854ff2d9095e5a9123 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 30 Nov 2021 12:54:29 +0000 Subject: [PATCH 017/109] Use custom generated Swift events. Add analytics PerformanceTimer event. --- .swiftlint.yml | 3 + Riot/Managers/Analytics/Analytics.swift | 52 ++++++------- Riot/Managers/Analytics/AnalyticsScreen.swift | 14 ++-- Riot/Managers/Analytics/EventExtensions.swift | 75 +++++++++---------- Riot/Modules/Application/LegacyAppDelegate.m | 6 +- 5 files changed, 71 insertions(+), 79 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 4d215eb98c..22cadcaac3 100755 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -56,6 +56,9 @@ function_body_length: warning: 100 error: 150 +nesting: + type_level: 2 + # naming rules can set warnings/errors for min_length and max_length # additionally they can set excluded names type_name: diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index 493b270ac1..d83501c56b 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -113,20 +113,19 @@ import AnalyticsEvents postHog?.flush() } - private func capture(event: DictionaryConvertible, named eventName: String) { - postHog?.capture(eventName, properties: event.dictionary) + private func capture(event: AnalyticsEventProtocol) { + postHog?.capture(event.eventName, properties: event.properties) } func trackScreen(_ screen: AnalyticsScreen) { - let event = AnalyticsEventScreen(durationMs: nil, eventName: .screen, screenName: screen.screenName) - // Screen capture differs compared to event capture. - postHog?.screen(event.screenName.rawValue, properties: event.dictionary) + let event = AnalyticsEvent.Screen(durationMs: nil, screenName: screen.screenName) + postHog?.screen(event.screenName.rawValue, properties: event.properties) } func trackE2EEError(_ reason: DecryptionFailureReason, count: Int) { for _ in 0.. profiler = MXSDKOptions.sharedInstance.profiler; - MXTaskProfile *launchTaskProfile = [profiler taskProfileWithName:kMXAnalyticsStartupLaunchScreen category:kMXAnalyticsStartupCategory]; + MXTaskProfile *launchTaskProfile = [profiler taskProfileWithName:MXTaskProfileNameStartupLaunchScreen category:MXTaskProfileCategoryStartup]; if (launchTaskProfile) { [profiler stopMeasuringTaskWithProfile:launchTaskProfile]; From 3a9d35a50b8264fc06747ebe786568f2e90d534f Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 2 Dec 2021 14:25:45 +0000 Subject: [PATCH 018/109] Add AnalyticsScreenTimer and track more screens. Update Analytics with new methods in MXAnalyticsDelegate. --- Riot/Managers/Analytics/Analytics.swift | 32 +++++-- Riot/Managers/Analytics/AnalyticsScreen.swift | 87 +++++++++++++++++-- .../Analytics/AnalyticsScreenTimer.swift | 82 +++++++++++++++++ Riot/Managers/Analytics/EventExtensions.swift | 21 +++++ Riot/Modules/Application/LegacyAppDelegate.m | 5 +- .../Common/Recents/RecentsViewController.h | 6 ++ .../Common/Recents/RecentsViewController.m | 3 + .../Communities/GroupsViewController.m | 10 ++- .../Home/GroupHomeViewController.m | 19 +++- .../Contacts/ContactsTableViewController.h | 6 ++ .../Contacts/ContactsTableViewController.m | 12 +++ .../EnterNewRoomDetailsViewController.swift | 8 ++ .../Favorites/FavouritesViewController.m | 2 + .../Files/HomeFilesSearchViewController.h | 7 ++ .../Files/HomeFilesSearchViewController.m | 12 +++ .../HomeMessagesSearchViewController.h | 7 ++ .../HomeMessagesSearchViewController.m | 12 +++ .../Rooms/DirectoryViewController.m | 15 +++- .../UnifiedSearchViewController.m | 4 + Riot/Modules/Home/HomeViewController.m | 5 +- .../People/InviteFriendsPresenter.swift | 2 + Riot/Modules/People/PeopleViewController.m | 2 + .../Room/Files/RoomFilesViewController.h | 7 ++ .../Room/Files/RoomFilesViewController.m | 8 ++ .../Detail/RoomMemberDetailsViewController.m | 19 +++- .../Members/RoomParticipantsViewController.h | 5 ++ .../Members/RoomParticipantsViewController.m | 8 ++ .../Room/RoomInfo/RoomInfoCoordinator.swift | 3 + .../RoomInfoListViewController.swift | 11 +++ Riot/Modules/Room/RoomViewController.m | 17 ++-- .../Room/Search/RoomSearchViewController.m | 12 +++ .../Settings/RoomSettingsViewController.h | 7 ++ .../Settings/RoomSettingsViewController.m | 8 ++ .../DirectoryServerPickerViewController.m | 17 ++++ Riot/Modules/Rooms/RoomsViewController.m | 7 ++ .../ShowDirectoryCoordinator.swift | 1 + .../ShowDirectoryViewController.swift | 9 ++ .../DeactivateAccountViewController.m | 20 +++++ .../Security/SecurityViewController.m | 16 ++++ .../Modules/Settings/SettingsViewController.m | 12 +++ .../SideMenu/SideMenuViewController.swift | 7 ++ .../StartChat/StartChatViewController.m | 2 + 42 files changed, 513 insertions(+), 42 deletions(-) create mode 100644 Riot/Managers/Analytics/AnalyticsScreenTimer.swift diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index d83501c56b..b2edfb6348 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -117,8 +117,8 @@ import AnalyticsEvents postHog?.capture(event.eventName, properties: event.properties) } - func trackScreen(_ screen: AnalyticsScreen) { - let event = AnalyticsEvent.Screen(durationMs: nil, screenName: screen.screenName) + func trackScreen(_ screen: AnalyticsScreen, duration milliseconds: Int?) { + let event = AnalyticsEvent.Screen(durationMs: milliseconds, screenName: screen.screenName) postHog?.screen(event.screenName.rawValue, properties: event.properties) } @@ -136,13 +136,14 @@ import AnalyticsEvents // MARK: - MXAnalyticsDelegate extension Analytics: MXAnalyticsDelegate { - func trackDuration(_ seconds: TimeInterval, category: MXTaskProfileCategory, name: MXTaskProfileName) { - if let analyticsName = name.analyticsName { - let event = AnalyticsEvent.PerformanceTimer(context: nil, name: analyticsName, timeMs: Int(seconds * 1000)) - capture(event: event) - } else { - MXLog.warning("[Analytics] Attempt to capture unknown profile task: \(category.rawValue) - \(name.rawValue)") + func trackDuration(_ milliseconds: Int, name: MXTaskProfileName, units: UInt) { + guard let analyticsName = name.analyticsName else { + MXLog.warning("[Analytics] Attempt to capture unknown profile task: \(name.rawValue)") + return } + + let event = AnalyticsEvent.PerformanceTimer(context: nil, itemCount: Int(units), name: analyticsName, timeMs: milliseconds) + capture(event: event) } func trackCallStarted(withVideo isVideo: Bool, numberOfParticipants: Int, incoming isIncoming: Bool) { @@ -165,4 +166,19 @@ extension Analytics: MXAnalyticsDelegate { func trackContactsAccessGranted(_ granted: Bool) { // Do we still want to track this? } + + func trackCreatedRoom(asDM isDM: Bool) { + let event = AnalyticsEvent.CreatedRoom(isDM: isDM) + capture(event: event) + } + + func trackJoinedRoom(asDM isDM: Bool, memberCount: UInt) { + guard let roomSize = AnalyticsEvent.JoinedRoom.RoomSize(memberCount: memberCount) else { + MXLog.warning("[Analytics] Attempt to capture joined room with invalid member count: \(memberCount)") + return + } + + let event = AnalyticsEvent.JoinedRoom(isDM: isDM, roomSize: roomSize) + capture(event: event) + } } diff --git a/Riot/Managers/Analytics/AnalyticsScreen.swift b/Riot/Managers/Analytics/AnalyticsScreen.swift index 98ed0c1ef5..fef1780f98 100644 --- a/Riot/Managers/Analytics/AnalyticsScreen.swift +++ b/Riot/Managers/Analytics/AnalyticsScreen.swift @@ -18,27 +18,96 @@ import Foundation import AnalyticsEvents @objc enum AnalyticsScreen: Int { - case group + case sidebar case home - case myGroups + case favourites + case people + case rooms + case searchRooms + case searchMessages + case searchPeople + case searchFiles case room - case roomDirectory + case roomDetails + case roomMembers case user + case roomSearch + case roomUploads + case roomSettings + case roomNotifications + case roomDirectory + case switchDirectory + case startChat + case createRoom + case settings + case settingsSecurity + case settingsDefaultNotifications + case settingsMentionsAndKeywords + case deactivateAccount + case group + case myGroups + case inviteFriends var screenName: AnalyticsEvent.Screen.ScreenName { switch self { - case .group: - return .Group + case .sidebar: + return .MobileSidebar case .home: return .Home - case .myGroups: - return .MyGroups + case .favourites: + return .MobileFavourites + case .people: + return .MobilePeople + case .rooms: + return .MobileRooms + case .searchRooms: + return .MobileSearchRooms + case .searchMessages: + return .MobileSearchMessages + case .searchPeople: + return .MobileSearchPeople + case .searchFiles: + return .MobileSearchFiles case .room: return .Room - case .roomDirectory: - return .RoomDirectory + case .roomDetails: + return .RoomDetails + case .roomMembers: + return .RoomMembers case .user: return .User + case .roomSearch: + return .RoomSearch + case .roomUploads: + return .RoomUploads + case .roomSettings: + return .RoomSettings + case .roomNotifications: + return .RoomNotifications + case .roomDirectory: + return .RoomDirectory + case .switchDirectory: + return .MobileSwitchDirectory + case .startChat: + return .StartChat + case .createRoom: + return .CreateRoom + case .settings: + return .Settings + case .settingsSecurity: + return .SettingsSecurity + case .settingsDefaultNotifications: + return .SettingsDefaultNotifications + case .settingsMentionsAndKeywords: + return .SettingsMentionsAndKeywords + case .deactivateAccount: + return .DeactivateAccount + case .group: + return .Group + case .myGroups: + return .MyGroups + case .inviteFriends: + return .MobileInviteFriends } } } diff --git a/Riot/Managers/Analytics/AnalyticsScreenTimer.swift b/Riot/Managers/Analytics/AnalyticsScreenTimer.swift new file mode 100644 index 0000000000..5c704f8d82 --- /dev/null +++ b/Riot/Managers/Analytics/AnalyticsScreenTimer.swift @@ -0,0 +1,82 @@ +// +// Copyright 2021 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 + +/// An object to record how long a screen has been presented for and +/// report the screen's display to the `Analytics` object. +@objcMembers class AnalyticsScreenTimer: NSObject { + + // MARK: - Properties + + /// The screen being tracked. + private let screen: AnalyticsScreen + + /// The date that the screen was presented to the user. + private var startDate: Date? + /// Whether the app was backgrounded whilst the screen was being presented. + private var didPause = false + + /// The duration in milliseconds that the screen has been shown for. The value will + /// be reported as `nil` if the timer isn't running, or if the app was backgrounded + /// during the screen's display. + private var duration: Int? { + guard let startDate = startDate else { + MXLog.warning("[AnalyticsScreenTimer] Duration requested on a stopped timer!") + return nil + } + + // Consider the duration invalid if the app has been backgrounded + guard !didPause else { return nil } + + let timeInterval = Date().timeIntervalSince(startDate) + return Int(timeInterval * 1000) + } + + // MARK: - Setup + + /// Create a new screen timer for the specified screen. + /// - Parameter screen: The screen that should be timed. + init(screen: AnalyticsScreen) { + self.screen = screen + + super.init() + + NotificationCenter.default.addObserver(self, selector: #selector(pause), name: UIApplication.willResignActiveNotification, object: nil) + } + + // MARK: - Public + + /// Start the timer. + func start() { + startDate = Date() + } + + /// Stop the timer and report the screen to `Analytics`. + func stop() { + guard let duration = duration else { return } + + Analytics.shared.trackScreen(screen, duration: duration) + self.startDate = nil + } + + // MARK: - Private + + /// Record that the timer has been interrupted by the app moving to the background. + @objc private func pause() { + didPause = true + } +} diff --git a/Riot/Managers/Analytics/EventExtensions.swift b/Riot/Managers/Analytics/EventExtensions.swift index 0c4c169f71..1d879c41ca 100644 --- a/Riot/Managers/Analytics/EventExtensions.swift +++ b/Riot/Managers/Analytics/EventExtensions.swift @@ -36,6 +36,8 @@ extension MXTaskProfileName { return .InitialSyncRequest case .initialSyncParsing: return .InitialSyncParsing + case .notificationsOpenEvent: + return .NotificationsOpenEvent default: return nil } @@ -79,3 +81,22 @@ extension DecryptionFailureReason { } } } + +extension AnalyticsEvent.JoinedRoom.RoomSize { + init?(memberCount: UInt) { + switch memberCount { + case 2: + self = .Two + case 3...10: + self = .ThreeToTen + case 11...100: + self = .ElevenToOneHundred + case 101...1000: + self = .OneHundredAndOneToAThousand + case 1001...: + self = .MoreThanAThousand + default: + return nil + } + } +} diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 908ecd7526..c917bd373d 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -2394,8 +2394,7 @@ - (void)showLaunchAnimation launchAnimationContainerView = launchLoadingView; - [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:MXTaskProfileNameStartupLaunchScreen - category:MXTaskProfileCategoryStartup]; + [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:MXTaskProfileNameStartupLaunchScreen]; } } @@ -2404,7 +2403,7 @@ - (void)hideLaunchAnimation if (launchAnimationContainerView) { id profiler = MXSDKOptions.sharedInstance.profiler; - MXTaskProfile *launchTaskProfile = [profiler taskProfileWithName:MXTaskProfileNameStartupLaunchScreen category:MXTaskProfileCategoryStartup]; + MXTaskProfile *launchTaskProfile = [profiler taskProfileWithName:MXTaskProfileNameStartupLaunchScreen]; if (launchTaskProfile) { [profiler stopMeasuringTaskWithProfile:launchTaskProfile]; diff --git a/Riot/Modules/Common/Recents/RecentsViewController.h b/Riot/Modules/Common/Recents/RecentsViewController.h index 8afc8b26ee..9e9e357a76 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.h +++ b/Riot/Modules/Common/Recents/RecentsViewController.h @@ -18,6 +18,7 @@ #import "MatrixKit.h" @class RootTabEmptyView; +@class AnalyticsScreenTimer; /** Notification to be posted when recents data is ready. Notification object will be the RecentsViewController instance. @@ -90,6 +91,11 @@ FOUNDATION_EXPORT NSString *const RecentsViewControllerDataReadyNotification; */ @property (nonatomic, weak) RootTabEmptyView *emptyView; +/** + The screen timer used for analytics if they've been enabled. The default value is nil. + */ +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + /** Return the sticky header for the specified section of the table view diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index a7a38937f2..0bcceb09aa 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -323,11 +323,14 @@ - (void)viewDidAppear:(BOOL)animated // the selected room (if any) is highlighted. [self refreshCurrentSelectedCell:YES]; } + + [self.screenTimer start]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; + [self.screenTimer stop]; } - (void)viewDidLayoutSubviews diff --git a/Riot/Modules/Communities/GroupsViewController.m b/Riot/Modules/Communities/GroupsViewController.m index c72e95dab9..306a4e55c3 100644 --- a/Riot/Modules/Communities/GroupsViewController.m +++ b/Riot/Modules/Communities/GroupsViewController.m @@ -42,6 +42,8 @@ @interface GroupsViewController () __weak id kThemeServiceDidChangeThemeNotificationObserver; } +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation GroupsViewController @@ -74,6 +76,8 @@ - (void)finalizeInit // Set itself as delegate by default. self.delegate = self; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenMyGroups]; } - (void)viewDidLoad @@ -203,9 +207,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - // Screen tracking - [Analytics.shared trackScreen:AnalyticsScreenMyGroups]; - // Deselect the current selected row, it will be restored on viewDidAppear (if any) NSIndexPath *indexPath = [self.groupsTableView indexPathForSelectedRow]; if (indexPath) @@ -258,11 +259,14 @@ - (void)viewDidAppear:(BOOL)animated // the selected group (if any) is highlighted. [self refreshCurrentSelectedCell:YES]; } + + [self.screenTimer start]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; + [self.screenTimer stop]; } #pragma mark - Override MXKGroupListViewController diff --git a/Riot/Modules/Communities/Home/GroupHomeViewController.m b/Riot/Modules/Communities/Home/GroupHomeViewController.m index b12cd5d706..6e5d5d3b5b 100644 --- a/Riot/Modules/Communities/Home/GroupHomeViewController.m +++ b/Riot/Modules/Communities/Home/GroupHomeViewController.m @@ -48,6 +48,8 @@ @interface GroupHomeViewController () @property (nonatomic, readonly) DTHTMLAttributedStringBuilderWillFlushCallback longDescriptionSanitizationCallback; +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation GroupHomeViewController @@ -95,6 +97,8 @@ - (void)finalizeInit MXStrongifyAndReturnIfNil(self); [element sanitizeWith:allowedHTMLTags bodyFont:self->_groupLongDescription.font imageHandler:[self groupLongDescriptionImageHandler]]; }; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenGroup]; } - (void)viewDidLoad @@ -205,9 +209,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - // Screen tracking - [Analytics.shared trackScreen:AnalyticsScreenGroup]; - // Release the potential pushed view controller [self releasePushedViewController]; @@ -259,6 +260,18 @@ - (void)viewWillDisappear:(BOOL)animated [self cancelRegistrationOnGroupChangeNotifications]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; diff --git a/Riot/Modules/Contacts/ContactsTableViewController.h b/Riot/Modules/Contacts/ContactsTableViewController.h index d35c71dbea..97c02515ba 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.h +++ b/Riot/Modules/Contacts/ContactsTableViewController.h @@ -19,6 +19,7 @@ #import "ContactTableViewCell.h" @class ContactsTableViewController; +@class AnalyticsScreenTimer; /** `ContactsTableViewController` delegate. @@ -119,5 +120,10 @@ */ @property (nonatomic, weak) id contactsTableViewControllerDelegate; +/** + The screen timer used for analytics if they've been enabled. The default value is nil. + */ +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index b564d226c6..bf83b6372c 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -177,6 +177,12 @@ - (void)viewWillAppear:(BOOL)animated [self updateFooterViewVisibility]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; @@ -201,6 +207,12 @@ - (void)viewWillDisappear:(BOOL)animated } } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + #pragma mark - /** diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift index d89e606356..63fe651974 100644 --- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift +++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift @@ -53,6 +53,7 @@ final class EnterNewRoomDetailsViewController: UIViewController { item.isEnabled = false return item }() + private var screenTimer = AnalyticsScreenTimer(screen: .createRoom) private enum RowType { case `default` @@ -215,10 +216,17 @@ final class EnterNewRoomDetailsViewController: UIViewController { self.keyboardAvoider?.startAvoiding() } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + screenTimer.start() + } + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) self.keyboardAvoider?.stopAvoiding() + + screenTimer.stop() } override var preferredStatusBarStyle: UIStatusBarStyle { diff --git a/Riot/Modules/Favorites/FavouritesViewController.m b/Riot/Modules/Favorites/FavouritesViewController.m index f7148a6e68..c3c10933be 100644 --- a/Riot/Modules/Favorites/FavouritesViewController.m +++ b/Riot/Modules/Favorites/FavouritesViewController.m @@ -40,6 +40,8 @@ - (void)finalizeInit [super finalizeInit]; self.enableDragging = YES; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenFavourites]; } - (void)viewDidLoad diff --git a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.h b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.h index bf98268585..71db0b6bcc 100644 --- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.h +++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.h @@ -17,6 +17,8 @@ #import "MatrixKit.h" +@class AnalyticsScreenTimer; + /** `HomeFilesSearchViewController` displays the files search in user's rooms under a `HomeViewController` segment. */ @@ -27,4 +29,9 @@ */ @property (nonatomic, readonly) MXEvent *selectedEvent; +/** + The screen timer used for analytics if they've been enabled. The default value is nil. + */ +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end diff --git a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m index ee756902c8..67ad65ab8b 100644 --- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m @@ -121,6 +121,18 @@ - (void)viewWillDisappear:(BOOL)animated [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionNewRoomNotification object:nil]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + #pragma mark - - (void)refreshSearchResult:(NSNotification *)notif diff --git a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.h b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.h index 8a35537713..9fdb9880a7 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.h +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.h @@ -17,6 +17,8 @@ #import "MatrixKit.h" +@class AnalyticsScreenTimer; + /** `HomeMessagesSearchViewController` displays messages search in user's rooms under a `HomeViewController` segment. */ @@ -27,4 +29,9 @@ */ @property (nonatomic, readonly) MXEvent *selectedEvent; +/** + The screen timer used for analytics if they've been enabled. The default value is nil. + */ +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end diff --git a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m index acf36b0539..9fc1d5ac81 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m @@ -128,6 +128,18 @@ - (void)viewWillDisappear:(BOOL)animated [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionNewRoomNotification object:nil]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + #pragma mark - - (void)refreshSearchResult:(NSNotification *)notif diff --git a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m index ec511bb3ca..66d724005c 100644 --- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m +++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m @@ -35,6 +35,8 @@ @interface DirectoryViewController () id kThemeServiceDidChangeThemeNotificationObserver; } +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation DirectoryViewController @@ -46,6 +48,8 @@ - (void)finalizeInit // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenRoomDirectory]; } - (void)viewDidLoad @@ -106,9 +110,6 @@ - (void)destroy - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [Analytics.shared trackScreen:AnalyticsScreenRoomDirectory]; // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -135,6 +136,8 @@ - (void)viewDidAppear:(BOOL)animated // the selected room (if any) is highlighted. [self refreshCurrentSelectedCell:YES]; } + + [self.screenTimer start]; } - (void)viewWillDisappear:(BOOL)animated @@ -148,6 +151,12 @@ - (void)viewWillDisappear:(BOOL)animated [super viewWillDisappear:animated]; } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + - (void)displayWitDataSource:(PublicRoomsDirectoryDataSource *)dataSource2 { // Let the data source provide cells diff --git a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m index 7d240e48c7..8c19563303 100644 --- a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m +++ b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m @@ -79,11 +79,13 @@ - (void)viewDidLoad [titles addObject:[VectorL10n searchRooms]]; recentsViewController = [RecentsViewController recentListViewController]; + recentsViewController.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSearchRooms]; recentsViewController.enableSearchBar = NO; [viewControllers addObject:recentsViewController]; [titles addObject:[VectorL10n searchMessages]]; messagesSearchViewController = [HomeMessagesSearchViewController searchViewController]; + messagesSearchViewController.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSearchMessages]; [viewControllers addObject:messagesSearchViewController]; // Add search People tab @@ -91,11 +93,13 @@ - (void)viewDidLoad peopleSearchViewController = [ContactsTableViewController contactsTableViewController]; peopleSearchViewController.contactsTableViewControllerDelegate = self; peopleSearchViewController.disableFindYourContactsFooter = YES; + peopleSearchViewController.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSearchPeople]; [viewControllers addObject:peopleSearchViewController]; // add Files tab [titles addObject:[VectorL10n searchFiles]]; filesSearchViewController = [HomeFilesSearchViewController searchViewController]; + filesSearchViewController.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSearchFiles]; [viewControllers addObject:filesSearchViewController]; [self initWithTitles:titles viewControllers:viewControllers defaultSelected:0]; diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index 89dd5d3c0e..2c2f702d76 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -68,6 +68,8 @@ - (void)finalizeInit selectedSection = -1; selectedRoomId = nil; selectedCollectionViewContentOffset = -1; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenHome]; } - (void)viewDidLoad @@ -100,9 +102,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - // Screen tracking - [Analytics.shared trackScreen:AnalyticsScreenHome]; - [AppDelegate theDelegate].masterTabBarController.navigationItem.title = [VectorL10n titleHome]; [ThemeService.shared.theme applyStyleOnNavigationBar:[AppDelegate theDelegate].masterTabBarController.navigationController.navigationBar]; diff --git a/Riot/Modules/People/InviteFriendsPresenter.swift b/Riot/Modules/People/InviteFriendsPresenter.swift index 325b7cf717..ad89957466 100644 --- a/Riot/Modules/People/InviteFriendsPresenter.swift +++ b/Riot/Modules/People/InviteFriendsPresenter.swift @@ -73,5 +73,7 @@ final class InviteFriendsPresenter: NSObject { } self.presentingViewController?.present(viewController, animated: animated, completion: nil) + + Analytics.shared.trackScreen(.inviteFriends, duration: nil) } } diff --git a/Riot/Modules/People/PeopleViewController.m b/Riot/Modules/People/PeopleViewController.m index 9d93622c2e..ace78de54c 100644 --- a/Riot/Modules/People/PeopleViewController.m +++ b/Riot/Modules/People/PeopleViewController.m @@ -50,6 +50,8 @@ - (void)finalizeInit [super finalizeInit]; directRoomsSectionNumber = 0; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenPeople]; } - (void)viewDidLoad diff --git a/Riot/Modules/Room/Files/RoomFilesViewController.h b/Riot/Modules/Room/Files/RoomFilesViewController.h index e8f3b7f297..92b7993446 100644 --- a/Riot/Modules/Room/Files/RoomFilesViewController.h +++ b/Riot/Modules/Room/Files/RoomFilesViewController.h @@ -16,6 +16,8 @@ limitations under the License. #import "MatrixKit.h" +@class AnalyticsScreenTimer; + /** This view controller displays the attachments of a room. Only one matrix session is handled by this view controller. */ @@ -23,4 +25,9 @@ limitations under the License. @property (nonatomic) BOOL showCancelBarButtonItem; +/** + The screen timer used for analytics if they've been enabled. The default value is nil. + */ +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end diff --git a/Riot/Modules/Room/Files/RoomFilesViewController.m b/Riot/Modules/Room/Files/RoomFilesViewController.m index 1e8e007c1d..0773118354 100644 --- a/Riot/Modules/Room/Files/RoomFilesViewController.m +++ b/Riot/Modules/Room/Files/RoomFilesViewController.m @@ -110,6 +110,14 @@ - (void)viewDidAppear:(BOOL)animated [UIView setAnimationsEnabled:NO]; [self roomInputToolbarView:self.inputToolbarView heightDidChanged:0 completion:nil]; [UIView setAnimationsEnabled:YES]; + + [self.screenTimer start]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; } - (void)userInterfaceThemeDidChange diff --git a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m index 68112586e3..687587dfe8 100644 --- a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m +++ b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m @@ -104,6 +104,8 @@ List of the direct chats (room ids) with this member. @property(nonatomic, strong) UserVerificationCoordinatorBridgePresenter *userVerificationCoordinatorBridgePresenter; +@property(nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation RoomMemberDetailsViewController @@ -139,6 +141,8 @@ - (void)finalizeInit // Keep visible the status bar by default. isStatusBarHidden = NO; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenUser]; } - (void)viewDidLoad @@ -239,9 +243,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - // Screen tracking - [Analytics.shared trackScreen:AnalyticsScreenUser]; - [self userInterfaceThemeDidChange]; // Hide the bottom border of the navigation bar to display the expander header @@ -264,6 +265,18 @@ - (void)viewWillDisappear:(BOOL)animated self.bottomImageView.hidden = YES; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id )coordinator { [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; diff --git a/Riot/Modules/Room/Members/RoomParticipantsViewController.h b/Riot/Modules/Room/Members/RoomParticipantsViewController.h index 757bb2fca3..e3bb565432 100644 --- a/Riot/Modules/Room/Members/RoomParticipantsViewController.h +++ b/Riot/Modules/Room/Members/RoomParticipantsViewController.h @@ -92,6 +92,11 @@ */ @property (nonatomic, weak) id delegate; +/** + The screen timer used for analytics if they've been enabled. The default value is nil. + */ +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + /** Returns the `UINib` object initialized for a `RoomParticipantsViewController`. diff --git a/Riot/Modules/Room/Members/RoomParticipantsViewController.m b/Riot/Modules/Room/Members/RoomParticipantsViewController.m index 6802f7a3b0..5df03b91e1 100644 --- a/Riot/Modules/Room/Members/RoomParticipantsViewController.m +++ b/Riot/Modules/Room/Members/RoomParticipantsViewController.m @@ -265,6 +265,8 @@ - (void)viewDidAppear:(BOOL)animated [contactsPickerViewController destroy]; contactsPickerViewController = nil; } + + [self.screenTimer start]; } - (void)viewWillDisappear:(BOOL)animated @@ -281,6 +283,12 @@ - (void)viewWillDisappear:(BOOL)animated [self searchBarCancelButtonClicked:_searchBarView]; } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + - (void)withdrawViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion { // Check whether the current view controller is displayed inside a segmented view controller in order to withdraw the right item diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift index ccfe76a7e8..4eaf2328d1 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift @@ -39,9 +39,11 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { participants.enableMention = true participants.mxRoom = self.room participants.delegate = self + participants.screenTimer = AnalyticsScreenTimer(screen: .roomMembers) let files = RoomFilesViewController() files.finalizeInit() + files.screenTimer = AnalyticsScreenTimer(screen: .roomUploads) MXKRoomDataSource.load(withRoomId: self.room.roomId, andMatrixSession: self.session) { (dataSource) in guard let dataSource = dataSource as? MXKRoomDataSource else { return } dataSource.filterMessagesWithURL = true @@ -52,6 +54,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { let settings = RoomSettingsViewController() settings.finalizeInit() + settings.screenTimer = AnalyticsScreenTimer(screen: .roomSettings) settings.initWith(self.session, andRoomId: self.room.roomId) if self.room.isDirect { diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift index 3590b5526a..78b5af425b 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift @@ -40,6 +40,7 @@ final class RoomInfoListViewController: UIViewController { private var errorPresenter: MXKErrorPresentation! private var activityPresenter: ActivityIndicatorPresenter! private var isRoomDirect: Bool = false + private var screenTimer = AnalyticsScreenTimer(screen: .roomDetails) private lazy var closeButton: CloseButton = { let button = CloseButton() @@ -128,12 +129,22 @@ final class RoomInfoListViewController: UIViewController { return self.theme.statusBarStyle } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + screenTimer.start() + } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() mainTableView.vc_relayoutHeaderView() } + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + screenTimer.stop() + } + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { coordinator.animate(alongsideTransition: {_ in self.basicInfoView.updateTrimmingOnTopic() diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 3792006c4d..ac57d3a781 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -134,8 +134,6 @@ NSNotificationName const RoomCallTileTappedNotification = @"RoomCallTileTappedNotification"; NSNotificationName const RoomGroupCallTileTappedNotification = @"RoomGroupCallTileTappedNotification"; -NSString * const RoomAnalyticsNotificationsCategory = @"notifications"; -NSString * const RoomAnalyticsNotificationsTimeToDisplayContent = @"timelineDisplay"; const NSTimeInterval kResizeComposerAnimationDuration = .05; @interface RoomViewController () cellData))onComplete; { diff --git a/Riot/Modules/Rooms/RoomsViewController.m b/Riot/Modules/Rooms/RoomsViewController.m index 63145b1c49..183db00466 100644 --- a/Riot/Modules/Rooms/RoomsViewController.m +++ b/Riot/Modules/Rooms/RoomsViewController.m @@ -36,6 +36,13 @@ + (instancetype)instantiate return viewController; } +- (void)finalizeInit +{ + [super finalizeInit]; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenRooms]; +} + - (void)viewDidLoad { [super viewDidLoad]; diff --git a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryCoordinator.swift b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryCoordinator.swift index 75a8365a7f..6ab5e25179 100644 --- a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryCoordinator.swift +++ b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryCoordinator.swift @@ -63,6 +63,7 @@ final class ShowDirectoryCoordinator: ShowDirectoryCoordinatorType { private func createDirectoryServerPickerViewController() -> DirectoryServerPickerViewController { let controller = DirectoryServerPickerViewController() + controller.finalizeInit() let dataSource: MXKDirectoryServersDataSource = MXKDirectoryServersDataSource(matrixSession: session) dataSource.finalizeInitialization() dataSource.roomDirectoryServers = BuildSettings.publicRoomsDirectoryServers diff --git a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewController.swift b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewController.swift index 1e2cf5daf7..ce22a34406 100644 --- a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewController.swift +++ b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewController.swift @@ -68,6 +68,8 @@ final class ShowDirectoryViewController: UIViewController { }() private var sections: [ShowDirectorySection] = [] + + private let screenTimer = AnalyticsScreenTimer(screen: .roomDirectory) // MARK: - Setup @@ -104,10 +106,17 @@ final class ShowDirectoryViewController: UIViewController { self.keyboardAvoider?.startAvoiding() } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + screenTimer.start() + } + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) self.keyboardAvoider?.stopAvoiding() + + screenTimer.stop() } override var preferredStatusBarStyle: UIStatusBarStyle { diff --git a/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m b/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m index ea62975294..0efbe7bdcc 100644 --- a/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m +++ b/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m @@ -47,6 +47,8 @@ @interface DeactivateAccountViewController () @property (weak, nonatomic) id themeDidChangeNotificationObserver; +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end #pragma mark - Implementation @@ -62,6 +64,12 @@ + (DeactivateAccountViewController*)instantiateWithMatrixSession:(MXSession*)mat return viewController; } +- (void)finalizeInit +{ + [super finalizeInit]; + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenDeactivateAccount]; +} + - (void)destroy { id notificationObserver = self.themeDidChangeNotificationObserver; @@ -97,6 +105,12 @@ - (void)viewWillAppear:(BOOL)animated [self userInterfaceThemeDidChange]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; @@ -104,6 +118,12 @@ - (void)viewDidLayoutSubviews [self.deactivateAcccountButton.layer setCornerRadius:kButtonCornerRadius]; } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + - (UIStatusBarStyle)preferredStatusBarStyle { return ThemeService.shared.theme.statusBarStyle; diff --git a/Riot/Modules/Settings/Security/SecurityViewController.m b/Riot/Modules/Settings/Security/SecurityViewController.m index 3334590830..99b7f7c933 100644 --- a/Riot/Modules/Settings/Security/SecurityViewController.m +++ b/Riot/Modules/Settings/Security/SecurityViewController.m @@ -119,6 +119,8 @@ @interface SecurityViewController () < @property (nonatomic, strong) SetPinCoordinatorBridgePresenter *setPinCoordinatorBridgePresenter; @property (nonatomic, strong) CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter; +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation SecurityViewController @@ -142,6 +144,8 @@ - (void)finalizeInit // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSettingsSecurity]; } - (void)viewDidLoad @@ -265,6 +269,12 @@ - (void)viewWillAppear:(BOOL)animated [self loadCrossSigning]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; @@ -276,6 +286,12 @@ - (void)viewWillDisappear:(BOOL)animated } } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + #pragma mark - Internal methods - (void)updateSections diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index b5d4c3b058..9aeaa77b72 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -283,6 +283,8 @@ @interface SettingsViewController () Date: Thu, 2 Dec 2021 16:56:04 +0000 Subject: [PATCH 019/109] Abstract PostHog out of the Analytics client. --- Riot/Managers/Analytics/Analytics.swift | 32 +++++----- .../Analytics/AnalyticsClientProtocol.swift | 44 +++++++++++++ .../Analytics/PostHogAnalyticsClient.swift | 63 +++++++++++++++++++ 3 files changed, 121 insertions(+), 18 deletions(-) create mode 100644 Riot/Managers/Analytics/AnalyticsClientProtocol.swift create mode 100644 Riot/Managers/Analytics/PostHogAnalyticsClient.swift diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index b2edfb6348..0222e2d7fe 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -23,9 +23,9 @@ import AnalyticsEvents static let shared = Analytics() - private var postHog: PHGPostHog? + private var client = PostHogAnalyticsClient() - var isRunning: Bool { postHog?.enabled ?? false } + var isRunning: Bool { client.isRunning } var shouldShowAnalyticsPrompt: Bool { // Show an analytics prompt when the user hasn't seen the PostHog prompt before @@ -34,8 +34,7 @@ import AnalyticsEvents } var promptShouldDisplayUpgradeMessage: Bool { - // Show an analytics prompt when the user hasn't seen the PostHog prompt before - // so long as they haven't previously declined the Matomo analytics prompt. + // Only show an upgrade prompt if the user previously accepted Matomo analytics. RiotSettings.shared.hasAcceptedMatomoAnalytics } @@ -72,11 +71,11 @@ import AnalyticsEvents func startIfEnabled() { guard RiotSettings.shared.enableAnalytics, !isRunning else { return } - // Ensures that analytics are configured BuildSettings - guard let configuration = PHGPostHogConfiguration.standard else { return } + client.start() + + // Sanity check in case something went wrong. + guard client.isRunning else { return } - postHog = PHGPostHog(configuration: configuration) - postHog?.enable() MXLog.debug("[Analytics] Started.") // Catch and log crashes @@ -90,7 +89,7 @@ import AnalyticsEvents return } - postHog?.identify(id) + client.identify(id: id) MXLog.debug("[Analytics] Identified.") RiotSettings.shared.isIdentifiedForAnalytics = true } @@ -98,28 +97,25 @@ import AnalyticsEvents func reset() { guard isRunning else { return } - postHog?.disable() - MXLog.debug("[Analytics] Stopped.") - - postHog?.reset() + client.reset() + MXLog.debug("[Analytics] Stopped and reset.") RiotSettings.shared.isIdentifiedForAnalytics = false - postHog = nil - + // Stop collecting crash logs MXLogger.logCrashes(false) } func forceUpload() { - postHog?.flush() + client.flush() } private func capture(event: AnalyticsEventProtocol) { - postHog?.capture(event.eventName, properties: event.properties) + client.capture(event) } func trackScreen(_ screen: AnalyticsScreen, duration milliseconds: Int?) { let event = AnalyticsEvent.Screen(durationMs: milliseconds, screenName: screen.screenName) - postHog?.screen(event.screenName.rawValue, properties: event.properties) + client.screen(event) } func trackE2EEError(_ reason: DecryptionFailureReason, count: Int) { diff --git a/Riot/Managers/Analytics/AnalyticsClientProtocol.swift b/Riot/Managers/Analytics/AnalyticsClientProtocol.swift new file mode 100644 index 0000000000..7ee0ae4298 --- /dev/null +++ b/Riot/Managers/Analytics/AnalyticsClientProtocol.swift @@ -0,0 +1,44 @@ +// +// Copyright 2021 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 AnalyticsEvents + +/// A protocol representing an analytics client. +protocol AnalyticsClientProtocol { + /// Whether the analytics client is currently reporting data or ignoring it. + var isRunning: Bool { get } + + /// Starts the analytics client reporting data. + func start() + + /// Associate the client with an ID. This is persisted until `reset` is called. + /// - Parameter id: The ID to associate with the user. + func identify(id: String) + + /// Stop the analytics client reporting data and reset all stored properties and events. + func reset() + + /// Send any queued events immediately. + func flush() + + /// Capture the supplied analytics event. + /// - Parameter event: The event to capture. + func capture(_ event: AnalyticsEventProtocol) + + /// Capture the supplied analytics screen event. + /// - Parameter event: The screen event to capture. + func screen(_ event: AnalyticsScreenProtocol) +} diff --git a/Riot/Managers/Analytics/PostHogAnalyticsClient.swift b/Riot/Managers/Analytics/PostHogAnalyticsClient.swift new file mode 100644 index 0000000000..ae9ee09af6 --- /dev/null +++ b/Riot/Managers/Analytics/PostHogAnalyticsClient.swift @@ -0,0 +1,63 @@ +// +// Copyright 2021 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 PostHog +import AnalyticsEvents + +/// An analytics client that reports events to a PostHog server. +class PostHogAnalyticsClient: AnalyticsClientProtocol { + /// The PHGPostHog object used to report events. + private var postHog: PHGPostHog? + + var isRunning: Bool { postHog?.enabled ?? false } + + func start() { + // Only start if analytics have been configured in BuildSettings + guard let configuration = PHGPostHogConfiguration.standard else { return } + + if postHog == nil { + postHog = PHGPostHog(configuration: configuration) + } + + postHog?.enable() + } + + func identify(id: String) { + postHog?.identify(id) + } + + func reset() { + postHog?.disable() + postHog?.reset() + + // As of PostHog 1.4.4, setting the client to nil here doesn't release + // it. Keep it around to avoid having multiple instances if the user re-enables + } + + func flush() { + postHog?.flush() + } + + func capture(_ event: AnalyticsEventProtocol) { + postHog?.capture(event.eventName, properties: event.properties) + } + + func screen(_ event: AnalyticsScreenProtocol) { + postHog?.screen(event.screenName.rawValue, properties: event.properties) + } + + +} From 8eab27ab8ea03829d3eee7ee687f463702ebe254 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 6 Dec 2021 12:52:33 +0000 Subject: [PATCH 020/109] Add AnalyticsPrompt to SwiftUI target and replace old UIAlertController. --- .../AnalyticsTick.pdf | Bin 0 -> 2320 bytes .../AnalyticsCheckmark.imageset/Contents.json | 15 ++ .../AnalyticsLogo.imageset/AnalyticsLogo.pdf | Bin 0 -> 19190 bytes .../AnalyticsLogo.imageset/Contents.json | 15 ++ .../Authentication/Analytics/Contents.json | 6 + Riot/Assets/en.lproj/Vector.strings | 30 +++- Riot/Generated/Images.swift | 2 + Riot/Generated/Strings.swift | 94 +++++++++++- Riot/Modules/TabBar/MasterTabBarController.h | 1 + Riot/Modules/TabBar/MasterTabBarController.m | 77 ++++------ Riot/Modules/TabBar/TabBarCoordinator.swift | 14 ++ .../AnalyticsPromptModels.swift | 98 ++++++++++++ .../AnalyticsPromptViewModel.swift | 76 ++++++++++ .../AnalyticsPromptCoordinator.swift | 94 ++++++++++++ .../MockAnalyticsPromptScreenState.swift | 54 +++++++ .../Test/UI/AnalyticsPromptUITests.swift | 65 ++++++++ .../View/AnalyticsPrompt.swift | 139 ++++++++++++++++++ .../View/AnalyticsPromptTermsText.swift | 42 ++++++ .../Modules/Common/Mock/MockAppScreens.swift | 1 + .../Util/PrimaryActionButtonStyle.swift | 30 +++- .../Room/PollEditForm/View/PollEditForm.swift | 2 +- .../TemplateUserProfileCoordinator.swift | 2 - .../TemplateRoomChatCoordinator.swift | 2 - .../TemplateRoomListCoordinator.swift | 2 - 24 files changed, 790 insertions(+), 71 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsCheckmark.imageset/AnalyticsTick.pdf create mode 100644 Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsCheckmark.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsLogo.imageset/AnalyticsLogo.pdf create mode 100644 Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsLogo.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Authentication/Analytics/Contents.json create mode 100644 RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift create mode 100644 RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift create mode 100644 RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift create mode 100644 RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift create mode 100644 RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift create mode 100644 RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift create mode 100644 RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptTermsText.swift diff --git a/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsCheckmark.imageset/AnalyticsTick.pdf b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsCheckmark.imageset/AnalyticsTick.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9696208e344f0435832daf9886df15bdaa85df61 GIT binary patch literal 2320 zcma);O^?$^5QgvlE9x>59B}&kLqbR}v#eIjYG(sC#36XhkX2$s99FQup11u`?Fp8^ zhp7G7RrOYNmn&DdH!q*4DhQ!SQuqE}LG<(~J$oja_AcKFz2uu$O?!X-NCt45-|Ek& zW_J{uo94fL-E4n!ZMXHrbG18mv|^V&SYa z$x&aoLa`rhW~-wx6-hnn7XG-PZjQw1u+!p_(J~H~;viThGqwnG0!&KA^+HFXS@lZjqfmaXDf=o_;XDc| zE+IS3wJ#I}XIf<1d0R^^Tv|rUIrqY|8m5X&1H(RlGwx3oW$d1MkD> z^t51n)as=ordj_7(Tab5wV7OQ5@$5o$~z~$q@VW^I!px%>@2x?)JME-B)^BW*L8j zH1}&+&7QVR-+ejNO;35wZYOuwU3+d?M9f2WhPS&D7`X{8MsY?wqBno;?;w?O23wlr zTz6k!tA5bmgDgDx2cB|*JqIj_U+>PlLwEns^y{hlAS4Ab4(a?$_{JDVz|(~zH>{{- zWS}!&j8s5#T%v?RJbWv8Wc>hXiN2Z9`Lx>~nv=-8_s0#&@u=^P&0~7Bzk56uWwY(N gGv$Pb0&iaL|8Jmv`0uhioriSLSW#SE{r30E?=F_x#Q*>R literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsCheckmark.imageset/Contents.json b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsCheckmark.imageset/Contents.json new file mode 100644 index 0000000000..146a290ce9 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsCheckmark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "AnalyticsTick.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsLogo.imageset/AnalyticsLogo.pdf b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsLogo.imageset/AnalyticsLogo.pdf new file mode 100644 index 0000000000000000000000000000000000000000..096c22d4bb13025e4de4835015cc9bf44eb673b4 GIT binary patch literal 19190 zcmdViTaP4Jbp_z}`W5+t1eVCoxJO6`2?iKhHUiK10SWn`sVRoReHnK*7WmiuTPGq< zL}k@n8d*YaSte&^#JTLt+H3D~Du3|XU;OIF&3U@JYfhKb+dusI?sWRePfjoX{cm4C zoL>CyH-Gr^!~F*`|6cs-!<#QZ{OQzs@UuASao`TFoe`S10+^3M9==f->Y^3D6N zU%q>IbN{zT-~aTd+fbkX8_vt z!%i&g{fBoCFJIri_{GD2e|7)x*{450ee(J7zwY1P{nuS{eW{N9N8?(d$HzPd9~+jM<19p3j=dHr(Q=hP_3hRe`iYhv;@EPH&yFcCi@$SvtzkOc|4(+*Zr~cBO z`tdw0eY-5N;BMiot*Yda1+9MSE^V_ME!ei_zMqD1IyGlD@7wv*cIUZUrlCD`=cXB_ zX*`JT@A%QMw0(P&=9Oh=SwG(n^T3X~%rKW_=(^MW-7d^@+>Pg0!LoQ|4~VREnuklb zi_FWfhN&6GUD!qfoY0gdC&24kar+%3_pN!I8#(CWLyNk?w)ypod z#>|?UcAjq5zH0eC_Wh^Lz@Tyqr26t6qx+ZRXyf|?E~`eKrh{~| zUeCF%1MZ^3#Jg~_@`YclR_^<&m954r@oK}Zh9|04J_@U$HY-E4D%xU+HXd8KK2XiQ z+jh72FrB+`92#qx&h0o1%lxrcZd(!h#L9iwcGp-q#^)9b{neg)VYRYQWwh9Fjri*) zniI&~I4`S}^X*J?mr?lio0V4~qjbwGdNwGTx?;Ow7-)UdQ#%}FtCbTrN9ksaxmbA} za2FjW-i2&?S^7ru+3ek}R<_2aICeniC|a!yboemSug~okOA>Bmg2z@~75xoX-i^E4 zyAC)u=dtUWr9TaL(llN8<43~vH(J@YR{Z=wvT}{|-OAH*R+b&sgmJJk|5-E3!O8)G zD>&{}zFb3Yw=zfD;CH#YS0>(tS1YsGhTTV#R>$5A`u(hY6U)z7xmvUubPwSAT{|bA zT(Y0>j0g2&AyxJJryr7%8^jZ~7p_&ZLP%9MI!xgx+k^$Zh7moB$O}yi!Z}q>#_7)& zskz}(cTIa9rm^qG6J0jbGFVP`?#KQz;$!Eb>6@%@Ir}Oi9H?C`U0!9nz8fbnyqvpB zH;Eyl#UOUFoM-67th*>n@A`3W)_1oD%i23*=Sh6Rd|4?7SVo_WV{0R5%IYrxDU0sT zUYIS~@_QQ@%cpVaFSC7ifU&*6=EIINPTM!ZpN!ru4T?~9(D;}qHw}`_aDp|wS%NHgdFU_T*BeVMM#)}I~ehj5O#Lr-1 z*Z8sBZ!1bDYYT!nehC8c!)-6C51z-lYw7vbDwJu^(KGAH3=uwkJzRW`4?{&W0>rR3 zK4Gefnq}_5ct#+cn@JQzD9q+nxW(80{x0k{FBlAMR)N$qJtfaflW#mW|cAIp*`2LY9>IfORy1pUW)%9O< z{o!Jz5W={k7|bZL2&b8<2)jiXcZAQpUe;TqqHe=-&zF{dO32SMF4SgSgur7{!_Y1n zwK_PBZF+n>>d|bRu5wpEG$;bH9FS|u z>w#CS0S<{25S<|->sld67VV8>yGpL(m9-yYs1)a0CSa!t09#S&XR)UA!>CYD+p(Ew zCI;o0W#)+^6gDcIKW3v0y~n zm0gP8rH)GNqTmCP^xOhkQ_iOWZ1yu4j_nj+YK!p43Er!VMYYvSu|FFojf1AKY_EWw(Ut-$rp!7FbuAGFkD5UjB(5==b}3s|l=BIo1?9F`Z_QDx^c1CD zC*jXi$6qqkh00f)7{`+FGrXca#XW4%7e&-i*az3L$Sf5b2u=vg9F{ph-2bGj`@&w8SN#PGeN|pYM zHv7 z$fWteK1I`nj1o8*F(tzn?et4KWNuM|p2V~~JE-OD@X3b5=WVe(i)KCu-^R$UDIXiZ2JZ;-FolgQL zU)ou8H8iF0Hj@v?OxaI7pjgn?WyHbTmk!CYpSEQ*0qXWSWGX@dJU4YmkbVeT3*qM1RSjvkIQ_; z<)8RQ#nqc6U4T{aXDy1a{O=l6Hqz0Jl3fG&hU-?DxhjOmQ`aYHUIG~Ga1HeOK88?e zvGAPxRQdq~tgw^w*lGfI#gk&=n{}4;?{n1^yq5ZUXm4UtL4 z*YWOyEbORb1F&YHGin@`FkoWNH2E&Q^LVEgxN5-CQSS-vdsSdbenNxq>XELZaA_U* z1l=;m#fOBsXz=P_ys=Fz>7Ww zi>9M<&h>PKS@?64zdlNhA8J#ot46{v4&C4psRZFSpP^=^ZIiO+%%5&z^u%zwQ#}n)LgUZOvhjvl`6f)%_JWn?m&F(8ht%} zOR?M)?d2^c&vS6GH1}lmC)PM2mNWYhvV`MDG9p!+o6BhJrDGqD}RWDvF4`YnabF>>qzhaKvguRC<_z?C! zF^@bKBE|tiEere?wubQ7p&-D=WH_V_WTta(4dOj#8`jSC*;W3J<7aBQI=x6a7JS$Q z9!X^-cI?^5wdkA%=QGij(~l4Mgf4Hs{UnZP0+4S7nQSnMn67-->GvQ+flf~+LnDy&6D z!Shxz5^iuPMlJ~A7$e9uEvJtMrXt98e23)}oVZzAR{T+iYExPW-OM0^U8Rr!-lZxn zow-Ec&4+aGJSzITwYBN{rV$t%0B`Bq((7J@0s_^+CoU4VYA9REQ7WdT%g#X# zB3()hDX&(cAk`$2@sdO89(aS^8AR5`>R+7$AjX$otD^W`-3uvx~LJsvE>ITBEZZrrB0DzWUV zeh;$E3^DWK|6(h<1AKM9gGnumxdqp#z-cA`22en*v>6XroSVi2)-CcPNvI@EgDM_? znrW~GeiA;~hb@BlI597+YTZR2-LyM|jCItIF~_$x?RH1H6Wt(5)cBHkk1!F>FnhKS0PM<+9ugQVGBi8;Tli%^*U;C_`vGw z@nu+Jp>gpajY@v74grht!A-izE!)61BV^=#7ii)d4F*kk0WxlN2G6EB#NS^NDrpl)89*hEYriT&I)&R2wkdQYyoO7CJ)>6FJgOm`Bsl`e zL<^ICs{sfH<-MsQRPv{d^33axMy)yzgM;j85;1+6%TwV@b7kIS?@ZneP-Gaz|uri z5G_UyI|Dx|>cSk=GVmla?8dA5Upje+SRBlf#0z$)TPj}kztHpG?M;l@wpUR%H;@VsQV4`&iGxmL zD_uen1+dm&=(c!4S%?PNH7;(!(h=IogE24MwCsYtWmW3~>gt|6cj+4`iO5~1v>C%; zZKnxIM5!$<7Gw7G^!uE3E&MUeOiMSq>@;?Z1WJ`QAe1V|Rc1`z^Js@+o~Pg=>`NbG zk+(42bX7|Hqk3!J)|6DyB}_Mw(7)ytinwGD%OZ1ZzNOZj$g_yd`xq{(iRG=rSfLM^92;AwoEx>J z!3I!E=C0e3@GLb$R?)7Dr;FTF{sfmTK7?7NQn;}iRYNzmZqtHC>d%Q{tJNHj?Q)P| zLFqfCWfnE1r_=Z82FpyCM8cOZs?0*DQP&*tmrQ`4DncS-2#FwXI~`H%2DBeTk02YJWCygT^^od@PEbOeFnZ#vYsE9m;|QI% zb?t06$%amxT;%-NawlEoG9x0L_>4}#PEr6MWDj8H?YQNWe7zy)O}Y&`S?E`@Bu!fZ ziCWQG!qD1-M>j;ZiVi?c<=b;P7Q)$8VzI*}R0`_|W{(_MW$DXettF;rExHAW7G4Z{ z%LfjrYF|&XpL|@-t?P#LYYg2z)Ml+h_=^s#B!^nDxDX{=c2`A?> zjsQw1=wG9(tOo1KO^E;jNQmy9u|-Vu45=%{5tMr)`y_4{|T!jO9sSt*sWijwGBs6Dz?5k)N3tC)+g|yssqWO{-1DxLCP5g_AMM?Zs7Y5r4 zos?Hfff7sbLv zh$Y8qF^#NB7(2YM2IJ#s96!B8u35To^O%?|6+b8S0r(}JgK4eSVcE0w<^0jaxfl~& z$yrkqmk^r{vQ6Bmx3UcQ%Rg(NwM)&%{K?Up`qs~UO zeiQ>FOs8?VY4x^N`pCW-rX@gd2bSc9EyBRp3WR)Hm%wpllk>>hvPj37gW`&^&9!Nf z-VG0BO*)aT(oZ89c|y7~n!Yh)@zAtv!sO*ykI^OEOV>3gfeu0-I>>ROPHD%r6I-P> z7V6@;__iiYI2@$rM!Af^{c_^~ARR>U5ExbXO??jPrcg13SjCJjk^|@BS7w#S$`t_A zA~y!&%%a@(J!VOWvJb3v!G;8IZVKe9ZJS1r>e#aE6Lb~Mz|YifoVV)CB6ZtUM?{-$ zcTs}hP2x12cVS9FvB*d2Xtbc8zaC5AKdud%Rbc9(+k(Z7>)$0L1bD4gaxMo}F-D@e z80@-DCex{u@}dnj<>4Bff^$__itGHd=&IR+Y%_q7pc*~JN(!8W$Tr`>m?$G!U!KyW z&L1LJIqnN1_{9Lk@?qOxwCcno`wJqt?v&+;klg3VNrLikzsG)1>h7B#-B43C+O9)A zX#gM5y2Nk4J63=!?3aPIsg*OY15nrup1unmWy^6o%}8X25_e%ZfUWQvq*KelZXF_q zYNX??3d-3)_=Tr($tRpfZVai?R!O7YQLqG3d=H}JR%1D4NbfjlWKMU(;hVoK@?Mo# zGH|uf#OC&t72AipiaWSdWHGtBs@tF?pD1(zCK4qv3zP#69Yd`=eFaAYj(XN>4WNvn z*E@c7e~AKBMCQv7<>IDkTqUrrs;a~k=Z;=BjaG^K9R_vbr5r7-PdV^+J;5gGNZEQh z+A^O4q|8TYSgg%wRcVw^hYO?mYZWV3! z^;uoGSFTMvNrM*@x8dJNcD1Y^ddHL zgG3YSV)*Ir>!saY{A4ckqVt|kOQxLO|PGE`I!C37uLW??10gG4Yu*MG`6 zBU|PoOSvi_*8**gEl{cPl)`V5q@N=7<-JW7eB!LZf7LZ>npg$z{v1i_Mdl(w&5F-i z71S+^42tDWh?QeLq9wS(YY+QNwka7fF{KA$0)rxgxQoX(O`%~q-AuG>`mB<~lzin7 z%X{^y+*hEWI+Cp`j`+CwEXuu}yq9n*6bmx@ORsoXrb)h)8(@srzFjcPa`r zfw=J5>h)DqRbs(ceI8_+)-zIaRk4hF3aLD{`40B8DxHc#z*Fr=z^MT8G(4b2oatzLd@MZzB@2veAjrS z_iM&qOn_SogwY5wNm%aYyC$O|NhDa4f|b89EA3dS`r@dx{HYF_BJh48Ex4$PJw zK4xj5B=~XauhP*7%@&^tE+A9NXvI$cEQlYcdtPYLCbDX_FL^o4XMGMiNDWq=!;aEG zQnBduinR5{f;i4T)b%SL)EmovT=?#Jn4E@?KYK{>VPK7deUk_jq6TC46~^>vU`wN_cxenv{OrI<)5ewgoI$>d=T z)ZD7DDy@cmz&PnLK2dH<)cwVC5R~<%!sHNQrB^}F41kK}md);&Rj0e?7$*6n>@VNv z+D-Yl?{s_Is_J)I{gQ9mASyhm6^4Fyu*ijjl6&V^Ia^65m|k{{mE4X-CPRpLp#sG# zcF>~pdlhZr61tOUs=hL-l{y{;PW71Ti%%59cErs)a`{MRNU)?yrKnA623xB|v_aDr zm!sWF;oX#Xm6&WQw*w9`_tH}38VPrze|u2a*h5SsCd$1EU{&frNzI?au(y;R_sW-+ zmlHJr=z?3Bc*8~ms6OPH?9Y`*l+>WE2BqayRh9gsiIa&pjaEtC!!x~@ds?URL(|XKZE(gRiJUb)R`KtY_N{Uar4u zi;*Pl$1zeWtnZ8wD1Q?pMQJxN5*0m;k-}pi#YiO8rdy0e_BckO*+(%_;-UJ`79*MO zag40qc#IK0h43Us66k+Vj0B7y$H=2-`^Fdvtj*-=?jxkaR4qSQ0MfVuF zdwBE3`}KD=e9BLm+@YV_Z<;9B`XNsgxm^1Fe(MD8{6l{0cIZeDNDyo((g@8UW}&J(sr>xYBv`1-Y(`Hbmfx%KC4u#;+d5PUHF z_VYI7_h5bp%wz2RhhA=eZf5=2n9qOx^8J677uH|M$$vijJ)5V$II~J#e0qx4Z9kmz zO;2s|7k)A4XRp5g)64H3`syjqj=24dPuxVDC zdil{m%9Ek?pBIW>Tz}K(lecd^JiIYwRB#pkHa}hT;e;0y(LqrWty{?NCjHA-Us&rY zTlnmBUGgVy|Ki=|$p+q&97aN&k3(gP@9T{F)<1dq;pMBhUp{{KukRlIc$Zf8-5NUi z|0mZ+)FR~Dzf6#SCqD?3f3l3yxqLi`P{79`O+a?azxq_^bl-?l4%~?J5T1xqqvzi~ zmbTih2UDW%}zo6V7zJL4r;YX(*e)YwVRu#Ya?Co3njrxkcUVQ%5 e|2#w&l>7d}%Xc4&c3hvD=er;L;FrJo)&ByI(8X#1 literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsLogo.imageset/Contents.json b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsLogo.imageset/Contents.json new file mode 100644 index 0000000000..7d49fc335d --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsLogo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "AnalyticsLogo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Authentication/Analytics/Contents.json b/Riot/Assets/Images.xcassets/Authentication/Analytics/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/Analytics/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index e2d6c1b3f1..8ba53009d0 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -41,6 +41,7 @@ "retry" = "Retry"; "on" = "On"; "off" = "Off"; +"enable" = "Enable"; "cancel" = "Cancel"; "save" = "Save"; "join" = "Join"; @@ -946,9 +947,32 @@ Tap the + to start adding people."; "no_voip" = "%@ is calling you but %@ does not support calls yet.\nYou can ignore this notification and answer the call from another device or you can reject it."; // Analytics -"analytics_prompt_title" = "Help us improve %@"; -"analytics_prompt_new_user" = "Would you like to help improve %@ by automatically reporting crash reports and usage data?\n\nWe don't record or profile any personal data, and we don't share anything with any third parties."; -"analytics_prompt_posthog_upgrade" = "To allow us to understand how people use multiple devices, we've enhanced our analytics data to include a randomly generated identifier associated with your account that will be shared across your devices.\n\nWe don't record or profile any personal data, and we don't share anything with any third parties.\n\nYou previously agreed to send anonymous usage data to %@ - is this still okay?"; +"analytics_prompt_title" = "Help improve %@"; +"analytics_prompt_description_new_user" = "Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; +"analytics_prompt_description_upgrade" = "You previously consented to share anonymous usage data with us. Now, to help understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; +"analytics_prompt_terms_start_new_user" = "You can read all our terms "; +"analytics_prompt_terms_link_new_user" = "here"; +"analytics_prompt_terms_end_new_user" = "."; +"analytics_prompt_terms_start_upgrade" = "Read all our terms "; +"analytics_prompt_terms_link_upgrade" = "here"; +"analytics_prompt_terms_end_upgrade" = ". Is that OK?"; +"analytics_prompt_point_1_start" = "We "; +"analytics_prompt_point_1_bolded_dont" = "don't"; +"analytics_prompt_point_1_end" = " record or profile any account data"; +"analytics_prompt_point_2_start" = "We "; +"analytics_prompt_point_2_bolded_dont" = "don't"; +"analytics_prompt_point_2_end" = " share information with third parties"; +"analytics_prompt_point_3" = "You can turn this off anytime in settings"; +"analytics_prompt_yes" = "Yes, that's fine"; +"analytics_prompt_stop" = "Stop sharing"; + +// TODO: Get markdown formatting working. +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "We don't record or profile any account data"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "We don't share information with third parties"; +"analytics_prompt_terms_new_user" = "You can read all our terms here."; +"analytics_prompt_terms_upgrade" = "Read all our terms here. Is that OK?"; // Crypto "e2e_enabling_on_app_update" = "%@ now supports end-to-end encryption but you need to log in again to enable it.\n\nYou can do it now or later from the application settings."; diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 3c25ee3364..70ad1b32ed 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -20,6 +20,8 @@ internal typealias AssetImageTypeAlias = ImageAsset.Image // swiftlint:disable identifier_name line_length nesting type_body_length type_name internal enum Asset { internal enum Images { + internal static let analyticsCheckmark = ImageAsset(name: "AnalyticsCheckmark") + internal static let analyticsLogo = ImageAsset(name: "AnalyticsLogo") internal static let socialLoginButtonApple = ImageAsset(name: "social_login_button_apple") internal static let socialLoginButtonFacebook = ImageAsset(name: "social_login_button_facebook") internal static let socialLoginButtonGithub = ImageAsset(name: "social_login_button_github") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index df6ce4e940..59315f1d9f 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -31,18 +31,94 @@ public class VectorL10n: NSObject { public static func activeCallDetails(_ p1: String) -> String { return VectorL10n.tr("Vector", "active_call_details", p1) } - /// Would you like to help improve %@ by automatically reporting crash reports and usage data?\n\nWe don't record or profile any personal data, and we don't share anything with any third parties. - public static func analyticsPromptNewUser(_ p1: String) -> String { - return VectorL10n.tr("Vector", "analytics_prompt_new_user", p1) + /// Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices. + public static var analyticsPromptDescriptionNewUser: String { + return VectorL10n.tr("Vector", "analytics_prompt_description_new_user") } - /// To allow us to understand how people use multiple devices, we've enhanced our analytics data to include a randomly generated identifier associated with your account that will be shared across your devices.\n\nWe don't record or profile any personal data, and we don't share anything with any third parties.\n\nYou previously agreed to send anonymous usage data to %@ - is this still okay? - public static func analyticsPromptPosthogUpgrade(_ p1: String) -> String { - return VectorL10n.tr("Vector", "analytics_prompt_posthog_upgrade", p1) + /// You previously consented to share anonymous usage data with us. Now, to help understand how people use multiple devices, we’ll generate a random identifier, shared by your devices. + public static var analyticsPromptDescriptionUpgrade: String { + return VectorL10n.tr("Vector", "analytics_prompt_description_upgrade") } - /// Help us improve %@ + /// We don't record or profile any account data + public static var analyticsPromptPoint1: String { + return VectorL10n.tr("Vector", "analytics_prompt_point_1") + } + /// don't + public static var analyticsPromptPoint1BoldedDont: String { + return VectorL10n.tr("Vector", "analytics_prompt_point_1_bolded_dont") + } + /// record or profile any account data + public static var analyticsPromptPoint1End: String { + return VectorL10n.tr("Vector", "analytics_prompt_point_1_end") + } + /// We + public static var analyticsPromptPoint1Start: String { + return VectorL10n.tr("Vector", "analytics_prompt_point_1_start") + } + /// We don't share information with third parties + public static var analyticsPromptPoint2: String { + return VectorL10n.tr("Vector", "analytics_prompt_point_2") + } + /// don't + public static var analyticsPromptPoint2BoldedDont: String { + return VectorL10n.tr("Vector", "analytics_prompt_point_2_bolded_dont") + } + /// share information with third parties + public static var analyticsPromptPoint2End: String { + return VectorL10n.tr("Vector", "analytics_prompt_point_2_end") + } + /// We + public static var analyticsPromptPoint2Start: String { + return VectorL10n.tr("Vector", "analytics_prompt_point_2_start") + } + /// You can turn this off anytime in settings + public static var analyticsPromptPoint3: String { + return VectorL10n.tr("Vector", "analytics_prompt_point_3") + } + /// Stop sharing + public static var analyticsPromptStop: String { + return VectorL10n.tr("Vector", "analytics_prompt_stop") + } + /// . + public static var analyticsPromptTermsEndNewUser: String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_end_new_user") + } + /// . Is that OK? + public static var analyticsPromptTermsEndUpgrade: String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_end_upgrade") + } + /// here + public static var analyticsPromptTermsLinkNewUser: String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_link_new_user") + } + /// here + public static var analyticsPromptTermsLinkUpgrade: String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_link_upgrade") + } + /// You can read all our terms here. + public static var analyticsPromptTermsNewUser: String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_new_user") + } + /// You can read all our terms + public static var analyticsPromptTermsStartNewUser: String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_start_new_user") + } + /// Read all our terms + public static var analyticsPromptTermsStartUpgrade: String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_start_upgrade") + } + /// Read all our terms here. Is that OK? + public static var analyticsPromptTermsUpgrade: String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_upgrade") + } + /// Help improve %@ public static func analyticsPromptTitle(_ p1: String) -> String { return VectorL10n.tr("Vector", "analytics_prompt_title", p1) } + /// Yes, that's fine + public static var analyticsPromptYes: String { + return VectorL10n.tr("Vector", "analytics_prompt_yes") + } /// Please review and accept the policies of this homeserver: public static var authAcceptPolicies: String { return VectorL10n.tr("Vector", "auth_accept_policies") @@ -1247,6 +1323,10 @@ public class VectorL10n: NSObject { public static var emojiPickerTitle: String { return VectorL10n.tr("Vector", "emoji_picker_title") } + /// Enable + public static var enable: String { + return VectorL10n.tr("Vector", "enable") + } /// Send an encrypted message… public static var encryptedRoomMessagePlaceholder: String { return VectorL10n.tr("Vector", "encrypted_room_message_placeholder") diff --git a/Riot/Modules/TabBar/MasterTabBarController.h b/Riot/Modules/TabBar/MasterTabBarController.h index 170e912d12..f8c4b4704c 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.h +++ b/Riot/Modules/TabBar/MasterTabBarController.h @@ -193,5 +193,6 @@ typedef NS_ENUM(NSUInteger, MasterTabBarIndex) { - (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectRoomPreviewWithParameters:(RoomPreviewNavigationParameters*)roomPreviewNavigationParameters completion:(void (^)(void))completion; - (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectContact:(MXKContact*)contact withPresentationParameters:(ScreenPresentationParameters*)presentationParameters; - (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectGroup:(MXGroup*)group inMatrixSession:(MXSession*)matrixSession presentationParameters:(ScreenPresentationParameters*)presentationParameters; +- (void)masterTabBarController:(MasterTabBarController *)masterTabBarController shouldPresentAnalyticsPromptAsUpgrade:(BOOL)isUpgradePrompt forMatrixSession:(MXSession*)matrixSession; @end diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index 61329d157b..69151e2760 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -70,6 +70,11 @@ @interface MasterTabBarController () currentAlert = nil; - - }]]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n yes] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - MXStrongifyAndReturnIfNil(self); - [Analytics.shared optInWith:mxSession]; - self->currentAlert = nil; - }]]; - - [currentAlert mxk_setAccessibilityIdentifier: @"HomeVCUseAnalyticsAlert"]; - [self presentViewController:currentAlert animated:YES completion:nil]; + [self.masterTabBarDelegate masterTabBarController:self + shouldPresentAnalyticsPromptAsUpgrade:Analytics.shared.promptShouldDisplayUpgradeMessage forMatrixSession:mxSession]; } #pragma mark - Review session diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 63bf354a20..f88b7d420d 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -487,6 +487,14 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { self.splitViewMasterPresentableDelegate?.splitViewMasterPresentableWantsToResetDetail(self) } + @available(iOS 14.0, *) + private func presentAnalyticsPrompt(promptType: AnalyticsPromptType, with session: MXSession) { + let parameters = AnalyticsPromptCoordinatorParameters(promptType: promptType, session: session, navigationRouter: navigationRouter) + let coordinator = AnalyticsPromptCoordinator(parameters: parameters) + coordinator.start() + add(childCoordinator: coordinator) + } + // MARK: UserSessions management private func registerUserSessionsServiceNotifications() { @@ -578,6 +586,12 @@ extension TabBarCoordinator: MasterTabBarControllerDelegate { self.masterTabBarController.navigationItem.leftBarButtonItem = sideMenuBarButtonItem } + + func masterTabBarController(_ masterTabBarController: MasterTabBarController!, shouldPresentAnalyticsPromptAsUpgrade isUpgradePrompt: Bool, forMatrixSession matrixSession: MXSession!) { + if #available(iOS 14.0, *) { + presentAnalyticsPrompt(promptType: isUpgradePrompt ? .upgrade : .newUser, with: matrixSession) + } + } } // MARK: - RoomCoordinatorDelegate diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift new file mode 100644 index 0000000000..8df27340a9 --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift @@ -0,0 +1,98 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt +// +// Copyright 2021 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 + +// The state is never modified so this is unnecessary. +enum AnalyticsPromptStateAction { } + +enum AnalyticsPromptViewAction { + /// Enable analytics. + case enable + /// Disable analytics. + case disable + /// Open the service terms link. + case openTermsURL +} + +enum AnalyticsPromptViewModelResult { + /// Enable analytics. + case enable + /// Disable analytics. + case disable +} + +struct AnalyticsPromptViewState: BindableState { + /// The type of prompt to display. + let promptType: AnalyticsPromptType + /// The app's bundle display name. + let appDisplayName: String +} + +enum AnalyticsPromptType { + case newUser + case upgrade +} + +extension AnalyticsPromptType { + var description: String { + switch self { + case .newUser: + return VectorL10n.analyticsPromptDescriptionNewUser + case .upgrade: + return VectorL10n.analyticsPromptDescriptionUpgrade + } + } + + var termsStrings: (String, String, String) { + switch self { + case .newUser: + return (VectorL10n.analyticsPromptTermsStartNewUser, + VectorL10n.analyticsPromptTermsLinkNewUser, + VectorL10n.analyticsPromptTermsEndNewUser) + case .upgrade: + return (VectorL10n.analyticsPromptTermsStartUpgrade, + VectorL10n.analyticsPromptTermsLinkUpgrade, + VectorL10n.analyticsPromptTermsEndUpgrade) + } + } + + var enableButtonTitle: String { + switch self { + case .newUser: + return VectorL10n.enable + case .upgrade: + return VectorL10n.analyticsPromptYes + } + } + + var disableButtonTitle: String { + switch self { + case .newUser: + return VectorL10n.cancel + case .upgrade: + return VectorL10n.analyticsPromptStop + } + } +} + +extension AnalyticsPromptType: CaseIterable { } + +extension AnalyticsPromptType: Identifiable { + var id: Self { self } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift new file mode 100644 index 0000000000..f65aded17e --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift @@ -0,0 +1,76 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt +// +// Copyright 2021 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 Combine + +@available(iOS 14, *) +typealias AnalyticsPromptViewModelType = StateStoreViewModel +@available(iOS 14, *) +class AnalyticsPromptViewModel: AnalyticsPromptViewModelType { + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var completion: ((AnalyticsPromptViewModelResult) -> Void)? + + // MARK: - Setup + + /// Initialize a view model with the specified prompt type and app display name. + init(promptType: AnalyticsPromptType, appDisplayName: String) { + super.init(initialViewState: AnalyticsPromptViewState(promptType: promptType, appDisplayName: appDisplayName)) + } + + // MARK: - Public + + override func process(viewAction: AnalyticsPromptViewAction) { + switch viewAction { + case .enable: + enable() + case .disable: + disable() + case .openTermsURL: + openTermsURL() + } + } + + override class func reducer(state: inout AnalyticsPromptViewState, action: AnalyticsPromptStateAction) { + // There is no mutable state to reduce :) + } + + /// Enable analytics. The call to the Analytics class is made in the completion. + private func enable() { + completion?(.enable) + } + + /// Disable analytics. The call to the Analytics class is made in the completion. + private func disable() { + completion?(.disable) + } + + /// Open the service terms link. + private func openTermsURL() { + guard let url = URL(string: "https://element.io/cookie-policy") else { return } + UIApplication.shared.open(url) + } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift new file mode 100644 index 0000000000..b61796e972 --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift @@ -0,0 +1,94 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt +/* + Copyright 2021 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 + +struct AnalyticsPromptCoordinatorParameters { + /// The type of prompt to display. + let promptType: AnalyticsPromptType + /// The session to use if analytics are enabled. + let session: MXSession + /// The navigation router used to display the prompt. + let navigationRouter: NavigationRouterType +} + +final class AnalyticsPromptCoordinator: Coordinator { + + // MARK: - Properties + + // MARK: Private + + private let parameters: AnalyticsPromptCoordinatorParameters + private let analyticsPromptHostingController: UIViewController + private var _analyticsPromptViewModel: Any? = nil + + @available(iOS 14.0, *) + fileprivate var analyticsPromptViewModel: AnalyticsPromptViewModel { + return _analyticsPromptViewModel as! AnalyticsPromptViewModel + } + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: (() -> Void)? + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: AnalyticsPromptCoordinatorParameters) { + self.parameters = parameters + let viewModel = AnalyticsPromptViewModel(promptType: parameters.promptType, appDisplayName: AppInfo.current.displayName) + + let view = AnalyticsPrompt(viewModel: viewModel.context) + _analyticsPromptViewModel = viewModel + analyticsPromptHostingController = VectorHostingController(rootView: view) + } + + // MARK: - Public + + func start() { + guard #available(iOS 14.0, *) else { + MXLog.debug("[AnalyticsPromptCoordinator] start: Invalid iOS version, returning.") + return + } + + MXLog.debug("[AnalyticsPromptCoordinator] did start.") + + parameters.navigationRouter.present(toPresentable(), animated: true) + + analyticsPromptViewModel.completion = { [weak self] result in + MXLog.debug("[AnalyticsPromptCoordinator] AnalyticsPromptViewModel did complete with result: \(result).") + + guard let self = self else { return } + + switch result { + case .enable: + Analytics.shared.optIn(with: self.parameters.session) + self.parameters.navigationRouter.dismissModule(animated: true, completion: nil) + case .disable: + Analytics.shared.optOut() + self.parameters.navigationRouter.dismissModule(animated: true, completion: nil) + } + } + } + + func toPresentable() -> UIViewController { + return self.analyticsPromptHostingController + } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift new file mode 100644 index 0000000000..3ac17376e7 --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift @@ -0,0 +1,54 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt +// +// Copyright 2021 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 SwiftUI + +/// Using an enum for the screen allows you define the different state cases with +/// the relevant associated data for each case. +@available(iOS 14.0, *) +enum MockAnalyticsPromptScreenState: MockScreenState, CaseIterable { + /// The type of prompt to display. + case promptType(AnalyticsPromptType) + + /// The associated screen + var screenType: Any.Type { + AnalyticsPrompt.self + } + + /// A list of screen state definitions + static var allCases: [MockAnalyticsPromptScreenState] { + AnalyticsPromptType.allCases.map { MockAnalyticsPromptScreenState.promptType($0) } + } + + /// Generate the view struct for the screen state. + var screenView: ([Any], AnyView) { + let promptType: AnalyticsPromptType + switch self { + case .promptType(let analyticsPromptType): + promptType = analyticsPromptType + } + let viewModel = AnalyticsPromptViewModel(promptType: promptType, appDisplayName: "Element") + + return ( + [promptType, viewModel], + AnyView(AnalyticsPrompt(viewModel: viewModel.context) + .addDependency(MockAvatarService.example)) + ) + } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift new file mode 100644 index 0000000000..ada017da69 --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift @@ -0,0 +1,65 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt +// +// Copyright 2021 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 XCTest +import RiotSwiftUI + +@available(iOS 14.0, *) +class AnalyticsPromptUITests: MockScreenTest { + + override class var screenType: MockScreenState.Type { + return MockAnalyticsPromptScreenState.self + } + + override class func createTest() -> MockScreenTest { + return AnalyticsPromptUITests(selector: #selector(verifyAnalyticsPromptScreen)) + } + + func verifyAnalyticsPromptScreen() throws { + guard let screenState = screenState as? MockAnalyticsPromptScreenState else { fatalError("no screen") } + switch screenState { + case .promptType(let promptType): + verifyAnalyticsPromptType(promptType) + } + } + + /// Verify that the prompt is displayed correctly for new users compared to upgrading from Matomo + func verifyAnalyticsPromptType(_ promptType: AnalyticsPromptType) { + let enableButton = app.buttons["enableButton"] + let disableButton = app.buttons["disableButton"] + + XCTAssert(enableButton.exists) + XCTAssert(disableButton.exists) + + switch promptType { + case .newUser: + XCTAssertEqual(enableButton.label, VectorL10n.enable) + XCTAssertEqual(disableButton.label, VectorL10n.cancel) + case .upgrade: + XCTAssertEqual(enableButton.label, VectorL10n.analyticsPromptYes) + XCTAssertEqual(disableButton.label, VectorL10n.analyticsPromptStop) + } + } + + func verifyAnalyticsPromptLongName(name: String) { + let displayNameText = app.staticTexts["displayNameText"] + XCTAssert(displayNameText.exists) + XCTAssertEqual(displayNameText.label, name) + } + +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift new file mode 100644 index 0000000000..de1789afc1 --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift @@ -0,0 +1,139 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt +// +// Copyright 2021 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 + +@available(iOS 14.0, *) +/// A prompt that asks the user whether they would like to enable Analytics or not. +struct AnalyticsPrompt: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme + + // MARK: Public + + @ObservedObject var viewModel: AnalyticsPromptViewModel.Context + + // MARK: Views + + /// The text that explains what analytics will do. + private var descriptionText: some View { + VStack { + Text("\(viewModel.viewState.promptType.description)\n") + + AnalyticsPromptTermsText(promptType: viewModel.viewState.promptType) + .onTapGesture { + viewModel.send(viewAction: .openTermsURL) + } + } + } + + /// The list of re-assurances about analytics. + private var checkmarkList: some View { + VStack(alignment: .leading) { + Label { + Text(VectorL10n.analyticsPromptPoint1Start) + + Text(VectorL10n.analyticsPromptPoint1BoldedDont).font(theme.fonts.bodySB) + + Text(VectorL10n.analyticsPromptPoint1End) + } icon: { + Image(uiImage: Asset.Images.analyticsCheckmark.image) + } + + Label { + Text(VectorL10n.analyticsPromptPoint2Start) + + Text(VectorL10n.analyticsPromptPoint2BoldedDont).font(theme.fonts.bodySB) + + Text(VectorL10n.analyticsPromptPoint2End) + } icon: { + Image(uiImage: Asset.Images.analyticsCheckmark.image) + } + + Label { + Text(VectorL10n.analyticsPromptPoint3) + } icon: { + Image(uiImage: Asset.Images.analyticsCheckmark.image) + } + } + .font(theme.fonts.body) + } + + /// The stack of enable/disable buttons. + private var buttons: some View { + VStack { + Button { viewModel.send(viewAction: .enable) } label: { + Text(viewModel.viewState.promptType.enableButtonTitle) + .font(theme.fonts.bodySB) + } + .buttonStyle(PrimaryActionButtonStyle()) + .accessibilityIdentifier("enableButton") + + Button { viewModel.send(viewAction: .disable) } label: { + Text(viewModel.viewState.promptType.disableButtonTitle) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.accent) + } + .buttonStyle(PrimaryActionButtonStyle(customColor: .clear)) + .accessibilityIdentifier("disableButton") + } + } + + var body: some View { + VStack { + ScrollView(showsIndicators: false) { + VStack { + Image(uiImage: Asset.Images.analyticsLogo.image) + .padding(.bottom, 25) + + Text(VectorL10n.analyticsPromptTitle(viewModel.viewState.appDisplayName)) + .font(theme.fonts.title2B) + .foregroundColor(theme.colors.primaryContent) + .padding(.bottom, 2) + + descriptionText + .foregroundColor(theme.colors.secondaryContent) + .multilineTextAlignment(.center) + + Divider() + .background(theme.colors.quinaryContent) + .padding(.vertical, 28) + + checkmarkList + .foregroundColor(theme.colors.secondaryContent) + } + .padding(.top, 50) + .padding(.horizontal, 16) + } + + buttons + .padding(.horizontal, 16) + } + .background(theme.colors.background.ignoresSafeArea()) + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct AnalyticsPrompt_Previews: PreviewProvider { + static let stateRenderer = MockAnalyticsPromptScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptTermsText.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptTermsText.swift new file mode 100644 index 0000000000..ec32deddab --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptTermsText.swift @@ -0,0 +1,42 @@ +// +// Copyright 2021 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 + +@available(iOS 14.0, *) +/// The last line of text in the description with highlighting on the link string. +struct AnalyticsPromptTermsText: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme + + // MARK: Public + + let promptType: AnalyticsPromptType + + // MARK: Views + + var body: some View { + let (start, link, end) = promptType.termsStrings + + Text(start) + + Text(link).foregroundColor(theme.colors.accent) + + Text(end) + } +} diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index 78535b31a5..2a11753a67 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -20,6 +20,7 @@ import Foundation @available(iOS 14.0, *) enum MockAppScreens { static let appScreens: [MockScreenState.Type] = [ + MockAnalyticsPromptScreenState.self, MockUserSuggestionScreenState.self, MockPollEditFormScreenState.self, MockPollTimelineScreenState.self, diff --git a/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift b/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift index 9d94bac8db..c0dd43d838 100644 --- a/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift +++ b/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift @@ -18,9 +18,10 @@ import SwiftUI @available(iOS 14.0, *) struct PrimaryActionButtonStyle: ButtonStyle { - @Environment(\.theme) private var theme: ThemeSwiftUI + @Environment(\.theme) private var theme + @Environment(\.isEnabled) private var isEnabled - var enabled: Bool = false + var customColor: Color? = nil func makeBody(configuration: Self.Configuration) -> some View { configuration.label @@ -28,10 +29,18 @@ struct PrimaryActionButtonStyle: ButtonStyle { .frame(maxWidth: .infinity) .foregroundColor(.white) .font(theme.fonts.body) - .background(configuration.isPressed ? theme.colors.accent.opacity(0.6) : theme.colors.accent) - .opacity(enabled ? 1.0 : 0.6) + .background(backgroundColor(configuration.isPressed)) + .opacity(isEnabled ? 1.0 : 0.6) .cornerRadius(8.0) } + + func backgroundColor(_ isPressed: Bool) -> Color { + if let customColor = customColor { + return customColor + } else { + return isPressed ? theme.colors.accent.opacity(0.6) : theme.colors.accent + } + } } @available(iOS 14.0, *) @@ -40,11 +49,20 @@ struct PrimaryActionButtonStyle_Previews: PreviewProvider { Group { VStack { Button("Enabled") { } - .buttonStyle(PrimaryActionButtonStyle(enabled: true)) + .buttonStyle(PrimaryActionButtonStyle()) Button("Disabled") { } - .buttonStyle(PrimaryActionButtonStyle(enabled: false)) + .buttonStyle(PrimaryActionButtonStyle()) .disabled(true) + + Button { } label: { + Text("Clear BG") + .foregroundColor(.red) + } + .buttonStyle(PrimaryActionButtonStyle(customColor: .clear)) + + Button("Red BG") { } + .buttonStyle(PrimaryActionButtonStyle(customColor: .red)) } .padding() } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift index bdf54e7bdf..9e81488a56 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift @@ -79,7 +79,7 @@ struct PollEditForm: View { Button(VectorL10n.pollEditFormCreatePoll) { viewModel.send(viewAction: .create) } - .buttonStyle(PrimaryActionButtonStyle(enabled: viewModel.viewState.confirmationButtonEnabled)) + .buttonStyle(PrimaryActionButtonStyle()) .disabled(!viewModel.viewState.confirmationButtonEnabled) } .padding() diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift index 190d12a9fa..3eb01bbddf 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift @@ -14,8 +14,6 @@ // limitations under the License. // -import Foundation -import UIKit import SwiftUI struct TemplateUserProfileCoordinatorParameters { diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift index feef94130c..a4a75ef884 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift @@ -14,8 +14,6 @@ // limitations under the License. // -import Foundation -import UIKit import SwiftUI struct TemplateRoomChatCoordinatorParameters { diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift index 4f04532319..6c7cfd36f3 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift @@ -14,8 +14,6 @@ // limitations under the License. // -import Foundation -import UIKit import SwiftUI struct TemplateRoomListCoordinatorParameters { From 932eca82ccfed5421ce73bb1fac5ef40c83dde25 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Dec 2021 12:09:26 +0000 Subject: [PATCH 021/109] Support link/html in analytics prompt strings. Show the new prompt to everyone, even if they previously opted out. Add docs to Analytics. --- Podfile | 1 + Podfile.lock | 4 +- Riot/Assets/en.lproj/Vector.strings | 22 +---- Riot/Generated/Strings.swift | 52 ++--------- Riot/Managers/Analytics/Analytics.swift | 70 ++++++++++---- Riot/Modules/Application/LegacyAppDelegate.m | 1 - Riot/Modules/TabBar/MasterTabBarController.h | 2 +- Riot/Modules/TabBar/MasterTabBarController.m | 16 ++-- Riot/Modules/TabBar/TabBarCoordinator.swift | 8 +- .../AnalyticsPromptModels.swift | 59 +++++++++--- .../AnalyticsPromptViewModel.swift | 4 +- .../AnalyticsPromptCoordinator.swift | 14 ++- .../Coordinator/AnalyticsPromptStrings.swift | 71 ++++++++++++++ .../MockAnalyticsPromptScreenState.swift | 3 +- .../MockAnalyticsPromptStrings.swift | 52 +++++++++++ .../View/AnalyticsPrompt.swift | 29 ++---- .../View/AnalyticsPromptCheckmarkItem.swift | 92 +++++++++++++++++++ .../View/AnalyticsPromptTermsText.swift | 48 ++++++++-- .../Util/PrimaryActionButtonStyle.swift | 4 +- 19 files changed, 401 insertions(+), 151 deletions(-) create mode 100644 RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift create mode 100644 RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift create mode 100644 RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptCheckmarkItem.swift diff --git a/Podfile b/Podfile index 9e9c5451c5..0cdf3544d3 100644 --- a/Podfile +++ b/Podfile @@ -69,6 +69,7 @@ abstract_target 'RiotPods' do # PostHog for analytics pod 'PostHog', '~> 1.4.4' + # pod 'AnalyticsEvents', :git => 'https://github.com/matrix-org/matrix-analytics-events.git', :branch => 'release/swift' pod 'AnalyticsEvents', :path => '../matrix-analytics-events/AnalyticsEvents.podspec' # Remove warnings from "bad" pods diff --git a/Podfile.lock b/Podfile.lock index f356c62a33..b378f9fbe8 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -190,7 +190,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce - AnalyticsEvents: 5d210d99ddf18f3c81116e5c98f6d9f159598f80 + AnalyticsEvents: 27b0d074e839d2d354d12ae679930e373cba5f45 BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24 BlueECC: 0d18e93347d3ec6d41416de21c1ffa4d4cd3c2cc BlueRSA: dfeef51db96bcc4edec654956c1581adbda4e6a3 @@ -231,6 +231,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 1a5c7e918ee799655f370ad9fae8cd457b8d1ca1 +PODFILE CHECKSUM: 9819df47b1ebbcea325ba9f24baa92878f3d0efe COCOAPODS: 1.11.2 diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 8ba53009d0..17ecfa6a22 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -950,29 +950,17 @@ Tap the + to start adding people."; "analytics_prompt_title" = "Help improve %@"; "analytics_prompt_description_new_user" = "Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; "analytics_prompt_description_upgrade" = "You previously consented to share anonymous usage data with us. Now, to help understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; -"analytics_prompt_terms_start_new_user" = "You can read all our terms "; +"analytics_prompt_terms_new_user" = "You can read all our terms %@."; "analytics_prompt_terms_link_new_user" = "here"; -"analytics_prompt_terms_end_new_user" = "."; -"analytics_prompt_terms_start_upgrade" = "Read all our terms "; +"analytics_prompt_terms_upgrade" = "Read all our terms %@. Is that OK?"; "analytics_prompt_terms_link_upgrade" = "here"; -"analytics_prompt_terms_end_upgrade" = ". Is that OK?"; -"analytics_prompt_point_1_start" = "We "; -"analytics_prompt_point_1_bolded_dont" = "don't"; -"analytics_prompt_point_1_end" = " record or profile any account data"; -"analytics_prompt_point_2_start" = "We "; -"analytics_prompt_point_2_bolded_dont" = "don't"; -"analytics_prompt_point_2_end" = " share information with third parties"; -"analytics_prompt_point_3" = "You can turn this off anytime in settings"; -"analytics_prompt_yes" = "Yes, that's fine"; -"analytics_prompt_stop" = "Stop sharing"; - -// TODO: Get markdown formatting working. /* Note: The word "don't" is formatted in bold */ "analytics_prompt_point_1" = "We don't record or profile any account data"; /* Note: The word "don't" is formatted in bold */ "analytics_prompt_point_2" = "We don't share information with third parties"; -"analytics_prompt_terms_new_user" = "You can read all our terms here."; -"analytics_prompt_terms_upgrade" = "Read all our terms here. Is that OK?"; +"analytics_prompt_point_3" = "You can turn this off anytime in settings"; +"analytics_prompt_yes" = "Yes, that's fine"; +"analytics_prompt_stop" = "Stop sharing"; // Crypto "e2e_enabling_on_app_update" = "%@ now supports end-to-end encryption but you need to log in again to enable it.\n\nYou can do it now or later from the application settings."; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 59315f1d9f..b8546ccdab 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -43,34 +43,10 @@ public class VectorL10n: NSObject { public static var analyticsPromptPoint1: String { return VectorL10n.tr("Vector", "analytics_prompt_point_1") } - /// don't - public static var analyticsPromptPoint1BoldedDont: String { - return VectorL10n.tr("Vector", "analytics_prompt_point_1_bolded_dont") - } - /// record or profile any account data - public static var analyticsPromptPoint1End: String { - return VectorL10n.tr("Vector", "analytics_prompt_point_1_end") - } - /// We - public static var analyticsPromptPoint1Start: String { - return VectorL10n.tr("Vector", "analytics_prompt_point_1_start") - } /// We don't share information with third parties public static var analyticsPromptPoint2: String { return VectorL10n.tr("Vector", "analytics_prompt_point_2") } - /// don't - public static var analyticsPromptPoint2BoldedDont: String { - return VectorL10n.tr("Vector", "analytics_prompt_point_2_bolded_dont") - } - /// share information with third parties - public static var analyticsPromptPoint2End: String { - return VectorL10n.tr("Vector", "analytics_prompt_point_2_end") - } - /// We - public static var analyticsPromptPoint2Start: String { - return VectorL10n.tr("Vector", "analytics_prompt_point_2_start") - } /// You can turn this off anytime in settings public static var analyticsPromptPoint3: String { return VectorL10n.tr("Vector", "analytics_prompt_point_3") @@ -79,14 +55,6 @@ public class VectorL10n: NSObject { public static var analyticsPromptStop: String { return VectorL10n.tr("Vector", "analytics_prompt_stop") } - /// . - public static var analyticsPromptTermsEndNewUser: String { - return VectorL10n.tr("Vector", "analytics_prompt_terms_end_new_user") - } - /// . Is that OK? - public static var analyticsPromptTermsEndUpgrade: String { - return VectorL10n.tr("Vector", "analytics_prompt_terms_end_upgrade") - } /// here public static var analyticsPromptTermsLinkNewUser: String { return VectorL10n.tr("Vector", "analytics_prompt_terms_link_new_user") @@ -95,21 +63,13 @@ public class VectorL10n: NSObject { public static var analyticsPromptTermsLinkUpgrade: String { return VectorL10n.tr("Vector", "analytics_prompt_terms_link_upgrade") } - /// You can read all our terms here. - public static var analyticsPromptTermsNewUser: String { - return VectorL10n.tr("Vector", "analytics_prompt_terms_new_user") - } - /// You can read all our terms - public static var analyticsPromptTermsStartNewUser: String { - return VectorL10n.tr("Vector", "analytics_prompt_terms_start_new_user") - } - /// Read all our terms - public static var analyticsPromptTermsStartUpgrade: String { - return VectorL10n.tr("Vector", "analytics_prompt_terms_start_upgrade") + /// You can read all our terms %@. + public static func analyticsPromptTermsNewUser(_ p1: String) -> String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_new_user", p1) } - /// Read all our terms here. Is that OK? - public static var analyticsPromptTermsUpgrade: String { - return VectorL10n.tr("Vector", "analytics_prompt_terms_upgrade") + /// Read all our terms %@. Is that OK? + public static func analyticsPromptTermsUpgrade(_ p1: String) -> String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_upgrade", p1) } /// Help improve %@ public static func analyticsPromptTitle(_ p1: String) -> String { diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index 0222e2d7fe..cccd687904 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -17,29 +17,36 @@ import PostHog import AnalyticsEvents +/// A class responsible for managing an analytics client +/// and sending events through this client. @objcMembers class Analytics: NSObject { // MARK: - Properties + /// The singleton instance to be used within the Riot target. static let shared = Analytics() + /// The analytics client to send events with. private var client = PostHogAnalyticsClient() + /// Whether or not the object is enabled and sending events to the server. var isRunning: Bool { client.isRunning } + /// Whether the user has yet to opt in or out of analytics collection. var shouldShowAnalyticsPrompt: Bool { - // Show an analytics prompt when the user hasn't seen the PostHog prompt before - // so long as they haven't previously declined the Matomo analytics prompt. - !RiotSettings.shared.hasSeenAnalyticsPrompt && !RiotSettings.shared.hasDeclinedMatomoAnalytics + // Show an analytics prompt when the user hasn't seen the PostHog prompt before. + !RiotSettings.shared.hasSeenAnalyticsPrompt } + /// Indicates whether the user previously accepted Matomo analytics and should be shown the upgrade prompt. var promptShouldDisplayUpgradeMessage: Bool { - // Only show an upgrade prompt if the user previously accepted Matomo analytics. RiotSettings.shared.hasAcceptedMatomoAnalytics } // MARK: - Public + /// Opts in to analytics tracking with the supplied session. + /// - Parameter session: The session to use to when reading/generating the analytics ID. func optIn(with session: MXSession?) { guard let session = session else { return } RiotSettings.shared.enableAnalytics = true @@ -63,11 +70,13 @@ import AnalyticsEvents } } + /// Opts out of analytics tracking and calls `reset` to clear any IDs and event queues. func optOut() { RiotSettings.shared.enableAnalytics = false reset() } + /// Starts the analytics client if the user has opted in, otherwise does nothing. func startIfEnabled() { guard RiotSettings.shared.enableAnalytics, !isRunning else { return } @@ -83,17 +92,9 @@ import AnalyticsEvents MXLogger.setBuildVersion(AppDelegate.theDelegate().build) } - private func identify(with settings: AnalyticsSettings) { - guard let id = settings.id else { - MXLog.warning("[Analytics] identify(with:) called before an ID has been generated.") - return - } - - client.identify(id: id) - MXLog.debug("[Analytics] Identified.") - RiotSettings.shared.isIdentifiedForAnalytics = true - } - + /// Resets the any IDs and event queues in the analytics client. This method + /// can be called on sign-out to remember opt-in status, but ensure the next + /// account used isn't associated with the previous one. func reset() { guard isRunning else { return } @@ -105,19 +106,52 @@ import AnalyticsEvents MXLogger.logCrashes(false) } + /// Flushes the event queue in the analytics client, uploading all pending events. + /// Normally events are sent in batches. Call this method when you need an event + /// to be sent immediately. func forceUpload() { client.flush() } + // MARK: - Private + + /// Identify (pseudonymously) any future events with the ID from the analytics account data settings. + /// - Parameter settings: The settings to use for identification. The ID must be set *before* calling this method. + private func identify(with settings: AnalyticsSettings) { + guard let id = settings.id else { + MXLog.warning("[Analytics] identify(with:) called before an ID has been generated.") + return + } + + client.identify(id: id) + MXLog.debug("[Analytics] Identified.") + RiotSettings.shared.isIdentifiedForAnalytics = true + } + + /// Capture an event in the `client`. + /// - Parameter event: The event to capture. private func capture(event: AnalyticsEventProtocol) { client.capture(event) } - +} + +// MARK: - Public tracking methods +// The following methods are exposed for compatibility with Objective-C as +// the `capture` method and the generated events cannot be bridged from Swift. +extension Analytics { + /// Track the presentation of a screen + /// - Parameters: + /// - screen: The screen that was shown. + /// - milliseconds: An optional value representing how long the screen was shown for in milliseconds. func trackScreen(_ screen: AnalyticsScreen, duration milliseconds: Int?) { let event = AnalyticsEvent.Screen(durationMs: milliseconds, screenName: screen.screenName) client.screen(event) } + /// Track an E2EE error that occurred + /// - Parameters: + /// - reason: The error that occurred. + /// - count: The number of times that error occurred. func trackE2EEError(_ reason: DecryptionFailureReason, count: Int) { for _ in 0.. NSAttributedString { + // Do some sanitisation before finalizing the string +// let sanitizeCallback: DTHTMLAttributedStringBuilderWillFlushCallback = { element in +// element?.sanitize(with: ["b"], bodyFont: .systemFont(ofSize: UIFont.systemFontSize), imageHandler: nil) +// print("Hello") +// } + + let options: [String: Any] = [ + DTUseiOS6Attributes: true, // Enable it to be able to display the attributed string in a UITextView + DTDefaultLinkDecoration: false, +// DTWillFlushBlockCallBack: sanitizeCallback + ] + + guard let attributedString = NSAttributedString(htmlData: htmlString.data(using: .utf8), + options: options, + documentAttributes: nil) else { + return NSAttributedString(string: htmlString) + } + + return MXKTools.removeDTCoreTextArtifacts(attributedString) + } + + static func attach(_ link: String, to terms: String) -> NSAttributedString { + let baseString = NSMutableAttributedString(string: terms) + let linkRange = (baseString.string as NSString).range(of: "%@") + let formattedLink = NSAttributedString(string: VectorL10n.analyticsPromptTermsLinkNewUser, + attributes: [.analyticsPromptTermsTextLink: true]) + baseString.replaceCharacters(in: linkRange, with: formattedLink) + + return baseString + } +} + diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift index 3ac17376e7..572ff21679 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift @@ -43,7 +43,8 @@ enum MockAnalyticsPromptScreenState: MockScreenState, CaseIterable { case .promptType(let analyticsPromptType): promptType = analyticsPromptType } - let viewModel = AnalyticsPromptViewModel(promptType: promptType, appDisplayName: "Element") + let viewModel = AnalyticsPromptViewModel(promptType: promptType, + strings: MockAnalyticsPromptStrings()) return ( [promptType, viewModel], diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift new file mode 100644 index 0000000000..a7a3f6a5f9 --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift @@ -0,0 +1,52 @@ +// +// Copyright 2021 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 MockAnalyticsPromptStrings: AnalyticsPromptStringsProtocol { + var appDisplayName = "Element" + + let point1: NSAttributedString + let point2: NSAttributedString + + let termsNewUser: NSAttributedString + let termsUpgrade: NSAttributedString + + let shortString = NSAttributedString(string: "This is a short string.") + let longString = NSAttributedString(string: "This is a very long string that will be used to test the layout over multiple lines of text to ensure everything is correct.") + + init() { + let point1 = NSMutableAttributedString(string: "We ") + point1.append(NSAttributedString(string: "don't", attributes: [.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)])) + point1.append(NSAttributedString(string: " record or profile any account data")) + self.point1 = point1 + + let point2 = NSMutableAttributedString(string: "We ") + point2.append(NSAttributedString(string: "don't", attributes: [.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)])) + point2.append(NSAttributedString(string: " share information with third parties")) + self.point2 = point2 + + let termsNewUser = NSMutableAttributedString(string: "You can read all our terms ") + termsNewUser.append(NSAttributedString(string: "here", attributes: [.analyticsPromptTermsTextLink: true])) + termsNewUser.append(NSAttributedString(string: ".")) + self.termsNewUser = termsNewUser + + let termsUpgrade = NSMutableAttributedString(string: "Read all our terms ") + termsUpgrade.append(NSAttributedString(string: "here", attributes: [.analyticsPromptTermsTextLink: true])) + termsUpgrade.append(NSAttributedString(string: ". Is that OK?")) + self.termsUpgrade = termsUpgrade + } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift index de1789afc1..fecd0b2847 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift @@ -39,7 +39,7 @@ struct AnalyticsPrompt: View { VStack { Text("\(viewModel.viewState.promptType.description)\n") - AnalyticsPromptTermsText(promptType: viewModel.viewState.promptType) + AnalyticsPromptTermsText(attributedString: viewModel.viewState.promptType.termsStrings) .onTapGesture { viewModel.send(viewAction: .openTermsURL) } @@ -49,27 +49,9 @@ struct AnalyticsPrompt: View { /// The list of re-assurances about analytics. private var checkmarkList: some View { VStack(alignment: .leading) { - Label { - Text(VectorL10n.analyticsPromptPoint1Start) - + Text(VectorL10n.analyticsPromptPoint1BoldedDont).font(theme.fonts.bodySB) - + Text(VectorL10n.analyticsPromptPoint1End) - } icon: { - Image(uiImage: Asset.Images.analyticsCheckmark.image) - } - - Label { - Text(VectorL10n.analyticsPromptPoint2Start) - + Text(VectorL10n.analyticsPromptPoint2BoldedDont).font(theme.fonts.bodySB) - + Text(VectorL10n.analyticsPromptPoint2End) - } icon: { - Image(uiImage: Asset.Images.analyticsCheckmark.image) - } - - Label { - Text(VectorL10n.analyticsPromptPoint3) - } icon: { - Image(uiImage: Asset.Images.analyticsCheckmark.image) - } + AnalyticsPromptCheckmarkItem(attributedString: viewModel.viewState.strings.point1) + AnalyticsPromptCheckmarkItem(attributedString: viewModel.viewState.strings.point2) + AnalyticsPromptCheckmarkItem(string: VectorL10n.analyticsPromptPoint3) } .font(theme.fonts.body) } @@ -101,7 +83,7 @@ struct AnalyticsPrompt: View { Image(uiImage: Asset.Images.analyticsLogo.image) .padding(.bottom, 25) - Text(VectorL10n.analyticsPromptTitle(viewModel.viewState.appDisplayName)) + Text(VectorL10n.analyticsPromptTitle(viewModel.viewState.strings.appDisplayName)) .font(theme.fonts.title2B) .foregroundColor(theme.colors.primaryContent) .padding(.bottom, 2) @@ -116,6 +98,7 @@ struct AnalyticsPrompt: View { checkmarkList .foregroundColor(theme.colors.secondaryContent) + .padding(.bottom, 16) } .padding(.top, 50) .padding(.horizontal, 16) diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptCheckmarkItem.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptCheckmarkItem.swift new file mode 100644 index 0000000000..9019e9033e --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptCheckmarkItem.swift @@ -0,0 +1,92 @@ +// +// Copyright 2021 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 + +@available(iOS 14.0, *) +struct AnalyticsPromptCheckmarkItem: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme + + /// A string with a bold property. + private struct StringComponent { + let string: String + let isBold: Bool + } + + /// Internal representation of the string as composable parts. + private let components: [StringComponent] + + // MARK: - Setup + + init(attributedString: NSAttributedString) { + var components = [StringComponent]() + let range = NSRange(location: 0, length: attributedString.length) + let string = attributedString.string as NSString + + attributedString.enumerateAttributes(in: range, options: []) { attributes, range, stop in + var isBold = false + + if let font = attributes[.font] as? UIFont { + isBold = font.fontDescriptor.symbolicTraits.contains(.traitBold) + } + + components.append(StringComponent(string: string.substring(with: range), isBold: isBold)) + } + + self.components = components + } + + init(string: String) { + self.components = [StringComponent(string: string, isBold: false)] + } + + // MARK: - Views + + var label: Text { + components.reduce(Text("")) { + $0 + Text($1.string).font($1.isBold ? theme.fonts.bodySB : theme.fonts.body) + } + } + + var body: some View { + Label { label } icon: { + Image(uiImage: Asset.Images.analyticsCheckmark.image) + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct AnalyticsPromptCheckmarkItem_Previews: PreviewProvider { + + static let strings = MockAnalyticsPromptStrings() + + static var previews: some View { + VStack(alignment:.leading) { + AnalyticsPromptCheckmarkItem(attributedString: strings.point1) + AnalyticsPromptCheckmarkItem(attributedString: strings.point2) + AnalyticsPromptCheckmarkItem(attributedString: strings.longString) + AnalyticsPromptCheckmarkItem(attributedString: strings.shortString) + } + .padding() + } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptTermsText.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptTermsText.swift index ec32deddab..0a3ab40c22 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptTermsText.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptTermsText.swift @@ -26,17 +26,49 @@ struct AnalyticsPromptTermsText: View { @Environment(\.theme) private var theme - // MARK: Public + /// A string with a link attribute. + private struct StringComponent { + let string: String + let isLink: Bool + } - let promptType: AnalyticsPromptType + /// Internal representation of the string as composable parts. + private let components: [StringComponent] - // MARK: Views + // MARK: - Setup - var body: some View { - let (start, link, end) = promptType.termsStrings + init(attributedString: NSAttributedString) { + var components = [StringComponent]() + let range = NSRange(location: 0, length: attributedString.length) + let string = attributedString.string as NSString - Text(start) - + Text(link).foregroundColor(theme.colors.accent) - + Text(end) + attributedString.enumerateAttributes(in: range, options: []) { attributes, range, stop in + let isLink = attributes.keys.contains(.analyticsPromptTermsTextLink) + components.append(StringComponent(string: string.substring(with: range), isLink: isLink)) + } + + self.components = components + } + + // MARK: - Views + + var body: some View { + components.reduce(Text("")) { + $0 + Text($1.string).foregroundColor($1.isLink ? theme.colors.accent : nil) + } + } +} + +// MARK: - Previews +@available(iOS 14.0, *) +struct AnalyticsPromptTermsText_Previews: PreviewProvider { + + static let strings = MockAnalyticsPromptStrings() + + static var previews: some View { + VStack(spacing: 8) { + AnalyticsPromptTermsText(attributedString: strings.termsNewUser) + AnalyticsPromptTermsText(attributedString: strings.termsUpgrade) + } } } diff --git a/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift b/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift index c0dd43d838..5824cbc85c 100644 --- a/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift +++ b/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift @@ -37,9 +37,9 @@ struct PrimaryActionButtonStyle: ButtonStyle { func backgroundColor(_ isPressed: Bool) -> Color { if let customColor = customColor { return customColor - } else { - return isPressed ? theme.colors.accent.opacity(0.6) : theme.colors.accent } + + return isPressed ? theme.colors.accent.opacity(0.6) : theme.colors.accent } } From 9d4a1d96d6d5b6758fc1c895c0fceb6b9b7ab213 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Dec 2021 12:29:02 +0000 Subject: [PATCH 022/109] Migrate doug/5035_posthog from MatrixKit. --- Riot/Managers/Analytics/Analytics.swift | 16 +++++---- .../MatrixKit/Utils/MXKAnalyticsConstants.h | 33 ------------------- Riot/Modules/MatrixKit/Utils/MXKTools.m | 5 +-- .../Modal/ServiceTermsModalCoordinator.swift | 6 ++-- Riot/SupportingFiles/Riot-Bridging-Header.h | 1 - 5 files changed, 13 insertions(+), 48 deletions(-) delete mode 100644 Riot/Modules/MatrixKit/Utils/MXKAnalyticsConstants.h diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index cccd687904..6b03cbb5fa 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -161,8 +161,15 @@ extension Analytics { /// Track whether the user accepted or declined the terms to an identity server. /// **Note** This method isn't currently implemented. - /// - Parameter granted: Pass `true` for accepted and `false` for declined. - func trackIdentityServerAccepted(granted: Bool) { + /// - Parameter accepted: Whether the terms were accepted. + func trackIdentityServerAccepted(_ accepted: Bool) { + // Do we still want to track this? + } + + /// Track whether the user granted or rejected access to the device contacts. + /// **Note** This method isn't currently implemented. + /// - Parameter granted: Whether access was granted. + func trackContactsAccessGranted(_ granted: Bool) { // Do we still want to track this? } } @@ -196,11 +203,6 @@ extension Analytics: MXAnalyticsDelegate { capture(event: event) } - /// **Note** This method isn't currently implemented. - func trackContactsAccessGranted(_ granted: Bool) { - // Do we still want to track this? - } - func trackCreatedRoom(asDM isDM: Bool) { let event = AnalyticsEvent.CreatedRoom(isDM: isDM) capture(event: event) diff --git a/Riot/Modules/MatrixKit/Utils/MXKAnalyticsConstants.h b/Riot/Modules/MatrixKit/Utils/MXKAnalyticsConstants.h deleted file mode 100644 index 97d3c25f2c..0000000000 --- a/Riot/Modules/MatrixKit/Utils/MXKAnalyticsConstants.h +++ /dev/null @@ -1,33 +0,0 @@ -// -// Copyright 2020 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 - - -typedef NSString *const MXKAnalyticsCategory NS_TYPED_EXTENSIBLE_ENUM; - -/** - The analytics category for local contacts. - */ -static MXKAnalyticsCategory const MXKAnalyticsCategoryContacts = @"localContacts"; - - -typedef NSString *const MXKAnalyticsName NS_TYPED_EXTENSIBLE_ENUM; - -/** - The analytics value for accept/decline of local contacts access. - */ -static MXKAnalyticsName const MXKAnalyticsNameContactsAccessGranted = @"accessGranted"; diff --git a/Riot/Modules/MatrixKit/Utils/MXKTools.m b/Riot/Modules/MatrixKit/Utils/MXKTools.m index 76753eed53..a6271fae0a 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKTools.m +++ b/Riot/Modules/MatrixKit/Utils/MXKTools.m @@ -27,7 +27,6 @@ #import "MXKAppSettings.h" #import #import "MXKSwiftHeader.h" -#import "MXKAnalyticsConstants.h" #pragma mark - Constants definitions @@ -884,9 +883,7 @@ + (void)checkAccessForContacts:(NSString *)manualChangeTitle // Request address book access [[CNContactStore new] requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) { - [MXSDKOptions.sharedInstance.analyticsDelegate trackValue:[NSNumber numberWithBool:granted] - category:MXKAnalyticsCategoryContacts - name:MXKAnalyticsNameContactsAccessGranted]; + [Analytics.shared trackContactsAccessGranted:granted]; dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift index 9dc3c59206..b4c04c610e 100644 --- a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift +++ b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift @@ -107,7 +107,7 @@ extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelega func serviceTermsModalScreenCoordinatorDidAccept(_ coordinator: ServiceTermsModalScreenCoordinatorType) { if serviceTerms.serviceType == MXServiceTypeIdentityService { - Analytics.shared.trackIdentityServerAccepted(granted: true) + Analytics.shared.trackIdentityServerAccepted(true) } self.delegate?.serviceTermsModalCoordinatorDidAccept(self) @@ -119,7 +119,7 @@ extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelega func serviceTermsModalScreenCoordinatorDidDecline(_ coordinator: ServiceTermsModalScreenCoordinatorType) { if serviceTerms.serviceType == MXServiceTypeIdentityService { - Analytics.shared.trackIdentityServerAccepted(granted: false) + Analytics.shared.trackIdentityServerAccepted(false) disableIdentityServer() } @@ -131,7 +131,7 @@ extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelega extension ServiceTermsModalCoordinator: UIAdaptivePresentationControllerDelegate { func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { if serviceTerms.serviceType == MXServiceTypeIdentityService { - Analytics.shared.trackIdentityServerAccepted(granted: false) + Analytics.shared.trackIdentityServerAccepted(false) } self.delegate?.serviceTermsModalCoordinatorDidDismissInteractively(self) diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index da79f11a3d..a899115f3b 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -63,4 +63,3 @@ #import "MXKRoomDataSourceManager.h" #import "MXRoom+Sync.h" #import "UIAlertController+MatrixKit.h" -#import "MXKAnalyticsConstants.h" From a2aa01f06ce6f101f283c81942d007d2f69c38ad Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Dec 2021 15:08:48 +0000 Subject: [PATCH 023/109] Move string formatting to Tools. Revert contacts tracking from MatrixKit. Final tweaks before PR. --- .gitignore | 3 -- Config/BuildSettings.swift | 6 ++- Gemfile.lock | 2 +- Podfile | 5 +- Podfile.lock | 48 ++++++++--------- Riot/Managers/Analytics/Analytics.swift | 12 ++--- ...ions.swift => AnalyticsEventHelpers.swift} | 0 .../Analytics/DictionaryConvertable.swift | 44 --------------- .../Analytics/PostHogAnalyticsClient.swift | 1 - Riot/Managers/Settings/RiotSettings.swift | 8 +-- Riot/Modules/Application/LegacyAppDelegate.m | 2 +- Riot/Modules/MatrixKit/Utils/MXKTools.m | 2 +- Riot/Utils/Tools.h | 10 ++++ Riot/Utils/Tools.m | 37 +++++++++++++ Riot/Utils/Tools.swift | 35 ++++++++++++ .../AnalyticsPromptModels.swift | 4 -- .../AnalyticsPromptViewModel.swift | 8 +-- .../AnalyticsPromptCoordinator.swift | 2 +- .../Coordinator/AnalyticsPromptStrings.swift | 54 +++---------------- .../MockAnalyticsPromptScreenState.swift | 3 +- .../MockAnalyticsPromptStrings.swift | 4 +- .../View/AnalyticsPromptTermsText.swift | 2 +- changelog.d/5035.change | 1 + 23 files changed, 142 insertions(+), 151 deletions(-) rename Riot/Managers/Analytics/{EventExtensions.swift => AnalyticsEventHelpers.swift} (100%) delete mode 100644 Riot/Managers/Analytics/DictionaryConvertable.swift create mode 100644 Riot/Utils/Tools.swift create mode 100644 changelog.d/5035.change diff --git a/.gitignore b/.gitignore index f4e7a1f10f..695d4cd613 100644 --- a/.gitignore +++ b/.gitignore @@ -29,9 +29,6 @@ vendor/ # Pods/ -# Never commit auto-generated secrets even if pods are checked in -Pods/CocoaPodsKeys/ - ## Ignore project files as we generate them with xcodegen (https://github.com/yonaskolb/XcodeGen) *.xcodeproj *.xcworkspace diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index c9ab63d851..9e4a5a1370 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -166,10 +166,12 @@ final class BuildSettings: NSObject { // MARK: - Analytics #warning("Testing environment.") - // Optional host for PostHog analytics. Set to nil to disable analytics. + /// Host to use for PostHog analytics. Set to nil to disable analytics. static let analyticsHost: String? = "https://posthog-poc.lab.element.dev" - // Public key for submitting analytics. Set to nil to disable analytics. + /// Public key for submitting analytics. Set to nil to disable analytics. static let analyticsKey: String? = "rs-pJjsYJTuAkXJfhaMmPUNBhWliDyTKLOOxike6ck8" + /// The URL to open with more information about analytics terms. + static let analyticsTermsURL = URL(string: "https://element.io/cookie-policy")! // MARK: - Bug report diff --git a/Gemfile.lock b/Gemfile.lock index ca9078e41f..a8674758c3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -299,4 +299,4 @@ DEPENDENCIES xcode-install BUNDLED WITH - 2.2.31 + 2.2.28 diff --git a/Podfile b/Podfile index 0cdf3544d3..7141544611 100644 --- a/Podfile +++ b/Podfile @@ -69,8 +69,8 @@ abstract_target 'RiotPods' do # PostHog for analytics pod 'PostHog', '~> 1.4.4' - # pod 'AnalyticsEvents', :git => 'https://github.com/matrix-org/matrix-analytics-events.git', :branch => 'release/swift' - pod 'AnalyticsEvents', :path => '../matrix-analytics-events/AnalyticsEvents.podspec' + pod 'AnalyticsEvents', :git => 'https://github.com/matrix-org/matrix-analytics-events.git', :branch => 'release/swift' + # pod 'AnalyticsEvents', :path => '../matrix-analytics-events/AnalyticsEvents.podspec' # Remove warnings from "bad" pods pod 'OLMKit', :inhibit_warnings => true @@ -129,6 +129,7 @@ abstract_target 'RiotPods' do end + post_install do |installer| installer.pods_project.targets.each do |target| diff --git a/Podfile.lock b/Podfile.lock index b378f9fbe8..afe3025d73 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -57,29 +57,16 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixKit (0.16.10): - - Down (~> 0.11.0) - - DTCoreText (~> 1.6.25) - - HPGrowingTextView (~> 1.1) - - libPhoneNumber-iOS (~> 0.9.13) - - MatrixKit/Core (= 0.16.10) - - MatrixSDK (= 0.20.10) - - MatrixKit/Core (0.16.10): - - Down (~> 0.11.0) - - DTCoreText (~> 1.6.25) - - HPGrowingTextView (~> 1.1) - - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.20.10) - - MatrixSDK (0.20.10): - - MatrixSDK/Core (= 0.20.10) - - MatrixSDK/Core (0.20.10): + - MatrixSDK (0.20.13): + - MatrixSDK/Core (= 0.20.13) + - MatrixSDK/Core (0.20.13): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - OLMKit (~> 3.2.5) - Realm (= 10.16.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.20.10): + - MatrixSDK/JingleCallStack (0.20.13): - JitsiMeetSDK (= 3.10.2) - MatrixSDK/Core - OLMKit (3.2.5): @@ -115,19 +102,22 @@ PODS: - ZXingObjC/All (3.6.5) DEPENDENCIES: - - AnalyticsEvents (from `../matrix-analytics-events/AnalyticsEvents.podspec`) + - AnalyticsEvents (from `https://github.com/matrix-org/matrix-analytics-events.git`, branch `release/swift`) - DGCollectionViewLeftAlignFlowLayout (~> 1.0.4) + - Down (~> 0.11.0) - DSWaveformImage (~> 6.1.1) + - DTCoreText (~> 1.6.25) - ffmpeg-kit-ios-audio (~> 4.5) - FLEX (~> 4.5.0) - FlowCommoniOS (~> 1.12.0) - GBDeviceInfo (~> 6.6.0) + - HPGrowingTextView (~> 1.1) - Introspect (~> 0.1) - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - - MatrixKit (= 0.16.10) - - MatrixSDK - - MatrixSDK/JingleCallStack + - libPhoneNumber-iOS (~> 0.9.13) + - MatrixSDK (= 0.20.13) + - MatrixSDK/JingleCallStack (= 0.20.13) - OLMKit - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) @@ -167,7 +157,6 @@ SPEC REPOS: - libPhoneNumber-iOS - LoggerAPI - Logging - - MatrixKit - MatrixSDK - OLMKit - PostHog @@ -186,11 +175,17 @@ SPEC REPOS: EXTERNAL SOURCES: AnalyticsEvents: - :path: "../matrix-analytics-events/AnalyticsEvents.podspec" + :branch: release/swift + :git: https://github.com/matrix-org/matrix-analytics-events.git + +CHECKOUT OPTIONS: + AnalyticsEvents: + :commit: aac06956d45cb86ea2bbd7a21b20b14ba8899fcf + :git: https://github.com/matrix-org/matrix-analytics-events.git SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce - AnalyticsEvents: 27b0d074e839d2d354d12ae679930e373cba5f45 + AnalyticsEvents: 333bf47d67dc628fadd29ce887b7ac93d8bd6e05 BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24 BlueECC: 0d18e93347d3ec6d41416de21c1ffa4d4cd3c2cc BlueRSA: dfeef51db96bcc4edec654956c1581adbda4e6a3 @@ -214,8 +209,7 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixKit: c3f0bb056ceeb015e2f1688543ac4dbcf88bef2f - MatrixSDK: 0e2ed8fc6f004cac4b4ab46f038a86fe49ce4007 + MatrixSDK: 945f082654830d7ae3a6e1e068b6dc22b2eae932 OLMKit: 9fb4799c4a044dd2c06bda31ec31a12191ad30b5 PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d @@ -231,6 +225,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 9819df47b1ebbcea325ba9f24baa92878f3d0efe +PODFILE CHECKSUM: f4ad67860350a28588e177245d1d0aff0fdcf186 COCOAPODS: 1.11.2 diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index 6b03cbb5fa..8378692547 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -165,13 +165,6 @@ extension Analytics { func trackIdentityServerAccepted(_ accepted: Bool) { // Do we still want to track this? } - - /// Track whether the user granted or rejected access to the device contacts. - /// **Note** This method isn't currently implemented. - /// - Parameter granted: Whether access was granted. - func trackContactsAccessGranted(_ granted: Bool) { - // Do we still want to track this? - } } // MARK: - MXAnalyticsDelegate @@ -217,4 +210,9 @@ extension Analytics: MXAnalyticsDelegate { let event = AnalyticsEvent.JoinedRoom(isDM: isDM, roomSize: roomSize) capture(event: event) } + + /// **Note** This method isn't currently implemented. + func trackContactsAccessGranted(_ granted: Bool) { + // Do we still want to track this? + } } diff --git a/Riot/Managers/Analytics/EventExtensions.swift b/Riot/Managers/Analytics/AnalyticsEventHelpers.swift similarity index 100% rename from Riot/Managers/Analytics/EventExtensions.swift rename to Riot/Managers/Analytics/AnalyticsEventHelpers.swift diff --git a/Riot/Managers/Analytics/DictionaryConvertable.swift b/Riot/Managers/Analytics/DictionaryConvertable.swift deleted file mode 100644 index 403dd91d71..0000000000 --- a/Riot/Managers/Analytics/DictionaryConvertable.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// Copyright 2021 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 DictionaryConvertible: Encodable { - var dictionary: [String: Any] { get } -} - -extension DictionaryConvertible { - var dictionary: [String: Any] { - let mirror = Mirror(reflecting: self) - let dict: [String: Any] = Dictionary(uniqueKeysWithValues: mirror.children.compactMap { (label: String?, value: Any) in - guard let label = label else { return nil } - - // Handle standard types such as String/Int/Bool - if let value = value as? NSCoding { - return (label, value) - } - - // AnalyticsEvent enums - if let value = value as? CustomStringConvertible { - return (label, value.description) - } - - return nil - }) - - return dict - } -} diff --git a/Riot/Managers/Analytics/PostHogAnalyticsClient.swift b/Riot/Managers/Analytics/PostHogAnalyticsClient.swift index ae9ee09af6..0552a459c2 100644 --- a/Riot/Managers/Analytics/PostHogAnalyticsClient.swift +++ b/Riot/Managers/Analytics/PostHogAnalyticsClient.swift @@ -59,5 +59,4 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol { postHog?.screen(event.screenName.rawValue, properties: event.properties) } - } diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 13a9a7b676..3aa22a8427 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -101,14 +101,12 @@ final class RiotSettings: NSObject { // MARK: Other - /// Whether the user has both seen the Matomo analytics prompt and declined it. - /// This is used to prevent users who previously opted out from being asked again. + /// Whether the user was previously shown the Matomo analytics prompt. var hasSeenAnalyticsPrompt: Bool { RiotSettings.defaults.object(forKey: UserDefaultsKeys.enableAnalytics) != nil } /// Whether the user has both seen the Matomo analytics prompt and declined it. - /// This is used to prevent users who previously opted out from being asked again. var hasDeclinedMatomoAnalytics: Bool { RiotSettings.defaults.object(forKey: UserDefaultsKeys.matomoAnalytics) != nil && !RiotSettings.defaults.bool(forKey: UserDefaultsKeys.matomoAnalytics) } @@ -119,11 +117,13 @@ final class RiotSettings: NSObject { RiotSettings.defaults.bool(forKey: UserDefaultsKeys.matomoAnalytics) } - /// Indicates if the device has already called identify for this session to PostHog. + /// `true` when the user has opted in to send analytics. @UserDefault(key: UserDefaultsKeys.enableAnalytics, defaultValue: false, storage: defaults) var enableAnalytics /// Indicates if the device has already called identify for this session to PostHog. + /// This is separate to `enableAnalytics` as logging out will leave analytics + /// enabled but reset identification. @UserDefault(key: "isIdentifiedForAnalytics", defaultValue: false, storage: defaults) var isIdentifiedForAnalytics diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index cf13618d56..6f6543e944 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -648,7 +648,7 @@ - (void)afterAppUnlockedByPin:(UIApplication *)application MXLogDebug(@"[AppDelegate] afterAppUnlockedByPin"); // Check if there is crash log to send - if (Analytics.shared.isRunning) + if (RiotSettings.shared.enableAnalytics) { [self checkExceptionToReport]; } diff --git a/Riot/Modules/MatrixKit/Utils/MXKTools.m b/Riot/Modules/MatrixKit/Utils/MXKTools.m index a6271fae0a..c941fbc985 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKTools.m +++ b/Riot/Modules/MatrixKit/Utils/MXKTools.m @@ -883,7 +883,7 @@ + (void)checkAccessForContacts:(NSString *)manualChangeTitle // Request address book access [[CNContactStore new] requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) { - [Analytics.shared trackContactsAccessGranted:granted]; + [MXSDKOptions.sharedInstance.analyticsDelegate trackContactsAccessGranted:granted]; dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/Riot/Utils/Tools.h b/Riot/Utils/Tools.h index 2d411a716b..59fabc9e57 100644 --- a/Riot/Utils/Tools.h +++ b/Riot/Utils/Tools.h @@ -59,4 +59,14 @@ */ + (NSAttributedString *)setTextColorAlpha:(CGFloat)alpha inAttributedString:(NSAttributedString*)attributedString; +/** Builds an attributed string from a string containing html. + @param htmlString The html string to use. + @param allowedTags The html tags that should be allowed. + + Note: It is recommended to include "p" and "body" tags in + `allowedTags` as these are often added when parsing. + */ ++ (NSAttributedString * _Nonnull)attributedStringFromHTML:(NSString * _Nonnull)htmlString + withAllowedTags:(NSArray * _Nonnull)allowedTags; + @end diff --git a/Riot/Utils/Tools.m b/Riot/Utils/Tools.m index f27ab4c334..1c111e4467 100644 --- a/Riot/Utils/Tools.m +++ b/Riot/Utils/Tools.m @@ -138,4 +138,41 @@ + (NSAttributedString *)setTextColorAlpha:(CGFloat)alpha inAttributedString:(NSA return string; } ++ (NSAttributedString *)attributedStringFromHTML:(NSString *)htmlString withAllowedTags:(NSArray *)allowedTags +{ + UIFont *font = [UIFont systemFontOfSize:[UIFont systemFontSize]]; + + // Do some sanitisation before finalizing the string + DTHTMLAttributedStringBuilderWillFlushCallback sanitizeCallback = ^(DTHTMLElement *element) { + [element sanitizeWith:allowedTags bodyFont:font imageHandler:nil]; + }; + + NSDictionary *options = @{ + DTUseiOS6Attributes: @(YES), // Enable it to be able to display the attributed string in a UITextView + DTDefaultFontFamily: font.familyName, + DTDefaultFontName: font.fontName, + DTDefaultFontSize: @(font.pointSize), + DTDefaultLinkDecoration: @(NO), + DTWillFlushBlockCallBack: sanitizeCallback + }; + + // 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. + NSAttributedString *string = [[NSAttributedString alloc] initWithHTMLData:[htmlString dataUsingEncoding:NSUTF8StringEncoding] options:options documentAttributes:NULL]; + + // Apply additional treatments + string = [MXKTools removeDTCoreTextArtifacts:string]; + + if (!string) { + return [[NSAttributedString alloc] initWithString:htmlString]; + } + + return string; +} + @end diff --git a/Riot/Utils/Tools.swift b/Riot/Utils/Tools.swift new file mode 100644 index 0000000000..ea34f30c8e --- /dev/null +++ b/Riot/Utils/Tools.swift @@ -0,0 +1,35 @@ +// +// Copyright 2021 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 Tools { + /// Builds an attributed string by replacing a `%@` placeholder with the supplied link text and URL. + /// - Parameters: + /// - string: The string to be formatted. + /// - link: The link text to be inserted. + /// - url: The URL to be linked to. + /// - Returns: An attributed string. + static func format(_ string: String, with link: String, using url: URL) -> NSAttributedString { + let baseString = NSMutableAttributedString(string: string) + let attributedLink = NSAttributedString(string: link, attributes: [.link: url]) + + let linkRange = (baseString.string as NSString).range(of: "%@") + baseString.replaceCharacters(in: linkRange, with: attributedLink) + + return baseString + } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift index b5a2f35f91..656558b4da 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift @@ -121,7 +121,3 @@ extension AnalyticsPromptType: Identifiable { } } } - -extension NSAttributedString.Key { - static let analyticsPromptTermsTextLink = NSAttributedString.Key("TermsTextLink") -} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift index 8f2c734521..999e2a95f6 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift @@ -29,6 +29,8 @@ class AnalyticsPromptViewModel: AnalyticsPromptViewModelType { // MARK: - Properties // MARK: Private + + let termsURL: URL // MARK: Public @@ -37,7 +39,8 @@ class AnalyticsPromptViewModel: AnalyticsPromptViewModelType { // MARK: - Setup /// Initialize a view model with the specified prompt type and app display name. - init(promptType: AnalyticsPromptType, strings: AnalyticsPromptStringsProtocol) { + init(promptType: AnalyticsPromptType, strings: AnalyticsPromptStringsProtocol, termsURL: URL) { + self.termsURL = termsURL super.init(initialViewState: AnalyticsPromptViewState(promptType: promptType, strings: strings)) } @@ -70,7 +73,6 @@ class AnalyticsPromptViewModel: AnalyticsPromptViewModelType { /// Open the service terms link. private func openTermsURL() { - guard let url = URL(string: "https://element.io/cookie-policy") else { return } - UIApplication.shared.open(url) + UIApplication.shared.open(termsURL) } } diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift index 96ca508764..a85fd8d44d 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift @@ -61,7 +61,7 @@ final class AnalyticsPromptCoordinator: Coordinator { promptType = .newUser(termsString: strings.termsNewUser) } - let viewModel = AnalyticsPromptViewModel(promptType: promptType, strings: strings) + let viewModel = AnalyticsPromptViewModel(promptType: promptType, strings: strings, termsURL: BuildSettings.analyticsTermsURL) let view = AnalyticsPrompt(viewModel: viewModel.context) _analyticsPromptViewModel = viewModel diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift index 8c1ba13110..e0a09f5520 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift @@ -20,52 +20,14 @@ import DTCoreText struct AnalyticsPromptStrings: AnalyticsPromptStringsProtocol { let appDisplayName = AppInfo.current.displayName - let point1: NSAttributedString - let point2: NSAttributedString + let point1 = Tools.attributedString(fromHTML: VectorL10n.analyticsPromptPoint1, withAllowedTags: ["b", "p"]) + let point2 = Tools.attributedString(fromHTML: VectorL10n.analyticsPromptPoint2, withAllowedTags: ["b", "p"]) - let termsNewUser: NSAttributedString - let termsUpgrade: NSAttributedString - - init() { - self.point1 = Self.parse(VectorL10n.analyticsPromptPoint1) - self.point2 = Self.parse(VectorL10n.analyticsPromptPoint2) - - self.termsNewUser = Self.attach(VectorL10n.analyticsPromptTermsLinkNewUser, - to: VectorL10n.analyticsPromptTermsNewUser("%@")) - self.termsUpgrade = Self.attach(VectorL10n.analyticsPromptTermsLinkUpgrade, - to: VectorL10n.analyticsPromptTermsUpgrade("%@")) - } - - static func parse(_ htmlString: String) -> NSAttributedString { - // Do some sanitisation before finalizing the string -// let sanitizeCallback: DTHTMLAttributedStringBuilderWillFlushCallback = { element in -// element?.sanitize(with: ["b"], bodyFont: .systemFont(ofSize: UIFont.systemFontSize), imageHandler: nil) -// print("Hello") -// } - - let options: [String: Any] = [ - DTUseiOS6Attributes: true, // Enable it to be able to display the attributed string in a UITextView - DTDefaultLinkDecoration: false, -// DTWillFlushBlockCallBack: sanitizeCallback - ] - - guard let attributedString = NSAttributedString(htmlData: htmlString.data(using: .utf8), - options: options, - documentAttributes: nil) else { - return NSAttributedString(string: htmlString) - } - - return MXKTools.removeDTCoreTextArtifacts(attributedString) - } - - static func attach(_ link: String, to terms: String) -> NSAttributedString { - let baseString = NSMutableAttributedString(string: terms) - let linkRange = (baseString.string as NSString).range(of: "%@") - let formattedLink = NSAttributedString(string: VectorL10n.analyticsPromptTermsLinkNewUser, - attributes: [.analyticsPromptTermsTextLink: true]) - baseString.replaceCharacters(in: linkRange, with: formattedLink) - - return baseString - } + let termsNewUser = Tools.format(VectorL10n.analyticsPromptTermsNewUser("%@"), + with: VectorL10n.analyticsPromptTermsLinkNewUser, + using: BuildSettings.analyticsTermsURL) + let termsUpgrade = Tools.format(VectorL10n.analyticsPromptTermsUpgrade("%@"), + with: VectorL10n.analyticsPromptTermsLinkUpgrade, + using: BuildSettings.analyticsTermsURL) } diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift index 572ff21679..9c303bbbe1 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift @@ -44,7 +44,8 @@ enum MockAnalyticsPromptScreenState: MockScreenState, CaseIterable { promptType = analyticsPromptType } let viewModel = AnalyticsPromptViewModel(promptType: promptType, - strings: MockAnalyticsPromptStrings()) + strings: MockAnalyticsPromptStrings(), + termsURL: URL(string: "https://element.io/cookie-policy")!) return ( [promptType, viewModel], diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift index a7a3f6a5f9..0e2dba938d 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift @@ -40,12 +40,12 @@ struct MockAnalyticsPromptStrings: AnalyticsPromptStringsProtocol { self.point2 = point2 let termsNewUser = NSMutableAttributedString(string: "You can read all our terms ") - termsNewUser.append(NSAttributedString(string: "here", attributes: [.analyticsPromptTermsTextLink: true])) + termsNewUser.append(NSAttributedString(string: "here", attributes: [.link: URL(string: "https://element.io/cookie-policy")!])) termsNewUser.append(NSAttributedString(string: ".")) self.termsNewUser = termsNewUser let termsUpgrade = NSMutableAttributedString(string: "Read all our terms ") - termsUpgrade.append(NSAttributedString(string: "here", attributes: [.analyticsPromptTermsTextLink: true])) + termsUpgrade.append(NSAttributedString(string: "here", attributes: [.link: URL(string: "https://element.io/cookie-policy")!])) termsUpgrade.append(NSAttributedString(string: ". Is that OK?")) self.termsUpgrade = termsUpgrade } diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptTermsText.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptTermsText.swift index 0a3ab40c22..7616e10848 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptTermsText.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptTermsText.swift @@ -43,7 +43,7 @@ struct AnalyticsPromptTermsText: View { let string = attributedString.string as NSString attributedString.enumerateAttributes(in: range, options: []) { attributes, range, stop in - let isLink = attributes.keys.contains(.analyticsPromptTermsTextLink) + let isLink = attributes.keys.contains(.link) components.append(StringComponent(string: string.substring(with: range), isLink: isLink)) } diff --git a/changelog.d/5035.change b/changelog.d/5035.change new file mode 100644 index 0000000000..6be81ff048 --- /dev/null +++ b/changelog.d/5035.change @@ -0,0 +1 @@ +Analytics: Replace Matomo with PostHog. \ No newline at end of file From 59486c824a6f23add3bbd99d2c183854fdd9ec8a Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 8 Dec 2021 12:00:23 +0000 Subject: [PATCH 024/109] Add accessibility labels/hints. Fix tests. Show analytics prompt to everyone. --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++++ Riot/Managers/Analytics/Analytics.swift | 6 +++--- .../MockAnalyticsPromptStrings.swift | 2 +- .../AnalyticsPrompt/View/AnalyticsPrompt.swift | 8 ++++++++ RiotTests/AnalyticsTests.swift | 16 +++++++++------- 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 17ecfa6a22..f513009c66 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -78,6 +78,7 @@ // Accessibility "accessibility_checkbox_label" = "checkbox"; +"accessibility_button_label" = "button"; // Authentication "auth_login" = "Log in"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index b8546ccdab..d47bdfb995 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -15,6 +15,10 @@ public class VectorL10n: NSObject { public static var accept: String { return VectorL10n.tr("Vector", "accept") } + /// button + public static var accessibilityButtonLabel: String { + return VectorL10n.tr("Vector", "accessibility_button_label") + } /// checkbox public static var accessibilityCheckboxLabel: String { return VectorL10n.tr("Vector", "accessibility_checkbox_label") diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index 8378692547..617f7fec43 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -32,10 +32,10 @@ import AnalyticsEvents /// Whether or not the object is enabled and sending events to the server. var isRunning: Bool { client.isRunning } - /// Whether the user has yet to opt in or out of analytics collection. + /// Whether to show the user the analytics opt in prompt. var shouldShowAnalyticsPrompt: Bool { - // Show an analytics prompt when the user hasn't seen the PostHog prompt before. - !RiotSettings.shared.hasSeenAnalyticsPrompt + // Only show the prompt once, and when analytics are configured in BuildSettings. + !RiotSettings.shared.hasSeenAnalyticsPrompt && PHGPostHogConfiguration.standard != nil } /// Indicates whether the user previously accepted Matomo analytics and should be shown the upgrade prompt. diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift index 0e2dba938d..ee0e59ed04 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift @@ -14,7 +14,7 @@ // limitations under the License. // -import Foundation +import UIKit struct MockAnalyticsPromptStrings: AnalyticsPromptStringsProtocol { var appDisplayName = "Element" diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift index fecd0b2847..a5a9635ac5 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift @@ -40,6 +40,8 @@ struct AnalyticsPrompt: View { Text("\(viewModel.viewState.promptType.description)\n") AnalyticsPromptTermsText(attributedString: viewModel.viewState.promptType.termsStrings) + .accessibilityLabel(Text(viewModel.viewState.promptType.termsStrings.string)) + .accessibilityValue(Text(VectorL10n.accessibilityButtonLabel)) .onTapGesture { viewModel.send(viewAction: .openTermsURL) } @@ -50,10 +52,15 @@ struct AnalyticsPrompt: View { private var checkmarkList: some View { VStack(alignment: .leading) { AnalyticsPromptCheckmarkItem(attributedString: viewModel.viewState.strings.point1) + .accessibilityLabel(Text(viewModel.viewState.strings.point1.string)) + AnalyticsPromptCheckmarkItem(attributedString: viewModel.viewState.strings.point2) + .accessibilityLabel(Text(viewModel.viewState.strings.point2.string)) + AnalyticsPromptCheckmarkItem(string: VectorL10n.analyticsPromptPoint3) } .font(theme.fonts.body) + .frame(maxWidth: .infinity) } /// The stack of enable/disable buttons. @@ -89,6 +96,7 @@ struct AnalyticsPrompt: View { .padding(.bottom, 2) descriptionText + .font(theme.fonts.body) .foregroundColor(theme.colors.secondaryContent) .multilineTextAlignment(.center) diff --git a/RiotTests/AnalyticsTests.swift b/RiotTests/AnalyticsTests.swift index b31cbd6c5f..5e15bf523d 100644 --- a/RiotTests/AnalyticsTests.swift +++ b/RiotTests/AnalyticsTests.swift @@ -28,8 +28,8 @@ class AnalyticsTests: XCTestCase { let displayUpgradeMessage = Analytics.shared.promptShouldDisplayUpgradeMessage // Then the regular prompt should be shown. - XCTAssertTrue(showPrompt, "A prompt should be shown when for a new user") - XCTAssertFalse(displayUpgradeMessage, "The prompt should not ask about upgrading from Matomo") + XCTAssertTrue(showPrompt, "A prompt should be shown for a new user.") + XCTAssertFalse(displayUpgradeMessage, "The prompt should not ask about upgrading from Matomo.") } func testAnalyticsPromptUpgradeFromMatomo() { @@ -42,8 +42,8 @@ class AnalyticsTests: XCTestCase { let displayUpgradeMessage = Analytics.shared.promptShouldDisplayUpgradeMessage // Then an upgrade prompt should be shown. - XCTAssertTrue(showPrompt, "A prompt should be shown when for a new user") - XCTAssertTrue(displayUpgradeMessage, "The prompt should not ask about upgrading from Matomo") + XCTAssertTrue(showPrompt, "A prompt should be shown to the user.") + XCTAssertTrue(displayUpgradeMessage, "The prompt should ask about upgrading from Matomo.") } func testAnalyticsPromptUserDeclinedMatomo() { @@ -53,9 +53,11 @@ class AnalyticsTests: XCTestCase { // When the user is prompted for analytics let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt + let displayUpgradeMessage = Analytics.shared.promptShouldDisplayUpgradeMessage - // Then no prompt should be shown. - XCTAssertFalse(showPrompt, "A prompt should be shown when for a new user") + // Then the regular prompt should be shown. + XCTAssertTrue(showPrompt, "A prompt should be shown to the user.") + XCTAssertFalse(displayUpgradeMessage, "The prompt should not ask about upgrading from Matomo.") } func testAnalyticsPromptUserAcceptedPostHog() { @@ -66,6 +68,6 @@ class AnalyticsTests: XCTestCase { let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt // Then no prompt should be shown. - XCTAssertFalse(showPrompt, "A prompt should be shown when for a new user") + XCTAssertFalse(showPrompt, "A prompt should not be shown any more.") } } From 1ff7170f989c7f571f61fb9a1e1b8edec6c4a368 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 8 Dec 2021 12:49:57 +0000 Subject: [PATCH 025/109] Add tap/click event. Improve Swift/ObjC bridging. --- Riot/Managers/Analytics/Analytics.swift | 9 ++++ .../Managers/Analytics/AnalyticsElement.swift | 30 ++++++++++++ .../Analytics/AnalyticsEventHelpers.swift | 2 - Riot/Managers/Analytics/AnalyticsScreen.swift | 1 + Riot/Managers/Analytics/DecryptionFailure.h | 49 ------------------- Riot/Managers/Analytics/DecryptionFailure.m | 31 ------------ .../Analytics/DecryptionFailure.swift | 40 +++++++++++++++ .../Analytics/DecryptionFailureTracker.h | 2 +- .../Analytics/DecryptionFailureTracker.m | 15 +++--- Riot/SupportingFiles/Riot-Bridging-Header.h | 1 - 10 files changed, 89 insertions(+), 91 deletions(-) create mode 100644 Riot/Managers/Analytics/AnalyticsElement.swift delete mode 100644 Riot/Managers/Analytics/DecryptionFailure.h delete mode 100644 Riot/Managers/Analytics/DecryptionFailure.m create mode 100644 Riot/Managers/Analytics/DecryptionFailure.swift diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index 617f7fec43..00135edf4f 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -148,6 +148,15 @@ extension Analytics { client.screen(event) } + /// Track an element that has been tapped + /// - Parameters: + /// - tap: The element that was tapped + /// - index: The index of the element, if it's in a list of elements + func trackTap(_ tap: AnalyticsElement, index: Int?) { + let event = AnalyticsEvent.Click(index: index, name: tap.elementName) + client.capture(event) + } + /// Track an E2EE error that occurred /// - Parameters: /// - reason: The error that occurred. diff --git a/Riot/Managers/Analytics/AnalyticsElement.swift b/Riot/Managers/Analytics/AnalyticsElement.swift new file mode 100644 index 0000000000..208f2cfc6b --- /dev/null +++ b/Riot/Managers/Analytics/AnalyticsElement.swift @@ -0,0 +1,30 @@ +// +// Copyright 2021 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 AnalyticsEvents + +/// A tappable UI element that can be track in Analytics. +@objc enum AnalyticsElement: Int { + case sendMessageButton + + /// The element name reported to the AnalyticsEvent. + var elementName: AnalyticsEvent.Click.Name { + switch self { + case .sendMessageButton: + return .SendMessageButton + } + } +} diff --git a/Riot/Managers/Analytics/AnalyticsEventHelpers.swift b/Riot/Managers/Analytics/AnalyticsEventHelpers.swift index 1d879c41ca..004b7717ef 100644 --- a/Riot/Managers/Analytics/AnalyticsEventHelpers.swift +++ b/Riot/Managers/Analytics/AnalyticsEventHelpers.swift @@ -76,8 +76,6 @@ extension DecryptionFailureReason { return .OlmIndexError case .unexpected: return .UnknownError - default: - return .UnknownError } } } diff --git a/Riot/Managers/Analytics/AnalyticsScreen.swift b/Riot/Managers/Analytics/AnalyticsScreen.swift index fef1780f98..86f567acdb 100644 --- a/Riot/Managers/Analytics/AnalyticsScreen.swift +++ b/Riot/Managers/Analytics/AnalyticsScreen.swift @@ -48,6 +48,7 @@ import AnalyticsEvents case myGroups case inviteFriends + /// The screen name reported to the AnalyticsEvent. var screenName: AnalyticsEvent.Screen.ScreenName { switch self { case .sidebar: diff --git a/Riot/Managers/Analytics/DecryptionFailure.h b/Riot/Managers/Analytics/DecryptionFailure.h deleted file mode 100644 index 113049b6f2..0000000000 --- a/Riot/Managers/Analytics/DecryptionFailure.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - Copyright 2018 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 - -/** - Failure reasons as defined in https://docs.google.com/document/d/1es7cTCeJEXXfRCTRgZerAM2Wg5ZerHjvlpfTW-gsOfI. - */ -typedef NS_ENUM(NSInteger, DecryptionFailureReason) { - DecryptionFailureReasonUnspecified, - DecryptionFailureReasonOlmKeysNotSent, - DecryptionFailureReasonOlmIndexError, - DecryptionFailureReasonUnexpected -}; - -/** - `DecryptionFailure` represents a decryption failure. - */ -@interface DecryptionFailure : NSObject - -/** - The id of the event that was unabled to decrypt. - */ -@property (nonatomic) NSString *failedEventId; - -/** - The time the failure has been reported. - */ -@property (nonatomic, readonly) NSTimeInterval ts; - -/** - Decryption failure reason. - */ -@property (nonatomic) DecryptionFailureReason reason; - -@end diff --git a/Riot/Managers/Analytics/DecryptionFailure.m b/Riot/Managers/Analytics/DecryptionFailure.m deleted file mode 100644 index 7cb470e8c7..0000000000 --- a/Riot/Managers/Analytics/DecryptionFailure.m +++ /dev/null @@ -1,31 +0,0 @@ -/* - Copyright 2018 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 "DecryptionFailure.h" - -@implementation DecryptionFailure - -- (instancetype)init -{ - self = [super init]; - if (self) - { - _ts = [NSDate date].timeIntervalSince1970; - } - return self; -} - -@end diff --git a/Riot/Managers/Analytics/DecryptionFailure.swift b/Riot/Managers/Analytics/DecryptionFailure.swift new file mode 100644 index 0000000000..4c6c4ec954 --- /dev/null +++ b/Riot/Managers/Analytics/DecryptionFailure.swift @@ -0,0 +1,40 @@ +// +// Copyright 2021 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 + +/// Failure reasons as defined in https://docs.google.com/document/d/1es7cTCeJEXXfRCTRgZerAM2Wg5ZerHjvlpfTW-gsOfI. +@objc enum DecryptionFailureReason: Int { + case unspecified + case olmKeysNotSent + case olmIndexError + case unexpected +} + +/// `DecryptionFailure` represents a decryption failure. +@objcMembers class DecryptionFailure: NSObject { + /// The id of the event that was unabled to decrypt. + let failedEventId: String + /// The time the failure has been reported. + let ts: TimeInterval = Date().timeIntervalSince1970 + /// Decryption failure reason. + let reason: DecryptionFailureReason + + init(failedEventId: String, reason: DecryptionFailureReason) { + self.failedEventId = failedEventId + self.reason = reason + } +} diff --git a/Riot/Managers/Analytics/DecryptionFailureTracker.h b/Riot/Managers/Analytics/DecryptionFailureTracker.h index 7903611ce1..b8f9ca467e 100644 --- a/Riot/Managers/Analytics/DecryptionFailureTracker.h +++ b/Riot/Managers/Analytics/DecryptionFailureTracker.h @@ -16,7 +16,7 @@ #import -#import "DecryptionFailure.h" +@class DecryptionFailureTracker; @class Analytics; @import MatrixSDK; diff --git a/Riot/Managers/Analytics/DecryptionFailureTracker.m b/Riot/Managers/Analytics/DecryptionFailureTracker.m index d34ba00178..0f2b2ff818 100644 --- a/Riot/Managers/Analytics/DecryptionFailureTracker.m +++ b/Riot/Managers/Analytics/DecryptionFailureTracker.m @@ -91,31 +91,32 @@ - (void)reportUnableToDecryptErrorForEvent:(MXEvent *)event withRoomState:(MXRoo return; } - DecryptionFailure *decryptionFailure = [[DecryptionFailure alloc] init]; - decryptionFailure.failedEventId = event.eventId; + NSString *failedEventId = event.eventId; + DecryptionFailureReason reason; // Categorise the error switch (event.decryptionError.code) { case MXDecryptingErrorUnknownInboundSessionIdCode: - decryptionFailure.reason = DecryptionFailureReasonOlmKeysNotSent; + reason = DecryptionFailureReasonOlmKeysNotSent; break; case MXDecryptingErrorOlmCode: - decryptionFailure.reason = DecryptionFailureReasonOlmIndexError; + reason = DecryptionFailureReasonOlmIndexError; break; case MXDecryptingErrorEncryptionNotEnabledCode: case MXDecryptingErrorUnableToDecryptCode: - decryptionFailure.reason = DecryptionFailureReasonUnexpected; + reason = DecryptionFailureReasonUnexpected; break; default: - decryptionFailure.reason = DecryptionFailureReasonUnspecified; + reason = DecryptionFailureReasonUnspecified; break; } - reportedFailures[event.eventId] = decryptionFailure; + reportedFailures[event.eventId] = [[DecryptionFailure alloc] initWithFailedEventId:failedEventId + reason:reason]; } - (void)dispatch diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index a899115f3b..6f9767f3cc 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -45,7 +45,6 @@ #import "RoomInputToolbarView.h" #import "NSArray+Element.h" #import "ShareItemSender.h" -#import "DecryptionFailure.h" // MatrixKit common imports, shared with all targets #import "MatrixKit-Bridging-Header.h" From 5361298bfc3755b704db013232d9d6490f5f1fa5 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 9 Dec 2021 16:20:22 +0000 Subject: [PATCH 026/109] Improve iPad layout. Add separate debug configuration. --- Config/BuildSettings.swift | 13 +++- Riot/Assets/en.lproj/Vector.strings | 7 +- Riot/Generated/Strings.swift | 16 ++-- Riot/Modules/Application/LegacyAppDelegate.m | 4 + .../AnalyticsPromptModels.swift | 13 +++- .../View/AnalyticsPrompt.swift | 76 +++++++++++-------- 6 files changed, 82 insertions(+), 47 deletions(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 9e4a5a1370..d07f6efb5c 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -165,11 +165,18 @@ final class BuildSettings: NSObject { static let roomsAllowToJoinPublicRooms: Bool = true // MARK: - Analytics - #warning("Testing environment.") - /// Host to use for PostHog analytics. Set to nil to disable analytics. + #if DEBUG + /// Host to use for PostHog analytics during development. Set to nil to disable analytics in debug builds. static let analyticsHost: String? = "https://posthog-poc.lab.element.dev" - /// Public key for submitting analytics. Set to nil to disable analytics. + /// Public key for submitting analytics during development. Set to nil to disable analytics in debug builds. static let analyticsKey: String? = "rs-pJjsYJTuAkXJfhaMmPUNBhWliDyTKLOOxike6ck8" + #else + /// Host to use for PostHog analytics. Set to nil to disable analytics. + static let analyticsHost: String? = "https://posthog.hss.element.io" + /// Public key for submitting analytics. Set to nil to disable analytics. + static let analyticsKey: String? = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO" + #endif + /// The URL to open with more information about analytics terms. static let analyticsTermsURL = URL(string: "https://element.io/cookie-policy")! diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index f513009c66..119391d9af 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -949,10 +949,12 @@ Tap the + to start adding people."; // Analytics "analytics_prompt_title" = "Help improve %@"; -"analytics_prompt_description_new_user" = "Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; -"analytics_prompt_description_upgrade" = "You previously consented to share anonymous usage data with us. Now, to help understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; +"analytics_prompt_message_new_user" = "Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; +"analytics_prompt_message_upgrade" = "You previously consented to share anonymous usage data with us. Now, to help understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ "analytics_prompt_terms_new_user" = "You can read all our terms %@."; "analytics_prompt_terms_link_new_user" = "here"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ "analytics_prompt_terms_upgrade" = "Read all our terms %@. Is that OK?"; "analytics_prompt_terms_link_upgrade" = "here"; /* Note: The word "don't" is formatted in bold */ @@ -960,6 +962,7 @@ Tap the + to start adding people."; /* Note: The word "don't" is formatted in bold */ "analytics_prompt_point_2" = "We don't share information with third parties"; "analytics_prompt_point_3" = "You can turn this off anytime in settings"; +"analytics_prompt_not_now" = "Not now"; "analytics_prompt_yes" = "Yes, that's fine"; "analytics_prompt_stop" = "Stop sharing"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index d47bdfb995..af25f0458a 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -16,8 +16,8 @@ public class VectorL10n: NSObject { return VectorL10n.tr("Vector", "accept") } /// button - public static var accessibilityButtonLabel: String { - return VectorL10n.tr("Vector", "accessibility_button_label") + public static var accessibilityButtonLabel: String { + return VectorL10n.tr("Vector", "accessibility_button_label") } /// checkbox public static var accessibilityCheckboxLabel: String { @@ -36,12 +36,16 @@ public class VectorL10n: NSObject { return VectorL10n.tr("Vector", "active_call_details", p1) } /// Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices. - public static var analyticsPromptDescriptionNewUser: String { - return VectorL10n.tr("Vector", "analytics_prompt_description_new_user") + public static var analyticsPromptMessageNewUser: String { + return VectorL10n.tr("Vector", "analytics_prompt_message_new_user") } /// You previously consented to share anonymous usage data with us. Now, to help understand how people use multiple devices, we’ll generate a random identifier, shared by your devices. - public static var analyticsPromptDescriptionUpgrade: String { - return VectorL10n.tr("Vector", "analytics_prompt_description_upgrade") + public static var analyticsPromptMessageUpgrade: String { + return VectorL10n.tr("Vector", "analytics_prompt_message_upgrade") + } + /// Not now + public static var analyticsPromptNotNow: String { + return VectorL10n.tr("Vector", "analytics_prompt_not_now") } /// We don't record or profile any account data public static var analyticsPromptPoint1: String { diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 6f6543e944..946f22b26e 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -650,7 +650,11 @@ - (void)afterAppUnlockedByPin:(UIApplication *)application // Check if there is crash log to send if (RiotSettings.shared.enableAnalytics) { + #if DEBUG + // Don't show alerts for crashes during development. + #else [self checkExceptionToReport]; + #endif } // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift index 656558b4da..add81e84b6 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift @@ -63,12 +63,12 @@ enum AnalyticsPromptType { extension AnalyticsPromptType { /// The main description string that should be displayed. - var description: String { + var message: String { switch self { case .newUser: - return VectorL10n.analyticsPromptDescriptionNewUser + return VectorL10n.analyticsPromptMessageNewUser case .upgrade: - return VectorL10n.analyticsPromptDescriptionUpgrade + return VectorL10n.analyticsPromptMessageUpgrade } } @@ -94,7 +94,7 @@ extension AnalyticsPromptType { var disableButtonTitle: String { switch self { case .newUser: - return VectorL10n.cancel + return VectorL10n.analyticsPromptNotNow case .upgrade: return VectorL10n.analyticsPromptStop } @@ -121,3 +121,8 @@ extension AnalyticsPromptType: Identifiable { } } } + +// For the RiotSwiftUI target presentation. +extension AnalyticsPromptType: CustomStringConvertible { + var description: String { id } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift index a5a9635ac5..8f7acf49d4 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift @@ -27,6 +27,11 @@ struct AnalyticsPrompt: View { // MARK: Private @Environment(\.theme) private var theme + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + private var horizontalPadding: CGFloat { + horizontalSizeClass == .regular ? 50 : 16 + } // MARK: Public @@ -35,9 +40,9 @@ struct AnalyticsPrompt: View { // MARK: Views /// The text that explains what analytics will do. - private var descriptionText: some View { + private var messageText: some View { VStack { - Text("\(viewModel.viewState.promptType.description)\n") + Text("\(viewModel.viewState.promptType.message)\n") AnalyticsPromptTermsText(attributedString: viewModel.viewState.promptType.termsStrings) .accessibilityLabel(Text(viewModel.viewState.promptType.termsStrings.string)) @@ -63,6 +68,31 @@ struct AnalyticsPrompt: View { .frame(maxWidth: .infinity) } + private var mainContent: some View { + VStack { + Image(uiImage: Asset.Images.analyticsLogo.image) + .padding(.bottom, 25) + + Text(VectorL10n.analyticsPromptTitle(viewModel.viewState.strings.appDisplayName)) + .font(theme.fonts.title2B) + .foregroundColor(theme.colors.primaryContent) + .padding(.bottom, 2) + + messageText + .font(theme.fonts.body) + .foregroundColor(theme.colors.secondaryContent) + .multilineTextAlignment(.center) + + Divider() + .background(theme.colors.quinaryContent) + .padding(.vertical, 28) + + checkmarkList + .foregroundColor(theme.colors.secondaryContent) + .padding(.bottom, 16) + } + } + /// The stack of enable/disable buttons. private var buttons: some View { VStack { @@ -77,45 +107,27 @@ struct AnalyticsPrompt: View { Text(viewModel.viewState.promptType.disableButtonTitle) .font(theme.fonts.bodySB) .foregroundColor(theme.colors.accent) + .padding(12) } - .buttonStyle(PrimaryActionButtonStyle(customColor: .clear)) .accessibilityIdentifier("disableButton") } } var body: some View { - VStack { - ScrollView(showsIndicators: false) { - VStack { - Image(uiImage: Asset.Images.analyticsLogo.image) - .padding(.bottom, 25) - - Text(VectorL10n.analyticsPromptTitle(viewModel.viewState.strings.appDisplayName)) - .font(theme.fonts.title2B) - .foregroundColor(theme.colors.primaryContent) - .padding(.bottom, 2) - - descriptionText - .font(theme.fonts.body) - .foregroundColor(theme.colors.secondaryContent) - .multilineTextAlignment(.center) - - Divider() - .background(theme.colors.quinaryContent) - .padding(.vertical, 28) - - checkmarkList - .foregroundColor(theme.colors.secondaryContent) - .padding(.bottom, 16) + GeometryReader { geometry in + VStack { + ScrollView(showsIndicators: false) { + mainContent + .padding(.top, 50) + .padding(.horizontal, horizontalPadding) } - .padding(.top, 50) - .padding(.horizontal, 16) + + buttons + .padding(.horizontal, horizontalPadding) + .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16) } - - buttons - .padding(.horizontal, 16) + .background(theme.colors.background.ignoresSafeArea()) } - .background(theme.colors.background.ignoresSafeArea()) } } From b57e537b4b1e323bfec1661023daaa43bd683872 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 13 Dec 2021 17:30:38 +0000 Subject: [PATCH 027/109] Leave analytics client running on sign out. Only identify with a running session. --- Riot/Managers/Analytics/Analytics.swift | 66 ++++++++++++------- .../Analytics/AnalyticsClientProtocol.swift | 6 +- .../Analytics/PostHogAnalyticsClient.swift | 5 +- Riot/Modules/Application/LegacyAppDelegate.m | 5 ++ 4 files changed, 55 insertions(+), 27 deletions(-) diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index 00135edf4f..2fb8ba079a 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -27,7 +27,7 @@ import AnalyticsEvents static let shared = Analytics() /// The analytics client to send events with. - private var client = PostHogAnalyticsClient() + private var client: AnalyticsClientProtocol = PostHogAnalyticsClient() /// Whether or not the object is enabled and sending events to the server. var isRunning: Bool { client.isRunning } @@ -46,34 +46,25 @@ import AnalyticsEvents // MARK: - Public /// Opts in to analytics tracking with the supplied session. - /// - Parameter session: The session to use to when reading/generating the analytics ID. + /// - Parameter session: An optional session to use to when reading/generating the analytics ID. + /// The session will be ignored if not running. func optIn(with session: MXSession?) { - guard let session = session else { return } RiotSettings.shared.enableAnalytics = true - - var settings = AnalyticsSettings(session: session) - - if settings.id == nil { - settings.generateID() - - session.setAccountData(settings.dictionary, forType: AnalyticsSettings.eventType) { - MXLog.debug("[Analytics] Successfully updated analytics settings in account data.") - } failure: { error in - MXLog.error("[Analytics] Failed to update analytics settings.") - } - } - startIfEnabled() - if !RiotSettings.shared.isIdentifiedForAnalytics { - identify(with: settings) - } + guard let session = session else { return } + useAnalyticsSettings(from: session) } - /// Opts out of analytics tracking and calls `reset` to clear any IDs and event queues. + /// Stops analytics tracking and calls `reset` to clear any IDs and event queues. func optOut() { RiotSettings.shared.enableAnalytics = false + + // The order is important here. PostHog ignores the reset if stopped. reset() + client.stop() + + MXLog.debug("[Analytics] Stopped.") } /// Starts the analytics client if the user has opted in, otherwise does nothing. @@ -92,14 +83,39 @@ import AnalyticsEvents MXLogger.setBuildVersion(AppDelegate.theDelegate().build) } - /// Resets the any IDs and event queues in the analytics client. This method - /// can be called on sign-out to remember opt-in status, but ensure the next + /// Use the analytics settings from the supplied session to configure analytics. + /// For now this is only used for (pseudonymous) identification. + /// - Parameter session: The session to read analytics settings from. + func useAnalyticsSettings(from session: MXSession) { + guard + RiotSettings.shared.enableAnalytics, + !RiotSettings.shared.isIdentifiedForAnalytics, + session.state == .running // Only use the session if it is running otherwise we could wipe out an existing analytics ID. + else { return } + + var settings = AnalyticsSettings(session: session) + + if settings.id == nil { + settings.generateID() + + session.setAccountData(settings.dictionary, forType: AnalyticsSettings.eventType) { + MXLog.debug("[Analytics] Successfully updated analytics settings in account data.") + self.identify(with: settings) + } failure: { error in + MXLog.error("[Analytics] Failed to update analytics settings.") + } + } else { + self.identify(with: settings) + } + } + + /// Resets the any IDs and event queues in the analytics client. This method should + /// be called on sign-out to maintain opt-in status, whilst ensuring the next /// account used isn't associated with the previous one. + /// Note: **MUST** be called before stopping PostHog or the reset is ignored. func reset() { - guard isRunning else { return } - client.reset() - MXLog.debug("[Analytics] Stopped and reset.") + MXLog.debug("[Analytics] Reset.") RiotSettings.shared.isIdentifiedForAnalytics = false // Stop collecting crash logs diff --git a/Riot/Managers/Analytics/AnalyticsClientProtocol.swift b/Riot/Managers/Analytics/AnalyticsClientProtocol.swift index 7ee0ae4298..af07a58fba 100644 --- a/Riot/Managers/Analytics/AnalyticsClientProtocol.swift +++ b/Riot/Managers/Analytics/AnalyticsClientProtocol.swift @@ -28,9 +28,13 @@ protocol AnalyticsClientProtocol { /// - Parameter id: The ID to associate with the user. func identify(id: String) - /// Stop the analytics client reporting data and reset all stored properties and events. + /// Reset all stored properties and any event queues on the client. Note that + /// the client will remain active, but in a fresh unidentified state. func reset() + /// Stop the analytics client reporting data. + func stop() + /// Send any queued events immediately. func flush() diff --git a/Riot/Managers/Analytics/PostHogAnalyticsClient.swift b/Riot/Managers/Analytics/PostHogAnalyticsClient.swift index 0552a459c2..1c71721120 100644 --- a/Riot/Managers/Analytics/PostHogAnalyticsClient.swift +++ b/Riot/Managers/Analytics/PostHogAnalyticsClient.swift @@ -40,8 +40,11 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol { } func reset() { - postHog?.disable() postHog?.reset() + } + + func stop() { + postHog?.disable() // As of PostHog 1.4.4, setting the client to nil here doesn't release // it. Keep it around to avoid having multiple instances if the user re-enables diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 946f22b26e..fd6354ad39 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1884,6 +1884,11 @@ - (void)initMatrixSessions [self.pushNotificationService checkPushKitPushersInSession:mxSession]; } + else if (mxSession.state == MXSessionStateRunning) + { + // Configure analytics from the session if necessary + [Analytics.shared useAnalyticsSettingsFrom:mxSession]; + } else if (mxSession.state == MXSessionStateClosed) { [self removeMatrixSession:mxSession]; From a5abff7eee64073ac794662c5297f63002a14451 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 15 Dec 2021 14:40:45 +0000 Subject: [PATCH 028/109] Address most PR comments. Update Podfile.lock --- .swiftlint.yml | 3 - Podfile.lock | 2 +- Riot/Managers/Analytics/Analytics.swift | 15 ++- .../Analytics/AnalyticsEventHelpers.swift | 100 ------------------ .../Analytics/AnalyticsSettings.swift | 12 ++- ...Element.swift => AnalyticsUIElement.swift} | 4 +- .../Analytics/DecryptionFailure.swift | 15 ++- .../Helpers/JoinedRoomSize+MemberCount.swift | 36 +++++++ .../MXCallHangupReason+Analytics.swift | 38 +++++++ .../Helpers/MXTaskProfileName+Analytics.swift | 42 ++++++++ Riot/Modules/TabBar/TabBarCoordinator.swift | 4 + Riot/SupportingFiles/Riot-Bridging-Header.h | 1 + Riot/Utils/HTMLFormatter.h | 38 +++++++ Riot/Utils/HTMLFormatter.m | 61 +++++++++++ .../{Tools.swift => HTMLFormatter.swift} | 4 +- Riot/Utils/Tools.h | 10 -- Riot/Utils/Tools.m | 37 ------- .../AnalyticsPromptCoordinator.swift | 2 + .../Coordinator/AnalyticsPromptStrings.swift | 18 ++-- 19 files changed, 274 insertions(+), 168 deletions(-) delete mode 100644 Riot/Managers/Analytics/AnalyticsEventHelpers.swift rename Riot/Managers/Analytics/{AnalyticsElement.swift => AnalyticsUIElement.swift} (82%) create mode 100644 Riot/Managers/Analytics/Helpers/JoinedRoomSize+MemberCount.swift create mode 100644 Riot/Managers/Analytics/Helpers/MXCallHangupReason+Analytics.swift create mode 100644 Riot/Managers/Analytics/Helpers/MXTaskProfileName+Analytics.swift create mode 100644 Riot/Utils/HTMLFormatter.h create mode 100644 Riot/Utils/HTMLFormatter.m rename Riot/Utils/{Tools.swift => HTMLFormatter.swift} (91%) diff --git a/.swiftlint.yml b/.swiftlint.yml index 22cadcaac3..4d215eb98c 100755 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -56,9 +56,6 @@ function_body_length: warning: 100 error: 150 -nesting: - type_level: 2 - # naming rules can set warnings/errors for min_length and max_length # additionally they can set excluded names type_name: diff --git a/Podfile.lock b/Podfile.lock index afe3025d73..be012e6902 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -180,7 +180,7 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: AnalyticsEvents: - :commit: aac06956d45cb86ea2bbd7a21b20b14ba8899fcf + :commit: f1805ad7c3fafa7fd9c6e2eaa9e0165f8142ecd2 :git: https://github.com/matrix-org/matrix-analytics-events.git SPEC CHECKSUMS: diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index 2fb8ba079a..16ced46cb0 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -164,15 +164,28 @@ extension Analytics { client.screen(event) } + /// The the presentation of a screen without including a duration + /// - Parameter screen: The screen that was shown + func trackScreen(_ screen: AnalyticsScreen) { + trackScreen(screen, duration: nil) + } + /// Track an element that has been tapped /// - Parameters: /// - tap: The element that was tapped /// - index: The index of the element, if it's in a list of elements - func trackTap(_ tap: AnalyticsElement, index: Int?) { + func trackTap(_ tap: AnalyticsUIElement, index: Int?) { let event = AnalyticsEvent.Click(index: index, name: tap.elementName) client.capture(event) } + /// Track an element that has been tapped without including an index + /// - Parameters: + /// - tap: The element that was tapped + func trackTap(_ tap: AnalyticsUIElement) { + trackTap(tap, index: nil) + } + /// Track an E2EE error that occurred /// - Parameters: /// - reason: The error that occurred. diff --git a/Riot/Managers/Analytics/AnalyticsEventHelpers.swift b/Riot/Managers/Analytics/AnalyticsEventHelpers.swift deleted file mode 100644 index 004b7717ef..0000000000 --- a/Riot/Managers/Analytics/AnalyticsEventHelpers.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// Copyright 2021 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 AnalyticsEvents - -// MARK: - Helpers - -extension MXTaskProfileName { - var analyticsName: AnalyticsEvent.PerformanceTimer.Name? { - switch self { - case .startupIncrementalSync: - return .StartupIncrementalSync - case .startupInitialSync: - return .StartupInitialSync - case .startupLaunchScreen: - return .StartupLaunchScreen - case .startupStorePreload: - return .StartupStorePreload - case .startupMountData: - return .StartupStoreReady - case .initialSyncRequest: - return .InitialSyncRequest - case .initialSyncParsing: - return .InitialSyncParsing - case .notificationsOpenEvent: - return .NotificationsOpenEvent - default: - return nil - } - } -} - -extension __MXCallHangupReason { - var errorName: AnalyticsEvent.Error.Name { - switch self { - case .userHangup: - return .VoipUserHangup - case .inviteTimeout: - return .VoipInviteTimeout - case .iceFailed: - return .VoipIceFailed - case .iceTimeout: - return .VoipIceTimeout - case .userMediaFailed: - return .VoipUserMediaFailed - case .unknownError: - return .UnknownError - default: - return .UnknownError - } - } -} - -extension DecryptionFailureReason { - var errorName: AnalyticsEvent.Error.Name { - switch self { - case .unspecified: - return .OlmUnspecifiedError - case .olmKeysNotSent: - return .OlmKeysNotSentError - case .olmIndexError: - return .OlmIndexError - case .unexpected: - return .UnknownError - } - } -} - -extension AnalyticsEvent.JoinedRoom.RoomSize { - init?(memberCount: UInt) { - switch memberCount { - case 2: - self = .Two - case 3...10: - self = .ThreeToTen - case 11...100: - self = .ElevenToOneHundred - case 101...1000: - self = .OneHundredAndOneToAThousand - case 1001...: - self = .MoreThanAThousand - default: - return nil - } - } -} diff --git a/Riot/Managers/Analytics/AnalyticsSettings.swift b/Riot/Managers/Analytics/AnalyticsSettings.swift index 67927cacee..aaa2f8353b 100644 --- a/Riot/Managers/Analytics/AnalyticsSettings.swift +++ b/Riot/Managers/Analytics/AnalyticsSettings.swift @@ -28,8 +28,11 @@ struct AnalyticsSettings { /// This is suggested to be a 128-bit hex encoded string. var id: String? - /// Unused on iOS but necessary to load the value in case opt in was declined on web, - /// but accepted on iOS. Otherwise generating an ID would wipe out the existing value. + /// Whether the user has opted in on web or not. This is unused on iOS but necessary + /// to store here so that it's value is preserved when updating the account data if we + /// generated an ID on iOS. + /// + /// `true` if opted in on web, `false` if opted out on web and `nil` if the web prompt is not yet seen. private var webOptIn: Bool? /// Generate a new random analytics ID. This method has no effect if an ID already exists. @@ -40,7 +43,8 @@ struct AnalyticsSettings { } extension AnalyticsSettings { - init(dictionary: Dictionary?) { + // Private as AnalyticsSettings should only be created from an MXSession + private init(dictionary: Dictionary?) { self.id = dictionary?[Constants.idKey] as? String self.webOptIn = dictionary?[Constants.webOptInKey] as? Bool } @@ -54,6 +58,8 @@ extension AnalyticsSettings { } } +// MARK: - Public initializer + extension AnalyticsSettings { init(session: MXSession) { self.init(dictionary: session.accountData.accountData(forEventType: AnalyticsSettings.eventType)) diff --git a/Riot/Managers/Analytics/AnalyticsElement.swift b/Riot/Managers/Analytics/AnalyticsUIElement.swift similarity index 82% rename from Riot/Managers/Analytics/AnalyticsElement.swift rename to Riot/Managers/Analytics/AnalyticsUIElement.swift index 208f2cfc6b..93a08e7e28 100644 --- a/Riot/Managers/Analytics/AnalyticsElement.swift +++ b/Riot/Managers/Analytics/AnalyticsUIElement.swift @@ -17,12 +17,14 @@ import AnalyticsEvents /// A tappable UI element that can be track in Analytics. -@objc enum AnalyticsElement: Int { +@objc enum AnalyticsUIElement: Int { case sendMessageButton /// The element name reported to the AnalyticsEvent. var elementName: AnalyticsEvent.Click.Name { switch self { + // Note: This is a test element that doesn't need to be captured. + // It will likely be removed when the AnalyticsEvent.Click is updated. case .sendMessageButton: return .SendMessageButton } diff --git a/Riot/Managers/Analytics/DecryptionFailure.swift b/Riot/Managers/Analytics/DecryptionFailure.swift index 4c6c4ec954..d011a0413c 100644 --- a/Riot/Managers/Analytics/DecryptionFailure.swift +++ b/Riot/Managers/Analytics/DecryptionFailure.swift @@ -14,7 +14,7 @@ // limitations under the License. // -import Foundation +import AnalyticsEvents /// Failure reasons as defined in https://docs.google.com/document/d/1es7cTCeJEXXfRCTRgZerAM2Wg5ZerHjvlpfTW-gsOfI. @objc enum DecryptionFailureReason: Int { @@ -22,6 +22,19 @@ import Foundation case olmKeysNotSent case olmIndexError case unexpected + + var errorName: AnalyticsEvent.Error.Name { + switch self { + case .unspecified: + return .OlmUnspecifiedError + case .olmKeysNotSent: + return .OlmKeysNotSentError + case .olmIndexError: + return .OlmIndexError + case .unexpected: + return .UnknownError + } + } } /// `DecryptionFailure` represents a decryption failure. diff --git a/Riot/Managers/Analytics/Helpers/JoinedRoomSize+MemberCount.swift b/Riot/Managers/Analytics/Helpers/JoinedRoomSize+MemberCount.swift new file mode 100644 index 0000000000..d5473d5f92 --- /dev/null +++ b/Riot/Managers/Analytics/Helpers/JoinedRoomSize+MemberCount.swift @@ -0,0 +1,36 @@ +// +// Copyright 2021 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 AnalyticsEvents + +extension AnalyticsEvent.JoinedRoom.RoomSize { + init?(memberCount: UInt) { + switch memberCount { + case 2: + self = .Two + case 3...10: + self = .ThreeToTen + case 11...100: + self = .ElevenToOneHundred + case 101...1000: + self = .OneHundredAndOneToAThousand + case 1001...: + self = .MoreThanAThousand + default: + return nil + } + } +} diff --git a/Riot/Managers/Analytics/Helpers/MXCallHangupReason+Analytics.swift b/Riot/Managers/Analytics/Helpers/MXCallHangupReason+Analytics.swift new file mode 100644 index 0000000000..4b8911ce89 --- /dev/null +++ b/Riot/Managers/Analytics/Helpers/MXCallHangupReason+Analytics.swift @@ -0,0 +1,38 @@ +// +// Copyright 2021 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 AnalyticsEvents + +extension __MXCallHangupReason { + var errorName: AnalyticsEvent.Error.Name { + switch self { + case .userHangup: + return .VoipUserHangup + case .inviteTimeout: + return .VoipInviteTimeout + case .iceFailed: + return .VoipIceFailed + case .iceTimeout: + return .VoipIceTimeout + case .userMediaFailed: + return .VoipUserMediaFailed + case .unknownError: + return .UnknownError + default: + return .UnknownError + } + } +} diff --git a/Riot/Managers/Analytics/Helpers/MXTaskProfileName+Analytics.swift b/Riot/Managers/Analytics/Helpers/MXTaskProfileName+Analytics.swift new file mode 100644 index 0000000000..99f89174e4 --- /dev/null +++ b/Riot/Managers/Analytics/Helpers/MXTaskProfileName+Analytics.swift @@ -0,0 +1,42 @@ +// +// Copyright 2021 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 AnalyticsEvents + +extension MXTaskProfileName { + var analyticsName: AnalyticsEvent.PerformanceTimer.Name? { + switch self { + case .startupIncrementalSync: + return .StartupIncrementalSync + case .startupInitialSync: + return .StartupInitialSync + case .startupLaunchScreen: + return .StartupLaunchScreen + case .startupStorePreload: + return .StartupStorePreload + case .startupMountData: + return .StartupStoreReady + case .initialSyncRequest: + return .InitialSyncRequest + case .initialSyncParsing: + return .InitialSyncParsing + case .notificationsOpenEvent: + return .NotificationsOpenEvent + default: + return nil + } + } +} diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 11398f220a..0030b92568 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -491,6 +491,10 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { private func presentAnalyticsPrompt(with session: MXSession) { let parameters = AnalyticsPromptCoordinatorParameters(session: session, navigationRouter: navigationRouter) let coordinator = AnalyticsPromptCoordinator(parameters: parameters) + coordinator.completion = { [weak self] in + self?.remove(childCoordinator: coordinator) + } + coordinator.start() add(childCoordinator: coordinator) } diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index 6f9767f3cc..c5a22e8a70 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -45,6 +45,7 @@ #import "RoomInputToolbarView.h" #import "NSArray+Element.h" #import "ShareItemSender.h" +#import "HTMLFormatter.h" // MatrixKit common imports, shared with all targets #import "MatrixKit-Bridging-Header.h" diff --git a/Riot/Utils/HTMLFormatter.h b/Riot/Utils/HTMLFormatter.h new file mode 100644 index 0000000000..605ee9d08e --- /dev/null +++ b/Riot/Utils/HTMLFormatter.h @@ -0,0 +1,38 @@ +// +// Copyright 2021 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 +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface HTMLFormatter : NSObject + +/** Builds an attributed string from a string containing html. + @param htmlString The html string to use. + @param allowedTags The html tags that should be allowed. + @param fontSize The default font size to use. + + Note: It is recommended to include "p" and "body" tags in + `allowedTags` as these are often added when parsing. + */ +- (NSAttributedString * _Nonnull)formatHTML:(NSString * _Nonnull)htmlString + withAllowedTags:(NSArray * _Nonnull)allowedTags + fontSize:(CGFloat)fontSize; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Riot/Utils/HTMLFormatter.m b/Riot/Utils/HTMLFormatter.m new file mode 100644 index 0000000000..260ab83f5b --- /dev/null +++ b/Riot/Utils/HTMLFormatter.m @@ -0,0 +1,61 @@ +// +// Copyright 2021 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 "HTMLFormatter.h" +#import "GeneratedInterface-Swift.h" + +@implementation HTMLFormatter + +- (NSAttributedString *)formatHTML:(NSString *)htmlString withAllowedTags:(NSArray *)allowedTags fontSize:(CGFloat)fontSize +{ + // TODO: This method should be more general purpose and usable from MXKEventFormatter and GroupHomeViewController + // FIXME: The implementation is currently in Objective-C as there is a crash in the callback when implemented in Swift + UIFont *font = [UIFont systemFontOfSize:fontSize]; + + // Do some sanitisation before finalizing the string + DTHTMLAttributedStringBuilderWillFlushCallback sanitizeCallback = ^(DTHTMLElement *element) { + [element sanitizeWith:allowedTags bodyFont:font imageHandler:nil]; + }; + + NSDictionary *options = @{ + DTUseiOS6Attributes: @(YES), // Enable it to be able to display the attributed string in a UITextView + DTDefaultFontFamily: font.familyName, + DTDefaultFontName: font.fontName, + DTDefaultFontSize: @(font.pointSize), + DTDefaultLinkDecoration: @(NO), + DTWillFlushBlockCallBack: sanitizeCallback + }; + + // 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. + NSAttributedString *string = [[NSAttributedString alloc] initWithHTMLData:[htmlString dataUsingEncoding:NSUTF8StringEncoding] options:options documentAttributes:NULL]; + + // Apply additional treatments + string = [MXKTools removeDTCoreTextArtifacts:string]; + + if (!string) { + return [[NSAttributedString alloc] initWithString:htmlString]; + } + + return string; +} + +@end diff --git a/Riot/Utils/Tools.swift b/Riot/Utils/HTMLFormatter.swift similarity index 91% rename from Riot/Utils/Tools.swift rename to Riot/Utils/HTMLFormatter.swift index ea34f30c8e..8319388e1c 100644 --- a/Riot/Utils/Tools.swift +++ b/Riot/Utils/HTMLFormatter.swift @@ -16,14 +16,14 @@ import Foundation -extension Tools { +extension HTMLFormatter { /// Builds an attributed string by replacing a `%@` placeholder with the supplied link text and URL. /// - Parameters: /// - string: The string to be formatted. /// - link: The link text to be inserted. /// - url: The URL to be linked to. /// - Returns: An attributed string. - static func format(_ string: String, with link: String, using url: URL) -> NSAttributedString { + func format(_ string: String, with link: String, using url: URL) -> NSAttributedString { let baseString = NSMutableAttributedString(string: string) let attributedLink = NSAttributedString(string: link, attributes: [.link: url]) diff --git a/Riot/Utils/Tools.h b/Riot/Utils/Tools.h index 59fabc9e57..2d411a716b 100644 --- a/Riot/Utils/Tools.h +++ b/Riot/Utils/Tools.h @@ -59,14 +59,4 @@ */ + (NSAttributedString *)setTextColorAlpha:(CGFloat)alpha inAttributedString:(NSAttributedString*)attributedString; -/** Builds an attributed string from a string containing html. - @param htmlString The html string to use. - @param allowedTags The html tags that should be allowed. - - Note: It is recommended to include "p" and "body" tags in - `allowedTags` as these are often added when parsing. - */ -+ (NSAttributedString * _Nonnull)attributedStringFromHTML:(NSString * _Nonnull)htmlString - withAllowedTags:(NSArray * _Nonnull)allowedTags; - @end diff --git a/Riot/Utils/Tools.m b/Riot/Utils/Tools.m index 1c111e4467..f27ab4c334 100644 --- a/Riot/Utils/Tools.m +++ b/Riot/Utils/Tools.m @@ -138,41 +138,4 @@ + (NSAttributedString *)setTextColorAlpha:(CGFloat)alpha inAttributedString:(NSA return string; } -+ (NSAttributedString *)attributedStringFromHTML:(NSString *)htmlString withAllowedTags:(NSArray *)allowedTags -{ - UIFont *font = [UIFont systemFontOfSize:[UIFont systemFontSize]]; - - // Do some sanitisation before finalizing the string - DTHTMLAttributedStringBuilderWillFlushCallback sanitizeCallback = ^(DTHTMLElement *element) { - [element sanitizeWith:allowedTags bodyFont:font imageHandler:nil]; - }; - - NSDictionary *options = @{ - DTUseiOS6Attributes: @(YES), // Enable it to be able to display the attributed string in a UITextView - DTDefaultFontFamily: font.familyName, - DTDefaultFontName: font.fontName, - DTDefaultFontSize: @(font.pointSize), - DTDefaultLinkDecoration: @(NO), - DTWillFlushBlockCallBack: sanitizeCallback - }; - - // 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. - NSAttributedString *string = [[NSAttributedString alloc] initWithHTMLData:[htmlString dataUsingEncoding:NSUTF8StringEncoding] options:options documentAttributes:NULL]; - - // Apply additional treatments - string = [MXKTools removeDTCoreTextArtifacts:string]; - - if (!string) { - return [[NSAttributedString alloc] initWithString:htmlString]; - } - - return string; -} - @end diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift index a85fd8d44d..208b289d76 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift @@ -89,9 +89,11 @@ final class AnalyticsPromptCoordinator: Coordinator { case .enable: Analytics.shared.optIn(with: self.parameters.session) self.parameters.navigationRouter.dismissModule(animated: true, completion: nil) + self.completion?() case .disable: Analytics.shared.optOut() self.parameters.navigationRouter.dismissModule(animated: true, completion: nil) + self.completion?() } } } diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift index e0a09f5520..4ce8ab20db 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift @@ -14,20 +14,20 @@ // limitations under the License. // -import DTCoreText +import Foundation @available(iOS 14.0, *) struct AnalyticsPromptStrings: AnalyticsPromptStringsProtocol { let appDisplayName = AppInfo.current.displayName - let point1 = Tools.attributedString(fromHTML: VectorL10n.analyticsPromptPoint1, withAllowedTags: ["b", "p"]) - let point2 = Tools.attributedString(fromHTML: VectorL10n.analyticsPromptPoint2, withAllowedTags: ["b", "p"]) + let point1 = HTMLFormatter().formatHTML(VectorL10n.analyticsPromptPoint1, withAllowedTags: ["b", "p"], fontSize: UIFont.systemFontSize) + let point2 = HTMLFormatter().formatHTML(VectorL10n.analyticsPromptPoint2, withAllowedTags: ["b", "p"], fontSize: UIFont.systemFontSize) - let termsNewUser = Tools.format(VectorL10n.analyticsPromptTermsNewUser("%@"), - with: VectorL10n.analyticsPromptTermsLinkNewUser, - using: BuildSettings.analyticsTermsURL) - let termsUpgrade = Tools.format(VectorL10n.analyticsPromptTermsUpgrade("%@"), - with: VectorL10n.analyticsPromptTermsLinkUpgrade, - using: BuildSettings.analyticsTermsURL) + let termsNewUser = HTMLFormatter().format(VectorL10n.analyticsPromptTermsNewUser("%@"), + with: VectorL10n.analyticsPromptTermsLinkNewUser, + using: BuildSettings.analyticsTermsURL) + let termsUpgrade = HTMLFormatter().format(VectorL10n.analyticsPromptTermsUpgrade("%@"), + with: VectorL10n.analyticsPromptTermsLinkUpgrade, + using: BuildSettings.analyticsTermsURL) } From 1df8514fcdc70f265b3f2e633195251090e90476 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 15 Dec 2021 15:06:10 +0000 Subject: [PATCH 029/109] Fix mutability on AnalyticsSettings. --- Riot/Managers/Analytics/AnalyticsSettings.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Managers/Analytics/AnalyticsSettings.swift b/Riot/Managers/Analytics/AnalyticsSettings.swift index aaa2f8353b..7d3352548c 100644 --- a/Riot/Managers/Analytics/AnalyticsSettings.swift +++ b/Riot/Managers/Analytics/AnalyticsSettings.swift @@ -26,14 +26,14 @@ struct AnalyticsSettings { /// A randomly generated analytics token for this user. /// This is suggested to be a 128-bit hex encoded string. - var id: String? + private(set) var id: String? /// Whether the user has opted in on web or not. This is unused on iOS but necessary /// to store here so that it's value is preserved when updating the account data if we /// generated an ID on iOS. /// /// `true` if opted in on web, `false` if opted out on web and `nil` if the web prompt is not yet seen. - private var webOptIn: Bool? + private let webOptIn: Bool? /// Generate a new random analytics ID. This method has no effect if an ID already exists. mutating func generateID() { From 919fd0ee3a0def6e9267c2be556c65cc6cef1d42 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 16 Dec 2021 13:48:14 +0000 Subject: [PATCH 030/109] Add an AnalyticsService to handle account data. --- Riot/Managers/Analytics/Analytics.swift | 22 +++--- .../Managers/Analytics/AnalyticsService.swift | 72 +++++++++++++++++++ .../Analytics/AnalyticsSettings.swift | 20 +++--- 3 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 Riot/Managers/Analytics/AnalyticsService.swift diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index 16ced46cb0..4dd67a5edb 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -89,23 +89,17 @@ import AnalyticsEvents func useAnalyticsSettings(from session: MXSession) { guard RiotSettings.shared.enableAnalytics, - !RiotSettings.shared.isIdentifiedForAnalytics, - session.state == .running // Only use the session if it is running otherwise we could wipe out an existing analytics ID. + !RiotSettings.shared.isIdentifiedForAnalytics else { return } - var settings = AnalyticsSettings(session: session) - - if settings.id == nil { - settings.generateID() - - session.setAccountData(settings.dictionary, forType: AnalyticsSettings.eventType) { - MXLog.debug("[Analytics] Successfully updated analytics settings in account data.") + let service = AnalyticsService(session: session) + service.settings { result in + switch result { + case .success(let settings): self.identify(with: settings) - } failure: { error in - MXLog.error("[Analytics] Failed to update analytics settings.") + case .failure: + MXLog.error("[Analytics] Failed to use analytics settings. Will continue to run without analytics ID.") } - } else { - self.identify(with: settings) } } @@ -135,7 +129,7 @@ import AnalyticsEvents /// - Parameter settings: The settings to use for identification. The ID must be set *before* calling this method. private func identify(with settings: AnalyticsSettings) { guard let id = settings.id else { - MXLog.warning("[Analytics] identify(with:) called before an ID has been generated.") + MXLog.error("[Analytics] identify(with:) called before an ID has been generated.") return } diff --git a/Riot/Managers/Analytics/AnalyticsService.swift b/Riot/Managers/Analytics/AnalyticsService.swift new file mode 100644 index 0000000000..820102c852 --- /dev/null +++ b/Riot/Managers/Analytics/AnalyticsService.swift @@ -0,0 +1,72 @@ +// +// Copyright 2021 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 AnalyticsServiceError: Error { + /// The session supplied to the service does not have a state of `MXSessionStateRunning`. + case sessionIsNotRunning + /// An error occurred but the session did not report what it was. + case unknown +} + +/// A service responsible for handling the `im.vector.analytics` event from the user's account data. +class AnalyticsService { + let session: MXSession + + /// Creates an analytics service with the supplied session. + /// - Parameter session: The session to use when reading analytics settings from account data. + init(session: MXSession) { + self.session = session + } + + /// The analytics settings for the current user. Calling this method will check whether the settings already + /// contain an `id` property and if not, will add one to the account data before calling the completion. + /// - Parameter completion: A completion handler that will be called when the request completes. + /// + /// The request will fail if the service's session does not have the `MXSessionStateRunning` state. + func settings(completion: @escaping (Result) -> Void) { + // Only use the session if it is running otherwise we could wipe out an existing analytics ID. + guard session.state == .running else { + MXLog.warning("[AnalyticsService] Aborting attempt to read analytics settings. The session may not be up-to-date.") + completion(.failure(AnalyticsServiceError.sessionIsNotRunning)) + return + } + + let settings = AnalyticsSettings(accountData: session.accountData) + + // The id has already be set so we are done here. + if settings.id != nil { + completion(.success(settings)) + return + } + + // Create a new ID and modify the event dictionary. + let id = UUID().uuidString + + var eventDictionary = settings.dictionary + eventDictionary[AnalyticsSettings.Constants.idKey] = id + + session.setAccountData(eventDictionary, forType: AnalyticsSettings.eventType) { + MXLog.debug("[AnalyticsService] Successfully updated analytics settings in account data.") + let settings = AnalyticsSettings(accountData: self.session.accountData) + completion(.success(settings)) + } failure: { error in + MXLog.warning("[AnalyticsService] Failed to update analytics settings.") + completion(.failure(error ?? AnalyticsServiceError.unknown)) + } + } +} diff --git a/Riot/Managers/Analytics/AnalyticsSettings.swift b/Riot/Managers/Analytics/AnalyticsSettings.swift index 7d3352548c..e847f0668c 100644 --- a/Riot/Managers/Analytics/AnalyticsSettings.swift +++ b/Riot/Managers/Analytics/AnalyticsSettings.swift @@ -16,17 +16,18 @@ import Foundation +/// An analytics settings event from the user's account data. struct AnalyticsSettings { static let eventType = "im.vector.analytics" - private enum Constants { + enum Constants { static let idKey = "id" static let webOptInKey = "pseudonymousAnalyticsOptIn" } /// A randomly generated analytics token for this user. - /// This is suggested to be a 128-bit hex encoded string. - private(set) var id: String? + /// This is suggested to be a UUID string. + let id: String? /// Whether the user has opted in on web or not. This is unused on iOS but necessary /// to store here so that it's value is preserved when updating the account data if we @@ -34,12 +35,6 @@ struct AnalyticsSettings { /// /// `true` if opted in on web, `false` if opted out on web and `nil` if the web prompt is not yet seen. private let webOptIn: Bool? - - /// Generate a new random analytics ID. This method has no effect if an ID already exists. - mutating func generateID() { - guard id == nil else { return } - id = UUID().uuidString - } } extension AnalyticsSettings { @@ -49,6 +44,7 @@ extension AnalyticsSettings { self.webOptIn = dictionary?[Constants.webOptInKey] as? Bool } + /// A dictionary representation of the settings. var dictionary: Dictionary { var dictionary = [AnyHashable: Any]() dictionary[Constants.idKey] = id @@ -61,7 +57,9 @@ extension AnalyticsSettings { // MARK: - Public initializer extension AnalyticsSettings { - init(session: MXSession) { - self.init(dictionary: session.accountData.accountData(forEventType: AnalyticsSettings.eventType)) + /// Create the analytics settings from account data. + /// - Parameter accountData: The account data to read the event from. + init(accountData: MXAccountData) { + self.init(dictionary: accountData.accountData(forEventType: AnalyticsSettings.eventType)) } } From 8e38a5fc124dbe441b512380288a0ec603d8e3ab Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 16 Dec 2021 13:50:10 +0000 Subject: [PATCH 031/109] Move Analytics from Managers to Modules. --- Riot/{Managers => Modules}/Analytics/Analytics.swift | 0 .../{Managers => Modules}/Analytics/AnalyticsClientProtocol.swift | 0 Riot/{Managers => Modules}/Analytics/AnalyticsScreen.swift | 0 Riot/{Managers => Modules}/Analytics/AnalyticsScreenTimer.swift | 0 Riot/{Managers => Modules}/Analytics/AnalyticsService.swift | 0 Riot/{Managers => Modules}/Analytics/AnalyticsSettings.swift | 0 Riot/{Managers => Modules}/Analytics/AnalyticsUIElement.swift | 0 Riot/{Managers => Modules}/Analytics/DecryptionFailure.swift | 0 Riot/{Managers => Modules}/Analytics/DecryptionFailureTracker.h | 0 Riot/{Managers => Modules}/Analytics/DecryptionFailureTracker.m | 0 .../Analytics/Helpers/JoinedRoomSize+MemberCount.swift | 0 .../Analytics/Helpers/MXCallHangupReason+Analytics.swift | 0 .../Analytics/Helpers/MXTaskProfileName+Analytics.swift | 0 .../{Managers => Modules}/Analytics/PHGPostHogConfiguration.swift | 0 Riot/{Managers => Modules}/Analytics/PostHogAnalyticsClient.swift | 0 15 files changed, 0 insertions(+), 0 deletions(-) rename Riot/{Managers => Modules}/Analytics/Analytics.swift (100%) rename Riot/{Managers => Modules}/Analytics/AnalyticsClientProtocol.swift (100%) rename Riot/{Managers => Modules}/Analytics/AnalyticsScreen.swift (100%) rename Riot/{Managers => Modules}/Analytics/AnalyticsScreenTimer.swift (100%) rename Riot/{Managers => Modules}/Analytics/AnalyticsService.swift (100%) rename Riot/{Managers => Modules}/Analytics/AnalyticsSettings.swift (100%) rename Riot/{Managers => Modules}/Analytics/AnalyticsUIElement.swift (100%) rename Riot/{Managers => Modules}/Analytics/DecryptionFailure.swift (100%) rename Riot/{Managers => Modules}/Analytics/DecryptionFailureTracker.h (100%) rename Riot/{Managers => Modules}/Analytics/DecryptionFailureTracker.m (100%) rename Riot/{Managers => Modules}/Analytics/Helpers/JoinedRoomSize+MemberCount.swift (100%) rename Riot/{Managers => Modules}/Analytics/Helpers/MXCallHangupReason+Analytics.swift (100%) rename Riot/{Managers => Modules}/Analytics/Helpers/MXTaskProfileName+Analytics.swift (100%) rename Riot/{Managers => Modules}/Analytics/PHGPostHogConfiguration.swift (100%) rename Riot/{Managers => Modules}/Analytics/PostHogAnalyticsClient.swift (100%) diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift similarity index 100% rename from Riot/Managers/Analytics/Analytics.swift rename to Riot/Modules/Analytics/Analytics.swift diff --git a/Riot/Managers/Analytics/AnalyticsClientProtocol.swift b/Riot/Modules/Analytics/AnalyticsClientProtocol.swift similarity index 100% rename from Riot/Managers/Analytics/AnalyticsClientProtocol.swift rename to Riot/Modules/Analytics/AnalyticsClientProtocol.swift diff --git a/Riot/Managers/Analytics/AnalyticsScreen.swift b/Riot/Modules/Analytics/AnalyticsScreen.swift similarity index 100% rename from Riot/Managers/Analytics/AnalyticsScreen.swift rename to Riot/Modules/Analytics/AnalyticsScreen.swift diff --git a/Riot/Managers/Analytics/AnalyticsScreenTimer.swift b/Riot/Modules/Analytics/AnalyticsScreenTimer.swift similarity index 100% rename from Riot/Managers/Analytics/AnalyticsScreenTimer.swift rename to Riot/Modules/Analytics/AnalyticsScreenTimer.swift diff --git a/Riot/Managers/Analytics/AnalyticsService.swift b/Riot/Modules/Analytics/AnalyticsService.swift similarity index 100% rename from Riot/Managers/Analytics/AnalyticsService.swift rename to Riot/Modules/Analytics/AnalyticsService.swift diff --git a/Riot/Managers/Analytics/AnalyticsSettings.swift b/Riot/Modules/Analytics/AnalyticsSettings.swift similarity index 100% rename from Riot/Managers/Analytics/AnalyticsSettings.swift rename to Riot/Modules/Analytics/AnalyticsSettings.swift diff --git a/Riot/Managers/Analytics/AnalyticsUIElement.swift b/Riot/Modules/Analytics/AnalyticsUIElement.swift similarity index 100% rename from Riot/Managers/Analytics/AnalyticsUIElement.swift rename to Riot/Modules/Analytics/AnalyticsUIElement.swift diff --git a/Riot/Managers/Analytics/DecryptionFailure.swift b/Riot/Modules/Analytics/DecryptionFailure.swift similarity index 100% rename from Riot/Managers/Analytics/DecryptionFailure.swift rename to Riot/Modules/Analytics/DecryptionFailure.swift diff --git a/Riot/Managers/Analytics/DecryptionFailureTracker.h b/Riot/Modules/Analytics/DecryptionFailureTracker.h similarity index 100% rename from Riot/Managers/Analytics/DecryptionFailureTracker.h rename to Riot/Modules/Analytics/DecryptionFailureTracker.h diff --git a/Riot/Managers/Analytics/DecryptionFailureTracker.m b/Riot/Modules/Analytics/DecryptionFailureTracker.m similarity index 100% rename from Riot/Managers/Analytics/DecryptionFailureTracker.m rename to Riot/Modules/Analytics/DecryptionFailureTracker.m diff --git a/Riot/Managers/Analytics/Helpers/JoinedRoomSize+MemberCount.swift b/Riot/Modules/Analytics/Helpers/JoinedRoomSize+MemberCount.swift similarity index 100% rename from Riot/Managers/Analytics/Helpers/JoinedRoomSize+MemberCount.swift rename to Riot/Modules/Analytics/Helpers/JoinedRoomSize+MemberCount.swift diff --git a/Riot/Managers/Analytics/Helpers/MXCallHangupReason+Analytics.swift b/Riot/Modules/Analytics/Helpers/MXCallHangupReason+Analytics.swift similarity index 100% rename from Riot/Managers/Analytics/Helpers/MXCallHangupReason+Analytics.swift rename to Riot/Modules/Analytics/Helpers/MXCallHangupReason+Analytics.swift diff --git a/Riot/Managers/Analytics/Helpers/MXTaskProfileName+Analytics.swift b/Riot/Modules/Analytics/Helpers/MXTaskProfileName+Analytics.swift similarity index 100% rename from Riot/Managers/Analytics/Helpers/MXTaskProfileName+Analytics.swift rename to Riot/Modules/Analytics/Helpers/MXTaskProfileName+Analytics.swift diff --git a/Riot/Managers/Analytics/PHGPostHogConfiguration.swift b/Riot/Modules/Analytics/PHGPostHogConfiguration.swift similarity index 100% rename from Riot/Managers/Analytics/PHGPostHogConfiguration.swift rename to Riot/Modules/Analytics/PHGPostHogConfiguration.swift diff --git a/Riot/Managers/Analytics/PostHogAnalyticsClient.swift b/Riot/Modules/Analytics/PostHogAnalyticsClient.swift similarity index 100% rename from Riot/Managers/Analytics/PostHogAnalyticsClient.swift rename to Riot/Modules/Analytics/PostHogAnalyticsClient.swift From bee737f97d81cc16ca1764fee3bf04eb20d9f276 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 16 Dec 2021 16:58:29 +0000 Subject: [PATCH 032/109] Retain AnalyticsService. Fix coordinator retain cycle. --- Podfile.lock | 16 ++++++++-------- Riot/Modules/Analytics/Analytics.swift | 11 ++++++++++- Riot/Modules/Analytics/AnalyticsService.swift | 7 ++++++- Riot/Modules/TabBar/TabBarCoordinator.swift | 5 +++-- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index be012e6902..8ff43e554e 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -57,16 +57,16 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixSDK (0.20.13): - - MatrixSDK/Core (= 0.20.13) - - MatrixSDK/Core (0.20.13): + - MatrixSDK (0.20.15): + - MatrixSDK/Core (= 0.20.15) + - MatrixSDK/Core (0.20.15): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - OLMKit (~> 3.2.5) - Realm (= 10.16.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.20.13): + - MatrixSDK/JingleCallStack (0.20.15): - JitsiMeetSDK (= 3.10.2) - MatrixSDK/Core - OLMKit (3.2.5): @@ -116,8 +116,8 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.20.13) - - MatrixSDK/JingleCallStack (= 0.20.13) + - MatrixSDK (= 0.20.15) + - MatrixSDK/JingleCallStack (= 0.20.15) - OLMKit - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) @@ -209,7 +209,7 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: 945f082654830d7ae3a6e1e068b6dc22b2eae932 + MatrixSDK: 2f4d3aacb1c53e2785f0be71d24b8e62e5c5c056 OLMKit: 9fb4799c4a044dd2c06bda31ec31a12191ad30b5 PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d @@ -225,6 +225,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: f4ad67860350a28588e177245d1d0aff0fdcf186 +PODFILE CHECKSUM: e60814fe2084a7dca3f82c3a1c4a1b763ae822c0 COCOAPODS: 1.11.2 diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index 4dd67a5edb..535ca7b2a4 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -29,6 +29,9 @@ import AnalyticsEvents /// The analytics client to send events with. private var client: AnalyticsClientProtocol = PostHogAnalyticsClient() + /// The service used to interact with account data settings. + private var service: AnalyticsService? + /// Whether or not the object is enabled and sending events to the server. var isRunning: Bool { client.isRunning } @@ -93,12 +96,18 @@ import AnalyticsEvents else { return } let service = AnalyticsService(session: session) - service.settings { result in + self.service = service + + service.settings { [weak self] result in + guard let self = self else { return } + switch result { case .success(let settings): self.identify(with: settings) + self.service = nil case .failure: MXLog.error("[Analytics] Failed to use analytics settings. Will continue to run without analytics ID.") + self.service = nil } } } diff --git a/Riot/Modules/Analytics/AnalyticsService.swift b/Riot/Modules/Analytics/AnalyticsService.swift index 820102c852..bf24edfd15 100644 --- a/Riot/Modules/Analytics/AnalyticsService.swift +++ b/Riot/Modules/Analytics/AnalyticsService.swift @@ -60,7 +60,12 @@ class AnalyticsService { var eventDictionary = settings.dictionary eventDictionary[AnalyticsSettings.Constants.idKey] = id - session.setAccountData(eventDictionary, forType: AnalyticsSettings.eventType) { + session.setAccountData(eventDictionary, forType: AnalyticsSettings.eventType) { [weak self] in + guard let self = self else { + completion(.failure(AnalyticsServiceError.unknown)) + return + } + MXLog.debug("[AnalyticsService] Successfully updated analytics settings in account data.") let settings = AnalyticsSettings(accountData: self.session.accountData) completion(.success(settings)) diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 0030b92568..ac72eb2eb9 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -491,8 +491,9 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { private func presentAnalyticsPrompt(with session: MXSession) { let parameters = AnalyticsPromptCoordinatorParameters(session: session, navigationRouter: navigationRouter) let coordinator = AnalyticsPromptCoordinator(parameters: parameters) - coordinator.completion = { [weak self] in - self?.remove(childCoordinator: coordinator) + coordinator.completion = { [weak self, weak coordinator] in + guard let self = self, let coordinator = coordinator else { return } + self.remove(childCoordinator: coordinator) } coordinator.start() From 6dafffba84efa5e3ca72e6931a91ff2fa6113615 Mon Sep 17 00:00:00 2001 From: Linerly Date: Thu, 16 Dec 2021 10:14:19 +0000 Subject: [PATCH 033/109] Translated using Weblate (Indonesian) Currently translated at 100.0% (441 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/id/ --- .../Assets/MatrixKitAssets.bundle/id.lproj/MatrixKit.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/id.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/id.lproj/MatrixKit.strings index 6dc5772bdc..ef186655b7 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/id.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/id.lproj/MatrixKit.strings @@ -345,7 +345,7 @@ // Country picker "country_picker_title" = "Pilih sebuah negara"; "microphone_access_not_granted_for_voice_message" = "Pesan suara membutuhkan akses ke Mikrofon tetapi %@ tidak memiliki izin untuk menggunakannya"; -"local_contacts_access_discovery_warning" = "Untuk menemukan kontak Anda yang sudah menggunakan Matrix, %@ dapat mengirim alamat email dan nomor telepon di kontak Anda ke server identitas Matrix yang Anda pilih. Di mana saja yang didukung, data personal akan di-hash sebelum dikirim - mohon cek kebijakan privasi identitas server Anda untuk detail lainnya."; +"local_contacts_access_discovery_warning" = "Untuk menemukan kontak Anda yang sudah menggunakan Matrix, %@ dapat mengirim alamat email dan nomor telepon di kontak Anda ke server identitas Matrix yang Anda pilih. Di mana saja yang didukung, data personal akan di-hash sebelum dikirim — mohon cek kebijakan privasi identitas server Anda untuk detail lainnya."; "local_contacts_access_discovery_warning_title" = "Penemuan pengguna"; "local_contacts_access_not_granted" = "Penemuan pengguna dari kontak lokal membutuhkan akses ke kontak Anda tetapi %@ tidak memiliki izin untuk menggunakannya"; "microphone_access_not_granted_for_call" = "Panggilan membutuhkan akses ke Mikrofon tetapi %@ tidak memiliki izin untuk menggunakannya"; From 4b63c5f6ee84bacebb4544b7ef3b0e57accc72e9 Mon Sep 17 00:00:00 2001 From: Denys Nykula Date: Thu, 16 Dec 2021 17:34:46 +0000 Subject: [PATCH 034/109] Translated using Weblate (Ukrainian) Currently translated at 100.0% (441 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/uk/ --- .../uk.lproj/MatrixKit.strings | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings index 26aef08f32..3d08db3f7a 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings @@ -546,3 +546,15 @@ "room_error_timeline_event_not_found" = "Застосунок намагався завантажити певну точку стрічки у цій кімнаті, але не зміг її знайти"; "room_error_timeline_event_not_found_title" = "Не вдалося завантажити позицію стрічки"; "room_error_cannot_load_timeline" = "Не вдалося завантажити стрічку"; +"ssl_only_accept" = "Приймайте сертифікат ЛИШЕ якщо адміністратор сервера оприлюднив відбиток, збіжний із наведеним угорі."; +"ssl_expected_existing_expl" = "Сертифікат, якому ви довіряли раніше, змінився на недовірений. Можливо, сервер оновив сертифікат. Запитайте в адміністратора, який відбиток очікуваний."; +"ssl_unexpected_existing_expl" = "Сертифікат відрізняється від довіреного вашим телефоном. Це ВКРАЙ НЕЗВИЧНО. Радимо НЕ ПРИЙМАТИ цей новий сертифікат."; +"ssl_cert_new_account_expl" = "Якщо адміністратор сервера каже, що так має бути, переконайтесь, що відбиток знизу збігається з відбитком адміністратора."; +"ssl_cert_not_trust" = "Це може означати, що хтось зловмисно перехоплює ваш трафік або ваш телефон не довіряє сертифікату, наданому віддаленим сервером."; + +// contacts list screen +"invitation_message" = "Поговорімо в matrix? Сайт https://matrix.org описує, як це зробити."; +"local_contacts_access_discovery_warning" = "Щоб виявляти, які ваші контакти вже в Matrix, %@ може надсилати адреси е-пошти й номери телефонів із вашої адресної книги на обраний сервер ідентифікації Matrix. Особисті дані хешуються перед надсиланням, якщо це підтримується: перевірте політику приватності свого сервера ідентифікації, щоб дізнатися більше."; +"local_contacts_access_not_granted" = "Для пошуку користувачів серед локальних контактів потрібен доступ до ваших контактів, але %@ не має такого дозволу"; +"e2e_export_prompt" = "Це дає змогу експортувати в локальний файл ключі до повідомлень, отриманих вами в зашифрованих кімнатах. Тоді ви зможете імпортувати файл до іншого клієнта Matrix у майбутньому, і той клієнт також зможе розшифрувати ці повідомлення.\nЕкспортований файл дасть змогу всім, хто його прочитає, розшифрувати всі видимі вам зашифровані повідомлення."; +"e2e_import_prompt" = "Це дає змогу імпортувати ключі шифрування, які ви раніше експортували з іншого клієнта Matrix. Тоді ви зможете розшифрувати всі повідомлення, які міг розшифрувати той клієнт.\nФайл експорту захищений парольною фразою. Введіть парольну фразу сюди, щоб розшифрувати файл."; From bde88c735bdae804a3d98d1a72f03bffa9a77273 Mon Sep 17 00:00:00 2001 From: artevaeckt Date: Wed, 15 Dec 2021 08:29:06 +0000 Subject: [PATCH 035/109] Translated using Weblate (German) Currently translated at 98.0% (1334 of 1361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 63ea02f51f..7c7c6aef57 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -208,7 +208,7 @@ "settings_ignored_users" = "IGNORIERTE NUTZER"; "settings_contacts" = "LOKALE KONTAKTE"; "settings_advanced" = "ERWEITERT"; -"settings_other" = "WEITERES"; +"settings_other" = "Weiteres"; "settings_devices" = "SITZUNGEN"; "settings_cryptography" = "VERSCHLÜSSELUNG"; "settings_sign_out" = "Abmelden"; @@ -1404,7 +1404,7 @@ "event_formatter_call_incoming_video" = "Eingehender Videoanruf"; "event_formatter_call_has_ended_with_time" = "Anruf beendet • %@"; "voice_message_stop_locked_mode_recording" = "Klicke, um die Aufnahme zu starten oder stoppen"; -"settings_device_notifications" = "Gerätbenachrichtigungen"; +"settings_device_notifications" = "Gerätebenachrichtigungen"; "voice_message_lock_screen_placeholder" = "Sprachnachricht"; "voice_message_remaining_recording_time" = "%@s übrig"; @@ -1439,7 +1439,7 @@ "settings_notify_me_for" = "Benachrichtige mich bei"; "settings_mentions_and_keywords" = "Erwähnungen und Schlüsselwörter"; "settings_confirm_media_size_description" = "Wenn dies aktiviert ist, wirst du beim Senden von Bildern und Videos gefragt, in welcher Größe sie gesendet werden sollen."; -"settings_confirm_media_size" = "Größe beim Senden bestätigen"; +"settings_confirm_media_size" = "Größe beim Senden auswählen"; "settings_notifications" = "BENACHRICHTIGUNGEN"; "settings_mentions_and_keywords_encryption_notice" = "Auf deinem Mobilgerät wirst du keine Benachrichtigungen für Erwähnungen und Schlüsselwörter in verschlüsselten Räumen erhalten."; "version_check_modal_subtitle_supported" = "Wir haben daran gearbeitet %@ zu verbessern um ein schnelleres und bereinigteres Erlebnis zu schaffen. Leider ist deine aktuelle iOS-Version mit einigen dieser Verbesserungen nicht kompatibel und wird daher nicht mehr unterstützt werden.\nWir empfehlen dir die Aktualisierung deines Betriebssystems um %@ weiterhin vollumfänglich zu nutzen."; @@ -1507,3 +1507,18 @@ "find_your_contacts_button_title" = "Finde deine Kontakte"; "contacts_address_book_permission_denied_alert_message" = "Um Kontakte zu aktivieren, öffne die Einstellungen deines Gerätes."; "contacts_address_book_permission_denied_alert_title" = "Kontakte deaktiviert"; +"poll_edit_form_create_options" = "Erstelle Optionen"; +"settings_discovery_accept_terms" = "Bedingungen des Identitätsservers akzeptieren"; +"find_your_contacts_message" = "Lass dir von %@ deine Kontakte anzeigen um schnell mit denen zu chatten, die du am besten kennst."; +"poll_timeline_votes_count" = "%lu Stimmen"; +"poll_timeline_one_vote" = "1 Stimme"; +"poll_edit_form_add_option" = "Option hinzufügen"; +"poll_edit_form_option_number" = "Option %lu"; +"poll_edit_form_question_or_topic" = "Frage oder Thematik"; +"room_event_action_end_poll" = "Umfrage beenden"; +"room_event_action_remove_poll" = "Umfrage entfernen"; + +// Mark: - Polls + +"poll_edit_form_create_poll" = "Umfrage erstellen"; +"settings_labs_enabled_polls" = "Umfragen"; From bcd5a3ccd7ebf6828e52a246b2b5dd13054c6e97 Mon Sep 17 00:00:00 2001 From: sr093906 Date: Wed, 15 Dec 2021 01:10:00 +0000 Subject: [PATCH 036/109] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (1361 of 1361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hans/ --- Riot/Assets/zh_Hans.lproj/Vector.strings | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index f54deca1f6..dfc1d66597 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -1548,7 +1548,7 @@ "space_home_show_all_rooms" = "显示所有聊天室"; "room_event_action_forward" = "转发"; "poll_edit_form_add_option" = "添加选项"; -"poll_edit_form_option_number" = "选项 %d"; +"poll_edit_form_option_number" = "选项 %lu"; "poll_edit_form_create_options" = "创建选项"; "poll_edit_form_input_placeholder" = "写些东西"; "poll_edit_form_question_or_topic" = "问题或话题"; @@ -1562,3 +1562,24 @@ "share_extension_low_quality_video_title" = "将以低画质发送视频"; "settings_discovery_accept_terms" = "接受身份服务器条款"; "settings_about" = "关于"; +"poll_timeline_not_closed_action" = "好"; +"poll_timeline_not_closed_subtitle" = "请再试一次"; +"poll_timeline_not_closed_title" = "结束投票失败"; +"poll_timeline_vote_not_registered_action" = "好"; +"poll_timeline_vote_not_registered_subtitle" = "抱歉,您的投票未登记,请重试"; +"poll_timeline_vote_not_registered_title" = "投票未登记"; +"poll_timeline_total_final_results" = "基于 %lu 票的最终结果"; +"poll_timeline_total_final_results_one_vote" = "基于 1 票的最终结果"; +"poll_timeline_total_votes_not_voted" = "已有 %lu 票。投票查看结果"; +"poll_timeline_total_one_vote_not_voted" = "已有 1 票。 投票查看结果"; +"poll_timeline_total_votes" = "%lu 票"; +"poll_timeline_total_one_vote" = "1 票"; +"poll_timeline_total_no_votes" = "尚无投票"; +"poll_timeline_votes_count" = "%lu 票"; +"poll_timeline_one_vote" = "1 票"; +"poll_edit_form_post_failure_action" = "好"; +"poll_edit_form_post_failure_subtitle" = "请再试一次"; +"poll_edit_form_post_failure_title" = "发布投票失败"; +"settings_labs_enabled_polls" = "投票"; +"room_event_action_end_poll" = "结束投票"; +"room_event_action_remove_poll" = "删除投票"; From 50f049e09e14b261f0267a4f78302629373208ef Mon Sep 17 00:00:00 2001 From: random Date: Wed, 15 Dec 2021 13:25:00 +0000 Subject: [PATCH 037/109] Translated using Weblate (Italian) Currently translated at 100.0% (1361 of 1361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ --- Riot/Assets/it.lproj/Vector.strings | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index ce4d6c96d2..46aca8d95e 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -1504,7 +1504,7 @@ "space_home_show_all_rooms" = "Mostra tutte le stanze"; "room_event_action_forward" = "Inoltra"; "poll_edit_form_add_option" = "Aggiungi opzione"; -"poll_edit_form_option_number" = "Opzione %d"; +"poll_edit_form_option_number" = "Opzione %lu"; "poll_edit_form_create_options" = "Crea opzioni"; "poll_edit_form_input_placeholder" = "Scrivi qualcosa"; "poll_edit_form_question_or_topic" = "Domanda o argomento"; @@ -1518,3 +1518,24 @@ "share_extension_low_quality_video_title" = "Il video verrà inviato in bassa qualità"; "settings_discovery_accept_terms" = "Accetta termini del server d'identità"; "settings_about" = "INFORMAZIONI"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_not_closed_subtitle" = "Riprova"; +"poll_timeline_not_closed_title" = "Chiusura del sondaggio fallita"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_timeline_vote_not_registered_subtitle" = "Spiacenti, il tuo voto non è stato registrato, riprova"; +"poll_timeline_vote_not_registered_title" = "Voto non registrato"; +"poll_timeline_total_final_results" = "Risultato finale basato su %lu voti"; +"poll_timeline_total_final_results_one_vote" = "Risultato finale basato su 1 voto"; +"poll_timeline_total_votes_not_voted" = "%lu voti inviati. Vota per vedere i risultati"; +"poll_timeline_total_one_vote_not_voted" = "1 voto inviato. Vota per vedere i risultati"; +"poll_timeline_total_votes" = "%lu voti"; +"poll_timeline_total_one_vote" = "1 voto"; +"poll_timeline_total_no_votes" = "Nessun voto"; +"poll_timeline_votes_count" = "%lu voti"; +"poll_timeline_one_vote" = "1 voto"; +"poll_edit_form_post_failure_action" = "OK"; +"poll_edit_form_post_failure_subtitle" = "Riprova"; +"poll_edit_form_post_failure_title" = "Invio del sondaggio fallito"; +"settings_labs_enabled_polls" = "Sondaggi"; +"room_event_action_end_poll" = "Termina sondaggio"; +"room_event_action_remove_poll" = "Rimuovi sondaggio"; From 456d6abc0cb740046bc0deab63fb0b187db7d5bf Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Tue, 14 Dec 2021 21:23:18 +0000 Subject: [PATCH 038/109] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1361 of 1361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ --- Riot/Assets/pt_BR.lproj/Vector.strings | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index ba473313ec..acafe6153c 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -1501,7 +1501,7 @@ "space_home_show_all_rooms" = "Mostrar todas as salas"; "room_event_action_forward" = "Encaminhar"; "poll_edit_form_add_option" = "Adicionar opção"; -"poll_edit_form_option_number" = "Opção %d"; +"poll_edit_form_option_number" = "Opção %lu"; "poll_edit_form_create_options" = "Criar opções"; "poll_edit_form_input_placeholder" = "Escreva algo"; "poll_edit_form_question_or_topic" = "Pergunta ou tópico"; @@ -1515,3 +1515,24 @@ "share_extension_low_quality_video_title" = "Vídeo vai ser enviado em baixa qualidade"; "settings_discovery_accept_terms" = "Aceitar Termos de Servidor de Identidade"; "settings_about" = "SOBRE"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_not_closed_subtitle" = "Por favor tente de novo"; +"poll_timeline_not_closed_title" = "Falha para terminar sondagem"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_timeline_vote_not_registered_subtitle" = "Desculpe, seu voto não foi registrado, por favor tente de novo"; +"poll_timeline_vote_not_registered_title" = "Voto não registrado"; +"poll_timeline_total_final_results" = "Resultados finais baseados em %lu votos"; +"poll_timeline_total_final_results_one_vote" = "Resultados finais baseados em 1 voto"; +"poll_timeline_total_votes_not_voted" = "%lu votos lançados. Vote para ver os resultados"; +"poll_timeline_total_one_vote_not_voted" = "1 voto lançado. Vote para ver os resultados"; +"poll_timeline_total_votes" = "%lu votos lançados"; +"poll_timeline_total_one_vote" = "1 voto lançado"; +"poll_timeline_total_no_votes" = "Nenhum voto lançado"; +"poll_timeline_votes_count" = "%lu votos"; +"poll_timeline_one_vote" = "1 voto"; +"poll_edit_form_post_failure_action" = "OK"; +"poll_edit_form_post_failure_subtitle" = "Por favor tente de novo"; +"poll_edit_form_post_failure_title" = "Falha para postar sondagem"; +"settings_labs_enabled_polls" = "Sondagens"; +"room_event_action_end_poll" = "Terminar sondagem"; +"room_event_action_remove_poll" = "Remover sondagem"; From 28ccc7d618a7c22195ec90c2b6e374d6502cbc15 Mon Sep 17 00:00:00 2001 From: Denys Nykula Date: Thu, 16 Dec 2021 18:22:13 +0000 Subject: [PATCH 039/109] Translated using Weblate (Ukrainian) Currently translated at 75.5% (1028 of 1361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index abf77edfd6..5406b4ab4a 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -1022,3 +1022,50 @@ "room_details_history_section_members_only" = "Лише учасники (від часу вибору цієї опції)"; "room_details_history_section_anyone" = "Будь-хто"; "room_details_history_section" = "Хто може переглядати історію?"; +"identity_server_settings_alert_disconnect" = "Від'єднатися від сервера ідентифікації %@?"; +"identity_server_settings_alert_disconnect_title" = "Від'єднатися від сервера ідентифікації"; +"identity_server_settings_alert_change" = "Від'єднатися від сервера ідентифікації %1$@ й натомість під'єднатися до %2$@?"; +"identity_server_settings_alert_change_title" = "Змінити сервер ідентифікації"; +"identity_server_settings_alert_no_terms" = "Обраний вами сервер ідентифікації не має жодних умов використання. Продовжуйте лише якщо довіряєте власнику сервісу."; +"identity_server_settings_alert_no_terms_title" = "Сервер ідентифікації не має умов використання"; +"identity_server_settings_disconnect_info" = "Після від'єднання від сервера ідентифікації вас більше не зможуть знаходити інші користувачі, а ви не зможете запрошувати інших за е-поштою чи телефоном."; +"identity_server_settings_no_is_description" = "Зараз ви не використовуєте сервер ідентифікації. Щоб знаходити наявні контакти й вони знаходили вас, додайте сервер угорі."; +"identity_server_settings_description" = "Ви користуєтесь %@, щоб знаходити людей за наявними контактами й вони знаходили вас."; + +// AuthenticatedSessionViewControllerFactory +"authenticated_session_flow_not_supported" = "Цей застосунок не підтримує способу входу, доступного на вашому домашньому сервері."; +"security_settings_user_password_description" = "Підтвердьте свою особу, ввівши пароль свого облікового запису"; +"security_settings_coming_soon" = "Вибачте, ця дія ще не доступна в %@ iOS. Якщо налаштуєте це іншим клієнтом Matrix, %@ iOS зможе також це використати."; +"security_settings_complete_security_alert_message" = "Спершу слід доповнити захист вашого поточного сеансу."; +"security_settings_blacklist_unverified_devices_description" = "Звірте всі сеанси користувача, щоб позначити його довіреним і надіслати йому повідомлення."; +"security_settings_crosssigning_bootstrap" = "Налаштувати"; +"security_settings_crosssigning_info_ok" = "Перехресне підписування готове до використання."; +"security_settings_crosssigning_info_trusted" = "Перехресне підписування увімкнено. Ви можете робити інших користувачів і свої інші сеанси довіреними на підставі перехресного підпису, але ви не можете перехресно підписувати цим сеансом, бо в нього ще нема закритих ключів перехресного підписування. Доповніть захист цього сеансу."; +"security_settings_crosssigning_info_exists" = "Ваш обліковий запис має ідентичність перехресного підписування, але вона ще не довірена цим сеансом. Доповніть захист цього сеансу."; +"security_settings_secure_backup_description" = "Зробіть резервну копію своїх ключів шифрування й даних облікового запису на випадок втрати доступу до своїх сеансів. Ваші ключі будуть захищені унікальним ключем безпеки."; +"security_settings_crypto_sessions_description_2" = "Якщо не впізнаєте вхід, скиньте пароль і налаштування безпечного резервного копіювання."; +"settings_identity_server_no_is_description" = "Зараз ви не використовуєте сервер ідентифікації. Щоб знаходити наявні контакти й вони знаходили вас, додайте сервер угорі."; +"settings_identity_server_description" = "Налаштований угорі сервер ідентифікації дає вам змогу знаходити людей за наявними контактами, а їм знаходити вас."; +"settings_discovery_three_pid_details_information_phone_number" = "Керуйте параметрами для цього номера телефону, за яким інші користувачі можуть вас знаходити й запрошувати до кімнат. Додавайте й видаляйте номери в Облікових записах."; +"settings_discovery_three_pid_details_information_email" = "Керуйте параметрами для цієї адреси е-пошти, за якою інші користувачі можуть вас знаходити й запрошувати до кімнат. Додавайте й видаляйте адреси в Облікових записах."; +"settings_discovery_three_pids_management_information_part1" = "Керуйте, за якими адресами е-пошти й номерами телефону інші користувачі зможуть вас знаходити й запрошувати до кімнат. Щоб додати адреси й номери в цей список чи вилучити наявні, перейдіть у "; +"settings_discovery_no_identity_server" = "Зараз ви не використовуєте сервер ідентифікації. Щоб наявні контакти могли вас знаходити, додайте такий сервер."; +"settings_key_backup_info_not_valid" = "Цей сеанс не створює резервної копії ваших ключів, але у вас уже є резервна копія, яку ви можете відновити й поповнювати далі."; +"settings_key_backup_info_version" = "Версія резервного копіювання ключів: %@"; +"settings_labs_message_reaction" = "Реагувати на повідомлення за допомогою емодзі"; +"settings_contacts_enable_sync_description" = "Це використовуватиме ваш сервер ідентифікації, щоб ви знаходили своїх контактів, а вони вас."; +"settings_integrations_allow_description" = "Використовуйте менеджер інтеграцій (%@), щоб керувати ботами, мостами, віджетами й пакунками наліпок.\n\nМенеджери інтеграцій отримують ваші параметри й можуть змінювати віджети, надсилати запрошення до кімнат і видавати дозволи від вашого імені."; +"settings_calls_stun_server_fallback_description" = "Дозволити допоміжний сервер викликів %@, коли ваш домашній сервер не надає свого (ваша IP-адреса ставатиме відомою при виклику)."; +"settings_callkit_info" = "Отримувати вхідні виклики, не розблоковуючи екрану. Перегляньте свої виклики (%@) в історії викликів системи. Якщо iCloud увімкнено, ця історія викликів надсилатиметься Apple."; +"settings_confirm_media_size_description" = "Коли це ввімкнено, при надсиланні зображень чи відео вам пропонуватиметься підтвердити їхній розмір."; +"settings_three_pids_management_information_part2" = "Знаходження"; +"settings_config_user_id" = "Ви ввійшли як %@"; +"unknown_devices_alert" = "Кімната містить сеанси, які досі не пройшли звірку.\nТобто нема гарантії, що ці сеанси належать користувачам, від імені яких вони створені.\nРадимо звірити кожен сеанс, перш ніж продовжити; але за потреби можете повторити надсилання повідомлення без звірки."; +"room_action_camera" = "Зробити світлину або відео"; +"room_ongoing_conference_call_with_close" = "Відбувається конференц-виклик. Приєднатись як %1$s чи %2$s. %@ його."; +"room_member_power_level_short_custom" = "Інше"; +"room_member_power_level_custom_in" = "Інше (%@) у %@"; +"room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Поки жоден сервер ідентифікації не налаштований, ви не можете почати бесіду з кимось за адресою е-пошти."; +"find_your_contacts_message" = "Дозвольте %@ показувати ваші контакти, щоб ви могли швидко почати бесіду з тими, кого знаєте найкраще."; +"find_your_contacts_title" = "Почніть із переліку своїх контактів"; +"store_full_description" = "Element — застосунок для листування й співпраці нового покоління:\n\n1. Надає вам контроль над збереженням вашої приватності\n2. Дає змогу спілкуватися з будь-ким у мережі Matrix і навіть за її межами, інтегруючись із такими застосунками, як Slack\n3. Оберігає вас від реклами, збору даних, бекдорів і прив'язаності до провайдера\n4. Захищає вас наскрізним шифруванням і звіркою інших перехресним підписуванням\n\nElement суттєво відрізняється від інших застосунків для листування й співпраці тим, що безцентровий і має відкритий код.\n\nElement дає змогу самостійно встановити сервер — чи обрати з публічних — щоб ви зберігали приватність своїх даних і розмов, власність і контроль над ними. Він надає вам доступ до відкритої мережі; тож ви можете спілкуватися з користувачами інших застосунків, не лише Element. А ще він добре захищений.\n\nElement здатен на це все завдяки своїй основі Matrix — стандарту відкритого, безцентрового спілкування.\n\nElement надає вам контроль, даючи змогу обрати, в кого зберігаються ваші розмови. В застосунку Element ви можете обрати між такими шляхами:\n\n1. Зареєструйте безкоштовний обліковий запис на публічному сервері matrix.org\n2. Самостійно розмістіть свій обліковий запис, встановивши сервер на власному обладнанні\n3. Отримайте обліковий запис на виділеному сервері, просто оформивши підписку на хостинг-платформу Element Matrix Services\n\nЧому Element?\n\nВОЛОДІЙТЕ СВОЇМИ ДАНИМИ: Ви обираєте, де зберігати свої дані й повідомлення. Ви володієте й керуєте ними, не якась МЕГАКОРПОРАЦІЯ, що аналізує ваші дані й передає їх сторонніх особам.\n\nВІДКРИТЕ ЛИСТУВАННЯ Й СПІВПРАЦЯ: Можете розмовляти з будь-ким іншим у мережі Matrix незалежно від того, використовують вони Element, інший застосунок Matrix чи навіть сторонню систему листування на зразок Slack, IRC чи XMPP.\n\nСУПЕРБЕЗПЕКА: Дійсно наскрізне шифрування (лише учасники розмови можуть розшифрувати повідомлення) й звірка пристроїв учасників розмови перехресним підписуванням.\n\nДОСКОНАЛЕ СПІЛКУВАННЯ: Листуйтеся, робіть голосові й відеовиклики, діліться файлами, транслюйте екран, під'єднуйте різноманітні інтеграції, ботів і віджети. Розбудовуйте кімнати, спільноти, будьте на зв'язку й досягайте цілей.\n\nСКРІЗЬ, ДЕ ВИ: Будьте на зв'язку, де б ви не були, завдяки повній синхронізації історії повідомлень між усіма вашим пристроями й онлайн-клієнтом https://element.io/app."; From 429ee95c92041e1fd8cf18c2a7469853529d3f23 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Wed, 15 Dec 2021 19:50:57 +0000 Subject: [PATCH 040/109] Translated using Weblate (Ukrainian) Currently translated at 75.5% (1028 of 1361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 161 +++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 5406b4ab4a..cbc3c3bac3 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -388,7 +388,7 @@ "settings_identity_server_settings" = "СЕРВЕР ІДЕНТИФІКАЦІЇ"; "settings_calls_settings" = "ВИКЛИКИ"; "settings_notifications" = "СПОВІЩЕННЯ"; -"settings_user_settings" = "НАЛАШТУВАННЯ КОРИСТУВАЧА"; +"settings_user_settings" = "КОРИСТУВАЦЬКІ НАЛАШТУВАННЯ"; "event_formatter_call_connecting" = "З'єднання…"; "settings_config_identity_server" = "Сервер ідентифікації %@"; "settings_config_home_server" = "Домашній сервер %@"; @@ -1069,3 +1069,162 @@ "find_your_contacts_message" = "Дозвольте %@ показувати ваші контакти, щоб ви могли швидко почати бесіду з тими, кого знаєте найкраще."; "find_your_contacts_title" = "Почніть із переліку своїх контактів"; "store_full_description" = "Element — застосунок для листування й співпраці нового покоління:\n\n1. Надає вам контроль над збереженням вашої приватності\n2. Дає змогу спілкуватися з будь-ким у мережі Matrix і навіть за її межами, інтегруючись із такими застосунками, як Slack\n3. Оберігає вас від реклами, збору даних, бекдорів і прив'язаності до провайдера\n4. Захищає вас наскрізним шифруванням і звіркою інших перехресним підписуванням\n\nElement суттєво відрізняється від інших застосунків для листування й співпраці тим, що безцентровий і має відкритий код.\n\nElement дає змогу самостійно встановити сервер — чи обрати з публічних — щоб ви зберігали приватність своїх даних і розмов, власність і контроль над ними. Він надає вам доступ до відкритої мережі; тож ви можете спілкуватися з користувачами інших застосунків, не лише Element. А ще він добре захищений.\n\nElement здатен на це все завдяки своїй основі Matrix — стандарту відкритого, безцентрового спілкування.\n\nElement надає вам контроль, даючи змогу обрати, в кого зберігаються ваші розмови. В застосунку Element ви можете обрати між такими шляхами:\n\n1. Зареєструйте безкоштовний обліковий запис на публічному сервері matrix.org\n2. Самостійно розмістіть свій обліковий запис, встановивши сервер на власному обладнанні\n3. Отримайте обліковий запис на виділеному сервері, просто оформивши підписку на хостинг-платформу Element Matrix Services\n\nЧому Element?\n\nВОЛОДІЙТЕ СВОЇМИ ДАНИМИ: Ви обираєте, де зберігати свої дані й повідомлення. Ви володієте й керуєте ними, не якась МЕГАКОРПОРАЦІЯ, що аналізує ваші дані й передає їх сторонніх особам.\n\nВІДКРИТЕ ЛИСТУВАННЯ Й СПІВПРАЦЯ: Можете розмовляти з будь-ким іншим у мережі Matrix незалежно від того, використовують вони Element, інший застосунок Matrix чи навіть сторонню систему листування на зразок Slack, IRC чи XMPP.\n\nСУПЕРБЕЗПЕКА: Дійсно наскрізне шифрування (лише учасники розмови можуть розшифрувати повідомлення) й звірка пристроїв учасників розмови перехресним підписуванням.\n\nДОСКОНАЛЕ СПІЛКУВАННЯ: Листуйтеся, робіть голосові й відеовиклики, діліться файлами, транслюйте екран, під'єднуйте різноманітні інтеграції, ботів і віджети. Розбудовуйте кімнати, спільноти, будьте на зв'язку й досягайте цілей.\n\nСКРІЗЬ, ДЕ ВИ: Будьте на зв'язку, де б ви не були, завдяки повній синхронізації історії повідомлень між усіма вашим пристроями й онлайн-клієнтом https://element.io/app."; +"security_settings_crosssigning_info_not_bootstrapped" = "Перехресне підписування ще не налаштовано."; +"security_settings_crosssigning" = "ПЕРЕХРЕСНЕ ПІДПИСУВАННЯ"; +"security_settings_backup" = "РЕЗЕРВНЕ КОПІЮВАННЯ ПОВІДОМЛЕНЬ"; +"security_settings_secure_backup_restore" = "Відновити з резервної копії"; +"key_verification_manually_verify_device_key_title" = "Ключ сеансу"; +"key_verification_manually_verify_device_id_title" = "ID сеансу"; +"identity_server_settings_alert_error_invalid_identity_server" = "%@ не дійсний сервер ідентифікації."; +"identity_server_settings_alert_error_terms_not_accepted" = "Ви повинні погодитися з умовами %@, щоб налаштувати сервер ідентифікації."; +"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "Усе одно відʼєднати"; +"identity_server_settings_alert_disconnect_button" = "Відʼєднати"; +"identity_server_settings_disconnect" = "Відʼєднати"; +"identity_server_settings_change" = "Змінити"; +"identity_server_settings_add" = "Додати"; +"identity_server_settings_place_holder" = "Введіть сервер ідентифікації"; + +// Identity server settings +"identity_server_settings_title" = "Сервер ідентифікації"; +"manage_session_sign_out" = "Вийти з цього сеансу"; +"manage_session_not_trusted" = "Не довірені"; +"manage_session_trusted" = "Довірені вами"; +"key_verification_manually_verify_device_name_title" = "Назва сеансу"; +"manage_session_name" = "Назва сеансу"; +"manage_session_info" = "ВІДОМОСТІ ПРО СЕАНС"; + +// Manage session +"manage_session_title" = "Керувати сеансом"; +"security_settings_complete_security_alert_title" = "Завершити налаштування безпеки"; +"user_verification_session_details_verify_action_current_user" = "Інтерактивна перевірка"; +"secrets_recovery_reset_action_part_2" = "Скинути все"; + +// MARK: - Secrets reset + +"secrets_reset_title" = "Скинути все"; +"cross_signing_setup_banner_subtitle" = "Спростіть перевірку інших своїх пристроїв"; + +// MARK: - Cross-signing + +// Banner + +"cross_signing_setup_banner_title" = "Налаштування шифрування"; +"secrets_reset_authentication_message" = "Введіть пароль свого облікового запису, щоб продовжити"; +"security_settings_blacklist_unverified_devices" = "Ніколи не надсилати повідомлення не довіреним сеансам"; +"security_settings_advanced" = "ДОДАТКОВО"; +"security_settings_export_keys_manually" = "Експорт ключів вручну"; +"security_settings_cryptography" = "КРИПТОГРАФІЯ"; +"security_settings_crosssigning_complete_security" = "Завершити налаштування безпеки"; +"security_settings_crosssigning_reset" = "Скинути"; +"secrets_reset_reset_action" = "Скинути"; +"security_settings_secure_backup_reset" = "Скинути"; +"settings_show_NSFW_public_rooms" = "Показувати загальнодоступні кімнати з делікатним вмістом"; +"settings_identity_server_no_is" = "Сервер ідентифікації не налаштований"; +"settings_discovery_three_pid_details_enter_sms_code_action" = "Введіть код активації з СМС"; +"settings_discovery_three_pid_details_cancel_email_validation_action" = "Скасувати перевірку е-пошти"; +"settings_key_backup_info_trust_signature_invalid_device_unverified" = "Резервна копія має недійсний підпис %@"; +"settings_key_backup_info_trust_signature_invalid_device_verified" = "Резервна копія має недійсний підпис %@"; +"settings_key_backup_info_trust_signature_valid_device_unverified" = "Резервна копія має підпис %@"; +"settings_key_backup_info_trust_signature_valid_device_verified" = "Резервна копія має дійсний підпис %@"; +"settings_send_crash_report" = "Надсилати анонімні дані про збої та користування"; +"settings_show_url_previews_description" = "Попередній перегляд виконується лише у кімнатах без шифрування."; +"settings_show_url_previews" = "Попередній перегляд вебсайтів"; +"settings_ui_theme_picker_message_match_system_theme" = "«Авто» застосовує тему вашого пристрою"; +"settings_ui_theme_picker_message_invert_colours" = "«Авто» застосовує налаштування вашого пристрою «Інвертувати кольори»"; +"settings_messages_containing_display_name" = "Моє показуване імʼя"; +"settings_discovery_three_pid_details_title_phone_number" = "Керувати номером телефону"; +"settings_discovery_three_pid_details_title_email" = "Керувати е-поштою"; +"settings_discovery_error_message" = "Сталася помилка. Повторіть спробу."; +"settings_discovery_three_pids_management_information_part2" = "Користувацькі налаштування"; +"settings_discovery_accept_terms" = "Погодитися з умовами використання сервера ідентифікації"; +"settings_discovery_terms_not_signed" = "Погодьтеся з умовами використання сервера ідентифікації (%@), щоб дозволити вашу виявність за електронною адресою та номером телефону."; +"settings_key_backup_delete_confirmation_prompt_msg" = "Ви впевнені? Ви втратите ваші зашифровані повідомлення якщо копія ключів не була створена коректно."; +"settings_key_backup_delete_confirmation_prompt_title" = "Видалити резервну копію"; +"settings_key_backup_button_connect" = "Налаштувати цьому сеансу резервне копіювання ключів"; +"settings_key_backup_button_delete" = "Видалити резервну копію"; +"settings_key_backup_button_restore" = "Відновити з резервної копії"; +"settings_key_backup_button_create" = "Почати використовувати резервне копіювання ключів"; +"settings_key_backup_info_trust_signature_valid" = "Резервна копія має чинний підпис цього сеансу"; +"settings_key_backup_info_trust_signature_unknown" = "Резервна копія містить підпис невідомого сеансу з ID: %@"; +"settings_key_backup_info_progress_done" = "Резервні копії всіх ключів створено"; +"settings_key_backup_info_progress" = "Резервне копіювання %@ ключів…"; +"security_settings_secure_backup_info_valid" = "Цей сеанс створює резервну копію ваших ключів."; +"settings_key_backup_info_valid" = "Цей сеанс створює резервну копію ваших ключів."; +"settings_key_backup_info_signout_warning" = "Створіть резервну копію ключів перед виходом, щоб не втратити їх."; +"settings_key_backup_info_none" = "Для цього сеансу не створюється резервна копія ваших ключів."; +"security_settings_secure_backup_info_checking" = "Перевірка…"; +"settings_key_backup_info_checking" = "Перевірка…"; +"settings_key_backup_info" = "Зашифровані повідомлення захищені наскрізним шифруванням. Лише ви та отримувачі повідомлень мають ключі для їх читання."; +"settings_deactivate_my_account" = "Деактивувати обліковий запис"; +"settings_enable_rageshake" = "Струснути пристрій, щоб повідомити про ваду"; +"settings_third_party_notices" = "Примітки третіх сторін"; +"settings_labs_enable_ringing_for_group_calls" = "Дзвінок групових викликів"; +"settings_labs_enabled_polls" = "Опитування"; +"settings_labs_create_conference_with_jitsi" = "Створити конференц-виклик за допомогою jitsi"; +"settings_labs_e2e_encryption_prompt_message" = "Щоб завершити налаштування шифрування вам потрібно повторно увійти."; +"settings_labs_e2e_encryption" = "Наскрізне шифрування"; +"settings_unignore_user" = "Показати всі повідомлення від %@?"; +"settings_ui_theme_picker_title" = "Вибрати тему"; +"settings_ui_theme_black" = "Чорна"; +"settings_ui_theme_dark" = "Темна"; +"settings_ui_theme_light" = "Світла"; +"settings_ui_theme_auto" = "Авто"; +"settings_ui_theme" = "Тема"; +"settings_ui_language" = "Мова"; +"settings_integrations_allow_button" = "Керування інтеграціями"; +"settings_calls_stun_server_fallback_button" = "Дозволити допоміжний сервер викликів"; +"settings_enable_callkit" = "Інтегрований виклик"; +"settings_new_keyword" = "Додати нове ключове слово"; +"settings_your_keywords" = "Ваші ключові слова"; +"settings_room_upgrades" = "Поліпшення кімнати"; +"settings_messages_by_a_bot" = "Повідомлення бота"; +"settings_call_invitations" = "Запрошення до виклику"; +"settings_room_invitations" = "Запрошення до кімнати"; +"settings_messages_containing_keywords" = "Ключові слова"; +"settings_messages_containing_user_name" = "Моє користувацьке імʼя"; +"settings_messages_containing_at_room" = "@room"; +"settings_encrypted_group_messages" = "Зашифровані групові повідомлення"; +"settings_group_messages" = "Групові повідомлення"; +"settings_other" = "Інше"; +"settings_mentions_and_keywords" = "Згадки та ключові слова"; +"settings_pin_rooms_with_unread" = "Закріплювати кімнати з новими повідомленнями"; +"settings_confirm_media_size" = "Підтверджувати розмір під час надсилання"; +"settings_discovery_settings" = "ВИЯВНІСТЬ"; +"room_preview_unlinked_email_warning" = "Запрошення надіслано на адресу %@, не повʼязану з цим обліковим записом. Ви можете увійти за допомогою іншого облікового запису або додати е-пошту до цього."; +"unknown_devices_verify" = "Перевірка…"; +"room_message_edits_history_title" = "Редагування повідомлення"; +"room_resource_usage_limit_reached_message_1_monthly_active_user" = "Цей домашній сервер досяг свого місячного обмеження активних користувачів, тож "; +"room_resource_usage_limit_reached_message_1_default" = "Цей домашній сервер досягнув одного зі своїх лімітів ресурсів, тож "; +"room_conference_call_no_power" = "Для керування конференц-викликами у цій кімнаті потрібен дозвіл"; +"room_ongoing_conference_call" = "Відбувається конференц-виклик. Приєднатись як %1$s чи %2$s."; +"room_participants_security_information_room_encrypted_for_dm" = "Повідомлення тут захищені наскрізним шифруванням.\n\nВаші повідомлення захищені замками, тож лише ви та отримувач маєте унікальні ключі для їхнього відмикання."; +"room_participants_security_information_room_encrypted" = "Повідомлення тут захищені наскрізним шифруванням.\n\nВаші повідомлення захищені замками, тож лише ви та отримувачі мають унікальні ключі для їхнього відмикання."; +"room_participants_action_security_status_complete_security" = "Завершити налаштування безпеки"; +"room_participants_idle" = "Неактивний"; +"settings_labs" = "ЛАБОРАТОРІЯ"; +"settings_about" = "ПРО"; +"settings_advanced" = "ДОДАТКОВО"; +"settings_phone_contacts" = "КОНТАКТИ ТЕЛЕФОНА"; +"settings_links" = "ПОСИЛАННЯ"; +"settings_sending_media" = "НАДСИЛАННЯ ЗОБРАЖЕНЬ І ВІДЕО"; +"room_preview_try_join_an_unknown_room" = "Ви намагаєтесь отримати доступ до %@. Бажаєте приєднатися, щоб взяти участь в обговоренні?"; +"room_preview_subtitle" = "Це попередній перегляд кімнати. Ви в режимі лише читання."; + +// Room Preview +"room_preview_invitation_format" = "%s запрошує вас приєднатися до цієї кімнати"; + +// Unknown devices +"unknown_devices_alert_title" = "Кімната містить невідомі сеанси"; +"external_link_confirmation_message" = "Посилання %@ спрямовує вас на інший сайт: %@.\n\nВи впевнені, що бажаєте продовжити?"; +"external_link_confirmation_title" = "Перевірте це посилання"; +"room_accessibility_hangup" = "Покласти слухавку"; +"room_resource_usage_limit_reached_message_contact_3" = " , щоб збільшити ліміт."; +"room_resource_usage_limit_reached_message_2" = "деякі користувачі не зможуть увійти."; +"room_resource_limit_exceeded_message_contact_3" = " , щоб продовжити користуватися цією службою."; +"room_resource_limit_exceeded_message_contact_2_link" = "зв’яжіться з адміністратором вашого сервера"; +"room_predecessor_link" = "Торкніться тут, щоб переглянути давніші повідомлення."; +"room_predecessor_information" = "Ця кімната — продовження спілкування в іншій кімнаті."; +"room_replacement_link" = "Спілкування продовжується тут."; +"room_replacement_information" = "Цю кімнату замінено й вона більше не активна."; +"room_event_action_end_poll" = "Завершити опитування"; +"room_event_action_remove_poll" = "Вилучити опитування"; From b23cc93c9dcbe683d5d14caa658fb6f793dce053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 14 Dec 2021 22:51:22 +0000 Subject: [PATCH 041/109] Translated using Weblate (Estonian) Currently translated at 100.0% (1361 of 1361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 9e209a1f2c..bf78485a30 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -1474,7 +1474,7 @@ "share_extension_low_quality_video_title" = "Saadame video madalama kvalitediga"; "settings_about" = "TEAVE MEIST"; "poll_edit_form_add_option" = "Lisa valik"; -"poll_edit_form_option_number" = "Valik %d"; +"poll_edit_form_option_number" = "Valik %lu"; "poll_edit_form_create_options" = "Koosta valikud"; "poll_edit_form_input_placeholder" = "Kirjuta midagi"; "poll_edit_form_question_or_topic" = "Küsimus või teema"; @@ -1484,3 +1484,24 @@ "poll_edit_form_create_poll" = "Koosta üks küsitlus"; "settings_discovery_accept_terms" = "Nõustu isikutuvastusserveri tingimustega"; +"poll_timeline_not_closed_action" = "Sobib"; +"poll_timeline_not_closed_subtitle" = "Palun proovi uuesti"; +"poll_timeline_not_closed_title" = "Küsitluse lõpetamine ei õnnestunud"; +"poll_timeline_vote_not_registered_action" = "Sobib"; +"poll_timeline_vote_not_registered_subtitle" = "Vabandust, aga sinu valik jäi salvestamata. Palun proovi uuesti"; +"poll_timeline_vote_not_registered_title" = "Hääl ei salvestunud"; +"poll_timeline_total_final_results" = "%lu'l häälel põhinev lõpptulemus"; +"poll_timeline_total_final_results_one_vote" = "Ühel häälel põhinev lõpptulemus"; +"poll_timeline_total_votes_not_voted" = "%lu hääletanut. Tulemuste nägemiseks osale ise ka küsitluses"; +"poll_timeline_total_one_vote_not_voted" = "1 hääletanu. Tulemuste nägemiseks osale ise ka küsitluses"; +"poll_timeline_total_votes" = "%lu hääletanut"; +"poll_timeline_total_one_vote" = "1 hääletanu"; +"poll_timeline_total_no_votes" = "Hääletanuid ei ole"; +"poll_timeline_votes_count" = "%lu häält"; +"poll_timeline_one_vote" = "1 hääl"; +"poll_edit_form_post_failure_action" = "Sobib"; +"poll_edit_form_post_failure_subtitle" = "Palun proovi uuesti"; +"poll_edit_form_post_failure_title" = "Küsitluse üleslaadimine ei õnnestunud"; +"settings_labs_enabled_polls" = "Küsitlused"; +"room_event_action_end_poll" = "Lõpeta küsitlus"; +"room_event_action_remove_poll" = "Kustuta küsitlus"; From c9d534e1ebc1809547e3d4f6c96b05638c7c3761 Mon Sep 17 00:00:00 2001 From: Linerly Date: Thu, 16 Dec 2021 10:11:51 +0000 Subject: [PATCH 042/109] Translated using Weblate (Indonesian) Currently translated at 100.0% (1361 of 1361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 62d27b60dc..c474eaf31d 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -102,7 +102,7 @@ "joined" = "Bergabung"; "collapse" = "tutup"; "store_promotional_text" = "Aplikasi perpesanan dan kolaborasi yang menjaga privasi, pada jaringan terbuka. Terdesentralisasi untuk Anda kendali. Tidak ada penambangan data, tidak ada pintu belakang dan tidak ada akses pihak ketiga."; -"store_full_description" = "Element adalah aplikasi messenger dan kolaborasi tipe baru yang:\n\n1. Menempatkan Anda dalam kendali untuk mempertahankan privasi Anda\n2. Memungkinkan Anda berkomunikasi dengan siapa pun di jaringan Matrix, dan bahkan di luar dengan mengintegrasikan dengan aplikasi seperti Slack\n3. Melindungi Anda dari iklan, menambangan data, pintu belakang, dan taman berdinding\n4. Mengamankan Anda melalui enkripsi ujung-ke-ujung, dengan penandatanganan silang untuk memverifikasi orang lain\n\nElement benar-benar berbeda dari aplikasi perpesanan dan kolaborasi lain karena Element terdesentralisasi dan sumber terbuka.\n\nElement memungkinkan Anda host sendiri - atau memilih host - sehingga Anda memiliki privasi, kepemilikan, dan kontrol data dan obrolan Anda. Ini memberi Anda akses ke jaringan terbuka, jadi Anda tidak hanya terjebak berbicara dengan pengguna Element. Itu sangat aman.\n\nElement dapat melakukan semua ini karena beroperasi pada Matrix - standar untuk komunikasi terdesentralisasi terbuka.\n\nElement menempatkan Anda dalam kendali dengan membiarkan Anda memilih siapa yang menghost percakapan Anda. Dari aplikasi Element, Anda dapat memilih untuk menghost dengan cara yang berbeda:\n\n1. Dapatkan akun gratis pada server publik matrix.org\n2. Host sendiri akun Anda dengan menjalankan server pada perangkat keras Anda sendiri\n3. Mendaftar untuk akun di server khusus dengan hanya berlangganan platform hosting Element Matrix Services\n\nMengapa memilih Element?\n\nMILIKI DATA ANDA: Anda memutuskan di mana untuk menyimpan data dan pesan Anda. Anda memilikinya dan mengendalikannya, bukan perusahaan besar yang menambang data Anda atau memberikan akses ke pihak ketiga.\n\nPESAN DAN KOLABORASI TERBUKA: Anda dapat mengobrol dengan orang lain di jaringan Matrix, jika mereka menggunakan Element atau aplikasi Matrix lain, dan bahkan jika mereka menggunakan sistem perpesanan seperti Slack, IRC atau XMPP.\n\nSANGAT AMAN: Enkripsi ujung-ke-ujung yang nyata (hanya mereka yang dalam percakapan dapat mendekripsi pesan), dan penandatanganan silang untuk memverifikasi perangkat anggota obrolan.\n\nKOMUNIKASI LENGKAP: Perpesanan, panggilan suara dan video, pembagian file, pembagian layar dan banyak integrasi, bot dan widget. Buat ruangan, komunitas, tetap terhubung dan selesaikan hal-hal.\n\nDI MANA PUN ANDA BERADA: Tetap berkomunikasi di mana pun Anda berada dengan riwayat pesan yang sepenuhnya disinkronkan di semua perangkat Anda dan di web di https://app.element.io/."; +"store_full_description" = "Element adalah aplikasi messenger dan kolaborasi tipe baru yang:\n\n1. Menempatkan Anda dalam kendali untuk mempertahankan privasi Anda\n2. Memungkinkan Anda berkomunikasi dengan siapa pun di jaringan Matrix, dan bahkan di luar dengan mengintegrasikan dengan aplikasi seperti Slack\n3. Melindungi Anda dari iklan, menambangan data, pintu belakang, dan taman berdinding\n4. Mengamankan Anda melalui enkripsi ujung-ke-ujung, dengan penandatanganan silang untuk memverifikasi orang lain\n\nElement benar-benar berbeda dari aplikasi perpesanan dan kolaborasi lain karena Element terdesentralisasi dan sumber terbuka.\n\nElement memungkinkan Anda host sendiri — atau memilih host — sehingga Anda memiliki privasi, kepemilikan, dan kontrol data dan obrolan Anda. Ini memberi Anda akses ke jaringan terbuka, jadi Anda tidak hanya terjebak berbicara dengan pengguna Element. Itu sangat aman.\n\nElement dapat melakukan semua ini karena beroperasi pada Matrix — standar untuk komunikasi terdesentralisasi terbuka.\n\nElement menempatkan Anda dalam kendali dengan membiarkan Anda memilih siapa yang menghost percakapan Anda. Dari aplikasi Element, Anda dapat memilih untuk menghost dengan cara yang berbeda:\n\n1. Dapatkan akun gratis pada server publik matrix.org\n2. Host sendiri akun Anda dengan menjalankan server pada perangkat keras Anda sendiri\n3. Mendaftar untuk akun di server khusus dengan hanya berlangganan platform hosting Element Matrix Services\n\nMengapa memilih Element?\n\nMILIKI DATA ANDA: Anda memutuskan di mana untuk menyimpan data dan pesan Anda. Anda memilikinya dan mengendalikannya, bukan perusahaan besar yang menambang data Anda atau memberikan akses ke pihak ketiga.\n\nPESAN DAN KOLABORASI TERBUKA: Anda dapat mengobrol dengan orang lain di jaringan Matrix, jika mereka menggunakan Element atau aplikasi Matrix lain, dan bahkan jika mereka menggunakan sistem perpesanan seperti Slack, IRC atau XMPP.\n\nSANGAT AMAN: Enkripsi ujung-ke-ujung yang nyata (hanya mereka yang dalam percakapan dapat mendekripsi pesan), dan penandatanganan silang untuk memverifikasi perangkat anggota obrolan.\n\nKOMUNIKASI LENGKAP: Perpesanan, panggilan suara dan video, pembagian file, pembagian layar dan banyak integrasi, bot dan widget. Buat ruangan, komunitas, tetap terhubung dan selesaikan hal-hal.\n\nDI MANA PUN ANDA BERADA: Tetap berkomunikasi di mana pun Anda berada dengan riwayat pesan yang sepenuhnya disinkronkan di semua perangkat Anda dan di web di https://app.element.io/."; // String for App Store "store_short_description" = "Obrolan/VoIP terdesentralisasi aman"; @@ -617,7 +617,7 @@ "spaces_empty_space_detail" = "Beberapa ruangan mungkin disembunyikan karena ruangannya pribadi dan Anda memerlukan sebuah undangan."; "leave_space_message" = "Apakah anda Anda yakin ingin keluar dari %@? Apakah Anda juga ingin meninggalkan semua ruangan dan space lainnya di space ini?"; "space_beta_announce_information" = "Space adalah cara baru untuk mengelompokkan ruangan dan orang. Mereka belum ada di iOS, tetapi Anda dapat menggunakannya sekarang di Web dan Desktop."; -"favourites_empty_view_information" = "Anda dapat memfavoritkan dengan beberapa cara - yang tercepat adalah dengan menekan dan menahan. Ketuk ikon bintang dan mereka akan secara otomatis muncul di sini."; +"favourites_empty_view_information" = "Anda dapat memfavoritkan dengan beberapa cara — yang tercepat adalah dengan menekan dan menahan. Ketuk ikon bintang dan mereka akan secara otomatis muncul di sini."; "home_empty_view_information" = "Aplikasi obrolan aman semua-dalam-satu untuk tim, teman, dan organisasi. Ketuk tombol + di bawah untuk menambahkan orang dan ruangan."; "pin_protection_explanatory" = "Menyiapkan PIN memungkinkan Anda melindungi data seperti pesan dan kontak, jadi hanya Anda yang dapat mengaksesnya dengan memasukkan PIN di awal aplikasi."; "major_update_information" = "Kami senang mengumumkan bahwa kami telah mengubah nama kami! Aplikasi Anda telah diperbarui dan Anda masuk ke akun Anda."; @@ -1538,7 +1538,7 @@ "space_feature_unavailable_information" = "Space adalah cara baru untuk mengelompokkan ruangan dan pengguna.\n\nMereka akan segera datang. Untuk saat ini, jika Anda bergabung sebuah space di platform lain, Anda akan dapat mengakses ruang mana saja yang Anda ikuti di sini."; // Success from passphrase -"key_backup_setup_success_from_passphrase_info" = "Kunci Anda sedang dicadangkan.\n\nKunci Keamanan Anda adalah jaring pengaman - Anda dapat menggunakannya untuk memulihkan akses ke pesan terenkripsi jika Anda lupa frasa sandi.\n\nSimpan Kunci Keamanan Anda di suatu tempat yang sangat aman, seperti pengelola kata sandi (atau brankas)."; +"key_backup_setup_success_from_passphrase_info" = "Kunci Anda sedang dicadangkan.\n\nKunci Keamanan Anda adalah jaring pengaman — Anda dapat menggunakannya untuk memulihkan akses ke pesan terenkripsi jika Anda lupa frasa sandi.\n\nSimpan Kunci Keamanan Anda di suatu tempat yang sangat aman, seperti pengelola kata sandi (atau brankas)."; "key_backup_setup_passphrase_info" = "Kami akan menyimpan salinan terenkripsi dari kunci Anda di server kami. Lindungi cadangan Anda dengan frasa agar tetap aman.\n\nUntuk keamanan maksimum, ini harus berbeda dari kata sandi akun Anda."; "key_backup_setup_intro_info" = "Pesan di ruang terenkripsi diamankan dengan enkripsi ujung-ke-ujung. Hanya Anda dan penerima yang memiliki kunci untuk membaca pesan ini.\n\nCadangkan kunci Anda dengan aman untuk menghindari kehilangannya."; "deactivate_account_informations_part5" = "Jika Anda ingin kami melupakan pesan Anda, silakan centang kotak di bawah ini\n\nVisibilitas pesan di Matrix mirip dengan email. Kami melupakan pesan Anda berarti bahwa pesan yang telah Anda kirim tidak akan dibagikan dengan pengguna baru atau tidak terdaftar, tetapi pengguna terdaftar yang sudah memiliki akses ke pesan ini akan tetap memiliki akses ke salinannya."; @@ -1649,7 +1649,7 @@ "group_details_title" = "Detail Komunitas"; "room_event_action_forward" = "Teruskan"; "poll_edit_form_add_option" = "Tambahkan opsi"; -"poll_edit_form_option_number" = "Opsi %d"; +"poll_edit_form_option_number" = "Opsi %lu"; "poll_edit_form_create_options" = "Buat opsi"; "poll_edit_form_input_placeholder" = "Tulis sesuatu"; "poll_edit_form_question_or_topic" = "Pertanyaan atau topik"; @@ -1663,3 +1663,24 @@ "share_extension_low_quality_video_title" = "Video akan dikirim dalam kualitas rendah"; "settings_discovery_accept_terms" = "Terima Persyaratan Server Identitas"; "settings_about" = "TENTANG"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_not_closed_subtitle" = "Mohon coba lagi"; +"poll_timeline_not_closed_title" = "Gagal untuk mengakhiri poll"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_timeline_vote_not_registered_subtitle" = "Maaf, suara Anda tidak diberikan, mohon coba lagi"; +"poll_timeline_vote_not_registered_title" = "Suara tidak diberikan"; +"poll_timeline_total_final_results" = "Hasil akhir berdasarkan %lu suara"; +"poll_timeline_total_final_results_one_vote" = "Hasil akhir berdasarkan 1 suara"; +"poll_timeline_total_votes_not_voted" = "%lu suara diberikan. Berikan suara untuk melihat hasilnya"; +"poll_timeline_total_one_vote_not_voted" = "1 suara diberikan. Berikan suara untuk melihat hasilnya"; +"poll_timeline_total_no_votes" = "Tidak ada suara yang diberikan"; +"poll_timeline_total_votes" = "%lu suara diberikan"; +"poll_timeline_total_one_vote" = "1 suara diberikan"; +"poll_timeline_votes_count" = "%lu suara"; +"poll_timeline_one_vote" = "1 suara"; +"poll_edit_form_post_failure_action" = "OK"; +"poll_edit_form_post_failure_subtitle" = "Silakan coba lagi"; +"poll_edit_form_post_failure_title" = "Gagal untuk mengirim poll"; +"settings_labs_enabled_polls" = "Poll"; +"room_event_action_end_poll" = "Akhiri poll"; +"room_event_action_remove_poll" = "Hapus poll"; From 959e074851306cd0169c3d2881eee3fba0664420 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Thu, 16 Dec 2021 03:56:27 +0000 Subject: [PATCH 043/109] Translated using Weblate (Slovak) Currently translated at 37.1% (505 of 1361 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 417 ++++++++++++++++++++++++++++ 1 file changed, 417 insertions(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index f485f1f640..25b6c9bda0 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -165,3 +165,420 @@ "callbar_only_single_active" = "Ťuknutím sa vrátite k hovoru (%@)"; "switch" = "Prepnúť"; "sending" = "Odosielanie"; +"settings_discovery_three_pids_management_information_part2" = "Používateľské nastavenia"; +"settings_key_backup_delete_confirmation_prompt_title" = "Vymazať zálohu"; +"settings_key_backup_button_delete" = "Vymazať zálohu"; +"settings_key_backup_info_algorithm" = "Algoritmus: %@"; +"settings_crypto_export" = "Exportovať kľúče"; +"settings_crypto_device_key" = "\nKľúč relácie:\n"; +"settings_crypto_device_id" = "\nID relácie: "; +"settings_crypto_device_name" = "Názov relácie: "; +"settings_confirm_password" = "Potvrdiť heslo"; +"settings_new_password" = "nové heslo"; +"settings_old_password" = "staré heslo"; +"settings_third_party_notices" = "Poznámky tretích strán"; +"settings_privacy_policy" = "Zásady ochrany súkromia"; +"settings_version" = "Verzia %s"; +"settings_labs_e2e_encryption" = "End-to-End šifrovanie"; +"settings_integrations_allow_button" = "Spravovať integrácie"; +"settings_room_upgrades" = "Aktualizácia miestnosti"; +"settings_messages_containing_user_name" = "My používateľské meno"; +"settings_group_messages" = "Skupinové správy"; +"settings_direct_messages" = "Priame správy"; +"settings_default" = "Predvolené oznámenia"; +"settings_notifications_disabled_alert_title" = "Oznámenia vypnuté"; +"settings_device_notifications" = "Oznámenia zariadenia"; +"settings_night_mode" = "Nočný režim"; +"settings_change_password" = "Zmeniť heslo"; +"settings_first_name" = "Krstné meno"; +"settings_display_name" = "Zobrazované meno"; +"settings_profile_picture" = "Profilový obrázok"; +"settings_sign_out" = "Odhlásiť sa"; +"settings_deactivate_account" = "DEAKTIVOVAŤ ÚČET"; +"settings_key_backup" = "ZÁLOHA KĽÚČA"; +"settings_phone_contacts" = "KONTAKTY TELEFÓNU"; +"settings_contacts" = "KONTAKTY ZARIADENIA"; +"settings_ignored_users" = "IGNOROVANÍ POUŽÍVATELIA"; +"settings_user_interface" = "POUŽÍVATEĽSKÉ ROZHRANIE"; +"settings_identity_server_settings" = "SERVER TOTOŽNOSTÍ"; +"settings_user_settings" = "POUŽÍVATEĽSKÉ NASTAVENIA"; +"settings_clear_cache" = "Vyprázdniť vyrovnávaciu pamäť"; +"settings_report_bug" = "Nahlásiť chybu"; +"room_preview_try_join_an_unknown_room_default" = "miestnosť"; +"room_title_one_member" = "1 člen"; +"room_title_members" = "%@ členovia"; +"room_title_invite_members" = "Pozvať členov"; + +// Room Title +"room_title_new_room" = "Nová miestnosť"; +"unknown_devices_title" = "Neznáme relácie"; +"unknown_devices_call_anyway" = "Napriek tomu zavolať"; +"unknown_devices_send_anyway" = "Napriek tomu odoslať"; +"room_place_voice_call" = "Audio hovor"; +"room_accessibility_hangup" = "Zavesiť"; +"room_accessibility_video_call" = "Video hovor"; +"room_action_send_file" = "Poslať súbor"; +"room_action_send_sticker" = "Odoslať nálepku"; +"room_event_action_reaction_show_less" = "Zobraziť menej"; +"room_event_action_reaction_show_all" = "Zobraziť všetko"; +"room_event_action_cancel_download" = "Zrušiť sťahovanie"; +"room_event_action_cancel_send" = "Zrušiť odosielanie"; +"room_event_action_report" = "Nahlásiť obsah"; +"room_event_action_view_source" = "Zobraziť zdroj"; +"room_prompt_cancel" = "Zrušiť všetky"; +"room_prompt_resend" = "Znovu odoslať všetky"; +"directory_cell_description" = "%tu miestnosti"; +"search_no_result" = "Žiadne výsledky"; + +// Room recents +"room_recents_directory_section" = "PRIEČINOK MIESTNOSTI"; + +// Social login + +"social_login_list_title_continue" = "Pokračovať s"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_edit_form_post_failure_action" = "OK"; +"settings_labs_enabled_polls" = "Ankety"; +"side_menu_action_feedback" = "Spätná väzba"; +"side_menu_action_help" = "Pomocník"; +"side_menu_action_settings" = "Nastavenia"; + +// Mark: - User avatar view + +"user_avatar_view_accessibility_label" = "obrázok"; + +// Mark: Avatar + +"space_avatar_view_accessibility_label" = "obrázok"; +"spaces_suggested_room" = "Navrhované"; +"spaces_left_panel_title" = "Priestory"; +"spaces_home_space_title" = "Domov"; +"space_beta_announce_badge" = "BETA"; +"room_intro_cell_information_dm_sentence1_part3" = ". "; +"room_intro_cell_information_room_sentence1_part3" = ". "; + +// Mark: - Room avatar view + +"room_avatar_view_accessibility_label" = "obrázok"; +"call_transfer_error_title" = "Chyba"; +"call_transfer_contacts_all" = "Všetky"; +"call_transfer_contacts_recent" = "Nedávne"; +"call_transfer_users" = "Používatelia"; +"room_info_list_section_other" = "Ostatné"; +"create_room_placeholder_address" = "#testroom:matrix.org"; +"create_room_placeholder_topic" = "Téma"; +"create_room_placeholder_name" = "Názov"; +"biometrics_cant_unlocked_alert_message_retry" = "Skúsiť znovu"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_reset_alert_action_reset" = "Obnoviť predvolené"; +"pin_protection_choose_pin_welcome_after_register" = "Vitajte."; +"secrets_reset_reset_action" = "Obnoviť predvolené"; +"secrets_setup_recovery_passphrase_confirm_passphrase_title" = "Potvrdiť"; +"secrets_setup_recovery_passphrase_validate_action" = "Hotovo"; +"secrets_setup_recovery_key_done_action" = "Hotovo"; +"secrets_setup_recovery_key_export_action" = "Uložiť"; +"secrets_setup_recovery_key_loading" = "Načítavanie…"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part3" = "."; + +// Session details + +"user_verification_session_details_trusted_title" = "Dôveryhodné"; +"user_verification_sessions_list_session_trusted" = "Dôveryhodné"; +"user_verification_sessions_list_table_title" = "Relácie"; +"user_verification_sessions_list_user_trust_level_unknown_title" = "Neznámy"; +"user_verification_sessions_list_user_trust_level_warning_title" = "Upozornenie"; + +// Sessions list + +"user_verification_sessions_list_user_trust_level_trusted_title" = "Dôveryhodné"; +"key_verification_tile_conclusion_done_title" = "Overené"; +"key_verification_tile_request_incoming_approval_decline" = "Odmietnuť"; +"key_verification_tile_request_incoming_approval_accept" = "Prijať"; +"key_verification_tile_request_status_waiting" = "Čakanie…"; + +// MARK: - Key Verification + +"key_verification_bootstrap_not_setup_title" = "Chyba"; + +// MARK: Reaction history +"reaction_history_title" = "Reakcie"; +"emoji_picker_symbols_category" = "Symboly"; +"emoji_picker_objects_category" = "Objekty"; +"emoji_picker_activity_category" = "Aktivity"; +"device_verification_emoji_headphones" = "Slúchadlá"; +"device_verification_emoji_anchor" = "Kotva"; +"device_verification_emoji_trumpet" = "Trúbka"; +"device_verification_emoji_guitar" = "Gitara"; +"device_verification_emoji_ball" = "Lopta"; +"device_verification_emoji_trophy" = "Trofej"; +"device_verification_emoji_rocket" = "Raketa"; +"device_verification_emoji_aeroplane" = "Lietadlo"; +"device_verification_emoji_bicycle" = "Bicykel"; +"device_verification_emoji_flag" = "Zástava"; +"device_verification_emoji_telephone" = "Telefón"; +"device_verification_emoji_hammer" = "Kladivo"; +"device_verification_emoji_key" = "Kľúč"; +"device_verification_emoji_lock" = "Zámka"; +"device_verification_emoji_scissors" = "Nožnice"; +"device_verification_emoji_paperclip" = "Kancelárska spinka"; +"device_verification_emoji_pencil" = "Ceruzka"; +"device_verification_emoji_book" = "Kniha"; +"device_verification_emoji_gift" = "Darček"; +"device_verification_emoji_clock" = "Budík"; +"device_verification_emoji_hourglass" = "Presýpacie hodiny"; +"device_verification_emoji_umbrella" = "Dáždnik"; +"device_verification_emoji_santa" = "Mikuláš"; +"device_verification_emoji_glasses" = "Okuliare"; +"device_verification_emoji_hat" = "Klobúk"; +"device_verification_emoji_robot" = "Robot"; +"device_verification_emoji_smiley" = "Úsmev"; +"device_verification_emoji_heart" = "Srdce"; +"device_verification_emoji_cake" = "Koláč"; +"device_verification_emoji_pizza" = "Pizza"; +"device_verification_emoji_corn" = "Kukurica"; +"device_verification_emoji_strawberry" = "Jahoda"; +"device_verification_emoji_apple" = "Jablko"; +"device_verification_emoji_banana" = "Banán"; +"device_verification_emoji_fire" = "Oheň"; +"device_verification_emoji_cloud" = "Oblak"; +"device_verification_emoji_moon" = "Mesiac"; +"device_verification_emoji_globe" = "Zemeguľa"; +"device_verification_emoji_mushroom" = "Huba"; +"device_verification_emoji_cactus" = "Kaktus"; +"device_verification_emoji_tree" = "Strom"; +"device_verification_emoji_flower" = "Kvet"; +"device_verification_emoji_butterfly" = "Motýľ"; +"device_verification_emoji_octopus" = "Chobotnica"; +"device_verification_emoji_fish" = "Ryba"; +"device_verification_emoji_turtle" = "Korytnačka"; +"device_verification_emoji_penguin" = "Tučniak"; +"device_verification_emoji_rooster" = "Kohút"; +"device_verification_emoji_panda" = "Panda"; +"device_verification_emoji_rabbit" = "Zajac"; +"device_verification_emoji_elephant" = "Slon"; +"device_verification_emoji_pig" = "Prasa"; +"device_verification_emoji_unicorn" = "Jednorožec"; +"device_verification_emoji_horse" = "Kôň"; +"device_verification_emoji_lion" = "Lev"; +"device_verification_emoji_cat" = "Mačka"; + +// MARK: Emoji +"device_verification_emoji_dog" = "Pes"; + +// MARK: Verified + +// Device + +"device_verification_verified_title" = "Overený!"; +"key_verification_manually_verify_device_validate_action" = "Overiť"; +"key_verification_self_verify_current_session_alert_validate_action" = "Overiť"; +"device_verification_self_verify_start_waiting" = "Čakanie…"; +"device_verification_self_verify_alert_validate_action" = "Overiť"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_backup_action" = "Zálohovať"; +"key_backup_recover_done_action" = "Hotovo"; +"key_backup_recover_from_recovery_key_recovery_key_title" = "Enter"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "."; +"key_backup_recover_from_passphrase_passphrase_title" = "Enter"; +"key_backup_setup_success_from_passphrase_done_action" = "Hotovo"; + +// Success + +"key_backup_setup_success_title" = "Úspech!"; +"key_backup_setup_passphrase_confirm_passphrase_valid" = "Super!"; +"key_backup_setup_passphrase_confirm_passphrase_title" = "Potvrdiť"; +"key_backup_setup_passphrase_passphrase_valid" = "Super!"; +"key_backup_setup_passphrase_passphrase_title" = "Enter"; +"key_backup_setup_intro_manual_export_info" = "(Pokročilé)"; +"key_backup_setup_skip_alert_skip_action" = "Preskočiť"; +"deactivate_account_forget_messages_information_part2_emphasize" = "Upozornenie"; +"service_terms_modal_decline_button" = "Odmietnuť"; +"service_terms_modal_accept_button" = "Prijať"; + +// Widget Picker +"widget_picker_title" = "Integrácie"; +"widget_menu_refresh" = "Obnoviť"; +"bug_report_send" = "Odoslať"; +"e2e_key_backup_wrong_version_button_settings" = "Nastavenia"; +"large_badge_value_k_format" = "%.1fK"; +"yesterday" = "Včera"; +"today" = "Dnes"; + +// Others +"or" = "alebo"; +"event_formatter_group_call_leave" = "Opustiť"; +"event_formatter_group_call_join" = "Vstúpiť"; +"event_formatter_call_retry" = "Skúsiť znovu"; +"event_formatter_call_answer" = "Odpovedať"; +"event_formatter_call_decline" = "Odmietnuť"; +"event_formatter_call_ringing" = "Vyzváňanie…"; +"event_formatter_call_connecting" = "Pripájanie…"; +"event_formatter_message_edited_mention" = "(upravené)"; +"directory_server_placeholder" = "matrix.org"; + +// Directory +"directory_title" = "Priečinok"; +"media_picker_select" = "Vybrať"; +"media_picker_library" = "Knižnica"; +"group_participants_invited_section" = "POZVANÍ"; +"group_participants_invite_prompt_title" = "Potvrdenie"; +"group_participants_remove_prompt_title" = "Potvrdenie"; +"group_details_rooms" = "Miestnosti"; +"group_details_people" = "Ľudia"; +"group_details_home" = "Domov"; +"room_notifs_settings_cancel_action" = "Zrušiť"; +"room_notifs_settings_done_action" = "Hotovo"; +"room_notifs_settings_none" = "Žiadne"; +"room_details_advanced_room_id_for_dm" = "ID:"; +"room_details_advanced_section" = "Pokročilé"; +"room_details_addresses_section" = "Adresy"; +"room_details_history_section_anyone" = "Ktokoľvek"; +"room_details_notifs" = "Oznámenia"; +"room_details_favourite_tag" = "Obľúbiť"; +"room_details_topic" = "Téma"; +"room_details_room_name_for_dm" = "Názov"; +"room_details_photo_for_dm" = "Fotka"; +"room_details_settings" = "Nastavenia"; +"room_details_integrations" = "Integrácie"; +"room_details_people" = "Členovia"; +"room_details_title_for_dm" = "Podrobnosti"; +"identity_server_settings_alert_disconnect_button" = "Odpojiť"; +"identity_server_settings_disconnect" = "Odpojiť"; +"identity_server_settings_change" = "Zmeniť"; +"identity_server_settings_add" = "Pridať"; +"security_settings_advanced" = "POKROČILÉ"; +"security_settings_cryptography" = "KRYPTOGRAFIA"; +"security_settings_crosssigning_reset" = "Obnoviť predvolené"; +"security_settings_crosssigning" = "KRÍŽOVÉ PODPISOVANIE"; +"security_settings_secure_backup_reset" = "Obnoviť predvolené"; +"security_settings_secure_backup_info_checking" = "Kontrolovanie…"; + +// Security settings +"security_settings_title" = "Zabezpečenie"; +"settings_discovery_three_pid_details_revoke_action" = "Odvolať"; +"settings_discovery_three_pid_details_share_action" = "Zdieľať"; +"settings_discovery_three_pids_management_information_part3" = "."; +"settings_key_backup_info_checking" = "Kontrolovanie…"; +"settings_copyright" = "Autorské práva"; +"settings_ui_theme_black" = "Čierny"; +"settings_ui_theme_dark" = "Tmavý"; +"settings_ui_theme_light" = "Svetlý"; +"settings_ui_theme" = "Vzhľad"; +"settings_ui_language" = "Jazyk"; +"settings_messages_containing_keywords" = "Kľúčové slová"; +"settings_messages_containing_at_room" = "@miestnosť"; +"settings_other" = "Ostatné"; +"settings_security" = "BEZPEČNOSŤ"; +"settings_three_pids_management_information_part3" = "."; +"settings_three_pids_management_information_part2" = "Objavovanie"; +"settings_phone_number" = "Telefón"; +"settings_email_address" = "Email"; +"settings_remove_prompt_title" = "Potvrdenie"; +"settings_surname" = "Priezvisko"; +"settings_cryptography" = "KRYPTOGRAFIA"; +"settings_devices" = "RELÁCIE"; +"settings_about" = "O APLIKÁCII"; +"settings_advanced" = "POKROČILÉ"; +"settings_integrations" = "INTEGRÁCIE"; +"settings_discovery_settings" = "OBJAVOVANIE"; +"settings_calls_settings" = "HOVORY"; +"settings_notifications" = "OZNÁMENIA"; +"settings_links" = "ODKAZY"; + +// Settings +"settings_title" = "Nastavenia"; +"unknown_devices_verify" = "Overiť…"; +"media_type_accessibility_sticker" = "Nálepka"; +"media_type_accessibility_file" = "Súbor"; +"media_type_accessibility_location" = "Miesto"; +"media_type_accessibility_video" = "Video"; +"media_type_accessibility_audio" = "Audio"; +"media_type_accessibility_image" = "Obrázok"; +"room_join_group_call" = "Vstúpiť"; +"room_accessibility_call" = "Hovor"; +"room_accessibility_upload" = "Nahrať"; +"room_accessibility_integrations" = "Integrácie"; +"room_accessibility_search" = "Hľadať"; +"room_resource_limit_exceeded_message_contact_1" = " Prosím "; +"room_action_reply" = "Odpovedať"; +"room_event_action_edit" = "Upraviť"; +"room_event_action_reply" = "Odpovedať"; +"room_event_action_delete" = "Vymazať"; +"room_event_action_resend" = "Znovu odoslať"; +"room_event_action_save" = "Uložiť"; +"room_event_action_permalink" = "Trvalý odkaz"; +"room_event_action_forward" = "Preposlať"; +"room_event_action_share" = "Zdieľať"; +"room_event_action_more" = "Viac"; +"room_event_action_redact" = "Odstrániť"; +"room_event_action_quote" = "Citovať"; +"room_event_action_copy" = "Kopírovať"; +"room_ongoing_conference_call_close" = "Zavrieť"; +"search_in_progress" = "Prehľadávanie…"; +"search_messages" = "Správy"; + +// Search +"search_rooms" = "Miestnosti"; +"group_section" = "KOMUNITY"; + +// Groups tab +"group_invite_section" = "POZVANIA"; + +// MARK: - Home + +"home_empty_view_title" = "Vitajte v %@,\n%@"; +"room_member_power_level_short_custom" = "Vlastná úroveň"; +"room_member_power_level_short_moderator" = "Moderátor"; +"room_member_power_level_short_admin" = "Správca"; +"room_member_power_level_custom_in" = "Vlastné (%@) v %@"; +"room_member_power_level_moderator_in" = "Moderátor v %@"; +"room_member_power_level_admin_in" = "Správca v %@"; +"room_participants_security_information_room_encrypted_for_dm" = "Správy sú tu šifrované end-to-end.\n\nVaše správy sú zabezpečené zámkami a jedinečné kľúče na ich odomknutie máte len vy a príjemca."; +"room_participants_security_information_room_encrypted" = "Správy v tejto miestnosti sú šifrované end-to-end.\n\nVaše správy sú zabezpečené zámkami a jedinečné kľúče na ich odomknutie máte len vy a príjemca."; +"room_participants_security_information_room_not_encrypted" = "Správy v tejto miestnosti nie sú šifrované end-to-end (od vás až k príjemcovi)."; +"room_participants_security_information_room_not_encrypted_for_dm" = "Správy tu nie sú šifrované end-to-end (od vás až k príjemcovi)."; +"room_participants_security_loading" = "Načítavanie…"; +"room_participants_action_security_status_loading" = "Načítavanie…"; +"room_participants_action_security_status_warning" = "Upozornenie"; +"room_participants_action_security_status_verify" = "Overiť"; +"room_participants_action_security_status_verified" = "Overený"; +"room_participants_action_mention" = "Zmieniť sa"; +"room_participants_action_start_video_call" = "Uskutočniť video hovor"; +"room_participants_action_start_voice_call" = "Uskutočniť hlasový hovor"; +"room_participants_action_start_new_chat" = "Začať novú konverzáciu"; +"room_participants_action_set_admin" = "Zmeniť na správcu"; +"room_participants_action_set_moderator" = "Udeliť stav moderátora"; +"room_participants_action_unignore" = "Zobraziť všetky správy od tohoto používateľa"; +"room_participants_action_ignore" = "Skryť všetky správy od tohoto používateľa"; +"room_participants_action_unban" = "Zrušiť zákaz"; +"room_participants_action_ban" = "Vylúčiť z tejto miestnosti"; +"room_participants_action_remove" = "Odstrániť z tejto miestnosti"; +"room_participants_action_leave" = "Opustiť túto miestnosť"; +"room_participants_action_invite" = "Pozvať"; +"room_participants_action_section_security" = "Zabezpečenie"; +"room_participants_action_section_other" = "Možnosti"; +"room_participants_action_section_devices" = "Relácie"; +"room_participants_action_section_direct_chats" = "Priame konverzácie"; +"room_participants_action_section_admin_tools" = "Nástroje správcu"; +"room_participants_filter_room_members_for_dm" = "Filtrovať členov"; +"room_participants_filter_room_members" = "Filtrovať členov v miestnosti"; +"room_participants_invite_prompt_msg" = "Ste si istí, že chcete pozvať používateľa %s do tejto konverzácie?"; +"room_participants_remove_third_party_invite_prompt_msg" = "Ste si istí, že chcete odvolať toto pozvanie?"; +"room_participants_remove_prompt_msg" = "Ste si istí, že chcete používateľa %s odstrániť z tejto konverzácie?"; +"room_participants_leave_prompt_msg_for_dm" = "Ste si istí, že chcete odísť?"; +"room_participants_leave_prompt_msg" = "Ste si istí, že chcete opustiť miestnosť?"; +"room_participants_leave_prompt_title" = "Opustiť miestnosť"; +"room_participants_multi_participants" = "%d účastníkov"; +"room_participants_one_participant" = "1 účastník"; +"room_participants_add_participant" = "Pridať účastníka"; +"contacts_address_book_permission_denied" = "Aplikácii %@ ste nepovolili prístup k svojim lokálnym kontaktom"; + +// Contacts +"contacts_address_book_section" = "LOKÁLNE KONTAKTY"; +"contacts_address_book_no_contact" = "Žiadne lokálne kontakty"; +"contacts_address_book_no_identity_server" = "Nebol nastavený server totožností"; +"contacts_address_book_matrix_users_toggle" = "Len používatelia Matrix"; +"directory_searching_title" = "Vyhľadávanie v priečinku…"; + +// Directory +"directory_cell_title" = "Prezrieť priečinok"; From b8fc9ce9e05bf5d0c7f2a92e652a060ec5295dde Mon Sep 17 00:00:00 2001 From: Denys Nykula Date: Fri, 17 Dec 2021 17:35:26 +0000 Subject: [PATCH 044/109] Translated using Weblate (Ukrainian) Currently translated at 91.8% (1263 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 314 +++++++++++++++++++++++++++- 1 file changed, 312 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index cbc3c3bac3..6a01ebc8a5 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -408,7 +408,7 @@ // Bug report "bug_report_title" = "Звіт про ваду"; -"e2e_key_backup_wrong_version_button_wasme" = "Це був я"; +"e2e_key_backup_wrong_version_button_wasme" = "Це я"; "e2e_key_backup_wrong_version_button_settings" = "Налаштування"; "settings_privacy_policy" = "Політика приватності"; "settings_term_conditions" = "Умови та положення"; @@ -591,7 +591,7 @@ "user_avatar_view_accessibility_label" = "аватар"; "room_intro_cell_information_dm_sentence1_part3" = ". "; -"room_intro_cell_information_room_without_topic_sentence2_part1" = "Додати тему"; +"room_intro_cell_information_room_without_topic_sentence2_part1" = "Додайте тему,"; "room_intro_cell_information_room_with_topic_sentence2" = "Тема: %@"; "room_intro_cell_information_room_sentence1_part3" = ". "; "room_intro_cell_information_room_sentence1_part1" = "Це початок "; @@ -1228,3 +1228,313 @@ "room_replacement_information" = "Цю кімнату замінено й вона більше не активна."; "room_event_action_end_poll" = "Завершити опитування"; "room_event_action_remove_poll" = "Вилучити опитування"; +"user_verification_session_details_information_trusted_current_user" = "Цей сеанс довірений для захищеного листування, бо ви звірили його:"; +"user_verification_session_details_information_trusted_other_user_part1" = "Цей сеанс довірений для захищеного листування, бо "; +"user_verification_session_details_information_trusted_other_user_part2" = " звіряє його:"; +"user_verification_session_details_information_untrusted_other_user" = " входить у новому сеансі:"; +"user_verification_session_details_additional_information_untrusted_other_user" = "Надіслані цьому сеансу й цим сеансом повідомлення позначатимуться застереженнями, поки цей користувач йому не довірить. Або ви можете власноруч звірити сеанс."; +"user_verification_session_details_additional_information_untrusted_current_user" = "Якщо ви не входили в цей сеанс, ваш обліковий запис може бути під загрозою."; +"user_verification_session_details_verify_action_other_user" = "Звірити власноруч"; +"key_verification_bootstrap_not_setup_message" = "Спершу налаштуйте перехресне підписування."; +"key_verification_verify_qr_code_information" = "Відскануйте код, щоб безпечно звірити одне одного."; +"key_verification_verify_qr_code_scan_code_action" = "Сканувати їхній код"; +"key_verification_verify_qr_code_cannot_scan_action" = "Не вдалося сканувати?"; +"key_verification_verify_qr_code_other_scan_my_code_title" = "Чи зміг інший користувач відсканувати QR-код?"; +"key_verification_verify_qr_code_scan_other_code_success_title" = "Код підтверджено!"; +"key_verification_verify_qr_code_scan_other_code_success_message" = "QR-код успішно підтверджено."; + +// MARK: Self verification start + +// New login +"device_verification_self_verify_alert_title" = "Новий вхід. Це були ви?"; +"device_verification_self_verify_alert_message" = "Звірте новий вхід до вашого облікового запису: %@"; +"device_verification_self_verify_start_information" = "Звірте цим сеансом свій новий. Це надасть йому доступ до зашифрованих повідомлень."; +"device_verification_self_verify_start_waiting" = "Очікування…"; + +// MARK: Self verification wait + +"device_verification_self_verify_wait_title" = "Доповніть безпеку"; +"device_verification_self_verify_wait_information" = "Звірте цей сеанс одним зі своїх інших, щоб надати йому доступ до зашифрованих повідомлень.\n\nВикористайте найостанніший %@ на своїх інших пристроях:"; + +// Recover from private key +"key_backup_recover_from_private_key_info" = "Відновлення резервної копії…"; + +// MARK: Scan confirmation + +// Scanning +"key_verification_scan_confirmation_scanning_title" = "Майже все! Чекаємо на підтвердження…"; +"key_verification_scan_confirmation_scanning_user_waiting_other" = "Очікування на %s…"; +"key_verification_scan_confirmation_scanning_device_waiting_other" = "Очікування іншого пристрою…"; + +// Scanned +"key_verification_scan_confirmation_scanned_title" = "Майже все!"; +"key_verification_scan_confirmation_scanned_user_information" = "Чи показує %@ такий же щит?"; +"key_verification_scan_confirmation_scanned_device_information" = "Чи показує інший пристрій такий же щит?"; +"device_verification_self_verify_wait_new_sign_in_title" = "Звірте цей вхід"; +"device_verification_self_verify_wait_additional_information" = "Це працює з %@ та іншими клієнтами Matrix, здатними на перехресне підписування."; + +// MARK: - Device Verification +"key_verification_other_session_title" = "Звірте сеанс"; +"key_verification_new_session_title" = "Звірте свій новий сеанс"; +"key_verification_this_session_title" = "Звірити цей сеанс"; +"device_verification_security_advice_emoji" = "Порівняйте унікальні емоджі, переконавшись, що їх показано в однаковому порядку."; +"device_verification_security_advice_number" = "Порівняйте числа, переконавшись, що їх показано в однаковому порядку."; + +// MARK: Verify + +"key_verification_verify_sas_title_emoji" = "Порівняйте емодзі"; +"key_verification_verify_sas_title_number" = "Порівняйте числа"; +"key_verification_verify_sas_cancel_action" = "Вони не збігаються"; +"key_verification_verify_sas_validate_action" = "Вони збігаються"; +"key_verification_verify_sas_additional_information" = "Для більшої безпеки зв'яжіться іншим довіреним засобом чи особисто."; +"key_verification_verify_qr_code_emoji_information" = "Звірити порівнянням унікальних емодзі."; +"key_verification_verify_qr_code_start_emoji_action" = "Звірити за допомогою емодзі"; +"key_verification_verified_other_session_information" = "Тепер ви можете читати захищені повідомлення в своєму іншому сеансі, а інші користувачі знатимуть, що можуть йому довіряти."; +"key_verification_verified_new_session_information" = "Тепер ви можете читати захищені повідомлення на своєму новому пристрої, а інші користувачі знатимуть, що можуть йому довіряти."; +"key_verification_verified_this_session_information" = "Тепер ви можете читати захищені повідомлення на цьому пристрої, а інші користувачі знатимуть, що можуть йому довіряти."; + +// User + +"key_verification_verified_user_information" = "Листування з цим користувачем наскрізно зашифроване й непрочитне для сторонніх."; + +// Current session + +"key_verification_self_verify_current_session_alert_title" = "Звірити цей сеанс"; +"key_verification_self_verify_current_session_alert_message" = "Інші користувачі можуть йому не довіряти."; +"key_verification_verify_qr_code_information_other_device" = "Відскануйте код знизу для звірки:"; + +// MARK: Manually Verify Device + +"key_verification_manually_verify_device_title" = "Звірити власноруч за допомогою тексту"; +"key_verification_manually_verify_device_instruction" = "Підтвердьте шляхом порівняння наступного рядка з рядком у користувацьких налаштуваннях вашого іншого сеансу:"; +"key_verification_manually_verify_device_additional_information" = "Якщо вони відрізняються, безпека вашого зв'язку може бути під загрозою."; +"user_verification_session_details_verify_action_current_user_manually" = "Звірити власноруч за допомогою тексту"; +"device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Використати ключ безпеки"; +"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Використати фразу чи ключ безпеки"; +"device_verification_self_verify_wait_recover_secrets_additional_information" = "Якщо не маєте доступу до наявного сеансу"; + +// Recover with passphrase + +"secrets_recovery_with_passphrase_title" = "Фраза безпеки"; +"secrets_recovery_with_passphrase_information_default" = "Отримайте доступ до своєї захищеної історії повідомлень та ідентичності перехресного підписування для звірки інших сеансів, увівши свою фразу безпеки."; +"secrets_recovery_with_passphrase_information_verify_device" = "Підтвердьте цей пристрій своєю фразою безпеки."; +"secrets_recovery_with_passphrase_passphrase_placeholder" = "Введіть фразу безпеки"; +"secrets_recovery_with_passphrase_recover_action" = "Використати фразу"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "Забули свою фразу безпеки? Можете "; +"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "використати ключ безпеки"; +"secrets_recovery_with_passphrase_invalid_passphrase_title" = "Не вдалося зайти до таємного сховища"; +"secrets_recovery_with_passphrase_invalid_passphrase_message" = "Переконайтеся, що вводите правильну фразу безпеки."; + +// Recover with key + +"secrets_recovery_with_key_title" = "Ключ безпеки"; +"secrets_recovery_with_key_information_default" = "Отримайте доступ до своєї захищеної історії повідомлень та ідентичності перехресного підписування для звірки інших сеансів, увівши свій ключ безпеки."; +"secrets_recovery_with_key_information_verify_device" = "Підтвердьте цей пристрій своїм ключем безпеки."; +"secrets_recovery_with_key_recovery_key_placeholder" = "Введіть ключ безпеки"; +"secrets_recovery_with_key_recover_action" = "Використати ключ"; +"secrets_recovery_with_key_invalid_recovery_key_title" = "Не вдалося зайти до таємного сховища"; +"secrets_recovery_with_key_invalid_recovery_key_message" = "Переконайтеся, що вводите правильний ключ безпеки."; +"secure_key_backup_setup_intro_use_security_key_title" = "Використати ключ безпеки"; +"secure_key_backup_setup_intro_use_security_key_info" = "Згенерувати ключ безпеки для зберігання в надійному місці, наприклад у менеджері паролів чи сейфі."; +"secure_key_backup_setup_intro_use_security_passphrase_title" = "Встановити фразу безпеки"; +"secure_key_backup_setup_intro_use_security_passphrase_info" = "Ввести таємну фразу, відому лише вам, і згенерувати ключ резервного копіювання."; + + +// Cancel + +"secure_key_backup_setup_cancel_alert_title" = "Ви впевнені?"; +"secure_key_backup_setup_cancel_alert_message" = "Якщо скасуєте це й загубите пристрій, то втратите зашифровані повідомлення й дані.\n\nВвімкнути захищене резервне копіювання й керувати своїми ключами можна в налаштуваннях."; + +// MARK: - Secrets set up + +// Recovery Key + +"secrets_setup_recovery_key_title" = "Збережіть ключ безпеки"; +"secrets_setup_recovery_key_information" = "Зберігайте ключ безпеки в надійному місці. Ним можна буде розблокувати ваші зашифровані повідомлення й дані."; +"secrets_setup_recovery_key_storage_alert_title" = "Зберігайте його у надійному місці"; + +// Recovery passphrase + +"secrets_setup_recovery_passphrase_title" = "Встановіть фразу безпеки"; +"secrets_setup_recovery_passphrase_information" = "Введіть відому лише вам фразу безпеки для захисту таємниць на вашому сервері."; +"secrets_setup_recovery_passphrase_additional_information" = "Не застосовуйте пароль облікового запису повторно."; +"secrets_setup_recovery_passphrase_confirm_information" = "Для підтвердження введіть таємну фразу ще раз."; +"secure_key_backup_setup_existing_backup_error_title" = "Резервна копія повідомлень уже є"; +"secure_key_backup_setup_existing_backup_error_info" = "Розблокуйте її, щоб продовжити попереднє захищене резервне копіювання, або видаліть її, щоб почати захищене резервне копіювання повідомлень заново."; +"secure_key_backup_setup_existing_backup_error_unlock_it" = "Розблокувати"; +"secure_key_backup_setup_existing_backup_error_delete_it" = "Видалити"; +"sign_out_non_existing_key_backup_alert_setup_secure_backup_action" = "Налаштувати резервне копіювання ключів"; +"major_update_information" = "Ми раді повідомити, що змінили назву! Ваш застосунок оновлено й ви ввійшли у свій обліковий запис."; +"pin_protection_reset_alert_message" = "Щоб скинути PIN-код, увійдіть заново та створіть новий"; +"pin_protection_explanatory" = "Встановлення PIN-коду захищає ваші дані, як-от повідомлення чи контакти, обов'язковим запитом вашого PIN-коду при відкритті застосунку."; +"pin_protection_not_allowed_pin" = "Цей PIN-код недостатньо безпечний. Спробуйте інший"; +"pin_protection_settings_section_footer" = "Щоб скинути PIN-код, потрібно ввійти заново та створити новий."; +"pin_protection_mismatch_too_many_times_error_message" = "Не пригадуєте свій PIN-код? Торкніться кнопки \"Забули PIN-код\"."; +"biometrics_usage_reason" = "Застосунок потребує підтвердження особи"; +"biometrics_cant_unlocked_alert_message_x" = "Розблокуйте за допомогою %@ або ввійдіть заново й увімкніть %@ ще раз"; +"biometrics_cant_unlocked_alert_message_login" = "Ввійти заново"; +"device_verification_self_verify_wait_recover_secrets_checking_availability" = "Перевірка інших можливостей звірки ..."; +"create_room_section_footer_type" = "Вхід до закритої кімнати — лише по запрошенню."; +"pin_protection_kick_user_alert_message" = "Забагато помилок, ваш сеанс припинено"; + +// MARK: - Secrets recovery + +"secrets_recovery_reset_action_part_1" = "Забули чи втратили всі способи відновлення? "; +"secrets_reset_information" = "Робіть це лише якщо у вас немає іншого пристрою для звірки."; +"secrets_reset_warning_title" = "Якщо ви скинете все"; +"secrets_reset_warning_message" = "Ви розпочнете знову, але без історії повідомлень, без довірених пристроїв та користувачів."; +"secrets_setup_recovery_passphrase_summary_title" = "Збережіть ключ безпеки"; +"secrets_setup_recovery_passphrase_summary_information" = "Запам'ятайте свою фразу безпеки. Нею можна буде розблокувати ваші зашифровані повідомлення й дані."; +"home_empty_view_information" = "Досконалий застосунок для захищеного спілкування команд, друзів і організацій. Торкніться кнопки + унизу, щоб додати людей і кімнати."; +"bug_report_background_mode" = "Продовжити на тлі"; +"room_intro_cell_information_room_without_topic_sentence2_part2" = " щоб люди знали, про що ця кімната."; +"room_intro_cell_information_dm_sentence2" = "У цій розмові лише ви двоє, більше ніхто не може приєднатися."; +"room_intro_cell_information_multiple_dm_sentence2" = "У цій розмові лише ви, поки хтось із вас не запросить іще когось приєднатися."; +"space_feature_unavailable_subtitle" = "Простори ще не готові на iOS, але доступні у вебпереглядачі та на комп'ютері"; +"space_feature_unavailable_information" = "Простори — новий спосіб групувати кімнати й людей.\n\nСкоро ми їх допрацюємо. Якщо приєднаєтеся до простору на іншій платформі, всі його кімнати стануть доступні й тут."; + +// Success from secure backup +"key_backup_setup_success_from_secure_backup_info" = "Створюється резервна копія ключів."; +"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "Введіть фразу безпеки для продовження."; +"secrets_recovery_with_key_information_unlock_secure_backup_with_key" = "Скористайтеся ключем безпеки для продовження."; +"key_verification_verify_qr_code_scan_code_other_device_action" = "Сканувати цим пристроєм"; +"voice_message_stop_locked_mode_recording" = "Торкніться запису, щоб зупинити або прослухати"; +"version_check_banner_subtitle_supported" = "Ми скоро припинимо підтримку %@ на iOS %@. Щоб усі можливості %@ залишалися вам доступні, радимо оновити вашу версію iOS."; +"version_check_banner_subtitle_deprecated" = "Ми більше не підтримуємо %@ на iOS %@. Щоб усі можливості %@ залишалися вам доступні, радимо оновити вашу версію iOS."; +"version_check_modal_subtitle_supported" = "Ми вдосконалюємо швидкодію та дизайн %@. На жаль, ваша версія iOS сумісна не з усіма нашими виправленнями й скоро втратить підтримку.\nРадимо оновити операційну систему, щоб усі можливості %@ були вам доступні."; +"version_check_modal_subtitle_deprecated" = "Ми вдосконалюємо швидкодію та дизайн %@. На жаль, ваша версія iOS сумісна не з усіма нашими виправленнями й більше не підтримується.\nРадимо оновити операційну систему, щоб усі можливості %@ були вам доступні."; +"leave_space_message" = "Точно вийти з %@? Вийти також із усіх кімнат і просторів цього простору?"; +"leave_space_message_admin_warning" = "Ви адмініструєте цей простір. Перед виходом переконайтесь, що передали права адміністрування іншому учаснику."; +"leave_space_only_action" = "Не виходити з жодної кімнати"; +"leave_space_and_all_rooms_action" = "Вийти з усіх кімнат і просторів"; +"spaces_explore_rooms" = "Дослідити кімнати"; +"spaces_empty_space_detail" = "Деяких кімнат може бути не видно, бо вони закриті й потребують запрошення."; +"spaces_suggested_room" = "Пропоновано"; +"room_details_access_section_anyone_for_dm" = "Будь-хто з посиланням, зокрема гості"; +"spaces_no_room_found_detail" = "Деяких результатів може бути не видно, бо вони закриті й потребують запрошення."; +"spaces_no_member_found_detail" = "Шукаєте когось, хто ще не в %@? Запросіть їх вебпереглядачем або комп'ютером."; +"spaces_invites_coming_soon_title" = "Запрошення ще в розробці"; +"spaces_coming_soon_detail" = "Ця можливість тут іще не втілена, але скоро буде. Поки що можете зробити це в Element на комп'ютері."; +"space_participants_action_remove" = "Вилучити з цього простору"; +"space_participants_action_ban" = "Заблокувати в цьому просторі"; + +// Service terms +"service_terms_modal_title_message" = "Щоб продовжити, прийміть такі умови й положення"; +"service_terms_modal_description_identity_server" = "Інші зможуть знаходити вас, якщо в контактах їхнього телефону записаний ваш номер чи е-пошта."; +"service_terms_modal_description_integration_manager" = "Це дасть змогу використовувати ботів, мости, віджети й пакунки наліпок."; +"service_terms_modal_information_description_identity_server" = "Сервер ідентифікації допомагає знаходити облікові записи за номером телефону чи адресою е-пошти."; +"service_terms_modal_information_description_integration_manager" = "Менеджер інтеграцій дає змогу додавати сторонній функціонал."; +"share_extension_low_quality_video_message" = "Надішліть в %@ у кращій якості, або нижче в низькій."; + +// Mark: - Polls + +"poll_edit_form_create_poll" = "Створити опитування"; +"poll_edit_form_poll_question_or_topic" = "Питання опитування або тема"; +"poll_edit_form_question_or_topic" = "Питання чи тема"; +"poll_edit_form_input_placeholder" = "Напишіть щось"; +"poll_edit_form_create_options" = "Створіть варіанти"; +"poll_edit_form_option_number" = "Варіант %lu"; +"poll_edit_form_add_option" = "Додати варіант"; +"poll_edit_form_post_failure_title" = "Не вдалося надіслати опитування"; +"poll_edit_form_post_failure_subtitle" = "Повторіть спробу"; +"poll_edit_form_post_failure_action" = "Гаразд"; +"poll_timeline_one_vote" = "1 голос"; +"poll_timeline_votes_count" = "%lu голосів"; +"poll_timeline_total_no_votes" = "Жодного голосу"; +"poll_timeline_total_one_vote" = "1 голос надіслано"; +"poll_timeline_total_votes" = "%lu голосів надіслано"; +"poll_timeline_total_one_vote_not_voted" = "1 голос надіслано. Проголосуйте, щоб побачити результати"; +"poll_timeline_total_votes_not_voted" = "%lu голосів надіслано. Проголосуйте, щоб побачити результати"; +"poll_timeline_total_final_results_one_vote" = "Остаточні результати на підставі 1 голосу"; +"poll_timeline_total_final_results" = "Остаточні результати на підставі %lu голосів"; +"poll_timeline_vote_not_registered_title" = "Голос не зареєстровано"; +"poll_timeline_vote_not_registered_subtitle" = "Не вдалося зареєструвати ваш голос, просимо повторити спробу"; +"poll_timeline_vote_not_registered_action" = "Гаразд"; +"poll_timeline_not_closed_title" = "Не вдалося завершити опитування"; +"poll_timeline_not_closed_subtitle" = "Повторіть спробу"; +"poll_timeline_not_closed_action" = "Гаразд"; +"e2e_need_log_in_again" = "Вам потрібно ввійти заново, щоб згенерувати ключі наскрізного шифрування для цього сеансу й надіслати відкритий ключ домашньому серверу.\nЦе одноразова дія; перепрошуємо за незручності."; + +// Crypto +"e2e_enabling_on_app_update" = "%@ тепер підтримує наскрізне шифрування. Перезайдіть, щоб його ввімкнути.\n\nМожете зробити це зараз або пізніше в налаштуваннях застосунку."; + +// Key backup wrong version +"e2e_key_backup_wrong_version_title" = "Нова резервна копія ключів"; +"e2e_key_backup_wrong_version" = "Знайдено нову резервну копію ключів до захищених повідомлень.\n\nЯкщо це були не ви, встановіть нову фразу безпеки в налаштуваннях."; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Ми не записуємо й не аналізуємо жодних даних облікового запису"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Ми не передаємо даних стороннім особам"; +"analytics_prompt_point_3" = "Можете вимкнути це коли завгодно в налаштуваннях"; +"analytics_prompt_not_now" = "Відкласти"; +"analytics_prompt_yes" = "Так, усе гаразд"; +"analytics_prompt_stop" = "Більше не надсилати"; +"analytics_prompt_terms_link_upgrade" = "тут"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Прочитайте всі наші умови %@. Згодні з ними?"; +"analytics_prompt_terms_link_new_user" = "тут"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Можете прочитати всі наші умови %@."; +"analytics_prompt_message_upgrade" = "Раніше ви погодилися надсилати нам анонімні дані про використання. Тепер, щоб розуміти, як люди використовують кілька пристроїв, ми створимо спільний для ваших пристроїв випадковий ідентифікатор."; +"analytics_prompt_message_new_user" = "Допомагайте нам визначати проблеми й удосконалювати Element, надсилаючи анонімні дані про використання. Щоб розуміти, як люди використовують кілька пристроїв, ми створимо спільний для ваших пристроїв випадковий ідентифікатор."; + +// Analytics +"analytics_prompt_title" = "Допоможіть удосконалити %@"; +"widget_integration_room_not_visible" = "Кімната %@ недоступна."; +"widget_integration_missing_user_id" = "В запиті бракує user_id."; +"widget_integration_missing_room_id" = "В запиті бракує room_id."; +"widget_integration_no_permission_in_room" = "У вас нема такого дозволу в цій кімнаті."; +"widget_integration_must_be_in_room" = "Вас нема в цій кімнаті."; +"widget_integration_positive_power_level" = "Рівень повноважень має бути цілим додатним числом."; +"widget_integration_room_not_recognised" = "Кімнату не знайдено."; +"widget_integration_failed_to_send_request" = "Не вдалося надіслати запит."; +"widget_integration_unable_to_create" = "Не вдалося створити віджет."; + +// Widget Integration Manager +"widget_integration_need_to_be_able_to_invite" = "Для цього вам потрібен дозвіл запрошувати користувачів."; +"widget_creation_failure" = "Помилка створення віджету"; +"widget_no_power_to_manage" = "Для керування віджетами у цій кімнаті потрібен дозвіл"; +"bug_report_progress_uploading" = "Надсилання звіту"; +"bug_report_progress_zipping" = "Збір журналів"; +"bug_report_send_screenshot" = "Надіслати знімок екрана"; +"room_details_access_section_for_dm" = "Хто може мати доступ?"; +"room_details_access_section" = "Хто має доступ до кімнати?"; +"room_details_low_priority_tag" = "Неважливе"; +"room_details_favourite_tag" = "Улюблене"; +"identity_server_settings_alert_disconnect_still_sharing_3pid" = "Сервер ідентифікації %@ досі поширює ваші особисті дані.\n\nРадимо вилучити адреси е-пошти й номери телефонів із сервера ідентифікації, перш ніж від'єднатися."; +"settings_analytics_and_crash_data" = "Надсилати дані про збої та аналітику"; +"accessibility_button_label" = "кнопка"; +"enable" = "Увімкнути"; +"call_no_stun_server_error_use_fallback_button" = "Спробувати %@"; +"call_no_stun_server_error_message_2" = "Також ви можете спробувати публічний сервер %@, але це буде менш надійно й сервер бачитиме вашу IP-адресу. Ви можете керувати цим у налаштуваннях"; +"call_no_stun_server_error_message_1" = "Запропонуйте адміністратору вашого домашнього сервера %@ налаштувати сервер TURN для надійної роботи викликів."; +"call_no_stun_server_error_title" = "Не вдалося зателефонувати через неправильно налаштований сервер"; +"call_jitsi_error" = "Не вдалося приєднатися до конференції."; +"call_already_displayed" = "Виклик уже триває."; +"photo_library_access_not_granted" = "%@ не має доступу до медіатеки, просимо змінити налаштування приватності"; +"camera_unavailable" = "Камера недоступна на вашому пристрої"; +"camera_access_not_granted" = "%@ не має дозволу використовувати камеру, просимо змінити налаштування приватності"; +"rage_shake_prompt" = "Схоже, ви розчаровано струсили телефон. Бажаєте надіслати звіт про ваду?"; +"bug_report_prompt" = "При останньому запуску застосунок закрився з помилкою. Бажаєте надіслати звіт про помилку?"; +"homeserver_connection_lost" = "Не вдалося з'єднатися з домашнім сервером."; +"event_formatter_jitsi_widget_removed_by_you" = "Ви вилучаєте голосову конференцію"; +"event_formatter_jitsi_widget_added_by_you" = "Ви додаєте голосову конференцію"; +"event_formatter_rerequest_keys_part2" = " ваших інших сеансів."; +"event_formatter_rerequest_keys_part1_link" = "Повторити запит ключів шифрування"; +"event_formatter_jitsi_widget_removed" = "%@ вилучає голосову конференцію"; +"event_formatter_jitsi_widget_added" = "%@ додає голосову конференцію"; +"event_formatter_widget_removed" = "%@ віджет видалено %@"; +"event_formatter_widget_added" = "%@ віджет додано %@"; + +// Events formatter +"event_formatter_member_updates" = "%tu змін членства"; +"directory_server_type_homeserver" = "Загальнодоступні кімнати якого домашнього сервера перелічити?"; +"receipt_status_read" = "Прочитано: "; + +// Read Receipts +"read_receipts_list" = "Список міток прочитання"; +"room_details_addresses_disable_main_address_prompt_msg" = "Ви вилучаєте головну адресу. Усталена головна адреса для цієї кімнати буде обрана випадково"; +"room_details_access_section_no_address_warning" = "Щоб на кімнату посилалися, вона повинна мати адресу"; +"room_details_access_section_anyone" = "Будь-хто з посиланням, зокрема гості"; +"room_details_access_section_anyone_apart_from_guest_for_dm" = "Будь-хто з посиланням, окрім гостей"; +"room_details_access_section_anyone_apart_from_guest" = "Будь-хто з посиланням, окрім гостей"; From 7a4299384b45778ea0923a29bc8a3bfe8e64b369 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Fri, 17 Dec 2021 13:00:53 +0000 Subject: [PATCH 045/109] Translated using Weblate (Ukrainian) Currently translated at 91.8% (1263 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 6a01ebc8a5..c0eb4286b2 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -1050,13 +1050,13 @@ "settings_discovery_three_pid_details_information_email" = "Керуйте параметрами для цієї адреси е-пошти, за якою інші користувачі можуть вас знаходити й запрошувати до кімнат. Додавайте й видаляйте адреси в Облікових записах."; "settings_discovery_three_pids_management_information_part1" = "Керуйте, за якими адресами е-пошти й номерами телефону інші користувачі зможуть вас знаходити й запрошувати до кімнат. Щоб додати адреси й номери в цей список чи вилучити наявні, перейдіть у "; "settings_discovery_no_identity_server" = "Зараз ви не використовуєте сервер ідентифікації. Щоб наявні контакти могли вас знаходити, додайте такий сервер."; -"settings_key_backup_info_not_valid" = "Цей сеанс не створює резервної копії ваших ключів, але у вас уже є резервна копія, яку ви можете відновити й поповнювати далі."; +"settings_key_backup_info_not_valid" = "Цей сеанс не створює резервної копії ваших ключів, але у вас уже є резервна копія, яку ви можете відновити й додати у майбутньому."; "settings_key_backup_info_version" = "Версія резервного копіювання ключів: %@"; "settings_labs_message_reaction" = "Реагувати на повідомлення за допомогою емодзі"; -"settings_contacts_enable_sync_description" = "Це використовуватиме ваш сервер ідентифікації, щоб ви знаходили своїх контактів, а вони вас."; -"settings_integrations_allow_description" = "Використовуйте менеджер інтеграцій (%@), щоб керувати ботами, мостами, віджетами й пакунками наліпок.\n\nМенеджери інтеграцій отримують ваші параметри й можуть змінювати віджети, надсилати запрошення до кімнат і видавати дозволи від вашого імені."; +"settings_contacts_enable_sync_description" = "Це використовуватиме ваш сервер ідентифікації, щоб ви знаходили свої контакти, а вони вас."; +"settings_integrations_allow_description" = "Використовуйте менеджер інтеграцій (%@), щоб керувати ботами, мостами, віджетами й пакунками наліпок.\n\nМенеджери інтеграцій отримують ваші параметри й можуть змінювати віджети, надсилати запрошення до кімнат і надавати повноваження від вашого імені."; "settings_calls_stun_server_fallback_description" = "Дозволити допоміжний сервер викликів %@, коли ваш домашній сервер не надає свого (ваша IP-адреса ставатиме відомою при виклику)."; -"settings_callkit_info" = "Отримувати вхідні виклики, не розблоковуючи екрану. Перегляньте свої виклики (%@) в історії викликів системи. Якщо iCloud увімкнено, ця історія викликів надсилатиметься Apple."; +"settings_callkit_info" = "Отримувати вхідні виклики, не розблоковуючи екран. Перегляньте свої виклики (%@) в історії викликів системи. Якщо iCloud увімкнено, ця історія викликів надсилатиметься Apple."; "settings_confirm_media_size_description" = "Коли це ввімкнено, при надсиланні зображень чи відео вам пропонуватиметься підтвердити їхній розмір."; "settings_three_pids_management_information_part2" = "Знаходження"; "settings_config_user_id" = "Ви ввійшли як %@"; From c18a9222d146113b0cb1d6c2a1cec189ba9bdbaf Mon Sep 17 00:00:00 2001 From: artevaeckt Date: Sat, 18 Dec 2021 13:31:27 +0000 Subject: [PATCH 046/109] Translated using Weblate (German) Currently translated at 97.4% (1340 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 7c7c6aef57..da12953d05 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -397,7 +397,7 @@ "bug_report_send_screenshot" = "Sende Bildschirmfoto"; "bug_report_progress_zipping" = "Sammele Protokolle"; "bug_report_progress_uploading" = "Bericht hochladen"; -"bug_report_send" = "Gesendet"; +"bug_report_send" = "Senden"; "collapse" = "zusammenklappen"; "auth_email_in_use" = "Diese E-Mail-Adresse wird bereits verwendet"; "auth_phone_in_use" = "Diese Telefonnummer wird bereits verwendet"; @@ -1522,3 +1522,12 @@ "poll_edit_form_create_poll" = "Umfrage erstellen"; "settings_labs_enabled_polls" = "Umfragen"; +"share_extension_send_now" = "Jetzt senden"; +"accessibility_button_label" = "Knopf"; +"settings_analytics_and_crash_data" = "Sende Absturz- und Analysedaten"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Wir senden keine Informationen an Dritte"; +"analytics_prompt_terms_link_new_user" = "hier"; + +// Analytics +"analytics_prompt_title" = "Hilf dabei %@ zu verbessern"; From 68ebfd1f268e532453d0766970dbd3d3a4212ea2 Mon Sep 17 00:00:00 2001 From: libexus Date: Fri, 17 Dec 2021 16:09:28 +0000 Subject: [PATCH 047/109] Translated using Weblate (German) Currently translated at 97.4% (1340 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index da12953d05..457ac82d94 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -55,7 +55,7 @@ "auth_missing_phone" = "Fehlende Telefon-Nummer"; "auth_missing_email_or_phone" = "Fehlende E-Mail-Adresse oder Telefon-Nummer"; "auth_password_dont_match" = "Passwörter stimmen nicht überein"; -"auth_username_in_use" = "Nutzername bereits verwendet"; +"auth_username_in_use" = "Benutzername bereits verwendet"; "auth_forgot_password" = "Passwort vergessen?"; "auth_msisdn_validation_title" = "Verifizierung ausstehend"; "auth_msisdn_validation_message" = "Bitte gib unten den Aktivierungs-Code ein, den wir per SMS verschickt haben."; @@ -1531,3 +1531,5 @@ // Analytics "analytics_prompt_title" = "Hilf dabei %@ zu verbessern"; +"settings_about" = "ÜBER"; +"enable" = "Aktivieren"; From 2fa62e8c89695e36422dd1da3f83d9942bda7b6e Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Sun, 19 Dec 2021 13:24:58 +0000 Subject: [PATCH 048/109] Translated using Weblate (French) Currently translated at 99.6% (1370 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/fr/ --- Riot/Assets/fr.lproj/Vector.strings | 86 ++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index 6ff0e40911..767c24666a 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -94,7 +94,7 @@ "room_creation_keep_private" = "Garder privée"; "room_creation_make_private" = "Rendre privée"; "room_creation_wait_for_creation" = "Un salon est déjà en cours de création. Veuillez patienter."; -"room_creation_invite_another_user" = "Rechercher/inviter par identifiant, nom ou e-mail"; +"room_creation_invite_another_user" = "Identifiant, nom ou e-mail"; // Room recents "room_recents_directory_section" = "RÉPERTOIRE DES SALONS"; "room_recents_favourites_section" = "FAVORIS"; @@ -251,9 +251,9 @@ "settings_user_settings" = "PRÉFÉRENCES UTILISATEUR"; "settings_notifications_settings" = "PRÉFÉRENCES DE NOTIFICATIONS"; "settings_ignored_users" = "UTILISATEURS IGNORÉS"; -"settings_contacts" = "CONTACTS LOCAUX"; +"settings_contacts" = "CONTACTS DE L’APPAREIL"; "settings_advanced" = "AVANCÉ"; -"settings_other" = "AUTRES"; +"settings_other" = "Autres"; "settings_labs" = "EXPÉRIMENTAL"; "settings_devices" = "SESSIONS"; "settings_cryptography" = "CHIFFREMENT"; @@ -1520,3 +1520,83 @@ "room_recents_suggested_rooms_section" = "SALONS RECOMMANDÉS"; "done" = "Terminé"; "open" = "Ouvrir"; +"poll_timeline_not_closed_action" = "Ok"; +"poll_timeline_not_closed_subtitle" = "Merci de réessayer"; +"poll_timeline_not_closed_title" = "Échec de la fermeture du sondage"; +"poll_timeline_vote_not_registered_action" = "Ok"; +"poll_timeline_vote_not_registered_subtitle" = "Désolé votre vote n’a pas été enregistré, veuillez réessayer"; +"poll_timeline_vote_not_registered_title" = "Vote non enregistré"; +"poll_timeline_total_final_results" = "Résultats finaux basés sur %lu votes"; +"poll_timeline_total_final_results_one_vote" = "Résultats finaux basés sur 1 vote"; +"poll_timeline_total_votes_not_voted" = "%lu votes recueillis. Votez pour consulter les résultats"; +"poll_timeline_total_one_vote_not_voted" = "1 vote recueilli. Votez pour connaître les résultats"; +"poll_timeline_total_votes" = "%lu votes recueillis"; +"poll_timeline_total_one_vote" = "1 vote recueilli"; +"poll_timeline_total_no_votes" = "Aucun vote recueilli"; +"poll_timeline_votes_count" = "%lu votes"; +"poll_timeline_one_vote" = "1 vote"; +"poll_edit_form_post_failure_action" = "Ok"; +"poll_edit_form_post_failure_subtitle" = "Merci de réessayer"; +"poll_edit_form_post_failure_title" = "Échec de la publication du sondage"; +"poll_edit_form_add_option" = "Ajouter une option"; +"poll_edit_form_option_number" = "Option %lu"; +"poll_edit_form_create_options" = "Ajouter des options"; +"poll_edit_form_input_placeholder" = "Écrivez quelque chose"; +"poll_edit_form_question_or_topic" = "Question ou sujet"; +"poll_edit_form_poll_question_or_topic" = "Question ou sujet du sondage"; + +// Mark: - Polls + +"poll_edit_form_create_poll" = "Créer un sondage"; +"space_home_show_all_rooms" = "Afficher tous les salons"; +"service_terms_modal_information_description_integration_manager" = "Un gestionnaire d’intégrations vous permet d’ajouter des fonctionnalités de tierces-parties."; +"service_terms_modal_information_description_identity_server" = "Un serveur d’identité vous aide à trouver vos contacts, en recherchant leur numéro de téléphone ou adresse e-mail pour vérifier s’ils ont déjà un compte."; +"service_terms_modal_information_title_integration_manager" = "Gestionnaire d’intégrations"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Serveur d’identité"; +"service_terms_modal_description_integration_manager" = "Ceci vous permettra d’utiliser les robots, passerelles, widgets et jeux d’autocollants."; +"service_terms_modal_description_identity_server" = "Ceci permettra aux personnes qui ont votre numéro de téléphone ou adresse e-mail de sauvegardées dans leurs contacts de vous trouver."; +"service_terms_modal_table_header_integration_manager" = "CONDITIONS D’UTILISATION DU GESTIONNAIRE D’INTÉGRATIONS"; +"service_terms_modal_table_header_identity_server" = "CONDITIONS D’UTILISATION DU SERVEUR D’IDENTITÉ"; +"service_terms_modal_footer" = "Ceci peut être désactivé a n’importe quel moment dans les paramètres."; + +// Service terms +"service_terms_modal_title_message" = "Pour poursuivre, acceptez nos conditions d’utilisation"; +"share_extension_send_now" = "Envoyer maintenant"; +"share_extension_low_quality_video_message" = "Envoyez depuis %@ pour une meilleure qualité. Ou envoyez en mauvaise qualité ci-dessous."; +"share_extension_low_quality_video_title" = "La vidéo sera envoyée en mauvaise qualité"; +"analytics_prompt_stop" = "Arrêter de partager"; +"analytics_prompt_yes" = "Oui, ça me va"; +"analytics_prompt_not_now" = "Pas maintenant"; +"analytics_prompt_point_3" = "Vous pouvez désactiver ceci à tout moment dans les paramètres"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Nous ne partageons pas les données avec des entités tierces"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Nous n’enregistrons et ne profilons pas de données liées à votre compte"; +"analytics_prompt_terms_link_upgrade" = "ici"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Lisez nos conditions d’utilisation %@. Êtes vous d’accord ?"; +"analytics_prompt_terms_link_new_user" = "ici"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Vous pouvez lire nos conditions d’utilisation %@."; +"analytics_prompt_message_upgrade" = "Vous aviez consenti précédemment à partager des rapports d’utilisation avec nous. Désormais, pour nous aider à comprendre comment les gens utilisent cette application sur plusieurs appareils, nous allons générer un identifiant aléatoire commun à tous vos appareils."; +"analytics_prompt_message_new_user" = "Aidez nous à identifier les problèmes et améliorer Element en envoyant des rapports d’usage anonymes. Pour comprendre de quelle manière les gens utilisent Element sur plusieurs appareils, nous créeront un identifiant aléatoire commun à tous vos appareils."; + +// Analytics +"analytics_prompt_title" = "Aidez à améliorer %@"; +"settings_discovery_accept_terms" = "Accepter les conditions du serveur d’identité"; +"settings_analytics_and_crash_data" = "Envoyer les rapports de plantages et d’utilisation"; +"settings_labs_enabled_polls" = "Sondages"; +"settings_about" = "À PROPOS"; +"settings_phone_contacts" = "CONTACTS DU TÉLÉPHONE"; +"room_event_action_forward" = "Transférer"; +"room_event_action_end_poll" = "Mettre fin au sondage"; +"room_event_action_remove_poll" = "Supprimer le sondage"; +"find_your_contacts_identity_service_error" = "Impossible de se connecter au serveur d’identité."; +"find_your_contacts_button_title" = "Trouvez vos contacts"; +"find_your_contacts_message" = "Permettez à %@ d’accéder à vos contacts pour commencer à discuter rapidement avec ceux que vous connaissez le mieux."; +"contacts_address_book_permission_denied_alert_message" = "Pour activer les contacts, rendez vous dans les paramètres de votre appareil."; +"contacts_address_book_permission_denied_alert_title" = "Contacts désactivés"; +"accessibility_button_label" = "bouton"; +"enable" = "Activer"; From ab3c085ad2700e68b6e7ef5415bae40eb9e7e707 Mon Sep 17 00:00:00 2001 From: sr093906 Date: Fri, 17 Dec 2021 01:24:02 +0000 Subject: [PATCH 049/109] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (1375 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/zh_Hans/ --- Riot/Assets/zh_Hans.lproj/Vector.strings | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index dfc1d66597..12863f8faa 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -1583,3 +1583,25 @@ "settings_labs_enabled_polls" = "投票"; "room_event_action_end_poll" = "结束投票"; "room_event_action_remove_poll" = "删除投票"; +"analytics_prompt_stop" = "停止共享"; +"analytics_prompt_yes" = "是的,没关系"; +"analytics_prompt_not_now" = "不是现在"; +"analytics_prompt_point_3" = "您可以随时在设置中关闭此功能"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "我们与第三方共享信息"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "我们 记录任何账户数据或绘制任何账户数据的画像"; +"analytics_prompt_terms_link_upgrade" = "此处"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "阅读我们所有的条款 %@。 那样行吗?"; +"analytics_prompt_terms_link_new_user" = "此处"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "你可以阅读我们所有的条款 %@。"; +"analytics_prompt_message_upgrade" = "您之前同意与我们共享匿名使用数据。 现在,为了帮助了解人们如何使用多个设备,我们将生成一个随机标识符,由您的设备共享。"; +"analytics_prompt_message_new_user" = "通过共享匿名使用数据帮助我们发现问题并改进 Element。 为了了解人们如何使用多个设备,我们将生成一个随机标识符,由您的设备共享。"; + +// Analytics +"analytics_prompt_title" = "帮助改进 %@"; +"settings_analytics_and_crash_data" = "发送崩溃和分析数据"; +"accessibility_button_label" = "按钮"; +"enable" = "启用"; From 64c225f8877005ad5c8e01e32f7e6aa8c65612b3 Mon Sep 17 00:00:00 2001 From: random Date: Fri, 17 Dec 2021 10:44:00 +0000 Subject: [PATCH 050/109] Translated using Weblate (Italian) Currently translated at 100.0% (1375 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ --- Riot/Assets/it.lproj/Vector.strings | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 46aca8d95e..797589e892 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -1539,3 +1539,25 @@ "settings_labs_enabled_polls" = "Sondaggi"; "room_event_action_end_poll" = "Termina sondaggio"; "room_event_action_remove_poll" = "Rimuovi sondaggio"; +"analytics_prompt_stop" = "Non condividere più"; +"analytics_prompt_yes" = "Sì, va bene"; +"analytics_prompt_not_now" = "Non ora"; +"analytics_prompt_point_3" = "Puoi disattivarlo in qualsiasi momento nelle impostazioni"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Non condividiamo informazioni con terze parti"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Non registriamo o profiliamo alcun dato dell'account"; +"analytics_prompt_terms_link_upgrade" = "qui"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Leggi i nostri termini di servizio %@. Accetti?"; +"analytics_prompt_terms_link_new_user" = "qui"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Puoi leggere i nostri termini di servizio %@."; +"analytics_prompt_message_upgrade" = "Hai acconsentito precedentemente a condividere con noi dati di utilizzo anonimi. Ora, per capire come le persone usano diversi dispositivi, genereremo un identificativo casuale, condiviso dai tuoi dispositivi."; +"analytics_prompt_message_new_user" = "Aiutaci a identificare problemi e a migliorare Element condividendo dati di utilizzo anonimi. Per capire come le persone usano diversi dispositivi, genereremo un identificativo casuale, condiviso dai tuoi dispositivi."; + +// Analytics +"analytics_prompt_title" = "Aiuta a migliorare %@"; +"settings_analytics_and_crash_data" = "Invia crash e dati analitici"; +"accessibility_button_label" = "pulsante"; +"enable" = "Attiva"; From df07e23eff04a276e0a8add00fee3326444890b7 Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Thu, 16 Dec 2021 21:32:52 +0000 Subject: [PATCH 051/109] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1375 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ --- Riot/Assets/pt_BR.lproj/Vector.strings | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index acafe6153c..7820fc1f4d 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -1536,3 +1536,25 @@ "settings_labs_enabled_polls" = "Sondagens"; "room_event_action_end_poll" = "Terminar sondagem"; "room_event_action_remove_poll" = "Remover sondagem"; +"analytics_prompt_stop" = "Parar de compartilhar"; +"analytics_prompt_yes" = "Sim, pode ser"; +"analytics_prompt_not_now" = "Não agora"; +"analytics_prompt_point_3" = "Você pode desativar isto a qualquer hora em configurações"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Nós não compartilhamos informação com terceiros"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Nós não gravamos ou perfilamos quaisquer dados de conta"; +"analytics_prompt_terms_link_upgrade" = "aqui"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Leia todos os nossos termos %@. Isso está OK?"; +"analytics_prompt_terms_link_new_user" = "aqui"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Você pode ler todos os nossos termos %@."; +"analytics_prompt_message_upgrade" = "Você previamente consentiu a compartilhar dados de uso anônimos conosco. Agora, para ajudar a entender como pessoas usam múltiplos dispositivos, nós vamos gerar um identificador aleatório, compartilhado por seus dispositivos."; +"analytics_prompt_message_new_user" = "Ajude-nos a identificar problemas e melhorar Element ao compartilhar dados de uso anônimos. Para entender como pessoas usam múltiplos dispositivos, nós geramos um identificador aleatório, compartilhado por seus dispositivos."; + +// Analytics +"analytics_prompt_title" = "Ajudar a melhorar %@"; +"settings_analytics_and_crash_data" = "Enviar dados de crash e analítica"; +"accessibility_button_label" = "botão"; +"enable" = "Habilitar"; From 5152a7230ac8661e2379d0bdd272434cb1d1e382 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Fri, 17 Dec 2021 21:42:43 +0000 Subject: [PATCH 052/109] Translated using Weblate (Ukrainian) Currently translated at 93.8% (1291 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 52 ++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index c0eb4286b2..ed0a38feb9 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -163,7 +163,7 @@ "room_participants_leave_prompt_msg_for_dm" = "Ви впевнені, що хочете вийти?"; "room_participants_leave_prompt_title_for_dm" = "Вийти"; "client_android_name" = "Element Android"; -"store_promotional_text" = "Застосунок для бесід та співпраці, що зберігає приватність у відкритій мережі. Децентралізований, щоб надати вам контроль над даними. Без обробки даних, без бекдорів, без доступу для третіх сторін."; +"store_promotional_text" = "Застосунок для спілкування та співпраці, що зберігає приватність у відкритій мережі. Децентралізований, щоб надати вам контроль над даними. Без обробки даних, без бекдорів, без доступу для третіх сторін."; "settings_three_pids_management_information_part3" = "."; "settings_three_pids_management_information_part1" = "Керуйте звідси адресами е-пошти чи номерами телефонів, які можна застосовувати для входу або відновлення облікового запису. Контролюйте хто і як може вас знайти "; "contacts_address_book_no_identity_server" = "Сервер ідентифікації не налаштований"; @@ -433,7 +433,7 @@ "room_widget_permission_theme_permission" = "Ваша тема"; "room_widget_permission_user_id_permission" = "Ваш ID користувача"; "room_widget_permission_avatar_url_permission" = "URL-адреса вашого аватара"; -"room_widget_permission_display_name_permission" = "Ваше показуване ім'я"; +"room_widget_permission_display_name_permission" = "Ваше показуване імʼя"; "room_widget_permission_creator_info_title" = "Цей віджет додано:"; // Room widget permissions @@ -1002,7 +1002,7 @@ // Room key request dialog "e2e_room_key_request_title" = "Запит ключів шифрування"; "share_extension_send_now" = "Надіслати зараз"; -"service_terms_modal_accept_button" = "Прийняти"; +"service_terms_modal_accept_button" = "Погодитися"; "room_details_flair_invalid_id_prompt_msg" = "%@ — неправильний ідентифікатор спільноти"; "room_details_flair_invalid_id_prompt_title" = "Неправильний формат"; "room_details_new_flair_placeholder" = "Додати новий ID спільноти (напр., +foo%@)"; @@ -1068,7 +1068,7 @@ "room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Поки жоден сервер ідентифікації не налаштований, ви не можете почати бесіду з кимось за адресою е-пошти."; "find_your_contacts_message" = "Дозвольте %@ показувати ваші контакти, щоб ви могли швидко почати бесіду з тими, кого знаєте найкраще."; "find_your_contacts_title" = "Почніть із переліку своїх контактів"; -"store_full_description" = "Element — застосунок для листування й співпраці нового покоління:\n\n1. Надає вам контроль над збереженням вашої приватності\n2. Дає змогу спілкуватися з будь-ким у мережі Matrix і навіть за її межами, інтегруючись із такими застосунками, як Slack\n3. Оберігає вас від реклами, збору даних, бекдорів і прив'язаності до провайдера\n4. Захищає вас наскрізним шифруванням і звіркою інших перехресним підписуванням\n\nElement суттєво відрізняється від інших застосунків для листування й співпраці тим, що безцентровий і має відкритий код.\n\nElement дає змогу самостійно встановити сервер — чи обрати з публічних — щоб ви зберігали приватність своїх даних і розмов, власність і контроль над ними. Він надає вам доступ до відкритої мережі; тож ви можете спілкуватися з користувачами інших застосунків, не лише Element. А ще він добре захищений.\n\nElement здатен на це все завдяки своїй основі Matrix — стандарту відкритого, безцентрового спілкування.\n\nElement надає вам контроль, даючи змогу обрати, в кого зберігаються ваші розмови. В застосунку Element ви можете обрати між такими шляхами:\n\n1. Зареєструйте безкоштовний обліковий запис на публічному сервері matrix.org\n2. Самостійно розмістіть свій обліковий запис, встановивши сервер на власному обладнанні\n3. Отримайте обліковий запис на виділеному сервері, просто оформивши підписку на хостинг-платформу Element Matrix Services\n\nЧому Element?\n\nВОЛОДІЙТЕ СВОЇМИ ДАНИМИ: Ви обираєте, де зберігати свої дані й повідомлення. Ви володієте й керуєте ними, не якась МЕГАКОРПОРАЦІЯ, що аналізує ваші дані й передає їх сторонніх особам.\n\nВІДКРИТЕ ЛИСТУВАННЯ Й СПІВПРАЦЯ: Можете розмовляти з будь-ким іншим у мережі Matrix незалежно від того, використовують вони Element, інший застосунок Matrix чи навіть сторонню систему листування на зразок Slack, IRC чи XMPP.\n\nСУПЕРБЕЗПЕКА: Дійсно наскрізне шифрування (лише учасники розмови можуть розшифрувати повідомлення) й звірка пристроїв учасників розмови перехресним підписуванням.\n\nДОСКОНАЛЕ СПІЛКУВАННЯ: Листуйтеся, робіть голосові й відеовиклики, діліться файлами, транслюйте екран, під'єднуйте різноманітні інтеграції, ботів і віджети. Розбудовуйте кімнати, спільноти, будьте на зв'язку й досягайте цілей.\n\nСКРІЗЬ, ДЕ ВИ: Будьте на зв'язку, де б ви не були, завдяки повній синхронізації історії повідомлень між усіма вашим пристроями й онлайн-клієнтом https://element.io/app."; +"store_full_description" = "Element — застосунок для листування й співпраці нового покоління:\n\n1. Надає вам контроль над збереженням вашої приватності\n2. Дає змогу спілкуватися з будь-ким у мережі Matrix і навіть за її межами, інтегруючись із такими застосунками, як Slack\n3. Оберігає вас від реклами, збору даних, бекдорів і прив'язаності до провайдера\n4. Захищає вас наскрізним шифруванням і звіркою інших перехресним підписуванням\n\nElement суттєво відрізняється від інших застосунків для листування й співпраці тим, що децентралізований і має відкритий код.\n\nElement дає змогу самостійно встановити сервер або обрати з-поміж загальнодоступних, щоб ви зберігали приватність своїх даних і розмов, власність і контроль над ними. Він надає вам доступ до відкритої мережі; тож ви можете спілкуватися з користувачами інших застосунків, не лише Element. А ще він добре захищений.\n\nElement здатен на це все завдяки своїй основі Matrix — стандарту відкритого, децентралізованого спілкування.\n\nElement надає вам контроль, даючи змогу обрати, в кого зберігаються ваші розмови. У застосунку Element ви можете обрати між такими шляхами:\n\n1. Зареєструвати безплатний обліковий запис на загальнодоступному сервері matrix.org\n2. Самостійно розмістити свій обліковий запис, встановивши сервер на власному обладнанні\n3. Отримати обліковий запис на виділеному сервері, просто передплативши хостинг-платформу Element Matrix Services\n\nЧому Element?\n\nВОЛОДІЙТЕ СВОЇМИ ДАНИМИ: Ви обираєте, де зберігати свої дані й повідомлення. Ви володієте й керуєте ними, не якась МЕГАКОРПОРАЦІЯ, що аналізує ваші дані й передає їх стороннім особам.\n\nВІДКРИТЕ ЛИСТУВАННЯ Й СПІВПРАЦЯ: Можете розмовляти з будь-ким іншим у мережі Matrix незалежно від того, використовують вони Element, інший застосунок Matrix чи навіть сторонню систему листування на зразок Slack, IRC чи XMPP.\n\nНАДБЕЗПЕКА: Справжнє наскрізне шифрування (лише учасники розмови можуть розшифрувати повідомлення) й звірка пристроїв учасників розмови перехресним підписуванням.\n\nДОСКОНАЛЕ СПІЛКУВАННЯ: Листуйтеся, робіть голосові й відеовиклики, діліться файлами, транслюйте екран, підʼєднуйте різноманітні інтеграції, ботів і віджети. Розбудовуйте кімнати, спільноти, будьте на звʼязку й досягайте цілей.\n\nСКРІЗЬ, ДЕ ВИ: Будьте на зв'язку, де б ви не були, завдяки повній синхронізації історії повідомлень між усіма вашим пристроями та онлайн-клієнтом https://app.element.io."; "security_settings_crosssigning_info_not_bootstrapped" = "Перехресне підписування ще не налаштовано."; "security_settings_crosssigning" = "ПЕРЕХРЕСНЕ ПІДПИСУВАННЯ"; "security_settings_backup" = "РЕЗЕРВНЕ КОПІЮВАННЯ ПОВІДОМЛЕНЬ"; @@ -1359,7 +1359,7 @@ "secrets_setup_recovery_passphrase_information" = "Введіть відому лише вам фразу безпеки для захисту таємниць на вашому сервері."; "secrets_setup_recovery_passphrase_additional_information" = "Не застосовуйте пароль облікового запису повторно."; "secrets_setup_recovery_passphrase_confirm_information" = "Для підтвердження введіть таємну фразу ще раз."; -"secure_key_backup_setup_existing_backup_error_title" = "Резервна копія повідомлень уже є"; +"secure_key_backup_setup_existing_backup_error_title" = "Резервна копія повідомлень уже існує"; "secure_key_backup_setup_existing_backup_error_info" = "Розблокуйте її, щоб продовжити попереднє захищене резервне копіювання, або видаліть її, щоб почати захищене резервне копіювання повідомлень заново."; "secure_key_backup_setup_existing_backup_error_unlock_it" = "Розблокувати"; "secure_key_backup_setup_existing_backup_error_delete_it" = "Видалити"; @@ -1372,9 +1372,9 @@ "pin_protection_mismatch_too_many_times_error_message" = "Не пригадуєте свій PIN-код? Торкніться кнопки \"Забули PIN-код\"."; "biometrics_usage_reason" = "Застосунок потребує підтвердження особи"; "biometrics_cant_unlocked_alert_message_x" = "Розблокуйте за допомогою %@ або ввійдіть заново й увімкніть %@ ще раз"; -"biometrics_cant_unlocked_alert_message_login" = "Ввійти заново"; +"biometrics_cant_unlocked_alert_message_login" = "Увійти заново"; "device_verification_self_verify_wait_recover_secrets_checking_availability" = "Перевірка інших можливостей звірки ..."; -"create_room_section_footer_type" = "Вхід до закритої кімнати — лише по запрошенню."; +"create_room_section_footer_type" = "Вхід до закритої кімнати — лише за запрошенням."; "pin_protection_kick_user_alert_message" = "Забагато помилок, ваш сеанс припинено"; // MARK: - Secrets recovery @@ -1386,7 +1386,7 @@ "secrets_setup_recovery_passphrase_summary_title" = "Збережіть ключ безпеки"; "secrets_setup_recovery_passphrase_summary_information" = "Запам'ятайте свою фразу безпеки. Нею можна буде розблокувати ваші зашифровані повідомлення й дані."; "home_empty_view_information" = "Досконалий застосунок для захищеного спілкування команд, друзів і організацій. Торкніться кнопки + унизу, щоб додати людей і кімнати."; -"bug_report_background_mode" = "Продовжити на тлі"; +"bug_report_background_mode" = "Продовжити у фоновому режимі"; "room_intro_cell_information_room_without_topic_sentence2_part2" = " щоб люди знали, про що ця кімната."; "room_intro_cell_information_dm_sentence2" = "У цій розмові лише ви двоє, більше ніхто не може приєднатися."; "room_intro_cell_information_multiple_dm_sentence2" = "У цій розмові лише ви, поки хтось із вас не запросить іще когось приєднатися."; @@ -1419,7 +1419,7 @@ "space_participants_action_ban" = "Заблокувати в цьому просторі"; // Service terms -"service_terms_modal_title_message" = "Щоб продовжити, прийміть такі умови й положення"; +"service_terms_modal_title_message" = "Щоб продовжити, погодьтеся з запропонованими умови й положеннями"; "service_terms_modal_description_identity_server" = "Інші зможуть знаходити вас, якщо в контактах їхнього телефону записаний ваш номер чи е-пошта."; "service_terms_modal_description_integration_manager" = "Це дасть змогу використовувати ботів, мости, віджети й пакунки наліпок."; "service_terms_modal_information_description_identity_server" = "Сервер ідентифікації допомагає знаходити облікові записи за номером телефону чи адресою е-пошти."; @@ -1538,3 +1538,37 @@ "room_details_access_section_anyone" = "Будь-хто з посиланням, зокрема гості"; "room_details_access_section_anyone_apart_from_guest_for_dm" = "Будь-хто з посиланням, окрім гостей"; "room_details_access_section_anyone_apart_from_guest" = "Будь-хто з посиланням, окрім гостей"; +"deactivate_account_informations_part5" = "Якщо ви хочете, щоб ми вилучили ваші повідомлення, встановіть прапорець унизу\n\nВидимість повідомлень у Matrix схожа на електронну пошту. Тобто ваші повідомлення не зможуть переглянути нові або незареєстровані користувачі, але зареєстровані користувачі, які вже мають доступ до цих повідомлень і надалі матимуть доступ до їхньої копії на своїх пристроях."; +"deactivate_account_informations_part4_emphasize" = "типово не вилучає надіслані вами повідомлення. "; +"deactivate_account_informations_part2_emphasize" = "Ця дія незворотна."; +"deactivate_account_informations_part1" = "Вашим обліковим записом більше ніколи не можна буде користуватися. Ви не зможете увійти, і ніхто не зможе повторно зареєструвати той самий ID користувача. Ваш обліковий запис буде видалено з усіх кімнат, а всі його дані вилучено з вашого сервера ідентифікації. "; +"e2e_room_key_request_message" = "Ваш не звірений сеанс «%@» запитує ключі шифрування."; +"e2e_room_key_request_message_new_device" = "Ви додали новий сеанс «%@», котрий запитує ключі шифрування."; +"room_widget_permission_information_title" = "Віджет може надсилати дані до %@:\n"; +"room_widget_permission_webview_information_title" = "Віджет може встановлювати куки та надсилати дані до %@:\n"; +"share_extension_failed_to_encrypt" = "Не вдалося надіслати. Перевірте налаштування шифрування для цієї кімнати в головному застосунку"; + +// Share extension +"share_extension_auth_prompt" = "Увійдіть до основного застосунку, щоб поділитися вмістом"; +"service_terms_modal_policy_checkbox_accessibility_hint" = "Позначте, щоб погодитися на %@"; +"gdpr_consent_not_given_alert_review_now_action" = "Переглянути зараз"; + +// GDPR +"gdpr_consent_not_given_alert_message" = "Перегляньте та погодьтеся з умовами користування, щоб продовжувати використовувати сервер %@."; +"e2e_room_key_request_ignore_request" = "Знехтувати запит"; +"e2e_room_key_request_share_without_verifying" = "Надіслати без перевірки"; +"e2e_room_key_request_start_verification" = "Починається перевірка…"; +"deactivate_account_forget_messages_information_part3" = ": внаслідок цього майбутні користувачі не можуть не розуміти змісту розмов)"; +"deactivate_account_forget_messages_information_part1" = "Видаліть усі надіслані мною повідомлення після деактивації мого облікового запису ("; +"rerequest_keys_alert_message" = "Запустіть %@ на іншому пристрої, який зможе розшифрувати повідомлення та надіслати ключі цьому сеансу."; +"widget_integration_manager_disabled" = "Необхідно увімкнути менеджер інтеграцій у налаштуваннях"; +"widget_menu_remove" = "Вилучити для всіх"; +"widget_menu_revoke_permission" = "Відкликати мій доступ"; +"widget_menu_open_outside" = "Відкрити у переглядачі"; +"widget_menu_refresh" = "Оновити"; +"widget_sticker_picker_no_stickerpacks_alert_add_now" = "Додати зараз?"; +"widget_sticker_picker_no_stickerpacks_alert" = "На разі жоден пакунок наліпок не увімкнено."; +"widget_integrations_server_failed_to_connect" = "Не вдалося зʼєднатися з сервером інтеграцій"; + +// Widget +"widget_no_integrations_server_configured" = "Сервер інтеграцій не налаштовано"; From 5fc1cf22580173f605be6342632e6577cea6dea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Thu, 16 Dec 2021 21:01:37 +0000 Subject: [PATCH 053/109] Translated using Weblate (Estonian) Currently translated at 100.0% (1375 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index bf78485a30..2a1abaf1d5 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -1505,3 +1505,25 @@ "settings_labs_enabled_polls" = "Küsitlused"; "room_event_action_end_poll" = "Lõpeta küsitlus"; "room_event_action_remove_poll" = "Kustuta küsitlus"; +"analytics_prompt_stop" = "Lõpeta andmete jagamine"; +"analytics_prompt_yes" = "Jah, see on sobilik"; +"analytics_prompt_not_now" = "Mitte praegu"; +"analytics_prompt_point_3" = "Seadistustest saad alati määrata, et see funktsionaalsus pole kasutusel"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Meie ei jaga teavet kolmandate osapooltega"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Meie ei salvesta ega profileeri sinu kasutajakonto andmeid"; +"analytics_prompt_terms_link_upgrade" = "siit"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Kõik meie tingimused leiad %@. Kas sa oled nõus?"; +"analytics_prompt_terms_link_new_user" = "siit"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Meie kasutustingimused leiad %@."; +"analytics_prompt_message_upgrade" = "Sa oled varem nõustunud meiega anonüümsete andmete jagamisega. Selleks, et mõistaksime, kuidas kasutajad erinevaid seadmeid pruugivad, me loome sinu seadmetele ühise juhusliku tunnuse."; +"analytics_prompt_message_new_user" = "Võimalike vigade leidmiseks ja Element'i arendamiseks jaga meiega anonüümseid andmeid. Selleks, et mõistaksime, kuidas kasutajad erinevaid seadmeid pruugivad me loome sinu seadmetele ühise juhusliku tunnuse."; + +// Analytics +"analytics_prompt_title" = "Aita arendada %@ rakendust"; +"settings_analytics_and_crash_data" = "Saada rakenduse vigade ja analüütika andmeid"; +"accessibility_button_label" = "nupp"; +"enable" = "Võta kasutusele"; From 4120d0a8960204242be41475e1f6ff3137ead5d3 Mon Sep 17 00:00:00 2001 From: Linerly Date: Fri, 17 Dec 2021 10:03:18 +0000 Subject: [PATCH 054/109] Translated using Weblate (Indonesian) Currently translated at 100.0% (1375 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ --- Riot/Assets/id.lproj/Vector.strings | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index c474eaf31d..e92ace0220 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -1684,3 +1684,25 @@ "settings_labs_enabled_polls" = "Poll"; "room_event_action_end_poll" = "Akhiri poll"; "room_event_action_remove_poll" = "Hapus poll"; +"analytics_prompt_stop" = "Berhenti membagikan"; +"analytics_prompt_yes" = "Iya, saya tidak keberatan"; +"analytics_prompt_not_now" = "Jangan sekarang"; +"analytics_prompt_point_3" = "Anda dapat mematikannya kapan saja di pengaturan"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Kami tidak membagikan informasi ini dengan pihak ketiga"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Kami tidak merekam atau memprofil data akun apapun"; +"analytics_prompt_terms_link_upgrade" = "di sini"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Baca semua kebijakan kami %@. Apakah Anda tidak keberatan?"; +"analytics_prompt_terms_link_new_user" = "di sini"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Anda dapat membaca semua kebijakan kami %@."; +"analytics_prompt_message_upgrade" = "Anda sebelumnya setuju untuk mengirimkan data penggunaan anonim dengan kami. Sekarang, supaya kami dapat memahami bagaimana orang-orang menggunakan beberapa perangkat-perangkat, kami akan membuat pengenal acak, yang dibagikan oleh perangkat Anda."; +"analytics_prompt_message_new_user" = "Bantu kami mengidentifikasi masalah-masalah dan membuat Element lebih baik dengan membagikan data penggunaan anonim. Untuk memahami bagaimana orang-orang menggunakan beberapa perangkat-perangkat, kami akan membuat pengenal acak, yang dibagikan oleh perangkat Anda."; + +// Analytics +"analytics_prompt_title" = "Bantu membuat %@ lebih baik"; +"settings_analytics_and_crash_data" = "Kirim data crash dan analitik"; +"accessibility_button_label" = "tombol"; +"enable" = "Aktifkan"; From 02c7eededd8ff617ab0e9ae93612bafc889a7433 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Sat, 18 Dec 2021 22:53:34 +0000 Subject: [PATCH 055/109] Translated using Weblate (Slovak) Currently translated at 65.6% (902 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 536 ++++++++++++++++++++++++++++ 1 file changed, 536 insertions(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 25b6c9bda0..a55477f049 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -582,3 +582,539 @@ // Directory "directory_cell_title" = "Prezrieť priečinok"; +"receipt_status_read" = "Prečítané: "; +"room_details_files" = "Nahrané súbory"; +"settings_ui_theme_auto" = "Automaticky"; + +// Success from passphrase +"key_backup_setup_success_from_passphrase_info" = "Vaše kľúče sa zálohujú.\n\nVáš bezpečnostný kľúč je bezpečnostná sieť - môžete ho použiť na obnovenie prístupu k zašifrovaným správam, ak zabudnete svoju prístupovú frázu.\n\nBezpečnostný kľúč uchovávajte na veľmi bezpečnom mieste, napríklad v správcovi hesiel (alebo v trezore)."; +"poll_timeline_not_closed_subtitle" = "Prosím, skúste to znova"; +"settings_unignore_user" = "Zobraziť všetky správy od %@?"; +"accessibility_button_label" = "tlačidlo"; +"settings_add_3pid_invalid_password_message" = "Neplatné poverovacie údaje"; +"security_settings_crypto_sessions" = "MOJE RELÁCIE"; + +// Deactivate account + +"deactivate_account_title" = "Deaktivovať účet"; +"service_terms_modal_information_title_integration_manager" = "Správca integrácie"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Server totožností"; +"e2e_room_key_request_ignore_request" = "Ignorovať žiadosť"; +"e2e_room_key_request_start_verification" = "Spustiť overovanie…"; +"share_extension_send_now" = "Odoslať teraz"; +"room_widget_permission_room_id_permission" = "ID miestnosti"; +"room_widget_permission_widget_id_permission" = "ID widgetu"; +"room_widget_permission_theme_permission" = "Váš vzhľad"; + +// Room widget permissions +"room_widget_permission_title" = "Načítať Widget"; +"widget_picker_manage_integrations" = "Spravovať integrácie…"; +"bug_report_send_screenshot" = "Odoslať snímku obrazovky"; +"event_formatter_group_call" = "Skupinový hovor"; +"event_formatter_call_end_call" = "Ukončiť hovor"; +"event_formatter_call_connection_failed" = "Pripojenie zlyhalo"; +"event_formatter_call_you_declined" = "Hovor odmietnutý"; +"event_formatter_call_has_ended" = "Hovor ukončený"; + +// Image picker +"image_picker_action_camera" = "Urobiť fotografiu"; +"group_participants_invite_malformed_id_title" = "Chyba pozvania"; +"group_participants_leave_prompt_title" = "Opustiť skupinu"; + +// Group participants +"group_participants_add_participant" = "Pridať účastníka"; +"group_home_multi_rooms_format" = "%tu miestnosti"; +"group_home_one_room_format" = "1 miestnosť"; +"group_home_multi_members_format" = "%@ členovia"; + +// Group Home +"group_home_one_member_format" = "1 člen"; + +// Group Details +"group_details_title" = "Podrobnosti o komunite"; +"room_notifs_settings_account_settings" = "Nastavenia účtu"; +"room_notifs_settings_all_messages" = "Všetky správy"; +"room_details_advanced_room_id" = "ID miestnosti:"; +"room_details_banned_users_section" = "Zakázaní používatelia"; +"room_details_flair_invalid_id_prompt_title" = "Neplatný formát"; +"room_details_direct_chat" = "Priama konverzácia"; +"room_details_mute_notifs" = "Stlmiť oznámenia"; +"room_details_low_priority_tag" = "Nízka priorita"; +"room_details_room_name" = "Názov miestnosti"; +"room_details_photo" = "Obrázok miestnosti"; +"room_details_search" = "Hľadať miestnosť"; +"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "Napriek tomu sa odpojiť"; + +// Identity server settings +"identity_server_settings_title" = "Server totožností"; +"manage_session_not_trusted" = "Nedôveryhodné"; +"manage_session_name" = "Názov relácie"; +"manage_session_info" = "INFORMÁCIE O RELÁCII"; + +// Manage session +"manage_session_title" = "Spravovať reláciu"; +"security_settings_secure_backup_setup" = "Nastaviť"; +"security_settings_secure_backup" = "BEZPEČNÉ ZÁLOHOVANIE"; +"security_settings_crypto_sessions_loading" = "Načítavanie relácií…"; +"security_settings_blacklist_unverified_devices" = "Nikdy neodosielať správy do nedôveryhodných relácií"; +"security_settings_crosssigning_bootstrap" = "Nastaviť"; +"security_settings_crosssigning_info_not_bootstrapped" = "Krížové podpisovanie ešte nie je nastavené."; +"security_settings_backup" = "ZÁLOHOVANIE SPRÁVY"; +"security_settings_secure_backup_delete" = "Vymazať zálohu"; +"bug_report_description" = "Popíšte prosím chybu. Čo ste urobili? Čo ste očakávali, že sa stane? Čo sa skutočne stalo?"; + +// Bug report +"bug_report_title" = "Hlásenie o chybe"; +"e2e_need_log_in_again" = "Musíte sa znova prihlásiť, aby ste vygenerovali end-to-end šifrovacie kľúče pre túto reláciu a odoslali verejný kľúč na váš domovský server.\nToto je jednorazové; ospravedlňujeme sa za nepríjemnosti."; + +// No VoIP support +"no_voip_title" = "Prichádzajúci hovor"; +"call_no_stun_server_error_message_2" = "Prípadne môžete skúsiť použiť verejný server na adrese %@, ale nebude to tak spoľahlivé a vaša IP adresa bude zdieľaná s týmto serverom. Môžete to spravovať aj v nastaveniach"; +"call_incoming_voice" = "Prichádzajúci hovor…"; + +// Call +"call_incoming_voice_prompt" = "Prichádzajúci hlasový hovor od %@"; +"call_incoming_video_prompt" = "Prichádzajúci videohovor od %@"; +"room_does_not_exist" = "%@ neexistuje"; +"camera_unavailable" = "Fotoaparát nie je k dispozícii na vašom zariadení"; +"camera_access_not_granted" = "%@ nemá povolenie používať fotoaparát, zmeňte to prosím nastaveniach ochrany súkromia"; +"do_not_ask_again" = "Viac sa nepýtať"; +"bug_report_prompt" = "Posledné spustenie aplikácie skončilo pádom. Chcete odoslať správu o páde?"; +"public_room_section_title" = "Verejné miestnosti (na %@):"; +"homeserver_connection_lost" = "Nepodarilo sa pripojiť k domovskému serveru."; +"key_backup_recover_invalid_passphrase_title" = "Nesprávna bezpečnostná fráza"; + +// MARK: Key backup recover + +"key_backup_recover_title" = "Zabezpečené správy"; + +// Success from secure backup +"key_backup_setup_success_from_secure_backup_info" = "Vaše kľúče sa zálohujú."; +"key_backup_setup_success_from_recovery_key_made_copy_action" = "Vytvoril som si kópiu"; +"key_backup_setup_success_from_recovery_key_make_copy_action" = "Vytvorte si kópiu"; +"key_backup_setup_success_from_recovery_key_recovery_key_title" = "Bezpečnostný kľúč"; +"key_backup_setup_success_from_passphrase_save_recovery_key_action" = "Uložte svoj bezpečnostný kľúč"; +"key_backup_setup_passphrase_set_passphrase_action" = "Nastaviť frázu"; +"key_backup_setup_passphrase_confirm_passphrase_placeholder" = "Potvrdiť frázu"; +"key_backup_setup_passphrase_passphrase_placeholder" = "Zadajte frázu"; + +// Passphrase + +"key_backup_setup_passphrase_title" = "Zabezpečte si svoju zálohu pomocou bezpečnostnej frázy"; +"key_backup_setup_intro_setup_connect_action_with_existing_backup" = "Pripojenie tohto zariadenia k službe Zálohovanie kľúčov"; +"key_backup_setup_intro_setup_action_without_existing_backup" = "Začať používať zálohovanie kľúčov"; +"key_backup_setup_skip_alert_message" = "Ak sa odhlásite alebo stratíte svoje zariadenie, môžete prísť o zabezpečené správy."; + + +// MARK: Key backup setup + +"key_backup_setup_title" = "Zálohovanie kľúčov"; +"secure_backup_setup_banner_subtitle" = "Zabezpečte sa proti strate šifrovaných správ a údajov"; + +// Banner + +"secure_backup_setup_banner_title" = "Bezpečné zálohovanie"; +"secure_key_backup_setup_intro_use_security_passphrase_title" = "Použiť bezpečnostnú frázu"; +"secure_key_backup_setup_intro_use_security_key_title" = "Použiť bezpečnostný kľúč"; +"secure_key_backup_setup_intro_info" = "Zabezpečte sa pred stratou šifrovaných správ a údajov zálohovaním šifrovacích kľúčov na domovskom serveri."; + +// MARK: Secure backup setup + +// Intro + +"secure_key_backup_setup_intro_title" = "Bezpečné zálohovanie"; +"rerequest_keys_alert_message" = "Spustite prosím %@ na inom zariadení, ktoré dokáže dešifrovať správu, aby mohlo poslať kľúče do tejto relácie."; + +// Re-request confirmation dialog +"rerequest_keys_alert_title" = "Žiadosť odoslaná"; +"deactivate_account_password_alert_message" = "Aby ste mohli pokračovať, prosím zadajte svoje heslo"; +"deactivate_account_password_alert_title" = "Deaktivovať účet"; +"deactivate_account_validate_action" = "Deaktivovať účet"; +"deactivate_account_informations_part2_emphasize" = "Táto akcia je nezvratná."; + +// Recovery passphrase + +"secrets_setup_recovery_passphrase_title" = "Nastaviť bezpečnostnú frázu"; +"secrets_setup_recovery_key_storage_alert_title" = "Udržujte ho v bezpečí"; +"secrets_setup_recovery_key_storage_alert_message" = "✓ Vytlačte ho a uložte na bezpečné miesto\n✓ Uložte ho na USB kľúč alebo záložný disk\n✓ Skopírujte si ho do osobného cloudového úložiska"; +"secrets_setup_recovery_key_information" = "Bezpečnostný kľúč uložte na bezpečné miesto. Môžete ho použiť na odomknutie zašifrovaných správ a údajov."; + +// MARK: - Secrets set up + +// Recovery Key + +"secrets_setup_recovery_key_title" = "Uložte svoj bezpečnostný kľúč"; +"secrets_recovery_with_key_invalid_recovery_key_message" = "Skontrolujte, či ste zadali správny bezpečnostný kľúč."; +"secrets_recovery_with_key_invalid_recovery_key_title" = "Nie je možné získať prístup k tajnému úložisku"; +"secrets_recovery_with_key_recovery_key_placeholder" = "Zadajte bezpečnostný kľúč"; +"secrets_recovery_with_passphrase_invalid_passphrase_message" = "Skontrolujte prosím, či ste zadali správnu bezpečnostnú frázu."; +"secrets_recovery_with_passphrase_invalid_passphrase_title" = "Nie je možné získať prístup k tajnému úložisku"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "použiť váš bezpečnostný kľúč"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "Neviete svoju bezpečnostnú frázu? Môžete "; +"secrets_recovery_with_passphrase_passphrase_placeholder" = "Vložiť bezpečnostnú frázu"; +"secrets_recovery_with_passphrase_information_verify_device" = "Na overenie tohto zariadenia použite bezpečnostnú frázu."; +"user_verification_session_details_information_untrusted_current_user" = "Overte túto reláciu, aby ste ju označili za dôveryhodnú a udelili jej prístup k zašifrovaným správam:"; +"user_verification_session_details_information_trusted_other_user_part2" = " ste ju overili:"; +"user_verification_session_details_information_trusted_other_user_part1" = "Táto relácia je dôveryhodná pre bezpečné zasielanie správ, pretože "; +"user_verification_session_details_information_trusted_current_user" = "Táto relácia je dôveryhodná pre bezpečné zasielanie správ, pretože ste ju overili:"; +"user_verification_sessions_list_information" = "Správy s týmto používateľom v tejto miestnosti sú end-to-end šifrované a tretie strany ich nemôžu čítať."; +"user_verification_start_additional_information" = "Aby ste si boli istý, urobte to osobne alebo použite iný dôveryhodný spôsob komunikácie."; +"user_verification_start_waiting_partner" = "Čaká sa na %@…"; +"user_verification_start_information_part2" = " skontrolovaním jednorazového kódu na oboch zariadeniach."; +"user_verification_start_information_part1" = "Pre väčšiu bezpečnosť overte "; +"key_verification_scan_confirmation_scanned_device_information" = "Zobrazuje druhé zariadenie rovnaký štít?"; +"key_verification_scan_confirmation_scanned_user_information" = "Zobrazuje %@ rovnaký štít?"; +"key_verification_scan_confirmation_scanning_device_waiting_other" = "Čakanie na druhé zariadenie…"; +"key_verification_scan_confirmation_scanning_user_waiting_other" = "Čaká sa na %@…"; + +// MARK: Scan confirmation + +// Scanning +"key_verification_scan_confirmation_scanning_title" = "Už je to takmer hotové! Čaká sa na potvrdenie…"; +"key_verification_verify_qr_code_scan_other_code_success_message" = "QR kód bol úspešne overený."; +"key_verification_verify_qr_code_other_scan_my_code_title" = "Naskenoval druhý používateľ QR kód úspešne?"; +"key_verification_verify_qr_code_start_emoji_action" = "Overte pomocou emoji"; +"key_verification_verify_qr_code_scan_code_other_device_action" = "Skenovať pomocou tohto zariadenia"; +"key_verification_verify_qr_code_scan_code_action" = "Naskenujte ich kód"; +"key_verification_verify_qr_code_emoji_information" = "Overenie porovnaním jedinečnej kombinácie emotikonov."; +"key_verification_verify_qr_code_information_other_device" = "Naskenujte nižšie uvedený kód na overenie:"; +"key_verification_verify_qr_code_information" = "Naskenujte kód, aby ste sa navzájom bezpečne overili."; + +// MARK: QR code + +"key_verification_verify_qr_code_title" = "Overte naskenovaním"; + +// Incoming key verification request + +"key_verification_incoming_request_incoming_alert_message" = "%@ žiada o overenie"; +"key_verification_tile_conclusion_warning_title" = "Nedôveryhodné prihlásenie"; +"error_not_supported_on_mobile" = "Toto nemôžete urobiť z mobilného telefónu %@."; +"emoji_picker_flags_category" = "Vlajky"; +"emoji_picker_places_category" = "Cestovanie a miesta"; +"emoji_picker_foods_category" = "Jedlo a nápoje"; +"emoji_picker_nature_category" = "Zvieratá a príroda"; + +// MARK: Emoji picker +"emoji_picker_title" = "Reakcie"; +"device_verification_emoji_bell" = "Zvonec"; +"device_verification_emoji_train" = "Vlak"; +"device_verification_emoji_spanner" = "Vidlicový kľúč"; + +// User + +"key_verification_verified_user_information" = "Správy s týmto používateľom sú end-to-end šifrované a tretie strany ich nemôžu čítať."; +"key_verification_verified_this_session_information" = "Na tomto zariadení teraz môžete čítať zabezpečené správy a ostatní používatelia budú vedieť, že mu môžu dôverovať."; +"key_verification_verified_new_session_information" = "Na novom zariadení teraz môžete čítať zabezpečené správy a ostatní používatelia budú vedieť, že mu môžu dôverovať."; +"key_verification_verified_other_session_information" = "Teraz môžete čítať zabezpečené správy vo vašej druhej relácii a ostatní používatelia budú vedieť, že jej môžu dôverovať."; +"key_verification_verified_new_session_title" = "Nová relácia overená!"; +"room_recents_unknown_room_error_message" = "Túto miestnosť nemôžeme nájsť. Uistite sa, že existuje"; +"room_creation_error_invite_user_by_email_without_identity_server" = "Nie je nakonfigurovaný žiadny server totožnosti, takže nemôžete pridať účastníka s e-mailom."; +"room_creation_appearance_picture" = "Obrázok konverzácie (nepovinné)"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Môžete si prečítať všetky naše podmienky %@."; +"analytics_prompt_terms_link_new_user" = "tu"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Prečítajte si všetky naše podmienky %@. Je to v poriadku?"; +"analytics_prompt_terms_link_upgrade" = "tu"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Nezaznamenávame ani neprofilujeme žiadne údaje o účte"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Nezdieľame informácie s tretími stranami"; +"analytics_prompt_point_3" = "Túto funkciu môžete kedykoľvek vypnúť v nastaveniach"; +"analytics_prompt_not_now" = "Teraz nie"; +"room_message_reply_to_placeholder" = "Odoslať odpoveď (nešifrovanú)…"; + + +// Room Details +"room_details_title" = "Podrobnosti o miestnosti"; +"settings_sending_media" = "ODOSIELANIE OBRÁZKOV A VIDEÍ"; +"settings_config_user_id" = "Prihlásený ako %@"; +"settings_mark_all_as_read" = "Označiť všetky správy ako prečítané"; +"settings_config_no_build_info" = "Žiadne informácie o zostavení"; +"room_preview_try_join_an_unknown_room" = "Pokúšate sa zobraziť %s. Chcete vstúpiť a pridať sa k diskusii?"; +"room_preview_unlinked_email_warning" = "Toto pozvanie bolo odoslané na emailovú adresu %s, ktorá nie je priradená k tomuto účtu. Môžete sa prihlásiť k inému účtu, alebo pridať túto emailovú adresu do vášho účtu."; +"room_preview_subtitle" = "Toto je náhľad do miestnosti. Všetky akcie pre túto miestnosť sú zakázané."; + +// Room Preview +"room_preview_invitation_format" = "Do tejto miestnosti vás pozval používateľ %@"; +"unknown_devices_answer_anyway" = "Odpovedať aj tak"; +"unknown_devices_alert" = "Táto miestnosť obsahuje neznáme relácie, ktoré neboli overené.\nTo znamená, že nie je zaručené, že relácie patria naozaj tým používateľom, o ktorých to tvrdia.\nPred pokračovaním odporúčame prejsť procesom overenia každej relácie, ale ak chcete, môžete správu poslať znova bez overenia."; + +// Unknown devices +"unknown_devices_alert_title" = "V miestnosti sú neznáme relácie"; +"external_link_confirmation_message" = "Odkaz %@ vás presmeruje na inú stránku: %@\n\nSte si istí, že chcete pokračovať?"; +"room_no_privileges_to_create_group_call" = "Ak chcete začať hovor, musíte byť administrátorom alebo moderátorom."; +"room_open_dialpad" = "Číselník"; +"room_message_edits_history_title" = "Úpravy správy"; +"room_resource_usage_limit_reached_message_2" = "niektorí používatelia sa nebudú môcť prihlásiť."; +"room_resource_usage_limit_reached_message_1_monthly_active_user" = "Tento domovský server dosiahol svoj mesačný limit aktívnych používateľov, takže "; +"room_resource_usage_limit_reached_message_1_default" = "Bol prekročený limit využitia prostriedkov pre tento domovský server, takže "; +"room_resource_limit_exceeded_message_contact_2_link" = "kontaktovať vášho správcu služieb"; +"room_predecessor_link" = "Kliknutím sem zobrazíte staršie správy."; +"room_predecessor_information" = "Táto miestnosť je pokračovaním inej konverzácie."; +"room_replacement_link" = "Konverzácia pokračuje tu."; +"room_replacement_information" = "Táto miestnosť bola nahradená a nie je viac aktívna."; +"room_action_send_photo_or_video" = "Odoslať fotografiu alebo video"; +"room_action_camera" = "Spraviť fotografiu alebo video"; +"room_warning_about_encryption" = "End-to-end šifrovanie je vo fáze beta a nemusí byť spoľahlivé.\n\nNa zabezpečenie údajov by ste mu zatiaľ nemali dôverovať.\n\nZariadenia zatiaľ nebudú schopné dešifrovať históriu z obdobia pred ich pripojením do miestnosti.\n\nŠifrované správy nebudú viditeľné v klientoch, ktoré ešte nemajú zavedené šifrovanie."; +"room_event_action_reaction_history" = "História reakcií"; +"room_event_action_view_encryption" = "Informácie o šifrovaní"; +"room_event_action_delete_confirmation_message" = "Určite chcete túto neodoslanú správu vymazať?"; +"room_event_action_report_prompt_ignore_user" = "Chcete skryť všetky správy od tohto používateľa?"; +"room_event_action_ban_prompt_reason" = "Dôvod zákazu tohto používateľa"; +"room_event_action_kick_prompt_reason" = "Dôvod vylúčenia tohto používateľa"; +"room_event_action_report_prompt_reason" = "Dôvod nahlásenia tohto obsahu"; +"room_conference_call_no_power" = "Potrebujete povolenie na správu konferenčného hovoru v tejto miestnosti"; +"room_unsent_messages_cancel_message" = "Určite chcete vymazať všetky neodoslané správy v tejto miestnosti?"; +"room_unsent_messages_notification" = "Správy sa nepodarilo odoslať."; +"room_offline_notification" = "Spojenie so serverom bolo prerušené."; +"encrypted_room_message_reply_to_placeholder" = "Odoslať šifrovanú odpoveď…"; +"encrypted_room_message_placeholder" = "Odoslať šifrovanú správu…"; +"room_do_not_have_permission_to_post" = "Nemáte povolenie písať do tejto miestnosti"; +"room_message_replying_to" = "Odpoveď na %@"; +"room_message_unable_open_link_error_message" = "Nie je možné otvoriť odkaz."; +"room_message_placeholder" = "Odoslať správu (nešifrovane)…"; +"room_many_users_are_typing" = "%@, %@ a ďalší píšu…"; +"room_two_users_are_typing" = "%@ a %@ píšu…"; +"room_accessiblity_scroll_to_bottom" = "Prejsť na koniec"; + +// Chat +"room_slide_to_end_group_call" = "Posuňte na ukončenie výzvy pre všetkých"; +"room_participants_action_set_default_power_level" = "Obnoviť na normálnu úroveň používateľa"; +"room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Nie je nastavený žiadny server totožností, takže nemôžete začať konverzáciu s kontaktom pomocou e-mailu."; +"room_participants_invite_malformed_id_title" = "Chyba pozvania"; +"room_participants_invite_another_user" = "Vyhľadať / pozvať podľa ID používateľa, mena alebo e-mailu"; +"find_your_contacts_identity_service_error" = "Nie je možné sa pripojiť k serveru totožností."; +"find_your_contacts_footer" = "Túto funkciu môžete kedykoľvek vypnúť v nastaveniach."; +"contacts_user_directory_section" = "POUŽÍVATEĽSKÝ ADRESÁR"; +"contacts_user_directory_offline_section" = "POUŽÍVATEĽSKÝ ADRESÁR (offline)"; +"contacts_address_book_permission_denied_alert_message" = "Ak chcete povoliť kontakty, prejdite do nastavení zariadenia."; +"contacts_address_book_permission_denied_alert_title" = "Kontakty sú vypnuté"; +"contacts_address_book_permission_required" = "Povolenie potrebné na prístup k lokálnym kontaktom"; +"directory_search_results_more_than" = ">%tu nájdených výsledkov pre %@"; +"directory_search_results" = "%tu nájdených výsledkov pre %@"; +"search_people_placeholder" = "Vyhľadať podľa ID používateľa, mena alebo e-mailu"; +"rooms_empty_view_information" = "Miestnosti sú skvelé na akýkoľvek skupinový rozhovor, súkromný alebo verejný. Ťuknutím na tlačidlo + vyhľadajte existujúce miestnosti alebo vytvorte nové."; +"people_empty_view_information" = "Bezpečne komunikujte s kýmkoľvek.Ťuknutím na tlačidlo + začnite pridávať ľudí."; +"room_creation_wait_for_creation" = "Miestnosť sa už vytvára. Počkajte, prosím."; +"room_creation_make_public_prompt_msg" = "Ste si istí, že chcete túto konverzáciu zverejniť? Ktokoľvek si môže prečítať vaše správy a pripojiť sa ku konverzácii."; +"social_login_button_title_sign_in" = "Prihlásiť sa s %@"; +"auth_reset_password_error_is_required" = "Nie je nastavený žiadny server identity: pridajte ho v možnostiach servera a obnovte heslo."; +"security_settings_crosssigning_info_trusted" = "Krížové podpisovanie je povolené. Na základe krížového podpisovania môžete dôverovať iným používateľom a ostatným reláciám, ale nemôžete krížovo podpisovať z tejto relácie, pretože nemá súkromné kľúče na krížové podpisovanie. Dokončite zabezpečenie tejto relácie."; +"manage_session_sign_out" = "Odhlásiť sa z tejto relácie"; +"security_settings_crosssigning_info_ok" = "Krížové podpisovanie je pripravené na použitie."; +"settings_discovery_error_message" = "Vyskytla sa chyba. Prosím, skúste to znova."; +"settings_crypto_blacklist_unverified_devices" = "Šifrovať len overeným reláciám"; +"settings_password_updated" = "Vaše heslo bolo aktualizované"; +"settings_notifications_disabled_alert_message" = "Ak chcete povoliť upozornenia, prejdite do nastavení zariadenia."; +"settings_enable_push_notif" = "Oznámenia na tomto zariadení"; +"auth_autodiscover_invalid_response" = "Neplatná odpoveď pri zisťovaní domovského servera"; +"auth_accept_policies" = "Prosím prečítajte si a odsúhlaste zmluvné podmienky tohoto domovského servera:"; +"enable" = "Povoliť"; +// String for App Store +"store_short_description" = "Bezpečné decentralizované konverzácie/VoIP"; +"key_backup_setup_passphrase_confirm_passphrase_invalid" = "fráza sa nezhoduje"; +"key_backup_setup_intro_manual_export_action" = "Exportovať kľúče manuálne"; +"key_backup_setup_skip_alert_title" = "Ste si istí?"; + + +// Cancel + +"secure_key_backup_setup_cancel_alert_title" = "Ste si istí?"; +"deactivate_account_informations_part3" = "\n\nDeaktivácia vášho konta "; + +// Room key request dialog +"e2e_room_key_request_title" = "Žiadosť o šifrovací kľúč"; +"room_widget_permission_user_id_permission" = "Vaše ID používateľa"; +"room_widget_permission_avatar_url_permission" = "URL adresa vášho obrázku"; +"room_widget_permission_display_name_permission" = "Vaše zobrazované meno"; +"widget_menu_open_outside" = "Otvoriť v prehliadači"; +"bug_report_background_mode" = "Pokračovať v pozadí"; +"e2e_key_backup_wrong_version_button_wasme" = "Bol(a) som to ja"; +"call_no_stun_server_error_use_fallback_button" = "Skúste použiť %s"; +"call_incoming_video" = "Prichádzajúci video hovor…"; +"event_formatter_group_call_incoming" = "%@ v %@"; +"event_formatter_call_active_video" = "Aktívny video hovor"; +"event_formatter_call_active_voice" = "Aktívny hovor"; +"event_formatter_call_incoming_video" = "Prichádzajúci video hovor"; +"event_formatter_call_incoming_voice" = "Prichádzajúci hovor"; +"image_picker_action_library" = "Vybrať z knižnice"; + +// Read Receipts +"read_receipts_list" = "Zoznam potvrdení o prečítaní"; + +// Group rooms +"group_rooms_filter_rooms" = "Filtrovať komunitné miestnosti"; +"group_participants_filter_members" = "Filtrovať členov komunity"; +"room_details_copy_room_url" = "Kopírovať URL adresu miestnosti"; +"room_details_copy_room_address" = "Kopírovať adresu miestnosti"; +"room_details_copy_room_id" = "Kopírovať ID miestnosti"; +"room_details_addresses_invalid_address_prompt_title" = "Nesprávny formát aliasu"; +"room_details_new_address" = "Pridať novú adresu"; +"identity_server_settings_alert_disconnect_title" = "Odpojiť server totožností"; +"identity_server_settings_alert_change_title" = "Zmeniť server totožností"; +"security_settings_export_keys_manually" = "Exportovať kľúče manuálne"; +"security_settings_secure_backup_restore" = "Obnoviť zo zálohy"; +"settings_discovery_three_pid_details_cancel_email_validation_action" = "Zrušiť overenie e-mailu"; +"settings_discovery_three_pid_details_title_phone_number" = "Spravovať telefónne číslo"; +"settings_key_backup_button_restore" = "Obnoviť zo zálohy"; +"settings_deactivate_my_account" = "Deaktivovať účet"; +"settings_add_3pid_password_title_msidsn" = "Pridať telefónne číslo"; +"settings_add_3pid_password_title_email" = "Pridať emailovú adresu"; +"settings_term_conditions" = "Podmienky používania"; +"settings_olm_version" = "Olm verzia %@"; +"settings_contacts_enable_sync" = "Nájdite svoje kontakty"; +"settings_show_url_previews" = "Zobraziť náhľad webovej stránky"; +"settings_ui_theme_picker_title" = "Vybrať vzhľad"; +"settings_new_keyword" = "Pridať nové kľúčové slovo"; +"settings_messages_containing_display_name" = "Moje zobrazované meno"; +"settings_encrypted_group_messages" = "Šifrované skupinové správy"; +"settings_encrypted_direct_messages" = "Šifrované priame správy"; +"settings_mentions_and_keywords" = "Zmienky a kľúčové slová"; +"settings_show_decrypted_content" = "Zobraziť dešifrovaný obsah"; +"settings_add_phone_number" = "Pridať telefónne číslo"; +"settings_add_email_address" = "Pridať emailovú adresu"; +"settings_sign_out_confirmation" = "Ste si istí?"; +"settings_config_home_server" = "Domovský server je %@"; +"account_logout_all" = "Odhlásiť sa zo všetkých účtov"; +"room_title_one_active_member" = "%@/%@ aktívny člen"; +"room_title_multiple_active_members" = "%@/%@ aktívnych členov"; +"room_multiple_typing_notification" = "%@ a ďalší"; +"room_event_failed_to_send" = "Nepodarilo sa odoslať"; +"room_event_action_delete_confirmation_title" = "Vymazať neodoslanú správu"; +"room_event_action_view_decrypted_source" = "Zobraziť dešifrovaný zdroj"; +"room_delete_unsent_messages" = "Vymazať neodoslané správy"; +"room_resend_unsent_messages" = "Znovu odoslať neodoslané správy"; +"room_unsent_messages_cancel_title" = "Vymazať neodoslané správy"; +"room_message_reply_to_short_placeholder" = "Odoslať odpoveď…"; +"room_message_short_placeholder" = "Odoslať správu…"; +"room_one_user_is_typing" = "%s píše…"; +"room_new_messages_notification" = "%d nových správ"; +"room_new_message_notification" = "%d nová správa"; +"room_jump_to_first_unread" = "Preskočiť na neprečítanú"; +"find_your_contacts_button_title" = "Nájdite svoje kontakty"; +"social_login_button_title_continue" = "Pokračovať s %@"; +"auth_softlogout_clear_data_button" = "Vymazať všetky údaje"; +"auth_softlogout_clear_data" = "Vymazať osobné údaje"; +"auth_softlogout_signed_out" = "Ste odhlásený"; +"send_to" = "Odoslať do %@"; +"poll_timeline_one_vote" = "1 hlas"; +"room_event_action_end_poll" = "Ukončiť anketu"; +"room_event_action_remove_poll" = "Odstrániť anketu"; +"poll_edit_form_add_option" = "Pridať možnosť"; +"poll_edit_form_option_number" = "Možnosť %lu"; +"poll_edit_form_create_options" = "Vytvoriť možnosti"; +"poll_edit_form_input_placeholder" = "Napíšte niečo"; + +// Mark: - Polls + +"poll_edit_form_create_poll" = "Vytvoriť anketu"; +"version_check_modal_action_title_supported" = "Rozumiem"; +"voice_message_lock_screen_placeholder" = "Hlasová správa"; +"voice_message_remaining_recording_time" = "%1$s ostáva"; +"side_menu_app_version" = "Verzia %s"; +"side_menu_action_invite_friends" = "Pozvať priateľov"; + +// Mark: - Side menu + +"side_menu_reveal_action_accessibility_label" = "Ľavý panel"; +"space_public_join_rule" = "Verejný priestor"; +"space_private_join_rule" = "Súkromný priestor"; +"spaces_coming_soon_title" = "Už čoskoro"; +"spaces_explore_rooms" = "Preskúmať miestnosti"; +"leave_space_title" = "Opustiť %@"; +"room_intro_cell_information_room_with_topic_sentence2" = "Téma: %@"; + +// Mark: - Room creation introduction cell + +"room_intro_cell_add_participants_action" = "Pridať ľudí"; +"call_transfer_dialpad" = "Číselník"; + +// MARK: - Dial Pad +"dialpad_title" = "Číselník"; +"room_info_list_several_members" = "%@ členovia"; + +// MARK: - Room Info + +"room_info_list_one_member" = "1 člen"; +"create_room_section_header_address" = "Adresa miestnosti"; +"create_room_type_public" = "Verejná miestnosť"; +"create_room_type_private" = "Súkromná miestnosť"; +"create_room_section_header_type" = "Typ miestnosti"; +"create_room_enable_encryption" = "Povoliť šifrovanie"; +"create_room_section_header_encryption" = "Šifrovanie miestnosti"; +"create_room_section_header_name" = "Názov miestnosti"; + +// MARK: - Create Room + +"create_room_title" = "Nová miestnosť"; +"searchable_directory_x_network" = "%@ Sieť"; +"biometrics_desetup_disable_button_title_x" = "Vypnúť %@"; +"biometrics_desetup_title_x" = "Vypnúť %@"; +"biometrics_setup_enable_button_title_x" = "Povoliť %@"; +"biometrics_setup_title_x" = "Povoliť %@"; +"biometrics_settings_enable_x" = "Povoliť %@"; +"pin_protection_settings_change_pin" = "Zmeniť PIN"; +"pin_protection_settings_enable_pin" = "Zapnúť PIN"; + +// MARK: - PIN Protection + +"pin_protection_choose_pin_welcome_after_login" = "Vitajte späť."; +"major_update_done_action" = "Rozumiem"; +"major_update_learn_more_action" = "Zistiť viac"; + +// MARK: - Secrets reset + +"secrets_reset_title" = "Obnoviť všetko"; +"secrets_setup_recovery_passphrase_confirm_passphrase_placeholder" = "Potvrdiť frázu"; +"secrets_recovery_with_key_recover_action" = "Použiť kľúč"; + +// Recover with key + +"secrets_recovery_with_key_title" = "Bezpečnostný kľúč"; +"secrets_recovery_with_passphrase_recover_action" = "Použiť frázu"; + +// Recover with passphrase + +"secrets_recovery_with_passphrase_title" = "Bezpečnostná fráza"; +"secrets_recovery_reset_action_part_2" = "Obnoviť všetko"; +"user_verification_session_details_verify_action_other_user" = "Manuálne overiť"; +"user_verification_session_details_verify_action_current_user" = "Interaktívne overiť"; +"user_verification_session_details_untrusted_title" = "Nedôveryhodné"; +"user_verification_sessions_list_session_untrusted" = "Nedôveryhodné"; + +// MARK: - User verification + +// Start + +"user_verification_start_verify_action" = "Spustiť overenie"; + +// Scanned +"key_verification_scan_confirmation_scanned_title" = "Už to skoro bude!"; +"key_verification_verify_qr_code_scan_other_code_success_title" = "Kód overený!"; +"key_verification_verify_qr_code_cannot_scan_action" = "Nemôžete naskenovať?"; +"key_verification_tile_request_status_accepted" = "Prijali ste"; +"key_verification_tile_request_status_cancelled" = "%@ bolo zrušené"; +"key_verification_tile_request_status_cancelled_by_me" = "Zrušili ste overenie"; +"key_verification_tile_request_status_data_loading" = "Načítavanie údajov…"; +"key_verification_tile_request_outgoing_title" = "Overenie odoslané"; + +// Tiles + +"key_verification_tile_request_incoming_title" = "Žiadosť o overenie"; + +// MARK: File upload +"file_upload_error_title" = "Nahrávanie súboru"; +"device_verification_emoji_light bulb" = "Žiarovka"; +"device_verification_verified_got_it_button" = "Rozumiem"; +"key_verification_manually_verify_device_key_title" = "Kľúč relácie"; +"key_verification_manually_verify_device_id_title" = "ID relácie"; +"key_verification_manually_verify_device_name_title" = "Názov relácie"; +"key_verification_verify_sas_validate_action" = "Zhodujú sa"; + +// MARK: Verify + +"key_verification_verify_sas_title_emoji" = "Porovnať emoji"; +"device_verification_self_verify_start_verify_action" = "Spustiť overovanie"; From 12614408b66c0673a6c15275476166d67089ebcc Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Sun, 19 Dec 2021 13:26:24 +0000 Subject: [PATCH 056/109] Translated using Weblate (French) Currently translated at 100.0% (6 of 6 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/fr/ --- Riot/Assets/fr.lproj/InfoPlist.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/fr.lproj/InfoPlist.strings b/Riot/Assets/fr.lproj/InfoPlist.strings index 464aeac9e9..38d044ce17 100644 --- a/Riot/Assets/fr.lproj/InfoPlist.strings +++ b/Riot/Assets/fr.lproj/InfoPlist.strings @@ -2,6 +2,6 @@ "NSCameraUsageDescription" = "L’appareil photo est utilisé pour prendre des photos, des vidéos et pour passer des appels vidéo."; "NSPhotoLibraryUsageDescription" = "La photothèque est utilisée pour envoyer des photos et des vidéos."; "NSMicrophoneUsageDescription" = "Element doit avoir accès au microphone pour passer des appels, capturer des vidéos et enregistrer des messages vocaux."; -"NSContactsUsageDescription" = "Pour découvrir vos contacts qui utilisent déjà Matrix, Element peut envoyer les adresses e-mail et les numéros de téléphone de votre carnet d’adresse à votre serveur d’identité Matrix. Si votre serveur d’identité le prend en charge, les données personnelles sont hachées avant l’envoi − vérifiez sa politique de confidentialité pour plus de détails."; +"NSContactsUsageDescription" = "Element affichera vos contacts pour que vous puissiez les inviter à parler."; "NSCalendarsUsageDescription" = "Voir vos rendez-vous dans l’application."; "NSFaceIDUsageDescription" = "Face ID est utilisé pour accéder à votre application."; From 8e81a01641dd8957df280373d8bd51fa2780e55d Mon Sep 17 00:00:00 2001 From: Denys Nykula Date: Mon, 20 Dec 2021 16:30:17 +0000 Subject: [PATCH 057/109] Translated using Weblate (Ukrainian) Currently translated at 99.3% (1366 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 121 +++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index ed0a38feb9..f7fef55af9 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -1263,7 +1263,7 @@ // Scanning "key_verification_scan_confirmation_scanning_title" = "Майже все! Чекаємо на підтвердження…"; -"key_verification_scan_confirmation_scanning_user_waiting_other" = "Очікування на %s…"; +"key_verification_scan_confirmation_scanning_user_waiting_other" = "Очікування на %@…"; "key_verification_scan_confirmation_scanning_device_waiting_other" = "Очікування іншого пристрою…"; // Scanned @@ -1572,3 +1572,122 @@ // Widget "widget_no_integrations_server_configured" = "Сервер інтеграцій не налаштовано"; +"bug_report_description" = "Опишіть ваду. Що ви робили? Виконання якої дії очікували? Що сталося натомість?"; +"bug_crash_report_title" = "Звіт про збій"; +"bug_crash_report_description" = "Будь ласка, опишіть свої дії перед збоєм:"; +"bug_report_logs_description" = "Задля діагностики, журнали клієнта будуть надіслані разом зі звітом про ваду. Якщо бажаєте надіслати лише текст угорі, зніміть галочку:"; +"bug_report_send_logs" = "Надіслати журнали"; +"group_participants_invite_another_user" = "Знайти чи запросити за ID користувача чи іменем"; +"group_participants_invite_malformed_id" = "Хибний ID. Треба Matrix ID вигляду «@localpart:domain»"; + + +// MARK: Key backup setup + +"key_backup_setup_title" = "Резервне копіювання ключів"; +"key_backup_setup_skip_alert_message" = "Ви втратите захищені повідомлення, якщо вийдете чи загубите пристрій."; + +// Intro + +"key_backup_setup_intro_title" = "Ніколи не втрачайте зашифровані повідомлення"; +"key_backup_setup_intro_info" = "Повідомлення в зашифрованих кімнатах захищені наскрізним шифруванням. Тільки ви та одержувачі маєте ключі для читання цих повідомлень.\n\nСтворіть захищену резервну копію ключів, щоб їх не втратити."; +"key_backup_setup_passphrase_info" = "Ми збережемо зашифровану копію ваших ключів на нашому сервері. Захистіть свою резервну копію парольною фразою.\n\nДля максимальної безпеки фраза повинна відрізнятися від пароля вашого облікового запису."; +"key_backup_setup_passphrase_set_passphrase_action" = "Встановити фразу"; + +// MARK: Key backup recover + +"key_backup_recover_title" = "Захищені повідомлення"; + +// Recover from passphrase + +"key_backup_recover_from_passphrase_info" = "Використайте фразу безпеки, щоб розблокувати історію зашифрованих повідомлень"; + +// Recover from recovery key + +"key_backup_recover_from_recovery_key_info" = "Використайте ключ безпеки, щоб розблокувати історію зашифрованих повідомлень"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Забули свою фразу безпеки? Можете "; +"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "використати ключ безпеки"; +"key_backup_recover_from_recovery_key_recovery_key_placeholder" = "Введіть ключ безпеки"; +"key_backup_recover_from_recovery_key_recover_action" = "Розблокувати історію"; +"key_backup_recover_from_recovery_key_lost_recovery_key_action" = "Втратили ключ безпеки? Встановіть новий у налаштуваннях."; + +// Success + +"key_backup_recover_success_info" = "Резервну копію відновлено!"; +"key_backup_recover_done_action" = "Готово"; +"key_backup_setup_intro_setup_action_without_existing_backup" = "Налаштувати резервне копіювання ключів"; + +// Passphrase + +"key_backup_setup_passphrase_title" = "Захистіть резервну копію фразою безпеки"; +"key_backup_setup_passphrase_setup_recovery_key_info" = "Або захистіть резервну копію ключем безпеки, який зберігатимете в надійному місці."; +"key_backup_setup_passphrase_setup_recovery_key_action" = "(Додатково) Налаштувати ключ безпеки"; + +// Success + +"key_backup_setup_success_title" = "Успіх!"; + +// Success from passphrase +"key_backup_setup_success_from_passphrase_info" = "Триває резервне копіювання ваших ключів.\n\nКлюч безпеки підстраховує вас: можете використати його для відновлення доступу до ваших зашифрованих повідомлень, якщо забудете парольну фразу.\n\nТримайте відновлювальний ключ у якомусь дуже надійному місці, наприклад у менеджері паролів (або сейфі)."; +"key_backup_setup_success_from_passphrase_save_recovery_key_action" = "Зберегти ключ безпеки"; + +// Success from recovery key +"key_backup_setup_success_from_recovery_key_info" = "Триває резервне копіювання ваших ключів.\n\nСкопіюйте цей ключ безпеки й надійно його зберігайте."; +"key_backup_setup_success_from_recovery_key_recovery_key_title" = "Ключ безпеки"; +"key_backup_setup_success_from_recovery_key_make_copy_action" = "Зробити копію"; +"key_backup_setup_success_from_recovery_key_made_copy_action" = "Вже маю копію"; +"key_backup_recover_invalid_passphrase_title" = "Хибна фраза безпеки"; +"key_backup_recover_invalid_passphrase" = "Не вдається розшифрувати резервну копію цією фразою: переконайтеся, що вводите правильну фразу безпеки."; +"key_backup_recover_invalid_recovery_key_title" = "Хибний ключ безпеки"; +"key_backup_recover_invalid_recovery_key" = "Не вдалося розшифрувати резервну копію цим ключем: переконайтеся, що вводите правильний ключ безпеки."; + +// MARK: Sign out warning + +"sign_out_existing_key_backup_alert_title" = "Точно вийти?"; +"sign_out_existing_key_backup_alert_sign_out_action" = "Вийти"; +"sign_out_non_existing_key_backup_alert_title" = "Якщо вийти зараз, ви втратите свої зашифровані повідомлення"; +"sign_out_non_existing_key_backup_alert_discard_key_backup_action" = "Мені не потрібні мої зашифровані повідомлення"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_title" = "Ви втратите доступ до ваших зашифрованих повідомлень"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_message" = "Ви втратите доступ до своїх зашифрованих повідомлень, якщо не зробите резервну копію ключів перед виходом з системи."; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_sign_out_action" = "Вийти"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_backup_action" = "Резервна копія"; +"sign_out_key_backup_in_progress_alert_title" = "Триває резервне копіювання ключів. Якщо вийти зараз, ви втратите доступ до своїх зашифрованих повідомлень."; +"sign_out_key_backup_in_progress_alert_discard_key_backup_action" = "Мені не потрібні мої зашифровані повідомлення"; +"sign_out_key_backup_in_progress_alert_cancel_action" = "Почекаю"; +"key_backup_setup_intro_manual_export_info" = "(Розширені)"; +"key_backup_setup_intro_manual_export_action" = "Експорт ключів вручну"; +"key_backup_setup_intro_setup_connect_action_with_existing_backup" = "Налаштувати цьому пристрою резервне копіювання ключів"; +"device_verification_cancelled" = "Інша сторона скасувала звірку."; +"device_verification_cancelled_by_me" = "Звірку скасовано. Причина: %@"; +"device_verification_error_cannot_load_device" = "Не вдалося отримати дані сеансу."; + +// Mark: Incoming +"device_verification_incoming_title" = "Надійшов запит на звірку"; +"device_verification_incoming_description_1" = "Звірте цей сеанс, щоб позначити його довіреним. Довірені сеанси партнерів дають змогу впевненіше користуватися наскрізним шифруванням повідомлень."; +"device_verification_incoming_description_2" = "Звірка цього сеансу позначить його довіреним для вас, а ваш довіреним для партнера."; + +// MARK: Start +"device_verification_start_title" = "Звірити, порівнявши короткий текстовий рядок"; +"device_verification_start_wait_partner" = "Очікування підтвердження партнером…"; +"device_verification_start_use_legacy" = "Нічого не з'являється? Ще не всі клієнти підтримують інтерактивну звірку. Звірте по-старому."; +"device_verification_start_verify_button" = "Почати звірку"; +"device_verification_start_use_legacy_action" = "Звірити по-старому"; +"key_verification_tile_request_status_expired" = "Термін сплив"; +"key_verification_user_title" = "Звірте їх"; +"user_verification_start_information_part2" = " порівнявши одноразовий код на обох ваших пристроях."; +"user_verification_start_information_part1" = "Звірте безпечніше, "; + +// MARK: - User verification + +// Start + +"user_verification_start_verify_action" = "Почати звірку"; +"user_verification_start_waiting_partner" = "Очікування на %@…"; +"user_verification_start_additional_information" = "Для безпеки зробіть це особисто або скористайтеся іншим способом зв'язку."; + +// Sessions list + +"user_verification_sessions_list_user_trust_level_trusted_title" = "Довіряєте"; +"user_verification_sessions_list_information" = "Листування з цим користувачем у цій кімнаті наскрізно зашифроване й непрочитне для сторонніх."; +"user_verification_session_details_information_untrusted_current_user" = "Звірте цей сеанс, щоб позначити його довіреним і надати йому доступ до зашифрованих повідомлень:"; +"secrets_setup_recovery_key_storage_alert_message" = "✓ Надрукуйте його й зберігайте в надійному місці\n✓ Збережіть його на USB-ключ або носій резервного копіювання\n✓ Скопіюйте його до вашого особистого хмарного сховища"; +"call_transfer_error_message" = "Не вдалося переадресувати виклик"; From 2ec2a344844c5f402b56ded148ff3414537f803a Mon Sep 17 00:00:00 2001 From: libexus Date: Fri, 17 Dec 2021 15:45:10 +0000 Subject: [PATCH 058/109] Translated using Weblate (German) Currently translated at 99.5% (439 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/de/ --- .../MatrixKitAssets.bundle/de.lproj/MatrixKit.strings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings index 05095f81df..6f858ab0dc 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings @@ -497,3 +497,9 @@ "attachment_medium_with_resolution" = "Mittel %@ (~%@)"; "attachment_small_with_resolution" = "Klein %@ (~%@)"; "attachment_size_prompt_message" = "Dies kannst du in den Einstellungen abschalten."; +"auth_reset_password_error_not_found" = "Nicht gefunden"; +"auth_username_in_use" = "Benutzername bereits verwendet"; +"auth_invalid_user_name" = "Ungültiger Benutzername"; +"rename" = "Umbenennen"; +"attachment_unsupported_preview_message" = "Dieser Dateityp wird nicht unterstützt."; +"attachment_unsupported_preview_title" = "Vorschau kann nicht angezeigt werden"; From a33a62bb4718ddc95a5eedc75240bac37f7e6de0 Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Sun, 19 Dec 2021 13:27:30 +0000 Subject: [PATCH 059/109] Translated using Weblate (French) Currently translated at 99.5% (439 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/fr/ --- .../Assets/MatrixKitAssets.bundle/fr.lproj/MatrixKit.strings | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/fr.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/fr.lproj/MatrixKit.strings index 758c3a3cbe..bc1ae242a0 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/fr.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/fr.lproj/MatrixKit.strings @@ -473,3 +473,7 @@ "auth_username_in_use" = ""; "auth_invalid_user_name" = "Nom d’utilisateur invalide"; "rename" = "Renommer"; +"auth_reset_password_error_not_found" = "Non trouvé"; +"auth_reset_password_error_unauthorized" = "Non autorisé"; +"attachment_unsupported_preview_message" = "Ce type de fichier n’est pas pris en charge."; +"attachment_unsupported_preview_title" = "Prévisualisation impossible"; From 7f34ff13a3f29cb8452f751112c010f9167b2f8e Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Sun, 19 Dec 2021 19:29:50 +0000 Subject: [PATCH 060/109] Translated using Weblate (Swedish) Currently translated at 100.0% (441 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/sv/ --- .../MatrixKitAssets.bundle/sv.lproj/MatrixKit.strings | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sv.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sv.lproj/MatrixKit.strings index 317d368105..2dba03f74b 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sv.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sv.lproj/MatrixKit.strings @@ -470,3 +470,11 @@ "attachment_size_prompt_message" = "Du kan stänga av detta i inställningarna."; "attachment_size_prompt_title" = "Bekräfta storlek att skicka"; "room_displayname_all_other_participants_left" = "%@ (Kvar)"; +"auth_reset_password_error_not_found" = "Hittades inte"; +"auth_reset_password_error_unauthorized" = "Obehörig"; +"auth_username_in_use" = "Användarnamn upptaget"; +"auth_invalid_user_name" = "Ogiltigt användarnamn"; +"rename" = "Döp om"; +"attachment_unsupported_preview_message" = "Den här filtypen stöds inte."; +"attachment_unsupported_preview_title" = "Kunde inte förhandsgranska"; +"room_displayname_all_other_members_left" = "%@ (Kvar)"; From 04e35ca3426390b73f3866b1e6f85175a5ecac0d Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Fri, 17 Dec 2021 12:47:42 +0000 Subject: [PATCH 061/109] Translated using Weblate (Ukrainian) Currently translated at 100.0% (441 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/uk/ --- .../Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings index 3d08db3f7a..955e8fd1aa 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings @@ -554,7 +554,7 @@ // contacts list screen "invitation_message" = "Поговорімо в matrix? Сайт https://matrix.org описує, як це зробити."; -"local_contacts_access_discovery_warning" = "Щоб виявляти, які ваші контакти вже в Matrix, %@ може надсилати адреси е-пошти й номери телефонів із вашої адресної книги на обраний сервер ідентифікації Matrix. Особисті дані хешуються перед надсиланням, якщо це підтримується: перевірте політику приватності свого сервера ідентифікації, щоб дізнатися більше."; +"local_contacts_access_discovery_warning" = "Щоб виявляти, які ваші контакти вже в Matrix, %@ може надсилати адреси е-пошти й номери телефонів із вашої адресної книги на обраний сервер ідентифікації Matrix. Особисті дані хешуються перед надсиланням, якщо це підтримується: перегляньте політику приватності свого сервера ідентифікації, щоб дізнатися більше."; "local_contacts_access_not_granted" = "Для пошуку користувачів серед локальних контактів потрібен доступ до ваших контактів, але %@ не має такого дозволу"; "e2e_export_prompt" = "Це дає змогу експортувати в локальний файл ключі до повідомлень, отриманих вами в зашифрованих кімнатах. Тоді ви зможете імпортувати файл до іншого клієнта Matrix у майбутньому, і той клієнт також зможе розшифрувати ці повідомлення.\nЕкспортований файл дасть змогу всім, хто його прочитає, розшифрувати всі видимі вам зашифровані повідомлення."; "e2e_import_prompt" = "Це дає змогу імпортувати ключі шифрування, які ви раніше експортували з іншого клієнта Matrix. Тоді ви зможете розшифрувати всі повідомлення, які міг розшифрувати той клієнт.\nФайл експорту захищений парольною фразою. Введіть парольну фразу сюди, щоб розшифрувати файл."; From 86418c8cd6fd1806542375780feb50d1d9110af8 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Sat, 18 Dec 2021 22:56:50 +0000 Subject: [PATCH 062/109] Translated using Weblate (Slovak) Currently translated at 36.7% (162 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/sk/ --- .../sk.lproj/MatrixKit.strings | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings index 8b13789179..a60bf879d6 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings @@ -1 +1,240 @@ + +"login_error_login_email_not_yet" = "Zatiaľ ste neklikli na odkaz zaslaný emailom"; +"login_error_user_in_use" = "Toto používateľské meno sa už používa"; +"login_error_limit_exceeded" = "Bol prekročený maximálny počet požiadaviek"; +"room_left_for_dm" = "Odišli ste"; + +// Room creation +"room_creation_name_title" = "Názov miestnosti:"; +"account_msisdn_validation_title" = "Čaká sa na overenie"; +"account_email_validation_title" = "Čaká sa na overenie"; + +// Account +"account_save_changes" = "Uložiť zmeny"; +"room_event_encryption_verify_title" = "Overiť reláciu\n\n"; +"room_event_encryption_info_device_not_verified" = "Neoverené"; +"room_event_encryption_info_device_fingerprint" = "Ed25519 odtlačok prsta\n"; +"room_event_encryption_info_device_name" = "Verejný názov\n"; +"room_event_encryption_info_device_unknown" = "neznáma relácia\n"; +"room_event_encryption_info_event_decryption_error" = "Chyba dešifrovania\n"; +"room_event_encryption_info_event_session_id" = "ID relácie\n"; +"room_event_encryption_info_event_user_id" = "ID používateľa:\n"; +"room_event_encryption_info_event" = "Informácie o udalosti\n"; +"device_details_rename_prompt_title" = "Názov relácie"; +"device_details_last_seen" = "Naposledy videné\n"; +"device_details_name" = "Verejný názov\n"; + +// Devices +"device_details_title" = "Informácie o relácii\n"; +"room_displayname_all_other_members_left" = "%@ (Odišiel)"; + +// room display name +"room_displayname_empty_room" = "Prázdna miestnosť"; +"notice_invalid_attachment" = "neplatná príloha"; +"notice_file_attachment" = "súborová attachment"; +"notice_video_attachment" = "video príloha"; +"notice_audio_attachment" = "zvuková príloha"; +"notice_image_attachment" = "obrázková príloha"; +"notice_encrypted_message" = "Šifrovaná správa"; +"notice_room_created_for_dm" = "%@ sa pripojil/a."; +"end_call" = "Ukončiť hovor"; +"reject_call" = "Odmietnuť hovor"; +"show_details" = "Zobraziť podrobnosti"; +"cancel_download" = "Zrušiť sťahovanie"; +"cancel_upload" = "Zrušiť nahrávanie"; +"select_all" = "Vybrať všetko"; +"start_chat" = "Začať konverzáciu"; +"sign_up" = "Zaregistrovať sa"; +"login_error_resource_limit_exceeded_contact_button" = "Kontaktovať správcu"; +"register_error_title" = "Registrácia zlyhala"; +"login_invalid_param" = "Neplatný parameter"; +"login_error_bad_json" = "Chybné údaje vo formáte JSON"; +"login_error_forbidden" = "Neplatné používateľské meno/heslo"; + +// Room Screen + +// general errors + +// Home Screen + +// Last seen time + +// call events + +/* -*- + Automatic localization for en + + The following key/value pairs were extracted from the android i18n file: + /console/src/main/res/values/strings.xml. +*/ + + +// titles + +// button names +"ok" = "OK"; +"login_error_title" = "Prihlásenie zlyhalo"; +"login_prompt_email_token" = "Zadajte svoj e-mailový overovací token:"; +"login_email_info" = "Zadanie e-mailovej adresy umožní ostatným používateľom ľahšie vás nájsť v službe Matrix a umožní vám v budúcnosti obnoviť heslo."; +"login_display_name_placeholder" = "Zobrazované meno (napr. Fero Novák)"; +"login_user_id_placeholder" = "Matrix ID (napr. @fero:matrix.org alebo fero)"; +"login_identity_server_info" = "Matrix poskytuje servery totožnosti na sledovanie, ktoré e-maily atď. patria k jednotlivým Matrix ID. V súčasnosti existuje iba stránka https://matrix.org."; +"login_home_server_info" = "Váš domovský server ukladá všetky vaše konverzácie a údaje o účte"; +"ssl_fingerprint_hash" = "Odtlačok (%s):"; +"call_more_actions_dialpad" = "Číselník"; +"call_ended" = "Hovor ukončený"; + +// gcm section +"settings_config_home_server" = "Domovský server: %@"; +"notification_settings_custom_sound" = "Vlastný zvuk"; +"notification_settings_always_notify" = "Vždy oznamovať"; +"notification_settings_never_notify" = "Nikdy neoznamovať"; + +// members list Screen + +// accounts list Screen + +// image size selection + +// invitation members list Screen + +// room creation dialog Screen + +// room info dialog Screen + +// room details dialog screen +"room_details_title" = "Podrobnosti o miestnosti"; +"num_members_other" = "%@ používatelia"; +"num_members_one" = "%@ používateľ"; +"create_account" = "Vytvoriť účet"; +"create_room" = "Vytvoriť miestnosť"; +"notice_room_leave_by_you" = "Odišli ste"; +"notice_room_join_by_you" = "Vstúpili ste"; +"notice_room_leave" = "%@ odišiel/a"; +"notice_room_join" = "%@ sa pripojil/a"; +"language_picker_default_language" = "Predvolené (%@)"; +"user_id_placeholder" = "napr.: @fero:domovskyserver"; +"power_level" = "Úroveň právomoci"; + +// Others +"user_id_title" = "ID používateľa:"; +"e2e_passphrase_create" = "Vytvoriť heslo"; +"e2e_passphrase_confirm" = "Potvrďte heslo"; +"e2e_passphrase_enter" = "Zadajte heslo"; + +// Search +"search_no_results" = "Žiadne výsledky"; +"contact_local_contacts" = "Lokálne kontakty"; + +// Contacts +"contact_mx_users" = "Používatelia Matrix"; +"attachment_multiselection_original" = "Aktuálna veľkosť"; +"login_optional_field" = "voliteľné"; +"ssl_remain_offline" = "Ignorovať"; +"ssl_logout_account" = "Odhlásiť sa"; +"call_ringing" = "Vyzváňanie…"; + +// Settings keys + +// call string +"call_connecting" = "Pripájanie…"; +"notification_settings_sender_hint" = "@pouzivatel:domena.sk"; + +// Settings screen +"settings_title_config" = "Nastavenie"; +"unban" = "Zrušiť zákaz"; +"ban" = "Zakázať"; +"kick" = "Vylúčiť"; +"invite" = "Pozvať"; +"membership_ban" = "Vylúčený"; +"membership_invite" = "Pozvaní"; +"login" = "Prihlásiť sa"; + +// actions +"action_logout" = "Odhlásiť sa"; +"rename" = "Premenovať"; +"view" = "Zobraziť"; +"delete" = "Vymazať"; +"share" = "Zdieľať"; +"redact" = "Odstrániť"; +"resend" = "Odoslať znovu"; +"copy_button_name" = "Kopírovať"; +"send" = "Odoslať"; +"leave" = "Opustiť"; +"save" = "Uložiť"; +"cancel" = "Zrušiť"; +"private" = "Súkromné"; +"public" = "Verejné"; +"default" = "predvolené"; +"error" = "Chyba"; +"unsent" = "Neodoslané"; +"offline" = "nedostupný"; +"e2e_export" = "Exportovať"; +"e2e_import" = "Importovať"; +"format_time_d" = "d"; +"format_time_h" = "h"; +"format_time_m" = "m"; + +// Time +"format_time_s" = "s"; +"group_section" = "Skupiny"; + +// Groups +"group_invite_section" = "Pozvánky"; +"attachment_e2e_keys_import" = "Importovať..."; +"room_creation_participants_title" = "Účastníci:"; +"room_event_encryption_verify_ok" = "Overiť"; +"room_event_encryption_info_unverify" = "Zrušiť overenie"; +"room_event_encryption_info_verify" = "Overiť..."; +"room_event_encryption_info_device_blocked" = "Na čiernej listine"; +"room_event_encryption_info_device_verified" = "Overené"; +"room_event_encryption_info_device_verification" = "Overenie\n"; +"room_event_encryption_info_device_id" = "ID\n"; +"room_event_encryption_info_event_none" = "žiadne"; +"room_event_encryption_info_event_unencrypted" = "nezašifrované"; +"room_event_encryption_info_event_algorithm" = "Algoritmus\n"; +"device_details_delete_prompt_title" = "Overenie"; +"device_details_identifier" = "ID\n"; + +// Settings +"settings" = "Nastavenia"; +"notice_sticker" = "nálepka"; +"unignore" = "Prestať ignorovať"; +"ignore" = "Ignorovať"; +"mention" = "Zmieniť sa"; +"submit" = "Odoslať"; +"retry" = "Skúsiť znovu"; +"continue" = "Pokračovať"; +"close" = "Zavrieť"; +"back" = "Späť"; +"abort" = "Prerušiť"; +"yes" = "Áno"; + +// Action +"no" = "Nie"; +"login_desktop_device" = "Desktop"; +"login_tablet_device" = "Tablet"; +"login_mobile_device" = "Mobil"; +"login_leave_fallback" = "Zrušiť"; +"login_home_server_title" = "URL domovského servera:"; +"login_email_placeholder" = "Emailová adresa"; +"login_password_placeholder" = "Heslo"; +"login_identity_server_title" = "URL adresa servera totožností:"; +"login_server_url_placeholder" = "URL (napr. https://matrix.org)"; + +// Login Screen +"login_create_account" = "Vytvoriť účet:"; +/* *********************** */ +/* iOS specific */ +/* *********************** */ + +"matrix" = "Matrix"; +"notification_settings_select_room" = "Vybrať miestnosť"; +"notification_settings_enable_notifications_warning" = "Všetky oznámenia sú v súčasnosti vypnuté pre všetky zariadenia."; +"notification_settings_enable_notifications" = "Povoliť oznámenia"; + +// Notification settings screen +"notification_settings_disable_all" = "Zakázať všetky oznámenia"; +"settings_title_notifications" = "Oznámenia"; +"notification_settings_room_rule_title" = "Miestnosť: '%@'"; From 21ec12f653c659567f87562153494d953b1793db Mon Sep 17 00:00:00 2001 From: libexus Date: Tue, 21 Dec 2021 20:57:35 +0000 Subject: [PATCH 063/109] Translated using Weblate (German) Currently translated at 99.7% (440 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/de/ --- .../Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings index 6f858ab0dc..295ea32598 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings @@ -503,3 +503,4 @@ "rename" = "Umbenennen"; "attachment_unsupported_preview_message" = "Dieser Dateityp wird nicht unterstützt."; "attachment_unsupported_preview_title" = "Vorschau kann nicht angezeigt werden"; +"auth_reset_password_error_unauthorized" = "Nicht Authorisiert"; From 8eae88e015d7246b317e869ec118969d60525d57 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Wed, 22 Dec 2021 01:31:18 +0000 Subject: [PATCH 064/109] Translated using Weblate (Slovak) Currently translated at 37.4% (165 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/sk/ --- .../Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings index a60bf879d6..d121b125c0 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings @@ -238,3 +238,6 @@ "notification_settings_disable_all" = "Zakázať všetky oznámenia"; "settings_title_notifications" = "Oznámenia"; "notification_settings_room_rule_title" = "Miestnosť: '%@'"; +"auth_username_in_use" = "Používateľské meno sa už používa"; +"login_error_must_start_http" = "URL adresa musí začínať http[s]://"; +"auth_reset_password_error_not_found" = "Nenájdené"; From c6d32a39e57cf9c6038f6551a79616c735bbbb2f Mon Sep 17 00:00:00 2001 From: artevaeckt Date: Thu, 23 Dec 2021 08:10:06 +0000 Subject: [PATCH 065/109] Translated using Weblate (German) Currently translated at 99.4% (1367 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 33 ++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 457ac82d94..236dacc70a 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -114,10 +114,10 @@ "contacts_address_book_no_contact" = "Keine lokalen Kontakte"; "contacts_address_book_permission_required" = "Berechtigungen benötigt um auf lokale Kontakte zuzugreifen"; // Chat participants -"room_participants_title" = "Teilnehmer"; +"room_participants_title" = "Teilnehmende"; "room_participants_add_participant" = "Teilnehmer hinzufügen"; "room_participants_one_participant" = "1 Teilnehmer"; -"room_participants_multi_participants" = "%d Teilnehmer"; +"room_participants_multi_participants" = "%d Teilnehmende"; "room_participants_leave_prompt_title" = "Raum verlassen"; "room_participants_leave_prompt_msg" = "Bist du sicher, dass du den Raum verlassen willst?"; "room_participants_remove_prompt_title" = "Bestätigung"; @@ -140,7 +140,7 @@ "room_participants_action_ban" = "Aus diesem Raum bannen"; "room_participants_action_ignore" = "Alle Nachrichten von diesem Nutzer verbergen"; "room_participants_action_unignore" = "Zeige alle Nachrichten von diesem Nutzer"; -"room_participants_action_set_moderator" = "Mache zu Moderator"; +"room_participants_action_set_moderator" = "Gib Moderationsrechte"; "room_participants_action_set_admin" = "Mache zum Administrator"; "room_participants_action_start_new_chat" = "Starte neuen Chat"; "room_participants_action_start_video_call" = "Starte Video-Anruf"; @@ -547,8 +547,8 @@ "room_resource_limit_exceeded_message_contact_2_link" = "kontaktiere deinen Dienst-Administrator"; "room_resource_limit_exceeded_message_contact_3" = " um diesen Dienst weiter zu nutzen."; "homeserver_connection_lost" = "Konnte keine Verbindung zum Heimserver herstellen."; -"room_resource_usage_limit_reached_message_1_default" = "Dieser Heimserver hat einer seiner Ressourcengrenzen überschritten, sodass "; -"room_resource_usage_limit_reached_message_1_monthly_active_user" = "Dieser Heimserver hat seine Begrenzung an monatlich aktiven Benutzer überschritten, sodass "; +"room_resource_usage_limit_reached_message_1_default" = "Dieser Heimserver hat eine seiner Ressourcengrenzen überschritten, sodass "; +"room_resource_usage_limit_reached_message_1_monthly_active_user" = "Dieser Heimserver hat seine Begrenzung an monatlich aktiven Benutzern überschritten, sodass "; "room_resource_usage_limit_reached_message_2" = "einige Benutzer nicht in der Lage sein werden, sich anzumelden."; "room_resource_usage_limit_reached_message_contact_3" = " um diese Obergrenze erhöhen zu lassen."; "auth_accept_policies" = "Bitte Regeln dieses Heimservers ansehen und akzeptieren:"; @@ -1001,10 +1001,10 @@ "skip" = "Überspringen"; "security_settings_crosssigning_info_not_bootstrapped" = "Quersignierung ist bisher nicht konfiguriert."; "room_member_power_level_admin_in" = "Admin in %@"; -"room_member_power_level_moderator_in" = "Moderator in %@"; +"room_member_power_level_moderator_in" = "Moderationsrechte in %@"; "room_member_power_level_custom_in" = "Benutzerdefiniert (%@) in %@"; "room_member_power_level_short_admin" = "Admin"; -"room_member_power_level_short_moderator" = "Moderator"; +"room_member_power_level_short_moderator" = "Mod"; "room_member_power_level_short_custom" = "Benutzerdefiniert"; "security_settings_secure_backup" = "SICHERE SICHERHEITSKOPIE"; "security_settings_secure_backup_synchronise" = "Synchronisiere"; @@ -1019,7 +1019,7 @@ "store_promotional_text" = "Privatsphäre-wahrende Kollaborations-App in einem offenen Netzwerk. Dezentral, um dir die Kontrolle zu geben. Keine Datenerfassung, keine Hintertüren und kein Zugriff durch Dritte."; "room_participants_action_security_status_complete_security" = "Vollständige Sicherheit"; "external_link_confirmation_title" = "Überprüfe diesen Link genau"; -"external_link_confirmation_message" = "Der Link %@ braucht zu lange auf der anderen Seite: %@\n\nSicher, dass du fortfahren möchtest?"; +"external_link_confirmation_message" = "Der Link %@ bringt dich auf eine andere Seite: %@\n\nSicher, dass du fortfahren möchtest?"; "security_settings_crypto_sessions_description_2" = "Wenn du dich nicht angemeldet hast, ändere dein Passwort und setze die Sichere Sicherheitskopie zurück."; "security_settings_secure_backup_description" = "Sichere die Schlüssel, um Datenverlust zu verhindern. Sie werden mit einem Sicherungsschlüssel gesichert."; "security_settings_crosssigning_info_exists" = "Dein Konto hat eine Quersignatur-Identität, aber dieser Sitzung wird noch nicht vertraut. Vervollständige die Sicherheit auf diese Sitzung."; @@ -1349,7 +1349,7 @@ "event_formatter_call_ringing" = "Läuten…"; "event_formatter_call_connecting" = "Verbinden…"; "settings_labs_enable_ringing_for_group_calls" = "Bei Gruppenanrufen klingeln"; -"room_no_privileges_to_create_group_call" = "Du musst Admin oder Moderator sein, um einen Anruf zu starten."; +"room_no_privileges_to_create_group_call" = "Du musst Admin oder Mod sein, um einen Anruf zu starten."; "room_join_group_call" = "Beitreten"; // Chat @@ -1502,7 +1502,7 @@ "service_terms_modal_title_message" = "Zum Fortfahren musst du die Nutzungsbedingungen akzeptieren"; "settings_contacts_enable_sync_description" = "Dies verwendet deinen Identitätsserver um dich mit deinen Kontakten zu verbinden."; "settings_phone_contacts" = "KONTAKTE AM HANDY"; -"room_event_action_forward" = "Weiter"; +"room_event_action_forward" = "Weiterleiten"; "find_your_contacts_identity_service_error" = "Konnte keine Verbindung zum Identitätsserver aufbauen."; "find_your_contacts_button_title" = "Finde deine Kontakte"; "contacts_address_book_permission_denied_alert_message" = "Um Kontakte zu aktivieren, öffne die Einstellungen deines Gerätes."; @@ -1533,3 +1533,16 @@ "analytics_prompt_title" = "Hilf dabei %@ zu verbessern"; "settings_about" = "ÜBER"; "enable" = "Aktivieren"; +"analytics_prompt_message_upgrade" = "Du hast in der Vergangenheit bereits zugestimmt anonyme Nutzungsdaten mit uns zu teilen. Jetzt werden wir als Hilfe, um zu verstehen, wie Personen mehrere Geräte benutzen, eine zufällige Kennung generieren, die zwischen deinen Geräten geteilt wird."; +"analytics_prompt_message_new_user" = "Hilf uns dabei Probleme zu identifizieren und Element zu verbessern, indem du anonyme Nutzungsdaten teilst. Um zu verstehen, wie Personen mehrere Geräte benutzen, werden wir eine zufällige Kennung generieren, die zwischen deinen Geräten geteilt wird."; +"find_your_contacts_title" = "Starte mit der Auflistung deiner Kontakte"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Du kannst unsere gesamten Bedingungen %@ nachlesen."; +"poll_timeline_total_votes" = "%lu Stimmen abgegeben"; +"poll_timeline_total_one_vote" = "1 Stimme abgegeben"; +"poll_timeline_total_no_votes" = "Keine Stimmen abgegeben"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_edit_form_post_failure_action" = "OK"; +"poll_edit_form_poll_question_or_topic" = "Frage oder Thema der Umfrage"; +"poll_edit_form_input_placeholder" = "Schreib etwas"; From 433fb62cd21c224e439206e6d9ad9febb7d80c8c Mon Sep 17 00:00:00 2001 From: libexus Date: Tue, 21 Dec 2021 20:52:27 +0000 Subject: [PATCH 066/109] Translated using Weblate (German) Currently translated at 99.4% (1367 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 236dacc70a..3272240ddf 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -1546,3 +1546,18 @@ "poll_edit_form_post_failure_action" = "OK"; "poll_edit_form_poll_question_or_topic" = "Frage oder Thema der Umfrage"; "poll_edit_form_input_placeholder" = "Schreib etwas"; +"analytics_prompt_terms_link_upgrade" = "hier"; +"poll_timeline_not_closed_title" = "Fehler beim Beenden der Abstimmung"; +"poll_timeline_vote_not_registered_subtitle" = "Wir konnten deine Stimme leider nicht erfassen. Versuche es bitte erneut"; +"poll_timeline_total_final_results" = "Es wurden %lu Stimmen abgegeben"; +"poll_timeline_total_final_results_one_vote" = "Es wurde 1 Stimme abgegeben"; +"poll_timeline_total_votes_not_voted" = "%lu Stimmen abgegeben. Stimme ab, um die Ergebnisse zu sehen"; +"poll_timeline_total_one_vote_not_voted" = "1 Stimme abgegeben. Stimme ab, um die Ergebnisse zu sehen"; +"poll_timeline_not_closed_subtitle" = "Versuche es bitte erneut"; +"poll_timeline_vote_not_registered_title" = "Stimme nicht erfasst"; +"poll_edit_form_post_failure_subtitle" = "Versuche es bitte erneut"; +"poll_edit_form_post_failure_title" = "Fehler beim Senden der Abstimmung"; +"share_extension_low_quality_video_message" = "Für eine bessere Qualität sende es in %@ oder sende es in niedriger Qualität."; +"share_extension_low_quality_video_title" = "Das Video wird in niedriger Qualität gesendet werden"; +"analytics_prompt_stop" = "Teilen beenden"; +"analytics_prompt_not_now" = "Nicht jetzt"; From f30508f2d8bf3751f30734e2d08ccd860258bdcd Mon Sep 17 00:00:00 2001 From: Whodiduexpect Date: Fri, 24 Dec 2021 20:05:36 +0000 Subject: [PATCH 067/109] Translated using Weblate (French) Currently translated at 100.0% (1375 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/fr/ --- Riot/Assets/fr.lproj/Vector.strings | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index 767c24666a..aafd1db9ad 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -62,7 +62,7 @@ "auth_missing_phone" = "Numéro de téléphone manquant"; "auth_missing_email_or_phone" = "Adresse e-mail ou numéro de téléphone manquant"; "auth_password_dont_match" = "Les mots de passe ne correspondent pas"; -"auth_username_in_use" = ""; +"auth_username_in_use" = "Nom d’utilisateur déjà pris"; "auth_forgot_password" = "Mot de passe oublié ?"; "auth_use_server_options" = "Utiliser un serveur personnalisé (avancé)"; "auth_email_validation_message" = "Merci de vérifier vos e-mails pour continuer l’inscription"; @@ -1600,3 +1600,7 @@ "contacts_address_book_permission_denied_alert_title" = "Contacts désactivés"; "accessibility_button_label" = "bouton"; "enable" = "Activer"; +"find_your_contacts_footer" = "Cette fonctionnalité peut être désactivé à tout moment à partir des paramètres."; +"find_your_contacts_title" = "Commencez par lister vos contacts"; +"settings_contacts_enable_sync_description" = "Cette fonctionnalité utilisera votre serveur d'identité pour vous connecter avec vos contacts, ainsi que pour les aider à vous trouver."; +"settings_contacts_enable_sync" = "Trouvez vos contacts"; From 00f844d689ee8e916d2972a908e51033cb4330bf Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Mon, 20 Dec 2021 19:23:08 +0000 Subject: [PATCH 068/109] Translated using Weblate (Ukrainian) Currently translated at 100.0% (1375 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ --- Riot/Assets/uk.lproj/Vector.strings | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index f7fef55af9..7694f2d5e7 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -1686,8 +1686,19 @@ // Sessions list -"user_verification_sessions_list_user_trust_level_trusted_title" = "Довіряєте"; +"user_verification_sessions_list_user_trust_level_trusted_title" = "Довірений"; "user_verification_sessions_list_information" = "Листування з цим користувачем у цій кімнаті наскрізно зашифроване й непрочитне для сторонніх."; "user_verification_session_details_information_untrusted_current_user" = "Звірте цей сеанс, щоб позначити його довіреним і надати йому доступ до зашифрованих повідомлень:"; "secrets_setup_recovery_key_storage_alert_message" = "✓ Надрукуйте його й зберігайте в надійному місці\n✓ Збережіть його на USB-ключ або носій резервного копіювання\n✓ Скопіюйте його до вашого особистого хмарного сховища"; "call_transfer_error_message" = "Не вдалося переадресувати виклик"; +"favourites_empty_view_information" = "Ви можете додати до улюблених кількома способами, найшвидший — це просто натиснути й утримувати. Торкніться зірки, і вони автоматично зʼявляться тут для безпечного зберігання."; + +// MARK: - Favourites + +"favourites_empty_view_title" = "Улюблені кімнати й люди"; +"group_participants_filter_members" = "Фільтр учасників спільноти"; +"group_participants_invite_prompt_msg" = "Ви впевнені, що хочете запросити %@ до цієї групи?"; +"room_details_fail_to_update_room_direct" = "Не вдалося оновити прямий прапор цієї кімнати"; +"room_details_flair_section" = "Показувати значки для спільнот"; +"settings_flair" = "Показувати значок, де це дозволено"; +"room_warning_about_encryption" = "Наскрізне шифрування ще на етапі бета-тестування й може бути ненадійним.\n\nПоки що не варто довіряти йому захист даних.\n\nПристрої ще не зможуть розшифрувати історію до того, як з них приєдналися до кімнати.\n\nЗашифровані повідомлення не буде показано у клієнтах, які ще не використовують шифрування."; From 285d7aed1a704b784bde299a9d75e64843ee812b Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Fri, 24 Dec 2021 00:18:27 +0000 Subject: [PATCH 069/109] Translated using Weblate (Slovak) Currently translated at 95.0% (1307 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 501 ++++++++++++++++++++++++++++ 1 file changed, 501 insertions(+) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index a55477f049..833708fd08 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -1118,3 +1118,504 @@ "key_verification_verify_sas_title_emoji" = "Porovnať emoji"; "device_verification_self_verify_start_verify_action" = "Spustiť overovanie"; + +// Recover from passphrase + +"key_backup_recover_from_passphrase_info" = "Použite svoju bezpečnostnú frázu na odomknutie histórie zabezpečených správ"; + +// Recover from private key +"key_backup_recover_from_private_key_info" = "Obnovenie zálohy…"; +"key_backup_recover_invalid_recovery_key" = "Zálohu sa nepodarilo dešifrovať pomocou tohto kľúča: overte prosím, či ste zadali správny bezpečnostný kľúč."; +"key_backup_recover_invalid_recovery_key_title" = "Nezhoda bezpečnostných kľúčov"; +"key_backup_recover_invalid_passphrase" = "Zálohu sa nepodarilo dešifrovať pomocou tejto frázy: overte prosím, či ste zadali správnu bezpečnostnú frázu."; +"key_backup_setup_passphrase_setup_recovery_key_action" = "( Pokročilé) Nastavenie pomocou bezpečnostného kľúča"; +"secure_key_backup_setup_cancel_alert_message" = "Ak to teraz zrušíte, môžete prísť o zašifrované správy a údaje, ak stratíte prístup k svojim prihlasovacím údajom.\n\nBezpečné zálohovanie a správu kľúčov môžete nastaviť aj v Nastaveniach."; +"secure_key_backup_setup_intro_use_security_passphrase_info" = "Zadajte tajnú frázu, ktorú poznáte len vy, a vygenerujte kľúč na zálohovanie."; +"secure_key_backup_setup_intro_use_security_key_info" = "Vygenerujte bezpečnostný kľúč a uložte ho na bezpečné miesto, napríklad do správcu hesiel alebo trezora."; +"deactivate_account_forget_messages_information_part3" = ": toto spôsobí, že budúci používatelia uvidia neúplný prehľad konverzácií)"; +"deactivate_account_forget_messages_information_part1" = "Prosím, zabudnite na všetky správy, ktoré som poslal, keď bude moje konto deaktivované ("; +"deactivate_account_informations_part5" = "Ak chcete, aby sme vaše správy zabudli, označte nižšie uvedené políčko\n\nViditeľnosť správ v Matrixe je podobná ako v prípade e-mailu. Naše zabudnutie vašich správ znamená, že správy, ktoré ste poslali, nebudú zdieľané s novými alebo neregistrovanými používateľmi, ale registrovaní používatelia, ktorí už majú prístup k týmto správam, budú mať stále prístup k ich kópii."; +"deactivate_account_informations_part1" = "Týmto sa vaše konto stane trvalo nepoužiteľným. Nebudete sa môcť prihlásiť a nikto nebude môcť opätovne zaregistrovať rovnaké ID používateľa. To spôsobí, že váš účet opustí všetky miestnosti, ktorých sa zúčastňuje, a odstráni údaje o vašom účte zo servera totožností. "; +"service_terms_modal_description_integration_manager" = "Toto vám umožní používať botov, premostenia, widgety a balíčky nálepiek."; +"service_terms_modal_description_identity_server" = "Vďaka tomu vás niekto nájde, ak má vaše telefónne číslo alebo e-mail uložený v kontaktoch telefónu."; +"widget_menu_revoke_permission" = "Zrušiť prístup pre mňa"; +"widget_sticker_picker_no_stickerpacks_alert_add_now" = "Pridať nejaké teraz?"; +"widget_creation_failure" = "Vytvorenie widgetu zlyhalo"; +"photo_library_access_not_granted" = "%@ nemá povolenie používať knižnicu fotografií, zmeňte to prosím nastaveniach ochrany súkromia"; +"event_formatter_jitsi_widget_removed_by_you" = "Odstránili ste konferenciu VoIP"; +"event_formatter_jitsi_widget_added_by_you" = "Pridali ste konferenciu VoIP"; +"event_formatter_call_back" = "Zavolať späť"; +"event_formatter_rerequest_keys_part2" = " z vašich ostatných relácií."; +"event_formatter_rerequest_keys_part1_link" = "Opätovne vyžiadať šifrovacie kľúče"; +"event_formatter_jitsi_widget_removed" = "VoIP konferenciu odstránil/a %@"; +"event_formatter_jitsi_widget_added" = "Konferenciu VoIP pridal/a %@"; +"room_notifs_settings_encrypted_room_notice" = "Upozorňujeme, že oznámenia o zmienkach a kľúčových slovách nie sú v mobilných zariadeniach k dispozícii v zašifrovaných miestnostiach."; +"room_details_fail_to_enable_encryption" = "Nepodarilo sa zapnúť šifrovanie v tejto miestnosti"; +"room_details_fail_to_update_room_directory_visibility" = "Nepodarilo sa aktualizovať viditeľnosť adresára miestnosti"; +"identity_server_settings_alert_error_terms_not_accepted" = "Ak ho chcete nastaviť %@ ako server identity, musíte prijať jeho podmienky."; +"identity_server_settings_alert_disconnect_still_sharing_3pid" = "Svoje osobné údaje stále zdieľate na serveri totožností %@.\n\nOdporúčame vám, aby ste pred odpojením odstránili svoje e-mailové adresy a telefónne čísla zo servera totožností."; +"identity_server_settings_disconnect_info" = "Odpojenie od servera totožností bude znamenať, že vás nebudú môcť objaviť iní používatelia a že nebudete môcť pozývať ostatných prostredníctvom e-mailu alebo telefónu."; +"identity_server_settings_description" = "V súčasnosti používate %@ na objavovanie existujúcich kontaktov, ktoré poznáte, a na to, aby vás mohli objaviť."; +"security_settings_crypto_sessions_description_2" = "Ak nespoznávate prihlásenie, zmeňte si heslo a vykonajte obnovenie funkcie Zabezpečené zálohovanie."; +"settings_identity_server_no_is_description" = "Momentálne nepoužívate žiadny server totožností. Ak chcete objaviť existujúce kontakty, ktoré poznáte, a byť nimi objaviteľní, pridajte jeden vyššie."; +"settings_analytics_and_crash_data" = "Odoslať údaje o páde a analytické údaje"; +"settings_labs_create_conference_with_jitsi" = "Vytvárajte konferenčné hovory pomocou aplikácie jitsi"; +"settings_callkit_info" = "Prijímajte prichádzajúce hovory na uzamknutej obrazovke. Prezrite si svoje %@ hovory v histórii hovorov systému. Ak je povolená služba iCloud, táto história hovorov bude zdieľaná so spoločnosťou Apple."; +"settings_notify_me_for" = "Informovať ma o"; +"poll_timeline_votes_count" = "%lu hlasov"; +"poll_edit_form_post_failure_subtitle" = "Prosím, skúste to znova"; +"version_check_modal_action_title_deprecated" = "Zistite ako"; +"version_check_modal_subtitle_deprecated" = "Pracovali sme na vylepšení %@, aby bolo používanie rýchlejšie a dokonalejšie. Bohužiaľ, vaša súčasná verzia systému iOS nie je kompatibilná s niektorými z týchto opráv a nebude ďalej podporovaná.\nOdporúčame vám aktualizovať operačný systém, aby ste mohli používať %@ naplno."; +"version_check_modal_title_deprecated" = "Už nepodporujeme iOS %@"; +"version_check_modal_subtitle_supported" = "Pracovali sme na vylepšení %@, aby bolo používanie rýchlejšie a dokonalejšie. Bohužiaľ, vaša súčasná verzia systému iOS nie je kompatibilná s niektorými z týchto opráv a nebude ďalej podporovaná.\nOdporúčame vám aktualizovať operačný systém, aby ste mohli používať %@ naplno."; +"version_check_modal_title_supported" = "Ukončujeme podporu pre iOS %@"; +"version_check_banner_subtitle_deprecated" = "V systéme iOS %@ už nepodporujeme %@. Ak chcete naďalej používať %@ naplno, odporúčame vám aktualizovať verziu systému iOS."; +"version_check_banner_title_deprecated" = "Už nepodporujeme iOS %@"; +"version_check_banner_subtitle_supported" = "Čoskoro ukončíme podporu pre %@ v systéme iOS %@. Ak chcete naďalej využívať plný potenciál %@, odporúčame vám aktualizovať verziu systému iOS."; + +// Mark: - Version check + +"version_check_banner_title_supported" = "Ukončujeme podporu pre iOS %@"; +"voice_message_stop_locked_mode_recording" = "Ťuknutím na nahrávku ju zastavíte alebo si ju vypočujete"; + +// Mark: - Voice Messages + +"voice_message_release_to_send" = "Podržaním nahrávate, uvoľnením odošlete"; +"user_avatar_view_accessibility_hint" = "Zmeniť obrázok používateľa"; +"space_avatar_view_accessibility_hint" = "Zmeniť obrázok priestoru"; +"space_home_show_all_rooms" = "Zobraziť všetky miestnosti"; +"space_participants_action_ban" = "Vylúčiť z tohto priestoru"; +"space_participants_action_remove" = "Odstrániť z tohto priestoru"; +"spaces_coming_soon_detail" = "Táto funkcia tu ešte nebola zapracovaná, ale je na ceste k jej zapracovaniu. Zatiaľ to môžete urobiť pomocou aplikácie Element v počítači."; +"spaces_invites_coming_soon_title" = "Pozvánky už čoskoro"; +"spaces_add_rooms_coming_soon_title" = "Pridávanie miestností už čoskoro"; +"spaces_no_member_found_detail" = "Hľadáte niekoho, kto nie je v %@? Zatiaľ ich môžete pozvať na webe alebo na počítači."; +"spaces_no_room_found_detail" = "Niektoré výsledky môžu byť skryté, pretože sú súkromné a na pripojenie k nim potrebujete pozvánku."; +"spaces_no_result_found_title" = "Nenašli sa žiadne výsledky"; +"spaces_empty_space_detail" = "Niektoré miestnosti môžu byť skryté, pretože sú súkromné a potrebujete pozvánku."; +"leave_space_only_action" = "Neopustiť žiadne miestnosti"; +"leave_space_message_admin_warning" = "Ste administrátorom tohto priestoru, pred odchodom sa uistite, že ste právo administrátora preniesli na iného člena."; +"leave_space_message" = "Ste si istí, že chcete opustiť %@? Chcete opustiť aj všetky miestnosti a priestory tohto priestoru?"; +"space_beta_announce_information" = "Priestory sú novým spôsobom zoskupovania miestností a ľudí. Zatiaľ nie sú v systéme iOS, ale už ich môžete používať na webe a počítači."; +"space_beta_announce_title" = "Priestory budú čoskoro"; +"space_feature_unavailable_information" = "Priestory sú novým spôsobom zoskupovania miestností a ľudí.\n\nBudú tu čoskoro. Zatiaľ platí, že ak sa k nim pripojíte na inej platforme, budete mať prístup ku všetkým miestnostiam, ku ktorým sa pripojíte tu."; +"space_feature_unavailable_subtitle" = "Spaces ešte nie sú v systéme iOS, ale už ich môžete používať na webe a počítači"; + +// Mark: - Spaces + +"space_feature_unavailable_title" = "Priestory tu ešte nie sú"; +"room_intro_cell_information_dm_sentence1_part1" = "Toto je začiatok vašej priamej správy s "; +"room_intro_cell_information_room_sentence1_part1" = "Toto je začiatok "; +"home_empty_view_information" = "Univerzálna aplikácia na bezpečný konverzáciu pre tímy, priateľov a organizácie. Ťuknutím na tlačidlo + nižšie pridáte ľudí a miestnosti."; +"create_room_section_footer_type" = "Do súkromnej miestnosti sa ľudia môžu pripojiť len s pozvánkou do miestnosti."; +"create_room_section_footer_encryption" = "Šifrovanie sa potom nedá vypnúť."; +"biometrics_cant_unlocked_alert_message_login" = "Prihlásiť sa znova"; +"biometrics_cant_unlocked_alert_message_x" = "Ak chcete odomknúť, použite %@ alebo sa prihláste a zapnite %@ znova"; +"biometrics_cant_unlocked_alert_title" = "Nie je možné odomknúť aplikáciu"; +"biometrics_usage_reason" = "Na prístup k aplikácii je potrebné overenie"; +"biometrics_setup_subtitle" = "Ušetrite si čas"; +"pin_protection_kick_user_alert_message" = "Príliš veľa chýb, boli ste odhlásení"; +"pin_protection_explanatory" = "Nastavenie kódu PIN umožňuje chrániť údaje, ako sú správy a kontakty, takže prístup k nim získate len vy zadaním kódu PIN na začiatku aplikácie."; +"pin_protection_not_allowed_pin" = "Z bezpečnostných dôvodov nie je tento kód PIN k dispozícii. Prosím, skúste iný PIN"; +"pin_protection_settings_enabled_forced" = "Zapnutý kód PIN"; +"pin_protection_settings_section_footer" = "Ak chcete obnoviť kód PIN, musíte sa znovu prihlásiť a vytvoriť nový kód PIN."; +"pin_protection_settings_section_header_with_biometrics" = "PIN a %@"; +"pin_protection_mismatch_too_many_times_error_message" = "Ak si nemôžete spomenúť na svoj kód PIN, ťuknite na tlačidlo Zabudnutý kód PIN."; +"pin_protection_mismatch_error_message" = "Prosím, skúste to znova"; +"pin_protection_mismatch_error_title" = "PIN kódy sa nezhodujú"; +"pin_protection_reset_alert_message" = "Ak chcete obnoviť kód PIN, musíte sa znovu prihlásiť a vytvoriť nový kód PIN"; +"secrets_reset_warning_message" = "Reštartujete bez histórie, bez správ, dôveryhodných zariadení a dôveryhodných používateľov."; +"secrets_reset_warning_title" = "Ak všetko obnovíte do pôvodného stavu"; +"secrets_reset_information" = "Urobte to len vtedy, ak nemáte žiadne iné zariadenie, pomocou ktorého by ste mohli toto zariadenie overiť."; +"secrets_recovery_with_key_information_default" = "Prístup k histórii zabezpečených správ a k totožnosti krížového podpisu na overenie iných relácií získate zadaním bezpečnostného kľúča."; +"user_verification_session_details_additional_information_untrusted_other_user" = "Pokiaľ tento používateľ nezačne tejto relácii dôverovať, správy odoslané do nej a z nej sú označené varovaním. Môžete ju tiež overiť ručne."; + +// MARK: Manually Verify Device + +"key_verification_manually_verify_device_title" = "Ručné overenie pomocou textu"; +"device_verification_self_verify_wait_recover_secrets_checking_availability" = "Kontrola ďalších možností overovania ..."; +"device_verification_self_verify_wait_recover_secrets_additional_information" = "Ak nemáte prístup k existujúcej relácii"; +"device_verification_error_cannot_load_device" = "Nie je možné načítať informácie o relácii."; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_message" = "Ak si pred odhlásením nezálohujete kľúče, stratíte prístup k zašifrovaným správam."; +"find_your_contacts_message" = "Nechajte %@ zobraziť svoje kontakty, aby ste mohli rýchlo začať komunikovať s tými, ktorých najlepšie poznáte."; +"callbar_only_multiple_paused" = "%@ pozastavené hovory"; +"identity_server_settings_alert_change" = "Odpojiť sa od servera totožnosti %1$@ a namiesto toho sa pripojiť k %2$@?"; +"identity_server_settings_alert_no_terms" = "Vami vybratý server totožností nemá podmienky používania služieb. Pokračujte len v prípade, že dôverujete vlastníkovi servera."; +"identity_server_settings_alert_no_terms_title" = "Server totožností nemá žiadne podmienky poskytovania služieb"; +"identity_server_settings_no_is_description" = "Momentálne nepoužívate server totožností. Ak chcete nájsť a byť objaviteľní existujúcimi kontaktmi, pridajte jeden vyššie."; +"security_settings_crosssigning_complete_security" = "Dokončiť zabezpečenie"; +"security_settings_crosssigning_info_exists" = "Váš účet má totožnosť krížového podpisu, ale táto relácia mu ešte nedôveruje. Dokončite zabezpečenie tejto relácie."; +"security_settings_secure_backup_description" = "Zálohujte si šifrovacie kľúče s údajmi o účte pre prípad, že stratíte prístup k reláciám. Vaše kľúče budú zabezpečené jedinečným bezpečnostným kľúčom."; +"settings_identity_server_description" = "Pomocou vyššie nastaveného servera totožnosti môžete nájsť existujúce kontakty, ktoré poznáte, a aj oni vás môžu nájsť."; +"settings_discovery_three_pid_details_information_phone_number" = "Spravujte predvoľby pre toto telefónne číslo, ktoré môžu ostatní používatelia použiť na vaše objavenie a pozvanie do miestností. Pridajte alebo odstráňte telefónne čísla v časti Účty."; +"settings_discovery_three_pids_management_information_part1" = "Spravujte, ktoré e-mailové adresy alebo telefónne čísla môžu ostatní používatelia použiť na vaše objavenie a pozvanie do miestností. E-mailové adresy alebo telefónne čísla môžete pridávať alebo odstraňovať z tohto zoznamu v "; +"settings_discovery_terms_not_signed" = "Musíte súhlasiť s podmienkami služby servera totožnosti (%@), aby vás mohli nájsť podľa e-mailovej adresy alebo telefónneho čísla."; +"settings_key_backup_button_connect" = "Pripojiť túto reláciu k zálohovaniu kľúčov"; +"settings_fail_to_update_password" = "Nepodarilo sa aktualizovať heslo"; +"settings_labs_enable_ringing_for_group_calls" = "Zvonenie pre skupinové hovory"; +"widget_integration_positive_power_level" = "Úroveň oprávnenia musí byť kladné celé číslo."; +"settings_integrations_allow_description" = "Použite správcu integrácií (%@) na spravovanie botov, premostení, widgetov a balíčkov s nálepkami. \n\nSprávcovia integrácie dostávajú konfiguračné údaje a môžu vo vašom mene upravovať widgety, posielať pozvánky do miestnosti a nastavovať úrovne oprávnení."; +"settings_mentions_and_keywords_encryption_notice" = "V mobilných zariadeniach nebudete dostávať upozornenia na zmienky a kľúčové slová v zašifrovaných miestnostiach."; +"settings_your_keywords" = "Vaše kľúčové slová"; +"settings_messages_by_a_bot" = "Správy od bota"; +"settings_call_invitations" = "Pozvánky na hovory"; +"settings_room_invitations" = "Pozvánky do miestnosti"; +"settings_global_settings_info" = "Všeobecné nastavenia oznámení sú k dispozícii vo webovom klientovi %@"; +"settings_confirm_media_size_description" = "Keď je táto funkcia zapnutá, budete vyzvaní, aby ste potvrdili, v akej veľkosti sa budú obrázky a videá odosielať."; +"settings_three_pids_management_information_part1" = "Tu môžete spravovať e-mailové adresy alebo telefónne čísla, ktoré môžete použiť na prihlásenie alebo obnovenie účtu. Ovládajte, kto vás môže nájsť v "; +"settings_fail_to_update_profile" = "Nepodarilo sa aktualizovať profil"; +"settings_email_address_placeholder" = "Zadajte svoju e-mailovú adresu"; +"settings_remove_phone_prompt_msg" = "Určite chcete odstrániť telefónne číslo %@?"; +"settings_remove_email_prompt_msg" = "Určite chcete odstrániť e-mailovú adresu %@?"; +"settings_sign_out_e2e_warn" = "Prídete o svoje end-to-end šifrovacie kľúče. To znamená, že v tomto zariadení už nebudete môcť čítať staré správy v zašifrovaných miestnostiach."; +"settings_labs" = "EXPERIMENTÁLNE"; +"room_ongoing_conference_call_with_close" = "Prebiehajúci konferenčný hovor. Pripojte sa ako %@ alebo %@. %@ to."; +"room_ongoing_conference_call" = "Prebiehajúci konferenčný hovor. Pripojte sa ako %@ alebo %@."; +"room_unsent_messages_unknown_devices_notification" = "Správu sa nepodarilo odoslať z dôvodu prítomnosti neznámych relácií."; +"room_participants_action_security_status_complete_security" = "Dokončiť zabezpečenie"; +"room_participants_invite_malformed_id" = "Chybné ID. Mala by to byť emailová adresa alebo Matrix ID ako napríklad \"@lokalnacast:domena\""; +"directory_search_fail" = "Nepodarilo sa načítať údaje"; +"directory_search_results_title" = "Prehľadávať výsledky adresára"; +"room_creation_dm_error" = "Nemohli sme vytvoriť vašu PS. Skontrolujte používateľov, ktorých chcete pozvať, a skúste to znova."; +"auth_softlogout_clear_data_sign_out_msg" = "Ste si istí, že chcete vymazať všetky údaje aktuálne uložené v tomto zariadení? Znovu sa prihláste, aby ste získali prístup k údajom a správam svojho účtu."; +"auth_softlogout_clear_data_message_2" = "Vyčistite to, ak ste skončili s používaním tohto zariadenia alebo sa chcete prihlásiť do iného účtu."; +"auth_softlogout_clear_data_message_1" = "Varovanie: Vaše osobné údaje (vrátane šifrovacích kľúčov) sú stále uložené na tomto zariadení."; +"auth_softlogout_recover_encryption_keys" = "Prihláste sa, aby ste obnovili šifrovacie kľúče uložené výlučne v tomto zariadení. Potrebujete ich na čítanie všetkých svojich zabezpečených správ na akomkoľvek zariadení."; +"auth_softlogout_reason" = "Váš správca domovského servera (%1$@) vás odhlásil z vášho účtu %2$@ (%3$@)."; +"auth_add_email_and_phone_warning" = "Registrácia pomocou e-mailu a telefónneho čísla naraz zatiaľ nie je podporovaná, kým nebude existovať api. Do úvahy sa bude brať iba telefónne číslo. Svoj e-mail môžete pridať do svojho profilu v nastaveniach."; +"auth_reset_password_success_message" = "Vaše heslo bolo obnovené.\n\nBoli ste odhlásení zo všetkých relácií a už nebudete dostávať push oznámenia. Ak chcete opätovne povoliť oznámenia, znovu sa prihláste na každom zariadení."; +"auth_untrusted_id_server" = "Server totožností nie je dôveryhodný"; +"auth_phone_is_required" = "Nie je nastavený žiadny server totožností, takže nemôžete pridať e-telefónne číslo, aby ste mohli v budúcnosti obnoviť svoje heslo."; +"auth_email_is_required" = "Nie je nastavený žiadny server totožností, takže nemôžete pridať e-mailovú adresu, aby ste mohli v budúcnosti obnoviť svoje heslo."; +"auth_add_email_phone_message_2" = "Nastavte si e-mail na obnovenie účtu. Neskôr môžete voliteľne použiť e-mail alebo telefón, aby vás mohli nájsť ľudia, ktorí vás poznajú."; +"cross_signing_setup_banner_subtitle" = "Jednoduchšie overenie ostatných zariadení"; +"room_details_advanced_e2e_encryption_blacklist_unverified_devices" = "Šifrovať len overeným reláciám"; +"spaces_empty_space_title" = "Tento priestor nemá žiadne miestnosti (zatiaľ)"; +"leave_space_and_all_rooms_action" = "Opustiť všetky miestnosti a priestory"; +"space_beta_announce_subtitle" = "Nová verzia komunít"; +"room_intro_cell_information_multiple_dm_sentence2" = "V tejto konverzácii ste len vy, pokiaľ niekto z vás niekoho nepozve, aby sa pripojil."; +"room_intro_cell_information_dm_sentence2" = "V tejto konverzácii ste len vy dvaja, nikto iný sa nemôže pripojiť."; +"room_intro_cell_information_room_without_topic_sentence2_part2" = " aby ľudia vedeli, o čom je táto miestnosť."; +"room_intro_cell_information_room_without_topic_sentence2_part1" = "Pridať tému"; +"room_avatar_view_accessibility_hint" = "Zmeniť obrázok miestnosti"; +"invite_friends_share_text" = "Hej, ozvi sa mi na %@: %@"; + +// MARK: - Invite friends + +"invite_friends_action" = "Pozvať priateľov do %@"; +"favourites_empty_view_information" = "Obľúbiť si môžete niekoľkými spôsobmi - najrýchlejšie je jednoducho stlačiť a podržať. Ťuknite na hviezdičku a automaticky sa tu zobrazia na bezpečné uloženie."; + +// MARK: - Favourites + +"favourites_empty_view_title" = "Obľúbené miestnosti a ľudia"; +"create_room_show_in_directory" = "Zobraziť miestnosť v adresári"; +"create_room_section_header_topic" = "Téma miestnosti (voliteľné)"; +"searchable_directory_search_placeholder" = "Meno alebo ID"; + +// MARK: - Searchable Directory View Controller + +"searchable_directory_create_new_room" = "Vytvoriť novú miestnosť"; +"biometrics_mode_face_id" = "Face ID"; + +// MARK: - Biometrics Protection + +"biometrics_mode_touch_id" = "Touch ID"; +"pin_protection_reset_alert_title" = "Obnoviť PIN"; +"pin_protection_forgot_pin" = "Zabudli ste PIN kód"; +"pin_protection_enter_pin" = "Zadajte svoj kód PIN"; +"pin_protection_confirm_pin_to_change" = "Potvrďte kód PIN, na zmenu kódu PIN"; +"pin_protection_confirm_pin_to_disable" = "Potvrďte kód PIN, ak chcete deaktivovať kód PIN"; +"pin_protection_confirm_pin" = "Potvrďte svoj kód PIN"; +"pin_protection_choose_pin" = "Vytvoriť bezpečnostný kód PIN"; +"major_update_information" = "S radosťou oznamujeme, že sme zmenili názov! Vaša aplikácia je aktualizovaná a ste prihlásení do svojho účtu."; + +// MARK: - Major update + +"major_update_title" = "Riot je teraz %@"; + +// MARK: - Cross-signing + +// Banner + +"cross_signing_setup_banner_title" = "Nastaviť šifrovanie"; +"secrets_reset_authentication_message" = "Zadajte heslo svojho účtu pre potvrdenie"; +"secrets_setup_recovery_passphrase_summary_information" = "Zapamätajte si bezpečnostnú frázu. Môžete ju použiť na odomknutie zašifrovaných správ a údajov."; +"secrets_setup_recovery_passphrase_summary_title" = "Uložte si bezpečnostnú frázu"; +"secrets_setup_recovery_passphrase_confirm_information" = "Opätovne zadajte bezpečnostnú frázu a potvrďte ju."; +"secrets_setup_recovery_passphrase_additional_information" = "Nepoužívajte heslo k svojmu účtu."; +"secrets_setup_recovery_passphrase_information" = "Zadajte bezpečnostnú frázu, ktorú poznáte len vy a ktorá sa používa na zabezpečenie tajomstiev na vašom serveri."; +"secrets_recovery_with_key_recovery_key_title" = "Zadajte"; +"secrets_recovery_with_key_information_unlock_secure_backup_with_key" = "Ak chcete pokračovať, zadajte bezpečnostný kľúč."; +"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "Ak chcete pokračovať, zadajte bezpečnostnú frázu."; +"secrets_recovery_with_key_information_verify_device" = "Na overenie tohto zariadenia použite bezpečnostná kľúč."; +"secrets_recovery_with_passphrase_passphrase_title" = "Zadajte"; +"secrets_recovery_with_passphrase_information_default" = "Prístup k histórii zabezpečených správ a k totožnosti krížového podpisu na overenie iných relácií získate zadaním bezpečnostnej frázy."; + +// MARK: - Secrets recovery + +"secrets_recovery_reset_action_part_1" = "Zabudli ste alebo ste stratili všetky možnosti obnovy? "; +"user_verification_session_details_verify_action_current_user_manually" = "Ručné overenie pomocou textu"; +"user_verification_session_details_additional_information_untrusted_current_user" = "Ak ste sa do tejto relácie neprihlásili, vaše konto môže byť ohrozené."; +"key_verification_tile_request_status_expired" = "Vypršala platnosť"; + + +// Generic errors +"error_invite_3pid_with_no_identity_server" = "V nastaveniach pridajte server totožnosti, ak chcete pozývať e-mailom."; +"emoji_picker_people_category" = "Smajlíky a ľudia"; +"file_upload_error_unsupported_file_type_message" = "Nepodporovaný typ súboru."; +"key_verification_self_verify_unverified_sessions_alert_message" = "Overte všetky vaše relácie, aby ste si boli istý, že sú vaše správy a účet bezpečné."; +"sign_out_non_existing_key_backup_alert_title" = "Ak sa teraz odhlásite, prídete o zašifrované správy"; +"device_verification_emoji_thumbs up" = "Palec hore"; + +// Device + +"device_verification_verify_wait_partner" = "Čakanie na potvrdenie od partnera…"; +"key_verification_manually_verify_device_additional_information" = "Ak sa nezhodujú, môže byť ohrozená bezpečnosť vašej komunikácie."; +"key_verification_manually_verify_device_instruction" = "Potvrďte to porovnaním nasledujúcich údajov s nastaveniami používateľa v inej relácii:"; +"key_verification_verify_sas_additional_information" = "V záujme maximálnej bezpečnosti použite iný dôveryhodný komunikačný prostriedok alebo to urobte osobne."; +"key_verification_verify_sas_cancel_action" = "Nezhodujú sa"; +"key_verification_verify_sas_title_number" = "Porovnať čísla"; +"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Použiť bezpečnostnú frázu alebo kľúč"; +"device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Použiť bezpečnostný kľúč"; +"device_verification_self_verify_wait_additional_information" = "Toto funguje s %@ a inými klientmi Matrix podporujúcimi krížové podpisovanie."; +"device_verification_self_verify_wait_information" = "Overte túto reláciu z niektorej z vašich ostatných relácií, čím jej udelíte prístup k zašifrovaným správam.\n\nNa ostatných zariadeniach použite najnovší %@:"; +"device_verification_self_verify_wait_new_sign_in_title" = "Overiť toto prihlásenie"; + +// MARK: Self verification wait + +"device_verification_self_verify_wait_title" = "Dokončiť zabezpečenie"; +"key_verification_self_verify_unverified_sessions_alert_validate_action" = "Overiť"; + +// Unverified sessions + +"key_verification_self_verify_unverified_sessions_alert_title" = "Zobraziť, kde ste prihlásený"; +"key_verification_self_verify_current_session_alert_message" = "Ostatní používatelia jej nemusia dôverovať."; + +// Current session + +"key_verification_self_verify_current_session_alert_title" = "Overiť túto reláciu"; +"device_verification_self_verify_start_information" = "Túto reláciu použite na overenie novej relácie, čím jej udelíte prístup k zašifrovaným správam."; +"device_verification_self_verify_alert_message" = "Overte nové prihlásenie prístupom k vášmu účtu: %@"; + +// MARK: Self verification start + +// New login +"device_verification_self_verify_alert_title" = "Nové prihlásenie. Boli ste to vy?"; +"device_verification_start_use_legacy_action" = "Použiť starší spôsob overenia"; +"device_verification_start_verify_button" = "Začať overenie"; +"device_verification_start_use_legacy" = "Nič sa neobjavuje? Nie všetci klienti ešte podporujú interaktívne overovanie. Použite staršie overovanie."; +"device_verification_start_wait_partner" = "Čaká sa na prijatie partnerom…"; + +// MARK: Start +"device_verification_start_title" = "Overiť porovnaním krátkeho textu"; +"device_verification_incoming_description_2" = "Overenie tejto relácie ju označí ako dôveryhodnú a zároveň označí vašu reláciu ako dôveryhodnú pre partnera."; +"device_verification_incoming_description_1" = "Overte túto reláciu a označte ju ako dôveryhodnú. Dôveryhodnosť relácií partnerov vám poskytuje pokoj na duši pri používaní end-to-end šifrovaných správ."; + +// Mark: Incoming +"device_verification_incoming_title" = "Prichádzajúca žiadosť o overenie"; +"device_verification_cancelled_by_me" = "Overovanie bolo zrušené. Dôvod: %@"; +"device_verification_cancelled" = "Druhá strana zrušila overenie."; +"device_verification_security_advice_number" = "Porovnajte čísla a uistite sa, že sú v rovnakom poradí."; +"device_verification_security_advice_emoji" = "Porovnajte jedinečné emoji a uistite sa, že sú zobrazené v rovnakom poradí."; +"key_verification_this_session_title" = "Overiť túto reláciu"; +"key_verification_new_session_title" = "Overte svoju novú reláciu"; + +// MARK: - Device Verification +"key_verification_other_session_title" = "Overiť reláciu"; +"sign_out_key_backup_in_progress_alert_cancel_action" = "Počkám"; +"sign_out_key_backup_in_progress_alert_discard_key_backup_action" = "Nezáleží mi na zašifrovaných správach"; +"sign_out_key_backup_in_progress_alert_title" = "Prebieha zálohovanie šifrovacích kľúčov. Ak sa teraz odhlásite, prídete o zašifrované správy."; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_sign_out_action" = "Odhlásiť sa"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_title" = "Stratíte svoje zašifrované správy"; +"sign_out_non_existing_key_backup_alert_discard_key_backup_action" = "Nezáleží mi na zašifrovaných správach"; +"sign_out_non_existing_key_backup_alert_setup_secure_backup_action" = "Začnite používať Bezpečné zálohovanie"; +"sign_out_existing_key_backup_alert_sign_out_action" = "Odhlásiť sa"; + +// MARK: Sign out warning + +"sign_out_existing_key_backup_alert_title" = "Naozaj sa chcete odhlásiť?"; + +// Success + +"key_backup_recover_success_info" = "Záloha obnovená!"; +"key_backup_recover_from_recovery_key_lost_recovery_key_action" = "Stratili ste kľúč obnovenia, v nastaveniach získate nový."; +"key_backup_recover_from_recovery_key_recover_action" = "Sprístupniť históriu"; +"key_backup_recover_from_recovery_key_recovery_key_placeholder" = "Zadajte bezpečnostný kľúč"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "použiť váš bezpečnostný kľúč"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Neviete svoju bezpečnostnú frázu? Môžete "; +"key_backup_recover_from_passphrase_recover_action" = "Sprístupniť históriu"; +"key_backup_recover_from_passphrase_passphrase_placeholder" = "Zadajte frázu"; + +// Success from recovery key +"key_backup_setup_success_from_recovery_key_info" = "Vaše kľúče sa zálohujú.\n\nVytvorte si kópiu tohto bezpečnostného kľúča a uschovajte ho na bezpečnom mieste."; +"key_backup_setup_passphrase_passphrase_invalid" = "Skúste pridať slovo"; +"key_backup_setup_passphrase_info" = "Šifrovanú kópiu vašich kľúčov uložíme na našom serveri. Chráňte svoju zálohu frázou, aby bola bezpečná.\n\nV záujme maximálneho zabezpečenia by sa mala líšiť od hesla k vášmu účtu."; +"key_backup_setup_intro_info" = "Správy v šifrovaných miestnostiach sú zabezpečené end-to-end šifrovaním. Kľúče na čítanie týchto správ máte len vy a príjemca (príjemcovia).\n\nKľúče si bezpečne zálohujte, aby ste ich nestratili."; +"secure_key_backup_setup_existing_backup_error_delete_it" = "Vymazať"; +"secure_key_backup_setup_existing_backup_error_unlock_it" = "Odomknúť"; +"secure_key_backup_setup_existing_backup_error_title" = "Záloha pre správy už existuje"; +"service_terms_modal_information_description_integration_manager" = "Správca integrácie umožňuje pridávať funkcie od tretích strán."; +"service_terms_modal_information_description_identity_server" = "Server totožností vám pomôže nájsť kontakty tak, že vyhľadá ich telefónne číslo alebo e-mailovú adresu a zistí, či už majú účet."; +"service_terms_modal_table_header_integration_manager" = "PODMIENKY SPRÁVCU INTEGRÁCIE"; +"service_terms_modal_table_header_identity_server" = "PODMIENKY SERVERA TOTOŽNOSTÍ"; +"service_terms_modal_footer" = "Toto môžete kedykoľvek vypnúť v nastaveniach."; + +// Service terms +"service_terms_modal_title_message" = "Ak chcete pokračovať, prijmite nižšie uvedené podmienky"; +"gdpr_consent_not_given_alert_review_now_action" = "Prečítať teraz"; + +// GDPR +"gdpr_consent_not_given_alert_message" = "Ak chcete pokračovať v používaní domovského servera %@, musíte si prečítať podmienky a súhlasiť s nimi."; +"e2e_room_key_request_share_without_verifying" = "Zdieľať bez overenia"; +"e2e_room_key_request_message" = "Vaša neoverená relácia '%@' požaduje šifrovacie kľúče."; +"e2e_room_key_request_message_new_device" = "Pridali ste novú reláciu \"%@\", ktorá požaduje šifrovacie kľúče."; +"share_extension_low_quality_video_message" = "Ak chcete poslať v lepšej kvalite, pošlite vo formáte %@ alebo pošlite v nízkej kvalite nižšie."; +"share_extension_low_quality_video_title" = "Video bude odoslané v nízkej kvalite"; +"room_widget_permission_information_title" = "Používaním zdieľate údaje s %@:\n"; +"room_widget_permission_webview_information_title" = "Používaním prijímate cookies od a zdieľate údaje %@:\n"; +"room_widget_permission_creator_info_title" = "Tento widget pridal:"; +"widget_integration_manager_disabled" = "V nastaveniach je potrebné povoliť správcu integrácie"; +"widget_integration_room_not_visible" = "Miestnosť %s nie je viditeľná."; +"widget_integration_missing_user_id" = "V požiadavke chýba user_id."; +"widget_integration_missing_room_id" = "V požiadavke chýba room_id."; +"widget_integration_no_permission_in_room" = "V tejto miestnosti na to nemáte povolenie."; +"widget_integration_must_be_in_room" = "Nenachádzate sa v tejto miestnosti."; +"widget_integration_room_not_recognised" = "Nie je možné rozpoznať takúto miestnosť."; +"widget_integration_failed_to_send_request" = "Nepodarilo sa odoslať požiadavku."; +"widget_integration_unable_to_create" = "Nie je možné vytvoriť widget."; +"widget_menu_remove" = "Odstrániť pre všetkých"; +"widget_no_power_to_manage" = "Musíte mať povolenie spravovať widgety v tejto miestnosti"; + +// Widget +"widget_no_integrations_server_configured" = "Nie je nastavený žiadny server integrácií"; +"bug_report_progress_zipping" = "Získavajú sa záznamy"; +"bug_report_send_logs" = "Odoslať záznamy"; +"bug_crash_report_description" = "Opíšte prosím, čo ste robili pred pádom aplikácie:"; +"bug_crash_report_title" = "Správa o poruche"; +"e2e_key_backup_wrong_version" = "Bola zistená nová záloha zabezpečenej správy.\n\nAk ste to neboli vy, nastavte novú bezpečnostnú frázu v Nastaveniach."; + +// Key backup wrong version +"e2e_key_backup_wrong_version_title" = "Nová záloha kľúča"; + +// Crypto +"e2e_enabling_on_app_update" = "%@ teraz podporuje end-to-end šifrovanie, ale na jeho zapnutie sa musíte znova prihlásiť.\n\nMôžete to urobiť teraz alebo neskôr v nastaveniach aplikácie."; +"analytics_prompt_stop" = "Zastaviť zdieľanie"; +"analytics_prompt_yes" = "Áno, je to v poriadku"; +"analytics_prompt_message_upgrade" = "Predtým ste nám udelili súhlas so zdieľaním anonymných údajov o používaní. Teraz, aby sme pomohli pochopiť, ako ľudia používajú viacero zariadení, vygenerujeme náhodný identifikátor zdieľaný vašimi zariadeniami."; +"analytics_prompt_message_new_user" = "Pomôžte nám identifikovať problémy a zlepšiť Element zdieľaním anonymných údajov o používaní. Aby sme pochopili, ako ľudia používajú viacero zariadení, vygenerujeme náhodný identifikátor, ktorý zdieľajú vaše zariadenia."; + +// Analytics +"analytics_prompt_title" = "Pomôžte zlepšiť %@"; +"call_already_displayed" = "Telefonát už prebieha."; +"network_offline_prompt" = "Zdá sa, že nie ste pripojený na internet."; +"event_formatter_widget_removed_by_you" = "Odstránili ste widget: %@"; + +// Events formatter with you +"event_formatter_widget_added_by_you" = "Pridali ste widget: %@"; +"event_formatter_call_missed_video" = "Zmeškaný video hovor"; +"event_formatter_call_missed_voice" = "Zmeškaný hlasový hovor"; +"event_formatter_call_has_ended_with_time" = "Hovor ukončený - %@"; +"event_formatter_widget_removed" = "%@ widget odstránil %@"; +"event_formatter_widget_added" = "%@ widget pridal %@"; +"directory_server_type_homeserver" = "Zadajte domovský server, z ktorého chcete zobraziť zoznam verejných miestností"; +"directory_server_all_native_rooms" = "Všetky natívne miestnosti Matrix"; +"directory_server_all_rooms" = "Všetky miestnosti na serveri %s"; +"directory_server_picker_title" = "Vybrať adresár"; + +// Media picker +"media_picker_title" = "Knižnica médií"; +"group_participants_invite_malformed_id" = "Chybné ID. Malo by to byť Matrix ID ako napríklad “@lokalnacast:domena“"; +"group_participants_invite_another_user" = "Vyhľadať / pozvať podľa ID používateľa alebo mena"; +"group_participants_invite_prompt_msg" = "Ste si istí, že chcete pozvať %@ do tejto skupiny?"; +"group_participants_remove_prompt_msg" = "Ste si istí, že chcete odstrániť %@ z tejto skupiny?"; +"group_participants_leave_prompt_msg" = "Ste si istí, že chcete opustiť skupinu?"; +"group_invitation_format" = "%@ vás pozval do tejto komunity"; +"room_notifs_settings_manage_notifications" = "Oznámenia môžete spravovať v %@"; +"room_notifs_settings_mentions_and_keywords" = "Iba zmienky a kľúčové slová"; + +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "Upozorniť ma na"; +"room_details_unset_main_address" = "Zrušiť nastavenie ako hlavnej adresy"; +"room_details_set_main_address" = "Nastaviť ako hlavnú adresu"; +"room_details_save_changes_prompt" = "Chcete uložiť zmeny?"; +"room_details_fail_to_update_room_communities" = "Nepodarilo sa aktualizovať príbuzné komunity"; +"room_details_fail_to_update_room_canonical_alias" = "Nepodarilo sa aktualizovať hlavnú adresu"; +"room_details_fail_to_remove_room_aliases" = "Nepodarilo sa odstrániť adresy miestností"; +"room_details_fail_to_add_room_aliases" = "Nepodarilo sa pridať nové adresy miestností"; +"room_details_fail_to_update_history_visibility" = "Nepodarilo sa aktualizovať viditeľnosť histórie"; +"room_details_fail_to_update_room_guest_access" = "Nepodarilo sa aktualizovať prístup hosťa do miestnosti"; +"room_details_fail_to_update_avatar" = "Nepodarilo sa aktualizovať fotografiu miestnosti"; +"room_details_fail_to_update_topic" = "Nepodarilo sa aktualizovať tému"; +"room_details_fail_to_update_room_name" = "Nepodarilo sa aktualizovať názov miestnosti"; +"room_details_advanced_e2e_encryption_disabled_for_dm" = "Šifrovanie tu nie je zapnuté."; +"room_details_advanced_e2e_encryption_disabled" = "V tejto miestnosti nie je zapnuté šifrovanie."; +"room_details_advanced_e2e_encryption_enabled_for_dm" = "Je tu zapnuté šifrovanie"; +"room_details_advanced_e2e_encryption_enabled" = "V tejto miestnosti je zapnuté šifrovanie"; +"room_details_advanced_enable_e2e_encryption" = "Povoliť šifrovanie (Pozor: nie je možné ho znova vypnúť!)"; +"room_details_flair_invalid_id_prompt_msg" = "%@ nie je platný identifikátor komunity"; +"room_details_new_flair_placeholder" = "Pridať nové ID komunity (napr. +foo%@)"; +"room_details_addresses_invalid_address_prompt_msg" = "%@ nie je platný formát pre alias"; +"room_details_new_address_placeholder" = "Pridať novú adresu (napr. #foo%@)"; +"room_details_no_local_addresses_for_dm" = "Táto nemá žiadne lokálne adresy"; +"room_details_no_local_addresses" = "Táto miestnosť nemá žiadne lokálne adresy"; +"room_details_history_section_prompt_msg" = "Zmeny týkajúce sa toho, kto môže čítať históriu, sa budú vzťahovať len na budúce správy v tejto miestnosti. Viditeľnosť existujúcej histórie zostane nezmenená."; +"room_details_history_section_prompt_title" = "Upozornenie o ochrane súkromia"; +"room_details_history_section_members_only_since_joined" = "Len členovia (odkedy vstúpili)"; +"room_details_history_section_members_only_since_invited" = "Len členovia (odkedy boli pozvaní)"; +"room_details_history_section_members_only" = "Len členovia (odkedy je táto voľba aktivovaná)"; +"room_details_history_section" = "Kto môže čítať históriu?"; +"room_details_access_section_directory_toggle_for_dm" = "Uverejniť v adresári miestností"; +"room_details_access_section_directory_toggle" = "Uverejniť túto miestnosť v adresári miestností"; +"room_details_access_section_no_address_warning" = "Ak chcete vytvoriť odkaz do miestnosti, musíte najprv nastaviť jej adresu"; +"room_details_access_section_anyone_for_dm" = "Ktokoľvek, kto pozná odkaz, vrátane hostí"; +"room_details_access_section_anyone_apart_from_guest" = "Ktokoľvek, kto pozná odkaz na miestnosť, okrem hostí"; +"room_details_access_section_anyone_apart_from_guest_for_dm" = "Ktokoľvek, kto pozná odkaz, okrem hostí"; +"room_details_access_section_anyone" = "Ktokoľvek, kto pozná odkaz na miestnosť, vrátane hostí"; +"room_details_access_section_invited_only" = "Iba ľudia, ktorí boli pozvaní"; +"room_details_access_section_for_dm" = "Kto má k tomuto prístup?"; +"room_details_access_section" = "Kto má prístup do tejto miestnosti?"; +"identity_server_settings_alert_error_invalid_identity_server" = "%@ nie je platný server totožností."; +"identity_server_settings_alert_disconnect" = "Odpojiť sa od servera totožností %@?"; +"identity_server_settings_place_holder" = "Zadajte server totožností"; + +// AuthenticatedSessionViewControllerFactory +"authenticated_session_flow_not_supported" = "Táto aplikácia nepodporuje mechanizmus overovania na vašom domovskom serveri."; +"security_settings_user_password_description" = "Potvrďte svoju totožnosť zadaním hesla k účtu"; +"security_settings_coming_soon" = "Prepáčte. Táto akcia zatiaľ nie je dostupná v systéme %@ iOS. Na jej nastavenie použite iného klienta Matrix. Systém %@ iOS to bude používať."; +"security_settings_complete_security_alert_title" = "Dokončiť zabezpečenie"; +"security_settings_complete_security_alert_message" = "Najprv by ste mali dokončiť zabezpečenie aktuálnej relácie."; +"security_settings_blacklist_unverified_devices_description" = "Overte všetky relácie používateľov, aby ste ich mohli označiť za dôveryhodné a odosielať im správy."; +"security_settings_secure_backup_info_valid" = "Táto relácia zálohuje vaše kľúče."; +"settings_show_NSFW_public_rooms" = "Zobraziť verejné miestnosti nevhodné do práce"; +"settings_identity_server_no_is" = "Nebol nastavený server totožností"; +"settings_discovery_three_pid_details_enter_sms_code_action" = "Zadajte aktivačný kód SMS"; +"settings_discovery_three_pid_details_information_email" = "Spravujte predvoľby pre túto e-mailovú adresu, ktorú môžu ostatní používatelia použiť na vaše objavenie a pozvanie do miestností. Pridávajte alebo odstraňujte e-mailové adresy v časti Účty."; +"settings_discovery_three_pid_details_title_email" = "Spravovať e-mail"; +"settings_discovery_accept_terms" = "Akceptovať podmienky servera totožnosti"; +"settings_discovery_no_identity_server" = "V súčasnosti nepoužívate server totožností. Ak chcete, aby vás existujúce kontakty mohli nájsť, pridajte si jeden."; +"settings_devices_description" = "Verejný názov relácie je viditeľný pre ľudí, s ktorými komunikujete"; +"settings_key_backup_delete_confirmation_prompt_msg" = "Ste si istý? Ak nie sú vaše kľúče správne zálohované, prídete o svoje zašifrované správy."; +"settings_key_backup_button_create" = "Začnite používať zálohovanie kľúčov"; +"settings_key_backup_info_trust_signature_invalid_device_unverified" = "Záloha má neplatný podpis z %@"; +"settings_key_backup_info_trust_signature_invalid_device_verified" = "Záloha má neplatný podpis z %@"; +"settings_key_backup_info_trust_signature_valid_device_unverified" = "Záloha má podpis z %@"; +"settings_key_backup_info_trust_signature_valid_device_verified" = "Zálohovanie má platný podpis z %@"; +"settings_key_backup_info_trust_signature_valid" = "Zálohovanie má platný podpis z tejto relácie"; +"settings_key_backup_info_trust_signature_unknown" = "Záloha má podpis z relácie s ID: %@"; +"settings_key_backup_info_progress_done" = "Všetky kľúče sú zálohované"; +"settings_key_backup_info_progress" = "Zálohovanie %@ kľúčov…"; +"settings_key_backup_info_not_valid" = "Táto relácia nezálohuje vaše kľúče, ale máte existujúcu zálohu, ktorú môžete obnoviť a pridať do budúcnosti."; +"settings_key_backup_info_valid" = "Táto relácia zálohuje vaše kľúče."; +"settings_key_backup_info_version" = "Verzia zálohy kľúča: %@"; +"settings_key_backup_info_signout_warning" = "Zálohujte si šifrovacie kľúče pred odhlásením, aby ste o ne neprišli."; +"settings_key_backup_info_none" = "Vaše kľúče sa z tejto relácie nezálohujú."; +"settings_key_backup_info" = "Šifrované správy sú zabezpečené end-to-end šifrovaním. Kľúče na čítanie týchto správ máte len vy a príjemca (príjemcovia)."; +"settings_add_3pid_password_message" = "Aby ste mohli pokračovať, prosím zadajte svoje heslo"; +"settings_labs_message_reaction" = "Reagujte na správy pomocou emoji"; +"settings_labs_e2e_encryption_prompt_message" = "Ak chcete dokončiť nastavenie šifrovania, musíte sa znova prihlásiť."; +"settings_contacts_enable_sync_description" = "Na prepojenie s vašimi kontaktmi sa použije váš server totožností a pomôže im vás nájsť."; +"settings_show_url_previews_description" = "Náhľady sa zobrazia len v nezašifrovaných miestnostiach."; + +// Recover from recovery key + +"key_backup_recover_from_recovery_key_info" = "Použite svoju bezpečnostný kľúč na odomknutie histórie zabezpečených správ"; From 4f1619e3e44f355a8f11471313a41ddccea64ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20I=2ESvindseth?= Date: Sat, 25 Dec 2021 21:01:11 +0000 Subject: [PATCH 070/109] Added translation using Weblate (Norwegian Nynorsk) --- Riot/Assets/nn.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) create mode 100644 Riot/Assets/nn.lproj/Vector.strings diff --git a/Riot/Assets/nn.lproj/Vector.strings b/Riot/Assets/nn.lproj/Vector.strings new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/Riot/Assets/nn.lproj/Vector.strings @@ -0,0 +1 @@ + From 70bd03fcbaec8bb9572784fceaf166b5b5036597 Mon Sep 17 00:00:00 2001 From: Whodiduexpect Date: Fri, 24 Dec 2021 20:20:33 +0000 Subject: [PATCH 071/109] Translated using Weblate (French) Currently translated at 100.0% (441 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/fr/ --- .../Assets/MatrixKitAssets.bundle/fr.lproj/MatrixKit.strings | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/fr.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/fr.lproj/MatrixKit.strings index bc1ae242a0..61a64b3212 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/fr.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/fr.lproj/MatrixKit.strings @@ -470,10 +470,11 @@ "attachment_small_with_resolution" = "Petit %@ (~%@)"; "attachment_size_prompt_message" = "Vous pouvez désactiver ceci dans les paramètres."; "attachment_size_prompt_title" = "Préciser la taille pour l’envoi"; -"auth_username_in_use" = ""; +"auth_username_in_use" = "Nom d’utilisateur déjà pris"; "auth_invalid_user_name" = "Nom d’utilisateur invalide"; "rename" = "Renommer"; "auth_reset_password_error_not_found" = "Non trouvé"; "auth_reset_password_error_unauthorized" = "Non autorisé"; "attachment_unsupported_preview_message" = "Ce type de fichier n’est pas pris en charge."; "attachment_unsupported_preview_title" = "Prévisualisation impossible"; +"room_displayname_all_other_members_left" = "%@ (Quitté)"; From 419d52b5a3ceea914273913d298f604ba4b85680 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Fri, 24 Dec 2021 23:00:14 +0000 Subject: [PATCH 072/109] Translated using Weblate (Slovak) Currently translated at 39.9% (176 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/sk/ --- .../MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings index d121b125c0..f8322bd416 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings @@ -241,3 +241,14 @@ "auth_username_in_use" = "Používateľské meno sa už používa"; "login_error_must_start_http" = "URL adresa musí začínať http[s]://"; "auth_reset_password_error_not_found" = "Nenájdené"; +"notice_unsupported_attachment" = "Nepodporovaná príloha: %@"; +"notice_encryption_enabled_unknown_algorithm" = "%1$@ zapol end-to-end šifrovanie (nerozpoznaný algoritmus %2$@)."; +"notice_encryption_enabled_ok" = "%@ zapol end-to-end šifrovanie."; +"notice_room_related_groups" = "Skupiny pridružené k tejto miestnosti sú: %@"; +"notice_room_aliases_for_dm" = "Aliasy sú: %@"; +"notice_room_aliases" = "Aliasy miestnosti sú: %@"; +"notice_room_power_level_acting_requirement" = "Minimálne úrovne oprávnenia, ktoré musí mať používateľ pred konaním, sú:"; +"notice_room_power_level_intro" = "Úrovne oprávnenia členov miestnosti sú:"; +"notice_room_power_level_intro_for_dm" = "Úrovne oprávnenia členov sú:"; +"notice_room_join_rule_public_by_you_for_dm" = "Zverejnili ste ju."; +"notice_room_join_rule_public_by_you" = "Túto miestnosť ste zverejnili."; From 42ad02ad91d24488c996ab2e106302e8098465b1 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Mon, 27 Dec 2021 20:21:01 +0000 Subject: [PATCH 073/109] Translated using Weblate (Slovak) Currently translated at 72.1% (318 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/sk/ --- .../sk.lproj/MatrixKit.strings | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings index f8322bd416..8a4796205f 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings @@ -252,3 +252,178 @@ "notice_room_power_level_intro_for_dm" = "Úrovne oprávnenia členov sú:"; "notice_room_join_rule_public_by_you_for_dm" = "Zverejnili ste ju."; "notice_room_join_rule_public_by_you" = "Túto miestnosť ste zverejnili."; +"notification_settings_global_info" = "Nastavenia oznámení sa ukladajú do vášho používateľského účtu a sú zdieľané medzi všetkými klientmi, ktorí ich podporujú (vrátane oznámení na ploche).\n\nPravidlá sa uplatňujú v poradí; prvé pravidlo, ktoré sa zhoduje, určuje výsledok správy.\nTakže: Oznámenia o správe sú dôležitejšie ako oznámenia na miestnosť, ktoré sú dôležitejšie ako oznámenia na odosielateľa.\nPri viacerých pravidlách rovnakého druhu má prednosť to, ktoré sa zhoduje ako prvé v zozname."; + +// contacts list screen +"invitation_message" = "Rád by som sa s vami porozprával pomocou Matrixu. Viac informácií nájdete na webovej stránke http://matrix.org."; +"notice_room_history_visible_to_members_from_joined_point_by_you" = "Zviditeľnili ste budúcu históriu miestnosti pre všetkých členov miestnosti, od okamihu ich vstupu."; +"notice_room_history_visible_to_members_from_invited_point_by_you_for_dm" = "Zviditeľnili ste budúce správy pre všetkých, od okamihu pozvania."; +"notice_room_history_visible_to_members_from_invited_point_by_you" = "Zviditeľnili ste budúcu históriu miestnosti pre všetkých členov miestnosti, od okamihu pozvania."; +"notice_room_history_visible_to_members_by_you_for_dm" = "Zviditeľnili ste budúce správy pre všetkých členov miestnosti."; +"notice_room_history_visible_to_members_by_you" = "Zviditeľnili ste budúcu históriu miestnosti pre všetkých jej členov."; +"notice_room_history_visible_to_anyone_by_you" = "Sprístupnili ste budúcu históriu miestnosti každému."; +"notice_encryption_enabled_unknown_algorithm_by_you" = "Zapli ste end-to-end šifrovanie (nerozpoznaný algoritmus %@)."; +"notice_encryption_enabled_ok_by_you" = "Zapli ste end-to-end šifrovanie."; +"notice_room_created_by_you_for_dm" = "Vstúpili ste."; +"notice_room_created_by_you" = "Vytvorili ste a nastavili ste miestnosť."; +"notice_profile_change_redacted_by_you" = "Aktualizovali ste svoj profil %@"; +"notice_room_topic_removed_by_you" = "Odstránili ste tému"; +"notice_room_name_removed_by_you_for_dm" = "Odstránili ste názov"; +"notice_room_name_removed_by_you" = "Odstránili ste názov miestnosti"; +"notice_conference_call_request_by_you" = "Požiadali ste o VoIP konferenciu"; +"notice_declined_video_call_by_you" = "Odmietli ste hovor"; +"notice_ended_video_call_by_you" = "Ukončili ste hovor"; +"notice_room_name_changed_by_you_for_dm" = "Zmenili ste svoje meno na %@."; +"notice_room_name_changed_by_you" = "Zmenili ste názov miestnosti na %@."; +"notice_topic_changed_by_you" = "Zmenili ste tému na \"%@\"."; +"notice_display_name_removed_by_you" = "Odstránili ste svoje zobrazované meno"; +"notice_display_name_changed_from_by_you" = "Zmenili ste si zobrazované meno z %@ na %@"; +"notice_display_name_set_by_you" = "Nastavili ste si zobrazované meno na %@"; +"notice_avatar_url_changed_by_you" = "Zmenili ste si obrázok v profile"; +"notice_room_withdraw_by_you" = "Odvolali ste pozvánku pre %@"; +"notice_room_ban_by_you" = "Zakázali ste používateľa %@"; +"notice_room_unban_by_you" = "Zrušili ste zákaz pre %@"; +"notice_room_kick_by_you" = "Vylúčili ste používateľa %@"; +"notice_room_reject_by_you" = "Odmietli ste pozvanie"; +"notice_room_third_party_revoked_invite_by_you_for_dm" = "Odvolali ste pozvánku pre %@"; +"notice_room_third_party_revoked_invite_by_you" = "Odvolali ste pozvánku pre %@ do miestnosti"; +"notice_room_third_party_registered_invite_by_you" = "Prijali ste pozvanie do %@"; +"notice_room_third_party_invite_by_you_for_dm" = "Pozvali ste používateľa %@"; +"notice_room_third_party_invite_by_you" = "Odoslali ste pozvánku používateľovi %@ do miestnosti"; +"notice_room_invite_you" = "%@ vás pozval"; + +// Notice Events with "You" +"notice_room_invite_by_you" = "Pozvali ste %@"; +"notice_conference_call_finished" = "Skončila sa VoIP konferencia"; +"notice_conference_call_started" = "Začala sa VoIP konferencia"; +"notice_conference_call_request" = "%@ požiadal/a o VoIP konferenciu"; +"notice_declined_video_call" = "%@ odmietol hovor"; +"notice_ended_video_call" = "%s ukončil/a hovor"; +"notice_room_name_changed_for_dm" = "%@ zmenil/a svoje meno na %@."; +"notice_room_name_changed" = "%@ zmenil názov miestnosti na %@."; +"notice_topic_changed" = "%@ zmenil tému na \"%@\"."; +"notice_display_name_removed" = "%@ odstránil svoje zobrazované meno"; +"notice_display_name_changed_from" = "%@ zmenil/a svoje zobrazované meno z %@ na %@"; +"notice_display_name_set" = "%@ nastavil/a svoje zobrazované meno na %@"; +"notice_avatar_url_changed" = "%@ zmenil/a svoj obrázok"; +"notice_room_reason" = ". Dôvod: %@"; +"notice_room_withdraw" = "%@ odvolal pozvanie od %@"; +"notice_room_ban" = "%@ zakázal používateľa %@"; +"notice_room_unban" = "%@ zrušil zákaz pre %@"; +"notice_room_reject" = "%@ odmietol pozvanie"; +"notice_room_third_party_revoked_invite_for_dm" = "%@ odvolal pozvánku pre %@"; +"notice_room_third_party_revoked_invite" = "%@ odvolal pozvánku pre %@, aby sa pripojil k miestnosti"; +"notice_room_third_party_registered_invite" = "%@ prijal pozvanie do %@"; +"notice_room_third_party_invite_for_dm" = "%@ pozval %@"; +"notice_room_third_party_invite" = "%@ poslal pozvánku %@ do miestnosti"; + +/* -*- + Automatic localization for en + + The following key/value pairs were extracted from the android i18n file: + /matrix-sdk/src/main/res/values/strings.xml. +*/ + +"notice_room_invite" = "%@ pozval %@"; + +// Language picker +"language_picker_title" = "Vyberte jazyk"; + +// Country picker +"country_picker_title" = "Vyberte krajnu"; +"microphone_access_not_granted_for_voice_message" = "Hlasové správy vyžadujú prístup k mikrofónu, ale %@ nemá povolenie na jeho používanie"; +"local_contacts_access_discovery_warning" = "Ak chcete vyhľadať kontakty, ktoré už používajú Matrix, %@ môže odoslať e-mailové adresy a telefónne čísla z vášho adresára na zvolený server identity Matrix. Ak je to podporované, osobné údaje sa pred odoslaním zahašujú - ďalšie podrobnosti nájdete v zásadách ochrany osobných údajov vášho servera totožností."; +"local_contacts_access_discovery_warning_title" = "Vyhľadávanie používateľov"; +"local_contacts_access_not_granted" = "Zisťovanie používateľov z lokálnych kontaktov vyžaduje prístup k vašim kontaktom, ale %@ nemá oprávnenie na ich používanie"; +"microphone_access_not_granted_for_call" = "Hovory vyžadujú prístup k mikrofónu, ale %@ nemá povolenie na jeho používanie"; + +// Permissions +"camera_access_not_granted_for_call" = "Videohovory vyžadujú prístup ku kamere, ale %@ nemá povolenie na jej používanie"; +"ssl_homeserver_url" = "URL adresa domovského servera: %@"; +"network_error_not_reachable" = "Skontrolujte prosím pripojenie k sieti"; +"not_supported_yet" = "Zatiaľ nie je podporované"; +"error_common_message" = "Vyskytla sa chyba. Skúste to prosím neskôr."; +"e2e_passphrase_not_match" = "Prístupové frázy sa musia zhodovať"; +"e2e_passphrase_empty" = "Prístupová fráza nesmie byť prázdna"; +"e2e_passphrase_too_short" = "Príliš krátka prístupová fráza (Musí mať minimálne %d znakov)"; +"e2e_export_prompt" = "Tento proces umožňuje exportovať kľúče od správ, ktoré ste prijali v zašifrovaných miestnostiach, do lokálneho súboru. Tento súbor potom budete môcť v budúcnosti importovať do iného klienta Matrix, takže tento klient bude môcť tieto správy tiež dešifrovať.\nExportovaný súbor umožní komukoľvek, kto si ho môže prečítať, dešifrovať všetky zašifrované správy, ktoré vidíte, preto by ste mali dbať na jeho bezpečnosť."; + +// E2E export +"e2e_export_room_keys" = "Exportovať kľúče miestností"; +"e2e_import_prompt" = "Tento proces umožňuje importovať šifrovacie kľúče, ktoré ste predtým exportovali z iného klienta Matrix. Potom budete môcť dešifrovať všetky správy, ktoré mohol dešifrovať iný klient.\nExportovaný súbor je chránený prístupovou frázou. Tu by ste mali zadať prístupovú frázu, aby ste súbor dešifrovali."; + +// E2E import +"e2e_import_room_keys" = "Importovať kľúče miestností"; +"search_searching" = "Prebieha vyhľadávanie..."; +"attachment_unsupported_preview_message" = "Tento typ súboru nie je podporovaný."; +"attachment_unsupported_preview_title" = "Nie je možné zobraziť náhľad"; +"attachment_e2e_keys_file_prompt" = "Tento súbor obsahuje šifrovacie kľúče exportované z klienta Matrix.\nChcete zobraziť obsah súboru alebo importovať kľúče, ktoré obsahuje?"; +"attachment_multiselection_size_prompt" = "Chcete odoslať obrázok ako:"; +"attachment_cancel_upload" = "Zrušiť nahrávanie?"; +"attachment_cancel_download" = "Zrušiť sťahovanie?"; +"attachment_large_with_resolution" = "Veľká %@ (~%@)"; +"attachment_medium_with_resolution" = "Stredná %@ (~%@)"; +"attachment_small_with_resolution" = "Malá %@ (~%@)"; +"attachment_large" = "Veľká (~%@)"; +"attachment_original" = "Skutočná veľkosť (%@)"; +"attachment_size_prompt_message" = "Túto funkciu môžete kedykoľvek vypnúť v nastaveniach."; + +// Attachment +"attachment_size_prompt" = "Chcete odoslať ako:"; +"room_member_power_level_prompt" = "Túto zmenu nebudete môcť vrátiť späť, pretože tomuto používateľovi udeľujete rovnakú úroveň moci, akú máte vy.\nSte si istí?"; + +// Room members +"room_member_ignore_prompt" = "Ste si istí, že chcete skryť všetky správy od tohto používateľa?"; +"message_reply_to_sender_sent_a_file" = "poslal súbor."; +"message_reply_to_sender_sent_a_voice_message" = "poslal/a zvukovú správu."; +"message_reply_to_sender_sent_an_audio_file" = "poslal/a zvukový súbor."; +"message_reply_to_sender_sent_a_video" = "poslal video."; + +// Reply to message +"message_reply_to_sender_sent_an_image" = "poslal obrázok."; +"room_left" = "Opustili ste miestnosť"; +"room_error_timeline_event_not_found" = "Aplikácia sa pokúšala načítať konkrétny bod na časovej osi tejto miestnosti, ale nedokázala ho nájsť"; +"room_error_timeline_event_not_found_title" = "Nepodarilo sa načítať pozíciu na časovej osi"; +"room_error_cannot_load_timeline" = "Nepodarilo sa načítať časovú os"; +"room_error_topic_edition_not_authorized" = "Nie ste oprávnený upraviť tému tejto miestnosti"; +"room_error_name_edition_not_authorized" = "Nemáte oprávnenie upraviť názov tejto miestnosti"; +"room_error_join_failed_empty_room" = "V súčasnosti nie je možné znovu vstúpiť do prázdnej miestnosti."; +"room_error_join_failed_title" = "Nepodarilo sa vstúpiť do miestnosti"; + +// Room +"room_please_select" = "Prosím, vyberte si miestnosť"; +"room_creation_participants_placeholder" = "(napr. @fero:domovskyserver1; @jano:domovskyserver2...)"; +"room_creation_alias_placeholder_with_homeserver" = "(napr. #foo%@)"; +"room_creation_alias_placeholder" = "(napr. #foo:napriklad.sk)"; +"room_creation_alias_title" = "Alias miestnosti:"; +"account_error_push_not_allowed" = "Oznámenia nie sú povolené"; +"account_error_msisdn_wrong_description" = "Zdá sa, že toto nie je platné telefónne číslo"; +"account_error_msisdn_wrong_title" = "Neplatné telefónne číslo"; +"account_error_email_wrong_description" = "Toto nevyzerá ako platná e-mailová adresa"; +"account_error_email_wrong_title" = "Neplatná e-mailová adresa"; +"account_error_matrix_session_is_not_opened" = "Relácia Matrix nie je otvorená"; +"account_error_picture_change_failed" = "Zmena obrázka sa nepodarila"; +"account_error_display_name_change_failed" = "Zmena zobrazovaného mena sa nepodarila"; +"account_msisdn_validation_error" = "Nie je možné overiť telefónne číslo."; +"account_msisdn_validation_message" = "Odoslali sme SMS s aktivačným kódom. Zadajte prosím tento kód nižšie."; +"account_email_validation_error" = "Nie je možné overiť e-mailovú adresu. Skontrolujte svoj e-mail a kliknite na odkaz, ktorý obsahuje. Po vykonaní tohto kroku kliknite na tlačidlo Pokračovať"; +"account_email_validation_message" = "Prosím, skontrolujte svoj e-mail a kliknite na odkaz, ktorý obsahuje. Po dokončení tohto kroku kliknite na tlačidlo Pokračovať."; +"room_event_encryption_verify_message" = "Ak chcete overiť, či je táto relácia dôveryhodná, kontaktujte jej vlastníka iným spôsobom (napr. osobne alebo telefonicky) a opýtajte sa ho, či kľúč, ktorý vidí v nastaveniach používateľa pre túto reláciu, sa zhoduje s nižšie uvedeným kľúčom:\n\n\tNázov relácie: %@\n\tID relácie: %@\n\tKľúč relácie: %@\n\nAk sa zhoduje, stlačte tlačidlo overiť nižšie. Ak nie, potom túto reláciu zachytáva niekto iný a pravdepodobne budete chcieť namiesto toho stlačiť tlačidlo čiernej listiny.\n\nV budúcnosti bude tento proces overovania sofistikovanejší."; +"room_event_encryption_info_block" = "Pridať na čiernu listinu"; +"room_event_encryption_info_unblock" = "Odstrániť z čiernej listiny"; +"room_event_encryption_info_device" = "\nInformácie o relácii odosielateľa\n"; +"room_event_encryption_info_event_identity_key" = "Identifikačný kľúč Curve25519\n"; + +// Encryption information +"room_event_encryption_info_title" = "Informácie o end-to-end šifrovaní\n\n"; +"device_details_last_seen_format" = "%@ @ %@\n"; +"settings_enable_push_notifications" = "Zapnúť push oznámenia"; +"settings_enable_inapp_notifications" = "Zapnúť oznámenia v aplikácii"; +"room_displayname_more_than_two_members" = "%@ a %@ ďalší"; +"room_displayname_two_members" = "%@ a %@"; +"notice_crypto_error_unknown_inbound_session_id" = "Relácia odosielateľa nám neposlala kľúče pre túto správu."; +"notice_crypto_unable_to_decrypt" = "** Nepodarilo sa dešifrovať: %@ **"; +"notice_room_history_visible_to_members_from_invited_point" = "%@ zviditeľnil/a budúcu históriu miestnosti pre všetkých jej členov od okamihu, keď sú pozvaní."; +"notice_room_history_visible_to_members_for_dm" = "%@ zmenil/a nastavenie, aby budúce správy boli viditeľné pre všetkých členov miestnosti."; +"notice_room_history_visible_to_members" = "%@ zviditeľnil/a budúcu históriu miestnosti pre všetkých členov miestnosti."; +"notice_room_history_visible_to_anyone" = "%@ zviditeľnil/a budúcu históriu miestnosti pre každého."; From aab7ea4015521c8d33fcdde5522ea95b67c65f47 Mon Sep 17 00:00:00 2001 From: jelv Date: Mon, 27 Dec 2021 13:20:03 +0000 Subject: [PATCH 074/109] Translated using Weblate (Dutch) Currently translated at 100.0% (1375 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ --- Riot/Assets/nl.lproj/Vector.strings | 45 ++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index 7f40726407..65b0d41c68 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -1633,7 +1633,7 @@ "contacts_address_book_permission_denied_alert_message" = "Om contacten in te schakelen, ga naar uw apparaatinstellingen."; "contacts_address_book_permission_denied_alert_title" = "Contacten uitgeschakeld"; "poll_edit_form_add_option" = "Optie toevoegen"; -"poll_edit_form_option_number" = "Optie %d"; +"poll_edit_form_option_number" = "Optie %lu"; "poll_edit_form_create_options" = "Opties maken"; "poll_edit_form_input_placeholder" = "Schrijf iets"; "poll_edit_form_question_or_topic" = "Vraag of onderwerp"; @@ -1647,3 +1647,46 @@ "share_extension_low_quality_video_title" = "Video zal in lage kwaliteit worden verstuurd"; "settings_discovery_accept_terms" = "Identiteitsserver-voorwaarden aanvaarden"; "settings_about" = "OVER"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_not_closed_subtitle" = "Probeer het opnieuw"; +"poll_timeline_not_closed_title" = "Sluiten van de poll mislukt"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_timeline_vote_not_registered_subtitle" = "Sorry, uw stem is niet geregistreerd. Probeer het opnieuw"; +"poll_timeline_vote_not_registered_title" = "Stem niet geregistreerd"; +"poll_timeline_total_final_results" = "Uitslag gebaseerd op %lu stemmen"; +"poll_timeline_total_final_results_one_vote" = "Uitslag gebaseerd op 1 stem"; +"poll_timeline_total_votes_not_voted" = "%lu stemmen uitgebracht. Stem om de resultaten te zien"; +"poll_timeline_total_one_vote_not_voted" = "1 stem uitgebracht. Stem om de resultaten te zien"; +"poll_timeline_total_votes" = "%lu stemmen uitgebracht"; +"poll_timeline_total_one_vote" = "1 stem uitgebracht"; +"poll_timeline_total_no_votes" = "Geen stemmen uitgebracht"; +"poll_timeline_votes_count" = "%lu stemmen"; +"poll_timeline_one_vote" = "1 stem"; +"poll_edit_form_post_failure_action" = "OK"; +"poll_edit_form_post_failure_subtitle" = "Probeer het opnieuw"; +"poll_edit_form_post_failure_title" = "Poll plaatsen mislukt"; +"analytics_prompt_stop" = "Delen stoppen"; +"analytics_prompt_yes" = "Ja, dat is prima"; +"analytics_prompt_not_now" = "Niet nu"; +"analytics_prompt_point_3" = "U kunt dit op elk moment uitzetten in de instellingen"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Wij delen geen informatie met derden"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "We registreren of profileren geen accountgegevens"; +"analytics_prompt_terms_link_upgrade" = "hier"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Lees al onze voorwaarden %@. Is dit akkoord?"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "U kunt al onze voorwaarden %@ lezen."; +"analytics_prompt_terms_link_new_user" = "hier"; +"analytics_prompt_message_upgrade" = "U heeft eerder toestemming gegeven om anonieme gebruiksgegevens met ons te delen. Om beter te begrijpen hoe mensen meerdere apparaten gebruiken, genereren we nu een willekeurige identificatiecode die door uw apparaten wordt gedeeld."; +"analytics_prompt_message_new_user" = "Help ons bij het identificeren van problemen en het verbeteren van Element door anonieme gebruiksgegevens te delen. Om te begrijpen hoe mensen meerdere apparaten gebruiken genereren we een willekeurige identificatie die we verspreiden over uw apparaten."; + +// Analytics +"analytics_prompt_title" = "Help %@ verbeteren"; +"settings_analytics_and_crash_data" = "Crash en analytische data versturen"; +"settings_labs_enabled_polls" = "Polls"; +"room_event_action_end_poll" = "Poll sluiten"; +"room_event_action_remove_poll" = "Poll verwijderen"; +"accessibility_button_label" = "knop"; +"enable" = "Inschakelen"; From feeee909b5b08e34172623e71d9f35d0cb485feb Mon Sep 17 00:00:00 2001 From: libexus Date: Wed, 29 Dec 2021 13:37:36 +0000 Subject: [PATCH 075/109] Translated using Weblate (German) Currently translated at 99.8% (1373 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 3272240ddf..f0fb19fc27 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -206,7 +206,7 @@ "settings_user_settings" = "NUTZER-EINSTELLUNGEN"; "settings_notifications_settings" = "BENACHRICHTIGUNGS-EINSTELLUNGEN"; "settings_ignored_users" = "IGNORIERTE NUTZER"; -"settings_contacts" = "LOKALE KONTAKTE"; +"settings_contacts" = "GERÄTEKONTAKTE"; "settings_advanced" = "ERWEITERT"; "settings_other" = "Weiteres"; "settings_devices" = "SITZUNGEN"; @@ -1001,7 +1001,7 @@ "skip" = "Überspringen"; "security_settings_crosssigning_info_not_bootstrapped" = "Quersignierung ist bisher nicht konfiguriert."; "room_member_power_level_admin_in" = "Admin in %@"; -"room_member_power_level_moderator_in" = "Moderationsrechte in %@"; +"room_member_power_level_moderator_in" = "Mod in %@"; "room_member_power_level_custom_in" = "Benutzerdefiniert (%@) in %@"; "room_member_power_level_short_admin" = "Admin"; "room_member_power_level_short_moderator" = "Mod"; @@ -1487,7 +1487,7 @@ "settings_contacts_enable_sync" = "Finde deine Kontakte"; "space_home_show_all_rooms" = "Alle Räume anzeigen"; "service_terms_modal_information_description_integration_manager" = "Ein Integrationsmanager erlaubt dir, externe Funktionen hinzuzufügen."; -"service_terms_modal_information_description_identity_server" = "Der Identitätsserver sucht anhand der Telefonnummern und E-Mails in deinen Kontakten, ob diese einen Matrix-Account haben."; +"service_terms_modal_information_description_identity_server" = "Der Identitätsserver sucht anhand der Telefonnummern und E-Mails deiner Kontakte nach ihren Matrix-Accounts."; "service_terms_modal_information_title_integration_manager" = "Integrationsmanager"; // Alert explaining what an identity server / integration manager is. @@ -1561,3 +1561,9 @@ "share_extension_low_quality_video_title" = "Das Video wird in niedriger Qualität gesendet werden"; "analytics_prompt_stop" = "Teilen beenden"; "analytics_prompt_not_now" = "Nicht jetzt"; +"analytics_prompt_yes" = "Das ist Okay"; +"analytics_prompt_point_3" = "Du kannst dies jederzeit in den Einstellungen deaktivieren"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Wir erfassen und analysieren keine Accountdaten"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Alle unsere Bedingungen lesen %@. Bist du damit einverstanden?"; From 2f7f7676119a51f79665fa82da4b4733d2d1f8cc Mon Sep 17 00:00:00 2001 From: Szimszon Date: Sun, 26 Dec 2021 21:21:30 +0000 Subject: [PATCH 076/109] Translated using Weblate (Hungarian) Currently translated at 99.9% (1374 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ --- Riot/Assets/hu.lproj/Vector.strings | 44 ++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 0d8050079b..2ade108e64 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -1533,7 +1533,7 @@ "space_home_show_all_rooms" = "Minden szoba megjelenítése"; "room_event_action_forward" = "Továbbítás"; "poll_edit_form_add_option" = "Lehetőség hozzáadása"; -"poll_edit_form_option_number" = "%d lehetőség"; +"poll_edit_form_option_number" = "%lu lehetőség"; "poll_edit_form_create_options" = "Lehetőségek hozzáadása"; "poll_edit_form_input_placeholder" = "Írjon valamit"; "poll_edit_form_question_or_topic" = "Kérdés vagy téma"; @@ -1547,3 +1547,45 @@ "share_extension_low_quality_video_title" = "Alacsony minőségű videó lesz elküldve"; "settings_discovery_accept_terms" = "Azonosítási Szolgáltatás felhasználási feltételeinek elfogadása"; "settings_about" = "NÉVJEGY"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_not_closed_subtitle" = "Kérlek próbáld újra"; +"poll_timeline_not_closed_title" = "Nem sikerült a szavazás lezárása"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_timeline_vote_not_registered_subtitle" = "Sajnos a szavazatod nem lett rögzítve. Kérlek ismételd meg újra"; +"poll_timeline_vote_not_registered_title" = "Szavazás nem sikerült"; +"poll_timeline_total_final_results" = "Végeredmény %lu szavazat alapján"; +"poll_timeline_total_final_results_one_vote" = "Eredmény 1 szavazat alapján"; +"poll_timeline_total_votes_not_voted" = "%lu szavazatot adtak le. Szavazz az eredmény megtekintéséhez"; +"poll_timeline_total_one_vote_not_voted" = "1 szavazatot adtak le. Szavazz az eredmény megtekintéséhez"; +"poll_timeline_total_votes" = "%lu szavazatot adtak le"; +"poll_timeline_total_one_vote" = "1 szavazatot adtak le"; +"poll_timeline_total_no_votes" = "Nem adtak le szavazatot"; +"poll_timeline_votes_count" = "%lu szavazat"; +"poll_timeline_one_vote" = "1 szavazat"; +"poll_edit_form_post_failure_action" = "OK"; +"poll_edit_form_post_failure_subtitle" = "Kérlek próbáld újra"; +"poll_edit_form_post_failure_title" = "A szavazást nem sikerült beküldeni"; +"analytics_prompt_stop" = "Megosztás megállítása"; +"analytics_prompt_yes" = "Igen, rendben van"; +"analytics_prompt_not_now" = "Nem most"; +"analytics_prompt_point_3" = "Ezt bármikor kikapcsolhatod a beállításokban"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Nem osztjuk meg az információt harmadik féllel"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Nem küldünk és nem profilozunk semmilyen fiók adatot"; +"analytics_prompt_terms_link_upgrade" = "itt"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Olvasd el minden feltételünket: %@. Rendben?"; +"analytics_prompt_terms_link_new_user" = "itt"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Az összes feltételünket elolvashatod itt: %@."; +"analytics_prompt_message_new_user" = "Segíts észrevennünk a hibákat, és jobbá tenni az Element-et a névtelen használati adatok küldése által. Ahhoz, hogy megértsük, hogyan használnak a felhasználók egyszerre több eszközt, egy véletlenszerű azonosítót generálunk, ami az eszközeid között meg lesz osztva."; + +// Analytics +"analytics_prompt_title" = "Segíts jobbá tenni %@"; +"settings_analytics_and_crash_data" = "Összeomlás és analitikai adatok küldése"; +"settings_labs_enabled_polls" = "Szavazások"; +"room_event_action_end_poll" = "Szavazás lezárása"; +"room_event_action_remove_poll" = "Szavazás törlése"; +"accessibility_button_label" = "gomb"; +"enable" = "Engedélyezés"; From 6c323633714636dbe2ba5ac674d29dbbe114a2be Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Tue, 28 Dec 2021 10:10:24 +0000 Subject: [PATCH 077/109] Translated using Weblate (Albanian) Currently translated at 99.7% (1371 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ --- Riot/Assets/sq.lproj/Vector.strings | 62 ++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index 75f1fc60e7..bca45030c5 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -222,7 +222,7 @@ "settings_ignored_users" = "PËRDORUES TË SHPËRFILLUR"; "settings_contacts" = "KONTAKTE PAJISJEJE"; "settings_advanced" = "TË MËTEJSHME"; -"settings_other" = "TË TJERA"; +"settings_other" = "Tjetër"; "settings_devices" = "SESIONE"; "settings_cryptography" = "KRIPTOGRAFI"; "settings_sign_out" = "Dilni"; @@ -1519,3 +1519,63 @@ "contacts_address_book_permission_denied_alert_message" = "Që të aktivizoni kontakte, kaloni te rregullimet e pajisjes tua."; "contacts_address_book_permission_denied_alert_title" = "Kontaktet u çaktivizuan"; "space_home_show_all_rooms" = "Shfaqi krejt dhomat"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_not_closed_subtitle" = "Ju lutemi, riprovoni"; +"poll_timeline_not_closed_title" = "S’u arrit të përfundohej pyetësori"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_timeline_vote_not_registered_subtitle" = "Na ndjeni, vota juaj s’u regjistrua, ju lutemi, riprovoni"; +"poll_timeline_vote_not_registered_title" = "Votë e paregjistruar"; +"poll_timeline_total_final_results" = "Rezultati përfundimtar, bazua në %lu votë"; +"poll_timeline_total_final_results_one_vote" = "Rezultati përfundimtar, bazua në 1 votë"; +"poll_timeline_total_votes_not_voted" = "%lu vota të hedhura. Që të shihni përfundimet, votoni"; +"poll_timeline_total_one_vote_not_voted" = "1 votë e hedhur. Që të shihni përfundimet, votoni"; +"poll_timeline_total_votes" = "%lu vota të hedhura"; +"poll_timeline_total_one_vote" = "1 votë e hedhur"; +"poll_timeline_total_no_votes" = "S’u votua gjë"; +"poll_timeline_votes_count" = "%lu vota"; +"poll_timeline_one_vote" = "1 votë"; +"poll_edit_form_post_failure_action" = "OK"; +"poll_edit_form_post_failure_subtitle" = "Ju lutemi, riprovoni"; +"poll_edit_form_post_failure_title" = "S’u arrit të postohej anketimi"; +"poll_edit_form_add_option" = "Shtoni mundësi"; +"poll_edit_form_option_number" = "Mundësia %lu"; +"poll_edit_form_create_options" = "Krijo mundësi"; +"poll_edit_form_input_placeholder" = "Shkruani diçka!"; +"poll_edit_form_question_or_topic" = "Pyetje ose temë"; +"poll_edit_form_poll_question_or_topic" = "Pyetje ose temë pyetësori"; + +// Mark: - Polls + +"poll_edit_form_create_poll" = "Krijoni anketim"; +"share_extension_send_now" = "Dërgoje tani"; +"share_extension_low_quality_video_message" = "Dërgojeni në %@. për cilësi më të mirë, ose dërgojeni në cilësi të ulët si më poshtë."; +"share_extension_low_quality_video_title" = "Videoja do të dërgohet në cilësi të ulët"; +"analytics_prompt_stop" = "Resht së ndari"; +"analytics_prompt_yes" = "Po, s’ka problem"; +"analytics_prompt_not_now" = "Jo tani"; +"analytics_prompt_point_3" = "Këtë mund të çaktivizoni në çfarëdo kohe, që nga rregullimet"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Nuk u japin hollësi palëve të treta"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Nuk regjistrojmë ose profilizojmë ndonjë të dhënë llogarie"; +"analytics_prompt_terms_link_upgrade" = "këtu"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Lexoni krejt kushtet tona %@. Në rregull?"; +"analytics_prompt_terms_link_new_user" = "këtu"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Mund të lexoni krejt kushtet tona %@."; +"analytics_prompt_message_upgrade" = "Keni pranuar më herët të ndani me ne të dhëna anonime përdorimi. Tani, që të na ndihmoni të kuptojmë se si njerëzit përdorin pajisje të shumta, do të prodhojmë një identifikues kuturu, të përbashkët për pajisjet tuaja."; +"analytics_prompt_message_new_user" = "Ndihmonani të identifikojmë probleme dhe të përmirësojmë Element-in, duke ndarë me ne të dhëna anonime përdorimi. Për të kuptuar se si i përdorin njerëzit disa pajisje njëherësh, do të prodhojmë një identifikues kuturu, të përbashkët për pajisjet tuaja."; + +// Analytics +"analytics_prompt_title" = "Ndihmoni të përmirësohet %@"; +"settings_discovery_accept_terms" = "Pranoni Kushte Shërbyesi Identitetesh"; +"settings_analytics_and_crash_data" = "Dërgoni të dhëna vithisjesh dhe analitike"; +"settings_labs_enabled_polls" = "Pyetësorë"; +"settings_about" = "MBI"; +"room_event_action_forward" = "Përpara"; +"room_event_action_end_poll" = "Përfundoje pyetësorin"; +"room_event_action_remove_poll" = "Hiqe pyetësorin"; +"accessibility_button_label" = "kopsë"; +"open" = "Hapur"; +"enable" = "Aktivizoje"; From 45dad1363a61b5b8c3dc643fd44c53a7aa1be34d Mon Sep 17 00:00:00 2001 From: DUCKCHI Date: Tue, 28 Dec 2021 09:08:05 +0000 Subject: [PATCH 078/109] Translated using Weblate (Korean) Currently translated at 71.9% (989 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ko/ --- Riot/Assets/ko.lproj/Vector.strings | 61 +++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/Riot/Assets/ko.lproj/Vector.strings b/Riot/Assets/ko.lproj/Vector.strings index 0baff08506..a88ae30cd9 100644 --- a/Riot/Assets/ko.lproj/Vector.strings +++ b/Riot/Assets/ko.lproj/Vector.strings @@ -1068,3 +1068,64 @@ "contacts_address_book_permission_denied_alert_message" = "연락처를 사용하려면, 설정으로 이동하십시오."; "contacts_address_book_permission_denied_alert_title" = "연락처 사용 안 함"; "rooms_empty_view_title" = "방"; + +// AuthenticatedSessionViewControllerFactory +"authenticated_session_flow_not_supported" = "이 앱은 해당 홈서버의 인증 구조를 지원하지 않습니다."; +"security_settings_user_password_description" = "신원을 확인하기 위해 계정 비밀번호를 입력해주세요."; +"event_formatter_widget_removed_by_you" = "위젯을 제거함 : %@"; + +// Events formatter with you +"event_formatter_widget_added_by_you" = "위젯을 추가함 : %@"; +"event_formatter_group_call_leave" = "떠나기"; +"event_formatter_group_call_join" = "들어가기"; +"event_formatter_group_call" = "그룹 전화"; +"event_formatter_call_end_call" = "통화 종료"; +"event_formatter_call_retry" = "재시도"; +"event_formatter_call_decline" = "거부"; +"event_formatter_call_back" = "통화 재시도"; +"event_formatter_call_connection_failed" = "연결 실패"; +"event_formatter_call_missed_voice" = "음성 통화 부재중"; +"event_formatter_call_missed_video" = "영상 통화 부재중"; +"event_formatter_call_you_declined" = "통화 거부"; +"event_formatter_call_active_voice" = "음성 통화 활성"; +"event_formatter_call_active_video" = "영상 통화 활성"; +"event_formatter_call_incoming_video" = "영상 통화 수신"; +"event_formatter_call_incoming_voice" = "음성 통화 수신"; +"event_formatter_call_has_ended_with_time" = "통화 끊김 %@"; +"event_formatter_call_has_ended" = "통화 끊기"; +"event_formatter_call_ringing" = "통화중…"; +"event_formatter_call_connecting" = "연결중…"; +"room_notifs_settings_encrypted_room_notice" = "암호화된 방에서는 멘션 및 키워드 알림이 작동하지 않습니다."; +"room_notifs_settings_account_settings" = "계정 설정"; +"room_notifs_settings_cancel_action" = "취소"; +"room_notifs_settings_done_action" = "적용"; +"room_notifs_settings_none" = "알림받지 않기"; +"room_notifs_settings_mentions_and_keywords" = "멘션과 키워드만"; +"room_notifs_settings_all_messages" = "모든 메시지"; +"room_details_advanced_e2e_encryption_disabled_for_dm" = "이 방의 암호화가 활성화되지 않음."; +"room_details_advanced_e2e_encryption_enabled_for_dm" = "이 방의 암호화 활성화됨"; +"room_details_advanced_room_id_for_dm" = "아이디 :"; +"room_details_no_local_addresses_for_dm" = "이 방은 로컬 주소를 가지고 있지 않음"; +"room_details_access_section_anyone_for_dm" = "게스트를 포함한 초대 링크를 알고 있는 누구나"; +"room_details_access_section_anyone_apart_from_guest_for_dm" = "게스트를 제외하고 초대 링크를 알고 있는 누구나"; +"room_details_access_section_for_dm" = "누가 접근할 수 있나요?"; +"room_details_notifs" = "알림"; +"room_details_room_name_for_dm" = "이름"; +"room_details_photo_for_dm" = "사진"; +"room_details_integrations" = "통합"; +"room_details_search" = "방 검색"; +"room_details_title_for_dm" = "정보"; +"manage_session_sign_out" = "세션 연결 끊기"; +"manage_session_not_trusted" = "신뢰하지 않음"; +"manage_session_trusted" = "신뢰하도록 설정됨"; +"manage_session_name" = "세션 이름"; +"manage_session_info" = "세션 정보"; + +// Manage session +"manage_session_title" = "세션 관리"; +"settings_analytics_and_crash_data" = "오류 및 분석 데이터 전송"; +"settings_labs_enabled_polls" = "투표"; +"room_event_action_end_poll" = "투표 종료"; +"room_event_action_remove_poll" = "투표 제거"; +"accessibility_button_label" = "버튼"; +"enable" = "활성화"; From 2fb6d1e08c63fc6ac95ca61009901c82230adf64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20I=2ESvindseth?= Date: Sat, 25 Dec 2021 21:04:41 +0000 Subject: [PATCH 079/109] Translated using Weblate (Norwegian Nynorsk) Currently translated at 0.4% (6 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nn/ --- Riot/Assets/nn.lproj/Vector.strings | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Riot/Assets/nn.lproj/Vector.strings b/Riot/Assets/nn.lproj/Vector.strings index 8b13789179..1eec1c5583 100644 --- a/Riot/Assets/nn.lproj/Vector.strings +++ b/Riot/Assets/nn.lproj/Vector.strings @@ -1 +1,11 @@ + +"title_rooms" = "Rom"; +"title_people" = "Folk"; +"title_favourites" = "Favorittar"; + +// Titles +"title_home" = "Heim"; +"warning" = "Åtvaring"; +// String for App Store +"store_short_description" = "Sikker desentralisert chat/IP-telefoni"; From 58523d57debcb18c2a71b7e7aecd3d19b23b35a9 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Fri, 31 Dec 2021 10:30:21 +0000 Subject: [PATCH 080/109] Translated using Weblate (Hungarian) Currently translated at 100.0% (1375 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ --- Riot/Assets/hu.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 2ade108e64..6060f16c3d 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -1589,3 +1589,4 @@ "room_event_action_remove_poll" = "Szavazás törlése"; "accessibility_button_label" = "gomb"; "enable" = "Engedélyezés"; +"analytics_prompt_message_upgrade" = "Korábban beleegyeztél, hogy velünk anonimizált adatokat osztasz meg. Most, hogy jobban megértsük, hogyan használnak több eszközt az emberek, véletlenszerű azonosítót állítunk elő amit az eszközeid használni fognak."; From 8483559d7871619cfdc0eaf1d4d9f829fdf39f68 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Sat, 1 Jan 2022 00:01:16 +0000 Subject: [PATCH 081/109] Translated using Weblate (Slovak) Currently translated at 95.0% (1307 of 1375 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ --- Riot/Assets/sk.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 833708fd08..ded23fa74c 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -627,7 +627,7 @@ "group_participants_add_participant" = "Pridať účastníka"; "group_home_multi_rooms_format" = "%tu miestnosti"; "group_home_one_room_format" = "1 miestnosť"; -"group_home_multi_members_format" = "%@ členovia"; +"group_home_multi_members_format" = "%tu členovia"; // Group Home "group_home_one_member_format" = "1 člen"; From a58ec7abc04cfe22ae1a2543d6c130b1792d720c Mon Sep 17 00:00:00 2001 From: Gil Oliveira Date: Sun, 2 Jan 2022 02:50:39 +0000 Subject: [PATCH 082/109] Added translation using Weblate (Portuguese) --- Riot/Assets/pt.lproj/Localizable.strings | 1 + 1 file changed, 1 insertion(+) create mode 100644 Riot/Assets/pt.lproj/Localizable.strings diff --git a/Riot/Assets/pt.lproj/Localizable.strings b/Riot/Assets/pt.lproj/Localizable.strings new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/Riot/Assets/pt.lproj/Localizable.strings @@ -0,0 +1 @@ + From 658aef2c29e047170ee03bacf1bd329858c05845 Mon Sep 17 00:00:00 2001 From: Gil Oliveira Date: Sun, 2 Jan 2022 02:50:51 +0000 Subject: [PATCH 083/109] Added translation using Weblate (Portuguese) --- Riot/Assets/pt.lproj/InfoPlist.strings | 1 + 1 file changed, 1 insertion(+) create mode 100644 Riot/Assets/pt.lproj/InfoPlist.strings diff --git a/Riot/Assets/pt.lproj/InfoPlist.strings b/Riot/Assets/pt.lproj/InfoPlist.strings new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/Riot/Assets/pt.lproj/InfoPlist.strings @@ -0,0 +1 @@ + From 6868900593c0000bedb143d298fe2c3abeae5999 Mon Sep 17 00:00:00 2001 From: Gil Oliveira Date: Sun, 2 Jan 2022 02:50:55 +0000 Subject: [PATCH 084/109] Added translation using Weblate (Portuguese) --- .../Assets/MatrixKitAssets.bundle/pt.lproj/MatrixKit.strings | 1 + 1 file changed, 1 insertion(+) create mode 100644 Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/pt.lproj/MatrixKit.strings diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/pt.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/pt.lproj/MatrixKit.strings new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/pt.lproj/MatrixKit.strings @@ -0,0 +1 @@ + From aaa29e5e1a821fb3d7387b2a91ca58f6060fe7c1 Mon Sep 17 00:00:00 2001 From: Gil Oliveira Date: Sun, 2 Jan 2022 02:51:04 +0000 Subject: [PATCH 085/109] Added translation using Weblate (Portuguese) --- Riot/Assets/pt.lproj/Vector.strings | 1 + 1 file changed, 1 insertion(+) create mode 100644 Riot/Assets/pt.lproj/Vector.strings diff --git a/Riot/Assets/pt.lproj/Vector.strings b/Riot/Assets/pt.lproj/Vector.strings new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/Riot/Assets/pt.lproj/Vector.strings @@ -0,0 +1 @@ + From 7f5219f8dbf0899ba1155cacf5571ad321fb9635 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Fri, 31 Dec 2021 17:34:41 +0000 Subject: [PATCH 086/109] Translated using Weblate (Albanian) Currently translated at 100.0% (441 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/sq/ --- .../Assets/MatrixKitAssets.bundle/sq.lproj/MatrixKit.strings | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sq.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sq.lproj/MatrixKit.strings index c00e170a46..888cbed6b6 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sq.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sq.lproj/MatrixKit.strings @@ -477,3 +477,4 @@ "rename" = "Riemërtojeni"; "attachment_unsupported_preview_message" = "Ky lloj kartele nuk mbulohet."; "attachment_unsupported_preview_title" = "S’arrihet të bëhet paraparje"; +"room_displayname_all_other_members_left" = "%@ (Iku)"; From 92144b7d25870050b0cfcd09e0d368de95bac730 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Sat, 1 Jan 2022 18:29:39 +0000 Subject: [PATCH 087/109] Translated using Weblate (Slovak) Currently translated at 93.1% (411 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/sk/ --- .../sk.lproj/MatrixKit.strings | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings index 8a4796205f..b429d25fa0 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings @@ -427,3 +427,103 @@ "notice_room_history_visible_to_members_for_dm" = "%@ zmenil/a nastavenie, aby budúce správy boli viditeľné pre všetkých členov miestnosti."; "notice_room_history_visible_to_members" = "%@ zviditeľnil/a budúcu históriu miestnosti pre všetkých členov miestnosti."; "notice_room_history_visible_to_anyone" = "%@ zviditeľnil/a budúcu históriu miestnosti pre každého."; +"notification_settings_by_default" = "Predvolene..."; +"notification_settings_other_alerts" = "Iné upozornenia"; +"room_creation_name_placeholder" = "(napr. pracovnaSkupina)"; +"message_reply_to_message_to_reply_to_prefix" = "V odpovedi na"; +"notice_in_reply_to" = "V odpovedi na"; +"notice_room_history_visible_to_members_from_joined_point_for_dm" = "%@ zviditeľnil budúce správy pre všetkých, od okamihu pripojenia."; +"notice_room_history_visible_to_members_from_joined_point" = "%@ zviditeľnil budúcu históriu miestnosti pre všetkých členov miestnosti, od okamihu ich vstupu."; +"notice_room_history_visible_to_members_from_invited_point_for_dm" = "%@ zviditeľnil budúce správy pre všetkých, od okamihu pozvania."; +"notice_error_unknown_event_type" = "Neznámy typ udalosti"; +"notice_room_power_level_event_requirement" = "Minimálne úrovne oprávnenia súvisiace s udalosťami sú:"; +"notice_room_join_rule_public_for_dm" = "%@ ju zverejnil/a."; +"notice_room_join_rule_public" = "%@ zverejnil/a túto miestnosť."; +"notice_room_join_rule_invite_by_you_for_dm" = "Ste ju vytvorili len na pozvanie."; +"notice_room_join_rule_invite_for_dm" = "%@ ju vytvoril len na pozvanie."; +"notice_event_redacted" = ""; +"capture_media" = "Spraviť fotografiu/video"; +"auth_reset_password_error_unauthorized" = "Neoprávnené"; +"auth_invalid_user_name" = "Neplatné používateľské meno"; +"ssl_only_accept" = "Certifikát akceptujte IBA vtedy, ak správca servera zverejnil odtlačok prsta, ktorý sa zhoduje s vyššie uvedeným."; +"ssl_expected_existing_expl" = "Certifikát sa zmenil z predtým dôveryhodného na nedôveryhodný. Server mohol obnoviť svoj certifikát. Obráťte sa na správcu servera, aby vám poskytol očakávaný odtlačok."; +"ssl_unexpected_existing_expl" = "Certifikát sa zmenil na iný, ktorému dôveroval váš telefón. To je VEĽMI NEOBVYKLÉ. Odporúča sa, aby ste tento nový certifikát NEPRIJALI."; +"ssl_cert_new_account_expl" = "Ak správca servera uviedol, že sa to očakáva, skontrolujte, či sa odtlačok prsta uvedený nižšie zhoduje s odtlačkom prsta, ktorý poskytol."; +"ssl_cert_not_trust" = "Môže to znamenať, že niekto úmyselne zachytáva vašu komunikáciu alebo že váš telefón nedôveruje certifikátu, ktorý poskytol vzdialený server."; +"ssl_could_not_verify" = "Nepodarilo sa overiť identitu vzdialeného servera."; + +// unrecognized SSL certificate +"ssl_trust" = "Dôverovať"; +"call_video_with_user" = "Videohovor s %@"; +"call_voice_with_user" = "Hlasový hovor s %@"; +"call_more_actions_change_audio_device" = "Zmeniť zvukové zariadenie"; +"call_more_actions_hold" = "Podržať"; +"call_holded" = "Podržali ste hovor"; +"call_remote_holded" = "%@ podržal hovor"; +"incoming_voice_call" = "Prichádzajúci hlasový hovor"; +"incoming_video_call" = "Prichádzajúci videohovor"; +"settings_config_user_id" = "ID používateľa: %@"; +"settings_config_identity_server" = "Server totožností: %@"; +"notification_settings_suppress_from_bots" = "Stlmiť oznámenia od botov"; +"notification_settings_receive_a_call" = "Upozorniť ma, keď mi niekto zavolá"; +"notification_settings_people_join_leave_rooms" = "Upozorniť ma, keď sa ľudia pridajú alebo opustia miestnosti"; +"notification_settings_invite_to_a_new_room" = "Upozorniť ma, keď budem pozvaný do novej miestnosti"; +"notification_settings_just_sent_to_me" = "Upozorniť ma zvukom na správy odoslané len mne"; +"notification_settings_contain_my_display_name" = "Upozorniť ma zvukom na správy, ktoré obsahujú moje zobrazované meno"; +"notification_settings_contain_my_user_name" = "Upozorniť ma zvukom na správy, ktoré obsahujú moje používateľské meno"; +"notification_settings_highlight" = "Zvýrazniť"; + +// Login Screen +"login_error_already_logged_in" = "Už ste prihlásení"; +"message_unsaved_changes" = "Máte neuložené zmeny. Opustením sa zrušia."; +"membership_leave" = "Odišiel"; +"notice_room_history_visible_to_members_from_joined_point_by_you_for_dm" = "Zviditeľnili ste budúce správy pre všetkých, od okamihu pripojenia."; +"notice_room_kick" = "%@ vylúčil %@"; +"attachment_medium" = "Stredná (~%@)"; +"attachment_small" = "Malá (~%@)"; +"room_no_conference_call_in_encrypted_rooms" = "Konferenčné hovory nie sú podporované v šifrovaných miestnostiach"; +"room_no_power_to_create_conference_call" = "Aby ste mohli začať konferenciu, musíte mať právo pozývať používateľov do miestnosti"; +"device_details_delete_prompt_message" = "Táto operácia si vyžaduje dodatočné overenie.\nAk chcete pokračovať, zadajte svoje heslo."; +"device_details_rename_prompt_message" = "Verejný názov relácie je viditeľný pre ľudí, s ktorými komunikujete"; +"settings_enter_validation_token_for" = "Zadajte overovací token pre %@:"; +"notice_error_unexpected_event" = "Neočakávaná udalosť"; +"notice_error_unsupported_event" = "Nepodporovaná udalosť"; +"notice_room_join_rule_invite_by_you" = "Urobili ste miestnosť len pre pozvaných."; +// New +"notice_room_join_rule_invite" = "%@ vytvoril miestnosť len na pozvanie."; +// Old +"notice_room_join_rule" = "Pravidlo pripojenia je: %@"; +"notice_room_created" = "%@ vytvoril a nastavil miestnosť."; +"notice_profile_change_redacted" = "%@ aktualizoval svoj profil %@"; +"notice_event_redacted_reason" = " [dôvod: %@]"; +"notice_event_redacted_by" = " používateľom %@"; +"notice_room_topic_removed" = "%@ odstránil tému"; +"notice_room_name_removed_for_dm" = "%@ odstránil názov"; +"notice_room_name_removed" = "%@ odstránil/a názov miestnosti"; + +// Events formatter +"notice_avatar_changed_too" = "(obrázok bol tiež zmenený)"; +"resend_message" = "Znovu odoslať správu"; +"reset_to_default" = "Obnoviť na predvolené"; +"invite_user" = "Pozvať používateľa matrix"; +"attach_media" = "Pripojiť médium z knižnice"; +"select_account" = "Vyberte účet"; +"start_video_call" = "Spustiť videohovor"; +"start_voice_call" = "Spustiť hlasový hovor"; +"set_admin" = "Nastaviť správcu"; +"set_moderator" = "Nastaviť moderátora"; +"set_default_power_level" = "Vynulovať úroveň oprávnenia"; +"set_power_level" = "Nastaviť úroveň oprávnenia"; +"submit_code" = "Odoslať kód"; +"dismiss" = "Odmietnuť"; +"discard" = "Zahodiť"; +"login_error_resource_limit_exceeded_message_contact" = "\n\nAk chcete pokračovať v používaní tejto služby, obráťte sa na správcu služby."; +"login_error_resource_limit_exceeded_message_monthly_active_user" = "Tento domovský server dosiahol svoj mesačný limit aktívnych používateľov."; +"login_error_resource_limit_exceeded_message_default" = "Tento domovský server prekročil jeden z limitov zdroja."; +"login_error_resource_limit_exceeded_title" = "Prekročený limit zdrojov"; +"login_error_forgot_password_is_not_supported" = "Zabudnuté heslo nie je v súčasnosti podporované"; +"login_error_not_json" = "Neobsahoval platný JSON"; +"login_error_unknown_token" = "Zadaný prístupový token nebol rozpoznaný"; +"login_error_registration_is_not_supported" = "Registrácia nie je v súčasnosti podporovaná"; +"login_error_do_not_support_login_flows" = "V súčasnosti nepodporujeme žiadny alebo všetky prihlasovacie toky definované týmto domovským serverom"; +"login_error_no_login_flow" = "Nepodarilo sa nám získať autentifikačné informácie z tohto domovského servera"; From 96068f9531bf5c6c87a570ee9facb0a83d51cb3d Mon Sep 17 00:00:00 2001 From: Gil Oliveira Date: Sun, 2 Jan 2022 02:52:59 +0000 Subject: [PATCH 088/109] Translated using Weblate (Portuguese) Currently translated at 66.6% (4 of 6 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/pt/ --- Riot/Assets/pt.lproj/InfoPlist.strings | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Riot/Assets/pt.lproj/InfoPlist.strings b/Riot/Assets/pt.lproj/InfoPlist.strings index 8b13789179..9cf9394536 100644 --- a/Riot/Assets/pt.lproj/InfoPlist.strings +++ b/Riot/Assets/pt.lproj/InfoPlist.strings @@ -1 +1,7 @@ + +"NSContactsUsageDescription" = "O Element vai mostrar os seus contactos para que os possa convidar para conversar."; +"NSMicrophoneUsageDescription" = "O Element necessita de aceder ao seu microfone para fazer e receber chamadas e para gravar mensagens de voz."; +"NSPhotoLibraryUsageDescription" = "A biblioteca de fotos é usada para enviar fotos e vídeos."; +// Permissions usage explanations +"NSCameraUsageDescription" = "A câmara é usada para tirar fotos e vídeos e fazer videochamadas."; From 01f53bd8d135281fd29ae25ba1ea2c7237fbde1a Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 10 Dec 2021 17:49:08 +0200 Subject: [PATCH 089/109] Rendering location messages in the timeline. --- .../Room/Location/Contents.json | 6 + .../Contents.json | 23 ++++ .../location_marker_icon.png | Bin 0 -> 529 bytes .../location_marker_icon@2x.png | Bin 0 -> 907 bytes .../location_marker_icon@3x.png | Bin 0 -> 1267 bytes .../Contents.json | 23 ++++ .../location_user_marker.png | Bin 0 -> 1005 bytes .../location_user_marker@2x.png | Bin 0 -> 1788 bytes .../location_user_marker@3x.png | Bin 0 -> 2640 bytes Riot/Assets/en.lproj/InfoPlist.strings | 1 + Riot/Generated/Images.swift | 2 + .../Files/CellData/FilesSearchCellData.m | 4 - .../Controllers/MXKRoomViewController.m | 3 - .../MatrixKit/Models/Room/MXKAttachment.h | 1 - .../MatrixKit/Models/Room/MXKAttachment.m | 6 - .../MXKRoomBubbleCellDataWithAppendingMode.m | 2 +- .../Utils/EventFormatter/MXKEventFormatter.m | 21 --- .../Room/CellData/RoomBubbleCellData.h | 3 +- .../Room/CellData/RoomBubbleCellData.m | 27 +++- .../Location/LocationUserMarkerView.swift | 33 +++++ .../Room/Location/LocationUserMarkerView.xib | 46 +++++++ .../Location/RoomTimelineLocationView.swift | 124 ++++++++++++++++++ .../Location/RoomTimelineLocationView.xib | 71 ++++++++++ Riot/Modules/Room/RoomViewController.m | 19 +++ .../Location/LocationBubbleCell.swift | 61 +++++++++ ...ocationWithPaginationTitleBubbleCell.swift | 25 ++++ .../LocationWithoutSenderInfoBubbleCell.swift | 25 ++++ Riot/SupportingFiles/Info.plist | 2 + Riot/target.yml | 6 + 29 files changed, 493 insertions(+), 41 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Room/Location/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@3x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@3x.png create mode 100644 Riot/Modules/Room/Location/LocationUserMarkerView.swift create mode 100644 Riot/Modules/Room/Location/LocationUserMarkerView.xib create mode 100644 Riot/Modules/Room/Location/RoomTimelineLocationView.swift create mode 100644 Riot/Modules/Room/Location/RoomTimelineLocationView.xib create mode 100644 Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift create mode 100644 Riot/Modules/Room/Views/BubbleCells/Location/LocationWithPaginationTitleBubbleCell.swift create mode 100644 Riot/Modules/Room/Views/BubbleCells/Location/LocationWithoutSenderInfoBubbleCell.swift diff --git a/Riot/Assets/Images.xcassets/Room/Location/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/Contents.json new file mode 100644 index 0000000000..707b2f06b9 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "location_marker_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_marker_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_marker_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon.png b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..be968b77da464435624d0712c6f69fc65b6c5f95 GIT binary patch literal 529 zcmV+s0`C2ZP)ldZ8Jw{ZL90)97mx6LTNGgQRdMWK%Iyfn3 z8b>dklaz~=E|!+SA~aH@VtF{Pbn4-=#Hti#od{LUO`xXbATA+yds+hZO~iLGA455{ z1W6Fd7flw)dONuZ)SUbASX6bqn?wK+E2Em0nJ$*C6Efsf1t@Nbifdkt_Pu_OARMr0 zek|RW5A8s(7A}cIqZvoU_7P0r^}SAPUbKh7GYoarz@JyTC7r_9WW{`o+~A%`Vk0pSEK&p>TX&~O664J;BQ zq{KVyx-nlO@yvQ79-7}%)r`IS-<_S0HNcW3w;P0jtR|;TmFsO%_y7nu0jMz@CIh5C z8v0~(emRPdQV2v40c#)5b`f!(2sW9I46L}5Ve7C5HK`?_k({pQ6ukz5CisAmj&ZX0 zJw6(MC*A_?C1;z;sN|QMcr!3L?Rw@h`&`!}1M%o_C*{~iz{Hqnn`KDwM(fc*);i99`)}Gv zUiNa6TOX&&_b6qW?ipafb|FG|$}k%YA9Z%K_>ajKWN|ywO7CHjg)7D?$`BrG0m#~Z zI+^#Y6G0U=H-p()iV1%Qs9JzlE0-Z`KoF+Id0-xHg+tg#kO-(8n3G8xyll%B$v*>F z{n9%|?N-_3MmMW$I0wCie+IyOthQ16^LD|lJ_1%IDw{jL02BTWkm6K|jTI9m{}SQ4 zHJeKkPBKHJ6qqb16Blnzk#2U4P(741J!3W5;5Ip;eqimYn|OC$OzedEUt66muDvLX zB0=RjsEbW>=MsC70HvZ=AP8|jj8&dW>_q~KBt-BQ76$gD4Zb)N+mfOO3u3Is)pgQe zhy@gNI`T?g8S>S4c-2B?LTw0)&+0q81i0%DTeFLz#eDo$-{D=2Mp_?3n20p+U44hQ z02h%AfcL;9#=!Z1xmql#nVkp2R%ZihP`jb2bD(X_6GKgE3vdS-JzR;5IjQ41CBZ&$ z+`sBxOTG`(b)Ax6J+Z>c4ltuq2-o>JE(9X8?!bvkHtMuPxK2sXZ)A?O_uby?3t;RG zThI3(AVIHH99l;QGn(>R32PR?cfc`iKd;@N(_u0@`K^$yQxbd!9PT{WMtFJ;YCo~& hroD*CFIln#;0L{PMRKTk+}Z#D002ovPDHLkV1j1Ah3EhP literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..8ce3cacae468cb11d2104c278519e6f23ebe27ff GIT binary patch literal 1267 zcmVFjQH%{1oNMm53_v38I_;`!%t$$_Y$P;JrZ< z3sQ;EPG?$1Wo%=!JKZxYSM9HgDhjk{`EPeme+i(Wp`oFn;d2F{CRVb?TL#jv$*NH4s&xnJ7hahtPcYR*cf*?si8P=HiEc+mCXH zL==Hq+2i$s(w~7~3sk^CETIepW-U9sU)ZG=dg3s2cgdzN*RuZi5SU;Vq;ECXg9xXu zak?AInt}+#;#iwzF37G!9P$e@ZHEv^U7#AonG-}LRe@?0XHK4&^!B*DKL#aKgR~3A zM>UGWxVSex0Y%gV;?v5VE_a}A+@Y{G{BaMIP;s#ZaIN3&VJf77GHoT*24bzPWSjzuUQ`U_uNxF0#97>xFJa%MHXZ% zY=S3c#9_`m{2ICko_t}ADD{xJ7@_Bz&MTCbT0EAQH%x3tj>- zXy9b^`QeS3_-q<$O$MG=YrOA~5%+^uhyA}ml#E?JC7l(*1@8G;&L!hXr+W(oq#y9oP`R@rRpf-A5{k@-s-sC8Y z`=?G{b-K4f0li|?qRJD~fPmEQMRj>$ZPsL47}{ zW-C&)9!9-kR<9N5zju%oyqZolb?ae*v|Oox_*O)?4*>`w(AiFRB%P~Q0|%4s2SJk6 zU`_i3r#vo>Qlt3{BFhp?ARhF&02e%YBx{3i+y@=|7?U0&sO|e)M}D^Rum=GNDiBxt zt}W}mnQ;>o@NdrvJ~-rnO%$R|!@=w@2uzg8YMLZHWmn5X2Ii|o5lG+FDB5SnqRr;D zT#bmH%oeI55p{V%!PW9K`KJdF2~vxFisfJ3yemN-1XiAg|>fAwda@0@KPsC_WbmxU0Bc_Y%BB?z)koSkkD8X6iJ d8X6h|{12&b+W
    jVG*002ovPDHLkV1gCWK;{4d literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/Contents.json new file mode 100644 index 0000000000..175219374a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "location_user_marker.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_user_marker@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_user_marker@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker.png b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker.png new file mode 100644 index 0000000000000000000000000000000000000000..eedadad5e75f36dd5acc6168bcce7c67fa51144e GIT binary patch literal 1005 zcmVGrHOn4!U;%kK!oUXdIHiD08%OH6UkH6h6-|n$jZRVYXO@=h%Aq54VnA1gfE2l zwAP*g-A0K`8~(NYT+xRmAYcJlKmafEpv69Y=;pNp^B|x}s%xFy4^>!zxx}ytgpC7@ z;&5&Oqw$1<>J6yc-`qzJW@KhDY4?moKyk?;2B9>>%GK`4#Vi*^7UQ^|GC^S1>56Sw z9p0@}xu{W+QBF#^XZr!JU9MONlUYOiGTr!m80F-tV$$x{Q$SsP;T0GgG zZ;nL4CzpFY=yF2_fM~lh7s5E}+Ws~;_()8cUhr@&6<=u0mmRe1ewuN5aa2qZ)5W)> zz2)bQ!i8!-G6?JYh6!OD#G8I}?$Zph00u%2)C0#o59fF&Ce`>-Ffb}ToB(h0{N*k( zg5F0=rhET_LlmI;4Q3M>fmEQfU_OpgDk~-84kQ3+>AcihCLuCFZ@(EytI$nI0zz!d z$6G;W!`2-4MP*bnk@f&gAz5OC01uLZpP0zdyNoBvpWBcCXfys7J!iBB34p2#Es24s zLjpjQO=;V9i@q}~M z&|Ep*dN?q&Hj_fVh1jQ?;3`0FaCAtFABIgIL5xAvrBOxOk*j95I$D6na{iEem9F=W zM;5{+)-+%JC>LqNV`^b^Rj`mhdnd(l$EWrI#6{3FAxxyEhK)w~kn)iQJ;UaQ@FBpX z&YS6dH8XW+th^Ydp?zzqeAEt&nR{nAN*2HqmkWi@jx}zgkG%@!fTN_8U62x%Tx}CR zmFz3FeSI-c586WgW(*gq!up7Y9heK66ZFapTGlMtsV~w|o=gnWwe<7;K%n16vDnzcb1Ky+>(qCeTSgVb>0RN+wt1F_N;f-em@EMPfQ%`Pw&mVnb{eH8gQlaq;UzKk>W}f6Li{vhLv5?a32;5JS@8nDosq<%hLXHQ%f{pF$ql@w0C^t z!6B4@buo(@C8IGH@R?HnUex+N0*hD`lXic#-&x=&$s+8jxMef7DX_%ao1Zoled+xY zN?{t8wEuqX&Ci=)2`gf*c7E9fR4*MW600Bpq?cDa`@3L)nNt}{A+6gWoPi43|Fm}Y zUTj20TZHDSW71(3Q_+i*nFve~+6g+p=DLQt#H3?i%URLe#td1- zxeCyUsch}-(=GF{=O9?a4Dhx--rDJo`^;D|OFSr9Hle5djd3H=V^dj@_-&C=3KLUW zX2hKK!u&r$D2;I`Ei+;g@}*rV8T24CYDS7#?d&&ezi^@sm}|WH+Fa`qj1+?eR^}9h z$z(;%1u+sWb0#BdE{MToITlg4#Hgql=#@!NtUAm%+O__1w1J4>x&?$vxUAJy|LXxU zh;SW*N+3}8`d^=k;YMGo{7MP>I{{~6OryUALZv8=&eo-!i;*-|IYfB(%kxysO6Tdi zq%kK5g3jZ0ev*puG&VtS0yE9h@L=rJBf)Faw@D~Wqp+ISg z7{@@M4+=>+w;A-(Lu67JVF-d#XswQv=k+9F93%GQFkS7x_ICeokTEk?h>maUe4dXX zcn5+T5T!0sF{a4ug5U&*)aA!iOrlO-2-Txh+~vo!JIbV;ae~maFW2}<-)et)0Yb%K z_sg?0F*?EAo16*dK>9p525yd(bXF;6X^a`X2NU1`go-#%W6XdU(<3+pp%VDmlm6EO zVj{lJTxy#6L-gD5UYQr>{3(H%wmuh%V+N9+4Zk;Xzp}MI_B*nl%$zV8>iFB8*6`aS zSIJ1!%#W;Vow>uGM9mz?h!!SuU718p`S1yWSwG7Z!+SNFMoH?L(!j%UyOxh#iRpV@ z8$X}$Em$QSu3Lr~D~9(4)*7}!unbK+Lv!NVWl3pvA)^!iPkGCGSp&(Nqwghk)G3p&dY?ZRp-TB1%QUve zV_%*+*{~^vu^KV`@G*@DxX9;chNjrwo^)*Dw3N0p29KaB6W(Tedg0bYe~FT^q%fsL zOh4%WC?7^w_ca7&j-IF7%M^0{Rda+-dq__gs+*uW3OWDE3P=Shyl0ZaCb&!mLe9Us zLednM_-#Ik!<|k7Mx^N=_Txj_J*`521vo+)MF?BfNWut7ztDuQs)&wJI3uQ?mEWJP zdmc6k(tYlmCgoWkO+enJrHerCl7zY^w;wpS*MXB_hT)VK$9_X8;N!Zc8k6G*K14j~ z^?FC2zN;=p<)s#4`r*RW{Mp6Mflg@q`1a(LDK1evF-##jHI%~CQVet2<%XA#IhvjJ zuI{+fm=~qCVi@iuC=WYuK@LV*>DEchy+%DRwHCu%lv95xOzp)mee*z|72T=3l!b!j eTrvM(LHHjo>)F3WZ~buq0000$CbMC#ZZl;Q>w_Ei-)f$Rw+pMJ_ zsSA8?lDWxywMZ$!+c}#{;~7-fGVQYAsdG3FZW5SJEr0^ z*AuAN0`t$`F;(+@Uxl4Ahpu!X`&0XQ;|ua1m_Rkj{wNBjj%l^ue^P^YWeZ#>LS`$L zrfd6!3)URWW}XvyU6u-0=#X7+KX0=5&&m+2xlDyK3yQaAyH8H2pb8hVan;iNX!eS) zv;y<>9u~2>6((dA4%f86E*Sc%e^`{TeZri`D>wya^gn&<$kDZKOtag$zDzS zzuagaJ)@l1cqK#iaw_Pn!Oo_u+D0oGs+V&OQ`LUDar%!(v_TuGWEU&PXQFANUwoCA zZ}hQJ{$X$D@!BstT6@TBd(_%*W97IJ8)=_wZI^ay!nQ{QY-4QuY+wK3s7Y(IwvgHR zi_I5^jn$27n9bYvI_I>tgzQH9pJx7&V}jfeQm@vv&Eb3|GhfIn4EXtFs#?qSk(ASr zZM9#%AuW3ZImahwyWc$|N1TNW0V^7JL30u^1gxms1TFKy-wyc*SW(H|jnku7l(9@h zHe5yO7C~W|JUnYLH7;);qYS$RHNsgh}2vQtI%ntd(C3KEAb+Hl1jR!u_9 zRoZ5vb~$X5{x1Yombh&us$^m4w+N~(aoda?vaQn>d#3I^AOO=gle|tKcF34GH?K&B z&0|Gx>g`vt7JsoqhG8FOFq^cWVl4ue4*ocsgavx$$m!OyBd4X6%nthq7|cFpEI~3#-}ON)~FbiGV>&3#-}OkYQg8k`Pk+^L}q; z$n5Susu(0xpLYr{yOJ^E_y`hXk7jniD}z6_Eg%iE0QhQp0dS>~*@2$`Ry1n?a3y5S zyiWit5>|Jn-*>59qE$i``f6}%@}kB|mCS6u-Y0;RzR+rK^4FI_CQRQDKuXgsbKEY` zDj`dgtITmF!>SUp6zcwX^!nT`g_{JBopH+?hm4u-5I|-=7p*8{b{@l7C1k8IdZ9@a zvM{V|6F|no@Ke+N9EFUPZW2JoSPffd6fz7XF^@uR?HzP892iv4P}NiR5EbJ>|ESbqPal;t_Xdl z%qGT{1mKF|FBO}kO9F63s9`r_5i)Q}i6CU)R2`KJT&rjPcxZrW2pKqskb!du88~M{ zw(=8M0b=Nx19F56oa&)nf>UfmCPV=NI3jE-nWoy%M2@Z+a znT!Jha72WB>1dti- zjNY2O#d?PTGV{4;jYHJUI$CP#Ch630TA%OEut{r%*x zFNI90!3hDR^hxPUqt~U7^=yel%Y+n|s5{vzt*eiRM$3d0gyi+~`>upcDgT%NRy4V) z9JvxQtTJI`Q>)66nXkK)kPZnTDUr9c-p%tqe(UteKBWZrE`zv&v)zMRvwmalE67dy zUK2oUGPnE94cWy40I$h}ID%ON|H#~sMVJ4enn5f)bKoCYc*7M`Gl-+F`U@9|3qyu# z2FvluqAF%By>%3-87vSit76vDkfEBvJfij6WmU|IbrU>W%^3GFgUf;s_nx-md|<4Q z*=nXWJtTlJiG3=>l)%=1j@ss!y-84Ud~&w?UF@Qh=$^4259}X|ML@;H8TKb73YopA zQ4HPWTjiIqit3Z_xftELn~M*WN4e!qqQV%lOroX zEaH6k;8=vQNm~IT^7{PV!OEBAr+OlZ?|Ch3(w1J}dmmerLaLBOw`v^rY0PKk6xcbe zx6?}X{Z0LBB|D#qrf&&K$9!KOt~!TJ%H#oFAMPD=%tGKXL1~y{#K-pgN*T!PDRI$4 zKx4&O26n?W&7ePRrOntyuK)P*m|6A-3dsg6?$s4>WgaqX>&K%L^Cv$bC>Ei&XLk?w zC}U1SW+7+=<0xR3vyfQ`T0uArnC0~G(DoC@Vj$n_ZobQaSCq#T=vM@}VE4hY+@@G$Z6UL+|8Ue)irxrOgJ46rCtAMEYNIvQ-$-j# zZ$EEn?Y9xMjfl=7vp6bB!ud=#&7O5C<=@6-LhEG*d{3RwW0E`J(1VHO1~R#8=h)HYweCgmn%maS@G=(li6 zkU~zX*Q8v9Y-H>7#U2wsLkmqLFuSXd&+b0W-GkV28L|=ELe0L~1B7jnXy5C#@1Gy1 zy-eDbT!+j;3!Nwb;Tp+($VNtCW2QURu585#*GOSPHWGzRd(G<2Y{d)LNZ~>@$u1Cx z`9Z1g%VAcF_6afTJZsEap+hz@u5QfxBD76KHw@PMB37+gN)fV27bgh!h3T6@Rk({_ z4F-dgKmL}VJ+3U2E@YEN6^?|Pd0Q};&nFP!j(O|7r_SD$p<>Nb>X1zvu25^+Ou8wG zzc%4BtyP>K&R+zo%c6f3QXS9x&?$4Mrd29rv(($IdS74Lb8^*gC=b2b$5L0RI4U2q z5qs6HIiUX+Wpdfk`ma9CGlXoywST|ZRvx!0RnWMnmEJkudD10_AY@b01q}h4W$l+= z=iT{VpL9JnxV^&7{Q?ARHdL}X5?-;pkr!wb1wyt!%%CA)OCV%RED0I{76U>S!)bubbleCellData return NO; } } - + // Add all components of the provided message for (MXKRoomBubbleComponent* component in cellData.bubbleComponents) { diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 9177cc5bed..c7913b3648 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -175,10 +175,6 @@ - (BOOL)isSupportedAttachment:(MXEvent*)event { isSupportedAttachment = hasUrl || hasFile; } - else if ([msgtype isEqualToString:kMXMessageTypeLocation]) - { - // Not supported yet - } else if ([msgtype isEqualToString:kMXMessageTypeFile]) { isSupportedAttachment = hasUrl || hasFile; @@ -1333,23 +1329,6 @@ - (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event withRoomState *error = MXKEventFormatterErrorUnsupported; } } - else if ([msgtype isEqualToString:kMXMessageTypeLocation]) - { - body = body? body : [MatrixKitL10n noticeLocationAttachment]; - if (![self isSupportedAttachment:event]) - { - MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported attachment %@", event.description); - if (_isForSubtitle || !_settings.showUnsupportedEventsInRoomHistory) - { - body = [MatrixKitL10n noticeInvalidAttachment]; - } - else - { - body = [MatrixKitL10n noticeUnsupportedAttachment:event.description]; - } - *error = MXKEventFormatterErrorUnsupported; - } - } else if ([msgtype isEqualToString:kMXMessageTypeFile]) { body = body? body : [MatrixKitL10n noticeFileAttachment]; diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index ca0058be54..706776efb2 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -32,7 +32,8 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) RoomBubbleCellDataTagCall, RoomBubbleCellDataTagGroupCall, RoomBubbleCellDataTagRoomCreationIntro, - RoomBubbleCellDataTagPoll + RoomBubbleCellDataTagPoll, + RoomBubbleCellDataTagLocation }; /** diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 4c9e9a4e2b..1f80230604 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -174,8 +174,17 @@ - (instancetype)initWithEvent:(MXEvent *)event andRoomState:(MXRoomState *)roomS self.displayTimestampForSelectedComponentOnLeftWhenPossible = NO; } } - } + break; + } + case MXEventTypeRoomMessage: + { + if (event.hasLocation) { + self.tag = RoomBubbleCellDataTagLocation; + self.collapsable = NO; + self.collapsed = NO; + } + } default: break; } @@ -273,6 +282,11 @@ - (BOOL)hasNoDisplay return NO; } + if (self.tag == RoomBubbleCellDataTagLocation) + { + return NO; + } + return [super hasNoDisplay]; } @@ -845,6 +859,9 @@ - (BOOL)addEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState case RoomBubbleCellDataTagPoll: shouldAddEvent = NO; break; + case RoomBubbleCellDataTagLocation: + shouldAddEvent = NO; + break; default: break; } @@ -857,6 +874,11 @@ - (BOOL)addEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState { case MXEventTypeRoomMessage: { + if (event.hasLocation) { + shouldAddEvent = NO; + break; + } + NSString *messageType = event.content[@"msgtype"]; if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest]) @@ -1075,9 +1097,6 @@ - (NSString*)accessibilityLabelForAttachmentType:(MXKAttachmentType)attachmentTy case MXKAttachmentTypeVideo: accessibilityLabel = [VectorL10n mediaTypeAccessibilityVideo]; break; - case MXKAttachmentTypeLocation: - accessibilityLabel = [VectorL10n mediaTypeAccessibilityLocation]; - break; case MXKAttachmentTypeFile: accessibilityLabel = [VectorL10n mediaTypeAccessibilityFile]; break; diff --git a/Riot/Modules/Room/Location/LocationUserMarkerView.swift b/Riot/Modules/Room/Location/LocationUserMarkerView.swift new file mode 100644 index 0000000000..995d1ae56d --- /dev/null +++ b/Riot/Modules/Room/Location/LocationUserMarkerView.swift @@ -0,0 +1,33 @@ +// +// Copyright 2021 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 +import Reusable +import Mapbox + +class LocationUserMarkerView: MGLAnnotationView, NibLoadable { + + @IBOutlet private var avatarView: UserAvatarView! + + override func awakeFromNib() { + super.awakeFromNib() + translatesAutoresizingMaskIntoConstraints = false + } + + func setAvatarData(_ avatarData: AvatarViewDataProtocol) { + avatarView.fill(with: avatarData) + } +} diff --git a/Riot/Modules/Room/Location/LocationUserMarkerView.xib b/Riot/Modules/Room/Location/LocationUserMarkerView.xib new file mode 100644 index 0000000000..26495f925e --- /dev/null +++ b/Riot/Modules/Room/Location/LocationUserMarkerView.xib @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift new file mode 100644 index 0000000000..efb1a1bd31 --- /dev/null +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -0,0 +1,124 @@ +// +// Copyright 2021 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 +import Reusable +import Mapbox + +class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { + + private struct Constants { + static let mapHeight: CGFloat = 300.0 + static let mapTilerKey = "bDAfUcrMPWTAB1KB38r6" + static let mapZoomLevel = 15.0 + static let mapStyleURLString = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=\(Constants.mapTilerKey)") + static let cellBorderRadius: CGFloat = 1.0 + static let cellCornerRadius: CGFloat = 8.0 + } + + // MARK: Properties + // MARK: - Private + + @IBOutlet private var descriptionLabel: UILabel! + + private var mapView: MGLMapView! + private var annotationView: LocationUserMarkerView? + + // MARK: - Public + + var showUserLocation: Bool { + get { + mapView.showsUserLocation + } + set { + mapView.showsUserLocation = newValue + } + } + + var locationDescription: String? { + get { + descriptionLabel.text + } + set { + descriptionLabel.text = newValue + } + } + + override func awakeFromNib() { + super.awakeFromNib() + + mapView = MGLMapView(frame: .zero, styleURL: Constants.mapStyleURLString) + mapView.delegate = self + mapView.logoView.isHidden = true + mapView.attributionButton.isHidden = true + + mapView.translatesAutoresizingMaskIntoConstraints = false + mapView.addConstraint(mapView.heightAnchor.constraint(equalToConstant: Constants.mapHeight)) + vc_addSubViewMatchingParent(mapView) + sendSubviewToBack(mapView) + + descriptionLabel.textColor = ThemeService.shared().theme.colors.primaryContent + descriptionLabel.font = ThemeService.shared().theme.fonts.footnote + + clipsToBounds = true + layer.borderColor = ThemeService.shared().theme.colors.quinaryContent.cgColor + layer.borderWidth = Constants.cellBorderRadius + layer.cornerRadius = Constants.cellCornerRadius + } + + // MARK: - Public + + public func displayGeoURI(_ geoURI: String, + userIdentifier: String, + userDisplayName: String, + userAvatarURL: String, + mediaManager: MXMediaManager) { + let locationString = geoURI.components(separatedBy: ":").last?.components(separatedBy: ";").first + + guard let locationComponents = locationString?.components(separatedBy: ","), + let latitude = locationComponents.first?.double, + let longitude = locationComponents.last?.double + else { + return + } + + annotationView = LocationUserMarkerView.loadFromNib() + + annotationView?.setAvatarData(AvatarViewData(matrixItemId: userIdentifier, + displayName: userDisplayName, + avatarUrl: userAvatarURL, + mediaManager: mediaManager, + fallbackImage: .matrixItem(userIdentifier, userDisplayName))) + + let location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + + if let annotations = mapView.annotations { + mapView.removeAnnotations(annotations) + } + + mapView.setCenter(location, zoomLevel: Constants.mapZoomLevel, animated: false) + + let pointAnnotation = MGLPointAnnotation() + pointAnnotation.coordinate = location + mapView.addAnnotation(pointAnnotation) + } + + // MARK: - MGLMapViewDelegate + + func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { + return annotationView + } +} diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib new file mode 100644 index 0000000000..1c4f0923a7 --- /dev/null +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index ac57d3a781..7ef1311d60 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -422,6 +422,10 @@ - (void)viewDidLoad [self.bubblesTableView registerClass:PollBubbleCell.class forCellReuseIdentifier:PollBubbleCell.defaultReuseIdentifier]; [self.bubblesTableView registerClass:PollWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:PollWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; [self.bubblesTableView registerClass:PollWithPaginationTitleBubbleCell.class forCellReuseIdentifier:PollWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + + [self.bubblesTableView registerClass:LocationBubbleCell.class forCellReuseIdentifier:LocationBubbleCell.defaultReuseIdentifier]; + [self.bubblesTableView registerClass:LocationWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:LocationWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [self.bubblesTableView registerClass:LocationWithPaginationTitleBubbleCell.class forCellReuseIdentifier:LocationWithPaginationTitleBubbleCell.defaultReuseIdentifier]; [self vc_removeBackTitle]; @@ -2731,6 +2735,21 @@ - (void)displayRoomPreview:(RoomPreviewData *)previewData cellViewClass = PollBubbleCell.class; } } + else if (bubbleData.tag == RoomBubbleCellDataTagLocation) + { + if (bubbleData.isPaginationFirstBubble) + { + cellViewClass = LocationWithPaginationTitleBubbleCell.class; + } + else if (bubbleData.shouldHideSenderInformation) + { + cellViewClass = LocationWithoutSenderInfoBubbleCell.class; + } + else + { + cellViewClass = LocationBubbleCell.class; + } + } else if (bubbleData.isIncoming) { if (bubbleData.isAttachmentWithThumbnail) diff --git a/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift new file mode 100644 index 0000000000..c095b61b35 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift @@ -0,0 +1,61 @@ +// +// Copyright 2021 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 + +class LocationBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable { + + private var locationView: RoomTimelineLocationView! + + override func render(_ cellData: MXKCellData!) { + super.render(cellData) + + guard #available(iOS 14.0, *), + let bubbleData = cellData as? RoomBubbleCellData, + let event = bubbleData.events.last, + event.eventType == __MXEventType.roomMessage, + event.hasLocation(), + let content = event.content[kMXMessageContentKeyExtensibleLocation] as? [String: String], + let geoURI = content[kMXMessageContentKeyExtensibleLocationURI] + else { + return + } + + locationView.locationDescription = content[kMXMessageContentKeyExtensibleLocationDescription] + + locationView.displayGeoURI(geoURI, + userIdentifier: bubbleData.senderId, + userDisplayName: bubbleData.senderDisplayName, + userAvatarURL: bubbleData.senderAvatarUrl, + mediaManager: bubbleData.mxSession.mediaManager) + } + + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.backgroundColor = .clear + bubbleCellContentView?.showSenderInfo = true + bubbleCellContentView?.showPaginationTitle = false + + guard let contentView = bubbleCellContentView?.innerContentView else { + return + } + + locationView = RoomTimelineLocationView.loadFromNib() + + contentView.vc_addSubViewMatchingParent(locationView) + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithPaginationTitleBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithPaginationTitleBubbleCell.swift new file mode 100644 index 0000000000..e3f6dd84f1 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithPaginationTitleBubbleCell.swift @@ -0,0 +1,25 @@ +// +// Copyright 2021 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 + +class LocationWithPaginationTitleBubbleCell: LocationBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showPaginationTitle = true + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithoutSenderInfoBubbleCell.swift new file mode 100644 index 0000000000..1329221dd0 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithoutSenderInfoBubbleCell.swift @@ -0,0 +1,25 @@ +// +// Copyright 2021 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 + +class LocationWithoutSenderInfoBubbleCell: LocationBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showSenderInfo = false + } +} diff --git a/Riot/SupportingFiles/Info.plist b/Riot/SupportingFiles/Info.plist index 5bcda43e69..4c4d11e2f0 100644 --- a/Riot/SupportingFiles/Info.plist +++ b/Riot/SupportingFiles/Info.plist @@ -65,6 +65,8 @@ The photo library is used to send photos and videos. NSSiriUsageDescription Siri is used to perform calls even from the lock screen. + NSLocationWhenInUseUsageDescription + Element needs access to your location before being able to share it. UIBackgroundModes audio diff --git a/Riot/target.yml b/Riot/target.yml index aaac28a9be..3c50467b8c 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -1,5 +1,11 @@ name: Riot +packages: + Mapbox: + url: https://github.com/maplibre/maplibre-gl-native-distribution + minVersion: 5.12.2 + maxVersion: 5.13.0 + schemes: Riot: analyze: From 5a72750f8d1fe5504d352434edf95fc58019e2e4 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 13 Dec 2021 10:04:30 +0200 Subject: [PATCH 090/109] vector-im/element-ios/issues/5298 - Replaced "msgtype" strings with new kMXMessageTypeKey. --- .../GlobalSearch/Files/CellData/FilesSearchCellData.m | 2 +- .../Modules/MatrixKit/Controllers/MXKRoomViewController.m | 2 +- Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m | 2 +- .../MatrixKit/Models/Room/MXKRoomBubbleComponent.m | 2 +- Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m | 8 ++++---- .../MatrixKit/Utils/EventFormatter/MXKEventFormatter.m | 6 +++--- Riot/Modules/Room/CellData/RoomBubbleCellData.m | 4 ++-- Riot/Modules/Room/DataSources/RoomDataSource.m | 2 +- Riot/Modules/Room/RoomViewController.m | 2 +- RiotNSE/NotificationService.swift | 2 +- .../Service/MatrixSDK/TemplateRoomChatService.swift | 2 +- RiotTests/MatrixKitTests/MXKEventFormatterTests.m | 6 ++---- 12 files changed, 19 insertions(+), 21 deletions(-) diff --git a/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m b/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m index 14331cfc41..606bfbe705 100644 --- a/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m +++ b/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m @@ -128,7 +128,7 @@ - (UIImage*)attachmentIcon { MXEvent *event = searchResult.result; NSString *msgtype; - MXJSONModelSetString(msgtype, event.content[@"msgtype"]); + MXJSONModelSetString(msgtype, event.content[kMXMessageTypeKey]); if ([msgtype isEqualToString:kMXMessageTypeImage]) { diff --git a/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m index 7e9042517c..24d8c8f1f4 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m @@ -2179,7 +2179,7 @@ - (void)promptUserToResendEvent:(NSString *)eventId if (event && event.eventType == MXEventTypeRoomMessage) { - NSString *msgtype = event.content[@"msgtype"]; + NSString *msgtype = event.content[kMXMessageTypeKey]; NSString* textMessage; if ([msgtype isEqualToString:kMXMessageTypeText]) diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m index 377b6a70b7..a2d796e685 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m @@ -91,7 +91,7 @@ - (instancetype)initWithEvent:(MXEvent*)event andMediaManager:(MXMediaManager*)m else { // Note: mxEvent.eventType is supposed to be MXEventTypeRoomMessage here. - NSString *msgtype = eventContent[@"msgtype"]; + NSString *msgtype = eventContent[kMXMessageTypeKey]; if ([msgtype isEqualToString:kMXMessageTypeImage]) { _type = MXKAttachmentTypeImage; diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m index 565519ba9e..e40ffbf027 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m @@ -115,7 +115,7 @@ - (void)updateLinkWithRoomState:(MXRoomState*)roomState return; } - NSString *messageType = self.event.content[@"msgtype"]; + NSString *messageType = self.event.content[kMXMessageTypeKey]; if (!messageType || !([messageType isEqualToString:kMXMessageTypeText] || [messageType isEqualToString:kMXMessageTypeNotice] || [messageType isEqualToString:kMXMessageTypeEmote])) { diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index 43ab3e623d..884cd439be 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -1951,7 +1951,7 @@ - (void)resendEventWithEventId:(NSString *)eventId success:(void (^)(NSString *) else if ([event.type isEqualToString:kMXEventTypeStringRoomMessage]) { // And retry the send the message according to its type - NSString *msgType = event.content[@"msgtype"]; + NSString *msgType = event.content[kMXMessageTypeKey]; if ([msgType isEqualToString:kMXMessageTypeText] || [msgType isEqualToString:kMXMessageTypeEmote]) { // Resend the Matrix event by reusing the existing echo @@ -2712,7 +2712,7 @@ - (BOOL)canPerformActionOnEvent:(MXEvent*)event return NO; } - NSString *messageType = event.content[@"msgtype"]; + NSString *messageType = event.content[kMXMessageTypeKey]; if (messageType == nil || [messageType isEqualToString:@"m.bad.encrypted"]) { return NO; } @@ -3928,7 +3928,7 @@ - (BOOL)canReactToEventWithId:(NSString*)eventId if ([self canPerformActionOnEvent:event]) { - NSString *messageType = event.content[@"msgtype"]; + NSString *messageType = event.content[kMXMessageTypeKey]; if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { @@ -3971,7 +3971,7 @@ - (BOOL)canEditEventWithId:(NSString*)eventId { MXEvent *event = [self eventWithEventId:eventId]; BOOL isRoomMessage = event.eventType == MXEventTypeRoomMessage; - NSString *messageType = event.content[@"msgtype"]; + NSString *messageType = event.content[kMXMessageTypeKey]; return isRoomMessage && ([messageType isEqualToString:kMXMessageTypeText] || [messageType isEqualToString:kMXMessageTypeEmote]) diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index c7913b3648..923fbbaa71 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -139,7 +139,7 @@ - (BOOL)isSupportedAttachment:(MXEvent*)event if (event.eventType == MXEventTypeRoomMessage) { NSString *msgtype; - MXJSONModelSetString(msgtype, event.content[@"msgtype"]); + MXJSONModelSetString(msgtype, event.content[kMXMessageTypeKey]); NSString *urlField; NSDictionary *fileField; @@ -1248,7 +1248,7 @@ - (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event withRoomState else { NSString *msgtype; - MXJSONModelSetString(msgtype, event.content[@"msgtype"]); + MXJSONModelSetString(msgtype, event.content[kMXMessageTypeKey]); NSString *body; BOOL isHTML = NO; @@ -1979,7 +1979,7 @@ - (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary if (event.eventType == MXEventTypeRoomMessage) { - NSString *msgtype = event.content[@"msgtype"]; + NSString *msgtype = event.content[kMXMessageTypeKey]; if ([msgtype isEqualToString:kMXMessageTypeEmote] == NO) { NSString *senderDisplayName = [self senderDisplayNameForEvent:event withRoomState:roomState]; diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 1f80230604..b3a388b271 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -879,7 +879,7 @@ - (BOOL)addEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState break; } - NSString *messageType = event.content[@"msgtype"]; + NSString *messageType = event.content[kMXMessageTypeKey]; if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { @@ -1013,7 +1013,7 @@ - (void)keyVerificationDidUpdate break; case MXEventTypeRoomMessage: { - NSString *msgType = event.content[@"msgtype"]; + NSString *msgType = event.content[kMXMessageTypeKey]; if ([msgType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index f160a851fb..1f6b511376 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -836,7 +836,7 @@ - (BOOL)shouldFetchKeyVerificationForEvent:(MXEvent*)event break; case MXEventTypeRoomMessage: { - NSString *msgType = event.content[@"msgtype"]; + NSString *msgType = event.content[kMXMessageTypeKey]; if ([msgType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 7ef1311d60..6de22da8a2 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6120,7 +6120,7 @@ - (RoomContextualMenuItem *)copyMenuItemWithEvent:(MXEvent*)event andCell:(id Date: Mon, 13 Dec 2021 12:08:52 +0200 Subject: [PATCH 091/109] vector-im/element-ios/issues/5298 - Added body message content keys and replaced plain string usages. --- .../Files/CellData/FilesSearchCellData.m | 2 +- .../MatrixKit/Controllers/MXKRoomViewController.m | 2 +- Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m | 2 +- .../MatrixKit/Models/Room/MXKRoomDataSource.m | 4 ++-- .../MatrixKit/Models/Search/MXKSearchCellData.m | 2 +- .../Utils/EventFormatter/MXKEventFormatter.m | 12 ++++++------ Riot/Modules/Room/CellData/RoomBubbleCellData.m | 2 +- RiotNSE/NotificationService.swift | 2 +- .../Service/MatrixSDK/TemplateRoomChatService.swift | 2 +- RiotTests/MatrixKitTests/MXKEventFormatterTests.m | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m b/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m index 606bfbe705..6e509e0895 100644 --- a/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m +++ b/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m @@ -37,7 +37,7 @@ - (instancetype)initWithSearchResult:(MXSearchResult *)searchResult2 andSearchDa roomId = event.roomId; // Title is here the file name stored in event body - title = [event.content[@"body"] isKindOfClass:[NSString class]] ? event.content[@"body"] : nil; + title = [event.content[kMXMessageBodyKey] isKindOfClass:[NSString class]] ? event.content[kMXMessageBodyKey] : nil; // Check attachment if any if ([searchDataSource.eventFormatter isSupportedAttachment:event]) diff --git a/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m index 24d8c8f1f4..942eeec588 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m @@ -2184,7 +2184,7 @@ - (void)promptUserToResendEvent:(NSString *)eventId NSString* textMessage; if ([msgtype isEqualToString:kMXMessageTypeText]) { - textMessage = event.content[@"body"]; + textMessage = event.content[kMXMessageBodyKey]; } // Show a confirmation popup to the end user diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m index a2d796e685..634ca547fb 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m @@ -119,7 +119,7 @@ - (instancetype)initWithEvent:(MXEvent*)event andMediaManager:(MXMediaManager*)m } } - MXJSONModelSetString(_originalFileName, eventContent[@"body"]); + MXJSONModelSetString(_originalFileName, eventContent[kMXMessageBodyKey]); MXJSONModelSetDictionary(_contentInfo, eventContent[@"info"]); MXJSONModelSetMXJSONModel(contentFile, MXEncryptedContentFile, eventContent[@"file"]); diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index 884cd439be..c07fcdce96 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -3992,7 +3992,7 @@ - (NSString*)editableTextMessageForEvent:(MXEvent*)event } else { - editableTextMessage = event.content[@"body"]; + editableTextMessage = event.content[kMXMessageBodyKey]; } return editableTextMessage; @@ -4109,7 +4109,7 @@ - (void)replaceTextMessageForEventWithId:(NSString*)eventId NSString *sanitizedText = [self sanitizedMessageText:text]; NSString *formattedText = [self htmlMessageFromSanitizedText:sanitizedText]; - NSString *eventBody = event.content[@"body"]; + NSString *eventBody = event.content[kMXMessageBodyKey]; NSString *eventFormattedBody = event.content[@"formatted_body"]; if (![sanitizedText isEqualToString:eventBody] && (!eventFormattedBody || ![formattedText isEqualToString:eventFormattedBody])) diff --git a/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m b/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m index 246dbd87de..d1daa83ad8 100644 --- a/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m +++ b/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m @@ -56,7 +56,7 @@ - (instancetype)initWithSearchResult:(MXSearchResult *)searchResult2 andSearchDa date = [searchDataSource.eventFormatter dateStringFromEvent:searchResult.result withTime:YES]; // Code from [MXEventFormatter stringFromEvent] for the particular case of a text message - message = [searchResult.result.content[@"body"] isKindOfClass:[NSString class]] ? searchResult.result.content[@"body"] : nil; + message = [searchResult.result.content[kMXMessageBodyKey] isKindOfClass:[NSString class]] ? searchResult.result.content[kMXMessageBodyKey] : nil; } return self; } diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 923fbbaa71..46abe46ab7 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -139,7 +139,7 @@ - (BOOL)isSupportedAttachment:(MXEvent*)event if (event.eventType == MXEventTypeRoomMessage) { NSString *msgtype; - MXJSONModelSetString(msgtype, event.content[kMXMessageTypeKey]); + MXJSONModelSetString(msgtype, event.content[@"msgtype"]); NSString *urlField; NSDictionary *fileField; @@ -1263,12 +1263,12 @@ - (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event withRoomState else if (eventThreadIdentifier) { isHTML = YES; - MXJSONModelSetString(body, event.content[@"body"]); + MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); MXEvent *threadRootEvent = [mxSession.store eventWithEventId:eventThreadIdentifier inRoom:event.roomId]; NSString *threadRootEventContent; - MXJSONModelSetString(threadRootEventContent, threadRootEvent.content[@"body"]); + MXJSONModelSetString(threadRootEventContent, threadRootEvent.content[kMXMessageBodyKey]); body = [NSString stringWithFormat:@"
    In reply to %@
    %@
    %@", [MXTools permalinkToEvent:eventThreadIdentifier inRoom:event.roomId], [MXTools permalinkToUserWithUserId:threadRootEvent.sender], @@ -1279,7 +1279,7 @@ - (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event withRoomState } else { - MXJSONModelSetString(body, event.content[@"body"]); + MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); } if (body) @@ -1561,7 +1561,7 @@ - (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event withRoomState else { NSString *body; - MXJSONModelSetString(body, event.content[@"body"]); + MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); // Check sticker validity if (![self isSupportedAttachment:event]) @@ -2100,7 +2100,7 @@ - (UIFont*)fontForEvent:(MXEvent*)event else if (!_isForSubtitle && event.eventType == MXEventTypeRoomMessage && (_emojiOnlyTextFont || _singleEmojiTextFont)) { NSString *message; - MXJSONModelSetString(message, event.content[@"body"]); + MXJSONModelSetString(message, event.content[kMXMessageBodyKey]); if (_emojiOnlyTextFont && [MXKTools isEmojiOnlyString:message]) { diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index b3a388b271..3d69115f1e 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -1066,7 +1066,7 @@ - (NSString *)accessibilityLabel { NSString *mediaName = [self accessibilityLabelForAttachmentType:self.attachment.type]; - MXJSONModelSetString(accessibilityLabel, self.events.firstObject.content[@"body"]); + MXJSONModelSetString(accessibilityLabel, self.events.firstObject.content[kMXMessageBodyKey]); if (accessibilityLabel) { accessibilityLabel = [NSString stringWithFormat:@"%@ %@", mediaName, accessibilityLabel]; diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index b3ef5bd61b..5547807521 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -386,7 +386,7 @@ class NotificationService: UNNotificationServiceExtension { } let msgType = event.content[kMXMessageTypeKey] as? String - let messageContent = event.content["body"] as? String + let messageContent = event.content[kMXMessageBodyKey] as? String let isReply = event.isReply() if isReply { diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift index dfb295384a..821f4ccac1 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift @@ -103,7 +103,7 @@ class TemplateRoomChatService: TemplateRoomChatServiceProtocol { }) .compactMap({ event -> TemplateRoomChatMessage? in guard let eventId = event.eventId, - let body = event.content["body"] as? String, + let body = event.content[kMXMessageBodyKey] as? String, let sender = senderForMessage(event: event) else { return nil } diff --git a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m index 320e82b43c..440022c841 100644 --- a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m +++ b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m @@ -52,7 +52,7 @@ - (void)setUp anEvent.wireType = kMXEventTypeStringRoomMessage; anEvent.originServerTs = (uint64_t) ([[NSDate date] timeIntervalSince1970] * 1000); anEvent.wireContent = @{ kMXMessageTypeKey: kMXMessageTypeText, - @"body": @"deded" }; + kMXMessageBodyKey: @"deded" }; maxHeaderSize = ceil(eventFormatter.defaultTextFont.pointSize * 1.2); } From 64d0559b855830b34c2613c0d5b398692f4553b1 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 16 Dec 2021 13:29:50 +0200 Subject: [PATCH 092/109] vector-im/element-ios/issues/5298 - Prevent the activity indicator modifier from reloading the whole view hierarchy. --- .../ActivityIndicatorModifier.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift b/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift index 821c71ef0f..1b083d1e0d 100644 --- a/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift +++ b/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift @@ -21,14 +21,17 @@ import SwiftUI /// A modifier for showing the activity indicator centered over a view. struct ActivityIndicatorModifier: ViewModifier { var show: Bool - + @ViewBuilder func body(content: Content) -> some View { + content + .overlay(activityIndicator, alignment: .center) + } + + @ViewBuilder + private var activityIndicator: some View { if show { - content - .overlay(ActivityIndicator(), alignment: .center) - } else { - content + ActivityIndicator() } } } From 6a46c42cfda85b24e7eb25a2b9c3087ec2631fcc Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 16 Dec 2021 14:02:42 +0200 Subject: [PATCH 093/109] vector-im/element-ios/issues/5298 - Implemented location sharing from the input toolbar action menu. --- .../action_location.imageset/Contents.json | 23 ++++ .../action_location.png | Bin 0 -> 529 bytes .../action_location@2x.png | Bin 0 -> 907 bytes .../action_location@3x.png | Bin 0 -> 1267 bytes Riot/Assets/en.lproj/Vector.strings | 20 +++ Riot/Generated/Images.swift | 1 + Riot/Generated/Strings.swift | 36 +++++ Riot/Managers/Settings/RiotSettings.swift | 3 + .../MatrixKit/Models/Room/MXKRoomDataSource.h | 19 +++ .../MatrixKit/Models/Room/MXKRoomDataSource.m | 23 ++++ .../Location/RoomTimelineLocationView.swift | 40 ++---- .../Location/RoomTimelineLocationView.xib | 1 + Riot/Modules/Room/RoomCoordinator.swift | 21 +++ Riot/Modules/Room/RoomViewController.h | 7 + Riot/Modules/Room/RoomViewController.m | 10 ++ .../Location/LocationBubbleCell.swift | 21 ++- Riot/target.yml | 7 +- RiotSwiftUI/Info.plist | 2 + .../Modules/Common/Mock/MockAppScreens.swift | 1 + .../LocationSharingCoordinator.swift | 101 ++++++++++++++ .../LocationSharingModels.swift | 76 +++++++++++ .../LocationSharingScreenState.swift | 35 +++++ .../LocationSharingViewModel.swift | 111 +++++++++++++++ .../Test/UI/LocationSharingUITests.swift | 36 +++++ .../Unit/LocationSharingViewModelTests.swift | 35 +++++ .../View/LocationSharingMapView.swift | 128 ++++++++++++++++++ .../View/LocationSharingUserMarkerView.swift | 53 ++++++++ .../View/LocationSharingView.swift | 86 ++++++++++++ RiotSwiftUI/target.yml | 1 + project.yml | 6 + 30 files changed, 867 insertions(+), 36 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@3x.png create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/Contents.json new file mode 100644 index 0000000000..5bb98bf57e --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "action_location.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "action_location@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "action_location@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location.png b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location.png new file mode 100644 index 0000000000000000000000000000000000000000..be968b77da464435624d0712c6f69fc65b6c5f95 GIT binary patch literal 529 zcmV+s0`C2ZP)ldZ8Jw{ZL90)97mx6LTNGgQRdMWK%Iyfn3 z8b>dklaz~=E|!+SA~aH@VtF{Pbn4-=#Hti#od{LUO`xXbATA+yds+hZO~iLGA455{ z1W6Fd7flw)dONuZ)SUbASX6bqn?wK+E2Em0nJ$*C6Efsf1t@Nbifdkt_Pu_OARMr0 zek|RW5A8s(7A}cIqZvoU_7P0r^}SAPUbKh7GYoarz@JyTC7r_9WW{`o+~A%`Vk0pSEK&p>TX&~O664J;BQ zq{KVyx-nlO@yvQ79-7}%)r`IS-<_S0HNcW3w;P0jtR|;TmFsO%_y7nu0jMz@CIh5C z8v0~(emRPdQV2v40c#)5b`f!(2sW9I46L}5Ve7C5HK`?_k({pQ6ukz5CisAmj&ZX0 zJw6(MC*A_?C1;z;sN|QMcr!3L?Rw@h`&`!}1M%o_C*{~iz{Hqnn`KDwM(fc*);i99`)}Gv zUiNa6TOX&&_b6qW?ipafb|FG|$}k%YA9Z%K_>ajKWN|ywO7CHjg)7D?$`BrG0m#~Z zI+^#Y6G0U=H-p()iV1%Qs9JzlE0-Z`KoF+Id0-xHg+tg#kO-(8n3G8xyll%B$v*>F z{n9%|?N-_3MmMW$I0wCie+IyOthQ16^LD|lJ_1%IDw{jL02BTWkm6K|jTI9m{}SQ4 zHJeKkPBKHJ6qqb16Blnzk#2U4P(741J!3W5;5Ip;eqimYn|OC$OzedEUt66muDvLX zB0=RjsEbW>=MsC70HvZ=AP8|jj8&dW>_q~KBt-BQ76$gD4Zb)N+mfOO3u3Is)pgQe zhy@gNI`T?g8S>S4c-2B?LTw0)&+0q81i0%DTeFLz#eDo$-{D=2Mp_?3n20p+U44hQ z02h%AfcL;9#=!Z1xmql#nVkp2R%ZihP`jb2bD(X_6GKgE3vdS-JzR;5IjQ41CBZ&$ z+`sBxOTG`(b)Ax6J+Z>c4ltuq2-o>JE(9X8?!bvkHtMuPxK2sXZ)A?O_uby?3t;RG zThI3(AVIHH99l;QGn(>R32PR?cfc`iKd;@N(_u0@`K^$yQxbd!9PT{WMtFJ;YCo~& hroD*CFIln#;0L{PMRKTk+}Z#D002ovPDHLkV1j1Ah3EhP literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@3x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..8ce3cacae468cb11d2104c278519e6f23ebe27ff GIT binary patch literal 1267 zcmVFjQH%{1oNMm53_v38I_;`!%t$$_Y$P;JrZ< z3sQ;EPG?$1Wo%=!JKZxYSM9HgDhjk{`EPeme+i(Wp`oFn;d2F{CRVb?TL#jv$*NH4s&xnJ7hahtPcYR*cf*?si8P=HiEc+mCXH zL==Hq+2i$s(w~7~3sk^CETIepW-U9sU)ZG=dg3s2cgdzN*RuZi5SU;Vq;ECXg9xXu zak?AInt}+#;#iwzF37G!9P$e@ZHEv^U7#AonG-}LRe@?0XHK4&^!B*DKL#aKgR~3A zM>UGWxVSex0Y%gV;?v5VE_a}A+@Y{G{BaMIP;s#ZaIN3&VJf77GHoT*24bzPWSjzuUQ`U_uNxF0#97>xFJa%MHXZ% zY=S3c#9_`m{2ICko_t}ADD{xJ7@_Bz&MTCbT0EAQH%x3tj>- zXy9b^`QeS3_-q<$O$MG=YrOA~5%+^uhyA}ml#E?JC7l(*1@8G;&L!hXr+W(oq#y9oP`R@rRpf-A5{k@-s-sC8Y z`=?G{b-K4f0li|?qRJD~fPmEQMRj>$ZPsL47}{ zW-C&)9!9-kR<9N5zju%oyqZolb?ae*v|Oox_*O)?4*>`w(AiFRB%P~Q0|%4s2SJk6 zU`_i3r#vo>Qlt3{BFhp?ARhF&02e%YBx{3i+y@=|7?U0&sO|e)M}D^Rum=GNDiBxt zt}W}mnQ;>o@NdrvJ~-rnO%$R|!@=w@2uzg8YMLZHWmn5X2Ii|o5lG+FDB5SnqRr;D zT#bmH%oeI55p{V%!PW9K`KJdF2~vxFisfJ3yemN-1XiAg|>fAwda@0@KPsC_WbmxU0Bc_Y%BB?z)koSkkD8X6iJ d8X6h|{12&b+W
    jVG*002ovPDHLkV1gCWK;{4d literal 0 HcmV?d00001 diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 119391d9af..babe98289b 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1852,3 +1852,23 @@ Tap the + to start adding people."; "poll_timeline_not_closed_subtitle" = "Please try again"; "poll_timeline_not_closed_action" = "OK"; + +// MARK: - Location sharing + +"location_sharing_title" = "Location"; + +"location_sharing_close_action" = "Close"; + +"location_sharing_share_action" = "Share"; + +"location_sharing_loading_map_error_title" = "Failed to load map"; + +"location_sharing_loading_map_error_message" = "Please try again"; + +"location_sharing_locating_user_error_title" = "Failed locating user"; + +"location_sharing_locating_user_error_message" = "Please try again"; + +"location_sharing_invalid_authorization_error_title" = "Failed getting authorization"; + +"location_sharing_invalid_authorization_error_message" = "Please update your system settings"; diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index c8919d5b97..61d692691e 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -115,6 +115,7 @@ internal enum Asset { internal static let peopleFloatingAction = ImageAsset(name: "people_floating_action") internal static let actionCamera = ImageAsset(name: "action_camera") internal static let actionFile = ImageAsset(name: "action_file") + internal static let actionLocation = ImageAsset(name: "action_location") internal static let actionMediaLibrary = ImageAsset(name: "action_media_library") internal static let actionPoll = ImageAsset(name: "action_poll") internal static let actionSticker = ImageAsset(name: "action_sticker") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index af25f0458a..9fbe34e43c 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2191,6 +2191,42 @@ public class VectorL10n: NSObject { public static var less: String { return VectorL10n.tr("Vector", "less") } + /// Close + public static var locationSharingCloseAction: String { + return VectorL10n.tr("Vector", "location_sharing_close_action") + } + /// Please update your system settings + public static var locationSharingInvalidAuthorizationErrorMessage: String { + return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_error_message") + } + /// Failed getting authorization + public static var locationSharingInvalidAuthorizationErrorTitle: String { + return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_error_title") + } + /// Please try again + public static var locationSharingLoadingMapErrorMessage: String { + return VectorL10n.tr("Vector", "location_sharing_loading_map_error_message") + } + /// Failed to load map + public static var locationSharingLoadingMapErrorTitle: String { + return VectorL10n.tr("Vector", "location_sharing_loading_map_error_title") + } + /// Please try again + public static var locationSharingLocatingUserErrorMessage: String { + return VectorL10n.tr("Vector", "location_sharing_locating_user_error_message") + } + /// Failed locating user + public static var locationSharingLocatingUserErrorTitle: String { + return VectorL10n.tr("Vector", "location_sharing_locating_user_error_title") + } + /// Share + public static var locationSharingShareAction: String { + return VectorL10n.tr("Vector", "location_sharing_share_action") + } + /// Location + public static var locationSharingTitle: String { + return VectorL10n.tr("Vector", "location_sharing_title") + } /// Got it public static var majorUpdateDoneAction: String { return VectorL10n.tr("Vector", "major_update_done_action") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 3aa22a8427..b994217519 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -187,6 +187,9 @@ final class RiotSettings: NSObject { @UserDefault(key: "roomScreenAllowPollsAction", defaultValue: false, storage: defaults) var roomScreenAllowPollsAction + + @UserDefault(key: "roomScreenAllowLocationAction", defaultValue: true, storage: defaults) + var roomScreenAllowLocationAction @UserDefault(key: "roomScreenShowsURLPreviews", defaultValue: true, storage: defaults) var roomScreenShowsURLPreviews diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h index 8559fba2d8..0510033a67 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h @@ -591,6 +591,25 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; success:(void (^)(NSString *eventId))success failure:(void (^)(NSError *error))failure; +/** + Send a location message to a room. + + While sending, a fake event will be echoed in the messages list. + Once complete, this local echo will be replaced by the event saved by the homeserver. + + @param latitude the location's latitude + @param longitude the location's longitude + @param description an optional description + @param success A block object called when the operation succeeds. It returns + the event id of the event generated on the homeserver + @param failure A block object called when the operation fails. + */ +- (void)sendLocationWithLatitude:(double)latitude + longitude:(double)longitude + description:(NSString *)description + success:(void (^)(NSString *))success + failure:(void (^)(NSError *))failure; + /** Send a generic non state event to a room. diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index c07fcdce96..c439bb54bf 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -1914,6 +1914,29 @@ - (void)sendMessageWithContent:(NSDictionary *)msgContent success:(void (^)(NSSt } } +- (void)sendLocationWithLatitude:(double)latitude + longitude:(double)longitude + description:(NSString *)description + success:(void (^)(NSString *))success + failure:(void (^)(NSError *))failure +{ + __block MXEvent *localEchoEvent = nil; + + // Make the request to the homeserver + [_room sendLocationWithLatitude:latitude + longitude:longitude + description:description + localEcho:&localEchoEvent + success:success failure:failure]; + + if (localEchoEvent) + { + // Make the data source digest this fake local echo message + [self queueEventForProcessing:localEchoEvent withRoomState:self.roomState direction:MXTimelineDirectionForwards]; + [self processQueuedEvents:nil]; + } +} + - (void)sendEventOfType:(MXEventTypeString)eventTypeString content:(NSDictionary*)msgContent success:(void (^)(NSString *eventId))success failure:(void (^)(NSError *error))failure { __block MXEvent *localEchoEvent = nil; diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift index efb1a1bd31..4e39b9d258 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -32,6 +32,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { // MARK: Properties // MARK: - Private + @IBOutlet private var descriptionContainerView: UIView! @IBOutlet private var descriptionLabel: UILabel! private var mapView: MGLMapView! @@ -39,21 +40,13 @@ class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { // MARK: - Public - var showUserLocation: Bool { - get { - mapView.showsUserLocation - } - set { - mapView.showsUserLocation = newValue - } - } - var locationDescription: String? { get { descriptionLabel.text } set { descriptionLabel.text = newValue + descriptionContainerView.isHidden = (newValue?.count ?? 0 == 0) } } @@ -70,31 +63,22 @@ class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { vc_addSubViewMatchingParent(mapView) sendSubviewToBack(mapView) - descriptionLabel.textColor = ThemeService.shared().theme.colors.primaryContent - descriptionLabel.font = ThemeService.shared().theme.fonts.footnote - clipsToBounds = true - layer.borderColor = ThemeService.shared().theme.colors.quinaryContent.cgColor layer.borderWidth = Constants.cellBorderRadius layer.cornerRadius = Constants.cellCornerRadius + + NotificationCenter.default.addObserver(self, selector: #selector(updateTheme), name: .themeServiceDidChangeTheme, object: nil) + updateTheme() } // MARK: - Public - public func displayGeoURI(_ geoURI: String, + public func displayLocation(_ location: CLLocationCoordinate2D, userIdentifier: String, userDisplayName: String, userAvatarURL: String, mediaManager: MXMediaManager) { - let locationString = geoURI.components(separatedBy: ":").last?.components(separatedBy: ";").first - - guard let locationComponents = locationString?.components(separatedBy: ","), - let latitude = locationComponents.first?.double, - let longitude = locationComponents.last?.double - else { - return - } - + annotationView = LocationUserMarkerView.loadFromNib() annotationView?.setAvatarData(AvatarViewData(matrixItemId: userIdentifier, @@ -103,8 +87,6 @@ class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { mediaManager: mediaManager, fallbackImage: .matrixItem(userIdentifier, userDisplayName))) - let location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) - if let annotations = mapView.annotations { mapView.removeAnnotations(annotations) } @@ -121,4 +103,12 @@ class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { return annotationView } + + // MARK: - Private + + @objc private func updateTheme() { + descriptionLabel.textColor = ThemeService.shared().theme.colors.primaryContent + descriptionLabel.font = ThemeService.shared().theme.fonts.footnote + layer.borderColor = ThemeService.shared().theme.colors.quinaryContent.cgColor + } } diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib index 1c4f0923a7..8beacacbe4 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib @@ -57,6 +57,7 @@ + diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index 7f860168f0..b61f79e980 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -31,6 +31,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { private var selectedEventId: String? private var pollEditFormCoordinator: PollEditFormCoordinator? + private var locationSharingCoordinator: LocationSharingCoordinator? private var roomDataSourceManager: MXKRoomDataSourceManager { return MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.parameters.session) @@ -267,6 +268,26 @@ extension RoomCoordinator: RoomViewControllerDelegate { pollEditFormCoordinator?.start() } + func roomViewControllerDidRequestLocationSharingFormPresentation(_ roomViewController: RoomViewController) { + guard #available(iOS 14.0, *) else { + return + } + + guard let navigationRouter = self.navigationRouter, let mediaManager = mxSession?.mediaManager, let user = mxSession?.myUser else { + MXLog.error("[RoomCoordinator] Invalid location sharing coordinator parameters. Returning.") + return + } + + let parameters = LocationSharingCoordinatorParameters(navigationRouter: navigationRouter, + roomDataSource: roomViewController.roomDataSource, + mediaManager: mediaManager, + user: user) + + locationSharingCoordinator = LocationSharingCoordinator(parameters: parameters) + + locationSharingCoordinator?.start() + } + func roomViewController(_ roomViewController: RoomViewController, canEndPollWithEventIdentifier eventIdentifier: String) -> Bool { guard #available(iOS 14.0, *) else { return false diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 74941c31ea..ed7be6cf49 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -183,6 +183,13 @@ handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters; */ - (void)roomViewControllerDidRequestPollCreationFormPresentation:(RoomViewController *)roomViewController; +/** + Ask the coordinator to invoke the location sharing form coordinator. + + @param roomViewController the `RoomViewController` instance. + */ +- (void)roomViewControllerDidRequestLocationSharingFormPresentation:(RoomViewController *)roomViewController; + - (BOOL)roomViewController:(RoomViewController *)roomViewController canEndPollWithEventIdentifier:(NSString *)eventIdentifier; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 6de22da8a2..586e16248f 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -2036,6 +2036,16 @@ - (void)setupActions { [self.delegate roomViewControllerDidRequestPollCreationFormPresentation:self]; }]]; } + if (RiotSettings.shared.roomScreenAllowLocationAction) + { + [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_location"] andAction:^{ + MXStrongifyAndReturnIfNil(self); + if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) { + ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO; + } + [self.delegate roomViewControllerDidRequestLocationSharingFormPresentation:self]; + }]]; + } if (RiotSettings.shared.roomScreenAllowCameraAction) { [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_camera"] andAction:^{ diff --git a/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift index c095b61b35..74907a7f05 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift @@ -34,13 +34,24 @@ class LocationBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable return } + let locationString = geoURI.components(separatedBy: ":").last?.components(separatedBy: ";").first + + guard let locationComponents = locationString?.components(separatedBy: ","), + let latitude = locationComponents.first?.double, + let longitude = locationComponents.last?.double + else { + return + } + + let location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + locationView.locationDescription = content[kMXMessageContentKeyExtensibleLocationDescription] - locationView.displayGeoURI(geoURI, - userIdentifier: bubbleData.senderId, - userDisplayName: bubbleData.senderDisplayName, - userAvatarURL: bubbleData.senderAvatarUrl, - mediaManager: bubbleData.mxSession.mediaManager) + locationView.displayLocation(location, + userIdentifier: bubbleData.senderId, + userDisplayName: bubbleData.senderDisplayName, + userAvatarURL: bubbleData.senderAvatarUrl, + mediaManager: bubbleData.mxSession.mediaManager) } override func setupViews() { diff --git a/Riot/target.yml b/Riot/target.yml index 3c50467b8c..81a912fbc0 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -1,11 +1,5 @@ name: Riot -packages: - Mapbox: - url: https://github.com/maplibre/maplibre-gl-native-distribution - minVersion: 5.12.2 - maxVersion: 5.13.0 - schemes: Riot: analyze: @@ -41,6 +35,7 @@ targets: - target: SiriIntents - target: RiotNSE - target: DesignKit + - package: Mapbox configFiles: Debug: Debug.xcconfig diff --git a/RiotSwiftUI/Info.plist b/RiotSwiftUI/Info.plist index 0a5393324e..a748828933 100644 --- a/RiotSwiftUI/Info.plist +++ b/RiotSwiftUI/Info.plist @@ -20,5 +20,7 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) + NSLocationWhenInUseUsageDescription + Element needs access to your location before being able to share it. diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index 2a11753a67..f64a1a51fc 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -20,6 +20,7 @@ import Foundation @available(iOS 14.0, *) enum MockAppScreens { static let appScreens: [MockScreenState.Type] = [ + MockLocationSharingScreenState.self, MockAnalyticsPromptScreenState.self, MockUserSuggestionScreenState.self, MockPollEditFormScreenState.self, diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift new file mode 100644 index 0000000000..337705e521 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift @@ -0,0 +1,101 @@ +// +// Copyright 2021 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 +import SwiftUI + +struct LocationSharingCoordinatorParameters { + let navigationRouter: NavigationRouterType + let roomDataSource: MXKRoomDataSource + let mediaManager: MXMediaManager + let user: MXUser +} + +final class LocationSharingCoordinator: Coordinator { + + // MARK: - Properties + + // MARK: Private + + private let parameters: LocationSharingCoordinatorParameters + private let locationSharingHostingController: UIViewController + private var _locationSharingViewModel: Any? = nil + + @available(iOS 14.0, *) + fileprivate var locationSharingViewModel: LocationSharingViewModel { + return _locationSharingViewModel as! LocationSharingViewModel + } + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: LocationSharingCoordinatorParameters) { + self.parameters = parameters + + let avatarData = AvatarInput(mxContentUri: parameters.user.avatarUrl, + matrixItemId: parameters.user.userId, + displayName: parameters.user.displayname) + + let viewModel = LocationSharingViewModel(accessToken: "bDAfUcrMPWTAB1KB38r6", avatarData: avatarData) + let view = LocationSharingView(context: viewModel.context) + .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) + + _locationSharingViewModel = viewModel + locationSharingHostingController = VectorHostingController(rootView: view) + } + + // MARK: - Public + func start() { + guard #available(iOS 14.0, *) else { + MXLog.error("[LocationSharingCoordinator] start: Invalid iOS version, returning.") + return + } + + parameters.navigationRouter.present(locationSharingHostingController, animated: true) + + locationSharingViewModel.completion = { [weak self] result in + guard let self = self else { return } + + switch result { + case .cancel: + self.parameters.navigationRouter.dismissModule(animated: true, completion: nil) + case .share(let latitude, let longitude): + self.locationSharingViewModel.dispatch(action: .startLoading) + + self.parameters.roomDataSource.sendLocation(withLatitude: latitude, + longitude: longitude, + description: nil) { [weak self] _ in + guard let self = self else { return } + + self.parameters.navigationRouter.dismissModule(animated: true, completion: nil) + self.locationSharingViewModel.dispatch(action: .stopLoading(nil)) + } failure: { [weak self] error in + guard let self = self else { return } + + MXLog.error("[LocationSharingCoordinator] Failed sharing location with error: \(String(describing: error))") + self.locationSharingViewModel.dispatch(action: .stopLoading(error)) + } + } + + } + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift new file mode 100644 index 0000000000..88310f4d1d --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift @@ -0,0 +1,76 @@ +// +// Copyright 2021 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 SwiftUI +import Combine +import CoreLocation + +enum LocationSharingViewError { + case failedLoadingMap + case failedLocatingUser + case invalidLocationAuthorization + case failedSharingLocation +} + +enum LocationSharingStateAction { + case error(LocationSharingViewError, LocationSharingViewModelCallback?) + case startLoading + case stopLoading(Error?) +} + +enum LocationSharingViewAction { + case cancel + case share +} + +typealias LocationSharingViewModelCallback = ((LocationSharingViewModelResult) -> Void) + +enum LocationSharingViewModelResult { + case cancel + case share(Double, Double) +} + +@available(iOS 14, *) +struct LocationSharingViewState: BindableState { + let accessToken: String + let avatarData: AvatarInputProtocol + var shareButtonEnabled: Bool = true + var showLoadingIndicator: Bool = false + + let errorSubject = PassthroughSubject() + + var bindings = LocationSharingViewStateBindings() +} + +struct LocationSharingViewStateBindings { + var alertInfo: ErrorAlertInfo? + var userLocation: CLLocationCoordinate2D? +} + +struct ErrorAlertInfo: Identifiable { + enum AlertType { + case mapLoadingError + case userLocatingError + case authorizationError + case locationSharingError + } + + let id: AlertType + let title: String + let message: String + let callback: (() -> Void)? +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift new file mode 100644 index 0000000000..44699462b9 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift @@ -0,0 +1,35 @@ +// +// Copyright 2021 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 SwiftUI + +@available(iOS 14.0, *) +enum MockLocationSharingScreenState: MockScreenState, CaseIterable { + case standard + + var screenType: Any.Type { + MockLocationSharingScreenState.self + } + + var screenView: ([Any], AnyView) { + let viewModel = LocationSharingViewModel(accessToken: "bDAfUcrMPWTAB1KB38r6", + avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: "Alice")) + return ([viewModel], + AnyView(LocationSharingView(context: viewModel.context) + .addDependency(MockAvatarService.example))) + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift new file mode 100644 index 0000000000..b4b85f29cd --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -0,0 +1,111 @@ +// +// Copyright 2021 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 Combine + +@available(iOS 14, *) +typealias LocationSharingViewModelType = StateStoreViewModel< LocationSharingViewState, + LocationSharingStateAction, + LocationSharingViewAction > +@available(iOS 14, *) +class LocationSharingViewModel: LocationSharingViewModelType { + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + let accessToken: String + let avatarData: AvatarInputProtocol + + var completion: ((LocationSharingViewModelResult) -> Void)? + + // MARK: - Setup + + init(accessToken: String, avatarData: AvatarInputProtocol) { + self.accessToken = accessToken + self.avatarData = avatarData + + super.init(initialViewState: LocationSharingViewState(accessToken: accessToken, avatarData: avatarData)) + + state.errorSubject.sink { [weak self] error in + guard let self = self else { return } + self.dispatch(action: .error(error, self.completion)) + }.store(in: &cancellables) + } + + // MARK: - Public + + override func process(viewAction: LocationSharingViewAction) { + switch viewAction { + case .cancel: + completion?(.cancel) + case .share: + guard let location = state.bindings.userLocation else { + dispatch(action: .error(.failedLocatingUser, completion)) + return + } + + completion?(.share(location.latitude, location.longitude)) + } + } + + override class func reducer(state: inout LocationSharingViewState, action: LocationSharingStateAction) { + switch action { + case .error(let error, let completion): + + let alertCallback: () -> Void = { + completion?(.cancel) + } + + switch error { + case .failedLoadingMap: + state.bindings.alertInfo = ErrorAlertInfo(id: .mapLoadingError, + title: VectorL10n.locationSharingLoadingMapErrorTitle, + message: VectorL10n.locationSharingLoadingMapErrorMessage, + callback: alertCallback) + case .failedLocatingUser: + state.bindings.alertInfo = ErrorAlertInfo(id: .userLocatingError, + title: VectorL10n.locationSharingLocatingUserErrorTitle, + message: VectorL10n.locationSharingLocatingUserErrorMessage, + callback: alertCallback) + case .invalidLocationAuthorization: + state.bindings.alertInfo = ErrorAlertInfo(id: .authorizationError, + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle, + message: VectorL10n.locationSharingInvalidAuthorizationErrorMessage, + callback: alertCallback) + default: + break + } + + case .startLoading: + state.showLoadingIndicator = true + state.shareButtonEnabled = false + case .stopLoading(let error): + state.showLoadingIndicator = false + state.shareButtonEnabled = true + + if error != nil { + state.bindings.alertInfo = ErrorAlertInfo(id: .locationSharingError, + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle, + message: VectorL10n.locationSharingInvalidAuthorizationErrorMessage, + callback: nil) + } + } + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift new file mode 100644 index 0000000000..36838557dc --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift @@ -0,0 +1,36 @@ +// +// Copyright 2021 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 XCTest +import RiotSwiftUI + +@available(iOS 14.0, *) +class LocationSharingUITests: XCTestCase { + + private var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + app.buttons[MockLocationSharingScreenState.screenStateKeys.first!].tap() + } + + func testInitialStateComponents() { + + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift new file mode 100644 index 0000000000..702b336be3 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift @@ -0,0 +1,35 @@ +// +// Copyright 2021 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 XCTest +import Combine + +@testable import RiotSwiftUI + +@available(iOS 14.0, *) +class LocationSharingViewModelTests: XCTestCase { + var viewModel: LocationSharingViewModel! + var context: LocationSharingViewModelType.Context! + var cancellables = Set() + + override func setUpWithError() throws { + context = viewModel.context + } + + func testInitialState() { + + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift new file mode 100644 index 0000000000..aa515338d2 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift @@ -0,0 +1,128 @@ +// +// Copyright 2021 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 Combine +import Mapbox + +@available(iOS 14, *) +struct LocationSharingMapView: UIViewRepresentable { + private struct Constants { + static let mapZoomLevel = 15.0 + static let mapStyleURLString = "https://api.maptiler.com/maps/streets/style.json?key=" + } + + let accessToken: String + let avatarData: AvatarInputProtocol + let errorSubject: PassthroughSubject + @Binding var userLocation: CLLocationCoordinate2D? + + func makeUIView(context: Context) -> some UIView { + let url = URL(string: Constants.mapStyleURLString + accessToken) + + let mapView = MGLMapView(frame: .zero, styleURL: url) + mapView.delegate = context.coordinator + + mapView.logoView.isHidden = true + mapView.attributionButton.isHidden = true + mapView.showsUserLocation = true + mapView.userTrackingMode = .follow + + return mapView + } + + func updateUIView(_ uiView: UIViewType, context: Context) { + + } + + func makeCoordinator() -> LocationSharingMapViewCoordinator { + LocationSharingMapViewCoordinator(avatarData: avatarData, + errorSubject: errorSubject, + userLocation: $userLocation) + } +} + +@available(iOS 14, *) +class LocationSharingMapViewCoordinator: NSObject, MGLMapViewDelegate { + + private let avatarData: AvatarInputProtocol + private let errorSubject: PassthroughSubject + @Binding var userLocation: CLLocationCoordinate2D? + + init(avatarData: AvatarInputProtocol, + errorSubject: PassthroughSubject, + userLocation: Binding) { + self.avatarData = avatarData + self.errorSubject = errorSubject + self._userLocation = userLocation + } + + // MARK: - MGLMapViewDelegate + + func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { + guard let _ = annotation as? MGLUserLocation else { + return nil + } + + return UserLocationAnnotatonView(avatarData: avatarData) + } + + func mapViewDidFailLoadingMap(_ mapView: MGLMapView, withError error: Error) { + errorSubject.send(.failedLoadingMap) + } + + func mapView(_ mapView: MGLMapView, didFailToLocateUserWithError error: Error) { + errorSubject.send(.failedLocatingUser) + } + + func mapView(_ mapView: MGLMapView, didUpdate userLocation: MGLUserLocation?) { + self.userLocation = userLocation?.coordinate + } + + func mapView(_ mapView: MGLMapView, didChangeLocationManagerAuthorization manager: MGLLocationManager) { + switch manager.authorizationStatus { + case .restricted: + fallthrough + case .denied: + errorSubject.send(.failedLocatingUser) + default: + break + } + } +} + +@available(iOS 14, *) +private class UserLocationAnnotatonView: MGLUserLocationAnnotationView { + + init(avatarData: AvatarInputProtocol) { + super.init(frame: .zero) + + guard let avatarImageView = UIHostingController(rootView: LocationSharingUserMarkerView(avatarData: avatarData)).view else { + return + } + + addSubview(avatarImageView) + + addConstraints([topAnchor.constraint(equalTo: avatarImageView.topAnchor), + leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor), + bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), + trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor)]) + } + + required init?(coder: NSCoder) { + fatalError() + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift new file mode 100644 index 0000000000..0727e0b5bd --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift @@ -0,0 +1,53 @@ +// +// Copyright 2021 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 + +@available(iOS 14.0, *) +struct LocationSharingUserMarkerView: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + let avatarData: AvatarInputProtocol + + var body: some View { + ZStack(alignment: .center) { + Image(uiImage: Asset.Images.locationUserMarker.image) + AvatarImage(avatarData: avatarData, size: .large) + .offset(.init(width: 0.0, height: -1.5)) + } + .accentColor(theme.colors.accent) + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct LocationSharingUserMarkerView_Previews: PreviewProvider { + static var previews: some View { + let avatarData = AvatarInput(mxContentUri: "", + matrixItemId: "", + displayName: "Alice") + + LocationSharingUserMarkerView(avatarData: avatarData) + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift new file mode 100644 index 0000000000..f760c7dc34 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift @@ -0,0 +1,86 @@ +// +// Copyright 2021 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 CoreLocation + +@available(iOS 14.0, *) +struct LocationSharingView: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + @ObservedObject var context: LocationSharingViewModel.Context + + var body: some View { + NavigationView { + LocationSharingMapView(accessToken: context.viewState.accessToken, + avatarData: context.viewState.avatarData, + errorSubject: context.viewState.errorSubject, + userLocation: $context.userLocation) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button(VectorL10n.cancel, action: { + context.send(viewAction: .cancel) + }) + } + ToolbarItem(placement: .principal) { + Text(VectorL10n.locationSharingTitle) + .font(.headline) + .foregroundColor(theme.colors.primaryContent) + } + ToolbarItem(placement: .navigationBarTrailing) { + Button(VectorL10n.locationSharingShareAction, action: { + context.send(viewAction: .share) + }) + .disabled(!context.viewState.shareButtonEnabled) + } + } + .navigationBarTitleDisplayMode(.inline) + .ignoresSafeArea() + .alert(item: $context.alertInfo) { info in + Alert(title: Text(info.title), message: Text(info.message), dismissButton: + .default(Text(VectorL10n.pollTimelineVoteNotRegisteredAction)) { + info.callback?() + }) + } + } + .accentColor(theme.colors.accent) + .activityIndicator(show: context.viewState.showLoadingIndicator) + } + + @ViewBuilder + private var activityIndicator: some View { + if context.viewState.showLoadingIndicator { + ActivityIndicator() + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct LocationSharingView_Previews: PreviewProvider { + static let stateRenderer = MockLocationSharingScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml index ecbf8a91cf..ab1c2dd597 100644 --- a/RiotSwiftUI/target.yml +++ b/RiotSwiftUI/target.yml @@ -31,6 +31,7 @@ targets: platform: iOS dependencies: - target: DesignKit + - package: Mapbox sources: - path: . excludes: diff --git a/project.yml b/project.yml index 44c6eeb12d..dcd15f63b1 100644 --- a/project.yml +++ b/project.yml @@ -35,3 +35,9 @@ include: - path: RiotSwiftUI/target.yml - path: RiotSwiftUI/targetUnitTests.yml - path: RiotSwiftUI/targetUITests.yml + +packages: + Mapbox: + url: https://github.com/maplibre/maplibre-gl-native-distribution + minVersion: 5.12.2 + maxVersion: 5.13.0 \ No newline at end of file From f0cdbe5a7ccd6d721eacbed61280a36270b5e58d Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 16 Dec 2021 18:08:05 +0200 Subject: [PATCH 094/109] vector-im/element-ios/issues/5298 - Attempting to set up cocoapods-keys. --- .github/workflows/ci-build.yml | 1 + .github/workflows/ci-tests.yml | 1 + .github/workflows/release-alpha.yml | 1 + Gemfile | 1 + Gemfile.lock | 69 +++++++++++-------- Podfile | 4 ++ .../Location/RoomTimelineLocationView.swift | 3 +- .../LocationSharingCoordinator.swift | 3 +- 8 files changed, 51 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 24e8ab2e6d..68a169237a 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -11,6 +11,7 @@ on: env: # Make the git branch for a PR available to our Fastfile MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + MapTilerAPIKey: ${{ secrets.MAPTILER_API_KEY }} jobs: build: diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 00d4fa2f9c..3fcc19ed32 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -12,6 +12,7 @@ on: env: # Make the git branch for a PR available to our Fastfile MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + MapTilerAPIKey: ${{ secrets.MAPTILER_API_KEY }} jobs: tests: diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml index dfbd7ac6a9..7ab8b331dc 100644 --- a/.github/workflows/release-alpha.yml +++ b/.github/workflows/release-alpha.yml @@ -11,6 +11,7 @@ on: env: # Make the git branch for a PR available to our Fastfile MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + MapTilerAPIKey: ${{ secrets.MAPTILER_API_KEY }} jobs: build: diff --git a/Gemfile b/Gemfile index 53efbaf921..ea061a17e6 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,7 @@ source "https://rubygems.org" gem "xcode-install" gem "fastlane" gem "cocoapods", '~>1.11.2' +gem "cocoapods-keys" plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index a8674758c3..78fe028a3a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,12 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.4) + CFPropertyList (3.0.5) rexml - activesupport (6.1.4.1) + RubyInline (3.12.5) + ZenTest (~> 4.3) + ZenTest (4.12.0) + activesupport (6.1.4.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -17,17 +20,17 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.510.0) - aws-sdk-core (3.121.1) + aws-partitions (1.541.0) + aws-sdk-core (3.124.0) aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.239.0) + aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.48.0) - aws-sdk-core (~> 3, >= 3.120.0) + aws-sdk-kms (1.52.0) + aws-sdk-core (~> 3, >= 3.122.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.103.0) - aws-sdk-core (~> 3, >= 3.120.0) + aws-sdk-s3 (1.109.0) + aws-sdk-core (~> 3, >= 3.122.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.4.0) @@ -64,6 +67,9 @@ GEM typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) cocoapods-downloader (1.5.1) + cocoapods-keys (2.2.1) + dotenv + osx_keychain cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -84,9 +90,9 @@ GEM dotenv (2.7.6) emoji_regex (3.2.3) escape (0.0.4) - ethon (0.14.0) + ethon (0.15.0) ffi (>= 1.15.0) - excon (0.86.0) + excon (0.89.0) faraday (1.8.0) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -109,10 +115,10 @@ GEM faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - faraday_middleware (1.1.0) + faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.5) - fastlane (2.195.0) + fastimage (2.2.6) + fastlane (2.199.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -161,7 +167,7 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.11.0) + google-apis-androidpublisher_v3 (0.14.0) google-apis-core (>= 0.4, < 2.a) google-apis-core (0.4.1) addressable (~> 2.5, >= 2.5.1) @@ -172,11 +178,11 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.7.0) + google-apis-iamcredentials_v1 (0.9.0) google-apis-core (>= 0.4, < 2.a) - google-apis-playcustomapp_v1 (0.5.0) + google-apis-playcustomapp_v1 (0.6.0) google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.8.0) + google-apis-storage_v1 (0.10.0) google-apis-core (>= 0.4, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) @@ -184,15 +190,15 @@ GEM google-cloud-env (1.5.0) faraday (>= 0.17.3, < 2.0) google-cloud-errors (1.2.0) - google-cloud-storage (1.34.1) - addressable (~> 2.5) + google-cloud-storage (1.35.0) + addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) google-apis-storage_v1 (~> 0.1) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.0.0) + googleauth (1.1.0) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -204,18 +210,18 @@ GEM http-cookie (1.0.4) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.8.10) + i18n (1.8.11) concurrent-ruby (~> 1.0) jmespath (1.4.0) - json (2.5.1) + json (2.6.1) jwt (2.3.0) memoist (0.16.2) - mime-types (3.3.1) + mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2021.0901) + mime-types-data (3.2021.1115) mini_magick (4.11.0) - mini_mime (1.1.1) - minitest (5.14.4) + mini_mime (1.1.2) + minitest (5.15.0) molinillo (0.8.0) multi_json (1.15.0) multipart-post (2.0.0) @@ -224,7 +230,9 @@ GEM naturally (2.2.1) netrc (0.11.0) optparse (0.1.1) - os (1.1.1) + os (1.1.4) + osx_keychain (1.0.2) + RubyInline (~> 3) plist (3.6.0) public_suffix (4.0.6) rake (13.0.6) @@ -255,7 +263,7 @@ GEM terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) - trailblazer-option (0.1.1) + trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.1) tty-spinner (0.9.3) @@ -285,13 +293,14 @@ GEM rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) - zeitwerk (2.4.2) + zeitwerk (2.5.1) PLATFORMS ruby DEPENDENCIES cocoapods (~> 1.11.2) + cocoapods-keys fastlane fastlane-plugin-diawi fastlane-plugin-versioning diff --git a/Podfile b/Podfile index 7141544611..c4dc2f06e1 100644 --- a/Podfile +++ b/Podfile @@ -129,6 +129,10 @@ abstract_target 'RiotPods' do end +plugin 'cocoapods-keys', { + :project => "Riot", + :keys => ["MapTilerAPIKey"] +} post_install do |installer| installer.pods_project.targets.each do |target| diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift index 4e39b9d258..75c6da7927 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -17,12 +17,13 @@ import UIKit import Reusable import Mapbox +import Keys class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { private struct Constants { static let mapHeight: CGFloat = 300.0 - static let mapTilerKey = "bDAfUcrMPWTAB1KB38r6" + static let mapTilerKey = RiotKeys().mapTilerAPIKey static let mapZoomLevel = 15.0 static let mapStyleURLString = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=\(Constants.mapTilerKey)") static let cellBorderRadius: CGFloat = 1.0 diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift index 337705e521..dc77cd6c32 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift @@ -17,6 +17,7 @@ import Foundation import UIKit import SwiftUI +import Keys struct LocationSharingCoordinatorParameters { let navigationRouter: NavigationRouterType @@ -55,7 +56,7 @@ final class LocationSharingCoordinator: Coordinator { matrixItemId: parameters.user.userId, displayName: parameters.user.displayname) - let viewModel = LocationSharingViewModel(accessToken: "bDAfUcrMPWTAB1KB38r6", avatarData: avatarData) + let viewModel = LocationSharingViewModel(accessToken: RiotKeys().mapTilerAPIKey, avatarData: avatarData) let view = LocationSharingView(context: viewModel.context) .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) From 352d56726e7669ac287b7fb46691df6e11402bb9 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 17 Dec 2021 14:27:43 +0200 Subject: [PATCH 095/109] vector-im/element-ios/issues/5298 - Improved error messages and moved map configuration to the BuildSettings --- Config/BuildSettings.swift | 5 +++ Riot/Assets/en.lproj/InfoPlist.strings | 2 +- Riot/Assets/en.lproj/Vector.strings | 19 +++------ Riot/Generated/Strings.swift | 40 +++++++------------ .../Location/RoomTimelineLocationView.swift | 3 +- Riot/SupportingFiles/Info.plist | 2 +- RiotSwiftUI/Info.plist | 2 +- .../LocationSharingCoordinator.swift | 2 +- .../LocationSharingModels.swift | 6 +-- .../LocationSharingScreenState.swift | 4 +- .../LocationSharingViewModel.swift | 32 +++++++-------- .../Unit/LocationSharingViewModelTests.swift | 2 +- .../View/LocationSharingMapView.swift | 7 +--- .../View/LocationSharingView.swift | 20 +++++++--- .../Room/PollEditForm/View/PollEditForm.swift | 2 +- .../PollTimeline/View/PollTimelineView.swift | 4 +- 16 files changed, 73 insertions(+), 79 deletions(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index d07f6efb5c..7998caf91b 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -15,6 +15,7 @@ // import Foundation +import Keys /// BuildSettings provides settings computed at build time. /// In future, it may be automatically generated from xcconfig files @@ -364,4 +365,8 @@ final class BuildSettings: NSObject { return true } + + // MARK: - Location Sharing + + static let tileServerMapURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=" + RiotKeys().mapTilerAPIKey)! } diff --git a/Riot/Assets/en.lproj/InfoPlist.strings b/Riot/Assets/en.lproj/InfoPlist.strings index e8ac01167d..a9513a6ed0 100644 --- a/Riot/Assets/en.lproj/InfoPlist.strings +++ b/Riot/Assets/en.lproj/InfoPlist.strings @@ -21,4 +21,4 @@ "NSContactsUsageDescription" = "Element will show your contacts so you can invite them to chat."; "NSCalendarsUsageDescription" = "See your scheduled meetings in the app."; "NSFaceIDUsageDescription" = "Face ID is used to access your app."; -"NSLocationWhenInUseUsageDescription" = "Element needs access to your location before being able to share it."; +"NSLocationWhenInUseUsageDescription" = "When you share your location to people, Element needs access to show them a map."; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index babe98289b..51ed3037c3 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -66,6 +66,7 @@ "less" = "Less"; "open" = "Open"; "done" = "Done"; +"ok" = "OK"; // Call Bar "callbar_only_single_active" = "Tap to return to the call (%@)"; @@ -1821,8 +1822,6 @@ Tap the + to start adding people."; "poll_edit_form_post_failure_subtitle" = "Please try again"; -"poll_edit_form_post_failure_action" = "OK"; - "poll_timeline_one_vote" = "1 vote"; "poll_timeline_votes_count" = "%lu votes"; @@ -1845,14 +1844,10 @@ Tap the + to start adding people."; "poll_timeline_vote_not_registered_subtitle" = "Sorry, your vote was not registered, please try again"; -"poll_timeline_vote_not_registered_action" = "OK"; - "poll_timeline_not_closed_title" = "Failed to end poll"; "poll_timeline_not_closed_subtitle" = "Please try again"; -"poll_timeline_not_closed_action" = "OK"; - // MARK: - Location sharing "location_sharing_title" = "Location"; @@ -1861,14 +1856,12 @@ Tap the + to start adding people."; "location_sharing_share_action" = "Share"; -"location_sharing_loading_map_error_title" = "Failed to load map"; - -"location_sharing_loading_map_error_message" = "Please try again"; +"location_sharing_loading_map_error_title" = "Element could not load the map. Please try again later."; -"location_sharing_locating_user_error_title" = "Failed locating user"; +"location_sharing_locating_user_error_title" = "Element could not access your location. Please try again later."; -"location_sharing_locating_user_error_message" = "Please try again"; +"location_sharing_invalid_authorization_error_title" = "Element does not have permission to access your location. You can enable access in Settings > Location"; -"location_sharing_invalid_authorization_error_title" = "Failed getting authorization"; +"location_sharing_invalid_authorization_not_now" = "Not now"; -"location_sharing_invalid_authorization_error_message" = "Please update your system settings"; +"location_sharing_invalid_authorization_settings" = "Settings"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 9fbe34e43c..273a1b5be7 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2195,27 +2195,23 @@ public class VectorL10n: NSObject { public static var locationSharingCloseAction: String { return VectorL10n.tr("Vector", "location_sharing_close_action") } - /// Please update your system settings - public static var locationSharingInvalidAuthorizationErrorMessage: String { - return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_error_message") - } - /// Failed getting authorization + /// Element does not have permission to access your location. You can enable access in Settings > Location public static var locationSharingInvalidAuthorizationErrorTitle: String { return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_error_title") } - /// Please try again - public static var locationSharingLoadingMapErrorMessage: String { - return VectorL10n.tr("Vector", "location_sharing_loading_map_error_message") + /// Not now + public static var locationSharingInvalidAuthorizationNotNow: String { + return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_not_now") + } + /// Settings + public static var locationSharingInvalidAuthorizationSettings: String { + return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_settings") } - /// Failed to load map + /// Element could not load the map. Please try again later. public static var locationSharingLoadingMapErrorTitle: String { return VectorL10n.tr("Vector", "location_sharing_loading_map_error_title") } - /// Please try again - public static var locationSharingLocatingUserErrorMessage: String { - return VectorL10n.tr("Vector", "location_sharing_locating_user_error_message") - } - /// Failed locating user + /// Element could not access your location. Please try again later. public static var locationSharingLocatingUserErrorTitle: String { return VectorL10n.tr("Vector", "location_sharing_locating_user_error_title") } @@ -2327,6 +2323,10 @@ public class VectorL10n: NSObject { public static var off: String { return VectorL10n.tr("Vector", "off") } + /// OK + public static var ok: String { + return VectorL10n.tr("Vector", "ok") + } /// On public static var on: String { return VectorL10n.tr("Vector", "on") @@ -2479,10 +2479,6 @@ public class VectorL10n: NSObject { public static var pollEditFormPollQuestionOrTopic: String { return VectorL10n.tr("Vector", "poll_edit_form_poll_question_or_topic") } - /// OK - public static var pollEditFormPostFailureAction: String { - return VectorL10n.tr("Vector", "poll_edit_form_post_failure_action") - } /// Please try again public static var pollEditFormPostFailureSubtitle: String { return VectorL10n.tr("Vector", "poll_edit_form_post_failure_subtitle") @@ -2495,10 +2491,6 @@ public class VectorL10n: NSObject { public static var pollEditFormQuestionOrTopic: String { return VectorL10n.tr("Vector", "poll_edit_form_question_or_topic") } - /// OK - public static var pollTimelineNotClosedAction: String { - return VectorL10n.tr("Vector", "poll_timeline_not_closed_action") - } /// Please try again public static var pollTimelineNotClosedSubtitle: String { return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle") @@ -2539,10 +2531,6 @@ public class VectorL10n: NSObject { public static func pollTimelineTotalVotesNotVoted(_ p1: Int) -> String { return VectorL10n.tr("Vector", "poll_timeline_total_votes_not_voted", p1) } - /// OK - public static var pollTimelineVoteNotRegisteredAction: String { - return VectorL10n.tr("Vector", "poll_timeline_vote_not_registered_action") - } /// Sorry, your vote was not registered, please try again public static var pollTimelineVoteNotRegisteredSubtitle: String { return VectorL10n.tr("Vector", "poll_timeline_vote_not_registered_subtitle") diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift index 75c6da7927..4e945e1687 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -25,7 +25,6 @@ class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { static let mapHeight: CGFloat = 300.0 static let mapTilerKey = RiotKeys().mapTilerAPIKey static let mapZoomLevel = 15.0 - static let mapStyleURLString = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=\(Constants.mapTilerKey)") static let cellBorderRadius: CGFloat = 1.0 static let cellCornerRadius: CGFloat = 8.0 } @@ -54,7 +53,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { override func awakeFromNib() { super.awakeFromNib() - mapView = MGLMapView(frame: .zero, styleURL: Constants.mapStyleURLString) + mapView = MGLMapView(frame: .zero, styleURL: BuildSettings.tileServerMapURL) mapView.delegate = self mapView.logoView.isHidden = true mapView.attributionButton.isHidden = true diff --git a/Riot/SupportingFiles/Info.plist b/Riot/SupportingFiles/Info.plist index 4c4d11e2f0..9d95fe9298 100644 --- a/Riot/SupportingFiles/Info.plist +++ b/Riot/SupportingFiles/Info.plist @@ -66,7 +66,7 @@ NSSiriUsageDescription Siri is used to perform calls even from the lock screen. NSLocationWhenInUseUsageDescription - Element needs access to your location before being able to share it. + When you share your location to people, Element needs access to show them a map. UIBackgroundModes audio diff --git a/RiotSwiftUI/Info.plist b/RiotSwiftUI/Info.plist index a748828933..c72d62e92f 100644 --- a/RiotSwiftUI/Info.plist +++ b/RiotSwiftUI/Info.plist @@ -21,6 +21,6 @@ CFBundleVersion $(CURRENT_PROJECT_VERSION) NSLocationWhenInUseUsageDescription - Element needs access to your location before being able to share it. + When you share your location to people, Element needs access to show them a map. diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift index dc77cd6c32..bcff3fbca1 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift @@ -56,7 +56,7 @@ final class LocationSharingCoordinator: Coordinator { matrixItemId: parameters.user.userId, displayName: parameters.user.displayname) - let viewModel = LocationSharingViewModel(accessToken: RiotKeys().mapTilerAPIKey, avatarData: avatarData) + let viewModel = LocationSharingViewModel(tileServerMapURL: BuildSettings.tileServerMapURL, avatarData: avatarData) let view = LocationSharingView(context: viewModel.context) .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift index 88310f4d1d..f4ceb2941e 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift @@ -46,7 +46,7 @@ enum LocationSharingViewModelResult { @available(iOS 14, *) struct LocationSharingViewState: BindableState { - let accessToken: String + let tileServerMapURL: URL let avatarData: AvatarInputProtocol var shareButtonEnabled: Bool = true var showLoadingIndicator: Bool = false @@ -71,6 +71,6 @@ struct ErrorAlertInfo: Identifiable { let id: AlertType let title: String - let message: String - let callback: (() -> Void)? + let primaryButton: (String, (() -> Void)?) + let secondaryButton: (String, (() -> Void)?)? } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift index 44699462b9..01c77b09d2 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift @@ -16,6 +16,7 @@ import Foundation import SwiftUI +import Keys @available(iOS 14.0, *) enum MockLocationSharingScreenState: MockScreenState, CaseIterable { @@ -26,7 +27,8 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable { } var screenView: ([Any], AnyView) { - let viewModel = LocationSharingViewModel(accessToken: "bDAfUcrMPWTAB1KB38r6", + let mapURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=" + RiotKeys().mapTilerAPIKey)! + let viewModel = LocationSharingViewModel(tileServerMapURL: mapURL, avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: "Alice")) return ([viewModel], AnyView(LocationSharingView(context: viewModel.context) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift index b4b85f29cd..b4dda5af39 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -30,18 +30,18 @@ class LocationSharingViewModel: LocationSharingViewModelType { // MARK: Public - let accessToken: String + let tileServerMapURL: URL let avatarData: AvatarInputProtocol var completion: ((LocationSharingViewModelResult) -> Void)? // MARK: - Setup - init(accessToken: String, avatarData: AvatarInputProtocol) { - self.accessToken = accessToken + init(tileServerMapURL: URL, avatarData: AvatarInputProtocol) { + self.tileServerMapURL = tileServerMapURL self.avatarData = avatarData - super.init(initialViewState: LocationSharingViewState(accessToken: accessToken, avatarData: avatarData)) + super.init(initialViewState: LocationSharingViewState(tileServerMapURL: tileServerMapURL, avatarData: avatarData)) state.errorSubject.sink { [weak self] error in guard let self = self else { return } @@ -69,26 +69,26 @@ class LocationSharingViewModel: LocationSharingViewModelType { switch action { case .error(let error, let completion): - let alertCallback: () -> Void = { - completion?(.cancel) - } - switch error { case .failedLoadingMap: state.bindings.alertInfo = ErrorAlertInfo(id: .mapLoadingError, title: VectorL10n.locationSharingLoadingMapErrorTitle, - message: VectorL10n.locationSharingLoadingMapErrorMessage, - callback: alertCallback) + primaryButton: (VectorL10n.ok, { completion?(.cancel) }), + secondaryButton: nil) case .failedLocatingUser: state.bindings.alertInfo = ErrorAlertInfo(id: .userLocatingError, title: VectorL10n.locationSharingLocatingUserErrorTitle, - message: VectorL10n.locationSharingLocatingUserErrorMessage, - callback: alertCallback) + primaryButton: (VectorL10n.ok, { completion?(.cancel) }), + secondaryButton: nil) case .invalidLocationAuthorization: state.bindings.alertInfo = ErrorAlertInfo(id: .authorizationError, title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle, - message: VectorL10n.locationSharingInvalidAuthorizationErrorMessage, - callback: alertCallback) + primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, { completion?(.cancel) }), + secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, { + if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { + UIApplication.shared.open(applicationSettingsURL) + } + })) default: break } @@ -103,8 +103,8 @@ class LocationSharingViewModel: LocationSharingViewModelType { if error != nil { state.bindings.alertInfo = ErrorAlertInfo(id: .locationSharingError, title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle, - message: VectorL10n.locationSharingInvalidAuthorizationErrorMessage, - callback: nil) + primaryButton: (VectorL10n.ok, nil), + secondaryButton: nil) } } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift index 702b336be3..dc51dd83ea 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift @@ -26,7 +26,7 @@ class LocationSharingViewModelTests: XCTestCase { var cancellables = Set() override func setUpWithError() throws { - context = viewModel.context + } func testInitialState() { diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift index aa515338d2..edbeb4298b 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift @@ -22,18 +22,15 @@ import Mapbox struct LocationSharingMapView: UIViewRepresentable { private struct Constants { static let mapZoomLevel = 15.0 - static let mapStyleURLString = "https://api.maptiler.com/maps/streets/style.json?key=" } - let accessToken: String + let tileServerMapURL: URL let avatarData: AvatarInputProtocol let errorSubject: PassthroughSubject @Binding var userLocation: CLLocationCoordinate2D? func makeUIView(context: Context) -> some UIView { - let url = URL(string: Constants.mapStyleURLString + accessToken) - - let mapView = MGLMapView(frame: .zero, styleURL: url) + let mapView = MGLMapView(frame: .zero, styleURL: tileServerMapURL) mapView.delegate = context.coordinator mapView.logoView.isHidden = true diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift index f760c7dc34..caa4e5846d 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift @@ -32,7 +32,7 @@ struct LocationSharingView: View { var body: some View { NavigationView { - LocationSharingMapView(accessToken: context.viewState.accessToken, + LocationSharingMapView(tileServerMapURL: context.viewState.tileServerMapURL, avatarData: context.viewState.avatarData, errorSubject: context.viewState.errorSubject, userLocation: $context.userLocation) @@ -57,10 +57,20 @@ struct LocationSharingView: View { .navigationBarTitleDisplayMode(.inline) .ignoresSafeArea() .alert(item: $context.alertInfo) { info in - Alert(title: Text(info.title), message: Text(info.message), dismissButton: - .default(Text(VectorL10n.pollTimelineVoteNotRegisteredAction)) { - info.callback?() - }) + if let secondaryButton = info.secondaryButton { + return Alert(title: Text(info.title), + primaryButton: .default(Text(info.primaryButton.0)) { + info.primaryButton.1?() + }, + secondaryButton: .default(Text(secondaryButton.0)) { + secondaryButton.1?() + }) + } else { + return Alert(title: Text(info.title), + dismissButton: .default(Text(info.primaryButton.0)) { + info.primaryButton.1?() + }) + } } } .accentColor(theme.colors.accent) diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift index 9e81488a56..57f3a63a3e 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift @@ -87,7 +87,7 @@ struct PollEditForm: View { .alert(isPresented: $viewModel.showsFailureAlert) { Alert(title: Text(VectorL10n.pollEditFormPostFailureTitle), message: Text(VectorL10n.pollEditFormPostFailureSubtitle), - dismissButton: .default(Text(VectorL10n.pollEditFormPostFailureAction))) + dismissButton: .default(Text(VectorL10n.ok))) } .frame(minHeight: proxy.size.height) // Make the VStack fill the ScrollView's parent .toolbar { diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift b/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift index 22a94bb686..70efcc0670 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift +++ b/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift @@ -50,7 +50,7 @@ struct PollTimelineView: View { .alert(isPresented: $viewModel.showsClosingFailureAlert) { Alert(title: Text(VectorL10n.pollTimelineNotClosedTitle), message: Text(VectorL10n.pollTimelineNotClosedSubtitle), - dismissButton: .default(Text(VectorL10n.pollTimelineNotClosedAction))) + dismissButton: .default(Text(VectorL10n.ok))) } } .disabled(poll.closed) @@ -62,7 +62,7 @@ struct PollTimelineView: View { .alert(isPresented: $viewModel.showsAnsweringFailureAlert) { Alert(title: Text(VectorL10n.pollTimelineVoteNotRegisteredTitle), message: Text(VectorL10n.pollTimelineVoteNotRegisteredSubtitle), - dismissButton: .default(Text(VectorL10n.pollTimelineVoteNotRegisteredAction))) + dismissButton: .default(Text(VectorL10n.ok))) } } .padding([.horizontal, .top], 2.0) From e6379c8588865262afa6d110f154191dda147038 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 17 Dec 2021 14:48:42 +0200 Subject: [PATCH 096/109] vector-im/element-ios/issues/5298 - Disable forwarding for location messages and allow replies. --- Riot/Generated/MatrixKitStrings.swift | 4 ++++ .../Assets/MatrixKitAssets.bundle/en.lproj/MatrixKit.strings | 1 + .../MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.m | 5 +++++ Riot/Modules/Room/RoomViewController.m | 4 +++- 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Riot/Generated/MatrixKitStrings.swift b/Riot/Generated/MatrixKitStrings.swift index e5c1e7f311..b178f6d8dd 100644 --- a/Riot/Generated/MatrixKitStrings.swift +++ b/Riot/Generated/MatrixKitStrings.swift @@ -695,6 +695,10 @@ public class MatrixKitL10n: NSObject { public static var messageReplyToSenderSentAnImage: String { return MatrixKitL10n.tr("message_reply_to_sender_sent_an_image") } + /// has shared their location. + public static var messageReplyToSenderSentTheirLocation: String { + return MatrixKitL10n.tr("message_reply_to_sender_sent_their_location") + } /// There are unsaved changes. Leaving will discard them. public static var messageUnsavedChanges: String { return MatrixKitL10n.tr("message_unsaved_changes") diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/en.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/en.lproj/MatrixKit.strings index f9e40406a6..b7937f1941 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/en.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/en.lproj/MatrixKit.strings @@ -258,6 +258,7 @@ "message_reply_to_sender_sent_an_audio_file" = "sent an audio file."; "message_reply_to_sender_sent_a_voice_message" = "sent a voice message."; "message_reply_to_sender_sent_a_file" = "sent a file."; +"message_reply_to_sender_sent_their_location" = "has shared their location."; "message_reply_to_message_to_reply_to_prefix" = "In reply to"; // Room members diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.m b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.m index f116c466b5..b592db0d64 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.m @@ -45,6 +45,11 @@ - (NSString *)senderSentAFile return [MatrixKitL10n messageReplyToSenderSentAFile]; } +- (NSString *)senderSentTheirLocation +{ + return [MatrixKitL10n messageReplyToSenderSentTheirLocation]; +} + - (NSString *)messageToReplyToPrefix { return [MatrixKitL10n messageReplyToMessageToReplyToPrefix]; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 586e16248f..925dfc86ab 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -3277,7 +3277,9 @@ - (void)showAdditionalActionsMenuForEvent:(MXEvent*)selectedEvent inCell:(id Date: Mon, 20 Dec 2021 17:33:59 +0200 Subject: [PATCH 097/109] vector-im/element-ios/issues/5298 - Displaying a modal when tapping on a location in the timeline. --- .../Room/CellData/RoomBubbleCellData.m | 4 +- .../Location/RoomTimelineLocationView.swift | 5 +- Riot/Modules/Room/RoomCoordinator.swift | 63 ++++++++++++++----- Riot/Modules/Room/RoomViewController.h | 11 ++++ Riot/Modules/Room/RoomViewController.m | 8 ++- .../Location/LocationBubbleCell.swift | 19 ++---- .../LocationSharingCoordinator.swift | 11 ++-- .../LocationSharingModels.swift | 11 +++- .../LocationSharingViewModel.swift | 13 +--- .../View/LocationSharingMapView.swift | 19 ++++-- .../View/LocationSharingView.swift | 11 ++-- 11 files changed, 110 insertions(+), 65 deletions(-) diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 3d69115f1e..81867be76e 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -179,7 +179,7 @@ - (instancetype)initWithEvent:(MXEvent *)event andRoomState:(MXRoomState *)roomS } case MXEventTypeRoomMessage: { - if (event.hasLocation) { + if (event.location) { self.tag = RoomBubbleCellDataTagLocation; self.collapsable = NO; self.collapsed = NO; @@ -874,7 +874,7 @@ - (BOOL)addEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState { case MXEventTypeRoomMessage: { - if (event.hasLocation) { + if (event.location) { shouldAddEvent = NO; break; } diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift index 4e945e1687..cf44b6efda 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -57,6 +57,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { mapView.delegate = self mapView.logoView.isHidden = true mapView.attributionButton.isHidden = true + mapView.isUserInteractionEnabled = false mapView.translatesAutoresizingMaskIntoConstraints = false mapView.addConstraint(mapView.heightAnchor.constraint(equalToConstant: Constants.mapHeight)) @@ -76,14 +77,14 @@ class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { public func displayLocation(_ location: CLLocationCoordinate2D, userIdentifier: String, userDisplayName: String, - userAvatarURL: String, + userAvatarURLString: String?, mediaManager: MXMediaManager) { annotationView = LocationUserMarkerView.loadFromNib() annotationView?.setAvatarData(AvatarViewData(matrixItemId: userIdentifier, displayName: userDisplayName, - avatarUrl: userAvatarURL, + avatarUrl: userAvatarURLString, mediaManager: mediaManager, fallbackImage: .matrixItem(userIdentifier, userDisplayName))) diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index b61f79e980..a79af06f15 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -269,23 +269,11 @@ extension RoomCoordinator: RoomViewControllerDelegate { } func roomViewControllerDidRequestLocationSharingFormPresentation(_ roomViewController: RoomViewController) { - guard #available(iOS 14.0, *) else { - return - } - - guard let navigationRouter = self.navigationRouter, let mediaManager = mxSession?.mediaManager, let user = mxSession?.myUser else { - MXLog.error("[RoomCoordinator] Invalid location sharing coordinator parameters. Returning.") - return - } - - let parameters = LocationSharingCoordinatorParameters(navigationRouter: navigationRouter, - roomDataSource: roomViewController.roomDataSource, - mediaManager: mediaManager, - user: user) - - locationSharingCoordinator = LocationSharingCoordinator(parameters: parameters) - - locationSharingCoordinator?.start() + startLocationCoordinatorWithEvent() + } + + func roomViewController(_ roomViewController: RoomViewController, didRequestLocationPresentationFor event: MXEvent, bubbleData: MXKRoomBubbleCellDataStoring) { + startLocationCoordinatorWithEvent(event, bubbleData: bubbleData) } func roomViewController(_ roomViewController: RoomViewController, canEndPollWithEventIdentifier eventIdentifier: String) -> Bool { @@ -303,4 +291,45 @@ extension RoomCoordinator: RoomViewControllerDelegate { PollTimelineProvider.shared.pollTimelineCoordinatorForEventIdentifier(eventIdentifier)?.endPoll() } + + // MARK: - Private + + private func startLocationCoordinatorWithEvent(_ event: MXEvent? = nil, bubbleData: MXKRoomBubbleCellDataStoring? = nil) { + guard #available(iOS 14.0, *) else { + return + } + + guard let navigationRouter = self.navigationRouter, + let mediaManager = mxSession?.mediaManager, + let user = mxSession?.myUser else { + MXLog.error("[RoomCoordinator] Invalid location sharing coordinator parameters. Returning.") + return + } + + var avatarData: AvatarInputProtocol + if event != nil, let bubbleData = bubbleData { + avatarData = AvatarInput(mxContentUri: bubbleData.senderAvatarUrl, + matrixItemId: bubbleData.senderId, + displayName: bubbleData.senderDisplayName) + } else { + avatarData = AvatarInput(mxContentUri: user.avatarUrl, + matrixItemId: user.userId, + displayName: user.displayname) + } + + var location: CLLocationCoordinate2D? + if let locationContent = event?.location { + location = CLLocationCoordinate2D(latitude: locationContent.latitude, longitude: locationContent.longitude) + } + + let parameters = LocationSharingCoordinatorParameters(navigationRouter: navigationRouter, + roomDataSource: roomViewController.roomDataSource, + mediaManager: mediaManager, + avatarData: avatarData, + location: location) + + locationSharingCoordinator = LocationSharingCoordinator(parameters: parameters) + + locationSharingCoordinator?.start() + } } diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index ed7be6cf49..f3b9f6fa61 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -190,6 +190,17 @@ handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters; */ - (void)roomViewControllerDidRequestLocationSharingFormPresentation:(RoomViewController *)roomViewController; +/** + Ask the coordinator to invoke the location sharing form coordinator. + + @param roomViewController the `RoomViewController` instance. + @param event the event containing location information + @param bubbleData the bubble data containing sender details + */ +- (void)roomViewController:(RoomViewController *)roomViewController +didRequestLocationPresentationForEvent:(MXEvent *)event + bubbleData:(id)bubbleData; + - (BOOL)roomViewController:(RoomViewController *)roomViewController canEndPollWithEventIdentifier:(NSString *)eventIdentifier; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 925dfc86ab..10af1f3bcf 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -2960,7 +2960,11 @@ - (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)ac } else { - [self showContextualMenuForEvent:tappedEvent fromSingleTapGesture:YES cell:cell animated:YES]; + if (tappedEvent.location) { + [_delegate roomViewController:self didRequestLocationPresentationForEvent:tappedEvent bubbleData:bubbleData]; + } else { + [self showContextualMenuForEvent:tappedEvent fromSingleTapGesture:YES cell:cell animated:YES]; + } } } } @@ -3279,7 +3283,7 @@ - (void)showAdditionalActionsMenuForEvent:(MXEvent*)selectedEvent inCell:(id() diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift index b4dda5af39..a63061c8b0 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -30,18 +30,13 @@ class LocationSharingViewModel: LocationSharingViewModelType { // MARK: Public - let tileServerMapURL: URL - let avatarData: AvatarInputProtocol - var completion: ((LocationSharingViewModelResult) -> Void)? // MARK: - Setup - init(tileServerMapURL: URL, avatarData: AvatarInputProtocol) { - self.tileServerMapURL = tileServerMapURL - self.avatarData = avatarData - - super.init(initialViewState: LocationSharingViewState(tileServerMapURL: tileServerMapURL, avatarData: avatarData)) + init(tileServerMapURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D? = nil) { + let viewState = LocationSharingViewState(tileServerMapURL: tileServerMapURL, avatarData: avatarData, location: location) + super.init(initialViewState: viewState) state.errorSubject.sink { [weak self] error in guard let self = self else { return } @@ -95,10 +90,8 @@ class LocationSharingViewModel: LocationSharingViewModelType { case .startLoading: state.showLoadingIndicator = true - state.shareButtonEnabled = false case .stopLoading(let error): state.showLoadingIndicator = false - state.shareButtonEnabled = true if error != nil { state.bindings.alertInfo = ErrorAlertInfo(id: .locationSharingError, diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift index edbeb4298b..53064ae546 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift @@ -26,6 +26,8 @@ struct LocationSharingMapView: UIViewRepresentable { let tileServerMapURL: URL let avatarData: AvatarInputProtocol + let location: CLLocationCoordinate2D? + let errorSubject: PassthroughSubject @Binding var userLocation: CLLocationCoordinate2D? @@ -35,8 +37,17 @@ struct LocationSharingMapView: UIViewRepresentable { mapView.logoView.isHidden = true mapView.attributionButton.isHidden = true - mapView.showsUserLocation = true - mapView.userTrackingMode = .follow + + if let location = location { + mapView.setCenter(location, zoomLevel: Constants.mapZoomLevel, animated: false) + + let pointAnnotation = MGLPointAnnotation() + pointAnnotation.coordinate = location + mapView.addAnnotation(pointAnnotation) + } else { + mapView.showsUserLocation = true + mapView.userTrackingMode = .follow + } return mapView } @@ -70,10 +81,6 @@ class LocationSharingMapViewCoordinator: NSObject, MGLMapViewDelegate { // MARK: - MGLMapViewDelegate func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { - guard let _ = annotation as? MGLUserLocation else { - return nil - } - return UserLocationAnnotatonView(avatarData: avatarData) } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift index caa4e5846d..6cfd32efc3 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift @@ -34,6 +34,7 @@ struct LocationSharingView: View { NavigationView { LocationSharingMapView(tileServerMapURL: context.viewState.tileServerMapURL, avatarData: context.viewState.avatarData, + location: context.viewState.location, errorSubject: context.viewState.errorSubject, userLocation: $context.userLocation) .toolbar { @@ -48,10 +49,12 @@ struct LocationSharingView: View { .foregroundColor(theme.colors.primaryContent) } ToolbarItem(placement: .navigationBarTrailing) { - Button(VectorL10n.locationSharingShareAction, action: { - context.send(viewAction: .share) - }) - .disabled(!context.viewState.shareButtonEnabled) + if context.viewState.shareButtonVisible { + Button(VectorL10n.locationSharingShareAction, action: { + context.send(viewAction: .share) + }) + .disabled(!context.viewState.shareButtonEnabled) + } } } .navigationBarTitleDisplayMode(.inline) From 8e629a145f5d5eeaeb2a998270986075d326325a Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Mon, 20 Dec 2021 17:34:45 +0200 Subject: [PATCH 098/109] vector-im/element-ios/issues/5298 - Custom notification text for shared locations. --- Riot/Assets/en.lproj/Localizable.strings | 3 +++ RiotNSE/NotificationService.swift | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/Riot/Assets/en.lproj/Localizable.strings b/Riot/Assets/en.lproj/Localizable.strings index 914bb11e28..f3547d6673 100644 --- a/Riot/Assets/en.lproj/Localizable.strings +++ b/Riot/Assets/en.lproj/Localizable.strings @@ -71,6 +71,9 @@ /* New file message from a specific person, not referencing a room. */ "FILE_FROM_USER" = "%@ sent a file %@"; +/* New file message from a specific person, not referencing a room. */ +"LOCATION_FROM_USER" = "%@ shared their location"; + /* A single unread message in a room */ "SINGLE_UNREAD_IN_ROOM" = "You received a message in %@"; diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 5547807521..c77a9dd86b 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -401,6 +401,11 @@ class NotificationService: UNNotificationServiceExtension { break } + if event.location != nil { + notificationBody = NSString.localizedUserNotificationString(forKey: "LOCATION_FROM_USER", arguments: [eventSenderName]) + break + } + switch msgType { case kMXMessageTypeEmote: notificationBody = NSString.localizedUserNotificationString(forKey: "ACTION_FROM_USER", arguments: [eventSenderName, messageContent as Any]) From a27dd1a2304718bdd318a471336a7ffba77f8fd8 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 21 Dec 2021 14:42:01 +0200 Subject: [PATCH 099/109] vector-im/element-ios/issues/5298 - Allow sharing locations to other system apps. --- .../Contents.json | 23 +++++ .../location_share_icon.png | Bin 0 -> 384 bytes .../location_share_icon@2x.png | Bin 0 -> 596 bytes .../location_share_icon@3x.png | Bin 0 -> 819 bytes Riot/Assets/en.lproj/Vector.strings | 4 + Riot/Generated/Images.swift | 1 + Riot/Generated/Strings.swift | 8 ++ .../LocationSharingCoordinator.swift | 87 ++++++++++++++++++ .../LocationSharingModels.swift | 6 +- .../LocationSharingViewModel.swift | 8 +- .../View/LocationSharingView.swift | 21 +++-- 11 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@3x.png diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/Contents.json new file mode 100644 index 0000000000..c3b8dbc7a6 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "location_share_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_share_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_share_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon.png b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..da7d7530b0c29f7c1709dc416eb14400c22c9b7b GIT binary patch literal 384 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1!HlL zyA#8@b22Z19F}xPUq=Rpjs4tz5?O(AqMj~}Ar*{wCpl&vau8{|Kikh+QM9P#&Vuk4 z7n--YuehMMt9!NHn?>O-I7{@6xb|Np;N2sdPl z|B_0cdu-ZbK?9ZSiL00XGPASPKd|kNoAL9yd$N-89@+~Q$~Y+~Ex0o!Sn z@0FZ5Dm-cBUsv0|zu8#wl=}Gh>&ERb;Lmzt^seYq>#slS<*tRb9LWsc*)F88ZtW_z z2ZwIN7HU7ZVrA{nHeyS3j3^P6-=jAU@#mv1T<0e9v#Z4w`@Xj^y=R9K?7O@e!^oTX+)u?LqfU0 zU*VHx8YGkoq=io!Dwa?tkR^Q5RH1|tfjr@p#)>2g3&?~|nk$f?P=IaWk+D7@uvjZb z(&oSZdL8uY-k=KwkQPV_|8{oQ+pG_)V8$YJL9v9CK;G~%@8`G5C8Pu_B;&l{W6~rV z-Z1w%lAS_Q;bXQ*$RH(vh<(0&E%-j-GX|YJ;bUTn)pb@k!FM~kg|t1))t?7*L}YtJ z7=dlrq22ubpP#~7&Jr6mK`j{aleArhPoF`p%U@UyOMu^Tho7YLNMDO3W(|~$5hxoY zP&P*)Q267MPO`j_&7dq0pF|}>0gHrJP|7F}Kd_=}QI$}@=btt#8n*ZaC$JNU&qw_w ipg~K=mpp^vzwrgxf%Vc_Jy)dw00003xjk<=kY4cNz!FSPHbY_dgLOjGZ#}z$71fLE0z;f`_>gN@oO|YarXxeS)aE|xY;X?Py>V9KY!wKc8>p3eH-yR9NBM?BvBf#G_G+=45C2X167 zxVhlm(=U%x&i>x3T*;i(-CXPXZ;v%!*AvzgqJ4TQlZq9tTkP8A5&6-Wx!scW)DyWc zjf=CJkNkYBXmLjK!W{l@*Y~`6k}l~iq*KYuSInhip)DO(RCR8Xn5RizbEseAleYNv z7Uds{oUS*7teWakFypi1uYYepHs0x#zq)Cqjjz3w3jg|=xo@>pDv#78IL*=2k)JPm zeA4UnFXsvvMAgN~Rq8wI@2r}i^~ILSNl8KFwV{Ugr=G*-T>4~xFOZYIVsg!-*Ee_a z>qxc&(}L(v|8C?u3_=M&((zs^POepd)~1JEuRY}>`!M- zvGq4*yS4XM*#%>nxmRt@N^}^SHO@;j28vewiu-QT{c%#=vxyuWoedMvh(!!uuS-}a zim?VvI`I4Hb1`v6*B_pRT!Jd$?0oG [Any] { + var items = [Any]() + + // Make the share sheet show a pretty location thumbnail + if let url = NSURL(string: "https://maps.apple.com?ll=\(location.latitude),\(location.longitude)") { + items.append(url) + } + + return items + } +} + +extension UIActivity.ActivityType { + static let shareToMapsApp = UIActivity.ActivityType("Element.ShareToMapsApp") +} + +class ShareToMapsAppActivity: UIActivity { + + enum MapsAppType { + case apple + case google + } + + let type: MapsAppType + let location: CLLocationCoordinate2D + + private override init() { + fatalError() + } + + init(type: MapsAppType, location: CLLocationCoordinate2D) { + self.type = type + self.location = location + } + + override var activityTitle: String? { + switch type { + case .apple: + return VectorL10n.locationSharingOpenAppleMaps + case .google: + return VectorL10n.locationSharingOpenGoogleMaps + } + } + + var activityCategory: UIActivity.Category { + return .action + } + + override var activityType: UIActivity.ActivityType { + return .shareToMapsApp + } + + override func canPerform(withActivityItems activityItems: [Any]) -> Bool { + return true + } + + override func prepare(withActivityItems activityItems: [Any]) { + var url: URL? + switch type { + case .apple: + url = URL(string: "https://maps.apple.com?ll=\(location.latitude),\(location.longitude)&q=Pin") + case .google: + url = URL(string: "https://www.google.com/maps/search/?api=1&query=\(location.latitude),\(location.longitude)") + } + + guard let url = url else { + activityDidFinish(false) + return + } + + UIApplication.shared.open(url, options: [:]) { [weak self] result in + self?.activityDidFinish(result) + } + } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift index 28f553c170..cdb8b62031 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift @@ -41,7 +41,7 @@ typealias LocationSharingViewModelCallback = ((LocationSharingViewModelResult) - enum LocationSharingViewModelResult { case cancel - case share(Double, Double) + case share(latitude: Double, longitude: Double) } @available(iOS 14, *) @@ -80,6 +80,6 @@ struct ErrorAlertInfo: Identifiable { let id: AlertType let title: String - let primaryButton: (String, (() -> Void)?) - let secondaryButton: (String, (() -> Void)?)? + let primaryButton: (title: String, action: (() -> Void)?) + let secondaryButton: (title: String, action: (() -> Void)?)? } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift index a63061c8b0..dd275f7a2f 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -16,6 +16,7 @@ import SwiftUI import Combine +import CoreLocation @available(iOS 14, *) typealias LocationSharingViewModelType = StateStoreViewModel< LocationSharingViewState, @@ -51,12 +52,17 @@ class LocationSharingViewModel: LocationSharingViewModelType { case .cancel: completion?(.cancel) case .share: + if let location = state.location { + completion?(.share(latitude: location.latitude, longitude: location.longitude)) + return + } + guard let location = state.bindings.userLocation else { dispatch(action: .error(.failedLocatingUser, completion)) return } - completion?(.share(location.latitude, location.longitude)) + completion?(.share(latitude: location.latitude, longitude: location.longitude)) } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift index 6cfd32efc3..9bc9325361 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift @@ -49,7 +49,14 @@ struct LocationSharingView: View { .foregroundColor(theme.colors.primaryContent) } ToolbarItem(placement: .navigationBarTrailing) { - if context.viewState.shareButtonVisible { + if context.viewState.location != nil { + Button { + context.send(viewAction: .share) + } label: { + Image(uiImage: Asset.Images.locationShareIcon.image) + } + .disabled(!context.viewState.shareButtonEnabled) + } else { Button(VectorL10n.locationSharingShareAction, action: { context.send(viewAction: .share) }) @@ -62,16 +69,16 @@ struct LocationSharingView: View { .alert(item: $context.alertInfo) { info in if let secondaryButton = info.secondaryButton { return Alert(title: Text(info.title), - primaryButton: .default(Text(info.primaryButton.0)) { - info.primaryButton.1?() + primaryButton: .default(Text(info.primaryButton.title)) { + info.primaryButton.action?() }, - secondaryButton: .default(Text(secondaryButton.0)) { - secondaryButton.1?() + secondaryButton: .default(Text(secondaryButton.title)) { + secondaryButton.action?() }) } else { return Alert(title: Text(info.title), - dismissButton: .default(Text(info.primaryButton.0)) { - info.primaryButton.1?() + dismissButton: .default(Text(info.primaryButton.title)) { + info.primaryButton.action?() }) } } From 28cb2d23d1446156fc0d0889eca740cd06e16b80 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 22 Dec 2021 09:17:51 +0200 Subject: [PATCH 100/109] vector-im/element-ios/issues/5298 - Settings screen toggle for disabling location sharing. --- Riot/Assets/en.lproj/Vector.strings | 4 ++ Riot/Generated/Strings.swift | 8 +++ .../Modules/Settings/SettingsViewController.m | 60 ++++++++++++++----- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 2338e1e41f..2283209f68 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1869,3 +1869,7 @@ Tap the + to start adding people."; "location_sharing_open_apple_maps" = "Open in Apple Maps"; "location_sharing_open_google_maps" = "Open in Google Maps"; + +"location_sharing_settings_header" = "Location sharing"; + +"location_sharing_settings_toggle_title" = "Enable location sharing"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index e4821ff3fc..615ce91ed3 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2223,6 +2223,14 @@ public class VectorL10n: NSObject { public static var locationSharingOpenGoogleMaps: String { return VectorL10n.tr("Vector", "location_sharing_open_google_maps") } + /// Location sharing + public static var locationSharingSettingsHeader: String { + return VectorL10n.tr("Vector", "location_sharing_settings_header") + } + /// Enable location sharing + public static var locationSharingSettingsToggleTitle: String { + return VectorL10n.tr("Vector", "location_sharing_settings_toggle_title") + } /// Share public static var locationSharingShareAction: String { return VectorL10n.tr("Vector", "location_sharing_share_action") diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 9aeaa77b72..aed8be3be9 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -47,10 +47,11 @@ NSString* const kSettingsViewControllerPhoneBookCountryCellId = @"kSettingsViewControllerPhoneBookCountryCellId"; -enum +typedef NS_ENUM(NSUInteger, SECTION_TAG) { SECTION_TAG_SIGN_OUT = 0, SECTION_TAG_USER_SETTINGS, + SECTION_TAG_LOCATION_SHARING, SECTION_TAG_SENDING_MEDIA, SECTION_TAG_LINKS, SECTION_TAG_SECURITY, @@ -69,7 +70,7 @@ SECTION_TAG_DEACTIVATE_ACCOUNT }; -enum +typedef NS_ENUM(NSUInteger, USER_SETTINGS_INDEX) { USER_SETTINGS_PROFILE_PICTURE_INDEX = 0, USER_SETTINGS_DISPLAYNAME_INDEX, @@ -80,24 +81,29 @@ USER_SETTINGS_ADD_PHONENUMBER_INDEX }; -enum +typedef NS_ENUM(NSUInteger, USER_SETTINGS_OFFSET) { USER_SETTINGS_EMAILS_OFFSET = 2000, USER_SETTINGS_PHONENUMBERS_OFFSET = 1000 }; -enum +typedef NS_ENUM(NSUInteger, LOCATION_SHARING) +{ + LOCATION_SHARING_ENABLED +}; + +typedef NS_ENUM(NSUInteger, SENDING_MEDIA) { SENDING_MEDIA_CONFIRM_SIZE = 0 }; -enum +typedef NS_ENUM(NSUInteger, LINKS_SHOW_URL_PREVIEWS) { LINKS_SHOW_URL_PREVIEWS_INDEX = 0, LINKS_SHOW_URL_PREVIEWS_DESCRIPTION_INDEX }; -enum +typedef NS_ENUM(NSUInteger, NOTIFICATION_SETTINGS) { NOTIFICATION_SETTINGS_ENABLE_PUSH_INDEX = 0, NOTIFICATION_SETTINGS_SYSTEM_SETTINGS, @@ -109,33 +115,34 @@ NOTIFICATION_SETTINGS_OTHER_SETTINGS_INDEX, }; -enum +typedef NS_ENUM(NSUInteger, CALLS_ENABLE_STUN_SERVER) { CALLS_ENABLE_STUN_SERVER_FALLBACK_INDEX = 0 }; -enum +typedef NS_ENUM(NSUInteger, INTEGRATIONS) { INTEGRATIONS_INDEX }; -enum { +typedef NS_ENUM(NSUInteger, LOCAL_CONTACTS) +{ LOCAL_CONTACTS_SYNC_INDEX, LOCAL_CONTACTS_PHONEBOOK_COUNTRY_INDEX }; -enum +typedef NS_ENUM(NSUInteger, USER_INTERFACE) { USER_INTERFACE_LANGUAGE_INDEX = 0, USER_INTERFACE_THEME_INDEX }; -enum +typedef NS_ENUM(NSUInteger, IDENTITY_SERVER) { IDENTITY_SERVER_INDEX }; -enum +typedef NS_ENUM(NSUInteger, ADVANCED) { ADVANCED_SHOW_NSFW_ROOMS_INDEX = 0, ADVANCED_CRASH_REPORT_INDEX, @@ -145,7 +152,7 @@ ADVANCED_REPORT_BUG_INDEX, }; -enum +typedef NS_ENUM(NSUInteger, ABOUT) { ABOUT_COPYRIGHT_INDEX = 0, ABOUT_TERM_CONDITIONS_INDEX, @@ -159,7 +166,7 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE) LABS_ENABLE_POLLS }; -enum +typedef NS_ENUM(NSUInteger, SECURITY) { SECURITY_BUTTON_INDEX = 0, }; @@ -374,6 +381,11 @@ - (void)updateSections sectionUserSettings.headerTitle = [VectorL10n settingsUserSettings]; [tmpSections addObject:sectionUserSettings]; + Section *sectionLocationSharing = [Section sectionWithTag:SECTION_TAG_LOCATION_SHARING]; + [sectionLocationSharing addRowWithTag:LOCATION_SHARING_ENABLED]; + sectionLocationSharing.headerTitle = VectorL10n.locationSharingSettingsHeader.uppercaseString; + [tmpSections addObject:sectionLocationSharing]; + if (BuildSettings.settingsScreenShowConfirmMediaSize) { Section *sectionMedia = [Section sectionWithTag:SECTION_TAG_SENDING_MEDIA]; @@ -1942,6 +1954,21 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N cell = passwordCell; } } + else if (section == SECTION_TAG_LOCATION_SHARING) + { + if (row == LOCATION_SHARING_ENABLED) + { + MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = VectorL10n.locationSharingSettingsToggleTitle; + labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.roomScreenAllowLocationAction; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + labelAndSwitchCell.mxkSwitch.enabled = YES; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleLocationSharing:) forControlEvents:UIControlEventTouchUpInside]; + + cell = labelAndSwitchCell; + } + } else if (section == SECTION_TAG_SENDING_MEDIA) { if (row == SENDING_MEDIA_CONFIRM_SIZE) @@ -2964,6 +2991,11 @@ - (void)onRemove3PID:(NSIndexPath*)indexPath } } +- (void)toggleLocationSharing:(UISwitch *)sender +{ + RiotSettings.shared.roomScreenAllowLocationAction = sender.on; +} + - (void)toggleConfirmMediaSize:(UISwitch *)sender { RiotSettings.shared.showMediaCompressionPrompt = sender.on; From 2c6b286718face5165302938fd1480b9fb9a2d07 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 22 Dec 2021 14:12:42 +0200 Subject: [PATCH 101/109] vector-im/element-ios/issues/5298 - Added build setting + ui and unit tests. --- Config/BuildSettings.swift | 8 ++ Riot/Managers/Settings/RiotSettings.swift | 2 +- .../Location/LocationBubbleCell.swift | 3 +- .../Modules/Settings/SettingsViewController.m | 11 +- .../Test/UI/AnalyticsPromptUITests.swift | 2 +- .../LocationSharingModels.swift | 2 +- .../LocationSharingScreenState.swift | 13 ++- .../Test/UI/LocationSharingUITests.swift | 23 +++- .../Unit/LocationSharingViewModelTests.swift | 103 +++++++++++++++++- 9 files changed, 149 insertions(+), 18 deletions(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 7998caf91b..00e6138e5e 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -369,4 +369,12 @@ final class BuildSettings: NSObject { // MARK: - Location Sharing static let tileServerMapURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=" + RiotKeys().mapTilerAPIKey)! + + static var locationSharingEnabled: Bool { + guard #available(iOS 14, *) else { + return false + } + + return false + } } diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index b994217519..1813d88189 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -188,7 +188,7 @@ final class RiotSettings: NSObject { @UserDefault(key: "roomScreenAllowPollsAction", defaultValue: false, storage: defaults) var roomScreenAllowPollsAction - @UserDefault(key: "roomScreenAllowLocationAction", defaultValue: true, storage: defaults) + @UserDefault(key: "roomScreenAllowLocationAction", defaultValue: false, storage: defaults) var roomScreenAllowLocationAction @UserDefault(key: "roomScreenShowsURLPreviews", defaultValue: true, storage: defaults) diff --git a/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift index befca8096a..c7628ae137 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift @@ -50,7 +50,8 @@ class LocationBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable bubbleCellContentView?.showSenderInfo = true bubbleCellContentView?.showPaginationTitle = false - guard let contentView = bubbleCellContentView?.innerContentView else { + guard #available(iOS 14.0, *), + let contentView = bubbleCellContentView?.innerContentView else { return } diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index aed8be3be9..2970c51f7d 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -381,10 +381,13 @@ - (void)updateSections sectionUserSettings.headerTitle = [VectorL10n settingsUserSettings]; [tmpSections addObject:sectionUserSettings]; - Section *sectionLocationSharing = [Section sectionWithTag:SECTION_TAG_LOCATION_SHARING]; - [sectionLocationSharing addRowWithTag:LOCATION_SHARING_ENABLED]; - sectionLocationSharing.headerTitle = VectorL10n.locationSharingSettingsHeader.uppercaseString; - [tmpSections addObject:sectionLocationSharing]; + if (BuildSettings.locationSharingEnabled) + { + Section *sectionLocationSharing = [Section sectionWithTag:SECTION_TAG_LOCATION_SHARING]; + [sectionLocationSharing addRowWithTag:LOCATION_SHARING_ENABLED]; + sectionLocationSharing.headerTitle = VectorL10n.locationSharingSettingsHeader.uppercaseString; + [tmpSections addObject:sectionLocationSharing]; + } if (BuildSettings.settingsScreenShowConfirmMediaSize) { diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift index ada017da69..b8a38a1171 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift @@ -49,7 +49,7 @@ class AnalyticsPromptUITests: MockScreenTest { switch promptType { case .newUser: XCTAssertEqual(enableButton.label, VectorL10n.enable) - XCTAssertEqual(disableButton.label, VectorL10n.cancel) + XCTAssertEqual(disableButton.label, VectorL10n.locationSharingInvalidAuthorizationNotNow) case .upgrade: XCTAssertEqual(enableButton.label, VectorL10n.analyticsPromptYes) XCTAssertEqual(disableButton.label, VectorL10n.analyticsPromptStop) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift index cdb8b62031..be9c188f9c 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift @@ -53,7 +53,7 @@ struct LocationSharingViewState: BindableState { var showLoadingIndicator: Bool = false var shareButtonVisible: Bool { - (location == nil) + return location == nil } var shareButtonEnabled: Bool { diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift index 01c77b09d2..f316d761e0 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift @@ -17,19 +17,28 @@ import Foundation import SwiftUI import Keys +import CoreLocation @available(iOS 14.0, *) enum MockLocationSharingScreenState: MockScreenState, CaseIterable { - case standard + case shareUserLocation + case displayExistingLocation var screenType: Any.Type { MockLocationSharingScreenState.self } var screenView: ([Any], AnyView) { + + var location: CLLocationCoordinate2D? + if self == .displayExistingLocation { + location = CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096) + } + let mapURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=" + RiotKeys().mapTilerAPIKey)! let viewModel = LocationSharingViewModel(tileServerMapURL: mapURL, - avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: "Alice")) + avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: "Alice"), + location: location) return ([viewModel], AnyView(LocationSharingView(context: viewModel.context) .addDependency(MockAvatarService.example))) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift index 36838557dc..6e68f35569 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift @@ -24,13 +24,30 @@ class LocationSharingUITests: XCTestCase { override func setUp() { continueAfterFailure = false - + app = XCUIApplication() app.launch() - app.buttons[MockLocationSharingScreenState.screenStateKeys.first!].tap() } - func testInitialStateComponents() { + func testInitialUserLocation() { + goToScreenWithIdentifier(MockLocationSharingScreenState.shareUserLocation.title) + + XCTAssertTrue(app.buttons["Cancel"].exists) + XCTAssertTrue(app.buttons["Share"].exists) + XCTAssertTrue(app.otherElements["Map"].exists) + } + + func testInitialExistingLocation() { + goToScreenWithIdentifier(MockLocationSharingScreenState.displayExistingLocation.title) + XCTAssertTrue(app.buttons["Cancel"].exists) + XCTAssertTrue(app.buttons["location share icon"].exists) + XCTAssertTrue(app.otherElements["Map"].exists) + } + + // Need a delay when showing the map otherwise the simulator breaks + private func goToScreenWithIdentifier(_ identifier: String) { + app.goToScreenWithIdentifier(identifier) + sleep(2) } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift index dc51dd83ea..e3371727c9 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift @@ -16,20 +16,113 @@ import XCTest import Combine +import CoreLocation @testable import RiotSwiftUI @available(iOS 14.0, *) class LocationSharingViewModelTests: XCTestCase { - var viewModel: LocationSharingViewModel! - var context: LocationSharingViewModelType.Context! + var cancellables = Set() - override func setUpWithError() throws { + func testInitialState() { + let viewModel = buildViewModel(withLocation: false) + + XCTAssertTrue(viewModel.context.viewState.shareButtonEnabled) + XCTAssertTrue(viewModel.context.viewState.shareButtonVisible) + XCTAssertFalse(viewModel.context.viewState.showLoadingIndicator) + XCTAssertNotNil(viewModel.context.viewState.tileServerMapURL) + XCTAssertNotNil(viewModel.context.viewState.avatarData) + + XCTAssertNil(viewModel.context.viewState.location) + XCTAssertNil(viewModel.context.viewState.bindings.userLocation) + XCTAssertNil(viewModel.context.viewState.bindings.alertInfo) } - func testInitialState() { - + func testCancellation() { + let viewModel = buildViewModel(withLocation: false) + + let expectation = self.expectation(description: "Cancellation completion should be invoked") + + viewModel.completion = { result in + switch result { + case .share: + XCTFail() + case .cancel: + expectation.fulfill() + } + } + + viewModel.context.send(viewAction: .cancel) + + waitForExpectations(timeout: 3) + } + + func testShareNoUserLocation() { + let viewModel = buildViewModel(withLocation: false) + + XCTAssertNil(viewModel.context.viewState.bindings.userLocation) + XCTAssertNil(viewModel.context.viewState.location) + + viewModel.context.send(viewAction: .share) + + XCTAssertNotNil(viewModel.context.viewState.bindings.alertInfo) + XCTAssertEqual(viewModel.context.viewState.bindings.alertInfo?.id, .userLocatingError) + } + + func testShareExistingLocation() { + let viewModel = buildViewModel(withLocation: true) + + let expectation = self.expectation(description: "Share completion should be invoked") + + viewModel.completion = { result in + switch result { + case .share(let latitude, let longitude): + XCTAssertEqual(latitude, viewModel.context.viewState.location?.latitude) + XCTAssertEqual(longitude, viewModel.context.viewState.location?.longitude) + expectation.fulfill() + case .cancel: + XCTFail() + } + } + + XCTAssertNil(viewModel.context.viewState.bindings.userLocation) + XCTAssertNotNil(viewModel.context.viewState.location) + + viewModel.context.send(viewAction: .share) + + XCTAssertNil(viewModel.context.viewState.bindings.alertInfo) + + waitForExpectations(timeout: 3) + } + + func testLoading() { + let viewModel = buildViewModel(withLocation: false) + + viewModel.dispatch(action: .startLoading) + + XCTAssertFalse(viewModel.context.viewState.shareButtonEnabled) + XCTAssertTrue(viewModel.context.viewState.showLoadingIndicator) + + viewModel.dispatch(action: .stopLoading(nil)) + + XCTAssertTrue(viewModel.context.viewState.shareButtonEnabled) + XCTAssertFalse(viewModel.context.viewState.showLoadingIndicator) + } + + func testInvalidLocationAuthorization() { + let viewModel = buildViewModel(withLocation: false) + + viewModel.context.viewState.errorSubject.send(.invalidLocationAuthorization) + + XCTAssertNotNil(viewModel.context.alertInfo) + XCTAssertEqual(viewModel.context.viewState.bindings.alertInfo?.id, .authorizationError) + } + + private func buildViewModel(withLocation: Bool) -> LocationSharingViewModel { + LocationSharingViewModel(tileServerMapURL: URL(string: "http://empty.com")!, + avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: ""), + location: (withLocation ? CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096) : nil)) } } From eead770633f81cf3ba1d6c63e7c1b226ece78176 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 4 Jan 2022 14:12:24 +0200 Subject: [PATCH 102/109] vector-im/element-ios/issues/5298 - Various tweaks following code review. --- Config/BuildSettings.swift | 7 -- Riot/Assets/en.lproj/Vector.strings | 10 +- Riot/Generated/Strings.swift | 30 ++--- Riot/Managers/AppInfo/AppInfo.swift | 16 ++- .../Views/FindYourContactsFooterView.swift | 2 +- .../People/InviteFriendsPresenter.swift | 2 +- .../Location/RoomTimelineLocationView.swift | 29 +++-- Riot/Modules/Room/RoomCoordinator.swift | 112 ++++++++++-------- .../Location/LocationBubbleCell.swift | 1 + .../SpaceMemberListViewController.swift | 6 +- .../SpaceExploreRoomViewController.swift | 2 +- .../StartChat/InviteFriendsHeaderView.swift | 2 +- Riot/Modules/TabBar/TabBarCoordinator.swift | 9 +- RiotSwiftUI/Common.xcconfig | 2 + .../AnalyticsPromptModels.swift | 2 +- .../AnalyticsPromptCoordinator.swift | 8 +- .../LocationSharingCoordinator.swift | 96 +++------------ .../Coordinator/ShareToMapsAppActivity.swift | 78 ++++++++++++ .../LocationSharingViewModel.swift | 8 +- .../Coordinator/PollEditFormCoordinator.swift | 18 +-- .../Coordinator/PollTimelineCoordinator.swift | 2 +- .../UserSuggestionCoordinator.swift | 2 +- .../TemplateUserProfileCoordinator.swift | 2 +- .../TemplateRoomsCoordinator.swift | 2 +- RiotSwiftUI/RiotSwiftUI-Bridging-Header.h | 5 + RiotSwiftUI/target.yml | 2 + 26 files changed, 246 insertions(+), 209 deletions(-) create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/ShareToMapsAppActivity.swift create mode 100644 RiotSwiftUI/RiotSwiftUI-Bridging-Header.h diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 00e6138e5e..ccdafb43ff 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -23,13 +23,6 @@ import Keys final class BuildSettings: NSObject { // MARK: - Bundle Settings - static var bundleDisplayName: String { - guard let bundleDisplayName = Bundle.app.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String else { - fatalError("CFBundleDisplayName should be defined") - } - return bundleDisplayName - } - static var applicationGroupIdentifier: String { guard let applicationGroupIdentifier = Bundle.app.object(forInfoDictionaryKey: "applicationGroupIdentifier") as? String else { fatalError("applicationGroupIdentifier should be defined") diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 2283209f68..86bb2c62b0 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -950,7 +950,7 @@ Tap the + to start adding people."; // Analytics "analytics_prompt_title" = "Help improve %@"; -"analytics_prompt_message_new_user" = "Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; +"analytics_prompt_message_new_user" = "Help us identify issues and improve %@ by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; "analytics_prompt_message_upgrade" = "You previously consented to share anonymous usage data with us. Now, to help understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; /* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ "analytics_prompt_terms_new_user" = "You can read all our terms %@."; @@ -1752,7 +1752,7 @@ Tap the + to start adding people."; "spaces_coming_soon_title" = "Coming soon"; "spaces_add_rooms_coming_soon_title" = "Adding rooms coming soon"; "spaces_invites_coming_soon_title" = "Invites coming soon"; -"spaces_coming_soon_detail" = "This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with Element on your computer."; +"spaces_coming_soon_detail" = "This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with %@ on your computer."; "space_participants_action_remove" = "Remove from this space"; "space_participants_action_ban" = "Ban from this space"; "space_home_show_all_rooms" = "Show all rooms"; @@ -1856,11 +1856,11 @@ Tap the + to start adding people."; "location_sharing_share_action" = "Share"; -"location_sharing_loading_map_error_title" = "Element could not load the map. Please try again later."; +"location_sharing_loading_map_error_title" = "%@ could not load the map. Please try again later."; -"location_sharing_locating_user_error_title" = "Element could not access your location. Please try again later."; +"location_sharing_locating_user_error_title" = "%@ could not access your location. Please try again later."; -"location_sharing_invalid_authorization_error_title" = "Element does not have permission to access your location. You can enable access in Settings > Location"; +"location_sharing_invalid_authorization_error_title" = "%@ does not have permission to access your location. You can enable access in Settings > Location"; "location_sharing_invalid_authorization_not_now" = "Not now"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 615ce91ed3..5764578fd4 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -35,9 +35,9 @@ public class VectorL10n: NSObject { public static func activeCallDetails(_ p1: String) -> String { return VectorL10n.tr("Vector", "active_call_details", p1) } - /// Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices. - public static var analyticsPromptMessageNewUser: String { - return VectorL10n.tr("Vector", "analytics_prompt_message_new_user") + /// Help us identify issues and improve %@ by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices. + public static func analyticsPromptMessageNewUser(_ p1: String) -> String { + return VectorL10n.tr("Vector", "analytics_prompt_message_new_user", p1) } /// You previously consented to share anonymous usage data with us. Now, to help understand how people use multiple devices, we’ll generate a random identifier, shared by your devices. public static var analyticsPromptMessageUpgrade: String { @@ -2195,9 +2195,9 @@ public class VectorL10n: NSObject { public static var locationSharingCloseAction: String { return VectorL10n.tr("Vector", "location_sharing_close_action") } - /// Element does not have permission to access your location. You can enable access in Settings > Location - public static var locationSharingInvalidAuthorizationErrorTitle: String { - return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_error_title") + /// %@ does not have permission to access your location. You can enable access in Settings > Location + public static func locationSharingInvalidAuthorizationErrorTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_error_title", p1) } /// Not now public static var locationSharingInvalidAuthorizationNotNow: String { @@ -2207,13 +2207,13 @@ public class VectorL10n: NSObject { public static var locationSharingInvalidAuthorizationSettings: String { return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_settings") } - /// Element could not load the map. Please try again later. - public static var locationSharingLoadingMapErrorTitle: String { - return VectorL10n.tr("Vector", "location_sharing_loading_map_error_title") + /// %@ could not load the map. Please try again later. + public static func locationSharingLoadingMapErrorTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_loading_map_error_title", p1) } - /// Element could not access your location. Please try again later. - public static var locationSharingLocatingUserErrorTitle: String { - return VectorL10n.tr("Vector", "location_sharing_locating_user_error_title") + /// %@ could not access your location. Please try again later. + public static func locationSharingLocatingUserErrorTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_locating_user_error_title", p1) } /// Open in Apple Maps public static var locationSharingOpenAppleMaps: String { @@ -5155,9 +5155,9 @@ public class VectorL10n: NSObject { public static var spacesAddRoomsComingSoonTitle: String { return VectorL10n.tr("Vector", "spaces_add_rooms_coming_soon_title") } - /// This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with Element on your computer. - public static var spacesComingSoonDetail: String { - return VectorL10n.tr("Vector", "spaces_coming_soon_detail") + /// This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with %@ on your computer. + public static func spacesComingSoonDetail(_ p1: String) -> String { + return VectorL10n.tr("Vector", "spaces_coming_soon_detail", p1) } /// Coming soon public static var spacesComingSoonTitle: String { diff --git a/Riot/Managers/AppInfo/AppInfo.swift b/Riot/Managers/AppInfo/AppInfo.swift index 457a7af870..fde62d75bd 100644 --- a/Riot/Managers/AppInfo/AppInfo.swift +++ b/Riot/Managers/AppInfo/AppInfo.swift @@ -19,17 +19,14 @@ import Foundation /// Used to handle the application information @objcMembers final class AppInfo: NSObject { - + // MARK: - Constants /// Current application information static var current: AppInfo { - let appDisplayName = BuildSettings.bundleDisplayName - let buildInfo: BuildInfo = BuildInfo() - - return AppInfo(displayName: appDisplayName, + return AppInfo(displayName: self.bundleDisplayName, appVersion: AppVersion.current, - buildInfo: buildInfo) + buildInfo: BuildInfo()) } // MARK: - Properties @@ -52,4 +49,11 @@ final class AppInfo: NSObject { self.appVersion = appVersion self.buildInfo = buildInfo } + + private static var bundleDisplayName: String { + guard let bundleDisplayName = Bundle.app.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String else { + fatalError("CFBundleDisplayName should be defined") + } + return bundleDisplayName + } } diff --git a/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift b/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift index ba0e9cf8a4..c97176f31e 100644 --- a/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift +++ b/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift @@ -55,7 +55,7 @@ class FindYourContactsFooterView: UIView, NibLoadable, Themable { button.layer.cornerRadius = 8 titleLabel.text = VectorL10n.findYourContactsTitle - messageLabel.text = VectorL10n.findYourContactsMessage(BuildSettings.bundleDisplayName) + messageLabel.text = VectorL10n.findYourContactsMessage(AppInfo.current.displayName) button.setTitle(VectorL10n.findYourContactsButtonTitle, for: .normal) footerLabel.text = VectorL10n.findYourContactsFooter } diff --git a/Riot/Modules/People/InviteFriendsPresenter.swift b/Riot/Modules/People/InviteFriendsPresenter.swift index ad89957466..8f6f9f8b6e 100644 --- a/Riot/Modules/People/InviteFriendsPresenter.swift +++ b/Riot/Modules/People/InviteFriendsPresenter.swift @@ -61,7 +61,7 @@ final class InviteFriendsPresenter: NSObject { private func buildShareText(with userId: String) -> String { let userMatrixToLink: String = MXTools.permalinkToUser(withUserId: userId) - return VectorL10n.inviteFriendsShareText(BuildSettings.bundleDisplayName, userMatrixToLink) + return VectorL10n.inviteFriendsShareText(AppInfo.current.displayName, userMatrixToLink) } private func present(_ viewController: UIViewController, animated: Bool) { diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift index cf44b6efda..82cebe7cd4 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -19,8 +19,10 @@ import Reusable import Mapbox import Keys -class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { +class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegate { + // MARK: - Constants + private struct Constants { static let mapHeight: CGFloat = 300.0 static let mapTilerKey = RiotKeys().mapTilerAPIKey @@ -29,8 +31,8 @@ class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { static let cellCornerRadius: CGFloat = 8.0 } - // MARK: Properties - // MARK: - Private + // MARK: - Properties + // MARK: Private @IBOutlet private var descriptionContainerView: UIView! @IBOutlet private var descriptionLabel: UILabel! @@ -38,7 +40,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { private var mapView: MGLMapView! private var annotationView: LocationUserMarkerView? - // MARK: - Public + // MARK: Public var locationDescription: String? { get { @@ -67,9 +69,6 @@ class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { clipsToBounds = true layer.borderWidth = Constants.cellBorderRadius layer.cornerRadius = Constants.cellCornerRadius - - NotificationCenter.default.addObserver(self, selector: #selector(updateTheme), name: .themeServiceDidChangeTheme, object: nil) - updateTheme() } // MARK: - Public @@ -99,17 +98,17 @@ class RoomTimelineLocationView: UIView, NibLoadable, MGLMapViewDelegate { mapView.addAnnotation(pointAnnotation) } - // MARK: - MGLMapViewDelegate + // MARK: - Themable - func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { - return annotationView + func update(theme: Theme) { + descriptionLabel.textColor = theme.colors.primaryContent + descriptionLabel.font = theme.fonts.footnote + layer.borderColor = theme.colors.quinaryContent.cgColor } - // MARK: - Private + // MARK: - MGLMapViewDelegate - @objc private func updateTheme() { - descriptionLabel.textColor = ThemeService.shared().theme.colors.primaryContent - descriptionLabel.font = ThemeService.shared().theme.fonts.footnote - layer.borderColor = ThemeService.shared().theme.colors.quinaryContent.cgColor + func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { + return annotationView } } diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index a79af06f15..fec16dd932 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -30,9 +30,6 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { private let activityIndicatorPresenter: ActivityIndicatorPresenterType private var selectedEventId: String? - private var pollEditFormCoordinator: PollEditFormCoordinator? - private var locationSharingCoordinator: LocationSharingCoordinator? - private var roomDataSourceManager: MXKRoomDataSourceManager { return MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.parameters.session) } @@ -199,6 +196,56 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { completion?() } + + private func startLocationCoordinatorWithEvent(_ event: MXEvent? = nil, bubbleData: MXKRoomBubbleCellDataStoring? = nil) { + guard #available(iOS 14.0, *) else { + return + } + + guard let navigationRouter = self.navigationRouter, + let mediaManager = mxSession?.mediaManager, + let user = mxSession?.myUser else { + MXLog.error("[RoomCoordinator] Invalid location sharing coordinator parameters. Returning.") + return + } + + var avatarData: AvatarInputProtocol + if event != nil, let bubbleData = bubbleData { + avatarData = AvatarInput(mxContentUri: bubbleData.senderAvatarUrl, + matrixItemId: bubbleData.senderId, + displayName: bubbleData.senderDisplayName) + } else { + avatarData = AvatarInput(mxContentUri: user.avatarUrl, + matrixItemId: user.userId, + displayName: user.displayname) + } + + var location: CLLocationCoordinate2D? + if let locationContent = event?.location { + location = CLLocationCoordinate2D(latitude: locationContent.latitude, longitude: locationContent.longitude) + } + + let parameters = LocationSharingCoordinatorParameters(roomDataSource: roomViewController.roomDataSource, + mediaManager: mediaManager, + avatarData: avatarData, + location: location) + + let coordinator = LocationSharingCoordinator(parameters: parameters) + + coordinator.completion = { [weak self, weak coordinator] in + guard let self = self, let coordinator = coordinator else { + return + } + + self.navigationRouter?.dismissModule(animated: true, completion: nil) + self.remove(childCoordinator: coordinator) + } + + add(childCoordinator: coordinator) + + navigationRouter.present(coordinator, animated: true) + coordinator.start() + } } // MARK: - RoomIdentifiable @@ -262,10 +309,22 @@ extension RoomCoordinator: RoomViewControllerDelegate { return } - let parameters = PollEditFormCoordinatorParameters(navigationRouter: self.navigationRouter, room: roomViewController.roomDataSource.room) - pollEditFormCoordinator = PollEditFormCoordinator(parameters: parameters) + let parameters = PollEditFormCoordinatorParameters(room: roomViewController.roomDataSource.room) + let coordinator = PollEditFormCoordinator(parameters: parameters) - pollEditFormCoordinator?.start() + coordinator.completion = { [weak self, weak coordinator] in + guard let self = self, let coordinator = coordinator else { + return + } + + self.navigationRouter?.dismissModule(animated: true, completion: nil) + self.remove(childCoordinator: coordinator) + } + + add(childCoordinator: coordinator) + + navigationRouter?.present(coordinator, animated: true) + coordinator.start() } func roomViewControllerDidRequestLocationSharingFormPresentation(_ roomViewController: RoomViewController) { @@ -291,45 +350,4 @@ extension RoomCoordinator: RoomViewControllerDelegate { PollTimelineProvider.shared.pollTimelineCoordinatorForEventIdentifier(eventIdentifier)?.endPoll() } - - // MARK: - Private - - private func startLocationCoordinatorWithEvent(_ event: MXEvent? = nil, bubbleData: MXKRoomBubbleCellDataStoring? = nil) { - guard #available(iOS 14.0, *) else { - return - } - - guard let navigationRouter = self.navigationRouter, - let mediaManager = mxSession?.mediaManager, - let user = mxSession?.myUser else { - MXLog.error("[RoomCoordinator] Invalid location sharing coordinator parameters. Returning.") - return - } - - var avatarData: AvatarInputProtocol - if event != nil, let bubbleData = bubbleData { - avatarData = AvatarInput(mxContentUri: bubbleData.senderAvatarUrl, - matrixItemId: bubbleData.senderId, - displayName: bubbleData.senderDisplayName) - } else { - avatarData = AvatarInput(mxContentUri: user.avatarUrl, - matrixItemId: user.userId, - displayName: user.displayname) - } - - var location: CLLocationCoordinate2D? - if let locationContent = event?.location { - location = CLLocationCoordinate2D(latitude: locationContent.latitude, longitude: locationContent.longitude) - } - - let parameters = LocationSharingCoordinatorParameters(navigationRouter: navigationRouter, - roomDataSource: roomViewController.roomDataSource, - mediaManager: mediaManager, - avatarData: avatarData, - location: location) - - locationSharingCoordinator = LocationSharingCoordinator(parameters: parameters) - - locationSharingCoordinator?.start() - } } diff --git a/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift index c7628ae137..2795d26470 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift @@ -32,6 +32,7 @@ class LocationBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable return } + locationView.update(theme: ThemeService.shared().theme) locationView.locationDescription = locationContent.locationDescription let location = CLLocationCoordinate2D(latitude: locationContent.latitude, longitude: locationContent.longitude) diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift index d050767586..9356ee9046 100644 --- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift +++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift @@ -154,7 +154,7 @@ final class SpaceMemberListViewController: RoomParticipantsViewController { // MARK: - Actions @objc private func onAddParticipantButtonPressed() { - self.errorPresenter.presentError(from: self, title: VectorL10n.spacesInvitesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail, animated: true, handler: nil) + self.errorPresenter.presentError(from: self, title: VectorL10n.spacesInvitesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil) } private func cancelButtonAction() { @@ -184,11 +184,11 @@ final class SpaceMemberListViewController: RoomParticipantsViewController { override func roomMemberDetailsViewController(_ roomMemberDetailsViewController: MXKRoomMemberDetailsViewController!, startChatWithMemberId matrixId: String!, completion: (() -> Void)!) { completion() - self.errorPresenter.presentError(from: self, title: VectorL10n.spacesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail, animated: true, handler: nil) + self.errorPresenter.presentError(from: self, title: VectorL10n.spacesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil) } override func roomMemberDetailsViewController(_ roomMemberDetailsViewController: MXKRoomMemberDetailsViewController!, placeVoipCallWithMemberId matrixId: String!, andVideo isVideoCall: Bool) { - self.errorPresenter.presentError(from: self, title: VectorL10n.spacesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail, animated: true, handler: nil) + self.errorPresenter.presentError(from: self, title: VectorL10n.spacesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil) } } diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift index fc04d7473a..5c13e08f8c 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift @@ -225,7 +225,7 @@ final class SpaceExploreRoomViewController: UIViewController { } @objc private func addRoomAction(semder: UIView) { - self.errorPresenter.presentError(from: self, title: VectorL10n.spacesAddRoomsComingSoonTitle, message: VectorL10n.spacesComingSoonDetail, animated: true, handler: nil) + self.errorPresenter.presentError(from: self, title: VectorL10n.spacesAddRoomsComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil) } // MARK: - UISearchBarDelegate diff --git a/Riot/Modules/StartChat/InviteFriendsHeaderView.swift b/Riot/Modules/StartChat/InviteFriendsHeaderView.swift index 30325e7049..2c61559c91 100644 --- a/Riot/Modules/StartChat/InviteFriendsHeaderView.swift +++ b/Riot/Modules/StartChat/InviteFriendsHeaderView.swift @@ -50,7 +50,7 @@ final class InviteFriendsHeaderView: UIView, NibLoadable, Themable { override func awakeFromNib() { super.awakeFromNib() - button.setTitle(VectorL10n.inviteFriendsAction(BuildSettings.bundleDisplayName), for: .normal) + button.setTitle(VectorL10n.inviteFriendsAction(AppInfo.current.displayName), for: .normal) button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside) button.layer.cornerRadius = 8 button.layer.borderWidth = 2 diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index ac72eb2eb9..37342d973b 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -489,15 +489,20 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { @available(iOS 14.0, *) private func presentAnalyticsPrompt(with session: MXSession) { - let parameters = AnalyticsPromptCoordinatorParameters(session: session, navigationRouter: navigationRouter) + let parameters = AnalyticsPromptCoordinatorParameters(session: session) let coordinator = AnalyticsPromptCoordinator(parameters: parameters) + coordinator.completion = { [weak self, weak coordinator] in guard let self = self, let coordinator = coordinator else { return } + + self.navigationRouter.dismissModule(animated: true, completion: nil) self.remove(childCoordinator: coordinator) } - coordinator.start() add(childCoordinator: coordinator) + + navigationRouter.present(coordinator, animated: true) + coordinator.start() } // MARK: UserSessions management diff --git a/RiotSwiftUI/Common.xcconfig b/RiotSwiftUI/Common.xcconfig index 9c6902651b..c4505aaf0b 100644 --- a/RiotSwiftUI/Common.xcconfig +++ b/RiotSwiftUI/Common.xcconfig @@ -27,4 +27,6 @@ INFOPLIST_FILE = RiotSwiftUI/Info.plist SKIP_INSTALL = YES +SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/$(PRODUCT_NAME)/RiotSwiftUI-Bridging-Header.h + SWIFT_OBJC_INTERFACE_HEADER_NAME = GeneratedInterface-Swift.h diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift index add81e84b6..b9678fdbd9 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift @@ -66,7 +66,7 @@ extension AnalyticsPromptType { var message: String { switch self { case .newUser: - return VectorL10n.analyticsPromptMessageNewUser + return VectorL10n.analyticsPromptMessageNewUser(AppInfo.current.displayName) case .upgrade: return VectorL10n.analyticsPromptMessageUpgrade } diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift index 208b289d76..c9e5f44b48 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift @@ -21,11 +21,9 @@ import SwiftUI struct AnalyticsPromptCoordinatorParameters { /// The session to use if analytics are enabled. let session: MXSession - /// The navigation router used to display the prompt. - let navigationRouter: NavigationRouterType } -final class AnalyticsPromptCoordinator: Coordinator { +final class AnalyticsPromptCoordinator: Coordinator, Presentable { // MARK: - Properties @@ -78,8 +76,6 @@ final class AnalyticsPromptCoordinator: Coordinator { MXLog.debug("[AnalyticsPromptCoordinator] did start.") - parameters.navigationRouter.present(toPresentable(), animated: true) - analyticsPromptViewModel.completion = { [weak self] result in MXLog.debug("[AnalyticsPromptCoordinator] AnalyticsPromptViewModel did complete with result: \(result).") @@ -88,11 +84,9 @@ final class AnalyticsPromptCoordinator: Coordinator { switch result { case .enable: Analytics.shared.optIn(with: self.parameters.session) - self.parameters.navigationRouter.dismissModule(animated: true, completion: nil) self.completion?() case .disable: Analytics.shared.optOut() - self.parameters.navigationRouter.dismissModule(animated: true, completion: nil) self.completion?() } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift index 029f7b61c2..931c6a3d75 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift @@ -20,14 +20,13 @@ import SwiftUI import Keys struct LocationSharingCoordinatorParameters { - let navigationRouter: NavigationRouterType let roomDataSource: MXKRoomDataSource let mediaManager: MXMediaManager let avatarData: AvatarInputProtocol let location: CLLocationCoordinate2D? } -final class LocationSharingCoordinator: Coordinator { +final class LocationSharingCoordinator: Coordinator, Presentable { // MARK: - Properties @@ -44,9 +43,10 @@ final class LocationSharingCoordinator: Coordinator { // MARK: Public - // Must be used only internally var childCoordinators: [Coordinator] = [] + var completion: (() -> Void)? + // MARK: - Setup @available(iOS 14.0, *) @@ -70,14 +70,12 @@ final class LocationSharingCoordinator: Coordinator { return } - parameters.navigationRouter.present(locationSharingHostingController, animated: true) - locationSharingViewModel.completion = { [weak self] result in guard let self = self else { return } switch result { case .cancel: - self.parameters.navigationRouter.dismissModule(animated: true, completion: nil) + self.completion?() case .share(let latitude, let longitude): if let location = self.parameters.location { self.showActivityControllerForLocation(location) @@ -91,8 +89,8 @@ final class LocationSharingCoordinator: Coordinator { description: nil) { [weak self] _ in guard let self = self else { return } - self.parameters.navigationRouter.dismissModule(animated: true, completion: nil) self.locationSharingViewModel.dispatch(action: .stopLoading(nil)) + self.completion?() } failure: { [weak self] error in guard let self = self else { return } @@ -104,85 +102,19 @@ final class LocationSharingCoordinator: Coordinator { } } - func showActivityControllerForLocation(_ location: CLLocationCoordinate2D) { - let vc = UIActivityViewController(activityItems: activityItems(location: location), - applicationActivities: [ShareToMapsAppActivity(type: .apple, location: location), - ShareToMapsAppActivity(type: .google, location: location)]) - locationSharingHostingController.present(vc, animated: true) - } - - func activityItems(location: CLLocationCoordinate2D) -> [Any] { - var items = [Any]() - - // Make the share sheet show a pretty location thumbnail - if let url = NSURL(string: "https://maps.apple.com?ll=\(location.latitude),\(location.longitude)") { - items.append(url) - } - - return items - } -} - -extension UIActivity.ActivityType { - static let shareToMapsApp = UIActivity.ActivityType("Element.ShareToMapsApp") -} - -class ShareToMapsAppActivity: UIActivity { - - enum MapsAppType { - case apple - case google - } - - let type: MapsAppType - let location: CLLocationCoordinate2D - - private override init() { - fatalError() - } - - init(type: MapsAppType, location: CLLocationCoordinate2D) { - self.type = type - self.location = location - } - - override var activityTitle: String? { - switch type { - case .apple: - return VectorL10n.locationSharingOpenAppleMaps - case .google: - return VectorL10n.locationSharingOpenGoogleMaps - } - } - - var activityCategory: UIActivity.Category { - return .action - } + // MARK: - Presentable - override var activityType: UIActivity.ActivityType { - return .shareToMapsApp + func toPresentable() -> UIViewController { + return locationSharingHostingController } - override func canPerform(withActivityItems activityItems: [Any]) -> Bool { - return true - } + // MARK: - Private - override func prepare(withActivityItems activityItems: [Any]) { - var url: URL? - switch type { - case .apple: - url = URL(string: "https://maps.apple.com?ll=\(location.latitude),\(location.longitude)&q=Pin") - case .google: - url = URL(string: "https://www.google.com/maps/search/?api=1&query=\(location.latitude),\(location.longitude)") - } - - guard let url = url else { - activityDidFinish(false) - return - } + private func showActivityControllerForLocation(_ location: CLLocationCoordinate2D) { + let vc = UIActivityViewController(activityItems: [ShareToMapsAppActivity.urlForMapsAppType(.apple, location: location)], + applicationActivities: [ShareToMapsAppActivity(type: .apple, location: location), + ShareToMapsAppActivity(type: .google, location: location)]) - UIApplication.shared.open(url, options: [:]) { [weak self] result in - self?.activityDidFinish(result) - } + locationSharingHostingController.present(vc, animated: true) } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/ShareToMapsAppActivity.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/ShareToMapsAppActivity.swift new file mode 100644 index 0000000000..6f7bd37804 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/ShareToMapsAppActivity.swift @@ -0,0 +1,78 @@ +// +// Copyright 2021 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 UIActivity.ActivityType { + static let shareToMapsApp = UIActivity.ActivityType("Element.ShareToMapsApp") +} + +class ShareToMapsAppActivity: UIActivity { + enum MapsAppType { + case apple + case google + } + + let type: MapsAppType + let location: CLLocationCoordinate2D + + private override init() { + fatalError() + } + + init(type: MapsAppType, location: CLLocationCoordinate2D) { + self.type = type + self.location = location + } + + static func urlForMapsAppType(_ type: MapsAppType, location: CLLocationCoordinate2D) -> URL { + switch type { + case .apple: + return URL(string: "https://maps.apple.com?ll=\(location.latitude),\(location.longitude)&q=Pin")! + case .google: + return URL(string: "https://www.google.com/maps/search/?api=1&query=\(location.latitude),\(location.longitude)")! + } + } + + override var activityTitle: String? { + switch type { + case .apple: + return VectorL10n.locationSharingOpenAppleMaps + case .google: + return VectorL10n.locationSharingOpenGoogleMaps + } + } + + var activityCategory: UIActivity.Category { + return .action + } + + override var activityType: UIActivity.ActivityType { + return .shareToMapsApp + } + + override func canPerform(withActivityItems activityItems: [Any]) -> Bool { + return true + } + + override func prepare(withActivityItems activityItems: [Any]) { + let url = Self.urlForMapsAppType(type, location: location) + + UIApplication.shared.open(url, options: [:]) { [weak self] result in + self?.activityDidFinish(result) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift index dd275f7a2f..765c0b5589 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -73,17 +73,17 @@ class LocationSharingViewModel: LocationSharingViewModelType { switch error { case .failedLoadingMap: state.bindings.alertInfo = ErrorAlertInfo(id: .mapLoadingError, - title: VectorL10n.locationSharingLoadingMapErrorTitle, + title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName) , primaryButton: (VectorL10n.ok, { completion?(.cancel) }), secondaryButton: nil) case .failedLocatingUser: state.bindings.alertInfo = ErrorAlertInfo(id: .userLocatingError, - title: VectorL10n.locationSharingLocatingUserErrorTitle, + title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), primaryButton: (VectorL10n.ok, { completion?(.cancel) }), secondaryButton: nil) case .invalidLocationAuthorization: state.bindings.alertInfo = ErrorAlertInfo(id: .authorizationError, - title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle, + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, { completion?(.cancel) }), secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, { if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { @@ -101,7 +101,7 @@ class LocationSharingViewModel: LocationSharingViewModelType { if error != nil { state.bindings.alertInfo = ErrorAlertInfo(id: .locationSharingError, - title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle, + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), primaryButton: (VectorL10n.ok, nil), secondaryButton: nil) } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift index da63d70986..ee0f60a784 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -21,11 +21,10 @@ import UIKit import SwiftUI struct PollEditFormCoordinatorParameters { - let navigationRouter: NavigationRouterType? let room: MXRoom } -final class PollEditFormCoordinator: Coordinator { +final class PollEditFormCoordinator: Coordinator, Presentable { // MARK: - Properties @@ -42,9 +41,10 @@ final class PollEditFormCoordinator: Coordinator { // MARK: Public - // Must be used only internally var childCoordinators: [Coordinator] = [] + var completion: (() -> Void)? + // MARK: - Setup @available(iOS 14.0, *) @@ -65,13 +65,11 @@ final class PollEditFormCoordinator: Coordinator { return } - parameters.navigationRouter?.present(pollEditFormHostingController, animated: true) - pollEditFormViewModel.completion = { [weak self] result in guard let self = self else { return } switch result { case .cancel: - self.parameters.navigationRouter?.dismissModule(animated: true, completion: nil) + self.completion?() case .create(let question, let answerOptions): var options = [MXEventContentPollStartAnswerOption]() for answerOption in answerOptions { @@ -88,8 +86,8 @@ final class PollEditFormCoordinator: Coordinator { self.parameters.room.sendPollStart(withContent: pollStartContent, localEcho: nil) { [weak self] result in guard let self = self else { return } - self.parameters.navigationRouter?.dismissModule(animated: true, completion: nil) self.pollEditFormViewModel.dispatch(action: .stopLoading(nil)) + self.completion?() } failure: { [weak self] error in guard let self = self else { return } @@ -99,4 +97,10 @@ final class PollEditFormCoordinator: Coordinator { } } } + + // MARK: - Private + + func toPresentable() -> UIViewController { + return pollEditFormHostingController + } } diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift b/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift index 8d201264f9..78c53acecf 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift @@ -27,7 +27,7 @@ struct PollTimelineCoordinatorParameters { } @available(iOS 14.0, *) -final class PollTimelineCoordinator: Coordinator, PollAggregatorDelegate { +final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDelegate { // MARK: - Properties diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index e7308710d5..d960a97963 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -26,7 +26,7 @@ protocol UserSuggestionCoordinatorDelegate: AnyObject { } @available(iOS 14.0, *) -final class UserSuggestionCoordinator: Coordinator { +final class UserSuggestionCoordinator: Coordinator, Presentable { // MARK: - Properties diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift index 3eb01bbddf..da0b965bef 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift @@ -20,7 +20,7 @@ struct TemplateUserProfileCoordinatorParameters { let session: MXSession } -final class TemplateUserProfileCoordinator: Coordinator { +final class TemplateUserProfileCoordinator: Coordinator, Presentable { // MARK: - Properties diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift index c7a79d69f0..d7af258817 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift @@ -19,7 +19,7 @@ import UIKit @objcMembers -final class TemplateRoomsCoordinator: Coordinator { +final class TemplateRoomsCoordinator: Coordinator, Presentable { // MARK: - Properties diff --git a/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h b/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h new file mode 100644 index 0000000000..b75953b6ea --- /dev/null +++ b/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h @@ -0,0 +1,5 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "BuildInfo.h" diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml index ab1c2dd597..66d318a6a6 100644 --- a/RiotSwiftUI/target.yml +++ b/RiotSwiftUI/target.yml @@ -39,6 +39,8 @@ targets: - "**/MatrixSDK/**" - "**/Coordinator/**" - "**/Test/**" + - path: ../Riot/Managers/AppInfo/ + - path: ../Riot/Categories/Bundle.swift - path: ../Riot/Generated/Strings.swift - path: ../Riot/Generated/Images.swift - path: ../Riot/Managers/Theme/ThemeIdentifier.swift From 446b321a4d1bf2f4138369869f76ca62d8150a05 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 4 Jan 2022 15:52:28 +0200 Subject: [PATCH 103/109] vector-im/element-ios/issues/5298 - Various tweaks following code review. --- RiotSwiftUI/Common.xcconfig | 2 +- RiotSwiftUI/Info.plist | 2 ++ RiotSwiftUI/targetUITests.yml | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Common.xcconfig b/RiotSwiftUI/Common.xcconfig index c4505aaf0b..37b97f356a 100644 --- a/RiotSwiftUI/Common.xcconfig +++ b/RiotSwiftUI/Common.xcconfig @@ -27,6 +27,6 @@ INFOPLIST_FILE = RiotSwiftUI/Info.plist SKIP_INSTALL = YES -SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/$(PRODUCT_NAME)/RiotSwiftUI-Bridging-Header.h +SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h SWIFT_OBJC_INTERFACE_HEADER_NAME = GeneratedInterface-Swift.h diff --git a/RiotSwiftUI/Info.plist b/RiotSwiftUI/Info.plist index c72d62e92f..859c68836d 100644 --- a/RiotSwiftUI/Info.plist +++ b/RiotSwiftUI/Info.plist @@ -20,6 +20,8 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) + CFBundleDisplayName + RiotSwiftUI NSLocationWhenInUseUsageDescription When you share your location to people, Element needs access to show them a map. diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml index 0e2a100dfa..68de22ac5f 100644 --- a/RiotSwiftUI/targetUITests.yml +++ b/RiotSwiftUI/targetUITests.yml @@ -37,6 +37,8 @@ targets: base: TEST_TARGET_NAME: RiotSwiftUI PRODUCT_BUNDLE_IDENTIFIER: org.matrix.RiotSwiftUITests$(rfc1034identifier) + SWIFT_OBJC_BRIDGING_HEADER: $(SRCROOT)/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h + SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h sources: # Source included/excluded here here are similar to RiotSwiftUI as we # need access to ScreenStates @@ -45,6 +47,8 @@ targets: - "**/MatrixSDK/**" - "**/Coordinator/**" - "**/Test/Unit/**" + - path: ../Riot/Managers/AppInfo/ + - path: ../Riot/Categories/Bundle.swift - path: ../Riot/Generated/Strings.swift - path: ../Riot/Generated/Images.swift - path: ../Riot/Managers/Theme/ThemeIdentifier.swift From 52598a2eabb25d82f14c79229e55fe453e586b43 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 6 Jan 2022 12:46:26 +0200 Subject: [PATCH 104/109] Fixed ffmpeg-kit breaking changes after minor release, fixed version to 4.5.1 --- Podfile | 2 +- .../VoiceMessages/VoiceMessageAudioConverter.swift | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Podfile b/Podfile index c4dc2f06e1..e465a8f52d 100644 --- a/Podfile +++ b/Podfile @@ -95,7 +95,7 @@ abstract_target 'RiotPods' do pod 'SwiftJWT', '~> 3.6.200' pod 'SideMenu', '~> 6.5' pod 'DSWaveformImage', '~> 6.1.1' - pod 'ffmpeg-kit-ios-audio', '~> 4.5' + pod 'ffmpeg-kit-ios-audio', '4.5.1' pod 'FLEX', '~> 4.5.0', :configurations => ['Debug'] diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift index 0c521b1b8e..9b3f85cec5 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift @@ -35,7 +35,7 @@ struct VoiceMessageAudioConverter { static func mediaDurationAt(_ sourceURL: URL, completion: @escaping (Result) -> Void) { FFprobeKit.getMediaInformationAsync(sourceURL.path) { session in - guard let session = session as? MediaInformationSession else { + guard let session = session else { completion(.failure(.generic("Invalid session"))) return } @@ -46,14 +46,14 @@ struct VoiceMessageAudioConverter { } DispatchQueue.main.async { - if returnCode.isSuccess() { + if returnCode.isValueSuccess() { let mediaInfo = session.getMediaInformation() if let duration = try? TimeInterval(value: mediaInfo?.getDuration() ?? "0") { completion(.success(duration)) } else { completion(.failure(.generic("Failed to get media duration"))) } - } else if returnCode.isCancel() { + } else if returnCode.isValueCancel() { completion(.failure(.cancelled)) } else { completion(.failure(.generic(String(returnCode.getValue())))) @@ -82,9 +82,9 @@ struct VoiceMessageAudioConverter { } DispatchQueue.main.async { - if returnCode.isSuccess() { + if returnCode.isValueSuccess() { completion(.success(())) - } else if returnCode.isCancel() { + } else if returnCode.isValueCancel() { completion(.failure(.cancelled)) } else { completion(.failure(.generic(String(returnCode.getValue())))) From e2a7df1daa46ce7b9d32d0227d8d28516c08ea7d Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 5 Jan 2022 16:52:53 +0200 Subject: [PATCH 105/109] Increased the BubbleCellContentView's inner content view bottom padding so it doesn't overlap the send receipt indicator. --- .../BubbleCells/BaseBubbleCell/BubbleCellContentView.xib | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib index 56c26c56c4..adbd1b79a9 100644 --- a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib +++ b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib @@ -140,14 +140,14 @@ - + - + From 4b90c106034899c12d76b3c73f5d842b0d7f0be2 Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Fri, 7 Jan 2022 21:34:03 +0000 Subject: [PATCH 106/109] Translated using Weblate (Slovak) Currently translated at 96.8% (427 of 441 strings) Translation: Element iOS/Element iOS (MatrixKit) Translate-URL: https://translate.element.io/projects/riot-ios/element-ios-matrixkit/sk/ --- .../sk.lproj/MatrixKit.strings | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings index b429d25fa0..1efe288395 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings @@ -527,3 +527,19 @@ "login_error_registration_is_not_supported" = "Registrácia nie je v súčasnosti podporovaná"; "login_error_do_not_support_login_flows" = "V súčasnosti nepodporujeme žiadny alebo všetky prihlasovacie toky definované týmto domovským serverom"; "login_error_no_login_flow" = "Nepodarilo sa nám získať autentifikačné informácie z tohto domovského servera"; +"call_more_actions_audio_use_device" = "Reproduktor zariadenia"; +"call_more_actions_unhold" = "Pokračovať"; +"call_invite_expired" = "Platnosť pozvánky na hovor vypršala"; +"notification_settings_per_word_info" = "Slová sa porovnávajú bez ohľadu na veľkosť písmen a môžu obsahovať zástupný znak *. Takže:\nfoo zodpovedá reťazcu foo ohraničenému oddeľovačmi slov (napr. interpunkčnými znamienkami a medzerami alebo začiatkom/koncom riadku).\nfoo* zodpovedá každému takému slovu, ktoré začína foo.\n*foo* zodpovedá každému takému slovu, ktoré obsahuje 3 písmená foo."; +"notice_redaction_by_you" = "Upravili ste udalosť (id: %@)"; +"notice_answered_video_call_by_you" = "Prijali ste hovor"; +"notice_placed_video_call_by_you" = "Uskutočnili ste videohovor"; +"notice_placed_voice_call_by_you" = "Uskutočnili ste hlasový hovor"; +"notice_answered_video_call" = "%@ prijal hovor"; +"notice_placed_video_call" = "%@ uskutočnil videohovor"; +"notice_placed_voice_call" = "%@ uskutočnil hlasový hovor"; +"attachment_size_prompt_title" = "Potvrdiť veľkosť na odoslanie"; +"notice_redaction" = "%@ upravil udalosť (id: %@)"; +"notice_feedback" = "Udalosť spätnej väzby (id: %@): %@"; +"resume_call" = "Pokračovať"; +"answer_call" = "Prijať hovor"; From e1e0e3717c01756ff4e5032cdbf492fa7dd9b1f9 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 11 Jan 2022 13:09:21 +0000 Subject: [PATCH 107/109] changelog.d: Upgrade MatrixSDK version ([v0.20.16](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.20.16)). --- Podfile | 2 +- changelog.d/x-nolink-0.change | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/x-nolink-0.change diff --git a/Podfile b/Podfile index e465a8f52d..6a5620b944 100644 --- a/Podfile +++ b/Podfile @@ -13,7 +13,7 @@ use_frameworks! # - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixSDKVersion = '0.20.15' +$matrixSDKVersion = '= 0.20.16' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change new file mode 100644 index 0000000000..5b9735d528 --- /dev/null +++ b/changelog.d/x-nolink-0.change @@ -0,0 +1 @@ +Upgrade MatrixSDK version ([v0.20.16](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.20.16)). \ No newline at end of file From 4ba56f9d7b6a1a8342ca14ccd9cdd5259fad9449 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 11 Jan 2022 13:09:22 +0000 Subject: [PATCH 108/109] version++ --- CHANGES.md | 12 ++++++++++++ changelog.d/5035.change | 1 - changelog.d/5311.bugfix | 1 - changelog.d/x-nolink-0.change | 1 - 4 files changed, 12 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/5035.change delete mode 100644 changelog.d/5311.bugfix delete mode 100644 changelog.d/x-nolink-0.change diff --git a/CHANGES.md b/CHANGES.md index e530b5806a..8c21124184 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,15 @@ +## Changes in 1.6.12 (2022-01-11) + +🙌 Improvements + +- Upgrade MatrixSDK version ([v0.20.16](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.20.16)). +- Analytics: Replace Matomo with PostHog. ([#5035](https://github.com/vector-im/element-ios/issues/5035)) + +🐛 Bugfixes + +- RoomVC: Fix left room reason label memory management. ([#5311](https://github.com/vector-im/element-ios/issues/5311)) + + ## Changes in 1.6.11 (2021-12-14) ✨ Features diff --git a/changelog.d/5035.change b/changelog.d/5035.change deleted file mode 100644 index 6be81ff048..0000000000 --- a/changelog.d/5035.change +++ /dev/null @@ -1 +0,0 @@ -Analytics: Replace Matomo with PostHog. \ No newline at end of file diff --git a/changelog.d/5311.bugfix b/changelog.d/5311.bugfix deleted file mode 100644 index cfd5998e51..0000000000 --- a/changelog.d/5311.bugfix +++ /dev/null @@ -1 +0,0 @@ -RoomVC: Fix left room reason label memory management. \ No newline at end of file diff --git a/changelog.d/x-nolink-0.change b/changelog.d/x-nolink-0.change deleted file mode 100644 index 5b9735d528..0000000000 --- a/changelog.d/x-nolink-0.change +++ /dev/null @@ -1 +0,0 @@ -Upgrade MatrixSDK version ([v0.20.16](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.20.16)). \ No newline at end of file From e8686dc50f41531dafad77b1551937d1d54e4500 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 11 Jan 2022 14:27:20 +0000 Subject: [PATCH 109/109] finish version++ --- Podfile.lock | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 8ff43e554e..749fe065fd 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -38,7 +38,7 @@ PODS: - DTFoundation/Core - DTFoundation/UIKit (1.7.18): - DTFoundation/Core - - ffmpeg-kit-ios-audio (4.5) + - ffmpeg-kit-ios-audio (4.5.1) - FLEX (4.5.0) - FlowCommoniOS (1.12.2) - GBDeviceInfo (6.6.0): @@ -49,6 +49,7 @@ PODS: - Introspect (0.1.3) - JitsiMeetSDK (3.10.2) - KeychainAccess (4.2.2) + - Keys (1.0.1) - KituraContracts (1.2.1): - LoggerAPI (~> 1.7) - KTCenterFlowLayout (1.3.1) @@ -57,16 +58,16 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixSDK (0.20.15): - - MatrixSDK/Core (= 0.20.15) - - MatrixSDK/Core (0.20.15): + - MatrixSDK (0.20.16): + - MatrixSDK/Core (= 0.20.16) + - MatrixSDK/Core (0.20.16): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - OLMKit (~> 3.2.5) - Realm (= 10.16.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.20.15): + - MatrixSDK/JingleCallStack (0.20.16): - JitsiMeetSDK (= 3.10.2) - MatrixSDK/Core - OLMKit (3.2.5): @@ -107,17 +108,18 @@ DEPENDENCIES: - Down (~> 0.11.0) - DSWaveformImage (~> 6.1.1) - DTCoreText (~> 1.6.25) - - ffmpeg-kit-ios-audio (~> 4.5) + - ffmpeg-kit-ios-audio (= 4.5.1) - FLEX (~> 4.5.0) - FlowCommoniOS (~> 1.12.0) - GBDeviceInfo (~> 6.6.0) - HPGrowingTextView (~> 1.1) - Introspect (~> 0.1) - KeychainAccess (~> 4.2.2) + - Keys (from `Pods/CocoaPodsKeys`) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.20.15) - - MatrixSDK/JingleCallStack (= 0.20.15) + - MatrixSDK (= 0.20.16) + - MatrixSDK/JingleCallStack (= 0.20.16) - OLMKit - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) @@ -177,6 +179,8 @@ EXTERNAL SOURCES: AnalyticsEvents: :branch: release/swift :git: https://github.com/matrix-org/matrix-analytics-events.git + Keys: + :path: Pods/CocoaPodsKeys CHECKOUT OPTIONS: AnalyticsEvents: @@ -194,7 +198,7 @@ SPEC CHECKSUMS: DSWaveformImage: 3c718a0cf99291887ee70d1d0c18d80101d3d9ce DTCoreText: ec749e013f2e1f76de5e7c7634642e600a7467ce DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536 - ffmpeg-kit-ios-audio: 8c44d93054e1a9743a7014ec3dd26cd1ad8f2a59 + ffmpeg-kit-ios-audio: 662ce2064e56733ca7d8216705efbc38d9e1c3fe FLEX: e51461dd6f0bfb00643c262acdfea5d5d12c596b FlowCommoniOS: ca92071ab526dc89905495a37844fd7e78d1a7f2 GBDeviceInfo: ed0db16230d2fa280e1cbb39a5a7f60f6946aaec @@ -203,13 +207,14 @@ SPEC CHECKSUMS: Introspect: 2be020f30f084ada52bb4387fff83fa52c5c400e JitsiMeetSDK: 2f118fa770f23e518f3560fc224fae3ac7062223 KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51 + Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 KituraContracts: e845e60dc8627ad0a76fa55ef20a45451d8f830b KTCenterFlowLayout: 6e02b50ab2bd865025ae82fe266ed13b6d9eaf97 libbase58: 7c040313537b8c44b6e2d15586af8e21f7354efd libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: 2f4d3aacb1c53e2785f0be71d24b8e62e5c5c056 + MatrixSDK: af6a70532bb43af59f43a1f4dae512a26afeab0b OLMKit: 9fb4799c4a044dd2c06bda31ec31a12191ad30b5 PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d @@ -225,6 +230,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: e60814fe2084a7dca3f82c3a1c4a1b763ae822c0 +PODFILE CHECKSUM: 2493587902f8f28bb2638303dd583c47e9f24d8b COCOAPODS: 1.11.2