diff --git a/.swiftlint.yml b/.swiftlint.yml index 765c414b4..4d215eb98 100755 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -11,7 +11,8 @@ disabled_rules: - large_tuple - shorthand_operator - vertical_parameter_alignment - - identifier_name + - identifier_name + - inclusive_language # Disabled until MasterTabBarController refactoring complete # some rules are only opt-in opt_in_rules: diff --git a/CHANGES.md b/CHANGES.md index 7ace58680..ecde7c1af 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,31 @@ +## Changes in 1.6.2 (2021-10-08) + +🙌 Improvements + +- Upgrade MatrixKit version ([v0.16.5](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.16.5)). +- URL Previews: Use attributed string whitespace for cell heights and stop breaking up the bubble data. ([#4896](https://github.com/vector-im/element-ios/issues/4896)) +- Replaced localizable strings with generated ones throughout the code. Fixed various translation issues. ([#4899](https://github.com/vector-im/element-ios/issues/4899)) +- Voice Message scrubbing should require a slightly longer press, to avoid accidental scrubbing when scrolling the timeline ([#4935](https://github.com/vector-im/element-ios/issues/4935)) +- Pods: Update ffmpeg-kit-ios-audio, FLEX, FlowCommoniOS, Reusable and SwiftLint. ([#4939](https://github.com/vector-im/element-ios/issues/4939)) +- Service Terms: Track an analytics value on accept/decline of an identity server. ([#4955](https://github.com/vector-im/element-ios/issues/4955)) + +🐛 Bugfixes + +- RecentsDataSource: Memory leak in [RecentsDataSource dataSource:didStateChange:]. ([#4193](https://github.com/vector-im/element-ios/pull/4193)) +- i18n: Standardise casing of identity server and integration manager. ([#4559](https://github.com/vector-im/element-ios/issues/4559)) +- MasterTabBarController: Listen to `MXSpaceNotificationCounter` to update the notification badge ([#4898](https://github.com/vector-im/element-ios/issues/4898)) +- Fixed unintentional voice message drafts on automatically cancelled recordings (under 1 second) ([#4970](https://github.com/vector-im/element-ios/issues/4970)) + +🧱 Build + +- Element Alpha: Build on macOS 11 to fix iOS 15 installation error. ([#4937](https://github.com/vector-im/element-ios/issues/4937)) +- Bundler: Update CocoaPods and fastlane and xcode-install. ([#4951](https://github.com/vector-im/element-ios/issues/4951)) + +📄 Documentation + +- Update PR template with a checkbox for accessibility and self review. ([#4920](https://github.com/vector-im/element-ios/issues/4920)) + + ## Changes in 1.6.1 (2021-09-30) 🙌 Improvements diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 2f0eca71b..90034d66f 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.6.2 -CURRENT_PROJECT_VERSION = 1.6.2 +MARKETING_VERSION = 1.6.3 +CURRENT_PROJECT_VERSION = 1.6.3 diff --git a/Podfile b/Podfile index b06b5f92b..0cb651d7a 100644 --- a/Podfile +++ b/Podfile @@ -13,7 +13,7 @@ use_frameworks! # - `{ {kit spec hash} => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for each repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixKitVersion = '= 0.16.4' +$matrixKitVersion = '= 0.16.5' # $matrixKitVersion = :local # $matrixKitVersion = {'develop' => 'develop'} @@ -48,6 +48,7 @@ abstract_target 'RiotPods' do pod 'GBDeviceInfo', '~> 6.6.0' pod 'Reusable', '~> 4.1' pod 'KeychainAccess', '~> 4.2.2' + pod 'WeakDictionary', '~> 2.0' # Piwik for analytics pod 'MatomoTracker', '~> 7.4.1' diff --git a/Podfile.lock b/Podfile.lock index 27e8a2ced..4a7bfb316 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -45,7 +45,7 @@ PODS: - GBDeviceInfo/Core (6.6.0) - GZIP (1.3.0) - HPGrowingTextView (1.1) - - JitsiMeetSDK (3.5.0) + - JitsiMeetSDK (3.10.2) - KeychainAccess (4.2.2) - KituraContracts (1.2.1): - LoggerAPI (~> 1.7) @@ -58,30 +58,30 @@ PODS: - MatomoTracker (7.4.1): - MatomoTracker/Core (= 7.4.1) - MatomoTracker/Core (7.4.1) - - MatrixKit (0.16.4): + - MatrixKit (0.16.5): - Down (~> 0.11.0) - DTCoreText (~> 1.6.25) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixKit/Core (= 0.16.4) - - MatrixSDK (= 0.20.4) - - MatrixKit/Core (0.16.4): + - MatrixKit/Core (= 0.16.5) + - MatrixSDK (= 0.20.5) + - MatrixKit/Core (0.16.5): - Down (~> 0.11.0) - DTCoreText (~> 1.6.25) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.20.4) - - MatrixSDK (0.20.4): - - MatrixSDK/Core (= 0.20.4) - - MatrixSDK/Core (0.20.4): + - MatrixSDK (= 0.20.5) + - MatrixSDK (0.20.5): + - MatrixSDK/Core (= 0.20.5) + - MatrixSDK/Core (0.20.5): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - OLMKit (~> 3.2.5) - - Realm (= 10.7.6) + - Realm (= 10.16.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.20.4): - - JitsiMeetSDK (= 3.5.0) + - MatrixSDK/JingleCallStack (0.20.5): + - JitsiMeetSDK (= 3.10.2) - MatrixSDK/Core - OLMKit (3.2.5): - OLMKit/olmc (= 3.2.5) @@ -89,9 +89,9 @@ PODS: - OLMKit/olmc (3.2.5) - OLMKit/olmcpp (3.2.5) - ReadMoreTextView (3.0.1) - - Realm (10.7.6): - - Realm/Headers (= 10.7.6) - - Realm/Headers (10.7.6) + - Realm (10.16.0): + - Realm/Headers (= 10.16.0) + - Realm/Headers (10.16.0) - Reusable (4.1.2): - Reusable/Storyboard (= 4.1.2) - Reusable/View (= 4.1.2) @@ -124,7 +124,7 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - MatomoTracker (~> 7.4.1) - - MatrixKit (= 0.16.4) + - MatrixKit (= 0.16.5) - MatrixSDK - MatrixSDK/JingleCallStack - OLMKit @@ -195,7 +195,7 @@ SPEC CHECKSUMS: GBDeviceInfo: ed0db16230d2fa280e1cbb39a5a7f60f6946aaec GZIP: 416858efbe66b41b206895ac6dfd5493200d95b3 HPGrowingTextView: 88a716d97fb853bcb08a4a08e4727da17efc9b19 - JitsiMeetSDK: ef6ebbad2237c0e3ea6ff61fea78745f9543b238 + JitsiMeetSDK: 2f118fa770f23e518f3560fc224fae3ac7062223 KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51 KituraContracts: e845e60dc8627ad0a76fa55ef20a45451d8f830b KTCenterFlowLayout: 6e02b50ab2bd865025ae82fe266ed13b6d9eaf97 @@ -204,11 +204,11 @@ SPEC CHECKSUMS: LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b MatomoTracker: 24a846c9d3aa76933183fe9d47fd62c9efa863fb - MatrixKit: e2836912d2dc2072a276526e92679d094b2bd6b5 - MatrixSDK: 317928f6ef7bbffebbf7dbf9ca9dad4920695f1e + MatrixKit: a37efb94bb7c53b5dc912f0fd35971861b6c28bf + MatrixSDK: 417fac309f510b5f8ac121ba8abe3b897953e1ce OLMKit: 9fb4799c4a044dd2c06bda31ec31a12191ad30b5 ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d - Realm: ed860452717c8db8f4bf832b6807f7f2ce708839 + Realm: b6027801398f3743fc222f096faa85281b506e6c Reusable: 6bae6a5e8aa793c9c441db0213c863a64bce9136 SideMenu: f583187d21c5b1dd04c72002be544b555a2627a2 SwiftBase32: 9399c25a80666dc66b51e10076bf591e3bbb8f17 @@ -219,6 +219,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 367d7a514c7d8ea3cc4b0adb5878c8a192c7c2c8 +PODFILE CHECKSUM: e189a08f2a6f081d6eb0f57aaa898833f27a9adb -COCOAPODS: 1.10.1 +COCOAPODS: 1.11.2 diff --git a/Riot/Assets/Images.xcassets/Common/information_button.imageset/Contents.json b/Riot/Assets/Images.xcassets/Common/information_button.imageset/Contents.json new file mode 100644 index 000000000..dc33b7f60 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Common/information_button.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "information_button.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Common/information_button.imageset/information_button.pdf b/Riot/Assets/Images.xcassets/Common/information_button.imageset/information_button.pdf new file mode 100644 index 000000000..1ed7eb5aa --- /dev/null +++ b/Riot/Assets/Images.xcassets/Common/information_button.imageset/information_button.pdf @@ -0,0 +1,91 @@ +%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 19.184572 19.000000 cm +0.552000 0.592000 0.648000 scn +9.087891 0.000000 m +14.071289 0.000000 18.184570 4.113281 18.184570 9.087891 c +18.184570 14.062500 14.062500 18.175781 9.079102 18.175781 c +4.104492 18.175781 0.000000 14.062500 0.000000 9.087891 c +0.000000 4.113281 4.113281 0.000000 9.087891 0.000000 c +h +9.087891 1.810547 m +5.053711 1.810547 1.828125 5.053711 1.828125 9.087891 c +1.828125 13.122070 5.053711 16.356445 9.079102 16.356445 c +13.113281 16.356445 16.356445 13.122070 16.365234 9.087891 c +16.374023 5.053711 13.122070 1.810547 9.087891 1.810547 c +h +9.079102 7.628906 m +9.562500 7.628906 9.843750 7.901367 9.852539 8.411133 c +9.984375 12.638672 l +10.001953 13.157227 9.615234 13.535156 9.070312 13.535156 c +8.525391 13.535156 8.147461 13.166016 8.165039 12.647461 c +8.288086 8.411133 l +8.305664 7.910156 8.586914 7.628906 9.079102 7.628906 c +h +9.079102 4.710938 m +9.650391 4.710938 10.116211 5.124023 10.116211 5.686523 c +10.116211 6.240234 9.659180 6.653320 9.079102 6.653320 c +8.507812 6.653320 8.041992 6.240234 8.041992 5.686523 c +8.041992 5.132812 8.516602 4.710938 9.079102 4.710938 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 1184 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 20.000000 20.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 + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001274 00000 n +0000001297 00000 n +0000001470 00000 n +0000001544 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1603 +%%EOF \ No newline at end of file diff --git a/Riot/Assets/Images.xcassets/Contacts/Contents.json b/Riot/Assets/Images.xcassets/Contacts/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Contacts/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Contacts/find_your_contacts_facepile.imageset/Contents.json b/Riot/Assets/Images.xcassets/Contacts/find_your_contacts_facepile.imageset/Contents.json new file mode 100644 index 000000000..824fd548a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Contacts/find_your_contacts_facepile.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "facepile.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "facepile@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "facepile@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Contacts/find_your_contacts_facepile.imageset/facepile.png b/Riot/Assets/Images.xcassets/Contacts/find_your_contacts_facepile.imageset/facepile.png new file mode 100644 index 000000000..9e5b49a61 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Contacts/find_your_contacts_facepile.imageset/facepile.png differ diff --git a/Riot/Assets/Images.xcassets/Contacts/find_your_contacts_facepile.imageset/facepile@2x.png b/Riot/Assets/Images.xcassets/Contacts/find_your_contacts_facepile.imageset/facepile@2x.png new file mode 100644 index 000000000..90ad6b27d Binary files /dev/null and b/Riot/Assets/Images.xcassets/Contacts/find_your_contacts_facepile.imageset/facepile@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Contacts/find_your_contacts_facepile.imageset/facepile@3x.png b/Riot/Assets/Images.xcassets/Contacts/find_your_contacts_facepile.imageset/facepile@3x.png new file mode 100644 index 000000000..f2324c7ee Binary files /dev/null and b/Riot/Assets/Images.xcassets/Contacts/find_your_contacts_facepile.imageset/facepile@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Integrations/Contents.json b/Riot/Assets/Images.xcassets/Integrations/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Integrations/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Integrations/integration_manager_iconpile.imageset/Contents.json b/Riot/Assets/Images.xcassets/Integrations/integration_manager_iconpile.imageset/Contents.json new file mode 100644 index 000000000..034126ed8 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Integrations/integration_manager_iconpile.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "integration_manager_iconpile.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Integrations/integration_manager_iconpile.imageset/integration_manager_iconpile.pdf b/Riot/Assets/Images.xcassets/Integrations/integration_manager_iconpile.imageset/integration_manager_iconpile.pdf new file mode 100644 index 000000000..86db4d93f --- /dev/null +++ b/Riot/Assets/Images.xcassets/Integrations/integration_manager_iconpile.imageset/integration_manager_iconpile.pdf @@ -0,0 +1,522 @@ +%PDF-1.7 + +1 0 obj + << /BBox [ 0.000000 0.000000 110.000000 46.000000 ] + /Resources << >> + /Subtype /Form + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Type /XObject + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +q +1.000000 0.000000 -0.000000 1.000000 46.000000 13.067383 cm +0.450980 0.490196 0.549020 scn +8.832680 0.000023 m +3.859828 0.578173 0.000000 4.804639 0.000000 9.932617 c +0.000000 15.455465 4.477152 19.932617 10.000000 19.932617 c +15.152602 19.932617 19.395014 16.035631 19.940670 11.028222 c +19.906002 11.044100 19.870073 11.058149 19.832964 11.070212 c +16.719622 12.082235 13.238947 11.455502 10.848575 9.043321 c +8.464049 6.637041 7.840186 3.135887 8.832680 0.000023 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 46.000000 13.067383 cm +0.450980 0.490196 0.549020 scn +10.722207 0.654825 m +19.219536 9.152152 l +16.732798 9.960095 14.059622 9.433351 12.260846 7.618164 c +10.476121 5.817157 9.948320 3.147501 10.722207 0.654825 c +h +f +n +Q +54.832680 13.067406 m +49.859829 13.645554 46.000000 17.872021 46.000000 23.000000 c +46.000000 28.522848 50.477154 33.000000 56.000000 33.000000 c +61.152603 33.000000 65.395012 29.103014 65.940674 24.095604 c +65.906006 24.111483 65.870071 24.125532 65.832962 24.137596 c +62.719620 25.149618 59.238945 24.522884 56.848576 22.110703 c +54.464050 19.704424 53.840187 16.203270 54.832680 13.067406 c +h +W* +n +56.722206 13.722206 m +65.219536 22.219536 l +62.732796 23.027477 60.059624 22.500732 58.260845 20.685547 c +56.476120 18.884541 55.948318 16.214884 56.722206 13.722206 c +h +W* +n +q +1.000000 0.000000 -0.000000 1.000000 46.000000 13.067383 cm +0.450980 0.490196 0.549020 scn +8.832680 0.000023 m +8.717499 -0.990683 l +9.053570 -1.029755 9.386534 -0.895788 9.601898 -0.634851 c +9.817264 -0.373913 9.885659 -0.021587 9.783568 0.300978 c +8.832680 0.000023 l +h +19.940670 11.028222 m +19.525362 10.121425 l +19.851667 9.971979 20.233105 10.009373 20.524176 10.219343 c +20.815245 10.429314 20.971058 10.779479 20.932178 11.136267 c +19.940670 11.028222 l +h +19.832964 11.070212 m +20.141296 12.018734 l +20.141291 12.018736 l +19.832964 11.070212 l +h +10.848575 9.043321 m +11.557022 8.341278 l +11.557022 8.341278 l +10.848575 9.043321 l +h +10.722207 0.654825 m +9.769680 0.359100 l +9.873262 0.025461 10.143859 -0.229664 10.483012 -0.313446 c +10.822165 -0.397228 11.180433 -0.297455 11.427460 -0.050428 c +10.722207 0.654825 l +h +19.219536 9.152152 m +19.924788 8.446899 l +20.170168 8.692279 20.270367 9.047563 20.189354 9.384994 c +20.108341 9.722424 19.857763 9.993490 19.527727 10.100719 c +19.219536 9.152152 l +h +12.260846 7.618164 m +12.969294 6.916121 l +12.969294 6.916121 l +12.260846 7.618164 l +h +8.947861 0.990728 m +4.472036 1.511093 0.997378 5.316795 0.997378 9.932617 c +-0.997378 9.932617 l +-0.997378 4.292482 3.247620 -0.354748 8.717499 -0.990683 c +8.947861 0.990728 l +h +0.997378 9.932617 m +0.997378 14.904629 5.027989 18.935240 10.000000 18.935240 c +10.000000 20.929995 l +3.926316 20.929995 -0.997378 16.006302 -0.997378 9.932617 c +0.997378 9.932617 l +h +10.000000 18.935240 m +14.638034 18.935240 18.458044 15.427099 18.949162 10.920177 c +20.932178 11.136267 l +20.331984 16.644163 15.667171 20.929995 10.000000 20.929995 c +10.000000 18.935240 l +h +20.355978 11.935019 m +20.286608 11.966791 20.214968 11.994786 20.141296 12.018734 c +19.524632 10.121691 l +19.525362 10.121425 l +20.355978 11.935019 l +h +20.141291 12.018736 m +16.712317 13.133358 12.825830 12.455570 10.140127 9.745363 c +11.557022 8.341278 l +13.652063 10.455433 16.726927 11.031113 19.524637 10.121689 c +20.141291 12.018736 l +h +10.140127 9.745363 m +7.462934 7.043747 6.791740 3.143171 7.881791 -0.300932 c +9.783568 0.300978 l +8.888631 3.128603 9.465164 6.230335 11.557022 8.341278 c +10.140127 9.745363 l +h +11.427460 -0.050428 m +19.924788 8.446899 l +18.514284 9.857405 l +10.016954 1.360079 l +11.427460 -0.050428 l +h +19.527727 10.100719 m +16.723921 11.011679 13.645474 10.432379 11.552399 8.320207 c +12.969294 6.916121 l +14.473769 8.434322 16.741674 8.908512 18.911345 8.203585 c +19.527727 10.100719 l +h +11.552399 8.320207 m +9.477654 6.226535 8.899933 3.160538 9.769680 0.359100 c +11.674734 0.950550 l +10.996708 3.134464 11.474587 5.407779 12.969294 6.916121 c +11.552399 8.320207 l +h +f +n +Q +Q + +endstream +endobj + +2 0 obj + 3998 +endobj + +3 0 obj + << /BBox [ 0.000000 0.000000 110.000000 46.000000 ] + /Resources << >> + /Subtype /Form + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Type /XObject + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 39.257812 0.000000 cm +0.890196 0.909804 0.941176 scn +37.742256 23.000000 m +37.742256 10.297451 27.444805 0.000000 14.742256 0.000000 c +9.132510 0.000000 3.991834 2.008327 0.000000 5.345207 c +4.760142 9.730907 7.742256 16.017200 7.742256 23.000000 c +7.742256 29.982801 4.760141 36.269093 0.000000 40.654793 c +3.991834 43.991673 9.132510 46.000000 14.742256 46.000000 c +27.444805 46.000000 37.742256 35.702549 37.742256 23.000000 c +h +f* +n +Q + +endstream +endobj + +4 0 obj + 506 +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 +1.000000 0.000000 -0.000000 1.000000 71.212158 0.000000 cm +0.890196 0.909804 0.941176 scn +-0.000005 6.274433 m +4.200984 10.596851 6.787903 16.496323 6.787903 23.000000 c +6.787903 29.503679 4.200984 35.403149 -0.000005 39.725567 c +4.119314 43.615345 9.675125 46.000000 15.787903 46.000000 c +28.490452 46.000000 38.787903 35.702549 38.787903 23.000000 c +38.787903 10.297451 28.490452 0.000000 15.787903 0.000000 c +9.675125 0.000000 4.119314 2.384655 -0.000005 6.274433 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 39.257812 0.000000 cm +0.890196 0.909804 0.941176 scn +37.742264 23.000000 m +37.742264 10.297451 27.444813 0.000000 14.742264 0.000000 c +9.132518 0.000000 3.991842 2.008327 0.000008 5.345207 c +4.760150 9.730907 7.742264 16.017200 7.742264 23.000000 c +7.742264 29.982801 4.760149 36.269093 0.000008 40.654793 c +3.991842 43.991673 9.132518 46.000000 14.742264 46.000000 c +27.444813 46.000000 37.742264 35.702549 37.742264 23.000000 c +h +f* +n +Q +q +/E1 gs +/X1 Do +Q +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.890196 0.909804 0.941176 scn +46.000000 23.000000 m +46.000000 10.297451 35.702549 0.000000 23.000000 0.000000 c +10.297451 0.000000 0.000000 10.297451 0.000000 23.000000 c +0.000000 35.702549 10.297451 46.000000 23.000000 46.000000 c +35.702549 46.000000 46.000000 35.702549 46.000000 23.000000 c +h +f* +n +Q +q +q +1.000000 -0.000000 -0.000000 1.000000 79.000000 12.999998 cm +0.450980 0.490196 0.549020 scn +0.000000 16.000000 m +0.000000 18.209139 1.790861 20.000000 4.000000 20.000000 c +15.999997 20.000000 l +18.209137 20.000000 19.999998 18.209139 19.999998 16.000000 c +19.999998 4.000003 l +19.999998 1.790863 18.209137 0.000002 15.999998 0.000002 c +4.000000 0.000002 l +1.790862 0.000002 0.000000 1.790863 0.000000 4.000002 c +0.000000 16.000000 l +h +f +n +Q +79.000000 29.000000 m +79.000000 31.209137 80.790863 33.000000 83.000000 33.000000 c +95.000000 33.000000 l +97.209137 33.000000 99.000000 31.209139 99.000000 29.000000 c +99.000000 17.000000 l +99.000000 14.790861 97.209137 13.000000 95.000000 13.000000 c +83.000000 13.000000 l +80.790863 13.000000 79.000000 14.790861 79.000000 17.000000 c +79.000000 29.000000 l +h +W* +n +q +1.000000 -0.000000 -0.000000 1.000000 79.000000 12.999998 cm +0.450980 0.490196 0.549020 scn +4.000000 18.000000 m +15.999997 18.000000 l +15.999997 22.000000 l +4.000000 22.000000 l +4.000000 18.000000 l +h +17.999998 16.000000 m +17.999998 4.000003 l +21.999998 4.000003 l +21.999998 16.000000 l +17.999998 16.000000 l +h +15.999998 2.000002 m +4.000000 2.000002 l +4.000000 -1.999998 l +15.999998 -1.999998 l +15.999998 2.000002 l +h +2.000000 4.000002 m +2.000000 16.000000 l +-2.000000 16.000000 l +-2.000000 4.000002 l +2.000000 4.000002 l +h +4.000000 2.000002 m +2.895431 2.000002 2.000000 2.895433 2.000000 4.000002 c +-2.000000 4.000002 l +-2.000000 0.686293 0.686293 -1.999998 4.000000 -1.999998 c +4.000000 2.000002 l +h +17.999998 4.000003 m +17.999998 2.895433 17.104567 2.000002 15.999998 2.000002 c +15.999998 -1.999998 l +19.313707 -1.999998 21.999998 0.686295 21.999998 4.000003 c +17.999998 4.000003 l +h +15.999997 18.000000 m +17.104567 18.000000 17.999998 17.104568 17.999998 16.000000 c +21.999998 16.000000 l +21.999998 19.313709 19.313705 22.000000 15.999997 22.000000 c +15.999997 18.000000 l +h +4.000000 22.000000 m +0.686291 22.000000 -2.000000 19.313707 -2.000000 16.000000 c +2.000000 16.000000 l +2.000000 17.104568 2.895431 18.000000 4.000000 18.000000 c +4.000000 22.000000 l +h +f +n +Q +Q +q +1.000000 -0.000000 -0.000000 1.000000 81.000000 14.999998 cm +1.000000 1.000000 1.000000 scn +16.000000 8.000000 m +16.000000 3.581722 12.418278 0.000000 8.000000 0.000000 c +3.581722 0.000000 0.000000 3.581722 0.000000 8.000000 c +0.000000 12.418278 3.581722 16.000000 8.000000 16.000000 c +12.418278 16.000000 16.000000 12.418278 16.000000 8.000000 c +h +f +n +Q +q +1.000000 -0.000000 -0.000000 1.000000 89.000000 19.896606 cm +0.450980 0.490196 0.549020 scn +0.750000 7.103394 m +0.750000 7.517607 0.414214 7.853394 0.000000 7.853394 c +-0.414214 7.853394 -0.750000 7.517607 -0.750000 7.103394 c +0.750000 7.103394 l +h +0.000000 3.290894 m +-0.750000 3.290894 l +-0.750000 3.041655 -0.626185 2.808699 -0.419605 2.669257 c +0.000000 3.290894 l +h +2.080395 0.981757 m +2.423716 0.750016 2.889895 0.840468 3.121636 1.183789 c +3.353378 1.527109 3.262925 1.993289 2.919605 2.225030 c +2.080395 0.981757 l +h +-0.750000 7.103394 m +-0.750000 3.290894 l +0.750000 3.290894 l +0.750000 7.103394 l +-0.750000 7.103394 l +h +-0.419605 2.669257 m +2.080395 0.981757 l +2.919605 2.225030 l +0.419605 3.912530 l +-0.419605 2.669257 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 14.000000 15.000000 cm +0.450980 0.490196 0.549020 scn +0.000000 14.727272 m +0.000000 16.534750 1.465250 18.000000 3.272727 18.000000 c +14.727273 18.000000 l +16.534750 18.000000 18.000000 16.534750 18.000000 14.727272 c +18.000000 3.272727 l +18.000000 1.465250 16.534750 0.000000 14.727273 0.000000 c +3.272727 0.000000 l +1.465250 0.000000 0.000000 1.465250 0.000000 3.272727 c +0.000000 14.727272 l +h +8.181818 12.681818 m +8.181818 11.100275 6.899724 9.818182 5.318182 9.818182 c +3.736639 9.818182 2.454545 11.100275 2.454545 12.681818 c +2.454545 14.263361 3.736639 15.545454 5.318182 15.545454 c +6.899724 15.545454 8.181818 14.263361 8.181818 12.681818 c +h +5.318182 2.454546 m +6.899724 2.454546 8.181818 3.736639 8.181818 5.318182 c +8.181818 6.899724 6.899724 8.181818 5.318182 8.181818 c +3.736639 8.181818 2.454545 6.899724 2.454545 5.318182 c +2.454545 3.736639 3.736639 2.454546 5.318182 2.454546 c +h +15.545454 5.318182 m +15.545454 3.736639 14.263361 2.454546 12.681818 2.454546 c +11.100276 2.454546 9.818182 3.736639 9.818182 5.318182 c +9.818182 6.899724 11.100276 8.181818 12.681818 8.181818 c +14.263361 8.181818 15.545454 6.899724 15.545454 5.318182 c +h +12.681818 9.818182 m +14.263361 9.818182 15.545454 11.100275 15.545454 12.681818 c +15.545454 14.263361 14.263361 15.545454 12.681818 15.545454 c +11.100276 15.545454 9.818182 14.263361 9.818182 12.681818 c +9.818182 11.100275 11.100276 9.818182 12.681818 9.818182 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 14.000000 15.000000 cm +0.450980 0.490196 0.549020 scn +0.000000 14.727272 m +0.000000 16.534750 1.465250 18.000000 3.272727 18.000000 c +14.727273 18.000000 l +16.534750 18.000000 18.000000 16.534750 18.000000 14.727272 c +18.000000 3.272727 l +18.000000 1.465250 16.534750 0.000000 14.727273 0.000000 c +3.272727 0.000000 l +1.465250 0.000000 0.000000 1.465250 0.000000 3.272727 c +0.000000 14.727272 l +h +8.181818 12.681818 m +8.181818 11.100275 6.899724 9.818182 5.318182 9.818182 c +3.736639 9.818182 2.454545 11.100275 2.454545 12.681818 c +2.454545 14.263361 3.736639 15.545454 5.318182 15.545454 c +6.899724 15.545454 8.181818 14.263361 8.181818 12.681818 c +h +5.318182 2.454546 m +6.899724 2.454546 8.181818 3.736639 8.181818 5.318182 c +8.181818 6.899724 6.899724 8.181818 5.318182 8.181818 c +3.736639 8.181818 2.454545 6.899724 2.454545 5.318182 c +2.454545 3.736639 3.736639 2.454546 5.318182 2.454546 c +h +15.545454 5.318182 m +15.545454 3.736639 14.263361 2.454546 12.681818 2.454546 c +11.100276 2.454546 9.818182 3.736639 9.818182 5.318182 c +9.818182 6.899724 11.100276 8.181818 12.681818 8.181818 c +14.263361 8.181818 15.545454 6.899724 15.545454 5.318182 c +h +12.681818 9.818182 m +14.263361 9.818182 15.545454 11.100275 15.545454 12.681818 c +15.545454 14.263361 14.263361 15.545454 12.681818 15.545454 c +11.100276 15.545454 9.818182 14.263361 9.818182 12.681818 c +9.818182 11.100275 11.100276 9.818182 12.681818 9.818182 c +h +f* +n +Q + +endstream +endobj + +7 0 obj + 7483 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 110.000000 46.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 + << /Type /Catalog + /Pages 9 0 R + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000004257 00000 n +0000004280 00000 n +0000005035 00000 n +0000005057 00000 n +0000005355 00000 n +0000012894 00000 n +0000012917 00000 n +0000013091 00000 n +0000013165 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +13225 +%%EOF \ No newline at end of file diff --git a/Riot/Assets/en.lproj/InfoPlist.strings b/Riot/Assets/en.lproj/InfoPlist.strings index 468d42cce..6d3378a2a 100644 --- a/Riot/Assets/en.lproj/InfoPlist.strings +++ b/Riot/Assets/en.lproj/InfoPlist.strings @@ -18,6 +18,6 @@ "NSCameraUsageDescription" = "The camera is used to take photos and videos, make video calls."; "NSPhotoLibraryUsageDescription" = "The photo library is used to send photos and videos."; "NSMicrophoneUsageDescription" = "Element needs to access your microphone to make and receive calls, take videos, and record voice messages."; -"NSContactsUsageDescription" = "To discover contacts already using Matrix, Element can send email addresses and phone numbers in your address book to your chosen Matrix identity server. Where supported, personal data is hashed before sending - please check your identity server's privacy policy for more details."; +"NSContactsUsageDescription" = "Element will show your contacts so you can invite them to chat."; "NSCalendarsUsageDescription" = "See your scheduled meetings in the app."; "NSFaceIDUsageDescription" = "Face ID is used to access your app."; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 32c50a8e7..63b974866 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -178,7 +178,7 @@ "room_creation_keep_private" = "Keep private"; "room_creation_make_private" = "Make private"; "room_creation_wait_for_creation" = "A room is already being created. Please wait."; -"room_creation_invite_another_user" = "Search / invite by User ID, Name or email"; +"room_creation_invite_another_user" = "User ID, name or email"; "room_creation_error_invite_user_by_email_without_identity_server" = "No identity server is configured so you cannot add a participant with an email."; "room_creation_dm_error" = "We couldn't create your DM. Please check the users you want to invite and try again."; @@ -244,8 +244,15 @@ Tap the + to start adding people."; "contacts_address_book_no_contact" = "No local contacts"; "contacts_address_book_permission_required" = "Permission required to access local contacts"; "contacts_address_book_permission_denied" = "You didn't allow %@ to access your local contacts"; +"contacts_address_book_permission_denied_alert_title" = "Contacts disabled"; +"contacts_address_book_permission_denied_alert_message" = "To enable contacts, go to your device settings."; "contacts_user_directory_section" = "USER DIRECTORY"; "contacts_user_directory_offline_section" = "USER DIRECTORY (offline)"; +"find_your_contacts_title" = "Start by listing your contacts"; +"find_your_contacts_message" = "Let %@ show your contacts so you can quickly start chatting with those you know best."; +"find_your_contacts_button_title" = "Find your contacts"; +"find_your_contacts_footer" = "This can be disabled anytime from settings."; +"find_your_contacts_identity_service_error" = "Unable to connect to the identity server."; // Chat participants "room_participants_title" = "Participants"; @@ -462,7 +469,8 @@ Tap the + to start adding people."; "settings_integrations" = "INTEGRATIONS"; "settings_user_interface" = "USER INTERFACE"; "settings_ignored_users" = "IGNORED USERS"; -"settings_contacts" = "LOCAL CONTACTS"; +"settings_contacts" = "DEVICE CONTACTS"; +"settings_phone_contacts" = "PHONE CONTACTS"; "settings_advanced" = "ADVANCED"; "settings_other" = "OTHER"; "settings_labs" = "LABS"; @@ -550,8 +558,9 @@ Tap the + to start adding people."; "settings_unignore_user" = "Show all messages from %@?"; -"settings_contacts_discover_matrix_users" = "Use emails and phone numbers to discover users"; +"settings_contacts_enable_sync" = "Find your contacts"; "settings_contacts_phonebook_country" = "Phonebook country"; +"settings_contacts_enable_sync_description" = "This will use your identity server to connect you with your contacts, and help them find you."; "settings_labs_e2e_encryption" = "End-to-End Encryption"; "settings_labs_e2e_encryption_prompt_message" = "To finish setting up encryption you must log in again."; @@ -1020,18 +1029,21 @@ Tap the + to start adding people."; "gdpr_consent_not_given_alert_review_now_action" = "Review now"; // Service terms -"service_terms_modal_title" = "Terms Of Service"; -"service_terms_modal_message" = "To continue you need to accept the terms of this service (%@)."; +"service_terms_modal_title_message" = "To continue, accept the below terms and conditions"; "service_terms_modal_accept_button" = "Accept"; "service_terms_modal_decline_button" = "Decline"; +"service_terms_modal_footer" = "This can be disabled anytime in settings."; -"service_terms_modal_description_for_identity_server_1" = "Find others by phone or email"; -"service_terms_modal_description_for_identity_server_2" = "Be found by phone or email"; -"service_terms_modal_description_for_integration_manager" = "Use Bots, bridges, widgets and sticker packs"; +"service_terms_modal_table_header_identity_server" = "IDENTITY SERVER TERMS"; +"service_terms_modal_table_header_integration_manager" = "INTEGRATION MANAGER TERMS"; +"service_terms_modal_description_identity_server" = "This will allow someone to find you if they have your phone number or email saved in their phone contacts."; +"service_terms_modal_description_integration_manager" = "This will allow you to use bots, bridges, widgets and sticker packs."; -// Service terms - Variant for identity server when displayed out of a context -"service_terms_modal_title_identity_server" = "Contact discovery"; -"service_terms_modal_message_identity_server" = "Accept the terms of the identity server (%@) to discover contacts."; +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Identity Server"; +"service_terms_modal_information_title_integration_manager" = "Integration Manager"; +"service_terms_modal_information_description_identity_server" = "An identity server helps you find your contacts, by looking up their phone number or email address, to see if they already have an account."; +"service_terms_modal_information_description_integration_manager" = "An integration manager lets you add features from third parties."; "service_terms_modal_policy_checkbox_accessibility_hint" = "Check to accept %@"; diff --git a/Riot/Assets/third_party_licenses.html b/Riot/Assets/third_party_licenses.html index 6e491fdca..fc4b54a43 100644 --- a/Riot/Assets/third_party_licenses.html +++ b/Riot/Assets/third_party_licenses.html @@ -1885,6 +1885,32 @@ permanent authorization for you to choose that version for the Library. +
  • + WeakDictionary (https://github.com/nicholascross/WeakDictionary/) +

    + MIT License +

    + Copyright (c) 2016 Nicholas Cross +

    + 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/Array.swift b/Riot/Categories/Array.swift new file mode 100644 index 000000000..88cf73e1f --- /dev/null +++ b/Riot/Categories/Array.swift @@ -0,0 +1,29 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +extension Array where Element: Equatable { + +/// Remove first collection element that is equal to the given `object` +/// Credits: https://stackoverflow.com/a/45008042 + mutating func vc_removeFirstOccurrence(of object: Element) { + guard let index = firstIndex(of: object) else { + return + } + remove(at: index) + } +} diff --git a/Riot/Categories/MXKImageView.swift b/Riot/Categories/MXKImageView.swift index 5f32960ed..2f324884a 100644 --- a/Riot/Categories/MXKImageView.swift +++ b/Riot/Categories/MXKImageView.swift @@ -17,9 +17,9 @@ import Foundation extension MXKImageView { - @objc func vc_setRoomAvatarImage(with url: String?, displayName: String, mediaManager: MXMediaManager) { + @objc func vc_setRoomAvatarImage(with url: String?, roomId: String, displayName: String, mediaManager: MXMediaManager) { // Use the display name to prepare the default avatar image. - let avatarImage = AvatarGenerator.generateAvatar(forText: displayName) + let avatarImage = AvatarGenerator.generateAvatar(forMatrixItem: roomId, withDisplayName: displayName) if let avatarUrl = url { self.enableInMemoryCache = true diff --git a/Riot/Categories/MXRoomSummary+Riot.m b/Riot/Categories/MXRoomSummary+Riot.m index 961a51a6e..7a83d1d40 100644 --- a/Riot/Categories/MXRoomSummary+Riot.m +++ b/Riot/Categories/MXRoomSummary+Riot.m @@ -19,32 +19,20 @@ #import "AvatarGenerator.h" +#ifdef IS_SHARE_EXTENSION +#import "RiotShareExtension-Swift.h" +#else +#import "Riot-Swift.h" +#endif + @implementation MXRoomSummary (Riot) - (void)setRoomAvatarImageIn:(MXKImageView*)mxkImageView { - // Use the room display name to prepare the default avatar image. - NSString *avatarDisplayName = self.displayname; - UIImage* avatarImage = [AvatarGenerator generateAvatarForMatrixItem:self.roomId withDisplayName:avatarDisplayName]; - - if (self.avatar) - { - mxkImageView.enableInMemoryCache = YES; - - [mxkImageView setImageURI:self.avatar - withType:nil - andImageOrientation:UIImageOrientationUp - toFitViewSize:mxkImageView.frame.size - withMethod:MXThumbnailingMethodCrop - previewImage:avatarImage - mediaManager:self.mxSession.mediaManager]; - } - else - { - mxkImageView.image = avatarImage; - } - - mxkImageView.contentMode = UIViewContentModeScaleAspectFill; + [mxkImageView vc_setRoomAvatarImageWith:self.avatar + roomId:self.roomId + displayName:self.displayname + mediaManager:self.mxSession.mediaManager]; } - (RoomEncryptionTrustLevel)roomEncryptionTrustLevel diff --git a/Riot/Categories/UIViewController.swift b/Riot/Categories/UIViewController.swift index 8245d9a26..e1c16760e 100644 --- a/Riot/Categories/UIViewController.swift +++ b/Riot/Categories/UIViewController.swift @@ -103,4 +103,16 @@ extension UIViewController { return fabImageView } + + /// Set leftBarButtonItem with split view display mode button if there is no leftBarButtonItem defined and splitViewController exists. + /// To be Used when view controller is displayed as detail controller in split view. + func vc_setupDisplayModeLeftBarButtonItemIfNeeded() { + guard let splitViewController = self.splitViewController, self.navigationItem.leftBarButtonItem == nil else { + return + } + + // If there is no leftBarButtonItem defined, + // set split view display mode button as left bar button item + self.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem + } } diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 2af5367f5..78b460c56 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -60,6 +60,7 @@ internal enum Asset { internal static let errorIcon = ImageAsset(name: "error_icon") internal static let faceidIcon = ImageAsset(name: "faceid_icon") internal static let group = ImageAsset(name: "group") + internal static let informationButton = ImageAsset(name: "information_button") internal static let monitor = ImageAsset(name: "monitor") internal static let placeholder = ImageAsset(name: "placeholder") internal static let plusIcon = ImageAsset(name: "plus_icon") @@ -74,6 +75,7 @@ internal enum Asset { internal static let touchidIcon = ImageAsset(name: "touchid_icon") internal static let addGroupParticipant = ImageAsset(name: "add_group_participant") internal static let removeIconBlue = ImageAsset(name: "remove_icon_blue") + internal static let findYourContactsFacepile = ImageAsset(name: "find_your_contacts_facepile") internal static let captureAvatar = ImageAsset(name: "capture_avatar") internal static let e2eBlocked = ImageAsset(name: "e2e_blocked") internal static let e2eUnencrypted = ImageAsset(name: "e2e_unencrypted") @@ -95,6 +97,7 @@ internal enum Asset { internal static let plusFloatingAction = ImageAsset(name: "plus_floating_action") internal static let versionCheckCloseIcon = ImageAsset(name: "version_check_close_icon") internal static let versionCheckInfoIcon = ImageAsset(name: "version_check_info_icon") + internal static let integrationManagerIconpile = ImageAsset(name: "integration_manager_iconpile") internal static let closeBanner = ImageAsset(name: "close_banner") internal static let importFilesButton = ImageAsset(name: "import_files_button") internal static let keyBackupLogo = ImageAsset(name: "key_backup_logo") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index d82e45c6f..f23c03d2a 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -559,6 +559,14 @@ public class VectorL10n: NSObject { public static func contactsAddressBookPermissionDenied(_ p1: String) -> String { return VectorL10n.tr("Vector", "contacts_address_book_permission_denied", p1) } + /// To enable contacts, go to your device settings. + public static var contactsAddressBookPermissionDeniedAlertMessage: String { + return VectorL10n.tr("Vector", "contacts_address_book_permission_denied_alert_message") + } + /// Contacts disabled + public static var contactsAddressBookPermissionDeniedAlertTitle: String { + return VectorL10n.tr("Vector", "contacts_address_book_permission_denied_alert_title") + } /// Permission required to access local contacts public static var contactsAddressBookPermissionRequired: String { return VectorL10n.tr("Vector", "contacts_address_book_permission_required") @@ -1403,6 +1411,26 @@ public class VectorL10n: NSObject { public static var fileUploadErrorUnsupportedFileTypeMessage: String { return VectorL10n.tr("Vector", "file_upload_error_unsupported_file_type_message") } + /// Find your contacts + public static var findYourContactsButtonTitle: String { + return VectorL10n.tr("Vector", "find_your_contacts_button_title") + } + /// This can be disabled anytime from settings. + public static var findYourContactsFooter: String { + return VectorL10n.tr("Vector", "find_your_contacts_footer") + } + /// Unable to connect to the identity server. + public static var findYourContactsIdentityServiceError: String { + return VectorL10n.tr("Vector", "find_your_contacts_identity_service_error") + } + /// Let %@ show your contacts so you can quickly start chatting with those you know best. + public static func findYourContactsMessage(_ p1: String) -> String { + return VectorL10n.tr("Vector", "find_your_contacts_message", p1) + } + /// Start by listing your contacts + public static var findYourContactsTitle: String { + return VectorL10n.tr("Vector", "find_your_contacts_title") + } /// To continue using the %@ homeserver you must review and agree to the terms and conditions. public static func gdprConsentNotGivenAlertMessage(_ p1: String) -> String { return VectorL10n.tr("Vector", "gdpr_consent_not_given_alert_message", p1) @@ -2463,7 +2491,7 @@ public class VectorL10n: NSObject { public static var roomCreationErrorInviteUserByEmailWithoutIdentityServer: String { return VectorL10n.tr("Vector", "room_creation_error_invite_user_by_email_without_identity_server") } - /// Search / invite by User ID, Name or email + /// User ID, name or email public static var roomCreationInviteAnotherUser: String { return VectorL10n.tr("Vector", "room_creation_invite_another_user") } @@ -4011,37 +4039,49 @@ public class VectorL10n: NSObject { public static var serviceTermsModalDeclineButton: String { return VectorL10n.tr("Vector", "service_terms_modal_decline_button") } - /// Find others by phone or email - public static var serviceTermsModalDescriptionForIdentityServer1: String { - return VectorL10n.tr("Vector", "service_terms_modal_description_for_identity_server_1") + /// This will allow someone to find you if they have your phone number or email saved in their phone contacts. + public static var serviceTermsModalDescriptionIdentityServer: String { + return VectorL10n.tr("Vector", "service_terms_modal_description_identity_server") } - /// Be found by phone or email - public static var serviceTermsModalDescriptionForIdentityServer2: String { - return VectorL10n.tr("Vector", "service_terms_modal_description_for_identity_server_2") + /// This will allow you to use bots, bridges, widgets and sticker packs. + public static var serviceTermsModalDescriptionIntegrationManager: String { + return VectorL10n.tr("Vector", "service_terms_modal_description_integration_manager") } - /// Use Bots, bridges, widgets and sticker packs - public static var serviceTermsModalDescriptionForIntegrationManager: String { - return VectorL10n.tr("Vector", "service_terms_modal_description_for_integration_manager") + /// This can be disabled anytime in settings. + public static var serviceTermsModalFooter: String { + return VectorL10n.tr("Vector", "service_terms_modal_footer") } - /// To continue you need to accept the terms of this service (%@). - public static func serviceTermsModalMessage(_ p1: String) -> String { - return VectorL10n.tr("Vector", "service_terms_modal_message", p1) + /// An identity server helps you find your contacts, by looking up their phone number or email address, to see if they already have an account. + public static var serviceTermsModalInformationDescriptionIdentityServer: String { + return VectorL10n.tr("Vector", "service_terms_modal_information_description_identity_server") } - /// Accept the terms of the identity server (%@) to discover contacts. - public static func serviceTermsModalMessageIdentityServer(_ p1: String) -> String { - return VectorL10n.tr("Vector", "service_terms_modal_message_identity_server", p1) + /// An integration manager lets you add features from third parties. + public static var serviceTermsModalInformationDescriptionIntegrationManager: String { + return VectorL10n.tr("Vector", "service_terms_modal_information_description_integration_manager") + } + /// Identity Server + public static var serviceTermsModalInformationTitleIdentityServer: String { + return VectorL10n.tr("Vector", "service_terms_modal_information_title_identity_server") + } + /// Integration Manager + public static var serviceTermsModalInformationTitleIntegrationManager: String { + return VectorL10n.tr("Vector", "service_terms_modal_information_title_integration_manager") } /// Check to accept %@ public static func serviceTermsModalPolicyCheckboxAccessibilityHint(_ p1: String) -> String { return VectorL10n.tr("Vector", "service_terms_modal_policy_checkbox_accessibility_hint", p1) } - /// Terms Of Service - public static var serviceTermsModalTitle: String { - return VectorL10n.tr("Vector", "service_terms_modal_title") + /// IDENTITY SERVER TERMS + public static var serviceTermsModalTableHeaderIdentityServer: String { + return VectorL10n.tr("Vector", "service_terms_modal_table_header_identity_server") } - /// Contact discovery - public static var serviceTermsModalTitleIdentityServer: String { - return VectorL10n.tr("Vector", "service_terms_modal_title_identity_server") + /// INTEGRATION MANAGER TERMS + public static var serviceTermsModalTableHeaderIntegrationManager: String { + return VectorL10n.tr("Vector", "service_terms_modal_table_header_integration_manager") + } + /// To continue, accept the below terms and conditions + public static var serviceTermsModalTitleMessage: String { + return VectorL10n.tr("Vector", "service_terms_modal_title_message") } /// Invalid credentials public static var settingsAdd3pidInvalidPasswordMessage: String { @@ -4127,13 +4167,17 @@ public class VectorL10n: NSObject { public static var settingsConfirmPassword: String { return VectorL10n.tr("Vector", "settings_confirm_password") } - /// LOCAL CONTACTS + /// DEVICE CONTACTS public static var settingsContacts: String { return VectorL10n.tr("Vector", "settings_contacts") } - /// Use emails and phone numbers to discover users - public static var settingsContactsDiscoverMatrixUsers: String { - return VectorL10n.tr("Vector", "settings_contacts_discover_matrix_users") + /// Find your contacts + public static var settingsContactsEnableSync: String { + return VectorL10n.tr("Vector", "settings_contacts_enable_sync") + } + /// This will use your identity server to connect you with your contacts, and help them find you. + public static var settingsContactsEnableSyncDescription: String { + return VectorL10n.tr("Vector", "settings_contacts_enable_sync_description") } /// Phonebook country public static var settingsContactsPhonebookCountry: String { @@ -4543,6 +4587,10 @@ public class VectorL10n: NSObject { public static var settingsPasswordUpdated: String { return VectorL10n.tr("Vector", "settings_password_updated") } + /// PHONE CONTACTS + public static var settingsPhoneContacts: String { + return VectorL10n.tr("Vector", "settings_phone_contacts") + } /// Phone public static var settingsPhoneNumber: String { return VectorL10n.tr("Vector", "settings_phone_number") diff --git a/Riot/Managers/Theme/Theme.swift b/Riot/Managers/Theme/Theme.swift index 37479e048..8c6f52a66 100644 --- a/Riot/Managers/Theme/Theme.swift +++ b/Riot/Managers/Theme/Theme.swift @@ -41,6 +41,7 @@ import DesignKit var textPrimaryColor: UIColor { get } var textSecondaryColor: UIColor { get } var textTertiaryColor: UIColor { get } + var textQuinaryColor: UIColor { get } var tintColor: UIColor { get } var tintBackgroundColor: UIColor { get } diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift index 1d4ce7149..b6aabc3ff 100644 --- a/Riot/Managers/Theme/Themes/DarkTheme.swift +++ b/Riot/Managers/Theme/Themes/DarkTheme.swift @@ -42,6 +42,7 @@ class DarkTheme: NSObject, Theme { var textPrimaryColor: UIColor = UIColor(rgb: 0xFFFFFF) var textSecondaryColor: UIColor = UIColor(rgb: 0xA9B2BC) var textTertiaryColor: UIColor = UIColor(rgb: 0x8E99A4) + var textQuinaryColor: UIColor = UIColor(rgb: 0x394049) var tintColor: UIColor = UIColor(displayP3Red: 0.05098039216, green: 0.7450980392, blue: 0.5450980392, alpha: 1.0) var tintBackgroundColor: UIColor = UIColor(rgb: 0x1F6954) diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift index cd618d29d..545b801b9 100644 --- a/Riot/Managers/Theme/Themes/DefaultTheme.swift +++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift @@ -42,6 +42,7 @@ class DefaultTheme: NSObject, Theme { var textPrimaryColor: UIColor = UIColor(rgb: 0x17191C) var textSecondaryColor: UIColor = UIColor(rgb: 0x737D8C) var textTertiaryColor: UIColor = UIColor(rgb: 0x8D99A5) + var textQuinaryColor: UIColor = UIColor(rgb: 0xE3E8F0) var tintColor: UIColor = UIColor(displayP3Red: 0.05098039216, green: 0.7450980392, blue: 0.5450980392, alpha: 1.0) var tintBackgroundColor: UIColor = UIColor(rgb: 0xe9fff9) diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift index 2fa364f52..32121a0f6 100755 --- a/Riot/Modules/Application/AppCoordinator.swift +++ b/Riot/Modules/Application/AppCoordinator.swift @@ -80,6 +80,9 @@ final class AppCoordinator: NSObject, AppCoordinatorType { self.setupLogger() self.setupTheme() + // Setup navigation router store + _ = NavigationRouterStore.shared + if BuildSettings.enableSideMenu { self.addSideMenu() } diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index d0b5217ff..2052c6a6a 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -87,7 +87,7 @@ NSString *const AppDelegateDidValidateEmailNotificationClientSecretKey = @"AppDe NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUniversalLinkDidChangeNotification"; -@interface LegacyAppDelegate () +@interface LegacyAppDelegate () { /** Reachability observer @@ -201,7 +201,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni @property (weak, nonatomic) UIAlertController *incomingKeyVerificationRequestAlertController; -@property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter; @property (nonatomic, strong) SlidingModalPresenter *slidingModalPresenter; @property (nonatomic, strong) SetPinCoordinatorBridgePresenter *setPinCoordinatorBridgePresenter; @property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter; @@ -674,9 +673,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Register to GDPR consent not given notification [self registerUserConsentNotGivenNotification]; - // Register to identity server terms not signed notification - [self registerIdentityServiceTermsNotSignedNotification]; - // Start monitoring reachability [[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) { @@ -1847,6 +1843,13 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [self checkLocalPrivateKeysInSession:mxSession]; [self.pushNotificationService checkPushKitPushersInSession:mxSession]; + + // Validate the availability of local contact sync for any changes to the + // authorization of contacts access that may have occurred since the last launch. + if (BuildSettings.allowLocalContactsAccess) + { + [MXKContactManager.sharedManager validateSyncLocalContactsState]; + } } else if (mxSession.state == MXSessionStateClosed) { @@ -4131,82 +4134,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } -#pragma mark - Identity server service terms - -// Observe identity server terms not signed notification -- (void)registerIdentityServiceTermsNotSignedNotification -{ - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleIdentityServiceTermsNotSignedNotification:) name:MXIdentityServiceTermsNotSignedNotification object:nil]; -} - -- (void)handleIdentityServiceTermsNotSignedNotification:(NSNotification*)notification -{ - MXLogDebug(@"[AppDelegate] IS Terms: handleIdentityServiceTermsNotSignedNotification."); - - NSString *baseURL; - NSString *accessToken; - - MXJSONModelSetString(baseURL, notification.userInfo[MXIdentityServiceNotificationIdentityServerKey]); - MXJSONModelSetString(accessToken, notification.userInfo[MXIdentityServiceNotificationAccessTokenKey]); - - [self presentIdentityServerTermsWithBaseURL:baseURL andAccessToken:accessToken]; -} - -- (void)presentIdentityServerTermsWithBaseURL:(NSString*)baseURL andAccessToken:(NSString*)accessToken -{ - MXSession *mxSession = self.mxSessions.firstObject; - - if (!mxSession || !baseURL || !accessToken || self.serviceTermsModalCoordinatorBridgePresenter.isPresenting) - { - return; - } - - ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:mxSession - baseUrl:baseURL - serviceType:MXServiceTypeIdentityService - outOfContext:YES - accessToken:accessToken]; - - serviceTermsModalCoordinatorBridgePresenter.delegate = self; - - [serviceTermsModalCoordinatorBridgePresenter presentFrom:self.presentedViewController animated:YES]; - self.serviceTermsModalCoordinatorBridgePresenter = serviceTermsModalCoordinatorBridgePresenter; -} - -- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter -{ - [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - - }]; - self.serviceTermsModalCoordinatorBridgePresenter = nil; -} - -- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter *)coordinatorBridgePresenter session:(MXSession *)session -{ - MXLogDebug(@"[AppDelegate] IS Terms: User has declined the use of the default IS."); - - // The user does not want to use the proposed IS. - // Disable IS feature on user's account - [session setIdentityServer:nil andAccessToken:nil]; - [session setAccountDataIdentityServer:nil success:^{ - } failure:^(NSError *error) { - MXLogDebug(@"[AppDelegate] IS Terms: Error: %@", error); - }]; - - [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - - }]; - self.serviceTermsModalCoordinatorBridgePresenter = nil; -} - -- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter -{ - [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - - }]; - self.serviceTermsModalCoordinatorBridgePresenter = nil; -} - #pragma mark - Settings - (void)setupUserDefaults diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index fa2a3e505..b44463775 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -417,7 +417,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro // Update here the index of the current selected cell (if any) - Useful in landscape mode with split view controller. NSIndexPath *currentSelectedCellIndexPath = nil; MasterTabBarController *masterTabBarController = [AppDelegate theDelegate].masterTabBarController; - if (masterTabBarController.currentRoomViewController) + if (masterTabBarController.selectedRoomId) { // Look for the rank of this selected room in displayed recents currentSelectedCellIndexPath = [self.dataSource cellIndexPathWithRoomId:masterTabBarController.selectedRoomId andMatrixSession:masterTabBarController.selectedRoomSession]; diff --git a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m index 963f5670a..0d540399b 100644 --- a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m +++ b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m @@ -125,6 +125,7 @@ } [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.avatarUrl + roomId:roomCellData.roomIdentifier displayName:roomCellData.roomDisplayname mediaManager:roomCellData.mxSession.mediaManager]; } diff --git a/Riot/Modules/Communities/GroupsViewController.m b/Riot/Modules/Communities/GroupsViewController.m index 4339c75c5..916c344ee 100644 --- a/Riot/Modules/Communities/GroupsViewController.m +++ b/Riot/Modules/Communities/GroupsViewController.m @@ -323,7 +323,7 @@ // Update here the index of the current selected cell (if any) - Useful in landscape mode with split view controller. NSIndexPath *currentSelectedCellIndexPath = nil; MasterTabBarController *masterTabBarController = [AppDelegate theDelegate].masterTabBarController; - if (masterTabBarController.currentGroupDetailViewController) + if (masterTabBarController.selectedGroup) { // Look for the rank of this selected group in displayed groups currentSelectedCellIndexPath = [self.dataSource cellIndexPathWithGroupId:masterTabBarController.selectedGroup.groupId]; diff --git a/Riot/Modules/Communities/Home/GroupHomeViewController.m b/Riot/Modules/Communities/Home/GroupHomeViewController.m index 3f0d8ac5f..5fbfbf82e 100644 --- a/Riot/Modules/Communities/Home/GroupHomeViewController.m +++ b/Riot/Modules/Communities/Home/GroupHomeViewController.m @@ -807,7 +807,7 @@ contact = [[MXKContact alloc] initMatrixContactWithDisplayName:userId andMatrixID:userId]; } - ContactDetailsViewController *contactDetailsViewController = [ContactDetailsViewController contactDetailsViewController]; + ContactDetailsViewController *contactDetailsViewController = [ContactDetailsViewController instantiate]; contactDetailsViewController.enableVoipCall = NO; contactDetailsViewController.contact = contact; diff --git a/Riot/Modules/Communities/Members/GroupParticipantsViewController.m b/Riot/Modules/Communities/Members/GroupParticipantsViewController.m index 34aa57c48..f46a9cf72 100644 --- a/Riot/Modules/Communities/Members/GroupParticipantsViewController.m +++ b/Riot/Modules/Communities/Members/GroupParticipantsViewController.m @@ -964,7 +964,7 @@ if (contact) { - ContactDetailsViewController *contactDetailsViewController = [ContactDetailsViewController contactDetailsViewController]; + ContactDetailsViewController *contactDetailsViewController = [ContactDetailsViewController instantiate]; contactDetailsViewController.enableVoipCall = NO; contactDetailsViewController.contact = contact; diff --git a/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinator.swift b/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinator.swift new file mode 100644 index 000000000..27c3d7df0 --- /dev/null +++ b/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinator.swift @@ -0,0 +1,55 @@ +// File created from ScreenTemplate +// $ createScreen.sh Communities GroupDetails +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +import UIKit + +final class GroupDetailsCoordinator: GroupDetailsCoordinatorProtocol { + + // MARK: - Properties + + // MARK: Private + + private let parameters: GroupDetailsCoordinatorParameters + private let groupDetailsViewController: GroupDetailsViewController + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + weak var delegate: GroupDetailsCoordinatorDelegate? + + // MARK: - Setup + + init(parameters: GroupDetailsCoordinatorParameters) { + self.parameters = parameters + let groupDetailsViewController: GroupDetailsViewController = GroupDetailsViewController.instantiate() + self.groupDetailsViewController = groupDetailsViewController + } + + // MARK: - Public + + func start() { + self.groupDetailsViewController.setGroup(self.parameters.group, withMatrixSession: self.parameters.session) + } + + func toPresentable() -> UIViewController { + return self.groupDetailsViewController + } +} diff --git a/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinatorParameters.swift b/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinatorParameters.swift new file mode 100644 index 000000000..8ff866d7a --- /dev/null +++ b/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinatorParameters.swift @@ -0,0 +1,29 @@ +// File created from ScreenTemplate +// $ createScreen.sh Communities GroupDetails +/* + 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 + +/// GroupDetailsCoordinator input parameters +struct GroupDetailsCoordinatorParameters { + + /// The Matrix session + let session: MXSession + + /// The group for which the details are displayed + let group: MXGroup +} diff --git a/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinatorProtocol.swift b/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinatorProtocol.swift new file mode 100644 index 000000000..fbb3f9ff8 --- /dev/null +++ b/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinatorProtocol.swift @@ -0,0 +1,28 @@ +// File created from ScreenTemplate +// $ createScreen.sh Communities GroupDetails +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +protocol GroupDetailsCoordinatorDelegate: AnyObject { + func groupDetailsCoordinatorDidCancel(_ coordinator: GroupDetailsCoordinatorProtocol) +} + +/// `GroupDetailsCoordinatorProtocol` is a protocol describing a Coordinator that handle communities navigation flow. +protocol GroupDetailsCoordinatorProtocol: Coordinator, Presentable { + var delegate: GroupDetailsCoordinatorDelegate? { get } +} diff --git a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.h b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.h index 74223bed3..e228101d4 100644 --- a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.h +++ b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.h @@ -35,7 +35,7 @@ @discussion This is the designated initializer for programmatic instantiation. @return An initialized `GroupDetailsViewController` object if successful, `nil` otherwise. */ -+ (instancetype)groupDetailsViewController; ++ (instancetype)instantiate; /** Set the group for which the details are displayed. diff --git a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m index 14752a8ff..c36120cc4 100644 --- a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m +++ b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m @@ -55,7 +55,7 @@ bundle:[NSBundle bundleForClass:self.class]]; } -+ (instancetype)groupDetailsViewController ++ (instancetype)instantiate { return [[[self class] alloc] initWithNibName:NSStringFromClass(self.class) bundle:[NSBundle bundleForClass:self.class]]; @@ -117,6 +117,9 @@ [self initWithTitles:titles viewControllers:viewControllers defaultSelected:0]; [super viewDidLoad]; + + // Display leftBarButtonItems or leftBarButtonItem to the right of the Back button + self.navigationItem.leftItemsSupplementBackButton = YES; } - (UIStatusBarStyle)preferredStatusBarStyle diff --git a/Riot/Modules/Contacts/ContactsTableViewController.h b/Riot/Modules/Contacts/ContactsTableViewController.h index a196bd788..8b535495b 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.h +++ b/Riot/Modules/Contacts/ContactsTableViewController.h @@ -66,6 +66,19 @@ */ @property (weak, nonatomic) IBOutlet UITableView *contactsTableView; +/** + When true, the footer that allows the user to enable local contacts sync will + never be shown. When false, the footer will shown when the user hasn't enabled + contact sync. + */ +@property (nonatomic) BOOL disableFindYourContactsFooter; + +/** + Indicates when there's an active search. This is used to determine when the contacts + access footer should be hidden in order to list the results from the server. + */ +@property (nonatomic) BOOL contactsAreFilteredWithSearch; + /** If YES, the table view will scroll at the top on the next data source refresh. It comes back to NO after each refresh. diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index 00c84c3ba..adbc1f9e9 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -28,7 +28,7 @@ #define CONTACTS_TABLEVC_DEFAULT_SECTION_HEADER_HEIGHT 30.0 #define CONTACTS_TABLEVC_LOCALCONTACTS_SECTION_HEADER_HEIGHT 65.0 -@interface ContactsTableViewController () +@interface ContactsTableViewController () { /** Observe kAppDelegateDidTapStatusBarNotification to handle tap on clock status bar. @@ -41,6 +41,10 @@ id kThemeServiceDidChangeThemeNotificationObserver; } +@property (nonatomic, strong) FindYourContactsFooterView *findYourContactsFooterView; + +@property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter; + @end @implementation ContactsTableViewController @@ -65,6 +69,10 @@ { [super finalizeInit]; + // By default, allow the find your contacts footer to be + // shown when local contacts sync hasn't been enabled. + self.disableFindYourContactsFooter = NO; + // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; @@ -92,6 +100,7 @@ // Hide line separators of empty cells self.contactsTableView.tableFooterView = [[UIView alloc] init]; + self.contactsAreFilteredWithSearch = NO; // Observe user interface theme change. kThemeServiceDidChangeThemeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kThemeServiceDidChangeThemeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -150,20 +159,6 @@ // Screen tracking [[Analytics sharedInstance] trackScreen:_screenName]; - if (BuildSettings.allowLocalContactsAccess) - { - // Check whether the access to the local contacts has not been already asked - // and check that the user has decided to use or not to use an identity server - if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusNotDetermined - || !contactsDataSource.mxSession.hasAccountDataIdentityServerValue) - { - // Allow by default the local contacts sync in order to discover matrix users. - // This setting change will trigger the loading of the local contacts, which will automatically - // ask user permission to access their local contacts. - [MXKAppSettings standardAppSettings].syncLocalContacts = YES; - } - } - // Observe kAppDelegateDidTapStatusBarNotification. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -171,16 +166,18 @@ }]; + // Load the local contacts for display. + [self refreshLocalContacts]; [self refreshContactsTable]; + + // Show the contacts access footer if necessary. + [self updateFooterViewVisibility]; } -- (void)viewDidAppear:(BOOL)animated +- (void)viewDidLayoutSubviews { - [super viewDidAppear:animated]; - - // Load the local contacts for display. - // In viewDidAppear as it may trigger a request for contacts access. - [self refreshLocalContacts]; + [super viewDidLayoutSubviews]; + [self updateFooterViewHeight]; } - (void)viewWillDisappear:(BOOL)animated @@ -203,6 +200,83 @@ #pragma mark - +/** + Creates a new `FindYourContactsFooterView` and caches it in + the `findYourContactsFooterView` property before returning it for use. + */ +- (FindYourContactsFooterView*)makeFooterView +{ + FindYourContactsFooterView *footerView = [FindYourContactsFooterView instantiate]; + footerView.delegate = self; + + self.findYourContactsFooterView = footerView; + + return footerView; +} + +/** + Checks whether local contacts sync is ready to use or if there are any search results + in the table, hiding the find your contacts footer if so. Otherwise the footer is shown + so long as it hasn't been disabled. + */ +- (void)updateFooterViewVisibility +{ + if (!BuildSettings.allowLocalContactsAccess || self.disableFindYourContactsFooter) + { + self.contactsTableView.tableFooterView = [[UIView alloc] init]; + return; + } + + // With contacts access granted, contact sync enabled and an identity server, the footer can be hidden. + if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized + && MXKAppSettings.standardAppSettings.syncLocalContacts + && contactsDataSource.mxSession.identityService.areAllTermsAgreed) + { + self.contactsTableView.tableFooterView = [[UIView alloc] init]; + return; + } + + // If the footer is to be shown, hide it when there's an active search. + if (self.contactsAreFilteredWithSearch) + { + self.contactsTableView.tableFooterView = [[UIView alloc] init]; + return; + } + + self.contactsTableView.tableFooterView = self.findYourContactsFooterView ?: [self makeFooterView]; + [self updateFooterViewHeight]; +} + +/** + Updates the height of the find your contacts footer to fill all available space. + */ +- (void)updateFooterViewHeight +{ + if (self.findYourContactsFooterView && self.findYourContactsFooterView == self.contactsTableView.tableFooterView) + { + // Calculate the natural size of the footer + CGSize fittingSize = CGSizeMake(self.view.frame.size.width, UILayoutFittingCompressedSize.height); + CGSize footerSize = [self.findYourContactsFooterView systemLayoutSizeFittingSize:fittingSize]; + + // Calculate the height available for the footer + CGFloat availableHeight = self.contactsTableView.bounds.size.height - self.contactsTableView.adjustedContentInset.top - self.contactsTableView.adjustedContentInset.bottom; + if (self.contactsTableView.tableHeaderView) + { + availableHeight -= self.contactsTableView.tableHeaderView.frame.size.height; + } + + // Fill all available height unless the footer is larger, in which case use its natural height + CGFloat finalHeight = availableHeight > footerSize.height ? availableHeight : footerSize.height; + self.findYourContactsFooterView.frame = CGRectMake(self.findYourContactsFooterView.frame.origin.x, + self.findYourContactsFooterView.frame.origin.y, + self.findYourContactsFooterView.frame.size.width, + finalHeight); + + // This assignment is technically redundant, but does prompt the table view to recalculate its content size + self.contactsTableView.tableFooterView = self.findYourContactsFooterView; + } +} + - (void)displayList:(ContactsDataSource*)listDataSource { // Cancel registration on existing dataSource if any @@ -228,42 +302,10 @@ return; } - // Do not scan local contacts in background if the user has not decided yet about using - // an identity server - BOOL doRefreshLocalContacts = NO; - for (MXSession *session in self.mxSessions) - { - if (session.hasAccountDataIdentityServerValue) - { - doRefreshLocalContacts = YES; - break; - } - } - - // Check whether the application is allowed to access the local contacts. - if (doRefreshLocalContacts + if (MXKAppSettings.standardAppSettings.syncLocalContacts + && contactsDataSource.mxSession.identityService.areAllTermsAgreed && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) { - // Check the user permission for syncing local contacts. This permission was handled independently on previous application version. - if (![MXKAppSettings standardAppSettings].syncLocalContacts) - { - // Check whether it was not requested yet. - if (![MXKAppSettings standardAppSettings].syncLocalContactsPermissionRequested) - { - [MXKAppSettings standardAppSettings].syncLocalContactsPermissionRequested = YES; - - [MXKContactManager requestUserConfirmationForLocalContactsSyncInViewController:self completionHandler:^(BOOL granted) { - - if (granted) - { - // Allow local contacts sync in order to discover matrix users. - [MXKAppSettings standardAppSettings].syncLocalContacts = YES; - } - - }]; - } - } - // Refresh the local contacts list. [[MXKContactManager sharedManager] refreshLocalContacts]; } @@ -292,7 +334,7 @@ // Update here the index of the current selected cell (if any) - Useful in landscape mode with split view controller. NSIndexPath *currentSelectedCellIndexPath = nil; MasterTabBarController *masterTabBarController = [AppDelegate theDelegate].masterTabBarController; - if (masterTabBarController.currentContactDetailViewController) + if (masterTabBarController.selectedContact) { // Look for the rank of this selected contact in displayed recents currentSelectedCellIndexPath = [contactsDataSource cellIndexPathWithContact:masterTabBarController.selectedContact]; @@ -321,6 +363,16 @@ } } +- (void)setContactsAreFilteredWithSearch:(BOOL)contactsAreFilteredWithSearch +{ + // Filter out redundant assignments. + if (_contactsAreFilteredWithSearch != contactsAreFilteredWithSearch) + { + _contactsAreFilteredWithSearch = contactsAreFilteredWithSearch; + [self updateFooterViewVisibility]; + } +} + #pragma mark - MXKDataSourceDelegate - (Class)cellViewClassForCellData:(MXKCellData*)cellData @@ -426,6 +478,8 @@ - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { [contactsDataSource searchWithPattern:searchText forceReset:NO]; + + self.contactsAreFilteredWithSearch = searchText.length ? YES : NO; } - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar @@ -460,4 +514,126 @@ [self withdrawViewControllerAnimated:YES completion:nil]; } +#pragma mark - FindYourContactsFooterViewDelegate + +- (void)contactsFooterViewDidRequestFindContacts:(FindYourContactsFooterView *)footerView +{ + // First check the identity if service terms have already been accepted + if (self->contactsDataSource.mxSession.identityService.areAllTermsAgreed) + { + // If they have we only require local contacts access. + [self checkAccessForContacts]; + } + else + { + MXWeakify(self); + + // The preparation can take some time so indicate this to the user + [self startActivityIndicator]; + footerView.isActionEnabled = NO; + + [self->contactsDataSource.mxSession prepareIdentityServiceForTermsWithDefault:RiotSettings.shared.identityServerUrlString + success:^(MXSession *session, NSString *baseURL, NSString *accessToken) { + MXStrongifyAndReturnIfNil(self); + + [self stopActivityIndicator]; + footerView.isActionEnabled = YES; + + // Present the terms of the identity server. + [self presentIdentityServerTermsWithSession:session baseURL:baseURL andAccessToken:accessToken]; + } failure:^(NSError *error) { + // The error was already logged before the block is called + MXStrongifyAndReturnIfNil(self); + + [self stopActivityIndicator]; + footerView.isActionEnabled = YES; + + // Alert the user that something went wrong. + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:VectorL10n.findYourContactsIdentityServiceError + message:nil + preferredStyle:UIAlertControllerStyleAlert]; + + [alertController addAction:[UIAlertAction actionWithTitle:MatrixKitL10n.ok + style:UIAlertActionStyleDefault + handler:nil]]; + + [self presentViewController:alertController animated:YES completion:nil]; + }]; + } +} + + - (void)checkAccessForContacts +{ + MXWeakify(self); + + // Check for contacts access, showing a pop-up if necessary. + [MXKTools checkAccessForContacts:VectorL10n.contactsAddressBookPermissionDeniedAlertTitle + withManualChangeMessage:VectorL10n.contactsAddressBookPermissionDeniedAlertMessage + showPopUpInViewController:self + completionHandler:^(BOOL granted) { + + MXStrongifyAndReturnIfNil(self); + + if (granted) + { + // When granted, local contacts can be shown. + [self showLocalContacts]; + } + }]; +} + +- (void)showLocalContacts +{ + // Enable local contacts sync and display. + MXKAppSettings.standardAppSettings.syncLocalContacts = YES; + self->contactsDataSource.showLocalContacts = YES; + + // Attempt to refresh the contacts manager. + [self refreshLocalContacts]; + + // Hide the find your contacts footer. + [self updateFooterViewVisibility]; +} + +#pragma mark - Identity server service terms + +- (void)presentIdentityServerTermsWithSession:(MXSession*)mxSession baseURL:(NSString*)baseURL andAccessToken:(NSString*)accessToken +{ + if (!mxSession || !baseURL || !accessToken || self.serviceTermsModalCoordinatorBridgePresenter.isPresenting) + { + return; + } + + ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:mxSession + baseUrl:baseURL + serviceType:MXServiceTypeIdentityService + accessToken:accessToken]; + + serviceTermsModalCoordinatorBridgePresenter.delegate = self; + + [serviceTermsModalCoordinatorBridgePresenter presentFrom:self animated:YES]; + self.serviceTermsModalCoordinatorBridgePresenter = serviceTermsModalCoordinatorBridgePresenter; +} + +#pragma mark ServiceTermsModalCoordinatorBridgePresenterDelegate + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ + [self checkAccessForContacts]; + }]; + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter *)coordinatorBridgePresenter session:(MXSession *)session +{ + [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidClose:(ServiceTermsModalCoordinatorBridgePresenter *)coordinatorBridgePresenter +{ + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/Contacts/DataSources/ContactsDataSource.h b/Riot/Modules/Contacts/DataSources/ContactsDataSource.h index 4e8005f0c..ea6db9c2e 100644 --- a/Riot/Modules/Contacts/DataSources/ContactsDataSource.h +++ b/Riot/Modules/Contacts/DataSources/ContactsDataSource.h @@ -50,6 +50,14 @@ typedef enum : NSUInteger NSMutableArray *filteredMatrixContacts; } +/** + Whether the data source should include local contacts in the table view. The default + value is set at initialisation to match the `MXKAppSettings` value for `syncLocalContacts`. + Note: After updating this property, the table view's data will need to be reloaded for it to have + any effect. + */ +@property (nonatomic) BOOL showLocalContacts; + /** Get the contact at the given index path. diff --git a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m index 6048304c0..15d0b6238 100644 --- a/Riot/Modules/Contacts/DataSources/ContactsDataSource.m +++ b/Riot/Modules/Contacts/DataSources/ContactsDataSource.m @@ -92,6 +92,16 @@ return self; } +- (instancetype)initWithMatrixSession:(MXSession *)mxSession +{ + self = [super initWithMatrixSession:mxSession]; + if (self) { + // Only show local contacts when contact sync is enabled and the identity server terms of service have been accepted. + _showLocalContacts = MXKAppSettings.standardAppSettings.syncLocalContacts && self.mxSession.identityService.areAllTermsAgreed; + } + return self; +} + - (void)destroy { [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKContactManagerDidUpdateMatrixContactsNotification object:nil]; @@ -473,8 +483,8 @@ searchInputSection = count++; } - // Keep visible the header for the both contact sections, even if their are empty. - if (BuildSettings.allowLocalContactsAccess) + // Keep visible the header for the both contact sections, even if they're are empty. + if (BuildSettings.allowLocalContactsAccess && self.showLocalContacts && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) { filteredLocalContactsSection = count++; } @@ -489,7 +499,7 @@ } // Keep visible the local contact header, even if the section is empty. - if (BuildSettings.allowLocalContactsAccess) + if (BuildSettings.allowLocalContactsAccess && self.showLocalContacts && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) { filteredLocalContactsSection = count++; } diff --git a/Riot/Modules/Contacts/Details/ContactDetailsCoordinator.swift b/Riot/Modules/Contacts/Details/ContactDetailsCoordinator.swift new file mode 100644 index 000000000..a5476fbc1 --- /dev/null +++ b/Riot/Modules/Contacts/Details/ContactDetailsCoordinator.swift @@ -0,0 +1,56 @@ +// File created from ScreenTemplate +// $ createScreen.sh Contacts ContactDetails +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +import UIKit + +final class ContactDetailsCoordinator: ContactDetailsCoordinatorProtocol { + + // MARK: - Properties + + // MARK: Private + + private let parameters: ContactDetailsCoordinatorParameters + private let contactDetailsViewController: ContactDetailsViewController + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + weak var delegate: ContactDetailsCoordinatorDelegate? + + // MARK: - Setup + + init(parameters: ContactDetailsCoordinatorParameters) { + self.parameters = parameters + let contactDetailsViewController: ContactDetailsViewController = ContactDetailsViewController.instantiate() + contactDetailsViewController.contact = self.parameters.contact + contactDetailsViewController.enableVoipCall = self.parameters.enableVoipCall + self.contactDetailsViewController = contactDetailsViewController + } + + // MARK: - Public + + func start() { + } + + func toPresentable() -> UIViewController { + return self.contactDetailsViewController + } +} diff --git a/Riot/Modules/Contacts/Details/ContactDetailsCoordinatorParameters.swift b/Riot/Modules/Contacts/Details/ContactDetailsCoordinatorParameters.swift new file mode 100644 index 000000000..a3bc00f76 --- /dev/null +++ b/Riot/Modules/Contacts/Details/ContactDetailsCoordinatorParameters.swift @@ -0,0 +1,35 @@ +// File created from ScreenTemplate +// $ createScreen.sh Contacts ContactDetails +/* + 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 + +/// ContactDetailsCoordinator input parameters +struct ContactDetailsCoordinatorParameters { + + /// The displayed contact + let contact: MXKContact + + /// Enable voip call (voice/video). NO by default + let enableVoipCall: Bool + + init(contact: MXKContact, + enableVoipCall: Bool = false) { + self.contact = contact + self.enableVoipCall = enableVoipCall + } +} diff --git a/Riot/Modules/Contacts/Details/ContactDetailsCoordinatorProtocol.swift b/Riot/Modules/Contacts/Details/ContactDetailsCoordinatorProtocol.swift new file mode 100644 index 000000000..57d73fc36 --- /dev/null +++ b/Riot/Modules/Contacts/Details/ContactDetailsCoordinatorProtocol.swift @@ -0,0 +1,28 @@ +// File created from ScreenTemplate +// $ createScreen.sh Contacts ContactDetails +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +protocol ContactDetailsCoordinatorDelegate: AnyObject { + func contactDetailsCoordinatorDidCancel(_ coordinator: ContactDetailsCoordinatorProtocol) +} + +/// `ContactDetailsCoordinatorProtocol` is a protocol describing a Coordinator that handle contact details navigation flow. +protocol ContactDetailsCoordinatorProtocol: Coordinator, Presentable { + var delegate: ContactDetailsCoordinatorDelegate? { get } +} diff --git a/Riot/Modules/Contacts/Details/ContactDetailsViewController.h b/Riot/Modules/Contacts/Details/ContactDetailsViewController.h index f00dbe3af..a41a8df84 100644 --- a/Riot/Modules/Contacts/Details/ContactDetailsViewController.h +++ b/Riot/Modules/Contacts/Details/ContactDetailsViewController.h @@ -69,7 +69,7 @@ typedef enum : NSUInteger @discussion This is the designated initializer for programmatic instantiation. @return An initialized `ContactDetailsViewController` object if successful, `nil` otherwise. */ -+ (instancetype)contactDetailsViewController; ++ (instancetype)instantiate; @end diff --git a/Riot/Modules/Contacts/Details/ContactDetailsViewController.m b/Riot/Modules/Contacts/Details/ContactDetailsViewController.m index 990b411ea..e26c94ab1 100644 --- a/Riot/Modules/Contacts/Details/ContactDetailsViewController.m +++ b/Riot/Modules/Contacts/Details/ContactDetailsViewController.m @@ -98,7 +98,7 @@ bundle:[NSBundle bundleForClass:self.class]]; } -+ (instancetype)contactDetailsViewController ++ (instancetype)instantiate { return [[[self class] alloc] initWithNibName:NSStringFromClass(self.class) bundle:[NSBundle bundleForClass:self.class]]; @@ -142,6 +142,9 @@ // Define directly the navigation titleView with the custom title view instance. Do not use anymore a container. self.navigationItem.titleView = contactTitleView; + // Display leftBarButtonItems or leftBarButtonItem to the right of the Back button + self.navigationItem.leftItemsSupplementBackButton = YES; + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; [tap setNumberOfTouchesRequired:1]; [tap setNumberOfTapsRequired:1]; diff --git a/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift b/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift new file mode 100644 index 000000000..ba0e9cf8a --- /dev/null +++ b/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift @@ -0,0 +1,87 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import Reusable + +@objc protocol FindYourContactsFooterViewDelegate { + func contactsFooterViewDidRequestFindContacts(_ footerView: FindYourContactsFooterView) +} + +@objcMembers +class FindYourContactsFooterView: UIView, NibLoadable, Themable { + + // MARK: - Properties + + weak var delegate: FindYourContactsFooterViewDelegate? + + /// Whether or not the view's button responds to taps. + var isActionEnabled: Bool { + get { button.isEnabled } + set { button.isEnabled = newValue } + } + + @IBOutlet weak private var containerView: UIView! + @IBOutlet weak private var titleLabel: UILabel! + @IBOutlet weak private var messageLabel: UILabel! + @IBOutlet weak private var button: CustomRoundedButton! + @IBOutlet weak private var footerLabel: UILabel! + + // MARK: - Setup + + static func instantiate() -> Self { + let view = Self.loadFromNib() + view.update(theme: ThemeService.shared().theme) + return view + } + + override func awakeFromNib() { + super.awakeFromNib() + + containerView.layer.cornerRadius = 8 + button.layer.cornerRadius = 8 + + titleLabel.text = VectorL10n.findYourContactsTitle + messageLabel.text = VectorL10n.findYourContactsMessage(BuildSettings.bundleDisplayName) + button.setTitle(VectorL10n.findYourContactsButtonTitle, for: .normal) + footerLabel.text = VectorL10n.findYourContactsFooter + } + + func update(theme: Theme) { + tintColor = theme.colors.accent + + containerView.backgroundColor = theme.colors.quinaryContent + + titleLabel.font = theme.fonts.bodySB + titleLabel.textColor = theme.colors.primaryContent + + messageLabel.font = theme.fonts.body + messageLabel.textColor = theme.colors.secondaryContent + + button.titleLabel?.font = theme.fonts.body + button.backgroundColor = theme.colors.accent + button.setTitleColor(theme.colors.background, for: .normal) + + footerLabel.font = theme.fonts.footnote.withSize(13) + footerLabel.textColor = theme.colors.tertiaryContent + } + + // MARK: - Action + + @IBAction private func buttonAction(_ sender: Any) { + delegate?.contactsFooterViewDidRequestFindContacts(self) + } +} diff --git a/Riot/Modules/Contacts/Views/FindYourContactsFooterView.xib b/Riot/Modules/Contacts/Views/FindYourContactsFooterView.xib new file mode 100644 index 000000000..86cc2734b --- /dev/null +++ b/Riot/Modules/Contacts/Views/FindYourContactsFooterView.xib @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m index 1de1f5b4b..502bb3754 100644 --- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m +++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m @@ -243,7 +243,7 @@ // Update here the index of the current selected cell (if any) - Useful in landscape mode with split view controller. NSIndexPath *currentSelectedCellIndexPath = nil; - if (masterTabBarController.currentRoomViewController) + if (masterTabBarController.selectedRoomId) { // Look for the rank of this selected room in displayed recents currentSelectedCellIndexPath = [dataSource cellIndexPathWithRoomId:masterTabBarController.selectedRoomId andMatrixSession:masterTabBarController.selectedRoomSession]; diff --git a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m index 4096b31b6..03375e8b9 100644 --- a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m +++ b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m @@ -91,6 +91,7 @@ [titles addObject:[VectorL10n searchPeople]]; peopleSearchViewController = [ContactsTableViewController contactsTableViewController]; peopleSearchViewController.contactsTableViewControllerDelegate = self; + peopleSearchViewController.disableFindYourContactsFooter = YES; [viewControllers addObject:peopleSearchViewController]; // add Files tab @@ -247,6 +248,7 @@ // Init the search for people peopleSearchDataSource = [[ContactsDataSource alloc] initWithMatrixSession:mainSession]; + peopleSearchDataSource.showLocalContacts = NO; peopleSearchDataSource.areSectionsShrinkable = YES; peopleSearchDataSource.displaySearchInputInContactsList = YES; peopleSearchDataSource.contactCellAccessoryImage = [[UIImage imageNamed: @"disclosure_icon"] vc_tintedImageUsingColor:ThemeService.shared.theme.textSecondaryColor];; diff --git a/Riot/Modules/Home/Views/RoomCollectionViewCell.m b/Riot/Modules/Home/Views/RoomCollectionViewCell.m index d40fd4d1c..a030b9a6f 100644 --- a/Riot/Modules/Home/Views/RoomCollectionViewCell.m +++ b/Riot/Modules/Home/Views/RoomCollectionViewCell.m @@ -131,6 +131,7 @@ } [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.avatarUrl + roomId:roomCellData.roomIdentifier displayName:roomCellData.roomDisplayname mediaManager:roomCellData.mxSession.mediaManager]; } diff --git a/Riot/Modules/Integrations/IntegrationManagerViewController.m b/Riot/Modules/Integrations/IntegrationManagerViewController.m index 596e4f8c2..7ba021062 100644 --- a/Riot/Modules/Integrations/IntegrationManagerViewController.m +++ b/Riot/Modules/Integrations/IntegrationManagerViewController.m @@ -745,7 +745,6 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:mxSession baseUrl:config.baseUrl serviceType:MXServiceTypeIntegrationManager - outOfContext:NO accessToken:config.scalarToken]; serviceTermsModalCoordinatorBridgePresenter.delegate = self; @@ -762,14 +761,6 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; self.serviceTermsModalCoordinatorBridgePresenter = nil; } -- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter -{ - [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - [self withdrawViewControllerAnimated:YES completion:nil]; - }]; - self.serviceTermsModalCoordinatorBridgePresenter = nil; -} - - (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter session:(MXSession * _Nonnull)session { [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ @@ -778,4 +769,9 @@ NSString *const kIntegrationManagerAddIntegrationScreen = @"add_integ"; self.serviceTermsModalCoordinatorBridgePresenter = nil; } +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidClose:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m b/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m index 7d146fd07..d3cb31902 100644 --- a/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m +++ b/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m @@ -151,7 +151,6 @@ ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:widget.mxSession baseUrl:config.baseUrl serviceType:MXServiceTypeIntegrationManager - outOfContext:NO accessToken:config.scalarToken]; serviceTermsModalCoordinatorBridgePresenter.delegate = self; @@ -173,16 +172,15 @@ self.serviceTermsModalCoordinatorBridgePresenter = nil; } -- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter -{ - [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; - self.serviceTermsModalCoordinatorBridgePresenter = nil; -} - - (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter session:(MXSession * _Nonnull)session { [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; self.serviceTermsModalCoordinatorBridgePresenter = nil; } +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidClose:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/Integrations/Widgets/WidgetViewController.m b/Riot/Modules/Integrations/Widgets/WidgetViewController.m index bb0def197..6d04db4c0 100644 --- a/Riot/Modules/Integrations/Widgets/WidgetViewController.m +++ b/Riot/Modules/Integrations/Widgets/WidgetViewController.m @@ -656,8 +656,7 @@ NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse MXLogDebug(@"[WidgetVC] presentTerms for %@", config.baseUrl); ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:widget.mxSession baseUrl:config.baseUrl - serviceType:MXServiceTypeIntegrationManager - outOfContext:NO + serviceType:MXServiceTypeIntegrationManager accessToken:config.scalarToken]; serviceTermsModalCoordinatorBridgePresenter.delegate = self; @@ -677,14 +676,6 @@ NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse self.serviceTermsModalCoordinatorBridgePresenter = nil; } -- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter -{ - [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - [self withdrawViewControllerAnimated:YES completion:nil]; - }]; - self.serviceTermsModalCoordinatorBridgePresenter = nil; -} - - (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter session:(MXSession * _Nonnull)session { [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ @@ -693,4 +684,9 @@ NSString *const kJavascriptSendResponseToPostMessageAPI = @"riotIOS.sendResponse self.serviceTermsModalCoordinatorBridgePresenter = nil; } +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidClose:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift b/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift index f1d98baa0..a06e9befd 100644 --- a/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift +++ b/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift @@ -61,7 +61,7 @@ final class KeyBackupRecoverCoordinatorBridgePresenter: NSObject { MXLog.debug("[KeyBackupRecoverCoordinatorBridgePresenter] Push complete security from \(navigationController)") - let navigationRouter = NavigationRouter(navigationController: navigationController) + let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) let keyBackupSetupCoordinator = KeyBackupRecoverCoordinator(session: self.session, keyBackupVersion: keyBackupVersion, navigationRouter: navigationRouter) keyBackupSetupCoordinator.delegate = self diff --git a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift index d9d4a20c1..5d8ead04c 100644 --- a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift +++ b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift @@ -101,7 +101,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject { MXLog.debug("[KeyVerificationCoordinatorBridgePresenter] Push complete security from \(navigationController)") - let navigationRouter = NavigationRouter(navigationController: navigationController) + let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .completeSecurity(isNewSignIn), navigationRouter: navigationRouter) keyVerificationCoordinator.delegate = self diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift new file mode 100644 index 000000000..ce8f41ef7 --- /dev/null +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -0,0 +1,242 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room Room +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation +import UIKit + +final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { + + // MARK: - Properties + + // MARK: Private + + private let parameters: RoomCoordinatorParameters + private let roomViewController: RoomViewController + private let activityIndicatorPresenter: ActivityIndicatorPresenterType + private var selectedEventId: String? + + private var roomDataSourceManager: MXKRoomDataSourceManager { + return MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.parameters.session) + } + + /// Indicate true if the Coordinator has started once + private var hasStartedOnce: Bool { + return self.roomViewController.delegate != nil + } + + private var navigationRouter: NavigationRouterType? { + + var finalNavigationRouter: NavigationRouterType? + + if let navigationRouter = self.parameters.navigationRouter { + finalNavigationRouter = navigationRouter + } else if let navigationRouterStore = self.parameters.navigationRouterStore, let currentNavigationController = self.roomViewController.navigationController { + // If no navigationRouter has been provided, try to get the navigation router from the current RoomViewController navigation controller if exists + finalNavigationRouter = navigationRouterStore.navigationRouter(for: currentNavigationController) + } + + return finalNavigationRouter + } + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + weak var delegate: RoomCoordinatorDelegate? + + var canReleaseRoomDataSource: Bool { + // If the displayed data is not a preview, let the manager release the room data source + // (except if the view controller has the room data source ownership). + return self.parameters.previewData == nil && self.roomViewController.roomDataSource != nil && self.roomViewController.hasRoomDataSourceOwnership == false + } + + // MARK: - Setup + + init(parameters: RoomCoordinatorParameters) { + self.parameters = parameters + self.selectedEventId = parameters.eventId + + self.roomViewController = RoomViewController.instantiate() + self.activityIndicatorPresenter = ActivityIndicatorPresenter() + + super.init() + } + + // MARK: - Public + + func start() { + self.start(withCompletion: nil) + } + + // NOTE: Completion closure has been added for legacy architecture purpose. + // Remove this completion after LegacyAppDelegate refactor. + func start(withCompletion completion: (() -> Void)?) { + self.roomViewController.delegate = self + + // Detect when view controller has been dismissed by gesture when presented modally (not in full screen). + self.roomViewController.presentationController?.delegate = self + + if let eventId = self.selectedEventId { + self.loadRoom(withId: self.parameters.roomId, and: eventId, completion: completion) + } else { + self.loadRoom(withId: self.parameters.roomId, completion: completion) + } + + // Add `roomViewController` to the NavigationRouter, only if it has been explicitly set as parameter + if let navigationRouter = self.parameters.navigationRouter { + if navigationRouter.modules.isEmpty == false { + navigationRouter.push(self.roomViewController, animated: true, popCompletion: nil) + } else { + navigationRouter.setRootModule(self.roomViewController, popCompletion: nil) + } + } + } + + func start(withEventId eventId: String, completion: (() -> Void)?) { + + self.selectedEventId = eventId + + if self.hasStartedOnce { + self.loadRoom(withId: self.parameters.roomId, and: eventId, completion: completion) + } else { + self.start(withCompletion: completion) + } + } + + func toPresentable() -> UIViewController { + return self.roomViewController + } + + // MARK: - Private + + private func loadRoom(withId roomId: String, completion: (() -> Void)?) { + + // Present activity indicator when retrieving roomDataSource for given room ID + self.activityIndicatorPresenter.presentActivityIndicator(on: roomViewController.view, animated: false) + + let roomDataSourceManager: MXKRoomDataSourceManager = MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.parameters.session) + + // LIVE: Show the room live timeline managed by MXKRoomDataSourceManager + roomDataSourceManager.roomDataSource(forRoom: roomId, create: true, onComplete: { [weak self] (roomDataSource) in + + guard let self = self else { + return + } + + self.activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true) + + if let roomDataSource = roomDataSource { + self.roomViewController.displayRoom(roomDataSource) + } + + completion?() + }) + } + + private func loadRoom(withId roomId: String, and eventId: String, completion: (() -> Void)?) { + + // Present activity indicator when retrieving roomDataSource for given room ID + self.activityIndicatorPresenter.presentActivityIndicator(on: roomViewController.view, animated: false) + + // Open the room on the requested event + RoomDataSource.load(withRoomId: roomId, + initialEventId: eventId, + andMatrixSession: self.parameters.session) { [weak self] (dataSource) in + + guard let self = self else { + return + } + + self.activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true) + + guard let roomDataSource = dataSource as? RoomDataSource else { + return + } + + roomDataSource.markTimelineInitialEvent = true + self.roomViewController.displayRoom(roomDataSource) + + // Give the data source ownership to the room view controller. + self.roomViewController.hasRoomDataSourceOwnership = true + + completion?() + } + } +} + +// MARK: - RoomIdentifiable +extension RoomCoordinator: RoomIdentifiable { + + var roomId: String? { + return self.parameters.roomId + } + + var mxSession: MXSession? { + self.parameters.session + } +} + +// MARK: - UIAdaptivePresentationControllerDelegate + +extension RoomCoordinator: UIAdaptivePresentationControllerDelegate { + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + self.delegate?.roomCoordinatorDidDismissInteractively(self) + } +} + +// MARK: - RoomViewControllerDelegate +extension RoomCoordinator: RoomViewControllerDelegate { + + func roomViewController(_ roomViewController: RoomViewController, showRoomWithId roomID: String) { + self.delegate?.roomCoordinator(self, didSelectRoomWithId: roomID) + } + + func roomViewController(_ roomViewController: RoomViewController, showMemberDetails roomMember: MXRoomMember) { + // TODO: + } + + func roomViewControllerShowRoomDetails(_ roomViewController: RoomViewController) { + // TODO: + } + + func roomViewControllerDidLeaveRoom(_ roomViewController: RoomViewController) { + self.delegate?.roomCoordinatorDidLeaveRoom(self) + } + + func roomViewControllerPreviewDidTapCancel(_ roomViewController: RoomViewController) { + self.delegate?.roomCoordinatorDidCancelRoomPreview(self) + } + + func roomViewController(_ roomViewController: RoomViewController, startChatWithUserId userId: String, completion: @escaping () -> Void) { + AppDelegate.theDelegate().createDirectChat(withUserId: userId, completion: completion) + } + + func roomViewController(_ roomViewController: RoomViewController, showCompleteSecurityFor session: MXSession) { + AppDelegate.theDelegate().presentCompleteSecurity(for: session) + } + + func roomViewController(_ roomViewController: RoomViewController, handleUniversalLinkFragment fragment: String, from universalLinkURL: URL?) -> Bool { + return AppDelegate.theDelegate().handleUniversalLinkFragment(fragment, from: universalLinkURL) + } + + func roomViewController(_ roomViewController: RoomViewController, handleUniversalLinkURL universalLinkURL: URL) -> Bool { + return AppDelegate.theDelegate().handleUniversalLinkURL(universalLinkURL) + } +} diff --git a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift new file mode 100644 index 000000000..d6ec2aa4e --- /dev/null +++ b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift @@ -0,0 +1,145 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import Foundation + +@objc protocol RoomCoordinatorBridgePresenterDelegate { + func roomCoordinatorBridgePresenterDidLeaveRoom(_ bridgePresenter: RoomCoordinatorBridgePresenter) + func roomCoordinatorBridgePresenterDidCancelRoomPreview(_ bridgePresenter: RoomCoordinatorBridgePresenter) + func roomCoordinatorBridgePresenter(_ bridgePresenter: RoomCoordinatorBridgePresenter, didSelectRoomWithId roomId: String) + func roomCoordinatorBridgePresenterDidDismissInteractively(_ bridgePresenter: RoomCoordinatorBridgePresenter) +} + +@objcMembers +class RoomCoordinatorBridgePresenterParameters: NSObject { + + /// The matrix session in which the room should be available. + let session: MXSession + + /// The room identifier + let roomId: String + + /// If not nil, the room will be opened on this event. + let eventId: String? + + /// The data for the room preview. + let previewData: RoomPreviewData? + + init(session: MXSession, + roomId: String, + eventId: String?, + previewData: RoomPreviewData?) { + self.session = session + self.roomId = roomId + self.eventId = eventId + self.previewData = previewData + } +} + +/// RoomCoordinatorBridgePresenter enables to start RoomCoordinator from a view controller. +/// This bridge is used while waiting for global usage of coordinator pattern. +/// **WARNING**: This class breaks the Coordinator abstraction and it has been introduced for **Objective-C compatibility only** (mainly for integration in legacy view controllers). Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. +@objcMembers +final class RoomCoordinatorBridgePresenter: NSObject { + + // MARK: - Properties + + // MARK: Private + + private let bridgeParameters: RoomCoordinatorBridgePresenterParameters + private var coordinator: RoomCoordinator? + + // MARK: Public + + weak var delegate: RoomCoordinatorBridgePresenterDelegate? + + // MARK: - Setup + + init(parameters: RoomCoordinatorBridgePresenterParameters) { + self.bridgeParameters = parameters + super.init() + } + + // MARK: - Public + + func present(from viewController: UIViewController, animated: Bool) { + + let coordinator = self.createRoomCoordinator() + coordinator.delegate = self + let presentable = coordinator.toPresentable() + presentable.modalPresentationStyle = .formSheet + viewController.present(presentable, animated: animated, completion: nil) + coordinator.start() + + self.coordinator = coordinator + } + + func push(from navigationController: UINavigationController, animated: Bool) { + + let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) + + let coordinator = self.createRoomCoordinator(with: navigationRouter) + coordinator.delegate = self + coordinator.start() // Will trigger view controller push + + self.coordinator = coordinator + } + + func dismiss(animated: Bool, completion: (() -> Void)?) { + guard let coordinator = self.coordinator else { + return + } + coordinator.toPresentable().dismiss(animated: animated) { + self.coordinator = nil + + completion?() + } + } + + // MARK: - Private + + private func createRoomCoordinator(with navigationRouter: NavigationRouterType = NavigationRouter(navigationController: RiotNavigationController())) -> RoomCoordinator { + + let coordinatorParameters: RoomCoordinatorParameters + + if let previewData = self.bridgeParameters.previewData { + coordinatorParameters = RoomCoordinatorParameters(navigationRouter: navigationRouter, previewData: previewData) + } else { + coordinatorParameters = RoomCoordinatorParameters(navigationRouter: navigationRouter, session: self.bridgeParameters.session, roomId: self.bridgeParameters.roomId, eventId: self.bridgeParameters.eventId) + } + + return RoomCoordinator(parameters: coordinatorParameters) + } +} + +// MARK: - RoomNotificationSettingsCoordinatorDelegate +extension RoomCoordinatorBridgePresenter: RoomCoordinatorDelegate { + + func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didSelectRoomWithId roomId: String) { + self.delegate?.roomCoordinatorBridgePresenter(self, didSelectRoomWithId: roomId) + } + + func roomCoordinatorDidLeaveRoom(_ coordinator: RoomCoordinatorProtocol) { + self.delegate?.roomCoordinatorBridgePresenterDidLeaveRoom(self) + } + + func roomCoordinatorDidCancelRoomPreview(_ coordinator: RoomCoordinatorProtocol) { + self.delegate?.roomCoordinatorBridgePresenterDidCancelRoomPreview(self) + } + + func roomCoordinatorDidDismissInteractively(_ coordinator: RoomCoordinatorProtocol) { + self.delegate?.roomCoordinatorBridgePresenterDidDismissInteractively(self) + } +} diff --git a/Riot/Modules/Room/RoomCoordinatorParameters.swift b/Riot/Modules/Room/RoomCoordinatorParameters.swift new file mode 100644 index 000000000..fbc9f3511 --- /dev/null +++ b/Riot/Modules/Room/RoomCoordinatorParameters.swift @@ -0,0 +1,76 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// RoomCoordinator input parameters +struct RoomCoordinatorParameters { + + // MARK: - Properties + + /// The navigation router that manage physical navigation + let navigationRouter: NavigationRouterType? + + /// The navigation router store that enables to get a NavigationRouter from a navigation controller + /// `navigationRouter` property takes priority on `navigationRouterStore` + let navigationRouterStore: NavigationRouterStoreProtocol? + + /// The matrix session in which the room should be available. + let session: MXSession + + /// The room identifier + let roomId: String + + /// If not nil, the room will be opened on this event. + let eventId: String? + + /// The data for the room preview. + let previewData: RoomPreviewData? + + // MARK: - Setup + + private init(navigationRouter: NavigationRouterType?, + navigationRouterStore: NavigationRouterStoreProtocol?, + session: MXSession, + roomId: String, + eventId: String?, + previewData: RoomPreviewData?) { + self.navigationRouter = navigationRouter + self.navigationRouterStore = navigationRouterStore + self.session = session + self.roomId = roomId + self.eventId = eventId + self.previewData = previewData + } + + /// Init to present a joined room + init(navigationRouter: NavigationRouterType? = nil, + navigationRouterStore: NavigationRouterStoreProtocol? = nil, + session: MXSession, + roomId: String, + eventId: String? = nil) { + + self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, session: session, roomId: roomId, eventId: eventId, previewData: nil) + } + + /// Init to present a room preview + init(navigationRouter: NavigationRouterType? = nil, + navigationRouterStore: NavigationRouterStoreProtocol? = nil, + previewData: RoomPreviewData) { + + self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, session: previewData.mxSession, roomId: previewData.roomId, eventId: nil, previewData: previewData) + } +} diff --git a/Riot/Modules/Room/RoomCoordinatorProtocol.swift b/Riot/Modules/Room/RoomCoordinatorProtocol.swift new file mode 100644 index 000000000..1c30f02ec --- /dev/null +++ b/Riot/Modules/Room/RoomCoordinatorProtocol.swift @@ -0,0 +1,48 @@ +// File created from ScreenTemplate +// $ createScreen.sh Room Room +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +protocol RoomCoordinatorDelegate: AnyObject { + func roomCoordinatorDidLeaveRoom(_ coordinator: RoomCoordinatorProtocol) + func roomCoordinatorDidCancelRoomPreview(_ coordinator: RoomCoordinatorProtocol) + func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didSelectRoomWithId roomId: String) + func roomCoordinatorDidDismissInteractively(_ coordinator: RoomCoordinatorProtocol) +} + +/// `RoomCoordinatorProtocol` is a protocol describing a Coordinator that handle room navigation flow. +protocol RoomCoordinatorProtocol: Coordinator, Presentable, RoomIdentifiable { + var delegate: RoomCoordinatorDelegate? { get } + + // Indicate if the underlying RoomDataSource can be released + var canReleaseRoomDataSource: Bool { get } + + /// Start the Coordinator with a setup completion. + /// NOTE: Completion closure has been added for legacy architecture purpose. + /// Remove this completion after LegacyAppDelegate refactor. + /// - Parameters: + /// - completion: called when the RoomDataSource has finish to load. + func start(withCompletion completion: (() -> Void)?) + + /// Use this method when the room screen is already shown and you want to go to a specific event. + /// i.e User tap on push notification message for the current displayed room + /// - Parameters: + /// - eventId: The id of the event to display. + /// - completion: called when the RoomDataSource has finish to load. + func start(withEventId eventId: String, completion: (() -> Void)?) +} diff --git a/Riot/Modules/Room/RoomIdentifiable.swift b/Riot/Modules/Room/RoomIdentifiable.swift new file mode 100644 index 000000000..2ccac42d7 --- /dev/null +++ b/Riot/Modules/Room/RoomIdentifiable.swift @@ -0,0 +1,24 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// `RoomIdentifiable` describes an object tied to a specific room id. +/// Useful to identify existing objects that should be removed when the user leaves a room for example. +protocol RoomIdentifiable { + var roomId: String? { get } + var mxSession: MXSession? { get } +} diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift index 4f5d3cb65..ae71c988b 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift @@ -73,7 +73,7 @@ final class RoomInfoCoordinatorBridgePresenter: NSObject { } func push(from navigationController: UINavigationController, animated: Bool) { - let navigationRouter = NavigationRouter(navigationController: navigationController) + let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) let roomInfoCoordinator = RoomInfoCoordinator(parameters: self.coordinatorParameters, navigationRouter: navigationRouter) roomInfoCoordinator.delegate = self diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 4181270d6..5aface6df 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -410,6 +410,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self vc_removeBackTitle]; + // Display leftBarButtonItems or leftBarButtonItem to the right of the Back button + self.navigationItem.leftItemsSupplementBackButton = YES; + [self setupRemoveJitsiWidgetRemoveView]; // Replace the default input toolbar view. diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinator.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinator.swift index 5fcd0e632..e572e92c2 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinator.swift +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinator.swift @@ -37,9 +37,9 @@ final class ServiceTermsModalScreenCoordinator: ServiceTermsModalScreenCoordinat // MARK: - Setup - init(serviceTerms: MXServiceTerms, outOfContext: Bool = false) { + init(serviceTerms: MXServiceTerms) { - let serviceTermsModalScreenViewModel = ServiceTermsModalScreenViewModel(serviceTerms: serviceTerms, outOfContext: outOfContext) + let serviceTermsModalScreenViewModel = ServiceTermsModalScreenViewModel(serviceTerms: serviceTerms) let serviceTermsModalScreenViewController = ServiceTermsModalScreenViewController.instantiate(with: serviceTermsModalScreenViewModel) self.serviceTermsModalScreenViewModel = serviceTermsModalScreenViewModel self.serviceTermsModalScreenViewController = serviceTermsModalScreenViewController @@ -70,8 +70,4 @@ extension ServiceTermsModalScreenCoordinator: ServiceTermsModalScreenViewModelCo func serviceTermsModalScreenViewModelDidDecline(_ viewModel: ServiceTermsModalScreenViewModelType) { self.delegate?.serviceTermsModalScreenCoordinatorDidDecline(self) } - - func serviceTermsModalScreenViewModelDidCancel(_ viewModel: ServiceTermsModalScreenViewModelType) { - self.delegate?.serviceTermsModalScreenCoordinatorDidCancel(self) - } } diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinatorType.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinatorType.swift index 909a0b33f..55969be50 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinatorType.swift +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenCoordinatorType.swift @@ -22,7 +22,6 @@ protocol ServiceTermsModalScreenCoordinatorDelegate: AnyObject { func serviceTermsModalScreenCoordinatorDidAccept(_ coordinator: ServiceTermsModalScreenCoordinatorType) func serviceTermsModalScreenCoordinator(_ coordinator: ServiceTermsModalScreenCoordinatorType, displayPolicy policy: MXLoginPolicyData) func serviceTermsModalScreenCoordinatorDidDecline(_ coordinator: ServiceTermsModalScreenCoordinatorType) - func serviceTermsModalScreenCoordinatorDidCancel(_ coordinator: ServiceTermsModalScreenCoordinatorType) } /// `ServiceTermsModalScreenCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow. diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewAction.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewAction.swift index 27bb11e05..7c10a2aaf 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewAction.swift +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewAction.swift @@ -24,5 +24,4 @@ enum ServiceTermsModalScreenViewAction { case display(MXLoginPolicyData) case accept case decline - case cancel } diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard index f7c793731..cbb3f012b 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.storyboard @@ -1,12 +1,11 @@ - - - - + + - + + @@ -15,95 +14,131 @@ - + - + - - + + - - + + - - - - - - - + + + + + - - + + - + + + + - - - - - - - - - - - + - + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + - - - - - - + + + + + + + + @@ -111,19 +146,28 @@ - - + + + + + - + + + + + + + diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift index 53d4cf9be..a1bbb053b 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewController.swift @@ -22,17 +22,27 @@ final class ServiceTermsModalScreenViewController: UIViewController { // MARK: - Constants + private enum Constants { + /// Reuse identifier for the prototype cell in the storyboard. + static let cellReuseIdentifier = "Service Terms Cell" + static let minimumTableViewHeight: CGFloat = 120 + } + // MARK: - Properties // MARK: Outlets @IBOutlet private weak var scrollView: UIScrollView! - @IBOutlet private weak var messageLabel: UILabel! + @IBOutlet private weak var imageView: UIImageView! + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var descriptionLabel: UILabel! + @IBOutlet private weak var footerLabel: UILabel! @IBOutlet private weak var tableView: UITableView! @IBOutlet private weak var acceptButton: UIButton! @IBOutlet private weak var declineButton: UIButton! - + @IBOutlet private weak var tableViewHeightConstraint: NSLayoutConstraint! + // MARK: Private private var viewModel: ServiceTermsModalScreenViewModelType! @@ -41,9 +51,8 @@ final class ServiceTermsModalScreenViewController: UIViewController { private var activityPresenter: ActivityIndicatorPresenter! private var policies: [MXLoginPolicyData] = [] - - /// Policies checked by the end user - private var checkedPolicies: Set = [] + + private var tableHeaderView: ServiceTermsModalTableHeaderView! // MARK: - Setup @@ -60,8 +69,6 @@ final class ServiceTermsModalScreenViewController: UIViewController { super.viewDidLoad() // Do any additional setup after loading the view. - - self.title = VectorL10n.serviceTermsModalTitle self.setupViews() self.activityPresenter = ActivityIndicatorPresenter() @@ -75,6 +82,11 @@ final class ServiceTermsModalScreenViewController: UIViewController { self.viewModel.process(viewAction: .load) } + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + tableViewHeightConstraint.constant = max(Constants.minimumTableViewHeight, tableView.contentSize.height) + } + override var preferredStatusBarStyle: UIStatusBarStyle { return self.theme.statusBarStyle } @@ -84,19 +96,30 @@ final class ServiceTermsModalScreenViewController: UIViewController { private func update(theme: Theme) { self.theme = theme - self.view.backgroundColor = theme.headerBackgroundColor + view.backgroundColor = theme.headerBackgroundColor - if let navigationBar = self.navigationController?.navigationBar { + if let navigationBar = navigationController?.navigationBar { theme.applyStyle(onNavigationBar: navigationBar) } - self.messageLabel.textColor = theme.textPrimaryColor + titleLabel.font = theme.fonts.bodySB + titleLabel.textColor = theme.colors.primaryContent + + descriptionLabel.font = theme.fonts.body + descriptionLabel.textColor = theme.colors.secondaryContent + + tableHeaderView.update(theme: theme) + + footerLabel.font = theme.fonts.footnote.withSize(13) + footerLabel.textColor = theme.colors.secondaryContent - self.acceptButton.backgroundColor = theme.backgroundColor - theme.applyStyle(onButton: self.acceptButton) + acceptButton.titleLabel?.font = theme.fonts.body + acceptButton.setTitleColor(theme.colors.background, for: .normal) + acceptButton.backgroundColor = theme.colors.accent theme.applyStyle(onButton: self.declineButton) - self.declineButton.setTitleColor(self.theme.warningColor, for: .normal) + declineButton.titleLabel?.font = theme.fonts.body + declineButton.setTitleColor(theme.warningColor, for: .normal) self.refreshViews() } @@ -110,40 +133,53 @@ final class ServiceTermsModalScreenViewController: UIViewController { } private func setupViews() { - let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in - self?.cancelButtonAction() - } - - self.navigationItem.rightBarButtonItem = cancelBarButtonItem - self.setupTableView() self.scrollView.keyboardDismissMode = .interactive - self.messageLabel.text = VectorL10n.serviceTermsModalMessage(self.viewModel.serviceUrl) + self.titleLabel.text = VectorL10n.serviceTermsModalTitleMessage + self.footerLabel.text = VectorL10n.serviceTermsModalFooter + + self.tableHeaderView.serviceURLLabel.text = viewModel.serviceUrl self.acceptButton.setTitle(VectorL10n.serviceTermsModalAcceptButton, for: .normal) self.acceptButton.setTitle(VectorL10n.serviceTermsModalAcceptButton, for: .highlighted) - self.refreshAcceptButton() + self.acceptButton.layer.cornerRadius = 8 - if self.viewModel.outOfContext - && self.viewModel.serviceType == MXServiceTypeIdentityService { - self.title = VectorL10n.serviceTermsModalTitleIdentityServer - self.messageLabel.text = VectorL10n.serviceTermsModalMessageIdentityServer(self.viewModel.serviceUrl) - - self.declineButton.setTitle(VectorL10n.serviceTermsModalDeclineButton, for: .normal) - self.declineButton.setTitle(VectorL10n.serviceTermsModalDeclineButton, for: .highlighted) + self.declineButton.setTitle(VectorL10n.serviceTermsModalDeclineButton, for: .normal) + self.declineButton.setTitle(VectorL10n.serviceTermsModalDeclineButton, for: .highlighted) + + if self.viewModel.serviceType == MXServiceTypeIdentityService { + self.descriptionLabel.text = VectorL10n.serviceTermsModalDescriptionIdentityServer + self.tableHeaderView.titleLabel.text = VectorL10n.serviceTermsModalTableHeaderIdentityServer + self.imageView.image = Asset.Images.findYourContactsFacepile.image } else { - self.declineButton.isHidden = true + self.descriptionLabel.text = VectorL10n.serviceTermsModalDescriptionIntegrationManager + self.tableHeaderView.titleLabel.text = VectorL10n.serviceTermsModalTableHeaderIntegrationManager + self.imageView.image = Asset.Images.integrationManagerIconpile.image } } private func setupTableView() { - self.tableView.delegate = self - self.tableView.dataSource = self - self.tableView.separatorStyle = .none - self.tableView.alwaysBounceVertical = false - self.tableView.backgroundColor = .clear - self.tableView.register(TableViewCellWithCheckBoxAndLabel.nib(), forCellReuseIdentifier: TableViewCellWithCheckBoxAndLabel.defaultReuseIdentifier()) + guard let tableView = tableView else { return } + + tableView.delegate = self + tableView.dataSource = self + tableView.separatorStyle = .none + tableView.alwaysBounceVertical = false + tableView.backgroundColor = .clear + tableView.register(TableViewCellWithCheckBoxAndLabel.nib(), forCellReuseIdentifier: TableViewCellWithCheckBoxAndLabel.defaultReuseIdentifier()) + + tableHeaderView = ServiceTermsModalTableHeaderView.instantiate() + tableHeaderView.delegate = self + tableView.tableHeaderView = tableHeaderView + + tableView.addConstraint(NSLayoutConstraint(item: tableView, + attribute: .width, + relatedBy: .equal, + toItem: tableHeaderView, + attribute: .width, + multiplier: 1, + constant: 10)) } private func render(viewState: ServiceTermsModalScreenViewState) { @@ -161,13 +197,14 @@ final class ServiceTermsModalScreenViewController: UIViewController { private func renderLoading() { self.activityPresenter.presentActivityIndicator(on: self.view, animated: true) + self.acceptButton.isEnabled = false } private func renderLoaded(policies: [MXLoginPolicyData], alreadyAcceptedPoliciesUrls: [String]) { self.activityPresenter.removeCurrentActivityIndicator(animated: true) self.policies = policies - self.updateCheckedPolicies(with: alreadyAcceptedPoliciesUrls) + self.acceptButton.isEnabled = true self.refreshViews() } @@ -187,21 +224,6 @@ final class ServiceTermsModalScreenViewController: UIViewController { private func refreshViews() { self.tableView.reloadData() - self.refreshAcceptButton() - } - - private func refreshAcceptButton() { - // Enable the button only if the user has accepted all policies - self.acceptButton.isEnabled = (self.policies.count == self.checkedPolicies.count) - } - - // Pre-check policies already accepted by the user - private func updateCheckedPolicies(with acceptedPoliciesUrls: [String]) { - for url in acceptedPoliciesUrls { - if let policyIndex = self.policies.firstIndex(where: { $0.url == url }) { - checkedPolicies.insert(policyIndex) - } - } } @@ -214,37 +236,6 @@ final class ServiceTermsModalScreenViewController: UIViewController { @IBAction private func declineButtonAction(_ sender: Any) { self.viewModel.process(viewAction: .decline) } - - private func cancelButtonAction() { - self.viewModel.process(viewAction: .cancel) - } - - @objc private func didTapCheckbox(sender: UITapGestureRecognizer) { - - guard let policyIndex = sender.view?.tag else { - return - } - - let isCheckBoxSelected: Bool - - if self.checkedPolicies.contains(policyIndex) { - self.checkedPolicies.remove(policyIndex) - isCheckBoxSelected = false - } else { - checkedPolicies.insert(policyIndex) - isCheckBoxSelected = true - } - - if let checkBoxImageView = sender.view as? UIImageView { - if isCheckBoxSelected { - checkBoxImageView.accessibilityTraits.insert(.selected) - } else { - checkBoxImageView.accessibilityTraits.remove(.selected) - } - } - - self.refreshViews() - } } @@ -259,72 +250,71 @@ extension ServiceTermsModalScreenViewController: ServiceTermsModalScreenViewMode // MARK: - UITableViewDataSource extension ServiceTermsModalScreenViewController: UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { + // Use individual sections for each policy so the cells aren't grouped together. + return policies.count + } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return self.policies.count + return 1 + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + // Reduce the height between sections to only be the footer height value. + return CGFloat.leastNormalMagnitude + } + + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + // Modify the footer size to reduce cell spacing. + return 8.0 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: Constants.cellReuseIdentifier, for: indexPath) - guard let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCellWithCheckBoxAndLabel.defaultReuseIdentifier(), for: indexPath) as? TableViewCellWithCheckBoxAndLabel else { - fatalError("\(String(describing: TableViewCellWithCheckBoxAndLabel.self)) should be registered") - } + let policy = policies[indexPath.section] - let policy = policies[indexPath.row] - let checked = checkedPolicies.contains(indexPath.row) - - cell.label.attributedText = self.cellLabel(for: policy) - cell.label.font = .systemFont(ofSize: 15) - cell.isEnabled = checked + cell.textLabel?.text = policy.name + cell.textLabel?.textColor = theme.colors.primaryContent + cell.textLabel?.font = theme.fonts.body cell.vc_setAccessoryDisclosureIndicator(withTheme: self.theme) - cell.backgroundColor = self.theme.backgroundColor - - if let checkBox = cell.checkBox, checkBox.gestureRecognizers?.isEmpty ?? true { - let gesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapCheckbox)) - gesture.numberOfTapsRequired = 1 - gesture.numberOfTouchesRequired = 1 - - checkBox.isUserInteractionEnabled = true - checkBox.tag = indexPath.row - checkBox.addGestureRecognizer(gesture) - - checkBox.isAccessibilityElement = true - checkBox.accessibilityTraits = .button - checkBox.accessibilityLabel = VectorL10n.accessibilityCheckboxLabel - checkBox.accessibilityHint = VectorL10n.serviceTermsModalPolicyCheckboxAccessibilityHint(policy.name) - } + cell.accessoryView?.tintColor = theme.colors.quarterlyContent + cell.backgroundColor = theme.colors.background + cell.selectionStyle = .default return cell } - - func cellLabel(for policy: MXLoginPolicyData) -> NSAttributedString { - - // TableViewCellWithCheckBoxAndLabel does not have a detailTextLabel - // Do it by hand - - var labelDetail: String = "" - switch self.viewModel.serviceType { - case MXServiceTypeIdentityService: - labelDetail = VectorL10n.serviceTermsModalDescriptionForIdentityServer1 - + "\n" - + VectorL10n.serviceTermsModalDescriptionForIdentityServer2 - case MXServiceTypeIntegrationManager: - labelDetail = VectorL10n.serviceTermsModalDescriptionForIntegrationManager - default: break - } - - let label = NSMutableAttributedString(string: policy.name, - attributes: [.foregroundColor: theme.textPrimaryColor]) - label.append(NSAttributedString(string: "\n")) - label.append(NSAttributedString(string: labelDetail, - attributes: [.foregroundColor: theme.textSecondaryColor])) - return label - } } +// MARK: - UITableViewDelegate + extension ServiceTermsModalScreenViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let policy = policies[indexPath.row] - self.viewModel.process(viewAction: .display(policy)) + let policy = policies[indexPath.section] + viewModel.process(viewAction: .display(policy)) + tableView.deselectRow(at: indexPath, animated: true) + } +} + + +// MARK: - ServiceTermsModalTableHeaderViewDelegate +extension ServiceTermsModalScreenViewController: ServiceTermsModalTableHeaderViewDelegate { + func tableHeaderViewDidTapInformationButton() { + let title: String + let message: String + + if viewModel.serviceType == MXServiceTypeIdentityService { + title = VectorL10n.serviceTermsModalInformationTitleIdentityServer + message = VectorL10n.serviceTermsModalInformationDescriptionIdentityServer + } else { + title = VectorL10n.serviceTermsModalInformationTitleIntegrationManager + message = VectorL10n.serviceTermsModalInformationDescriptionIntegrationManager + } + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: Bundle.mxk_localizedString(forKey: "ok"), style: .default)) + + present(alertController, animated: true) } } diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModel.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModel.swift index a49d6954f..7fd88739f 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModel.swift +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModel.swift @@ -21,7 +21,6 @@ import Foundation final class ServiceTermsModalScreenViewModel: ServiceTermsModalScreenViewModelType { // MARK: - Properties - let outOfContext: Bool // MARK: Private @@ -43,9 +42,8 @@ final class ServiceTermsModalScreenViewModel: ServiceTermsModalScreenViewModelTy // MARK: - Setup - init(serviceTerms: MXServiceTerms, outOfContext: Bool) { + init(serviceTerms: MXServiceTerms) { self.serviceTerms = serviceTerms - self.outOfContext = outOfContext } // MARK: - Public @@ -60,8 +58,6 @@ final class ServiceTermsModalScreenViewModel: ServiceTermsModalScreenViewModelTy self.acceptTerms() case .decline: self.coordinatorDelegate?.serviceTermsModalScreenViewModelDidDecline(self) - case .cancel: - self.coordinatorDelegate?.serviceTermsModalScreenViewModelDidCancel(self) } } @@ -99,6 +95,14 @@ final class ServiceTermsModalScreenViewModel: ServiceTermsModalScreenViewModelTy return } self.update(viewState: .accepted) + + // Send a notification to update the identity service immediately. + if self.serviceTerms.serviceType == MXServiceTypeIdentityService { + let userInfo = [MXIdentityServiceNotificationIdentityServerKey: self.serviceTerms.baseUrl] + NotificationCenter.default.post(name: .MXIdentityServiceTermsAccepted, object: nil, userInfo: userInfo) + } + + // Notify the delegate. self.coordinatorDelegate?.serviceTermsModalScreenViewModelDidAccept(self) }, failure: { [weak self] (error) in diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModelType.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModelType.swift index 4dd4b6317..b6381baf9 100644 --- a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModelType.swift +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalScreenViewModelType.swift @@ -26,7 +26,6 @@ protocol ServiceTermsModalScreenViewModelCoordinatorDelegate: AnyObject { func serviceTermsModalScreenViewModel(_ coordinator: ServiceTermsModalScreenViewModelType, displayPolicy policy: MXLoginPolicyData) func serviceTermsModalScreenViewModelDidAccept(_ viewModel: ServiceTermsModalScreenViewModelType) func serviceTermsModalScreenViewModelDidDecline(_ viewModel: ServiceTermsModalScreenViewModelType) - func serviceTermsModalScreenViewModelDidCancel(_ viewModel: ServiceTermsModalScreenViewModelType) } /// Protocol describing the view model used by `ServiceTermsModalScreenViewController` @@ -34,9 +33,6 @@ protocol ServiceTermsModalScreenViewModelType { var serviceUrl: String { get } var serviceType: MXServiceType { get } - /// If true, terms are displayed out of a context of a flow (like a background 3pids lookup) - /// In this case, the wording needs to provide more information about the intent - var outOfContext: Bool { get } var policies: [MXLoginPolicyData]? { get set } var alreadyAcceptedPoliciesUrls: [String] { get set } diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalTableHeaderView.swift b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalTableHeaderView.swift new file mode 100644 index 000000000..5ea5673ae --- /dev/null +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalTableHeaderView.swift @@ -0,0 +1,56 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import Reusable + +protocol ServiceTermsModalTableHeaderViewDelegate: AnyObject { + func tableHeaderViewDidTapInformationButton() +} + +class ServiceTermsModalTableHeaderView: UIView, NibLoadable, Themable { + + // MARK: - Properties + + weak var delegate: ServiceTermsModalTableHeaderViewDelegate? + + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var serviceURLLabel: UILabel! + + // MARK: - Setup + + static func instantiate() -> Self { + let view = Self.loadFromNib() + view.translatesAutoresizingMaskIntoConstraints = false + view.update(theme: ThemeService.shared().theme) + return view + } + + func update(theme: Theme) { + titleLabel.font = theme.fonts.footnote + titleLabel.textColor = theme.colors.secondaryContent + + serviceURLLabel.font = theme.fonts.callout + serviceURLLabel.textColor = theme.colors.secondaryContent + } + + // MARK: - Action + + @IBAction private func buttonAction(_ sender: Any) { + delegate?.tableHeaderViewDidTapInformationButton() + } + +} diff --git a/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalTableHeaderView.xib b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalTableHeaderView.xib new file mode 100644 index 000000000..f35dc1ea0 --- /dev/null +++ b/Riot/Modules/ServiceTerms/Modal/Modal/ServiceTermsModalTableHeaderView.xib @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift index 3c9ccc737..4b05e230c 100644 --- a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift +++ b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift @@ -19,7 +19,7 @@ import UIKit @objcMembers -final class ServiceTermsModalCoordinator: ServiceTermsModalCoordinatorType { +final class ServiceTermsModalCoordinator: NSObject, ServiceTermsModalCoordinatorType { // MARK: - Properties @@ -28,7 +28,6 @@ final class ServiceTermsModalCoordinator: ServiceTermsModalCoordinatorType { private let navigationRouter: NavigationRouterType private let session: MXSession private let serviceTerms: MXServiceTerms - private let outOfContext: Bool // MARK: Public @@ -38,11 +37,10 @@ final class ServiceTermsModalCoordinator: ServiceTermsModalCoordinatorType { weak var delegate: ServiceTermsModalCoordinatorDelegate? // MARK: - Setup - init(session: MXSession, baseUrl: String, serviceType: MXServiceType, outOfContext: Bool, accessToken: String) { + init(session: MXSession, baseUrl: String, serviceType: MXServiceType, accessToken: String) { self.navigationRouter = NavigationRouter(navigationController: RiotNavigationController()) self.session = session self.serviceTerms = MXServiceTerms(baseUrl: baseUrl, serviceType: serviceType, matrixSession: session, accessToken: accessToken) - self.outOfContext = outOfContext } // MARK: - Public methods @@ -53,6 +51,8 @@ final class ServiceTermsModalCoordinator: ServiceTermsModalCoordinatorType { rootCoordinator.start() self.add(childCoordinator: rootCoordinator) + + self.toPresentable().presentationController?.delegate = self self.navigationRouter.setRootModule(rootCoordinator) } @@ -64,7 +64,7 @@ final class ServiceTermsModalCoordinator: ServiceTermsModalCoordinatorType { // MARK: - Private methods private func createServiceTermsModalLoadTermsScreenCoordinator() -> ServiceTermsModalScreenCoordinator { - let coordinator = ServiceTermsModalScreenCoordinator(serviceTerms: self.serviceTerms, outOfContext: self.outOfContext) + let coordinator = ServiceTermsModalScreenCoordinator(serviceTerms: self.serviceTerms) coordinator.delegate = self return coordinator } @@ -87,12 +87,29 @@ final class ServiceTermsModalCoordinator: ServiceTermsModalCoordinatorType { @objc private func didTapCancelOnPolicyScreen() { self.removePolicyScreen() } + + /// Removes the identity server from the `MXSession` and it's account data. + private func disableIdentityServer() { + MXLog.debug("[ServiceTermsModalCoordinator] IS Terms: User has declined the use of the default IS.") + + // The user does not want to use the proposed IS. + // Disable IS feature on user's account + session.setIdentityServer(nil, andAccessToken: nil) + session.setAccountDataIdentityServer(nil, success: nil) { error in + guard let errorDescription = error?.localizedDescription else { return } + MXLog.error("[ServiceTermsModalCoordinator] IS Terms: Error: \(errorDescription)") + } + } } // MARK: - ServiceTermsModalLoadTermsScreenCoordinatorDelegate extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelegate { func serviceTermsModalScreenCoordinatorDidAccept(_ coordinator: ServiceTermsModalScreenCoordinatorType) { + if serviceTerms.serviceType == MXServiceTypeIdentityService { + Analytics.sharedInstance().trackValue(1, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted) + } + self.delegate?.serviceTermsModalCoordinatorDidAccept(self) } @@ -101,10 +118,22 @@ extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelega } func serviceTermsModalScreenCoordinatorDidDecline(_ coordinator: ServiceTermsModalScreenCoordinatorType) { + if serviceTerms.serviceType == MXServiceTypeIdentityService { + Analytics.sharedInstance().trackValue(1, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted) + disableIdentityServer() + } + self.delegate?.serviceTermsModalCoordinatorDidDecline(self) } +} - func serviceTermsModalScreenCoordinatorDidCancel(_ coordinator: ServiceTermsModalScreenCoordinatorType) { - self.delegate?.serviceTermsModalCoordinatorDidCancel(self) +// MARK: - UIAdaptivePresentationControllerDelegate +extension ServiceTermsModalCoordinator: UIAdaptivePresentationControllerDelegate { + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + if serviceTerms.serviceType == MXServiceTypeIdentityService { + Analytics.sharedInstance().trackValue(0, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted) + } + + self.delegate?.serviceTermsModalCoordinatorDidDismissInteractively(self) } } diff --git a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorBridgePresenter.swift b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorBridgePresenter.swift index 4b55a86f0..aac545e3e 100644 --- a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorBridgePresenter.swift +++ b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorBridgePresenter.swift @@ -21,7 +21,7 @@ import Foundation @objc protocol ServiceTermsModalCoordinatorBridgePresenterDelegate { func serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept(_ coordinatorBridgePresenter: ServiceTermsModalCoordinatorBridgePresenter) func serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline(_ coordinatorBridgePresenter: ServiceTermsModalCoordinatorBridgePresenter, session: MXSession) - func serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel(_ coordinatorBridgePresenter: ServiceTermsModalCoordinatorBridgePresenter) + func serviceTermsModalCoordinatorBridgePresenterDelegateDidClose(_ coordinatorBridgePresenter: ServiceTermsModalCoordinatorBridgePresenter) } /// ServiceTermsModalCoordinatorBridgePresenter enables to start ServiceTermsModalCoordinator from a view controller. @@ -36,7 +36,6 @@ final class ServiceTermsModalCoordinatorBridgePresenter: NSObject { private let session: MXSession private let baseUrl: String private let serviceType: MXServiceType - private let outOfContext: Bool private let accessToken: String private var coordinator: ServiceTermsModalCoordinator? @@ -50,11 +49,10 @@ final class ServiceTermsModalCoordinatorBridgePresenter: NSObject { // MARK: - Setup - init(session: MXSession, baseUrl: String, serviceType: MXServiceType, outOfContext: Bool = false, accessToken: String) { + init(session: MXSession, baseUrl: String, serviceType: MXServiceType, accessToken: String) { self.session = session self.baseUrl = baseUrl self.serviceType = serviceType - self.outOfContext = outOfContext self.accessToken = accessToken super.init() } @@ -67,10 +65,9 @@ final class ServiceTermsModalCoordinatorBridgePresenter: NSObject { // } func present(from viewController: UIViewController, animated: Bool) { - let serviceTermsModalCoordinator = ServiceTermsModalCoordinator(session: self.session, baseUrl: self.baseUrl, serviceType: self.serviceType, outOfContext: self.outOfContext, accessToken: accessToken) + let serviceTermsModalCoordinator = ServiceTermsModalCoordinator(session: self.session, baseUrl: self.baseUrl, serviceType: self.serviceType, accessToken: accessToken) serviceTermsModalCoordinator.delegate = self let presentable = serviceTermsModalCoordinator.toPresentable() - presentable.presentationController?.delegate = self viewController.present(presentable, animated: animated, completion: nil) serviceTermsModalCoordinator.start() @@ -100,36 +97,13 @@ extension ServiceTermsModalCoordinatorBridgePresenter: ServiceTermsModalCoordina func serviceTermsModalCoordinatorDidAccept(_ coordinator: ServiceTermsModalCoordinatorType) { self.delegate?.serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept(self) - - if serviceType == MXServiceTypeIdentityService { - Analytics.sharedInstance().trackValue(1, category: kMXKAnalyticsContactsCategory, name: AnalyticsContactsIdentityServerAccepted) - } } func serviceTermsModalCoordinatorDidDecline(_ coordinator: ServiceTermsModalCoordinatorType) { self.delegate?.serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline(self, session: self.session) - - if serviceType == MXServiceTypeIdentityService { - Analytics.sharedInstance().trackValue(0, category: kMXKAnalyticsContactsCategory, name: AnalyticsContactsIdentityServerAccepted) - } } - - func serviceTermsModalCoordinatorDidCancel(_ coordinator: ServiceTermsModalCoordinatorType) { - self.delegate?.serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel(self) - - if serviceType == MXServiceTypeIdentityService { - Analytics.sharedInstance().trackValue(0, category: kMXKAnalyticsContactsCategory, name: AnalyticsContactsIdentityServerAccepted) - } - } -} - -// MARK: - UIAdaptivePresentationControllerDelegate -extension ServiceTermsModalCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate { - func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { - self.delegate?.serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel(self) - - if serviceType == MXServiceTypeIdentityService { - Analytics.sharedInstance().trackValue(0, category: kMXKAnalyticsContactsCategory, name: AnalyticsContactsIdentityServerAccepted) - } + + func serviceTermsModalCoordinatorDidDismissInteractively(_ coordinator: ServiceTermsModalCoordinatorType) { + self.delegate?.serviceTermsModalCoordinatorBridgePresenterDelegateDidClose(self) } } diff --git a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorType.swift b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorType.swift index 34c09cb18..fe9081c39 100644 --- a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorType.swift +++ b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinatorType.swift @@ -21,7 +21,7 @@ import Foundation protocol ServiceTermsModalCoordinatorDelegate: AnyObject { func serviceTermsModalCoordinatorDidAccept(_ coordinator: ServiceTermsModalCoordinatorType) func serviceTermsModalCoordinatorDidDecline(_ coordinator: ServiceTermsModalCoordinatorType) - func serviceTermsModalCoordinatorDidCancel(_ coordinator: ServiceTermsModalCoordinatorType) + func serviceTermsModalCoordinatorDidDismissInteractively(_ coordinator: ServiceTermsModalCoordinatorType) } /// `ServiceTermsModalCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow. diff --git a/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsCoordinatorBridgePresenter.swift b/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsCoordinatorBridgePresenter.swift index 340db8255..7dfbd0aad 100644 --- a/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsCoordinatorBridgePresenter.swift @@ -31,7 +31,7 @@ final class SettingsDiscoveryThreePidDetailsCoordinatorBridgePresenter: NSObject private let threePid: MX3PID private var coordinator: SettingsDiscoveryThreePidDetailsCoordinator? - private var router: NavigationRouter? + private var router: NavigationRouterType? // MARK: - Setup @@ -45,7 +45,7 @@ final class SettingsDiscoveryThreePidDetailsCoordinatorBridgePresenter: NSObject func push(from navigationController: UINavigationController, animated: Bool, popCompletion: (() -> Void)?) { - let router = NavigationRouter(navigationController: navigationController) + let router = NavigationRouterStore.shared.navigationRouter(for: navigationController) let settingsDiscoveryThreePidDetailsCoordinator = SettingsDiscoveryThreePidDetailsCoordinator(session: self.session, threePid: self.threePid) diff --git a/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerCoordinatorBridgePresenter.swift b/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerCoordinatorBridgePresenter.swift index fdbe312f1..c25e81b78 100644 --- a/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerCoordinatorBridgePresenter.swift @@ -32,7 +32,7 @@ final class SettingsIdentityServerCoordinatorBridgePresenter: NSObject { // MARK: Private private let session: MXSession - private var router: NavigationRouter? + private var router: NavigationRouterType? private var coordinator: SettingsIdentityServerCoordinator? // MARK: Public @@ -50,7 +50,7 @@ final class SettingsIdentityServerCoordinatorBridgePresenter: NSObject { func push(from navigationController: UINavigationController, animated: Bool, popCompletion: (() -> Void)?) { - let router = NavigationRouter(navigationController: navigationController) + let router = NavigationRouterStore.shared.navigationRouter(for: navigationController) let settingsIdentityServerCoordinator = SettingsIdentityServerCoordinator(session: self.session) diff --git a/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.swift b/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.swift index 3375490bb..4b4d4d94e 100644 --- a/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.swift +++ b/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.swift @@ -395,7 +395,7 @@ extension SettingsIdentityServerViewController: ServiceTermsModalCoordinatorBrid self.hideTerms(accepted: false) } - func serviceTermsModalCoordinatorBridgePresenterDelegateDidCancel(_ coordinatorBridgePresenter: ServiceTermsModalCoordinatorBridgePresenter) { + func serviceTermsModalCoordinatorBridgePresenterDelegateDidClose(_ coordinatorBridgePresenter: ServiceTermsModalCoordinatorBridgePresenter) { self.hideTerms(accepted: false) } } diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index d8dbda6ae..6d91b4357 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -127,7 +127,8 @@ enum enum { LOCAL_CONTACTS_SYNC_INDEX, - LOCAL_CONTACTS_PHONEBOOK_COUNTRY_INDEX + LOCAL_CONTACTS_PHONEBOOK_COUNTRY_INDEX, + LOCAL_CONTACTS_SYNC_DESCRIPTION_INDEX }; enum @@ -179,6 +180,7 @@ SignOutAlertPresenterDelegate, SingleImagePickerPresenterDelegate, SettingsDiscoveryTableViewSectionDelegate, SettingsDiscoveryViewModelCoordinatorDelegate, SettingsIdentityServerCoordinatorBridgePresenterDelegate, +ServiceTermsModalCoordinatorBridgePresenterDelegate, TableViewSectionsDelegate> { // Current alert (if any). @@ -280,6 +282,8 @@ TableViewSectionsDelegate> @property (nonatomic, strong) UserInteractiveAuthenticationService *userInteractiveAuthenticationService; +@property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter; + @end @implementation SettingsViewController @@ -462,7 +466,12 @@ TableViewSectionsDelegate> { [sectionLocalContacts addRowWithTag:LOCAL_CONTACTS_PHONEBOOK_COUNTRY_INDEX]; } - sectionLocalContacts.headerTitle = [VectorL10n settingsContacts]; + else + { + [sectionLocalContacts addRowWithTag:LOCAL_CONTACTS_SYNC_DESCRIPTION_INDEX]; + } + NSString *headerTitle = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone ? VectorL10n.settingsPhoneContacts : VectorL10n.settingsContacts; + sectionLocalContacts.headerTitle = headerTitle; [tmpSections addObject:sectionLocalContacts]; } @@ -2196,7 +2205,7 @@ TableViewSectionsDelegate> MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; labelAndSwitchCell.mxkLabel.numberOfLines = 0; - labelAndSwitchCell.mxkLabel.text = [VectorL10n settingsContactsDiscoverMatrixUsers]; + labelAndSwitchCell.mxkLabel.text = VectorL10n.settingsContactsEnableSync; labelAndSwitchCell.mxkSwitch.on = [MXKAppSettings standardAppSettings].syncLocalContacts; labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; labelAndSwitchCell.mxkSwitch.enabled = YES; @@ -2224,6 +2233,15 @@ TableViewSectionsDelegate> [cell vc_setAccessoryDisclosureIndicatorWithCurrentTheme]; cell.selectionStyle = UITableViewCellSelectionStyleDefault; } + else if (row == LOCAL_CONTACTS_SYNC_DESCRIPTION_INDEX) + { + MXKTableViewCell *descriptionCell = [self getDefaultTableViewCell:tableView]; + descriptionCell.textLabel.text = VectorL10n.settingsContactsEnableSyncDescription; + descriptionCell.textLabel.numberOfLines = 0; + descriptionCell.selectionStyle = UITableViewCellSelectionStyleNone; + + cell = descriptionCell; + } } else if (section == SECTION_TAG_ADVANCED) { @@ -3102,17 +3120,51 @@ TableViewSectionsDelegate> { if (sender.on) { - [MXKContactManager requestUserConfirmationForLocalContactsSyncInViewController:self completionHandler:^(BOOL granted) { - - [MXKAppSettings standardAppSettings].syncLocalContacts = granted; + // First check if the service terms have already been accepted + MXSession *session = self.mxSessions.firstObject; + if (session.identityService.areAllTermsAgreed) + { + // If they have we only require local contacts access. + [self checkAccessForContacts]; + } + else + { + MXWeakify(self); - [self updateSections]; - }]; + // The preparation can take some time so indicate this to the user + [self startActivityIndicator]; + + [session prepareIdentityServiceForTermsWithDefault:RiotSettings.shared.identityServerUrlString + success:^(MXSession *session, NSString *baseURL, NSString *accessToken) { + MXStrongifyAndReturnIfNil(self); + + [self stopActivityIndicator]; + + // Present the terms of the identity server. + [self presentIdentityServerTermsWithSession:session baseURL:baseURL andAccessToken:accessToken]; + } failure:^(NSError *error) { + MXStrongifyAndReturnIfNil(self); + + [self stopActivityIndicator]; + + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:VectorL10n.findYourContactsIdentityServiceError + message:nil + preferredStyle:UIAlertControllerStyleAlert]; + + [alertController addAction:[UIAlertAction actionWithTitle:MatrixKitL10n.ok + style:UIAlertActionStyleDefault + handler:nil]]; + + [self presentViewController:alertController animated:YES completion:nil]; + + [MXKAppSettings standardAppSettings].syncLocalContacts = NO; + [self updateSections]; + }]; + } } else { [MXKAppSettings standardAppSettings].syncLocalContacts = NO; - [self updateSections]; } } @@ -4457,6 +4509,28 @@ TableViewSectionsDelegate> } } +#pragma mark - Local Contacts Sync + + - (void)checkAccessForContacts +{ + MXWeakify(self); + + // Check for contacts access, showing a pop-up if necessary. + [MXKTools checkAccessForContacts:VectorL10n.contactsAddressBookPermissionDeniedAlertTitle + withManualChangeMessage:VectorL10n.contactsAddressBookPermissionDeniedAlertMessage + showPopUpInViewController:self + completionHandler:^(BOOL granted) { + + MXStrongifyAndReturnIfNil(self); + + if (granted) + { + // When granted, local contacts can be shown. + [MXKAppSettings standardAppSettings].syncLocalContacts = YES; + [self updateSections]; + } + }]; +} #pragma mark - Identity server @@ -4468,7 +4542,25 @@ TableViewSectionsDelegate> identityServerSettingsCoordinatorBridgePresenter.delegate = self; } -#pragma mark - SettingsIdentityServerCoordinatorBridgePresenterDelegate +- (void)presentIdentityServerTermsWithSession:(MXSession*)mxSession baseURL:(NSString*)baseURL andAccessToken:(NSString*)accessToken +{ + if (!mxSession || !baseURL || !accessToken || self.serviceTermsModalCoordinatorBridgePresenter.isPresenting) + { + return; + } + + ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter = [[ServiceTermsModalCoordinatorBridgePresenter alloc] initWithSession:mxSession + baseUrl:baseURL + serviceType:MXServiceTypeIdentityService + accessToken:accessToken]; + + serviceTermsModalCoordinatorBridgePresenter.delegate = self; + + [serviceTermsModalCoordinatorBridgePresenter presentFrom:self animated:YES]; + self.serviceTermsModalCoordinatorBridgePresenter = serviceTermsModalCoordinatorBridgePresenter; +} + +#pragma mark SettingsIdentityServerCoordinatorBridgePresenterDelegate - (void)settingsIdentityServerCoordinatorBridgePresenterDelegateDidComplete:(SettingsIdentityServerCoordinatorBridgePresenter *)coordinatorBridgePresenter { @@ -4476,6 +4568,31 @@ TableViewSectionsDelegate> [self refreshSettings]; } +#pragma mark ServiceTermsModalCoordinatorBridgePresenterDelegate + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ + [self checkAccessForContacts]; + }]; + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter session:(MXSession *)session +{ + // Disable the contacts toggle as the terms weren't accepted. + [self updateSections]; + + [coordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + +- (void)serviceTermsModalCoordinatorBridgePresenterDelegateDidClose:(ServiceTermsModalCoordinatorBridgePresenter * _Nonnull)coordinatorBridgePresenter +{ + [self updateSections]; // Disables the contacts toggle as the terms weren't accepted. + self.serviceTermsModalCoordinatorBridgePresenter = nil; +} + #pragma mark - TableViewSectionsDelegate - (void)tableViewSectionsDidUpdateSections:(TableViewSections *)sections diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift index 31880d487..1fd948dd0 100644 --- a/Riot/Modules/SplitView/SplitViewCoordinator.swift +++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift @@ -32,6 +32,12 @@ class SplitViewCoordinatorParameters { final class SplitViewCoordinator: NSObject, SplitViewCoordinatorType { + // MARK: - Constants + + private enum Constants { + static let detailModulesCheckDelay: Double = 0.3 + } + // MARK: - Properties // MARK: Private @@ -42,6 +48,11 @@ final class SplitViewCoordinator: NSObject, SplitViewCoordinatorType { private weak var masterPresentable: SplitViewMasterPresentable? private var detailNavigationController: UINavigationController? + private var detailNavigationRouter: NavigationRouterType? + + private var selectedNavigationRouter: NavigationRouterType? { + return self.masterPresentable?.selectedNavigationRouter + } private weak var tabBarCoordinator: TabBarCoordinatorType? @@ -77,13 +88,17 @@ final class SplitViewCoordinator: NSObject, SplitViewCoordinatorType { self.splitViewController.delegate = self + // Create primary controller let tabBarCoordinator = self.createTabBarCoordinator() tabBarCoordinator.delegate = self tabBarCoordinator.splitViewMasterPresentableDelegate = self tabBarCoordinator.start(with: spaceId) - let detailNavigationController = self.createDetailNavigationController() + // Create secondary controller + let placeholderDetailViewController = self.createPlaceholderDetailsViewController() + let detailNavigationController = RiotNavigationController(rootViewController: placeholderDetailViewController) + // Setup split view controller self.splitViewController.viewControllers = [tabBarCoordinator.toPresentable(), detailNavigationController] self.add(childCoordinator: tabBarCoordinator) @@ -91,8 +106,11 @@ final class SplitViewCoordinator: NSObject, SplitViewCoordinatorType { self.tabBarCoordinator = tabBarCoordinator self.masterPresentable = tabBarCoordinator self.detailNavigationController = detailNavigationController + self.detailNavigationRouter = NavigationRouter(navigationController: detailNavigationController) self.parameters.router.setRootModule(self.splitViewController) + + self.registerNavigationRouterNotifications() } else { // Pop to home screen when selecting a new space self.popToHome(animated: true) { @@ -105,7 +123,7 @@ final class SplitViewCoordinator: NSObject, SplitViewCoordinatorType { func toPresentable() -> UIViewController { return self.splitViewController } - + // TODO: Do not expose publicly this method func restorePlaceholderDetails() { // Be sure that the primary is then visible too. @@ -113,22 +131,14 @@ final class SplitViewCoordinator: NSObject, SplitViewCoordinatorType { splitViewController.preferredDisplayMode = .allVisible } - if splitViewController.viewControllers.count == 2 { - let mainViewController = splitViewController.viewControllers[0] - - let emptyDetailsViewController = self.createPlaceholderDetailsViewController() - - splitViewController.viewControllers = [mainViewController, emptyDetailsViewController] - } + self.resetDetailNavigationControllerWithPlaceholder(animated: false) // Release the current selected item (room/contact/group...). self.tabBarCoordinator?.releaseSelectedItems() } func popToHome(animated: Bool, completion: (() -> Void)?) { - if let secondNavController = self.detailNavigationController { - secondNavController.popToRootViewController(animated: animated) - } + self.resetDetailNavigationControllerWithPlaceholder(animated: animated) // Force back to the main screen if this is not the one that is displayed self.tabBarCoordinator?.popToHome(animated: animated, completion: completion) @@ -149,54 +159,196 @@ final class SplitViewCoordinator: NSObject, SplitViewCoordinatorType { return tabBarCoordinator } - private func createDetailNavigationController() -> UINavigationController { - let placeholderDetailViewController = self.createPlaceholderDetailsViewController() - let detailNavigationController = RiotNavigationController(rootViewController: placeholderDetailViewController) - return detailNavigationController + private func resetDetailNavigationControllerWithPlaceholder(animated: Bool) { + guard let detailNavigationRouter = self.detailNavigationRouter else { + return + } + + // Check if placeholder is already shown + if detailNavigationRouter.modules.count == 1 && detailNavigationRouter.modules.last is PlaceholderDetailViewController { + return + } + + // Set placeholder screen as root controller of detail navigation controller + let placeholderDetailsVC = self.createPlaceholderDetailsViewController() + detailNavigationRouter.setRootModule(placeholderDetailsVC, hideNavigationBar: false, animated: animated, popCompletion: nil) + } + + private func isPlaceholderShown(from secondaryViewController: UIViewController) -> Bool { + + if let detailNavigationController = secondaryViewController as? UINavigationController, let topViewController = detailNavigationController.viewControllers.last { + return topViewController is PlaceholderDetailViewController + } else { + return secondaryViewController is PlaceholderDetailViewController + } + } + + private func releaseRoomDataSourceIfNeeded(for roomCoordinator: RoomCoordinatorProtocol) { + + guard roomCoordinator.canReleaseRoomDataSource, + let session = roomCoordinator.mxSession, + let roomId = roomCoordinator.roomId else { + return + } + + let existingRoomCoordinatorWithSameRoomId = self.detailModules.first { presentable -> Bool in + if let currentRoomCoordinator = presentable as? RoomCoordinatorProtocol { + return currentRoomCoordinator.roomId == roomCoordinator.roomId + } + return false + } + + guard existingRoomCoordinatorWithSameRoomId == nil else { + MXLog.debug("[SplitViewCoordinator] Do not release RoomDataSource for room id \(roomId), another RoomCoordinator with same room id using it") + return + } + + let dataSourceManager = MXKRoomDataSourceManager.sharedManager(forMatrixSession: session) + dataSourceManager?.closeRoomDataSource(withRoomId: roomId, forceClose: false) + } + + private func registerNavigationRouterNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(navigationRouterDidPopViewController(_:)), name: NavigationRouter.didPopModule, object: nil) + } + + @objc private func navigationRouterDidPopViewController(_ notification: Notification) { + + guard let userInfo = notification.userInfo, + let navigationRouter = userInfo[NavigationRouter.NotificationUserInfoKey.navigationRouter] as? NavigationRouterType, + let poppedController = userInfo[NavigationRouter.NotificationUserInfoKey.viewController] as? UIViewController else { + return + } + + // In our split view configuration is possible to have nested navigation controller (see https://blog.malcolmhall.com/2017/01/27/default-behaviour-of-uisplitviewcontroller-collapsesecondaryviewcontroller/)). + // When the split view controller has one column visible with the detail navigation controller nested inside the primary, + // check to see whether the primary navigation controller is popping the detail navigation controller. + // In this case the detail navigation controller will be popped but not its content. It means completions will not be called. + if navigationRouter === self.selectedNavigationRouter, + let poppedNavigationController = poppedController as? UINavigationController, + poppedNavigationController == self.detailNavigationController { + + // Clear the detailNavigationRouter to trigger completions associated to each controllers + self.detailNavigationRouter?.popAllModules(animated: false) + } + + if let poppedModule = userInfo[NavigationRouter.NotificationUserInfoKey.module] as? Presentable { + + if let roomCoordinator = poppedModule as? RoomCoordinatorProtocol { + + // If the RoomCoordinator view controller is popped from the detail navigation controller, check if the associated room data source should be released. + // If there is no other RoomCoordinator using the same data source, release it. + // A small delay is set to be sure navigation stack manipulation ended before checking the whole stack. + DispatchQueue.main.asyncAfter(deadline: .now() + Constants.detailModulesCheckDelay) { + self.releaseRoomDataSourceIfNeeded(for: roomCoordinator) + } + } + } } } // MARK: - UISplitViewControllerDelegate extension SplitViewCoordinator: UISplitViewControllerDelegate { + /// Provide the new secondary view controller for the split view interface. + /// This method returns the view controller to use as the secondary view controller in the expanded split view interface (when 2 column are visible). + /// Sample case: large iPhone goes from portrait to landsacpe. func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? { - if let detailViewController = self.masterPresentable?.secondViewControllerWhenSeparatedFromPrimary() { - return detailViewController + // If the primary root controller of the UISplitViewController is a UINavigationController, + // it's possible to have nested navigation controllers due to private property `_allowNestedNavigationControllers` set to true (https://blog.malcolmhall.com/2017/01/27/default-behaviour-of-uisplitviewcontroller-collapsesecondaryviewcontroller/). + // So if the top view controller of the primary navigation controller is a navigation controller and it corresponds to the existing `detailNavigationController` instance. + // Return `detailNavigationController` as is, it will be used as the secondary view of the split view controller. + if let topMostNavigationController = self.selectedNavigationRouter?.modules.last as? UINavigationController, topMostNavigationController == self.detailNavigationController { + + return self.detailNavigationController } - // Else return the default empty details view controller from the storyboard. + // Else return the default empty details view controller. // Be sure that the primary is then visible too. if splitViewController.displayMode == .primaryHidden { splitViewController.preferredDisplayMode = .allVisible } - return self.createPlaceholderDetailsViewController() + // Restore detail navigation controller with placeholder as root + self.resetDetailNavigationControllerWithPlaceholder(animated: false) + + // Return up to date detail navigation controller + // In any cases `detailNavigationController` will be used as secondary view of the split view controller. + return self.detailNavigationController } + /// Adjust the primary view controller and incorporate the secondary view controller into the collapsed interface if needed. + /// Return false to let the split view controller try to incorporate the secondary view controller's content into the collapsed interface, + /// or true to indicate that you do not want the split view controller to do anything with the secondary view controller. + /// Sample case: large iPhone goes from landscape to portrait. func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool { - return self.masterPresentable?.collapseDetailViewController ?? false + + // If the secondary view is the placeholder screen do not merge the secondary into the primary. + // Note: In this case, the secondaryViewController will be automatically discarded. + if self.isPlaceholderShown(from: secondaryViewController) { + return true + } + + // Return false to let the split view controller try to incorporate the secondary view controller's content into the collapsed interface. + // If the primary root controller of a UISplitViewController is a UINavigationController, + // it's possible to have nested navigation controllers due to private property `_allowNestedNavigationControllers` set to true (https://blog.malcolmhall.com/2017/01/27/default-behaviour-of-uisplitviewcontroller-collapsesecondaryviewcontroller/). + // So in this case returning false here will push the `detailNavigationController` on top of the `primaryNavigationController`. + // Sample primary view stack: + // primaryNavigationController[ + // MasterTabBarController, + // detailNavigationController[RoomViewController, RoomInfoListViewController]] + // Note that normally pushing a navigation controller on top of a navigation controller don't work. + return false } } -/// MARK: - UINavigationControllerDelegate +// MARK: - TabBarCoordinatorDelegate extension SplitViewCoordinator: TabBarCoordinatorDelegate { func tabBarCoordinatorDidCompleteAuthentication(_ coordinator: TabBarCoordinatorType) { self.delegate?.splitViewCoordinatorDidCompleteAuthentication(self) } } -/// MARK: - SplitViewMasterPresentableDelegate +// MARK: - SplitViewMasterPresentableDelegate extension SplitViewCoordinator: SplitViewMasterPresentableDelegate { - func splitViewMasterPresentable(_ presentable: Presentable, wantsToDisplay detailPresentable: Presentable) { - MXLog.debug("[SplitViewCoordinator] splitViewMasterPresentable: \(presentable) wantsToDisplay detailPresentable: \(detailPresentable)") + + var detailModules: [Presentable] { + return self.detailNavigationRouter?.modules ?? [] + } + + func splitViewMasterPresentable(_ presentable: Presentable, wantsToReplaceDetailWith detailPresentable: Presentable, popCompletion: (() -> Void)?) { + MXLog.debug("[SplitViewCoordinator] splitViewMasterPresentable: \(presentable) wantsToReplaceDetailWith detailPresentable: \(detailPresentable)") guard let detailNavigationController = self.detailNavigationController else { MXLog.debug("[SplitViewCoordinator] splitViewMasterPresentable: Failed to display because detailNavigationController is nil") return } - detailNavigationController.viewControllers = [detailPresentable.toPresentable()] + let detailController = detailPresentable.toPresentable() + + // Reset the detail navigation controller with the given detail controller + self.detailNavigationRouter?.setRootModule(detailPresentable, popCompletion: popCompletion) + + // This will call first UISplitViewControllerDelegate method: `splitViewController(_:showDetail:sender:)`, if implemented, to give the opportunity to customise `UISplitViewController.showDetailViewController(:sender:)` behavior. + // - If the split view controller is collpased (one column visible): + // The `detailNavigationController` will be pushed on top of the primary navigation controller. + // In fact if the primary root controller of a UISplitViewController is a UINavigationController, + // it's possible to have nested navigation controllers due to private property `_allowNestedNavigationControllers` set to true (https://blog.malcolmhall.com/2017/01/27/default-behaviour-of-uisplitviewcontroller-collapsesecondaryviewcontroller/). + // - Else if the split view controller is not collpased (two column visible) + // It will set the `detailNavigationController` as the secondary view of the split view controller self.splitViewController.showDetailViewController(detailNavigationController, sender: nil) + + // Set leftBarButtonItem with split view display mode button if there is no leftBarButtonItem defined + detailController.vc_setupDisplayModeLeftBarButtonItemIfNeeded() + } + + func splitViewMasterPresentable(_ presentable: Presentable, wantsToStack detailPresentable: Presentable, popCompletion: (() -> Void)?) { + + guard let detailNavigationRouter = self.detailNavigationRouter else { + MXLog.debug("[SplitViewCoordinator] Failed to stack \(detailPresentable) because detailNavigationRouter is nil") + return + } + + detailNavigationRouter.push(detailPresentable, animated: true, popCompletion: popCompletion) } } diff --git a/Riot/Modules/SplitView/SplitViewPresentable.swift b/Riot/Modules/SplitView/SplitViewPresentable.swift index f09134d3f..3521c76bd 100644 --- a/Riot/Modules/SplitView/SplitViewPresentable.swift +++ b/Riot/Modules/SplitView/SplitViewPresentable.swift @@ -17,17 +17,30 @@ import UIKit protocol SplitViewMasterPresentableDelegate: AnyObject { - func splitViewMasterPresentable(_ presentable: Presentable, wantsToDisplay detailPresentable: Presentable) + + /// Detail items from the split view + var detailModules: [Presentable] { get } + + /// Replace split view detail with the given detailPresentable + func splitViewMasterPresentable(_ presentable: Presentable, wantsToReplaceDetailWith detailPresentable: Presentable, popCompletion: (() -> Void)?) + + /// Stack the detailPresentable on the existing split view detail stack + func splitViewMasterPresentable(_ presentable: Presentable, wantsToStack detailPresentable: Presentable, popCompletion: (() -> Void)?) +} + +/// `SplitViewMasterPresentableDelegate` default implementation +extension SplitViewMasterPresentableDelegate { + func splitViewMasterPresentable(_ presentable: Presentable, wantsToDisplay detailPresentable: Presentable) { + splitViewMasterPresentable(presentable, wantsToReplaceDetailWith: detailPresentable, popCompletion: nil) + } } /// Protocol used by the master view presentable of a UISplitViewController protocol SplitViewMasterPresentable: AnyObject, Presentable { - var splitViewMasterPresentableDelegate: SplitViewMasterPresentableDelegate? { get set } + var splitViewMasterPresentableDelegate: SplitViewMasterPresentableDelegate? { get set } - /// Indicate true if the detail can be collapsed - var collapseDetailViewController: Bool { get } - - /// Return the detail view controller to display when the detail is separated from the master view controller - func secondViewControllerWhenSeparatedFromPrimary() -> UIViewController? + /// Return the currently selected and visible NavigationRouter + /// It will be used to manage detail controllers + var selectedNavigationRouter: NavigationRouterType? { get } } diff --git a/Riot/Modules/StartChat/InviteFriendsHeaderView.swift b/Riot/Modules/StartChat/InviteFriendsHeaderView.swift index c1d81ea06..30325e704 100644 --- a/Riot/Modules/StartChat/InviteFriendsHeaderView.swift +++ b/Riot/Modules/StartChat/InviteFriendsHeaderView.swift @@ -50,27 +50,28 @@ final class InviteFriendsHeaderView: UIView, NibLoadable, Themable { override func awakeFromNib() { super.awakeFromNib() - self.button.setTitle(VectorL10n.inviteFriendsAction(BuildSettings.bundleDisplayName), for: .normal) - self.button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside) + button.setTitle(VectorL10n.inviteFriendsAction(BuildSettings.bundleDisplayName), for: .normal) + button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside) + button.layer.cornerRadius = 8 + button.layer.borderWidth = 2 } // MARK: - Public func update(theme: Theme) { - self.backgroundColor = theme.backgroundColor + button.layer.borderColor = theme.tintColor.cgColor + button.setTitleColor(theme.tintColor, for: .normal) + button.setTitleColor(theme.tintColor.withAlphaComponent(Constants.buttonHighlightedAlpha), for: .highlighted) + button.vc_setBackgroundColor(theme.baseColor, for: .normal) - self.button.setTitleColor(theme.baseTextPrimaryColor, for: .normal) - self.button.setTitleColor(theme.baseTextPrimaryColor.withAlphaComponent(Constants.buttonHighlightedAlpha), for: .highlighted) - self.button.vc_setBackgroundColor(theme.tintColor, for: .normal) + let buttonImage = Asset.Images.shareActionButton.image.vc_tintedImage(usingColor: theme.tintColor) - let buttonImage = Asset.Images.shareActionButton.image.vc_tintedImage(usingColor: theme.baseIconPrimaryColor) - - self.button.setImage(buttonImage, for: .normal) + button.setImage(buttonImage, for: .normal) } // MARK: - Action @objc private func buttonAction(_ sender: UIButton) { - self.delegate?.inviteFriendsHeaderView(self, didTapButton: button) + delegate?.inviteFriendsHeaderView(self, didTapButton: button) } } diff --git a/Riot/Modules/StartChat/InviteFriendsHeaderView.xib b/Riot/Modules/StartChat/InviteFriendsHeaderView.xib index fa0d1a1f6..3f62c617e 100644 --- a/Riot/Modules/StartChat/InviteFriendsHeaderView.xib +++ b/Riot/Modules/StartChat/InviteFriendsHeaderView.xib @@ -1,9 +1,9 @@ - + - + @@ -11,14 +11,14 @@ - +