diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 525870b51..d07f6efb5 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -165,8 +165,20 @@ final class BuildSettings: NSObject { static let roomsAllowToJoinPublicRooms: Bool = true // MARK: - Analytics - static let analyticsServerUrl = URL(string: "https://piwik.riot.im/piwik.php") - static let analyticsAppId = "14" + #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 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")! // MARK: - Bug report diff --git a/Podfile b/Podfile index 92a7bdad0..714154461 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,10 @@ 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.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 pod 'OLMKit', :inhibit_warnings => true diff --git a/Podfile.lock b/Podfile.lock index 3f62ea753..8ff43e554 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) @@ -56,9 +57,6 @@ 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): @@ -76,6 +74,7 @@ PODS: - OLMKit/olmcpp (= 3.2.5) - OLMKit/olmc (3.2.5) - OLMKit/olmcpp (3.2.5) + - PostHog (1.4.4) - ReadMoreTextView (3.0.1) - Realm (10.16.0): - Realm/Headers (= 10.16.0) @@ -103,6 +102,7 @@ PODS: - ZXingObjC/All (3.6.5) DEPENDENCIES: + - 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) @@ -116,10 +116,10 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatomoTracker (~> 7.4.1) - MatrixSDK (= 0.20.15) - MatrixSDK/JingleCallStack (= 0.20.15) - OLMKit + - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) - Reusable (~> 4.1) - SideMenu (~> 6.5) @@ -157,9 +157,9 @@ SPEC REPOS: - libPhoneNumber-iOS - LoggerAPI - Logging - - MatomoTracker - MatrixSDK - OLMKit + - PostHog - ReadMoreTextView - Realm - Reusable @@ -173,8 +173,19 @@ SPEC REPOS: - zxcvbn-ios - ZXingObjC +EXTERNAL SOURCES: + AnalyticsEvents: + :branch: release/swift + :git: https://github.com/matrix-org/matrix-analytics-events.git + +CHECKOUT OPTIONS: + AnalyticsEvents: + :commit: f1805ad7c3fafa7fd9c6e2eaa9e0165f8142ecd2 + :git: https://github.com/matrix-org/matrix-analytics-events.git + SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce + AnalyticsEvents: 333bf47d67dc628fadd29ce887b7ac93d8bd6e05 BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24 BlueECC: 0d18e93347d3ec6d41416de21c1ffa4d4cd3c2cc BlueRSA: dfeef51db96bcc4edec654956c1581adbda4e6a3 @@ -198,9 +209,9 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatomoTracker: 24a846c9d3aa76933183fe9d47fd62c9efa863fb MatrixSDK: 2f4d3aacb1c53e2785f0be71d24b8e62e5c5c056 OLMKit: 9fb4799c4a044dd2c06bda31ec31a12191ad30b5 + PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d Realm: b6027801398f3743fc222f096faa85281b506e6c Reusable: 6bae6a5e8aa793c9c441db0213c863a64bce9136 @@ -214,6 +225,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 989bcc8b1857dc64a9b810ddaf4446903adbe162 +PODFILE CHECKSUM: e60814fe2084a7dca3f82c3a1c4a1b763ae822c0 COCOAPODS: 1.11.2 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/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 ec23ad509..000000000 Binary files a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread@2x.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread@2x.png deleted file mode 100644 index 0fc08fd6b..000000000 Binary files a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread@2x.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread@3x.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread@3x.png deleted file mode 100644 index 64c675d3c..000000000 Binary files a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread@3x.png and /dev/null differ 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 000000000..38cd35e5f Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Vector.png differ diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Vector@2x.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Vector@2x.png new file mode 100644 index 000000000..dc6713ff5 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Vector@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Vector@3x.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Vector@3x.png new file mode 100644 index 000000000..11067906e Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_thread.imageset/Vector@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_filter_applied.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Threads/threads_filter_applied.imageset/Contents.json new file mode 100644 index 000000000..65fc07b23 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Threads/threads_filter_applied.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Right controls.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Right controls@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Right controls@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_filter_applied.imageset/Right controls.png b/Riot/Assets/Images.xcassets/Room/Threads/threads_filter_applied.imageset/Right controls.png new file mode 100644 index 000000000..b517cdfb1 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Threads/threads_filter_applied.imageset/Right controls.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_filter_applied.imageset/Right controls@2x.png b/Riot/Assets/Images.xcassets/Room/Threads/threads_filter_applied.imageset/Right controls@2x.png new file mode 100644 index 000000000..a9a275aef Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Threads/threads_filter_applied.imageset/Right controls@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_filter_applied.imageset/Right controls@3x.png b/Riot/Assets/Images.xcassets/Room/Threads/threads_filter_applied.imageset/Right controls@3x.png new file mode 100644 index 000000000..190f810eb Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Threads/threads_filter_applied.imageset/Right controls@3x.png differ 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 100% 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 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 000000000..b2cc0cb72 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@2x.png b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@2x.png new file mode 100644 index 000000000..fe86d55c3 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@3x.png b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@3x.png new file mode 100644 index 000000000..fae5443be Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/link_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/link_icon.imageset/Contents.json new file mode 100644 index 000000000..b5cd56ac8 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/link_icon.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "Link.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Link@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Link@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/link_icon.imageset/Link.png b/Riot/Assets/Images.xcassets/Room/link_icon.imageset/Link.png new file mode 100644 index 000000000..546ec9f08 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/link_icon.imageset/Link.png differ diff --git a/Riot/Assets/Images.xcassets/Room/link_icon.imageset/Link@2x.png b/Riot/Assets/Images.xcassets/Room/link_icon.imageset/Link@2x.png new file mode 100644 index 000000000..85c2c1bce Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/link_icon.imageset/Link@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/link_icon.imageset/Link@3x.png b/Riot/Assets/Images.xcassets/Room/link_icon.imageset/Link@3x.png new file mode 100644 index 000000000..9e44e1899 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/link_icon.imageset/Link@3x.png differ diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 2a2ccb8ef..20ea0ca21 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"; @@ -77,6 +78,7 @@ // Accessibility "accessibility_checkbox_label" = "checkbox"; +"accessibility_button_label" = "button"; // Authentication "auth_login" = "Log in"; @@ -387,6 +389,7 @@ Tap the + to start adding people."; "room_event_action_reaction_show_all" = "Show all"; "room_event_action_reaction_show_less" = "Show less"; "room_event_action_reaction_history" = "Reaction history"; +"room_event_copy_link_info" = "Link copied to clipboard."; "room_warning_about_encryption" = "End-to-end encryption is in beta and may not be reliable.\n\nYou should not yet trust it to secure data.\n\nDevices will not yet be able to decrypt history from before they joined the room.\n\nEncrypted messages will not be visible on clients that do not yet implement encryption."; "room_event_failed_to_send" = "Failed to send"; "room_action_camera" = "Take photo or video"; @@ -427,8 +430,8 @@ Tap the + to start adding people."; "threads_action_my_threads" = "My threads"; "threads_empty_title" = "Keep discussions organised with threads"; "threads_empty_info_all" = "Threads help keep your conversations on-topic and easy to track."; -"threads_empty_info_my" = "Reply to an ongoing thread or use “Thread” when selecting a message to start a new one."; -"threads_empty_tip" = "Tip: Use “Thread” option when selecting a message."; +"threads_empty_info_my" = "Reply to an ongoing thread or tap a message and use “Thread” to start a new one."; +"threads_empty_tip" = "Tip: Tap a message and use “Thread” to start one."; "threads_empty_show_all_threads" = "Show all threads"; "media_type_accessibility_image" = "Image"; @@ -595,7 +598,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"; @@ -964,8 +967,24 @@ 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 improve %@"; +"analytics_prompt_message_new_user" = "Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; +"analytics_prompt_message_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 */ +"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_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"; // 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/Categories/UIView+Toast.swift b/Riot/Categories/UIView+Toast.swift new file mode 100644 index 000000000..e00858afc --- /dev/null +++ b/Riot/Categories/UIView+Toast.swift @@ -0,0 +1,310 @@ +// +// 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: - ToastPosition + +/// Vertical position for a toast +@objc +enum ToastPosition: Int { + /// Toast will be placed at the top of the screen, with a margin to the safe area insets of the superview. Max height is also limited with safe area insets. + case top + /// Toast will be placed at the middle of the screen vertically. Max height is also limited with safe area insets. + case middle + /// Toast will be placed at the bottom of the screen, with a margin to the safe area insets of the superview. Max height is also limited with safe area insets. + case bottom +} + +// MARK: - UIView Extension + +extension UIView { + + private enum Constants { + static let defaultDuration: TimeInterval = 2.0 + static let defaultPosition: ToastPosition = .bottom + } + + private static var operationQueue: OperationQueue = { + let queue = OperationQueue.vc_createSerialOperationQueue(name: "ToastQueue") + queue.qualityOfService = .userInteractive + queue.underlyingQueue = .main + return queue + }() + + /// Show a toast message with the given properties. + /// - Parameters: + /// - message: Message to be displayed + /// - image: Icon to be displayed. Placed left to the message. Will be tinted. + /// - duration: Duration of the toast messsage + /// - position: Vertical position of the toast message in the view. Toast view spans the receiver view horizontally, taking into account the safe area insets. + /// - additionalMargin: By default, a toast placed according to safe area insets, with a margin. + /// For `top` and `bottom` positions, adds toast an additional margin from the top and bottom respectively. + /// Has no effect for `middle` position. + @objc + func vc_toast(message: String?, + image: UIImage? = nil, + duration: TimeInterval = Constants.defaultDuration, + position: ToastPosition = Constants.defaultPosition, + additionalMargin: CGFloat = 0.0) { + let view = ToastView(withMessage: message, image: image) + vc_toast(view: view, duration: duration, position: position, additionalMargin: additionalMargin) + } + + /// Show a toast view with the given properties. + /// - Parameters: + /// - view: View to be displayed as a toast + /// - duration: Duration of the toast messsage + /// - position: Vertical position of the toast message in the view. Toast view spans the receiver view horizontally, taking into account the safe area insets. + /// - additionalMargin: By default, a toast placed according to safe area insets, with a margin. + /// For `top` and `bottom` positions, adds toast an additional margin from the top and bottom respectively. + /// Has no effect for `middle` position. + @objc + func vc_toast(view: UIView, + duration: TimeInterval = Constants.defaultDuration, + position: ToastPosition = Constants.defaultPosition, + additionalMargin: CGFloat = 0.0) { + let operation = ToastOperation(containerView: self, + toastView: view, + duration: duration, + position: position, + additionalMargin: additionalMargin, + completion: nil) + Self.operationQueue.addOperation(operation) + } + +} + +// MARK: - ToastOperation + +/// Async toast UI operation. Will run on the main thread. +private class ToastOperation: AsyncOperation { + + private enum Constants { + static let margin: UIEdgeInsets = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) + static let animationDuration: TimeInterval = 0.25 + static let timeBetweenToasts: TimeInterval = 0.5 + } + + private var containerView: UIView + private var toastView: UIView + private var duration: TimeInterval + private var position: ToastPosition + private var additionalMargin: CGFloat + private var completion: (() -> Void)? + private var timer: Timer? + + init(containerView: UIView, + toastView: UIView, + duration: TimeInterval, + position: ToastPosition, + additionalMargin: CGFloat, + completion: (() -> Void)? = nil) { + self.containerView = containerView + self.toastView = toastView + self.duration = duration + self.position = position + self.additionalMargin = additionalMargin + self.completion = completion + } + + override func main() { + showToast { + self.invalidateTimer() + let timer = Timer(timeInterval: self.duration, + target: self, + selector: #selector(self.timerFired(_:)), + userInfo: nil, + repeats: false) + RunLoop.main.add(timer, forMode: .common) + self.timer = timer + } + } + + @objc + private func timerFired(_ timer: Timer) { + invalidateTimer() + hideToast() + } + + private func showToast(_ completion: @escaping () -> Void) { + toastView.alpha = 0.0 + containerView.addSubview(toastView) + toastView.translatesAutoresizingMaskIntoConstraints = false + switch position { + case .top: + NSLayoutConstraint.activate([ + toastView.leadingAnchor.constraint(equalTo: containerView.safeAreaLayoutGuide.leadingAnchor, + constant: Constants.margin.left), + toastView.topAnchor.constraint(equalTo: containerView.safeAreaLayoutGuide.topAnchor, + constant: Constants.margin.top + additionalMargin), + toastView.trailingAnchor.constraint(equalTo: containerView.safeAreaLayoutGuide.trailingAnchor, + constant: -Constants.margin.right), + toastView.bottomAnchor.constraint(lessThanOrEqualTo: containerView.safeAreaLayoutGuide.bottomAnchor, + constant: -Constants.margin.bottom) + ]) + case .middle: + NSLayoutConstraint.activate([ + toastView.leadingAnchor.constraint(equalTo: containerView.safeAreaLayoutGuide.leadingAnchor, + constant: Constants.margin.left), + toastView.topAnchor.constraint(greaterThanOrEqualTo: containerView.safeAreaLayoutGuide.topAnchor, + constant: Constants.margin.top), + toastView.trailingAnchor.constraint(equalTo: containerView.safeAreaLayoutGuide.trailingAnchor, + constant: -Constants.margin.right), + toastView.bottomAnchor.constraint(lessThanOrEqualTo: containerView.safeAreaLayoutGuide.bottomAnchor, + constant: -Constants.margin.bottom), + toastView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor) + ]) + case .bottom: + NSLayoutConstraint.activate([ + toastView.leadingAnchor.constraint(equalTo: containerView.safeAreaLayoutGuide.leadingAnchor, + constant: Constants.margin.left), + toastView.topAnchor.constraint(greaterThanOrEqualTo: containerView.safeAreaLayoutGuide.topAnchor, + constant: Constants.margin.top), + toastView.trailingAnchor.constraint(equalTo: containerView.safeAreaLayoutGuide.trailingAnchor, + constant: -Constants.margin.right), + toastView.bottomAnchor.constraint(equalTo: containerView.safeAreaLayoutGuide.bottomAnchor, + constant: -Constants.margin.bottom - additionalMargin) + ]) + } + + UIView.animate(withDuration: Constants.animationDuration, + delay: 0.0, + options: [.curveEaseOut, .allowUserInteraction], + animations: { + self.toastView.alpha = 1.0 + }, completion: { _ in + completion() + }) + } + + private func hideToast() { + UIView.animate(withDuration: Constants.animationDuration, + delay: 0.0, + options: [.curveEaseIn, .beginFromCurrentState], + animations: { + self.toastView.alpha = 0.0 + }, completion: { _ in + self.toastView.removeFromSuperview() + DispatchQueue.main.asyncAfter(deadline: .now() + Constants.timeBetweenToasts) { + self.finish() + self.completion?() + } + }) + } + + private func invalidateTimer() { + timer?.invalidate() + timer = nil + } + +} + +// MARK: - ToastView + +/// Default view for a basic toast. +private class ToastView: UIView, Themable { + + private enum Constants { + static let padding: UIEdgeInsets = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) + static let cornerRadius: CGFloat = 8.0 + } + + private lazy var imageView: UIImageView = { + let view = UIImageView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + return view + }() + + private lazy var messageLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 15) + label.backgroundColor = .clear + label.numberOfLines = 0 + label.textAlignment = .left + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var stackView: UIStackView = { + let result = UIStackView() + result.axis = .horizontal + result.distribution = .fill + result.alignment = .center + result.spacing = 8.0 + result.backgroundColor = .clear + + addSubview(result) + result.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + result.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.padding.left), + result.topAnchor.constraint(equalTo: topAnchor, constant: Constants.padding.top), + result.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.padding.right), + result.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.padding.bottom) + ]) + + return result + }() + + init(withMessage message: String?, + image: UIImage? = nil) { + super.init(frame: .zero) + + if let image = image { + imageView.image = image + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: image.size.width), + imageView.heightAnchor.constraint(equalToConstant: image.size.height) + ]) + stackView.addArrangedSubview(imageView) + } + + messageLabel.text = message + stackView.addArrangedSubview(messageLabel) + + stackView.layoutIfNeeded() + layer.cornerRadius = Constants.cornerRadius + layer.masksToBounds = true + registerThemeServiceDidChangeThemeNotification() + themeDidChange() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func registerThemeServiceDidChangeThemeNotification() { + NotificationCenter.default.addObserver(self, + selector: #selector(themeDidChange), + name: .themeServiceDidChangeTheme, + object: nil) + } + + @objc + private func themeDidChange() { + self.update(theme: ThemeService.shared().theme) + } + + // MARK: Themable + + func update(theme: Theme) { + backgroundColor = theme.colors.quinaryContent + imageView.tintColor = theme.colors.tertiaryContent + messageLabel.textColor = theme.colors.primaryContent + } + +} diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index be9b2a5cd..1486c6796 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") @@ -131,8 +133,8 @@ internal enum Asset { internal static let roomContextMenuEdit = ImageAsset(name: "room_context_menu_edit") internal static let roomContextMenuMore = ImageAsset(name: "room_context_menu_more") internal static let roomContextMenuReply = ImageAsset(name: "room_context_menu_reply") - internal static let roomContextMenuReplyInThread = ImageAsset(name: "room_context_menu_reply_in_thread") internal static let roomContextMenuRetry = ImageAsset(name: "room_context_menu_retry") + internal static let roomContextMenuThread = ImageAsset(name: "room_context_menu_thread") internal static let inputCloseIcon = ImageAsset(name: "input_close_icon") internal static let inputEditIcon = ImageAsset(name: "input_edit_icon") internal static let inputReplyIcon = ImageAsset(name: "input_reply_icon") @@ -152,6 +154,8 @@ internal enum Asset { internal static let pollEndIcon = ImageAsset(name: "poll_end_icon") internal static let pollWinnerIcon = ImageAsset(name: "poll_winner_icon") internal static let threadsFilter = ImageAsset(name: "threads_filter") + internal static let threadsFilterApplied = ImageAsset(name: "threads_filter_applied") + internal static let threadsIcon = ImageAsset(name: "threads_icon") internal static let urlPreviewClose = ImageAsset(name: "url_preview_close") internal static let urlPreviewCloseDark = ImageAsset(name: "url_preview_close_dark") internal static let voiceMessageCancelGradient = ImageAsset(name: "voice_message_cancel_gradient") @@ -169,6 +173,7 @@ internal enum Asset { internal static let detailsIcon = ImageAsset(name: "details_icon") internal static let editIcon = ImageAsset(name: "edit_icon") internal static let integrationsIcon = ImageAsset(name: "integrations_icon") + internal static let linkIcon = ImageAsset(name: "link_icon") internal static let mainAliasIcon = ImageAsset(name: "main_alias_icon") internal static let membersListIcon = ImageAsset(name: "members_list_icon") internal static let modIcon = ImageAsset(name: "mod_icon") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 86745c6c1..b1d4df96c 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") @@ -31,6 +35,58 @@ public class VectorL10n: NSObject { public static func activeCallDetails(_ p1: String) -> String { return VectorL10n.tr("Vector", "active_call_details", p1) } + /// Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices. + public static var analyticsPromptMessageNewUser: String { + return VectorL10n.tr("Vector", "analytics_prompt_message_new_user") + } + /// You previously consented to share anonymous usage data with us. Now, to help understand how people use multiple devices, we’ll generate a random identifier, shared by your devices. + public static var analyticsPromptMessageUpgrade: String { + 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 { + return VectorL10n.tr("Vector", "analytics_prompt_point_1") + } + /// We don't share information with third parties + public static var analyticsPromptPoint2: String { + return VectorL10n.tr("Vector", "analytics_prompt_point_2") + } + /// 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") + } + /// 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 %@. + public static func analyticsPromptTermsNewUser(_ p1: String) -> String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_new_user", p1) + } + /// 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 { + 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") @@ -1235,6 +1291,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") @@ -1443,10 +1503,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") @@ -3091,6 +3147,10 @@ public class VectorL10n: NSObject { public static var roomEventActionViewSource: String { return VectorL10n.tr("Vector", "room_event_action_view_source") } + /// Link copied to clipboard. + public static var roomEventCopyLinkInfo: String { + return VectorL10n.tr("Vector", "room_event_copy_link_info") + } /// Failed to send public static var roomEventFailedToSend: String { return VectorL10n.tr("Vector", "room_event_failed_to_send") @@ -4251,6 +4311,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") @@ -4783,10 +4847,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") @@ -5171,7 +5231,7 @@ public class VectorL10n: NSObject { public static var threadsEmptyInfoAll: String { return VectorL10n.tr("Vector", "threads_empty_info_all") } - /// Reply to an ongoing thread or use “Thread” when selecting a message to start a new one. + /// Reply to an ongoing thread or tap a message and use “Thread” to start a new one. public static var threadsEmptyInfoMy: String { return VectorL10n.tr("Vector", "threads_empty_info_my") } @@ -5179,7 +5239,7 @@ public class VectorL10n: NSObject { public static var threadsEmptyShowAllThreads: String { return VectorL10n.tr("Vector", "threads_empty_show_all_threads") } - /// Tip: Use “Thread” option when selecting a message. + /// Tip: Tap a message and use “Thread” to start one. public static var threadsEmptyTip: String { return VectorL10n.tr("Vector", "threads_empty_tip") } 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/DecryptionFailure.h b/Riot/Managers/Analytics/DecryptionFailure.h deleted file mode 100644 index 7ea43962c..000000000 --- a/Riot/Managers/Analytics/DecryptionFailure.h +++ /dev/null @@ -1,51 +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. - */ -struct DecryptionFailureReasonStruct -{ - __unsafe_unretained NSString * const unspecified; - __unsafe_unretained NSString * const olmKeysNotSent; - __unsafe_unretained NSString * const olmIndexError; - __unsafe_unretained NSString * const unexpected; -}; -extern const struct DecryptionFailureReasonStruct DecryptionFailureReason; - -/** - `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) NSString *reason; - -@end diff --git a/Riot/Managers/Analytics/DecryptionFailure.m b/Riot/Managers/Analytics/DecryptionFailure.m deleted file mode 100644 index d43b0ec9f..000000000 --- a/Riot/Managers/Analytics/DecryptionFailure.m +++ /dev/null @@ -1,38 +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" - -const struct DecryptionFailureReasonStruct DecryptionFailureReason = { - .unspecified = @"unspecified_error", - .olmKeysNotSent = @"olm_keys_not_sent_error", - .olmIndexError = @"olm_index_error", - .unexpected = @"unexpected_error" -}; - -@implementation DecryptionFailure - -- (instancetype)init -{ - self = [super init]; - if (self) - { - _ts = [NSDate date].timeIntervalSince1970; - } - return self; -} - -@end diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 2a55090ec..2175efd5e 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,31 @@ 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 was previously shown the Matomo analytics prompt. + var hasSeenAnalyticsPrompt: Bool { + RiotSettings.defaults.object(forKey: UserDefaultsKeys.enableAnalytics) != nil } - @UserDefault(key: UserDefaultsKeys.enableCrashReport, defaultValue: false, storage: defaults) - var enableCrashReport + /// Whether the user has both seen the Matomo analytics prompt and declined it. + var hasDeclinedMatomoAnalytics: Bool { + RiotSettings.defaults.object(forKey: UserDefaultsKeys.matomoAnalytics) != nil && !RiotSettings.defaults.bool(forKey: UserDefaultsKeys.matomoAnalytics) + } + + /// 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) + } + + /// `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 @UserDefault(key: "enableRageShake", defaultValue: false, storage: defaults) var enableRageShake diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift new file mode 100644 index 000000000..535ca7b2a --- /dev/null +++ b/Riot/Modules/Analytics/Analytics.swift @@ -0,0 +1,259 @@ +// +// 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 + +/// 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: 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 } + + /// Whether to show the user the analytics opt in prompt. + var shouldShowAnalyticsPrompt: Bool { + // 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. + var promptShouldDisplayUpgradeMessage: Bool { + RiotSettings.shared.hasAcceptedMatomoAnalytics + } + + // MARK: - Public + + /// Opts in to analytics tracking with the supplied session. + /// - 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?) { + RiotSettings.shared.enableAnalytics = true + startIfEnabled() + + guard let session = session else { return } + useAnalyticsSettings(from: session) + } + + /// 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. + func startIfEnabled() { + guard RiotSettings.shared.enableAnalytics, !isRunning else { return } + + client.start() + + // Sanity check in case something went wrong. + guard client.isRunning else { return } + + MXLog.debug("[Analytics] Started.") + + // Catch and log crashes + MXLogger.logCrashes(true) + MXLogger.setBuildVersion(AppDelegate.theDelegate().build) + } + + /// 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 + else { return } + + let service = AnalyticsService(session: session) + 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 + } + } + } + + /// 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] Reset.") + RiotSettings.shared.isIdentifiedForAnalytics = false + + // Stop collecting crash logs + 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.error("[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) + } + + /// 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: 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. + /// - count: The number of times that error occurred. + func trackE2EEError(_ reason: DecryptionFailureReason, count: Int) { + for _ in 0..) -> 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) { [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)) + } failure: { error in + MXLog.warning("[AnalyticsService] Failed to update analytics settings.") + completion(.failure(error ?? AnalyticsServiceError.unknown)) + } + } +} diff --git a/Riot/Modules/Analytics/AnalyticsSettings.swift b/Riot/Modules/Analytics/AnalyticsSettings.swift new file mode 100644 index 000000000..e847f0668 --- /dev/null +++ b/Riot/Modules/Analytics/AnalyticsSettings.swift @@ -0,0 +1,65 @@ +// +// 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 + +/// An analytics settings event from the user's account data. +struct AnalyticsSettings { + static let eventType = "im.vector.analytics" + + enum Constants { + static let idKey = "id" + static let webOptInKey = "pseudonymousAnalyticsOptIn" + } + + /// A randomly generated analytics token for this user. + /// 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 + /// 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 let webOptIn: Bool? +} + +extension AnalyticsSettings { + // 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 + } + + /// A dictionary representation of the settings. + var dictionary: Dictionary { + var dictionary = [AnyHashable: Any]() + dictionary[Constants.idKey] = id + dictionary[Constants.webOptInKey] = webOptIn + + return dictionary + } +} + +// MARK: - Public initializer + +extension AnalyticsSettings { + /// 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)) + } +} diff --git a/Riot/Modules/Analytics/AnalyticsUIElement.swift b/Riot/Modules/Analytics/AnalyticsUIElement.swift new file mode 100644 index 000000000..93a08e7e2 --- /dev/null +++ b/Riot/Modules/Analytics/AnalyticsUIElement.swift @@ -0,0 +1,32 @@ +// +// 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 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/Modules/Analytics/DecryptionFailure.swift b/Riot/Modules/Analytics/DecryptionFailure.swift new file mode 100644 index 000000000..d011a0413 --- /dev/null +++ b/Riot/Modules/Analytics/DecryptionFailure.swift @@ -0,0 +1,53 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AnalyticsEvents + +/// 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 + + 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. +@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/Modules/Analytics/DecryptionFailureTracker.h similarity index 92% rename from Riot/Managers/Analytics/DecryptionFailureTracker.h rename to Riot/Modules/Analytics/DecryptionFailureTracker.h index b2dbbfc77..b8f9ca467 100644 --- a/Riot/Managers/Analytics/DecryptionFailureTracker.h +++ b/Riot/Modules/Analytics/DecryptionFailureTracker.h @@ -16,8 +16,9 @@ #import -#import "DecryptionFailure.h" +@class DecryptionFailureTracker; +@class Analytics; @import MatrixSDK; @interface DecryptionFailureTracker : NSObject @@ -32,7 +33,7 @@ /** The delegate object to receive analytics events. */ -@property (nonatomic, weak) id delegate; +@property (nonatomic, weak) Analytics *delegate; /** Report an event unable to decrypt. diff --git a/Riot/Managers/Analytics/DecryptionFailureTracker.m b/Riot/Modules/Analytics/DecryptionFailureTracker.m similarity index 83% rename from Riot/Managers/Analytics/DecryptionFailureTracker.m rename to Riot/Modules/Analytics/DecryptionFailureTracker.m index 56521eb58..0f2b2ff81 100644 --- a/Riot/Managers/Analytics/DecryptionFailureTracker.m +++ b/Riot/Modules/Analytics/DecryptionFailureTracker.m @@ -15,6 +15,7 @@ */ #import "DecryptionFailureTracker.h" +#import "GeneratedInterface-Swift.h" // Call `checkFailures` every `CHECK_INTERVAL` @@ -90,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 = DecryptionFailureReason.olmKeysNotSent; + reason = DecryptionFailureReasonOlmKeysNotSent; break; case MXDecryptingErrorOlmCode: - decryptionFailure.reason = DecryptionFailureReason.olmIndexError; + reason = DecryptionFailureReasonOlmIndexError; break; case MXDecryptingErrorEncryptionNotEnabledCode: case MXDecryptingErrorUnableToDecryptCode: - decryptionFailure.reason = DecryptionFailureReason.unexpected; + reason = DecryptionFailureReasonUnexpected; break; default: - decryptionFailure.reason = DecryptionFailureReason.unspecified; + reason = DecryptionFailureReasonUnspecified; break; } - reportedFailures[event.eventId] = decryptionFailure; + reportedFailures[event.eventId] = [[DecryptionFailure alloc] initWithFailedEventId:failedEventId + reason:reason]; } - (void)dispatch @@ -152,17 +154,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/Modules/MatrixKit/Utils/MXKAnalyticsConstants.h b/Riot/Modules/Analytics/Helpers/JoinedRoomSize+MemberCount.swift similarity index 51% rename from Riot/Modules/MatrixKit/Utils/MXKAnalyticsConstants.h rename to Riot/Modules/Analytics/Helpers/JoinedRoomSize+MemberCount.swift index 97d3c25f2..d5473d5f9 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKAnalyticsConstants.h +++ b/Riot/Modules/Analytics/Helpers/JoinedRoomSize+MemberCount.swift @@ -1,5 +1,5 @@ // -// Copyright 2020 The Matrix.org Foundation C.I.C +// 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. @@ -14,20 +14,23 @@ // limitations under the License. // -#import +import AnalyticsEvents - -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"; +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/Analytics/Helpers/MXCallHangupReason+Analytics.swift b/Riot/Modules/Analytics/Helpers/MXCallHangupReason+Analytics.swift new file mode 100644 index 000000000..4b8911ce8 --- /dev/null +++ b/Riot/Modules/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/Modules/Analytics/Helpers/MXTaskProfileName+Analytics.swift b/Riot/Modules/Analytics/Helpers/MXTaskProfileName+Analytics.swift new file mode 100644 index 000000000..99f89174e --- /dev/null +++ b/Riot/Modules/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/Analytics/PHGPostHogConfiguration.swift b/Riot/Modules/Analytics/PHGPostHogConfiguration.swift new file mode 100644 index 000000000..c02b85c30 --- /dev/null +++ b/Riot/Modules/Analytics/PHGPostHogConfiguration.swift @@ -0,0 +1,28 @@ +// +// 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 + +extension PHGPostHogConfiguration { + 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 + } +} diff --git a/Riot/Modules/Analytics/PostHogAnalyticsClient.swift b/Riot/Modules/Analytics/PostHogAnalyticsClient.swift new file mode 100644 index 000000000..1c7172112 --- /dev/null +++ b/Riot/Modules/Analytics/PostHogAnalyticsClient.swift @@ -0,0 +1,65 @@ +// +// 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?.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 + } + + 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) + } + +} 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 a263fc25d..dade2a505 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -433,16 +433,16 @@ 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 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]; + [analytics startIfEnabled]; self.localAuthenticationService = [[LocalAuthenticationService alloc] initWithPinCodePreferences:[PinCodePreferences shared]]; @@ -587,7 +587,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,9 +648,13 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni MXLogDebug(@"[AppDelegate] afterAppUnlockedByPin"); // Check if there is crash log to send - if (RiotSettings.shared.enableCrashReport) + 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. @@ -1933,6 +1937,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]; @@ -2278,6 +2287,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) @@ -2443,8 +2455,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni launchAnimationContainerView = launchLoadingView; - [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:kMXAnalyticsStartupLaunchScreen - category:kMXAnalyticsStartupCategory]; + [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:MXTaskProfileNameStartupLaunchScreen]; } } @@ -2453,7 +2464,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if (launchAnimationContainerView) { id profiler = MXSDKOptions.sharedInstance.profiler; - MXTaskProfile *launchTaskProfile = [profiler taskProfileWithName:kMXAnalyticsStartupLaunchScreen category:kMXAnalyticsStartupCategory]; + MXTaskProfile *launchTaskProfile = [profiler taskProfileWithName:MXTaskProfileNameStartupLaunchScreen]; if (launchTaskProfile) { [profiler stopMeasuringTaskWithProfile:launchTaskProfile]; diff --git a/Riot/Modules/Authentication/AuthenticationViewController.m b/Riot/Modules/Authentication/AuthenticationViewController.m index 188e33268..67b2a9e8b 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/AuthenticationViewController.m @@ -309,9 +309,6 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"Authentication"]; [_keyboardAvoider startAvoiding]; } @@ -330,7 +327,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.h b/Riot/Modules/Common/Recents/RecentsViewController.h index a9e9c4863..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. @@ -85,16 +86,16 @@ FOUNDATION_EXPORT NSString *const RecentsViewControllerDataReadyNotification; */ @property (nonatomic) CGFloat stickyHeaderHeight; -/** - The analytics instance screen name (Default is "RecentsScreen"). - */ -@property (nonatomic) NSString *screenName; - /** Empty view to display when there is no item to show on the screen. */ @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 085b8f6c8..789effc07 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -106,9 +106,6 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; - // Set default screen name - _screenName = @"RecentsScreen"; - // Enable the search bar in the recents table, and remove the search option from the navigation bar. _enableSearchBar = YES; self.enableBarButtonSearch = NO; @@ -259,9 +256,6 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:_screenName]; - // Reset back user interactions self.userInteractionEnabled = YES; @@ -329,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 91a881352..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 sharedInstance] trackScreen:@"Groups"]; - // 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 018c15eb9..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 sharedInstance] trackScreen:@"GroupDetailsHome"]; - // 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/Communities/Members/GroupParticipantsViewController.m b/Riot/Modules/Communities/Members/GroupParticipantsViewController.m index 7be31d255..9a87f6562 100644 --- a/Riot/Modules/Communities/Members/GroupParticipantsViewController.m +++ b/Riot/Modules/Communities/Members/GroupParticipantsViewController.m @@ -219,9 +219,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] 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..855c46580 100644 --- a/Riot/Modules/Communities/Rooms/GroupRoomsViewController.m +++ b/Riot/Modules/Communities/Rooms/GroupRoomsViewController.m @@ -183,9 +183,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] 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..339af8584 100644 --- a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m +++ b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m @@ -136,9 +136,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"GroupDetails"]; } - (void)viewWillDisappear:(BOOL)animated diff --git a/Riot/Modules/Contacts/ContactsTableViewController.h b/Riot/Modules/Contacts/ContactsTableViewController.h index f1b3effaf..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. @@ -85,11 +86,6 @@ */ @property (nonatomic) BOOL shouldScrollToTopOnRefresh; -/** - The analytics instance screen name (Default is "ContactsTable"). - */ -@property (nonatomic) NSString *screenName; - /** Callback used to take into account the change of the user interface theme. */ @@ -124,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 1a8bec148..bf83b6372 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -76,8 +76,6 @@ // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; - - _screenName = @"ContactsTable"; } - (void)viewDidLoad @@ -159,9 +157,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:_screenName]; MXWeakify(self); @@ -182,6 +177,12 @@ [self updateFooterViewVisibility]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; @@ -206,6 +207,12 @@ } } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + #pragma mark - /** diff --git a/Riot/Modules/Contacts/Details/ContactDetailsViewController.m b/Riot/Modules/Contacts/Details/ContactDetailsViewController.m index 3d332dbaa..be2fe5596 100644 --- a/Riot/Modules/Contacts/Details/ContactDetailsViewController.m +++ b/Riot/Modules/Contacts/Details/ContactDetailsViewController.m @@ -232,9 +232,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"ContactDetails"]; // Hide the bottom border of the navigation bar to display the expander header [self hideNavigationBarBorder:YES]; 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 12f129d68..c3c10933b 100644 --- a/Riot/Modules/Favorites/FavouritesViewController.m +++ b/Riot/Modules/Favorites/FavouritesViewController.m @@ -39,9 +39,9 @@ { [super finalizeInit]; - self.screenName = @"Favourites"; - 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 8a77c3200..535d48d93 100644 --- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m @@ -109,9 +109,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"FilesGlobalSearch"]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionDidLeaveRoomNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionNewRoomNotification object:nil]; } @@ -124,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 e001228b7..b0dd6b913 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m @@ -115,9 +115,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"MessagesGlobalSearch"]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionDidLeaveRoomNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionNewRoomNotification object:nil]; @@ -131,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 e01bd3d2b..beeb90385 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 sharedInstance] trackScreen:@"Directory"]; // 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 bd0a07b3f..8c1956330 100644 --- a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m +++ b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m @@ -79,12 +79,13 @@ [titles addObject:[VectorL10n searchRooms]]; recentsViewController = [RecentsViewController recentListViewController]; + recentsViewController.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSearchRooms]; recentsViewController.enableSearchBar = NO; - recentsViewController.screenName = @"UnifiedSearchRooms"; [viewControllers addObject:recentsViewController]; [titles addObject:[VectorL10n searchMessages]]; messagesSearchViewController = [HomeMessagesSearchViewController searchViewController]; + messagesSearchViewController.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSearchMessages]; [viewControllers addObject:messagesSearchViewController]; // Add search People tab @@ -92,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]; @@ -144,9 +147,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"UnifiedSearch"]; - // Let's child display the loading not the home view controller if (self.activityIndicator) { diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index 0f7f5234b..2c2f702d7 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -69,7 +69,7 @@ selectedRoomId = nil; selectedCollectionViewContentOffset = -1; - self.screenName = @"Home"; + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenHome]; } - (void)viewDidLoad diff --git a/Riot/Modules/MatrixKit/Utils/MXKTools.m b/Riot/Modules/MatrixKit/Utils/MXKTools.m index 76753eed5..c941fbc98 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]; + [MXSDKOptions.sharedInstance.analyticsDelegate trackContactsAccessGranted:granted]; dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/Riot/Modules/MediaPicker/Library/MediaAlbumContentViewController.m b/Riot/Modules/MediaPicker/Library/MediaAlbumContentViewController.m index b87015e59..c01c845e6 100644 --- a/Riot/Modules/MediaPicker/Library/MediaAlbumContentViewController.m +++ b/Riot/Modules/MediaPicker/Library/MediaAlbumContentViewController.m @@ -163,9 +163,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"MediaAlbumContent"]; self.navigationItem.title = _assetsCollection.localizedTitle; diff --git a/Riot/Modules/MediaPicker/MediaPickerViewController.m b/Riot/Modules/MediaPicker/MediaPickerViewController.m index 92261789c..190af4d17 100644 --- a/Riot/Modules/MediaPicker/MediaPickerViewController.m +++ b/Riot/Modules/MediaPicker/MediaPickerViewController.m @@ -212,9 +212,6 @@ [super viewWillAppear:animated]; [self userInterfaceThemeDidChange]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"MediaPicker"]; if (!userAlbumsQueue) { 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 1cb481a90..ace78de54 100644 --- a/Riot/Modules/People/PeopleViewController.m +++ b/Riot/Modules/People/PeopleViewController.m @@ -51,7 +51,7 @@ directRoomsSectionNumber = 0; - self.screenName = @"People"; + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenPeople]; } - (void)viewDidLoad diff --git a/Riot/Modules/Room/Attachements/AttachmentsViewController.m b/Riot/Modules/Room/Attachements/AttachmentsViewController.m index 596c6a3b5..31bb29519 100644 --- a/Riot/Modules/Room/Attachements/AttachmentsViewController.m +++ b/Riot/Modules/Room/Attachements/AttachmentsViewController.m @@ -73,14 +73,6 @@ return ThemeService.shared.theme.statusBarStyle; } -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"AttachmentsViewer"]; -} - - (void)destroy { [super destroy]; diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuAction.swift b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuAction.swift index d138df3cf..87089ecca 100644 --- a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuAction.swift +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuAction.swift @@ -59,7 +59,7 @@ import Foundation case .reply: image = Asset.Images.roomContextMenuReply.image case .replyInThread: - image = Asset.Images.roomContextMenuReplyInThread.image + image = Asset.Images.roomContextMenuThread.image case .edit: image = Asset.Images.roomContextMenuEdit.image case .more: diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 154c41544..72e1bb2c4 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -564,7 +564,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] ]]; } diff --git a/Riot/Modules/Room/EventMenu/EventMenuBuilder.swift b/Riot/Modules/Room/EventMenu/EventMenuBuilder.swift new file mode 100644 index 000000000..a8d3caf66 --- /dev/null +++ b/Riot/Modules/Room/EventMenu/EventMenuBuilder.swift @@ -0,0 +1,50 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Class to build an event menu. +@objcMembers +class EventMenuBuilder: NSObject { + + private var items: [EventMenuItemType: UIAlertAction] = [:] + + /// Returns true if no items or only one item with the type `EventMenuItemType.cancel`. + var isEmpty: Bool { + return items.isEmpty || (items.count == 1 && items.first?.key == .cancel) + } + + /// Add a menu item. + /// - Parameters: + /// - type: item type + /// - action: alert action + func addItem(withType type: EventMenuItemType, + action: UIAlertAction) { + items[type] = action + } + + /// Builds the action menu items. + /// - Returns: alert actions. Sorted by item types. + func build() -> [UIAlertAction] { + items.sorted(by: { $0.key < $1.key }).map { $1 } + } + + /// Reset the builder. Builder will be empty after this method call. + func reset() { + items.removeAll() + } + +} diff --git a/Riot/Modules/Room/EventMenu/EventMenuItemType.swift b/Riot/Modules/Room/EventMenu/EventMenuItemType.swift new file mode 100644 index 000000000..c15d2fbdb --- /dev/null +++ b/Riot/Modules/Room/EventMenu/EventMenuItemType.swift @@ -0,0 +1,49 @@ +// +// 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 + +/// Type of an event menu item. Ordering of the cases is important. See `EventMenuBuilder`. +@objc +enum EventMenuItemType: Int { + case viewInRoom + case copy + case retrySending + case cancelSending + case cancelDownloading + case saveMedia + case quote + case forward + case permalink + case share + case removePoll + case endPoll + case reactionHistory + case viewSource + case viewDecryptedSource + case viewEncryption + case report + case remove + case cancel +} + +extension EventMenuItemType: Comparable { + + static func < (lhs: EventMenuItemType, rhs: EventMenuItemType) -> Bool { + return lhs.rawValue < rhs.rawValue + } + +} 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 d1888c6d7..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 sharedInstance] trackScreen:@"RoomMemberDetails"]; - [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 2d7e3426a..0c8cee07f 100644 --- a/Riot/Modules/Room/Members/RoomParticipantsViewController.m +++ b/Riot/Modules/Room/Members/RoomParticipantsViewController.m @@ -245,9 +245,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"RoomParticipants"]; // Refresh display [self refreshTableView]; @@ -268,6 +265,8 @@ [contactsPickerViewController destroy]; contactsPickerViewController = nil; } + + [self.screenTimer start]; } - (void)viewWillDisappear:(BOOL)animated @@ -284,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 622b21da2..817ef52b5 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -257,11 +257,13 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter; @property (nonatomic, strong) ShareManager *shareManager; +@property (nonatomic, strong) EventMenuBuilder *eventMenuBuilder; @property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator; @property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView; @property (nonatomic, readwrite) RoomDisplayConfiguration *displayConfiguration; +@property (nonatomic) AnalyticsScreenTimer *screenTimer; @end @@ -347,6 +349,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; formattedBodyParser = [FormattedBodyParser new]; + self.eventMenuBuilder = [EventMenuBuilder new]; _showMissedDiscussionsBadge = YES; _scrollToBottomHidden = YES; @@ -360,6 +363,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; _voiceMessageController = [[VoiceMessageController alloc] initWithThemeService:ThemeService.shared mediaServiceProvider:VoiceMessageMediaServiceProvider.sharedProvider]; self.voiceMessageController.delegate = self; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenRoom]; } - (void)viewDidLoad @@ -592,9 +597,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"ChatRoom"]; - // Refresh the room title view [self refreshRoomTitle]; @@ -635,8 +637,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self.roomDataSource reload]; [LegacyAppDelegate theDelegate].lastNavigatedRoomIdFromPush = nil; - notificationTaskProfile = [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:AnalyticsNoficationsTimeToDisplayContent - category:AnalyticsNoficationsCategory]; + notificationTaskProfile = [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:MXTaskProfileNameNotificationsOpenEvent]; } } @@ -733,6 +734,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; hasJitsiCall = NO; [self reloadBubblesTable:YES]; } + + // Screen tracking + [self.screenTimer start]; } - (void)viewDidDisappear:(BOOL)animated @@ -768,6 +772,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; hasJitsiCall = YES; [self reloadBubblesTable:YES]; } + + [self.screenTimer stop]; } - (void)viewDidLayoutSubviews @@ -1554,8 +1560,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (BadgedBarButtonItem *)threadListBarButtonItem { UIButton *button = [UIButton new]; + UIImage *icon = [[UIImage imageNamed:@"threads_icon"] vc_resizedWith:CGSizeMake(24, 24)]; button.contentEdgeInsets = UIEdgeInsetsMake(4, 8, 4, 8); - [button setImage:[UIImage imageNamed:@"room_context_menu_reply_in_thread"] + [button setImage:icon forState:UIControlStateNormal]; [button addTarget:self action:@selector(onThreadListTapped:) @@ -3272,17 +3279,19 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; currentAlert = nil; } + [self.eventMenuBuilder reset]; + MXWeakify(self); - UIAlertController *actionsMenu = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; BOOL showThreadOption = RiotSettings.shared.enableThreads - && !self.roomDataSource.threadId - && !selectedEvent.threadId; + && !self.roomDataSource.threadId + && !selectedEvent.threadId; if (showThreadOption && [self canCopyEvent:selectedEvent andCell:cell]) { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCopy] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeCopy + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCopy] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self cancelEventSelection]; @@ -3294,9 +3303,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Add actions for a failed event if (selectedEvent.sentState == MXEventSentStateFailed) { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n retry] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeRetrySending + action:[UIAlertAction actionWithTitle:[VectorL10n retry] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self cancelEventSelection]; @@ -3305,9 +3315,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self.roomDataSource resendEventWithEventId:selectedEvent.eventId success:nil failure:nil]; }]]; - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionDelete] - style:UIAlertActionStyleDestructive - handler:^(UIAlertAction * action) { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeRemove + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionDelete] + style:UIAlertActionStyleDestructive + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self cancelEventSelection]; @@ -3316,6 +3327,22 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } + // View in room action + if (self.roomDataSource.threadId && [selectedEvent.eventId isEqualToString:self.roomDataSource.threadId]) + { + // if in the thread and selected event is the root event + // add "View in room" action + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewInRoom + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewInRoom] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + [self.delegate roomViewController:self + showRoomWithId:self.roomDataSource.roomId + eventId:selectedEvent.eventId]; + }]]; + } + // Add actions for text message if (!attachment) { @@ -3337,9 +3364,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; selectedEvent.sentState == MXEventSentStateEncrypting || selectedEvent.sentState == MXEventSentStateSending) { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancelSending + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); self->currentAlert = nil; @@ -3352,35 +3380,12 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } - if (self.roomDataSource.threadId && [selectedEvent.eventId isEqualToString:self.roomDataSource.threadId]) - { - // if in the thread and selected event is the root event - // add "View in room" action - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewInRoom] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - MXStrongifyAndReturnIfNil(self); - [self.delegate roomViewController:self - showRoomWithId:self.roomDataSource.roomId - eventId:selectedEvent.eventId]; - }]]; - } - - if (selectedEvent.sentState == MXEventSentStateSent && selectedEvent.eventType != MXEventTypePollStart) - { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - MXStrongifyAndReturnIfNil(self); - [self presentEventForwardingDialogForSelectedEvent:selectedEvent]; - }]]; - } - if (!isJitsiCallEvent && selectedEvent.eventType != MXEventTypePollStart) { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionQuote] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeQuote + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionQuote] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self cancelEventSelection]; @@ -3393,11 +3398,23 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } + if (selectedEvent.sentState == MXEventSentStateSent && selectedEvent.eventType != MXEventTypePollStart) + { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeForward + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + [self presentEventForwardingDialogForSelectedEvent:selectedEvent]; + }]]; + } + if (!isJitsiCallEvent && BuildSettings.messageDetailsAllowShare && selectedEvent.eventType != MXEventTypePollStart) { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeShare + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self cancelEventSelection]; @@ -3438,9 +3455,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; attachment.type == MXKAttachmentTypeImage || attachment.type == MXKAttachmentTypeVideo || attachment.type == MXKAttachmentTypeVoiceMessage)) { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeForward + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self presentEventForwardingDialogForSelectedEvent:selectedEvent]; }]]; @@ -3450,9 +3468,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { if (attachment.type == MXKAttachmentTypeImage || attachment.type == MXKAttachmentTypeVideo) { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionSave] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeSaveMedia + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionSave] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self cancelEventSelection]; @@ -3487,9 +3506,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; NSString *uploadId = roomBubbleTableViewCell.bubbleData.attachment.contentURL; if ([MXMediaManager existingUploaderWithId:uploadId]) { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancelSending + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelSend] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); @@ -3521,9 +3541,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { if (BuildSettings.messageDetailsAllowShare) { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeShare + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionShare] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self cancelEventSelection]; @@ -3568,9 +3589,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; NSString *downloadId = roomBubbleTableViewCell.bubbleData.attachment.downloadId; if ([MXMediaManager existingDownloaderWithIdentifier:downloadId]) { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelDownload] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancelDownloading + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCancelDownload] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self cancelEventSelection]; @@ -3586,24 +3608,92 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } } - + + if (BuildSettings.messageDetailsAllowPermalink) + { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypePermalink + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionPermalink] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + [self cancelEventSelection]; + + // Create a matrix.to permalink that is common to all matrix clients + NSString *permalink = [MXTools permalinkToEvent:selectedEvent.eventId inRoom:selectedEvent.roomId]; + + if (permalink) + { + MXKPasteboardManager.shared.pasteboard.string = permalink; + [self.view vc_toastWithMessage:VectorL10n.roomEventCopyLinkInfo + image:[UIImage imageNamed:@"link_icon"] + duration:2.0 + position:ToastPositionBottom + additionalMargin:self.roomInputToolbarContainerHeightConstraint.constant]; + } + else + { + MXLogDebug(@"[RoomViewController] Contextual menu permalink action failed. Permalink is nil room id/event id: %@/%@", selectedEvent.roomId, selectedEvent.eventId); + } + }]]; + } + + if (BuildSettings.messageDetailsAllowViewSource) + { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewSource + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewSource] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + [self cancelEventSelection]; + + // Display event details + [self showEventDetails:selectedEvent]; + }]]; + + + // Add "View Decrypted Source" for e2ee event we can decrypt + if (selectedEvent.isEncrypted && selectedEvent.clearEvent) + { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewDecryptedSource + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewDecryptedSource] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + [self cancelEventSelection]; + + // Display clear event details + [self showEventDetails:selectedEvent.clearEvent]; + }]]; + } + } + // Do not allow to redact the event that enabled encryption (m.room.encryption) // because it breaks everything if (selectedEvent.eventType != MXEventTypeRoomEncryption) { NSString *title; + UIAlertActionStyle style; + EventMenuItemType itemType; if (selectedEvent.eventType == MXEventTypePollStart) { title = [VectorL10n roomEventActionRemovePoll]; + style = UIAlertActionStyleDefault; + itemType = EventMenuItemTypeRemovePoll; } else { title = [VectorL10n roomEventActionRedact]; + style = UIAlertActionStyleDestructive; + itemType = EventMenuItemTypeRemove; } - [actionsMenu addAction:[UIAlertAction actionWithTitle:title - style:UIAlertActionStyleDestructive - handler:^(UIAlertAction * action) { + [self.eventMenuBuilder addItemWithType:itemType + action:[UIAlertAction actionWithTitle:title + style:style + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self cancelEventSelection]; @@ -3625,11 +3715,14 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } - if (selectedEvent.eventType == MXEventTypePollStart && [selectedEvent.sender isEqualToString:self.mainSession.myUserId]) { - if ([self.delegate roomViewController:self canEndPollWithEventIdentifier:selectedEvent.eventId]) { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionEndPoll] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + if (selectedEvent.eventType == MXEventTypePollStart && [selectedEvent.sender isEqualToString:self.mainSession.myUserId]) + { + if ([self.delegate roomViewController:self canEndPollWithEventIdentifier:selectedEvent.eventId]) + { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeEndPoll + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionEndPoll] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self.delegate roomViewController:self endPollWithEventIdentifier:selectedEvent.eventId]; @@ -3639,43 +3732,13 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } } - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] - style:UIAlertActionStyleCancel - handler:^(UIAlertAction * action) { - MXStrongifyAndReturnIfNil(self); - - [self hideContextualMenuAnimated:YES]; - }]]; - - if (BuildSettings.messageDetailsAllowPermalink) - { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionPermalink] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - MXStrongifyAndReturnIfNil(self); - - [self cancelEventSelection]; - - // Create a matrix.to permalink that is common to all matrix clients - NSString *permalink = [MXTools permalinkToEvent:selectedEvent.eventId inRoom:selectedEvent.roomId]; - - if (permalink) - { - MXKPasteboardManager.shared.pasteboard.string = permalink; - } - else - { - MXLogDebug(@"[RoomViewController] Contextual menu permalink action failed. Permalink is nil room id/event id: %@/%@", selectedEvent.roomId, selectedEvent.eventId); - } - }]]; - } - // Add reaction history if event contains reactions if (roomBubbleTableViewCell.bubbleData.reactions[selectedEvent.eventId].aggregatedReactionsWithNonZeroCount) { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReactionHistory] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeReactionHistory + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReactionHistory] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self cancelEventSelection]; @@ -3685,41 +3748,12 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } - if (BuildSettings.messageDetailsAllowViewSource) - { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewSource] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - MXStrongifyAndReturnIfNil(self); - - [self cancelEventSelection]; - - // Display event details - [self showEventDetails:selectedEvent]; - }]]; - - - // Add "View Decrypted Source" for e2ee event we can decrypt - if (selectedEvent.isEncrypted && selectedEvent.clearEvent) - { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewDecryptedSource] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - MXStrongifyAndReturnIfNil(self); - - [self cancelEventSelection]; - - // Display clear event details - [self showEventDetails:selectedEvent.clearEvent]; - }]]; - } - } - if (![selectedEvent.sender isEqualToString:self.mainSession.myUserId] && RiotSettings.shared.roomContextualMenuShowReportContentOption) { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReport] - style:UIAlertActionStyleDestructive - handler:^(UIAlertAction * action) { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeReport + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReport] + style:UIAlertActionStyleDestructive + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self cancelEventSelection]; @@ -3809,9 +3843,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; if (!isJitsiCallEvent && self.roomDataSource.room.summary.isEncrypted) { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewEncryption] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeViewEncryption + action:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewEncryption] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self cancelEventSelection]; @@ -3821,11 +3856,29 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } + [self.eventMenuBuilder addItemWithType:EventMenuItemTypeCancel + action:[UIAlertAction actionWithTitle:[VectorL10n cancel] + style:UIAlertActionStyleCancel + handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + [self hideContextualMenuAnimated:YES]; + }]]; + } // Do not display empty action sheet - if (actionsMenu.actions.count > 1) + if (!self.eventMenuBuilder.isEmpty) { + UIAlertController *actionsMenu = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; + + // build actions and add them to the alert + NSArray *actions = [self.eventMenuBuilder build]; + for (UIAlertAction *action in actions) + { + [actionsMenu addAction:action]; + } + NSInteger bubbleComponentIndex = [roomBubbleTableViewCell.bubbleData bubbleComponentIndexForEventId:selectedEvent.eventId]; CGRect sourceRect = [roomBubbleTableViewCell componentFrameInContentViewForIndex:bubbleComponentIndex]; @@ -6074,10 +6127,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; NSMutableArray *items = [NSMutableArray arrayWithCapacity:5]; - if (!showThreadOption) - { - [items addObject:[self copyMenuItemWithEvent:event andCell:cell]]; - } [items addObject:[self replyMenuItemWithEvent:event]]; if (showThreadOption) { @@ -6085,6 +6134,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [items addObject:[self replyInThreadMenuItemWithEvent:event]]; } [items addObject:[self editMenuItemWithEvent:event]]; + if (!showThreadOption) + { + [items addObject:[self copyMenuItemWithEvent:event andCell:cell]]; + } if (showMoreOption) { [items addObject:[self moreMenuItemWithEvent:event andCell:cell]]; diff --git a/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m b/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m index a7d211c00..b0c6c589a 100644 --- a/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m +++ b/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m @@ -109,9 +109,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] 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 7a8c5f8cb..85a15e267 100644 --- a/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m +++ b/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m @@ -111,9 +111,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] 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 b15288c3a..90c6bee63 100644 --- a/Riot/Modules/Room/Search/RoomSearchViewController.m +++ b/Riot/Modules/Room/Search/RoomSearchViewController.m @@ -33,6 +33,8 @@ MXKSearchDataSource *filesSearchDataSource; } +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation RoomSearchViewController @@ -49,6 +51,8 @@ [super finalizeInit]; // The navigation bar tint color and the rageShake Manager are handled by super (see SegmentedViewController). + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenRoomSearch]; } - (void)viewDidLoad @@ -106,9 +110,6 @@ [self.activityIndicator stopAnimating]; self.activityIndicator = nil; } - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"RoomsSearch"]; // Enable the search field by default at the screen opening if (self.searchBarHidden) @@ -124,6 +125,8 @@ // Refresh the search results. // Note: We wait for 'viewDidAppear' call to consider the actual view size during this update. [self updateSearch]; + + [self.screenTimer start]; } - (void)viewWillDisappear:(BOOL)animated @@ -138,6 +141,12 @@ [super viewWillDisappear:animated]; } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + - (UIStatusBarStyle)preferredStatusBarStyle { return ThemeService.shared.theme.statusBarStyle; diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.h b/Riot/Modules/Room/Settings/RoomSettingsViewController.h index dd960e8a6..1882a38cb 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.h +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.h @@ -19,6 +19,8 @@ #import "MediaPickerViewController.h" #import "TableViewCellWithCheckBoxes.h" +@class AnalyticsScreenTimer; + /** List the settings fields. Used to preselect/edit a field */ @@ -52,5 +54,10 @@ typedef enum : NSUInteger { */ @property (nonatomic) RoomSettingsViewControllerField selectedRoomSettingsField; +/** + 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/Settings/RoomSettingsViewController.m b/Riot/Modules/Room/Settings/RoomSettingsViewController.m index 9980e50df..1f77beb9a 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m @@ -311,9 +311,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"RoomSettings"]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didUpdateRules:) name:kMXNotificationCenterDidUpdateRules object:nil]; @@ -334,6 +331,8 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti { self.selectedRoomSettingsField = _selectedRoomSettingsField; } + + [self.screenTimer start]; } - (void)viewWillDisappear:(BOOL)animated @@ -351,6 +350,12 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti } } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + // Those methods are called when the viewcontroller is added or removed from a container view controller. - (void)willMoveToParentViewController:(nullable UIViewController *)parent { @@ -1027,27 +1032,32 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomDetailsCopyRoomUrl] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - - // Create a matrix.to permalink to the room - - NSString *permalink = [MXTools permalinkToRoom:roomAliasLabel.text]; - - if (permalink) - { - MXKPasteboardManager.shared.pasteboard.string = permalink; - } - else - { - MXLogDebug(@"[RoomSettingsViewController] Copy room URL failed. Room URL is nil"); - } - } - - }]]; + + if (weakSelf) + { + typeof(self) self = weakSelf; + self->currentAlert = nil; + + // Create a matrix.to permalink to the room + + NSString *permalink = [MXTools permalinkToRoom:roomAliasLabel.text]; + + if (permalink) + { + MXKPasteboardManager.shared.pasteboard.string = permalink; + [self.view vc_toastWithMessage:VectorL10n.roomEventCopyLinkInfo + image:[UIImage imageNamed:@"link_icon"] + duration:2.0 + position:ToastPositionBottom + additionalMargin:0.0]; + } + else + { + MXLogDebug(@"[RoomSettingsViewController] Copy room URL failed. Room URL is nil"); + } + } + + }]]; // The user can only delete alias they has created, even if the Admin has set it as canonical. // So, let the server answer if it's possible to delete an alias. diff --git a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift index 45cf598e9..8ed8f74ae 100644 --- a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift +++ b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift @@ -27,7 +27,7 @@ protocol ThreadSummaryViewDelegate: AnyObject { class ThreadSummaryView: UIView { private enum Constants { - static let viewHeight: CGFloat = 32 + static let viewHeight: CGFloat = 40 static let viewDefaultWidth: CGFloat = 320 static let cornerRadius: CGFloat = 4 static let lastMessageFont: UIFont = .systemFont(ofSize: 13) @@ -38,6 +38,7 @@ class ThreadSummaryView: UIView { @IBOutlet private weak var lastMessageAvatarView: UserAvatarView! @IBOutlet private weak var lastMessageContentLabel: UILabel! + private var theme: Theme = ThemeService.shared().theme private(set) var thread: MXThread! private lazy var tapGestureRecognizer: UITapGestureRecognizer = { @@ -74,12 +75,12 @@ class ThreadSummaryView: UIView { } else { lastMessageAvatarView.avatarImageView.image = nil } - if let lastMessageText = viewModel.lastMessageText { - let mutableAttributedString = NSMutableAttributedString(attributedString: lastMessageText) - mutableAttributedString.setAttributes([ + if let lastMessage = viewModel.lastMessageText { + let mutable = NSMutableAttributedString(attributedString: lastMessage) + mutable.setAttributes([ .font: Constants.lastMessageFont - ], range: NSRange(location: 0, length: mutableAttributedString.length)) - lastMessageContentLabel.attributedText = mutableAttributedString + ], range: NSRange(location: 0, length: mutable.length)) + lastMessageContentLabel.attributedText = mutable } else { lastMessageContentLabel.attributedText = nil } @@ -112,7 +113,9 @@ class ThreadSummaryView: UIView { room.state { [weak self] roomState in guard let self = self else { return } let formatterError = UnsafeMutablePointer.allocate(capacity: 1) - let lastMessageText = eventFormatter.attributedString(from: lastMessage, with: roomState, error: formatterError) + let lastMessageText = eventFormatter.attributedString(from: lastMessage, + with: roomState, + error: formatterError) let viewModel = ThreadSummaryViewModel(numberOfReplies: thread.numberOfReplies, lastMessageSenderAvatar: avatarViewData, @@ -137,6 +140,8 @@ extension ThreadSummaryView: NibOwnerLoadable {} extension ThreadSummaryView: Themable { func update(theme: Theme) { + self.theme = theme + backgroundColor = theme.colors.system iconView.tintColor = theme.colors.secondaryContent numberOfRepliesLabel.textColor = theme.colors.secondaryContent diff --git a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib index 62e9a2d6c..12f97e11c 100644 --- a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib +++ b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib @@ -18,35 +18,38 @@ - + - + - - + + - + @@ -84,6 +87,6 @@ - + diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift index a39d39a8e..22efeafba 100644 --- a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift @@ -25,11 +25,6 @@ enum ThreadRoomTitleViewMode { @objcMembers class ThreadRoomTitleView: RoomTitleView { - private enum Constants { - static let titleLeadingConstraintOnPortrait: CGFloat = 6 - static let titleLeadingConstraintOnLandscape: CGFloat = 18 - } - var mode: ThreadRoomTitleViewMode = .allThreads { didSet { update() @@ -37,7 +32,6 @@ class ThreadRoomTitleView: RoomTitleView { } @IBOutlet private weak var titleLabel: UILabel! - @IBOutlet private weak var titleLabelLeadingConstraint: NSLayoutConstraint! @IBOutlet private weak var roomAvatarView: RoomAvatarView! @IBOutlet private weak var roomEncryptionBadgeView: UIImageView! @IBOutlet private weak var roomNameLabel: UILabel! @@ -81,7 +75,7 @@ class ThreadRoomTitleView: RoomTitleView { room.displayName)) let encrpytionBadge: UIImage? - if let summary = room.summary, room.mxSession.crypto != nil { + if let summary = room.summary, summary.isEncrypted, room.mxSession.crypto != nil { encrpytionBadge = EncryptionTrustLevelBadgeImageHelper.roomBadgeImage(for: summary.roomEncryptionTrustLevel()) } else { encrpytionBadge = nil @@ -100,16 +94,6 @@ class ThreadRoomTitleView: RoomTitleView { registerThemeServiceDidChangeThemeNotification() } - override func updateLayout(for orientation: UIInterfaceOrientation) { - super.updateLayout(for: orientation) - - if orientation.isPortrait { - titleLabelLeadingConstraint.constant = Constants.titleLeadingConstraintOnPortrait - } else { - titleLabelLeadingConstraint.constant = Constants.titleLeadingConstraintOnLandscape - } - } - // MARK: - Private private func registerThemeServiceDidChangeThemeNotification() { diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib index 3cab9f1a2..577c14329 100644 --- a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib @@ -12,61 +12,90 @@ - + - - + + - - - - - - - - - - + - + - - - - + + + + @@ -74,9 +103,8 @@ - - + diff --git a/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m b/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m index d04214673..5064dcb15 100644 --- a/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m +++ b/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m @@ -38,6 +38,9 @@ // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. id kThemeServiceDidChangeThemeNotificationObserver; } + +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation DirectoryServerPickerViewController @@ -49,6 +52,8 @@ // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSwitchDirectory]; } - (void)destroy @@ -145,9 +150,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"DirectoryServerPicker"]; - // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -158,6 +160,12 @@ [dataSource loadData]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + - (void)viewWillDisappear:(BOOL)animated { if (kAppDelegateDidTapStatusBarNotificationObserver) @@ -169,6 +177,12 @@ [super viewWillDisappear:animated]; } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + - (void)displayWithDataSource:(MXKDirectoryServersDataSource*)theDataSource onComplete:(void (^)(id cellData))onComplete; { diff --git a/Riot/Modules/Rooms/RoomsViewController.m b/Riot/Modules/Rooms/RoomsViewController.m index 25ae25ce9..183db0046 100644 --- a/Riot/Modules/Rooms/RoomsViewController.m +++ b/Riot/Modules/Rooms/RoomsViewController.m @@ -40,7 +40,7 @@ { [super finalizeInit]; - self.screenName = @"Rooms"; + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenRooms]; } - (void)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/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift index 4b05e230c..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.sharedInstance().trackValue(1, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted) + Analytics.shared.trackIdentityServerAccepted(true) } 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.trackIdentityServerAccepted(false) 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.trackIdentityServerAccepted(false) } self.delegate?.serviceTermsModalCoordinatorDidDismissInteractively(self) diff --git a/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m b/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m index 3e4b4297e..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; @@ -95,9 +103,12 @@ static CGFloat const kTextFontSize = 15.0; [super viewWillAppear:animated]; [self userInterfaceThemeDidChange]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"DeactivateAccount"]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; } - (void)viewDidLayoutSubviews @@ -107,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/Language/LanguagePickerViewController.m b/Riot/Modules/Settings/Language/LanguagePickerViewController.m index aac54c076..7c4c69055 100644 --- a/Riot/Modules/Settings/Language/LanguagePickerViewController.m +++ b/Riot/Modules/Settings/Language/LanguagePickerViewController.m @@ -106,14 +106,6 @@ } } -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"CountryPicker"]; -} - - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath; { cell.textLabel.textColor = ThemeService.shared.theme.textPrimaryColor; diff --git a/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m b/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m index 0ab2fe0d8..bdbc14ee1 100644 --- a/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m +++ b/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m @@ -95,14 +95,6 @@ return ThemeService.shared.theme.statusBarStyle; } -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"CountryPicker"]; -} - - (void)destroy { [super destroy]; diff --git a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m index b9b9ac474..42920094f 100644 --- a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m +++ b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m @@ -161,9 +161,6 @@ enum { { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] 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..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 @@ -250,9 +254,6 @@ TableViewSectionsDelegate> { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"Security"]; - // Release the potential pushed view controller [self releasePushedViewController]; @@ -268,6 +269,12 @@ TableViewSectionsDelegate> [self loadCrossSigning]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; @@ -279,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 365d915d1..ae42f7406 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -284,6 +284,8 @@ TableViewSectionsDelegate> @property (nonatomic) BOOL isPreparingIdentityService; @property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter; +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation SettingsViewController @@ -316,6 +318,8 @@ TableViewSectionsDelegate> isSavingInProgress = NO; isResetPwdInProgress = NO; is3PIDBindingInProgress = NO; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSettings]; } - (void)updateSections @@ -777,9 +781,6 @@ TableViewSectionsDelegate> - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"Settings"]; // Refresh display [self refreshSettings]; @@ -809,6 +810,8 @@ TableViewSectionsDelegate> [self releasePushedViewController]; [self.settingsDiscoveryTableViewSection reload]; + + [self.screenTimer start]; } - (void)viewWillDisappear:(BOOL)animated @@ -852,6 +855,12 @@ TableViewSectionsDelegate> } } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + #pragma mark - Internal methods - (void)pushViewController:(UIViewController*)viewController @@ -2252,11 +2261,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 = RiotSettings.shared.enableAnalytics; 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; } @@ -3127,27 +3136,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 optOut]; - RiotSettings.shared.enableCrashReport = YES; - - [[Analytics sharedInstance] start]; + // Remove potential crash file. + [MXLogger deleteCrashLog]; } } 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 18ef614dd..4f2d71dc1 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 @@ -82,6 +80,8 @@ // Assign itself as delegate self.contactsTableViewControllerDelegate = self; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenStartChat]; } - (void)viewDidLoad diff --git a/Riot/Modules/TabBar/MasterTabBarController.h b/Riot/Modules/TabBar/MasterTabBarController.h index 170e912d1..b607c0ea9 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 shouldPresentAnalyticsPromptForMatrixSession:(MXSession*)matrixSession; @end diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index 4a4751d83..6d4e175b8 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,11 +201,18 @@ 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. + if (Analytics.shared.shouldShowAnalyticsPrompt) { - [self promptUserBeforeUsingAnalytics]; + MXSession *mxSession = self.mxSessions.firstObject; + if (mxSession) + { + [self promptUserBeforeUsingAnalyticsForSession:mxSession]; + } + else + { + self.presentAnalyticsPromptOnAddSession = YES; + } } [self refreshTabBarBadges]; @@ -405,6 +417,12 @@ return; } + if (self.presentAnalyticsPromptOnAddSession) + { + self.presentAnalyticsPromptOnAddSession = NO; + [self promptUserBeforeUsingAnalyticsForSession:mxSession]; + } + // Check whether the controller'€™s view is loaded into memory. if (self.homeViewController) { @@ -921,50 +939,14 @@ #pragma mark - -- (void)promptUserBeforeUsingAnalytics +- (void)promptUserBeforeUsingAnalyticsForSession:(MXSession *)mxSession { - MXLogDebug(@"[MasterTabBarController]: Invite the user to send crash reports"); - - __weak typeof(self) weakSelf = self; - - [currentAlert dismissViewControllerAnimated:NO completion:nil]; - - NSString *appDisplayName = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"]; - - currentAlert = [UIAlertController alertControllerWithTitle:[VectorL10n googleAnalyticsUsePrompt:appDisplayName] message:nil 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; - } - - }]]; - - [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]; - - }]]; - - [currentAlert mxk_setAccessibilityIdentifier: @"HomeVCUseAnalyticsAlert"]; - [self presentViewController:currentAlert animated:YES completion:nil]; + // Analytics aren't collected on iOS 12 & 13. + if (@available(iOS 14.0, *)) + { + MXLogDebug(@"[MasterTabBarController]: Invite the user to send analytics"); + [self.masterTabBarDelegate masterTabBarController:self shouldPresentAnalyticsPromptForMatrixSession:mxSession]; + } } #pragma mark - Review session diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 8a942b9f3..3871a76af 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -559,6 +559,19 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { self.splitViewMasterPresentableDelegate?.splitViewMasterPresentableWantsToResetDetail(self) } + @available(iOS 14.0, *) + private func presentAnalyticsPrompt(with session: MXSession) { + let parameters = AnalyticsPromptCoordinatorParameters(session: session, navigationRouter: navigationRouter) + let coordinator = AnalyticsPromptCoordinator(parameters: parameters) + coordinator.completion = { [weak self, weak coordinator] in + guard let self = self, let coordinator = coordinator else { return } + self.remove(childCoordinator: coordinator) + } + + coordinator.start() + add(childCoordinator: coordinator) + } + // MARK: UserSessions management private func registerUserSessionsServiceNotifications() { @@ -650,6 +663,12 @@ extension TabBarCoordinator: MasterTabBarControllerDelegate { self.masterTabBarController.navigationItem.leftBarButtonItem = sideMenuBarButtonItem } + + func masterTabBarController(_ masterTabBarController: MasterTabBarController!, shouldPresentAnalyticsPromptForMatrixSession matrixSession: MXSession!) { + if #available(iOS 14.0, *) { + presentAnalyticsPrompt(with: matrixSession) + } + } } // MARK: - RoomCoordinatorDelegate diff --git a/Riot/Modules/Threads/Thread/ThreadViewController.swift b/Riot/Modules/Threads/Thread/ThreadViewController.swift index bee392509..2007b9186 100644 --- a/Riot/Modules/Threads/Thread/ThreadViewController.swift +++ b/Riot/Modules/Threads/Thread/ThreadViewController.swift @@ -122,6 +122,8 @@ class ThreadViewController: RoomViewController { } MXKPasteboardManager.shared.pasteboard.string = permalink + view.vc_toast(message: VectorL10n.roomEventCopyLinkInfo, + image: Asset.Images.linkIcon.image) } private func sharePermalink() { diff --git a/Riot/Modules/Threads/ThreadList/ThreadListCoordinator.swift b/Riot/Modules/Threads/ThreadList/ThreadListCoordinator.swift index 1dabc738a..a870d8808 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListCoordinator.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListCoordinator.swift @@ -69,6 +69,10 @@ extension ThreadListCoordinator: ThreadListViewModelCoordinatorDelegate { self.delegate?.threadListCoordinatorDidSelectThread(self, thread: thread) } + func threadListViewModelDidSelectThreadViewInRoom(_ viewModel: ThreadListViewModelProtocol, thread: MXThread) { + self.delegate?.threadListCoordinatorDidSelectRoom(self, roomId: thread.roomId, eventId: thread.id) + } + func threadListViewModelDidCancel(_ viewModel: ThreadListViewModelProtocol) { self.delegate?.threadListCoordinatorDidCancel(self) } diff --git a/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift b/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift index a4227b909..e15c70f35 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift @@ -21,6 +21,7 @@ import Foundation protocol ThreadListCoordinatorDelegate: AnyObject { func threadListCoordinatorDidLoadThreads(_ coordinator: ThreadListCoordinatorProtocol) func threadListCoordinatorDidSelectThread(_ coordinator: ThreadListCoordinatorProtocol, thread: MXThread) + func threadListCoordinatorDidSelectRoom(_ coordinator: ThreadListCoordinatorProtocol, roomId: String, eventId: String) func threadListCoordinatorDidCancel(_ coordinator: ThreadListCoordinatorProtocol) } diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewAction.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewAction.swift index 629e430e8..a03d0332f 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewAction.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewAction.swift @@ -25,5 +25,9 @@ enum ThreadListViewAction { case showFilterTypes case selectFilterType(_ type: ThreadListFilterType) case selectThread(_ index: Int) + case longPressThread(_ index: Int) + case actionViewInRoom + case actionCopyLinkToThread + case actionShare case cancel } diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard b/Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard index ddfb9060c..c56d0f29d 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard @@ -20,9 +20,11 @@ + + @@ -52,6 +54,11 @@ + + + + + diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift index 56f16c5df..7c9eba632 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift @@ -147,15 +147,21 @@ final class ThreadListViewController: UIViewController { case .idle: break case .loading: - self.renderLoading() + renderLoading() case .loaded: - self.renderLoaded() + renderLoaded() case .empty(let viewModel): - self.renderEmptyView(withViewModel: viewModel) + renderEmptyView(withViewModel: viewModel) case .showingFilterTypes: - self.renderShowingFilterTypes() + renderShowingFilterTypes() + case .showingLongPressActions: + renderShowingLongPressActions() + case .share(let string): + renderShare(string) + case .toastForCopyLink: + toastForCopyLink() case .error(let error): - self.render(error: error) + render(error: error) } } @@ -170,6 +176,12 @@ final class ThreadListViewController: UIViewController { threadsTableView.isHidden = false self.threadsTableView.reloadData() navigationItem.rightBarButtonItem?.isEnabled = true + switch viewModel.selectedFilterType { + case .all: + navigationItem.rightBarButtonItem?.image = Asset.Images.threadsFilter.image + case .myThreads: + navigationItem.rightBarButtonItem?.image = Asset.Images.threadsFilterApplied.image + } } private func renderEmptyView(withViewModel emptyViewModel: ThreadListEmptyViewModel) { @@ -178,6 +190,12 @@ final class ThreadListViewController: UIViewController { threadsTableView.isHidden = true emptyView.isHidden = false navigationItem.rightBarButtonItem?.isEnabled = viewModel.selectedFilterType == .myThreads + switch viewModel.selectedFilterType { + case .all: + navigationItem.rightBarButtonItem = nil + case .myThreads: + navigationItem.rightBarButtonItem?.image = Asset.Images.threadsFilterApplied.image + } } private func renderShowingFilterTypes() { @@ -214,6 +232,49 @@ final class ThreadListViewController: UIViewController { self.present(alertController, animated: true, completion: nil) } + private func renderShowingLongPressActions() { + let controller = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + controller.addAction(UIAlertAction(title: VectorL10n.roomEventActionViewInRoom, + style: .default, + handler: { [weak self] action in + guard let self = self else { return } + self.viewModel.process(viewAction: .actionViewInRoom) + })) + + controller.addAction(UIAlertAction(title: VectorL10n.threadCopyLinkToThread, + style: .default, + handler: { [weak self] action in + guard let self = self else { return } + self.viewModel.process(viewAction: .actionCopyLinkToThread) + })) + + controller.addAction(UIAlertAction(title: VectorL10n.roomEventActionShare, + style: .default, + handler: { [weak self] action in + guard let self = self else { return } + self.viewModel.process(viewAction: .actionShare) + })) + + controller.addAction(UIAlertAction(title: VectorL10n.cancel, + style: .cancel, + handler: nil)) + + self.present(controller, animated: true, completion: nil) + } + + private func renderShare(_ string: String) { + let activityVC = UIActivityViewController(activityItems: [string], + applicationActivities: nil) + activityVC.modalTransitionStyle = .coverVertical + present(activityVC, animated: true, completion: nil) + } + + private func toastForCopyLink() { + view.vc_toast(message: VectorL10n.roomEventCopyLinkInfo, + image: Asset.Images.linkIcon.image) + } + private func render(error: Error) { self.activityPresenter.removeCurrentActivityIndicator(animated: true) self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) @@ -225,6 +286,22 @@ final class ThreadListViewController: UIViewController { private func filterButtonTapped(_ sender: UIBarButtonItem) { self.viewModel.process(viewAction: .showFilterTypes) } + + @IBAction private func longPressed(_ sender: UILongPressGestureRecognizer) { + guard sender.state == .began else { + return + } + let point = sender.location(in: threadsTableView) + guard let indexPath = threadsTableView.indexPathForRow(at: point) else { + return + } + guard let cell = threadsTableView.cellForRow(at: indexPath) else { + return + } + if cell.isHighlighted { + viewModel.process(viewAction: .longPressThread(indexPath.row)) + } + } } @@ -249,10 +326,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 41ab5a702..40572f39f 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift @@ -31,6 +31,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { private var roomState: MXRoomState? private var currentOperation: MXHTTPOperation? + private var longPressedThread: MXThread? // MARK: Public @@ -73,6 +74,14 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { loadData() case .selectThread(let index): selectThread(index) + case .longPressThread(let index): + longPressThread(index) + case .actionViewInRoom: + actionViewInRoom() + case .actionCopyLinkToThread: + actionCopyLinkToThread() + case .actionShare: + actionShare() case .cancel: cancelOperations() coordinatorDelegate?.threadListViewModelDidCancel(self) @@ -103,7 +112,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { room.displayName)) let encrpytionBadge: UIImage? - if let summary = room.summary, session.crypto != nil { + if let summary = room.summary, summary.isEncrypted, session.crypto != nil { encrpytionBadge = EncryptionTrustLevelBadgeImageHelper.roomBadgeImage(for: summary.roomEncryptionTrustLevel()) } else { encrpytionBadge = nil @@ -117,14 +126,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, @@ -180,7 +189,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, @@ -265,6 +275,43 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { coordinatorDelegate?.threadListViewModelDidSelectThread(self, thread: thread) } + private func longPressThread(_ index: Int) { + guard index < threads.count else { + return + } + longPressedThread = threads[index] + viewState = .showingLongPressActions + } + + private func actionViewInRoom() { + guard let thread = longPressedThread else { + return + } + coordinatorDelegate?.threadListViewModelDidSelectThreadViewInRoom(self, thread: thread) + longPressedThread = nil + } + + private func actionCopyLinkToThread() { + guard let thread = longPressedThread else { + return + } + if let permalink = MXTools.permalink(toEvent: thread.id, inRoom: thread.roomId) { + MXKPasteboardManager.shared.pasteboard.string = permalink + viewState = .toastForCopyLink + } + longPressedThread = nil + } + + private func actionShare() { + guard let thread = longPressedThread else { + return + } + if let permalink = MXTools.permalink(toEvent: thread.id, inRoom: thread.roomId) { + viewState = .share(permalink) + } + longPressedThread = nil + } + private func cancelOperations() { self.currentOperation?.cancel() } diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift index be0892c47..457cf39d5 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift @@ -25,6 +25,7 @@ protocol ThreadListViewModelViewDelegate: AnyObject { protocol ThreadListViewModelCoordinatorDelegate: AnyObject { func threadListViewModelDidLoadThreads(_ viewModel: ThreadListViewModelProtocol) func threadListViewModelDidSelectThread(_ viewModel: ThreadListViewModelProtocol, thread: MXThread) + func threadListViewModelDidSelectThreadViewInRoom(_ viewModel: ThreadListViewModelProtocol, thread: MXThread) func threadListViewModelDidCancel(_ viewModel: ThreadListViewModelProtocol) } diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift index 073c70645..07196a49e 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift @@ -25,5 +25,8 @@ enum ThreadListViewState { case loaded case empty(_ viewModel: ThreadListEmptyViewModel) case showingFilterTypes + case showingLongPressActions + case share(_ string: String) + case toastForCopyLink case error(Error) } diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift index 8219f59d4..e7e5cf32d 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift @@ -29,6 +29,11 @@ class ThreadTableViewCell: UITableViewCell { @IBOutlet private weak var lastMessageTimeLabel: UILabel! @IBOutlet private weak var summaryView: ThreadSummaryView! @IBOutlet private weak var notificationStatusView: ThreadNotificationStatusView! + + private static var usernameColorGenerator: UserNameColorGenerator = { + let generator = UserNameColorGenerator() + return generator + }() override func awakeFromNib() { super.awakeFromNib() @@ -42,6 +47,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.attributedText = viewModel.rootMessageText lastMessageTimeLabel.text = viewModel.lastMessageTime @@ -58,6 +68,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/ThreadTableViewCell.xib b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib index 8e63bca54..d6e5ec497 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib @@ -11,11 +11,11 @@ - - + + - + @@ -26,14 +26,14 @@ - - + + + + @@ -76,9 +79,9 @@ - + - + @@ -90,7 +93,7 @@ - + diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadViewModel.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadViewModel.swift index 528c5e382..34170f71f 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: NSAttributedString? 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..d4b40f6f5 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.xib +++ b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.xib @@ -1,6 +1,6 @@ - + @@ -20,23 +20,23 @@ - + - + - + - + - + @@ -57,33 +57,33 @@ -