From 0f20fc54de8cca965216bb59969ada85145d8c32 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 16 Dec 2021 16:29:07 +0200 Subject: [PATCH 01/40] 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 bceefcd26..78535b31a 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 4bf0df19d..d4ac2a9e7 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 ce9f09f35..5e29d179b 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 02/40] 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 695d4cd61..f4e7a1f10 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 525870b51..4be53baea 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 53efbaf92..ea061a17e 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 a8674758c..c014fde7c 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 92a7bdad0..d9925aa6d 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 768862c3b..e2d6c1b3f 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 b615810b2..ee1a8d284 100644 --- a/Riot/Assets/third_party_licenses.html +++ b/Riot/Assets/third_party_licenses.html @@ -1897,6 +1897,34 @@ Library. 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 4ab7572a3..df6ce4e94 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 5ad851929..000000000 --- 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 6bf7269b8..000000000 --- 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 000000000..d1e2388db --- /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 000000000..d85cfc8b2 --- /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 000000000..a9a0ba590 --- /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 5b31329d4..3413dbdaf 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 72dc4f61e..40e3c2377 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 180cfe27a..9de053ba9 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -433,16 +433,14 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni _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 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Analytics: Force to send the pending actions [[DecryptionFailureTracker sharedInstance] dispatch]; - [[Analytics sharedInstance] dispatch]; + [Analytics.shared forceUpload]; } - (void)applicationWillEnterForeground:(UIApplication *)application @@ -648,7 +646,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni 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 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // 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 188e33268..a698c9bad 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/AuthenticationViewController.m @@ -311,7 +311,7 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"Authentication"]; + [Analytics.shared trackScreen:@"Authentication"]; [_keyboardAvoider startAvoiding]; } @@ -330,7 +330,7 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; 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 b8f0d0a80..922c30463 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -260,7 +260,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro [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 91a881352..cd722d323 100644 --- a/Riot/Modules/Communities/GroupsViewController.m +++ b/Riot/Modules/Communities/GroupsViewController.m @@ -204,7 +204,7 @@ [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 018c15eb9..12d240f64 100644 --- a/Riot/Modules/Communities/Home/GroupHomeViewController.m +++ b/Riot/Modules/Communities/Home/GroupHomeViewController.m @@ -206,7 +206,7 @@ [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 7be31d255..f8a0508c4 100644 --- a/Riot/Modules/Communities/Members/GroupParticipantsViewController.m +++ b/Riot/Modules/Communities/Members/GroupParticipantsViewController.m @@ -220,7 +220,7 @@ [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 cabf6cdde..a51a762f1 100644 --- a/Riot/Modules/Communities/Rooms/GroupRoomsViewController.m +++ b/Riot/Modules/Communities/Rooms/GroupRoomsViewController.m @@ -184,7 +184,7 @@ [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 28e7e64de..96660bcc6 100644 --- a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m +++ b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m @@ -138,7 +138,7 @@ [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 1a8bec148..ab59604f6 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -161,7 +161,7 @@ [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 3d332dbaa..a42ee9225 100644 --- a/Riot/Modules/Contacts/Details/ContactDetailsViewController.m +++ b/Riot/Modules/Contacts/Details/ContactDetailsViewController.m @@ -234,7 +234,7 @@ [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 63035f726..c67f9e3f9 100644 --- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m @@ -110,7 +110,7 @@ [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 00fd9d20d..5a439b6bb 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m @@ -117,7 +117,7 @@ [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 47300a956..f62b5beec 100644 --- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m +++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m @@ -108,7 +108,7 @@ [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 bd0a07b3f..d2f044854 100644 --- a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m +++ b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m @@ -145,7 +145,7 @@ [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 b87015e59..f847f5c6d 100644 --- a/Riot/Modules/MediaPicker/Library/MediaAlbumContentViewController.m +++ b/Riot/Modules/MediaPicker/Library/MediaAlbumContentViewController.m @@ -165,7 +165,7 @@ [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 92261789c..9aa20846b 100644 --- a/Riot/Modules/MediaPicker/MediaPickerViewController.m +++ b/Riot/Modules/MediaPicker/MediaPickerViewController.m @@ -214,7 +214,7 @@ [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 596c6a3b5..556539a6a 100644 --- a/Riot/Modules/Room/Attachements/AttachmentsViewController.m +++ b/Riot/Modules/Room/Attachements/AttachmentsViewController.m @@ -78,7 +78,7 @@ [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 d1888c6d7..0d68d9b42 100644 --- a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m +++ b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m @@ -240,7 +240,7 @@ [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 1776b8775..737a3d1be 100644 --- a/Riot/Modules/Room/Members/RoomParticipantsViewController.m +++ b/Riot/Modules/Room/Members/RoomParticipantsViewController.m @@ -247,7 +247,7 @@ [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 46c83de52..5fb98c36e 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -568,7 +568,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"ChatRoom"]; + [Analytics.shared trackScreen:@"ChatRoom"]; // Refresh the room title view [self refreshRoomTitle]; @@ -610,8 +610,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [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 0e14b53ee..17bfde94a 100644 --- a/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m +++ b/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m @@ -111,7 +111,7 @@ [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 4e93f01d6..13ec858ad 100644 --- a/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m +++ b/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m @@ -112,7 +112,7 @@ [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 2a0983263..f73ada35f 100644 --- a/Riot/Modules/Room/Search/RoomSearchViewController.m +++ b/Riot/Modules/Room/Search/RoomSearchViewController.m @@ -108,7 +108,7 @@ } // 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 7ca017b57..de36a4035 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m @@ -313,7 +313,7 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti [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 d04214673..c3a7c752c 100644 --- a/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m +++ b/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m @@ -146,7 +146,7 @@ [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 4b05e230c..d4d7b90dc 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 3e4b4297e..b617fe465 100644 --- a/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m +++ b/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m @@ -97,7 +97,7 @@ static CGFloat const kTextFontSize = 15.0; [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 aac54c076..c93b88ae1 100644 --- a/Riot/Modules/Settings/Language/LanguagePickerViewController.m +++ b/Riot/Modules/Settings/Language/LanguagePickerViewController.m @@ -111,7 +111,7 @@ [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 0ab2fe0d8..8842d7e87 100644 --- a/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m +++ b/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m @@ -100,7 +100,7 @@ [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 b9b9ac474..02eac7c51 100644 --- a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m +++ b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m @@ -162,7 +162,7 @@ enum { [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 5c6d93aab..9b61048a6 100644 --- a/Riot/Modules/Settings/Security/SecurityViewController.m +++ b/Riot/Modules/Settings/Security/SecurityViewController.m @@ -251,7 +251,7 @@ TableViewSectionsDelegate> [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 1993ab9f7..6978f15e8 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -778,7 +778,7 @@ TableViewSectionsDelegate> [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"Settings"]; + [Analytics.shared trackScreen:@"Settings"]; // Refresh display [self refreshSettings]; @@ -2251,11 +2251,11 @@ TableViewSectionsDelegate> { 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 @@ TableViewSectionsDelegate> [[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"); + MXLogDebug(@"[SettingsViewController] disable automatic crash report and analytics sending"); + [Analytics.shared optOutWith:self.mainSession]; - RiotSettings.shared.enableCrashReport = YES; - - [[Analytics sharedInstance] start]; + // Remove potential crash file. + [MXLogger deleteCrashLog]; } } diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index 4a4751d83..db0b2d77e 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -196,11 +196,15 @@ 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 @@ - (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 5f83b5bc5..5371b3100 100644 --- a/Riot/Modules/UserDevices/UsersDevicesViewController.m +++ b/Riot/Modules/UserDevices/UsersDevicesViewController.m @@ -121,7 +121,7 @@ [super viewWillAppear:animated]; // Screen tracking - [[Analytics sharedInstance] trackScreen:@"UnknowDevices"]; + [Analytics.shared trackScreen:@"UnknownDevices"]; [self.tableView reloadData]; } From e8d02c545802fbcb6c7a3d171d7e6c4278772d45 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 19 Nov 2021 10:00:24 +0000 Subject: [PATCH 03/40] 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 d9925aa6d..97bb4efee 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 3f62ea753..0722e15bc 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 d1e2388db..91625a9ba 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -22,58 +22,70 @@ import PostHog static let shared = Analytics() + private var postHog: PHGPostHog? + private(set) var isRunning = false - private var postHog: PHGPostHog? + 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 shouldShowPseudonymousAnalyticsPrompt(for session: MXSession) -> Bool { - return AnalyticsSettings(session: session).showPseudonymousAnalyticsPrompt - } - 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.") + } + } + + startIfEnabled() + + if !RiotSettings.shared.isIdentifiedForAnalytics { + identify(with: 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.") - } + func optOut() { + RiotSettings.shared.enableAnalytics = false + reset() } - private func start(with pseudonymousID: String) { - guard !isRunning else { return } + 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 d85cfc8b2..2b26c17b4 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 a9a0ba590..04f17290d 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 3413dbdaf..13a9a7b67 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 9de053ba9..a85fe1e2b 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -433,7 +433,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni _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 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni 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 6978f15e8..304266411 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -2252,7 +2252,7 @@ TableViewSectionsDelegate> 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 @@ TableViewSectionsDelegate> 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 db0b2d77e..61329d157 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -197,14 +197,9 @@ 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 @@ 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 @@ 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 000000000..b31cbd6c5 --- /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 0e1c0bfaac856aec75dadf41643a261850e7f316 Mon Sep 17 00:00:00 2001 From: Doug Date: Fri, 19 Nov 2021 16:32:39 +0000 Subject: [PATCH 04/40] 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 91625a9ba..3acf02df7 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 2573c2ed3bfed1dd108d87af23e12dbaa4a0945a Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 22 Nov 2021 14:28:09 +0000 Subject: [PATCH 05/40] 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 97bb4efee..fa5354524 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 0722e15bc..223232453 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 3acf02df7..d1c36a391 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 f71851451677e5c3a07f9470ed3f338f457a6282 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 24 Nov 2021 10:43:22 +0000 Subject: [PATCH 06/40] Add specific methods to track analytics and test generated event types. --- Riot/Managers/Analytics/Analytics.swift | 65 +++++++++++----- 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, 234 insertions(+), 49 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 d1c36a391..ba6a55ec3 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)") } -} - -extension Analytics: MXAnalyticsDelegate { - @objc func trackDuration(_ seconds: TimeInterval, category: String, name: String) { -// postHog?.capture("\(category):\(name)", properties: ["duration": seconds]) + + 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 56521eb58..d34ba0017 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 @@ NSString *const kDecryptionFailureTrackerAnalyticsCategory = @"e2e.failure"; 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 @@ NSString *const kDecryptionFailureTrackerAnalyticsCategory = @"e2e.failure"; 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 000000000..c8bc925b4 --- /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 000000000..718c71eba --- /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 000000000..11100e091 --- /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 a85fe1e2b..4357bec9b 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -2395,7 +2395,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni 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 5fb98c36e..bcc330c61 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 07/40] 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 4be53baea..c9ab63d85 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 ea061a17e..53efbaf92 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 c014fde7c..ca9078e41 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 fa5354524..f908e3884 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 223232453..21da4f99d 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 ba6a55ec3..6b0bbb7b4 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 2b26c17b4..67927cace 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 04f17290d..c02b85c30 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 33ef957053b439b1f778f8e1b84cda3279dfc3d7 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 24 Nov 2021 18:04:54 +0000 Subject: [PATCH 08/40] 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 f908e3884..9e9c5451c 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 21da4f99d..f356c62a3 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 6b0bbb7b4..358ce63e0 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.. { [super viewWillAppear:animated]; - // Screen tracking - [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 304266411..b5d4c3b05 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -776,9 +776,6 @@ TableViewSectionsDelegate> - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [Analytics.shared trackScreen:@"Settings"]; // Refresh display [self refreshSettings]; diff --git a/Riot/Modules/StartChat/StartChatViewController.m b/Riot/Modules/StartChat/StartChatViewController.m index 18ef614dd..f9807d408 100644 --- a/Riot/Modules/StartChat/StartChatViewController.m +++ b/Riot/Modules/StartChat/StartChatViewController.m @@ -73,8 +73,6 @@ { [super finalizeInit]; - self.screenName = @"StartChat"; - _isAddParticipantSearchBarEditing = NO; // Prepare room participants diff --git a/Riot/Modules/UserDevices/UsersDevicesViewController.m b/Riot/Modules/UserDevices/UsersDevicesViewController.m index 5371b3100..308f0afcd 100644 --- a/Riot/Modules/UserDevices/UsersDevicesViewController.m +++ b/Riot/Modules/UserDevices/UsersDevicesViewController.m @@ -120,9 +120,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [Analytics.shared trackScreen:@"UnknownDevices"]; - [self.tableView reloadData]; } From 6bbcc74eeabe1e23d83b5c55c9383fa3a38de53b Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 25 Nov 2021 18:28:32 +0000 Subject: [PATCH 09/40] 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 358ce63e0..493b270ac 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 d73827749..a7a38937f 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -256,9 +256,6 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro { [super viewWillAppear:animated]; - // Screen tracking - [Analytics.shared trackScreen:AnalyticsScreenHome]; - // Reset back user interactions self.userInteractionEnabled = YES; From 418865a388324cd74bf08f0b7cb5dccec8d67f80 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 30 Nov 2021 12:54:29 +0000 Subject: [PATCH 10/40] 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 | 77 +++++++++---------- Riot/Modules/Application/LegacyAppDelegate.m | 6 +- 5 files changed, 72 insertions(+), 80 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 4d215eb98..22cadcaac 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 493b270ac..d83501c56 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 315f5a9cc4a9145110e428ef15fc99edea3b36cf Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 2 Dec 2021 14:25:45 +0000 Subject: [PATCH 11/40] 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 d83501c56..b2edfb634 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 98ed0c1ef..fef1780f9 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 000000000..5c704f8d8 --- /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 0c4c169f7..1d879c41c 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 908ecd752..c917bd373 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -2394,8 +2394,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni launchAnimationContainerView = launchLoadingView; - [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:MXTaskProfileNameStartupLaunchScreen - category:MXTaskProfileCategoryStartup]; + [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:MXTaskProfileNameStartupLaunchScreen]; } } @@ -2404,7 +2403,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni 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 8afc8b26e..9e9e357a7 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 a7a38937f..0bcceb09a 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -323,11 +323,14 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro // 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 c72e95dab..306a4e55c 100644 --- a/Riot/Modules/Communities/GroupsViewController.m +++ b/Riot/Modules/Communities/GroupsViewController.m @@ -42,6 +42,8 @@ __weak id kThemeServiceDidChangeThemeNotificationObserver; } +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation GroupsViewController @@ -74,6 +76,8 @@ // Set itself as delegate by default. self.delegate = self; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenMyGroups]; } - (void)viewDidLoad @@ -203,9 +207,6 @@ { [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 @@ // 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 b12cd5d70..6e5d5d3b5 100644 --- a/Riot/Modules/Communities/Home/GroupHomeViewController.m +++ b/Riot/Modules/Communities/Home/GroupHomeViewController.m @@ -48,6 +48,8 @@ @property (nonatomic, readonly) DTHTMLAttributedStringBuilderWillFlushCallback longDescriptionSanitizationCallback; +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation GroupHomeViewController @@ -95,6 +97,8 @@ MXStrongifyAndReturnIfNil(self); [element sanitizeWith:allowedHTMLTags bodyFont:self->_groupLongDescription.font imageHandler:[self groupLongDescriptionImageHandler]]; }; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenGroup]; } - (void)viewDidLoad @@ -205,9 +209,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [Analytics.shared trackScreen:AnalyticsScreenGroup]; - // Release the potential pushed view controller [self releasePushedViewController]; @@ -259,6 +260,18 @@ [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 d35c71dbe..97c02515b 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 b564d226c..bf83b6372 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -177,6 +177,12 @@ [self updateFooterViewVisibility]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; @@ -201,6 +207,12 @@ } } +- (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 d89e60635..63fe65197 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 f7148a6e6..c3c10933b 100644 --- a/Riot/Modules/Favorites/FavouritesViewController.m +++ b/Riot/Modules/Favorites/FavouritesViewController.m @@ -40,6 +40,8 @@ [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 bf9826858..71db0b6bc 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 ee756902c..67ad65ab8 100644 --- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m @@ -121,6 +121,18 @@ [[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 8a3553771..9fdb9880a 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 acf36b053..9fc1d5ac8 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m @@ -128,6 +128,18 @@ [[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 ec511bb3c..66d724005 100644 --- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m +++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m @@ -35,6 +35,8 @@ id kThemeServiceDidChangeThemeNotificationObserver; } +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation DirectoryViewController @@ -46,6 +48,8 @@ // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenRoomDirectory]; } - (void)viewDidLoad @@ -106,9 +110,6 @@ - (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 @@ // the selected room (if any) is highlighted. [self refreshCurrentSelectedCell:YES]; } + + [self.screenTimer start]; } - (void)viewWillDisappear:(BOOL)animated @@ -148,6 +151,12 @@ [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 7d240e48c..8c1956330 100644 --- a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m +++ b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m @@ -79,11 +79,13 @@ [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 @@ 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 89dd5d3c0..2c2f702d7 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -68,6 +68,8 @@ selectedSection = -1; selectedRoomId = nil; selectedCollectionViewContentOffset = -1; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenHome]; } - (void)viewDidLoad @@ -100,9 +102,6 @@ { [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 325b7cf71..ad8995746 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 9d93622c2..ace78de54 100644 --- a/Riot/Modules/People/PeopleViewController.m +++ b/Riot/Modules/People/PeopleViewController.m @@ -50,6 +50,8 @@ [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 e8f3b7f29..92b799344 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 1e8e007c1..077311835 100644 --- a/Riot/Modules/Room/Files/RoomFilesViewController.m +++ b/Riot/Modules/Room/Files/RoomFilesViewController.m @@ -110,6 +110,14 @@ [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 68112586e..687587dfe 100644 --- a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m +++ b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m @@ -104,6 +104,8 @@ @property(nonatomic, strong) UserVerificationCoordinatorBridgePresenter *userVerificationCoordinatorBridgePresenter; +@property(nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation RoomMemberDetailsViewController @@ -139,6 +141,8 @@ // Keep visible the status bar by default. isStatusBarHidden = NO; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenUser]; } - (void)viewDidLoad @@ -239,9 +243,6 @@ { [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 @@ 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 757bb2fca..e3bb56543 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 6802f7a3b..5df03b91e 100644 --- a/Riot/Modules/Room/Members/RoomParticipantsViewController.m +++ b/Riot/Modules/Room/Members/RoomParticipantsViewController.m @@ -265,6 +265,8 @@ [contactsPickerViewController destroy]; contactsPickerViewController = nil; } + + [self.screenTimer start]; } - (void)viewWillDisappear:(BOOL)animated @@ -281,6 +283,12 @@ [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 ccfe76a7e..4eaf2328d 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 3590b5526..78b5af425 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 3792006c4..ac57d3a78 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 63145b1c4..183db0046 100644 --- a/Riot/Modules/Rooms/RoomsViewController.m +++ b/Riot/Modules/Rooms/RoomsViewController.m @@ -36,6 +36,13 @@ 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 75a8365a7..6ab5e2517 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 1e2cf5daf..ce22a3440 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 ea6297529..0efbe7bdc 100644 --- a/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m +++ b/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m @@ -47,6 +47,8 @@ static CGFloat const kTextFontSize = 15.0; @property (weak, nonatomic) id themeDidChangeNotificationObserver; +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end #pragma mark - Implementation @@ -62,6 +64,12 @@ static CGFloat const kTextFontSize = 15.0; return viewController; } +- (void)finalizeInit +{ + [super finalizeInit]; + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenDeactivateAccount]; +} + - (void)destroy { id notificationObserver = self.themeDidChangeNotificationObserver; @@ -97,6 +105,12 @@ static CGFloat const kTextFontSize = 15.0; [self userInterfaceThemeDidChange]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; @@ -104,6 +118,12 @@ static CGFloat const kTextFontSize = 15.0; [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 333459083..99b7f7c93 100644 --- a/Riot/Modules/Settings/Security/SecurityViewController.m +++ b/Riot/Modules/Settings/Security/SecurityViewController.m @@ -119,6 +119,8 @@ TableViewSectionsDelegate> @property (nonatomic, strong) SetPinCoordinatorBridgePresenter *setPinCoordinatorBridgePresenter; @property (nonatomic, strong) CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter; +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation SecurityViewController @@ -142,6 +144,8 @@ TableViewSectionsDelegate> // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSettingsSecurity]; } - (void)viewDidLoad @@ -265,6 +269,12 @@ TableViewSectionsDelegate> [self loadCrossSigning]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; @@ -276,6 +286,12 @@ TableViewSectionsDelegate> } } +- (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 b5d4c3b05..9aeaa77b7 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -283,6 +283,8 @@ TableViewSectionsDelegate> @property (nonatomic) BOOL isPreparingIdentityService; @property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter; +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation SettingsViewController @@ -315,6 +317,8 @@ TableViewSectionsDelegate> isSavingInProgress = NO; isResetPwdInProgress = NO; is3PIDBindingInProgress = NO; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSettings]; } - (void)updateSections @@ -805,6 +809,8 @@ TableViewSectionsDelegate> [self releasePushedViewController]; [self.settingsDiscoveryTableViewSection reload]; + + [self.screenTimer start]; } - (void)viewWillDisappear:(BOOL)animated @@ -848,6 +854,12 @@ TableViewSectionsDelegate> } } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + #pragma mark - Internal methods - (void)pushViewController:(UIViewController*)viewController diff --git a/Riot/Modules/SideMenu/SideMenuViewController.swift b/Riot/Modules/SideMenu/SideMenuViewController.swift index f7fecb60c..cec36f497 100644 --- a/Riot/Modules/SideMenu/SideMenuViewController.swift +++ b/Riot/Modules/SideMenu/SideMenuViewController.swift @@ -48,6 +48,7 @@ final class SideMenuViewController: UIViewController { private var keyboardAvoider: KeyboardAvoider? private var errorPresenter: MXKErrorPresentation! private var activityPresenter: ActivityIndicatorPresenter! + private var screenTimer = AnalyticsScreenTimer(screen: .sidebar) private var sideMenuActionViews: [SideMenuActionView] = [] private weak var sideMenuVersionView: SideMenuVersionView? @@ -86,8 +87,14 @@ final class SideMenuViewController: UIViewController { navigationController?.setNavigationBarHidden(true, animated: animated) } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + screenTimer.start() + } + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) + screenTimer.stop() } override var preferredStatusBarStyle: UIStatusBarStyle { diff --git a/Riot/Modules/StartChat/StartChatViewController.m b/Riot/Modules/StartChat/StartChatViewController.m index f9807d408..4f2d71dc1 100644 --- a/Riot/Modules/StartChat/StartChatViewController.m +++ b/Riot/Modules/StartChat/StartChatViewController.m @@ -80,6 +80,8 @@ // Assign itself as delegate self.contactsTableViewControllerDelegate = self; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenStartChat]; } - (void)viewDidLoad From d4f5dbbd11327c6e6e846600f74aa099201c2824 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 2 Dec 2021 16:56:04 +0000 Subject: [PATCH 12/40] 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 b2edfb634..0222e2d7f 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 000000000..7ee0ae429 --- /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 000000000..ae9ee09af --- /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 ee74bc16fb2e0a1b1d54d8a323802b9551a5c241 Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 6 Dec 2021 12:52:33 +0000 Subject: [PATCH 13/40] Add AnalyticsPrompt to SwiftUI target and replace old UIAlertController. --- .../AnalyticsTick.pdf | 123 ++++ .../AnalyticsCheckmark.imageset/Contents.json | 15 + .../AnalyticsLogo.imageset/AnalyticsLogo.pdf | 641 ++++++++++++++++++ .../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, 1554 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 000000000..9696208e3 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsCheckmark.imageset/AnalyticsTick.pdf @@ -0,0 +1,123 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.000000 -1.000000 cm +0.049479 0.742188 0.545395 scn +10.000000 23.000000 m +3.947715 23.000000 -1.000000 18.052284 -1.000000 12.000000 c +1.000000 12.000000 l +1.000000 16.947716 5.052285 21.000000 10.000000 21.000000 c +10.000000 23.000000 l +h +-1.000000 12.000000 m +-1.000000 5.947716 3.947715 1.000000 10.000000 1.000000 c +10.000000 3.000000 l +5.052285 3.000000 1.000000 7.052285 1.000000 12.000000 c +-1.000000 12.000000 l +h +10.000000 1.000000 m +16.052284 1.000000 21.000000 5.947716 21.000000 12.000000 c +19.000000 12.000000 l +19.000000 7.052285 14.947715 3.000000 10.000000 3.000000 c +10.000000 1.000000 l +h +21.000000 12.000000 m +21.000000 18.052284 16.052284 23.000000 10.000000 23.000000 c +10.000000 21.000000 l +14.947715 21.000000 19.000000 16.947716 19.000000 12.000000 c +21.000000 12.000000 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 5.545532 4.655060 cm +0.049479 0.742188 0.545395 scn +0.717378 6.153159 m +0.332610 6.549356 -0.300487 6.558620 -0.696684 6.173852 c +-1.092881 5.789084 -1.102146 5.155987 -0.717378 4.759790 c +0.717378 6.153159 l +h +3.257576 2.102139 m +2.540198 1.405455 l +2.728505 1.211555 2.987285 1.102139 3.257576 1.102139 c +3.527867 1.102139 3.786646 1.211555 3.974954 1.405455 c +3.257576 2.102139 l +h +11.626469 9.284243 m +12.011237 9.680439 12.001972 10.313537 11.605776 10.698304 c +11.209579 11.083073 10.576482 11.073808 10.191713 10.677610 c +11.626469 9.284243 l +h +-0.717378 4.759790 m +2.540198 1.405455 l +3.974954 2.798823 l +0.717378 6.153159 l +-0.717378 4.759790 l +h +3.974954 1.405455 m +11.626469 9.284243 l +10.191713 10.677610 l +2.540198 2.798823 l +3.974954 1.405455 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 1679 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 22.000000 22.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001769 00000 n +0000001792 00000 n +0000001965 00000 n +0000002039 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2098 +%%EOF \ No newline at end of file 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 000000000..146a290ce --- /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 000000000..096c22d4b --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsLogo.imageset/AnalyticsLogo.pdf @@ -0,0 +1,641 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << /ExtGState << /E2 << /ca 0.400000 >> + /E1 << /ca 0.400000 >> + >> >> + /BBox [ 0.000000 0.000000 119.000000 93.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 14.875000 -0.000015 cm +0.049479 0.742188 0.545395 scn +0.000000 44.521278 m +0.000000 69.109695 20.036579 89.042557 44.625000 89.042557 c +44.625000 89.042557 l +69.213417 89.042557 89.250000 69.109695 89.250000 44.521278 c +89.250000 44.521278 l +89.250000 19.932861 69.213417 0.000000 44.625000 0.000000 c +44.625000 0.000000 l +20.036579 0.000000 0.000000 19.932861 0.000000 44.521278 c +0.000000 44.521278 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 52.227402 46.594299 cm +1.000000 1.000000 1.000000 scn +0.000000 20.730219 m +0.000000 22.447567 1.395428 23.839752 3.116776 23.839752 c +14.592426 23.839752 23.895279 14.558517 23.895279 3.109533 c +23.895279 1.392185 22.499851 0.000000 20.778503 0.000000 c +19.057156 0.000000 17.661728 1.392185 17.661728 3.109533 c +17.661728 11.123821 11.149731 17.620686 3.116776 17.620686 c +1.395428 17.620686 0.000000 19.012871 0.000000 20.730219 c +h +f* +n +Q +q +-1.000000 0.000000 -0.000000 -1.000000 66.772202 42.448242 cm +1.000000 1.000000 1.000000 scn +0.000000 20.730206 m +0.000000 22.447552 1.395429 23.839737 3.116779 23.839737 c +14.592443 23.839737 23.895306 14.558502 23.895306 3.109520 c +23.895306 1.392172 22.499878 -0.000011 20.778526 -0.000011 c +19.057177 -0.000011 17.661749 1.392172 17.661749 3.109520 c +17.661749 11.123808 11.149744 17.620672 3.116779 17.620672 c +1.395429 17.620672 0.000000 19.012857 0.000000 20.730206 c +h +f* +n +Q +q +-0.000000 1.000000 -1.000000 -0.000000 57.366512 37.265598 cm +1.000000 1.000000 1.000000 scn +0.000000 20.722975 m +0.000000 22.444323 1.392186 23.839752 3.109534 23.839752 c +14.558520 23.839752 23.839758 14.536892 23.839758 3.061234 c +23.839758 1.339886 22.447573 -0.055544 20.730225 -0.055544 c +19.012875 -0.055544 17.620689 1.339886 17.620689 3.061234 c +17.620689 11.094195 11.123824 17.606197 3.109534 17.606197 c +1.392186 17.606197 0.000000 19.001625 0.000000 20.722975 c +h +f* +n +Q +q +-0.000000 -1.000000 1.000000 -0.000000 61.633194 51.777008 cm +1.000000 1.000000 1.000000 scn +0.000000 20.722975 m +0.000000 22.444324 1.392186 23.839752 3.109534 23.839752 c +14.558520 23.839752 23.839758 14.536893 23.839758 3.061237 c +23.839758 1.339888 22.447573 -0.055540 20.730225 -0.055540 c +19.012875 -0.055540 17.620689 1.339888 17.620689 3.061237 c +17.620689 11.094196 11.123824 17.606197 3.109534 17.606197 c +1.392186 17.606197 0.000000 19.001627 0.000000 20.722975 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 28.123089 16.695465 cm +1.000000 1.000000 1.000000 scn +10.448288 0.000008 m +11.057861 0.000008 11.560491 0.448122 11.646045 1.077618 c +12.576446 7.617959 13.464069 8.514189 19.795069 9.229038 c +20.436726 9.303724 20.917965 9.826525 20.917965 10.434681 c +20.917965 11.053506 20.447418 11.554968 19.805763 11.640323 c +13.506846 12.461866 12.694082 13.262072 11.646045 19.802414 c +11.539103 20.431910 11.057861 20.869354 10.448288 20.869354 c +9.849410 20.869354 9.346780 20.431910 9.250531 19.791744 c +8.330826 13.251402 7.443202 12.355172 1.112203 11.640323 c +0.470547 11.565637 0.000000 11.053506 0.000000 10.434681 c +0.000000 9.826525 0.459853 9.314394 1.112203 9.229038 c +7.411120 8.354148 8.191800 7.607290 9.250531 1.066948 c +9.368169 0.437452 9.860105 0.000008 10.448288 0.000008 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 28.123089 15.195465 cm +0.049479 0.742188 0.545395 scn +11.646045 2.577618 m +10.903506 2.683247 l +10.902877 2.678619 l +11.646045 2.577618 l +h +19.795069 10.729038 m +19.879219 9.983770 l +19.881781 9.984068 l +19.795069 10.729038 l +h +19.805763 13.140323 m +19.904661 13.883777 l +19.902761 13.884024 l +19.805763 13.140323 l +h +11.646045 21.302414 m +12.386630 21.421087 l +12.385450 21.428030 l +11.646045 21.302414 l +h +9.250531 21.291744 m +8.508834 21.403259 l +8.507838 21.396183 l +9.250531 21.291744 l +h +1.112203 13.140323 m +1.028052 13.885592 l +1.025491 13.885293 l +1.112203 13.140323 l +h +1.112203 10.729038 m +1.215387 11.471931 l +1.209505 11.472700 l +1.112203 10.729038 l +h +9.250531 2.566948 m +8.510169 2.447100 l +8.511623 2.438120 l +8.513294 2.429176 l +9.250531 2.566948 l +h +10.448288 0.750008 m +11.450765 0.750008 12.255557 1.493191 12.389213 2.476614 c +10.902877 2.678619 l +10.865425 2.403053 10.664957 2.250008 10.448288 2.250008 c +10.448288 0.750008 l +h +12.388570 2.471989 m +12.620602 4.103085 12.844038 5.334888 13.138243 6.288948 c +13.429995 7.235054 13.776924 7.858273 14.225985 8.307880 c +15.140809 9.223818 16.666159 9.620979 19.879219 9.983774 c +19.710920 11.474303 l +16.592981 11.122249 14.509018 10.713870 13.164680 9.367895 c +12.484159 8.686545 12.039045 7.814714 11.704848 6.730967 c +11.373105 5.655174 11.136688 4.322321 10.903521 2.683245 c +12.388570 2.471989 l +h +19.881781 9.984068 m +20.873766 10.099530 21.667965 10.918526 21.667965 11.934681 c +20.167965 11.934681 l +20.167965 11.734525 19.999683 11.507918 19.708359 11.474010 c +19.881781 9.984068 l +h +21.667965 11.934681 m +21.667965 12.966555 20.881077 13.753887 19.904659 13.883774 c +19.706867 12.396872 l +20.013762 12.356048 20.167965 12.140457 20.167965 11.934681 c +21.667965 11.934681 l +h +19.902761 13.884024 m +18.330524 14.089085 17.151228 14.287123 16.235826 14.561028 c +15.331247 14.831694 14.731587 15.163220 14.286853 15.607450 c +13.840208 16.053589 13.492528 16.670565 13.191763 17.611713 c +12.888441 18.560867 12.648228 19.788363 12.386598 21.421082 c +10.905493 21.183746 l +11.167881 19.546295 11.422276 18.221136 11.762950 17.155106 c +12.106180 16.081072 12.551881 15.220337 13.226795 14.546188 c +13.903622 13.870131 14.753702 13.438795 15.805837 13.123979 c +16.847151 12.812399 18.131544 12.602333 19.708765 12.396622 c +19.902761 13.884024 l +h +12.385450 21.428030 m +12.223795 22.379583 11.461336 23.119354 10.448288 23.119354 c +10.448288 21.619354 l +10.654386 21.619354 10.854410 21.484234 10.906639 21.176800 c +12.385450 21.428030 l +h +10.448288 23.119354 m +9.455997 23.119354 8.656921 22.387987 8.508867 21.403254 c +9.992196 21.180237 l +10.036638 21.475830 10.242823 21.619354 10.448288 21.619354 c +10.448288 23.119354 l +h +8.507838 21.396183 m +8.278477 19.765110 8.056973 18.533388 7.764218 17.579443 c +7.473907 16.633463 7.127907 16.010515 6.679636 15.561167 c +5.766263 14.645599 4.241331 14.248406 1.028053 13.885587 c +1.196352 12.395059 l +4.314074 12.747088 6.398453 13.155437 7.741569 14.501781 c +8.421542 15.183390 8.865580 16.055490 9.198210 17.139366 c +9.528394 18.215273 9.762733 19.548208 9.993224 21.187307 c +8.507838 21.396183 l +h +1.025491 13.885293 m +0.028613 13.769261 -0.750000 12.956783 -0.750000 11.934681 c +0.750000 11.934681 l +0.750000 12.150229 0.912482 12.362013 1.198914 12.395352 c +1.025491 13.885293 l +h +-0.750000 11.934681 m +-0.750000 10.922595 0.016880 10.115961 1.014900 9.985377 c +1.209505 11.472700 l +0.902826 11.512827 0.750000 11.730454 0.750000 11.934681 c +-0.750000 11.934681 l +h +1.009022 9.986170 m +2.582546 9.767614 3.760892 9.563175 4.675031 9.286510 c +5.578484 9.013078 6.174878 8.682938 6.616406 8.241908 c +7.059544 7.799271 7.404263 7.187467 7.703608 6.250257 c +8.005574 5.304842 8.245770 4.080437 8.510169 2.447100 c +9.990894 2.686794 l +9.725928 4.323629 9.471515 5.645210 9.132493 6.706642 c +8.790851 7.776280 8.348207 8.632186 7.676467 9.303166 c +7.003119 9.975756 6.157125 10.405144 5.109545 10.722197 c +4.072651 11.036015 2.791318 11.253017 1.215384 11.471908 c +1.009022 9.986170 l +h +8.513294 2.429176 m +8.688872 1.489628 9.455215 0.750008 10.448288 0.750008 c +10.448288 2.250008 l +10.264993 2.250008 10.047464 2.385277 9.987769 2.704720 c +8.513294 2.429176 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 72.573807 58.608093 cm +1.000000 1.000000 1.000000 scn +8.619835 -0.000011 m +9.122732 -0.000011 9.537402 0.373419 9.607985 0.897997 c +10.375565 6.348283 11.107853 7.095141 16.330927 7.690849 c +16.860292 7.753087 17.257317 8.188755 17.257317 8.695551 c +17.257317 9.211239 16.869114 9.629124 16.339748 9.700253 c +11.143145 10.384872 10.472614 11.051710 9.607985 16.501997 c +9.519756 17.026575 9.122732 17.391113 8.619835 17.391113 c +8.125761 17.391113 7.711091 17.026575 7.631686 16.493105 c +6.872929 11.042820 6.140640 10.295961 0.917567 9.700253 c +0.388201 9.638015 0.000000 9.211239 0.000000 8.695551 c +0.000000 8.188755 0.379379 7.761978 0.917567 7.690849 c +6.114172 6.961774 6.758233 6.339392 7.631686 0.889107 c +7.728737 0.364527 8.134583 -0.000011 8.619835 -0.000011 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 72.573807 57.608093 cm +0.049479 0.742188 0.545395 scn +9.607985 1.897997 m +9.112861 1.967726 l +9.112450 1.964672 l +9.607985 1.897997 l +h +16.330927 8.690849 m +16.387587 8.194067 l +16.389311 8.194269 l +16.330927 8.690849 l +h +16.339748 10.700253 m +16.406334 11.195801 l +16.405056 11.195970 l +16.339748 10.700253 l +h +9.607985 17.501997 m +10.101830 17.580339 l +10.101059 17.584925 l +9.607985 17.501997 l +h +7.631686 17.493105 m +7.137113 17.566721 l +7.136462 17.562048 l +7.631686 17.493105 l +h +0.917567 10.700253 m +0.860907 11.197035 l +0.859183 11.196833 l +0.917567 10.700253 l +h +0.917567 8.690849 m +0.987038 9.186015 l +0.983079 9.186539 l +0.917567 8.690849 l +h +7.631686 1.889107 m +7.137843 1.809963 l +7.140029 1.798147 l +7.631686 1.889107 l +h +8.619835 0.499989 m +9.388696 0.499989 10.001672 1.074373 10.103518 1.831324 c +9.112450 1.964672 l +9.073133 1.672462 8.856770 1.499989 8.619835 1.499989 c +8.619835 0.499989 l +h +10.103099 1.828268 m +10.294624 3.188222 10.480069 4.223436 10.725984 5.028956 c +10.970345 5.829387 11.264832 6.369568 11.654185 6.763333 c +12.442908 7.560994 13.744701 7.892640 16.387587 8.194070 c +16.274267 9.187629 l +13.694078 8.893350 12.018190 8.553713 10.943106 7.466446 c +10.400556 6.917747 10.041607 6.212054 9.769561 5.320940 c +9.499068 4.434915 9.305134 3.332915 9.112870 1.967726 c +10.103099 1.828268 l +h +16.389311 8.194269 m +17.155991 8.284410 17.757317 8.920855 17.757317 9.695551 c +16.757317 9.695551 l +16.757317 9.456655 16.564592 9.221766 16.272543 9.187428 c +16.389311 8.194269 l +h +17.757317 9.695551 m +17.757317 10.482604 17.162529 11.094193 16.406334 11.195800 c +16.273165 10.204706 l +16.575699 10.164056 16.757317 9.939874 16.757317 9.695551 c +17.757317 9.695551 l +h +16.405056 11.195970 m +15.107601 11.366901 14.126670 11.532858 13.361839 11.764020 c +12.604449 11.992933 12.090017 12.277006 11.704502 12.665974 c +11.317406 13.056538 11.022324 13.591100 10.770527 14.386979 c +10.517109 15.187984 10.317721 16.219311 10.101810 17.580338 c +9.114160 17.423656 l +9.330563 16.059540 9.539227 14.963654 9.817105 14.085340 c +10.096603 13.201900 10.456060 12.505035 10.994250 11.962027 c +11.534022 11.417421 12.215625 11.065775 13.072525 10.806786 c +13.921983 10.550046 14.973595 10.375915 16.274443 10.204536 c +16.405056 11.195970 l +h +10.101059 17.584925 m +9.977288 18.320837 9.395552 18.891113 8.619835 18.891113 c +8.619835 17.891113 l +8.849913 17.891113 9.062225 17.732313 9.114909 17.419067 c +10.101059 17.584925 l +h +8.619835 18.891113 m +7.859531 18.891113 7.250215 18.326427 7.137135 17.566717 c +8.126238 17.419493 l +8.171968 17.726725 8.391990 17.891113 8.619835 17.891113 c +8.619835 18.891113 l +h +7.136462 17.562048 m +6.947139 16.202108 6.763302 15.166948 6.518592 14.361504 c +6.275430 13.561155 5.981721 13.021152 5.592998 12.627558 c +4.805452 11.830145 3.503936 11.498478 0.860908 11.197033 c +0.974226 10.203474 l +3.554271 10.497736 5.230436 10.837353 6.304493 11.924868 c +6.846570 12.473737 7.204643 13.179608 7.475406 14.070805 c +7.744622 14.956905 7.936855 16.058958 8.126910 17.424164 c +7.136462 17.562048 l +h +0.859183 11.196833 m +0.089259 11.106312 -0.500000 10.475979 -0.500000 9.695551 c +0.500000 9.695551 l +0.500000 9.946500 0.687143 10.169718 0.975950 10.203673 c +0.859183 11.196833 l +h +-0.500000 9.695551 m +-0.500000 8.923503 0.079786 8.297226 0.852054 8.195160 c +0.983079 9.186539 l +0.678971 9.226731 0.500000 9.454006 0.500000 9.695551 c +-0.500000 9.695551 l +h +0.848098 8.195699 m +2.146421 8.013546 3.126409 7.842214 3.889960 7.608789 c +4.646159 7.377612 5.157835 7.094736 5.540681 6.708459 c +5.924912 6.320785 6.217544 5.790504 6.468155 4.997945 c +6.720429 4.200129 6.919805 3.171420 7.137986 1.809986 c +8.125387 1.968225 l +7.906841 3.331934 7.698164 4.424881 7.421624 5.299438 c +7.143422 6.179253 6.786478 6.872063 6.250936 7.412404 c +5.714010 7.954142 5.035716 8.304206 4.182313 8.565100 c +3.336262 8.823746 2.287016 9.003614 0.987036 9.186000 c +0.848098 8.195699 l +h +7.140029 1.798147 m +7.274719 1.070122 7.860904 0.499989 8.619835 0.499989 c +8.619835 1.499989 l +8.408263 1.499989 8.182755 1.658932 8.123343 1.980066 c +7.140029 1.798147 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 107.227104 75.129669 cm +0.049479 0.742188 0.545395 scn +5.619252 -0.000010 m +5.947090 -0.000010 6.217412 0.238985 6.263425 0.574716 c +6.763808 4.062898 7.241186 4.540888 10.646096 4.922141 c +10.991189 4.961974 11.250008 5.240800 11.250008 5.565150 c +11.250008 5.895190 10.996941 6.162637 10.651848 6.208159 c +7.264192 6.646316 6.827075 7.073092 6.263425 10.561275 c +6.205909 10.897006 5.947090 11.130310 5.619252 11.130310 c +5.297166 11.130310 5.026845 10.897006 4.975080 10.555585 c +4.480448 7.067402 4.003070 6.589413 0.598160 6.208159 c +0.253068 6.168327 0.000000 5.895190 0.000000 5.565150 c +0.000000 5.240800 0.247316 4.967664 0.598160 4.922141 c +3.985816 4.455533 4.405678 4.057208 4.975080 0.569025 c +5.038347 0.233294 5.302918 -0.000010 5.619252 -0.000010 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 103.008408 84.868683 cm +0.049479 0.742188 0.545395 scn +3.160831 -0.000001 m +3.345240 -0.000001 3.497297 0.134433 3.523178 0.323282 c +3.804644 2.285384 4.073170 2.554254 5.988433 2.768708 c +6.182547 2.791114 6.328133 2.947954 6.328133 3.130401 c +6.328133 3.316049 6.185782 3.466487 5.991668 3.492094 c +4.086111 3.738557 3.840232 3.978619 3.523178 5.940721 c +3.490826 6.129570 3.345240 6.260803 3.160831 6.260803 c +2.979658 6.260803 2.827601 6.129570 2.798484 5.937521 c +2.520254 3.975418 2.251728 3.706549 0.336465 3.492094 c +0.142351 3.469688 0.000000 3.316049 0.000000 3.130401 c +0.000000 2.947954 0.139115 2.794315 0.336465 2.768708 c +2.242023 2.506241 2.478195 2.282184 2.798484 0.320081 c +2.834072 0.131233 2.982893 -0.000001 3.160831 -0.000001 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 103.711479 69.564484 cm +0.049479 0.742188 0.545395 scn +3.863233 0.000004 m +4.088622 0.000004 4.274468 0.164312 4.306101 0.395127 c +4.650115 2.793253 4.978312 3.121871 7.319186 3.383983 c +7.556437 3.411368 7.734375 3.603061 7.734375 3.826052 c +7.734375 4.052955 7.560391 4.236824 7.323141 4.268121 c +4.994129 4.569354 4.693611 4.862762 4.306101 7.260888 c +4.266560 7.491703 4.088622 7.652100 3.863233 7.652100 c +3.641799 7.652100 3.455953 7.491703 3.420365 7.256976 c +3.080306 4.858850 2.752109 4.530232 0.411235 4.268121 c +0.173984 4.240736 0.000000 4.052955 0.000000 3.826052 c +0.000000 3.603061 0.170030 3.415280 0.411235 3.383983 c +2.740246 3.063190 3.028902 2.789341 3.420365 0.391215 c +3.463861 0.160400 3.645753 0.000004 3.863233 0.000004 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 -0.070938 78.607880 cm +0.049479 0.742188 0.545395 scn +5.268045 -0.000012 m +5.575393 -0.000012 5.828820 0.224045 5.871957 0.538792 c +6.341066 3.808963 6.788608 4.257079 9.980709 4.614503 c +10.304233 4.651846 10.546875 4.913247 10.546875 5.217325 c +10.546875 5.526737 10.309625 5.777468 9.986101 5.820146 c +6.810176 6.230918 6.400379 6.631021 5.871957 9.901192 c +5.818036 10.215940 5.575393 10.434662 5.268045 10.434662 c +4.966090 10.434662 4.712663 10.215940 4.664135 9.895857 c +4.200418 6.625686 3.752876 6.177571 0.560775 5.820146 c +0.237251 5.782803 0.000000 5.526737 0.000000 5.217325 c +0.000000 4.913247 0.231859 4.657181 0.560775 4.614503 c +3.736700 4.177058 4.130321 3.803629 4.664135 0.533457 c +4.723447 0.218710 4.971482 -0.000012 5.268045 -0.000012 c +h +f +n +Q +q +/E1 gs +1.000000 0.000000 -0.000000 1.000000 9.772858 88.346924 cm +0.049479 0.742188 0.545395 scn +2.458421 -0.000008 m +2.601850 -0.000008 2.720115 0.104552 2.740246 0.251434 c +2.959164 1.777514 3.168016 1.986634 4.657663 2.153433 c +4.808641 2.170859 4.921874 2.292846 4.921874 2.434749 c +4.921874 2.579142 4.811157 2.696150 4.660179 2.716066 c +3.178081 2.907759 2.986843 3.094474 2.740246 4.620554 c +2.715083 4.767436 2.601850 4.869507 2.458421 4.869507 c +2.317508 4.869507 2.199242 4.767436 2.176596 4.618064 c +1.960194 3.091985 1.751342 2.882864 0.261695 2.716066 c +0.110717 2.698639 0.000000 2.579142 0.000000 2.434749 c +0.000000 2.292846 0.108201 2.173349 0.261695 2.153433 c +1.743793 1.949291 1.927482 1.775024 2.176596 0.248944 c +2.204275 0.102062 2.320024 -0.000008 2.458421 -0.000008 c +h +f +n +Q +q +/E2 gs +1.000000 0.000000 -0.000000 1.000000 13.288479 82.086121 cm +0.049479 0.742188 0.545395 scn +2.458421 -0.000008 m +2.601850 -0.000008 2.720116 0.104552 2.740247 0.251434 c +2.959164 1.777514 3.168017 1.986634 4.657664 2.153433 c +4.808642 2.170859 4.921875 2.292846 4.921875 2.434749 c +4.921875 2.579142 4.811158 2.696150 4.660181 2.716066 c +3.178082 2.907759 2.986844 3.094474 2.740247 4.620554 c +2.715084 4.767436 2.601850 4.869507 2.458421 4.869507 c +2.317509 4.869507 2.199243 4.767436 2.176596 4.618064 c +1.960195 3.091985 1.751342 2.882864 0.261695 2.716066 c +0.110717 2.698639 0.000000 2.579142 0.000000 2.434749 c +0.000000 2.292846 0.108201 2.173349 0.261695 2.153433 c +1.743793 1.949291 1.927483 1.775024 2.176596 0.248944 c +2.204276 0.102062 2.320025 -0.000008 2.458421 -0.000008 c +h +f +n +Q + +endstream +endobj + +2 0 obj + 17245 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 119.000000 93.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 93.000000 m +119.000000 93.000000 l +119.000000 0.000000 l +0.000000 0.000000 l +0.000000 93.000000 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 234 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 46 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 119.000000 93.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000017630 00000 n +0000017654 00000 n +0000018137 00000 n +0000018159 00000 n +0000018457 00000 n +0000018559 00000 n +0000018580 00000 n +0000018754 00000 n +0000018828 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +18888 +%%EOF \ No newline at end of file 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 000000000..7d49fc335 --- /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 000000000..73c00596a --- /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 e2d6c1b3f..8ba53009d 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 3c25ee336..70ad1b32e 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 df6ce4e94..59315f1d9 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 170e912d1..f8c4b4704 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 61329d157..69151e276 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -70,6 +70,11 @@ @property(nonatomic) BOOL reviewSessionAlertHasBeenDisplayed; +/** + A flag to indicate that the analytics prompt should be shown during `-addMatrixSession:`. + */ +@property(nonatomic) BOOL presentAnalyticsPromptOnAddSession; + @end @implementation MasterTabBarController @@ -196,10 +201,24 @@ if (!authIsShown) { +#warning This is for debugging, Remove me! +// [RiotSettings.defaults setBool:YES forKey:@"enableCrashReport"]; +// [RiotSettings.defaults setBool:NO forKey:@"enableCrashReport"]; + [RiotSettings.defaults removeObjectForKey:@"enableCrashReport"]; + [RiotSettings.defaults removeObjectForKey:@"enableAnalytics"]; + // Check whether the user should be prompted to send analytics. if (Analytics.shared.shouldShowAnalyticsPrompt) { - [self promptUserBeforeUsingAnalytics]; + MXSession *mxSession = self.mxSessions.firstObject; + if (mxSession) + { + [self promptUserBeforeUsingAnalyticsForSession:mxSession]; + } + else + { + self.presentAnalyticsPromptOnAddSession = YES; + } } [self refreshTabBarBadges]; @@ -404,6 +423,12 @@ return; } + if (self.presentAnalyticsPromptOnAddSession) + { + self.presentAnalyticsPromptOnAddSession = NO; + [self promptUserBeforeUsingAnalyticsForSession:mxSession]; + } + // Check whether the controller'€™s view is loaded into memory. if (self.homeViewController) { @@ -920,56 +945,12 @@ #pragma mark - -- (void)promptUserBeforeUsingAnalytics +- (void)promptUserBeforeUsingAnalyticsForSession:(MXSession *)mxSession { MXLogDebug(@"[MasterTabBarController]: Invite the user to send analytics"); - 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 *title = [VectorL10n analyticsPromptTitle:AppInfo.current.displayName]; - NSString *message; - if (Analytics.shared.promptShouldDisplayUpgradeMessage) - { - message = [VectorL10n analyticsPromptPosthogUpgrade:AppInfo.current.displayName]; - } - else - { - message = [VectorL10n analyticsPromptNewUser:AppInfo.current.displayName]; - } - - currentAlert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n no] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - MXStrongifyAndReturnIfNil(self); - [Analytics.shared optOut]; - self->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 63bf354a2..f88b7d420 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 000000000..8df27340a --- /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 000000000..f65aded17 --- /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 000000000..b61796e97 --- /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 000000000..3ac17376e --- /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 000000000..ada017da6 --- /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 000000000..de1789afc --- /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 000000000..ec32dedda --- /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 78535b31a..2a11753a6 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 9d94bac8d..c0dd43d83 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 bdf54e7bd..9e81488a5 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 190d12a9f..3eb01bbdd 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 feef94130..a4a75ef88 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 4f0453231..6c7cfd36f 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 61f5764ccaafebe67be3dde4cc6f63c8f9ce6ac3 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Dec 2021 12:09:26 +0000 Subject: [PATCH 14/40] 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 9e9c5451c..0cdf3544d 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 f356c62a3..b378f9fbe 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 8ba53009d..17ecfa6a2 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 59315f1d9..b8546ccda 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 func analyticsPromptTermsNewUser(_ p1: String) -> String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_new_user", p1) } - /// 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") + /// 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 0222e2d7f..cccd68790 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 3ac17376e..572ff2167 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 000000000..a7a3f6a5f --- /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 de1789afc..fecd0b284 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 000000000..9019e9033 --- /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 ec32dedda..0a3ab40c2 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 + + 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 + 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 { - let (start, link, end) = promptType.termsStrings - - Text(start) - + Text(link).foregroundColor(theme.colors.accent) - + Text(end) + 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 c0dd43d83..5824cbc85 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 d2785df5ba3a76ae7c4d9130d6136fb9efcef61c Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Dec 2021 12:29:02 +0000 Subject: [PATCH 15/40] 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 cccd68790..6b03cbb5f 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 97d3c25f2..000000000 --- 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 76753eed5..a6271fae0 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 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo // 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 9dc3c5920..b4c04c610 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 da79f11a3..a899115f3 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 70e0eed440c458de2bf05e373bb664b7fe113d0f Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 7 Dec 2021 15:08:48 +0000 Subject: [PATCH 16/40] 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 f4e7a1f10..695d4cd61 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 c9ab63d85..9e4a5a137 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 ca9078e41..a8674758c 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 0cdf3544d..714154461 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 b378f9fbe..afe3025d7 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 6b03cbb5f..837869254 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 403dd91d7..000000000 --- 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 ae9ee09af..0552a459c 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 13a9a7b67..3aa22a842 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 cf13618d5..6f6543e94 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -648,7 +648,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni 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 a6271fae0..c941fbc98 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKTools.m +++ b/Riot/Modules/MatrixKit/Utils/MXKTools.m @@ -883,7 +883,7 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo // 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 2d411a716..59fabc9e5 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 f27ab4c33..1c111e446 100644 --- a/Riot/Utils/Tools.m +++ b/Riot/Utils/Tools.m @@ -138,4 +138,41 @@ 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 000000000..ea34f30c8 --- /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 b5a2f35f9..656558b4d 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 8f2c73452..999e2a95f 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 96ca50876..a85fd8d44 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 8c1ba1311..e0a09f552 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 572ff2167..9c303bbbe 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 a7a3f6a5f..0e2dba938 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 0a3ab40c2..7616e1084 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 000000000..6be81ff04 --- /dev/null +++ b/changelog.d/5035.change @@ -0,0 +1 @@ +Analytics: Replace Matomo with PostHog. \ No newline at end of file From ecfaed87b7ff40b1f681bc896f94d84a6e08d605 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 8 Dec 2021 12:00:23 +0000 Subject: [PATCH 17/40] 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 17ecfa6a2..f513009c6 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 b8546ccda..d47bdfb99 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 837869254..617f7fec4 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 0e2dba938..ee0e59ed0 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 fecd0b284..a5a9635ac 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 b31cbd6c5..5e15bf523 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 28464b8a9997ef673de25f312f36852bc911e58a Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 8 Dec 2021 12:49:57 +0000 Subject: [PATCH 18/40] 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 617f7fec4..00135edf4 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 000000000..208f2cfc6 --- /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 1d879c41c..004b7717e 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 fef1780f9..86f567acd 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 113049b6f..000000000 --- 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 7cb470e8c..000000000 --- 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 000000000..4c6c4ec95 --- /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 7903611ce..b8f9ca467 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 d34ba0017..0f2b2ff81 100644 --- a/Riot/Managers/Analytics/DecryptionFailureTracker.m +++ b/Riot/Managers/Analytics/DecryptionFailureTracker.m @@ -91,31 +91,32 @@ NSString *const kDecryptionFailureTrackerAnalyticsCategory = @"e2e.failure"; 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 a899115f3..6f9767f3c 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 b44642a31d1be0a91d79c5244d6f1fe1727d7fca Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 9 Dec 2021 16:20:22 +0000 Subject: [PATCH 19/40] 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 9e4a5a137..d07f6efb5 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 f513009c6..119391d9a 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 d47bdfb99..af25f0458 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 6f6543e94..946f22b26 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -650,7 +650,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // 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 656558b4d..add81e84b 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 a5a9635ac..8f7acf49d 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 5c19dca3ecdf5ec60a8db61949bb5588e339a71f Mon Sep 17 00:00:00 2001 From: Doug Date: Mon, 13 Dec 2021 17:30:38 +0000 Subject: [PATCH 20/40] Leave analytics client running on sign out. Only identify with a running session. --- Riot/Managers/Analytics/Analytics.swift | 68 ++++++++++++------- .../Analytics/AnalyticsClientProtocol.swift | 6 +- .../Analytics/PostHogAnalyticsClient.swift | 5 +- Riot/Modules/Application/LegacyAppDelegate.m | 5 ++ 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index 00135edf4..2fb8ba079 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 - /// account used isn't associated with the previous one. - func reset() { - guard isRunning else { return } + /// 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() { 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 7ee0ae429..af07a58fb 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 0552a459c..1c7172112 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 946f22b26..fd6354ad3 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1884,6 +1884,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [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 0ef348e2cad642e3f8f2e5d173a966d33bc53aac Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 15 Dec 2021 14:40:45 +0000 Subject: [PATCH 21/40] 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 22cadcaac..4d215eb98 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 afe3025d7..be012e690 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 2fb8ba079..16ced46cb 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 004b7717e..000000000 --- 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 67927cace..aaa2f8353 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 208f2cfc6..93a08e7e2 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 4c6c4ec95..d011a0413 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 000000000..d5473d5f9 --- /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 000000000..4b8911ce8 --- /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 000000000..99f89174e --- /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 11398f220..0030b9256 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 6f9767f3c..c5a22e8a7 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 000000000..605ee9d08 --- /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 000000000..260ab83f5 --- /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 ea34f30c8..8319388e1 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 59fabc9e5..2d411a716 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 1c111e446..f27ab4c33 100644 --- a/Riot/Utils/Tools.m +++ b/Riot/Utils/Tools.m @@ -138,41 +138,4 @@ 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 a85fd8d44..208b289d7 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 e0a09f552..4ce8ab20d 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 b6438c1893aefc0f67e98137e55c75078f4ac095 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 15 Dec 2021 15:06:10 +0000 Subject: [PATCH 22/40] 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 aaa2f8353..7d3352548 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 d75fed56e4d8222610d8b9962a1f0112afb976ce Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 16 Dec 2021 13:48:14 +0000 Subject: [PATCH 23/40] 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 16ced46cb..4dd67a5ed 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 000000000..820102c85 --- /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 7d3352548..e847f0668 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 d7ba5dd3e5969255022ef5f5862b791611d3ba0e Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 16 Dec 2021 13:50:10 +0000 Subject: [PATCH 24/40] 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 5658bf97b5d66c44c38c5bf404327fc17db273a2 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 16 Dec 2021 16:58:29 +0000 Subject: [PATCH 25/40] 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 be012e690..8ff43e554 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 4dd67a5ed..535ca7b2a 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 820102c85..bf24edfd1 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 0030b9256..ac72eb2eb 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 ec69538fe5f84e260da87778dbe7f2e6a6869281 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 4 Jan 2022 15:46:07 +0300 Subject: [PATCH 26/40] Update icons & summary view margins --- .../Thread.png | Bin 318 -> 0 bytes .../Thread@2x.png | Bin 418 -> 0 bytes .../Thread@3x.png | Bin 706 -> 0 bytes .../Contents.json | 26 ++++++++++++++++++ .../Vector.png | Bin 0 -> 389 bytes .../Vector@2x.png | Bin 0 -> 593 bytes .../Vector@3x.png | Bin 0 -> 801 bytes .../threads_icon.imageset}/Contents.json | 3 ++ .../Threads/threads_icon.imageset/Thread.png | Bin 0 -> 484 bytes .../threads_icon.imageset/Thread@2x.png | Bin 0 -> 780 bytes .../threads_icon.imageset/Thread@3x.png | Bin 0 -> 1029 bytes Riot/Generated/Images.swift | 3 +- .../RoomContextualMenuAction.swift | 2 +- Riot/Modules/Room/RoomViewController.m | 3 +- .../Views/Threads/ThreadSummaryView.swift | 3 +- .../Room/Views/Threads/ThreadSummaryView.xib | 22 +++++++-------- .../ThreadList/ThreadListViewModel.swift | 4 +-- .../Views/Empty/ThreadListEmptyView.swift | 1 + .../Views/Empty/ThreadListEmptyView.xib | 4 +-- 19 files changed, 52 insertions(+), 19 deletions(-) delete mode 100644 Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread.png delete mode 100644 Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread@2x.png delete mode 100644 Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread@3x.png create mode 100644 Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Vector.png create mode 100644 Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Vector@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Vector@3x.png rename Riot/Assets/Images.xcassets/Room/{ContextMenu/room_context_menu_reply_in_thread.imageset => Threads/threads_icon.imageset}/Contents.json (84%) create mode 100644 Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread.png create mode 100644 Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@3x.png diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread.png deleted file mode 100644 index ec23ad509d45acc94beb51fef16bf7fdda0b0902..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 318 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|&H|6fVg?3oVGw3ym^DWND9BhG z=tK=QvEMO8= zR8pGBA#K!Y$(U!5cYya9N1p-v0`KUbFVFqauUYN)b@lc4y0a2Ki0v+G&P?gk6=^Nt z37ah3JN-6eBmat~X?&Xtt5 z!$1&4?@TBpr6iS*bbzq@1wkO-2a}`(QUV>|7U%%_Ks)SUtXO&NN^!+Q{)Q{<5G;4GW3BaaWYgX*88F|4fF3n ziLVZtu1<-s5<8;}9D}+rT1F=nqIE(Sam!@k9KNv!Oz4B?GPEv2NEXw_bc`BEZZm|W zjxjPCC6f#-imZ@gqgtMBm&Ha>gHU{z#dJ}K)9o_D$O@^m?XpfNL?^Th9ULRO?C^*m z7-!{N)&|H=uX=4p!Vx$HWg!bUKkz|TMi#=J@o7{D@l##) zz)kW%$j#$-qg3AnWwa2*cW_sfrY9Mm#PM{lVJ>X%;AqFa^7|JU;jJRpS94(q_!>MR zfM8pnQFlQm#5s6`V6!^`n-zWV4DG!EcuFVCH9BFg;jOR`Y#~V1L^FpqLVAjaU}Z;E z!cfv+@^+X&1MD3f$7*IY1w+hBktl6Sl;+dnFhPXW@MB%`_zPlRmj^V&(^r#SBJ*)( zH^x-W@4uXE6Rx*;AVJSYl*YO%l4Z9te1|pN7;}wA4Z4oun$oyI+ZwJZjT^MB;hNGl zj|DXi+SYJQY1E+W7_KRe8gw1QHKiM4uF(l|jZTIc5yEmkCqm%_In=l zSIf`pyE#>>>Kol(#1&QnMOb8)GgQS&m`Z&n89hn(XzfdUTWm`4t4jUdHjC02Wvt1c zkLt3Lutn(|wmVCgS)~-6D7`9IX3VA1s}cqqluNPPUtKJ{D(uJUw_gt)KDw>;)b9U@ o>C$_g@D~~QK+KQ&`T$S(4FO#8{Gg*8G5`Po07*qoM6N<$f{Z{lTmS$7 diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Contents.json new file mode 100644 index 000000000..32dd965fb --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "Vector.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Vector@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Vector@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Vector.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Vector.png new file mode 100644 index 0000000000000000000000000000000000000000..38cd35e5f76db714560f013800ebf86df3f923b7 GIT binary patch literal 389 zcmV;00eb$4P)J9V;&Jm&#D8#Gy zpY_Xgm6PV4XJq1>t)+6(?4V-<-$4zQpXt2(=CS6PuC`R*?G~-zrIKlL)N6~#u~`-3 zPw=OlF)T}i<|w%mHbOTvGY0mdOHw+NVlzsarJL=a{#cLSF?)TTt2`RV=NdIoJb|rC jZE_sgpMM|WK~#7F?U+qY z!ax+q-%LkcU`TJEN6;JC5Et(B2%dnFhzToT#R3g@0^9%Bh>VLu zWLyZHq`>U=#J%z?h#A>U`d^iLZC#GmPI^0rorFLI0odwLvo7cVe3QcP`$X^#2|z@$ zyETUEaF_DLQ8(h#S5z5;bNPTnNDx9=STor~Hi}el<|DM28mJPa&eZTOR*Ezq zTPB(%IkTjUhN^NNW1_?MplM}Rks#GGG8#G7o5v&#@ zRN|ekX>h?f@!F1rce_6!KD@g!f6eTAW<4hG1l7r9gBax#AlLwe8b|~oaED0l0R6AO zn(x1a$KyJi-!5zZ3;KzRY=9cL%is6b;#J?>5nTEHv2K(M2LMe_3y=9|Z3$jk_MrSj zL9uH*WAm4PKvlHtCT1x3SSTaO!Drt}≶Kg>C?jc*XLY4; z<2d5}d!k^~(1KM*3sxO1Satq8!4z_E=v5-v%8A3kU2C z;su)%v*uYxbVROIde(dwoJja&ML^GC)zN}gN5|4>c%E1~eLS3p0>skkGbh%9&{|NI$E&mXu+zZ1*?u0tP-X(+{xsW7RE&MPq z_@*67l@9)cc+6mykl??CP=>b%!scWErG#c|&SNyxryi8wA9^P^y1fJ9`lFkkH$Ud&r( zQdP;XNh?@77mgzA(z3d6luZiMH4T3NI%p$g3v_#z00000NkvXXu0mjf_{3j^ literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Contents.json similarity index 84% rename from Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Contents.json rename to Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Contents.json index d2c033d2d..92eec1362 100644 --- a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread.png b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread.png new file mode 100644 index 0000000000000000000000000000000000000000..b2cc0cb7234fc8f5f2abb293b0549d547c373106 GIT binary patch literal 484 zcmVGAwoIq12c>QPzZ^V zzDyf|tdRnL{DKzO5k3=0h_q+FW;zM|;d5Px-s0-oc6g8tGGGh+hSw%x5xw?qZAXAq zGX6+5{aBGm0%0GUgtUmZTKwq{l84#jU<3obp*{CA?;s)CB-D1l@${5hA_dwcO3aJP zo9PP-FiS^}978)$BTE)AkVYWjcy@gApC!b_UrPwoD!MlPw(~Gp%_dh%B$5y~{>ZvW z0gJ5c9d{3-g!p#_7S*<6E-qQX1_MV~AB{W(0%DW+I=sf_!z*(M&+^XM<4(}K={Vo7 aFC3q^XLw01qM;!G0000;`EFqR4QCktk5ouGEpPChvtf1LJ=nm)#;_iT|l_~+A z$ryqZ0VXhGf$f=3LNc}oG{@&&-}wOy27^IkXq%fo|Hic<=bdQw7MX_sV&lnO@a04J z(w!R#;Jv?b?Sq5w;;@WdLA_`^x>_g1ivoBbzaHAQ{X?kgB9~BTMbUBd^Z8)q)pbp0 zPyW??emg&hLg9qUDkoU$nu*R|=D+hXd>+C}Bs$h z!T!M`C`8uY-9R=!!7h{F4DJ%>8*GleVhSN>ju8Nwqgo1gBj=wV_`cFx!1Mi* zGkN22qD$-;6#;tf>zaoYcoNXB)|)Pfc3NLZhX9Y*PoxZIRv$Y?J_k7DB5fo}Sykjq zVNR#<>Jd^R{wTP>Pq%su!)kr~^YNAlBzxOqbqwUr{QS!c)py?Xt)irVxVW z7y+O;MgVAz5da!N9Df$iX(a$BwzOAJ8v){#EM`aj#q;_&fY@!|FK?7gV=Rz;I0l`@ znZN3i4i2~z({_oAxW#oqymT*Y-)Wrp*4>t}05l}dx{O&N_5@jc7&+s?s(4>TY68r% z?5MKoA7_Pha)6i~XZGKtymdZC(E-Bpj9s-podw7#N?T;6mT%U}oXkrghb7(7*O7r?V?XzwL{}hsfhU$ z-$sX*ZMp?btJFKZgY8tjWQACQE?==^e9BgGwyA?P`Vi*{K_$l7OFkygDRXSJO1`cy ztM>o?qO7NxOTXpYpAXMf0$Rtw(6CuVRH0&D_~$86;Y+f9o#qk@&*0Qp#;9$o@NDtl zvd6YhOBM=jiz|6`L@Q>ReU+7I$}F?pYmc{230x=8w0!;Qx}AT*>Z6o@1lL>5%yG$| z$LqUav6w@hG3Mur^Z&FCT3#uzl3YKNZK7t20*Cg$Q>VmP5}epS-rX(gI(u=YuI;G< zySHuXX7DN8x<%;ytSu({g#6w!-{&xzX|&$;?c2I2>BriR_t|@bS~u4O@ju&oGN7;G z9e;Cuz!o_c_LZ>}iv^^r4BR$5N<0Zzxcd0&Ox_-+4$-wjPo5opIahLX(UBcodPjn< zUOCJ%olh}>0WVtMGHFKD>yQw0E@5?WTxm;>uO%GCeB$qZQ{^dv#_f=T0pN zwA~l9d)*1-iJGGGI1Ov zgc1d31c}cSIK*_Mi!JlGVAGz!ME>Qicb@GFXH?{v{cG~GJ!Tw^Lc!-#zAHKj96Gu9 zx3|61|4nhuZ`mZlCFuC==MWE~06?@JDHSL{m*1cJ}$W5zncICGRN-cLng0kFv z?tGtdzw}nG-P00HMHSQFe&yYlD8}8Bapw@ - + - + - - + + - + @@ -84,6 +84,6 @@ - + diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift index 1e980520d..b4de8df79 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift @@ -117,14 +117,14 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { private var emptyViewModel: ThreadListEmptyViewModel { switch selectedFilterType { case .all: - return ThreadListEmptyViewModel(icon: Asset.Images.roomContextMenuReplyInThread.image, + return ThreadListEmptyViewModel(icon: Asset.Images.threadsIcon.image, title: VectorL10n.threadsEmptyTitle, info: VectorL10n.threadsEmptyInfoAll, tip: VectorL10n.threadsEmptyTip, showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads, showAllThreadsButtonHidden: true) case .myThreads: - return ThreadListEmptyViewModel(icon: Asset.Images.roomContextMenuReplyInThread.image, + return ThreadListEmptyViewModel(icon: Asset.Images.threadsIcon.image, title: VectorL10n.threadsEmptyTitle, info: VectorL10n.threadsEmptyInfoMy, tip: VectorL10n.threadsEmptyTip, diff --git a/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift index 213f1fa18..98e9b0535 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift @@ -64,6 +64,7 @@ extension ThreadListEmptyView: Themable { func update(theme: Theme) { iconBackgroundView.backgroundColor = theme.colors.system + iconView.tintColor = theme.colors.secondaryContent titleLabel.textColor = theme.colors.primaryContent infoLabel.textColor = theme.colors.secondaryContent tipLabel.textColor = theme.colors.secondaryContent diff --git a/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.xib b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.xib index 50c62af99..bf0f68982 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.xib +++ b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.xib @@ -36,7 +36,7 @@ - + @@ -105,6 +105,6 @@ - + From e367c8152bf3c1eb76c811f85e57fe8e632eb0b2 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 4 Jan 2022 16:31:53 +0300 Subject: [PATCH 27/40] Shrink summary view if content is small --- Riot/Modules/Room/DataSources/RoomDataSource.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 6cdcb8efe..6f3b583b9 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -553,7 +553,7 @@ const CGFloat kTypingCellHeight = 24; constant:leftMargin], topConstraint, [threadSummaryView.heightAnchor constraintEqualToConstant:[ThreadSummaryView contentViewHeightForThread:component.thread fitting:cellData.maxTextViewWidth]], - [threadSummaryView.trailingAnchor constraintEqualToAnchor:threadSummaryView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin] + [threadSummaryView.trailingAnchor constraintLessThanOrEqualToAnchor:threadSummaryView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin] ]]; } From b811010b8faab690962c2317a8e3b963c149207f Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 4 Jan 2022 17:26:47 +0300 Subject: [PATCH 28/40] User name coloring on thread list --- .../ThreadList/ThreadListViewController.swift | 2 +- .../Threads/ThreadList/ThreadListViewModel.swift | 3 ++- .../ThreadList/Views/Cell/ThreadTableViewCell.swift | 12 ++++++++++++ .../ThreadList/Views/Cell/ThreadViewModel.swift | 1 + 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift index 56f16c5df..9b9b55bc8 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift @@ -249,10 +249,10 @@ extension ThreadListViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: ThreadTableViewCell = tableView.dequeueReusableCell(for: indexPath) + cell.update(theme: theme) if let threadVM = viewModel.threadViewModel(at: indexPath.row) { cell.configure(withViewModel: threadVM) } - cell.update(theme: theme) return cell } diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift index b4de8df79..0a3e8aaad 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift @@ -179,7 +179,8 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { lastMessageSenderAvatar: lastAvatarViewData, lastMessageText: lastMessageText) - return ThreadViewModel(rootMessageSenderAvatar: rootAvatarViewData, + return ThreadViewModel(rootMessageSenderUserId: rootMessageSender?.userId, + rootMessageSenderAvatar: rootAvatarViewData, rootMessageSenderDisplayName: rootMessageSender?.displayname, rootMessageText: rootMessageText, lastMessageTime: lastMessageTime, diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift index 000ee1b57..3a17d8b11 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift @@ -28,6 +28,11 @@ class ThreadTableViewCell: UITableViewCell { @IBOutlet private weak var rootMessageContentLabel: UILabel! @IBOutlet private weak var lastMessageTimeLabel: UILabel! @IBOutlet private weak var summaryView: ThreadSummaryView! + + private static var usernameColorGenerator: UserNameColorGenerator = { + let generator = UserNameColorGenerator() + return generator + }() override func awakeFromNib() { super.awakeFromNib() @@ -41,6 +46,11 @@ class ThreadTableViewCell: UITableViewCell { } else { rootMessageAvatarView.avatarImageView.image = nil } + if let senderUserId = viewModel.rootMessageSenderUserId { + rootMessageSenderLabel.textColor = Self.usernameColorGenerator.color(from: senderUserId) + } else { + rootMessageSenderLabel.textColor = Self.usernameColorGenerator.defaultColor + } rootMessageSenderLabel.text = viewModel.rootMessageSenderDisplayName rootMessageContentLabel.text = viewModel.rootMessageText lastMessageTimeLabel.text = viewModel.lastMessageTime @@ -56,6 +66,8 @@ extension ThreadTableViewCell: NibReusable {} extension ThreadTableViewCell: Themable { func update(theme: Theme) { + Self.usernameColorGenerator.defaultColor = theme.colors.primaryContent + Self.usernameColorGenerator.userNameColors = theme.colors.namesAndAvatars rootMessageAvatarView.backgroundColor = .clear rootMessageContentLabel.textColor = theme.colors.primaryContent lastMessageTimeLabel.textColor = theme.colors.secondaryContent diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadViewModel.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadViewModel.swift index 9bc842345..9692058f4 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadViewModel.swift @@ -17,6 +17,7 @@ import Foundation struct ThreadViewModel { + var rootMessageSenderUserId: String? var rootMessageSenderAvatar: AvatarViewDataProtocol? var rootMessageSenderDisplayName: String? var rootMessageText: String? From c0ada7adf303fa1f93d0989a06ff8203d5b8cd56 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 4 Jan 2022 17:27:13 +0300 Subject: [PATCH 29/40] Layout fixes --- .../Room/Views/Threads/ThreadSummaryView.xib | 5 +++- .../Views/Cell/ThreadTableViewCell.xib | 27 ++++++++++--------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib index 66752fca3..12f97e11c 100644 --- a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib +++ b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib @@ -34,6 +34,9 @@