diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index d1cd1b3a9..c86de1d80 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -16,7 +16,7 @@ env: jobs: build: name: Build - runs-on: macos-latest + runs-on: macos-11 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index eac0c885d..65846be15 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -16,7 +16,7 @@ env: jobs: tests: name: Tests - runs-on: macos-latest + runs-on: macos-11 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/triage-incoming.yml b/.github/workflows/triage-incoming.yml new file mode 100644 index 000000000..40d550741 --- /dev/null +++ b/.github/workflows/triage-incoming.yml @@ -0,0 +1,15 @@ +name: Move new issues onto Issue triage board + +on: + issues: + types: [opened] + +jobs: + automate-project-columns: + runs-on: ubuntu-latest + steps: + - uses: alex-page/github-project-automation-plus@v0.8.1 + with: + project: Issue triage + column: Incoming + repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/.github/workflows/triage-needs-info.yml b/.github/workflows/triage-needs-info.yml new file mode 100644 index 000000000..4a4a6a7c0 --- /dev/null +++ b/.github/workflows/triage-needs-info.yml @@ -0,0 +1,16 @@ +name: Move X-Needs-Info into Need info column in the Issue triage board + +on: + issues: + types: [labeled] + +jobs: + Move_Labeled_Issue_On_Project_Board: + runs-on: ubuntu-latest + steps: + - uses: konradpabjan/move-labeled-or-milestoned-issue@v2.0 + with: + action-token: ${{ secrets.GITHUB_TOKEN }} + project-url: "https://github.com/vector-im/element-ios/projects/12" + column-name: "Need info" + label-name: "X-Needs-Info" 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 ecde7c1af..ce2055130 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,21 @@ +## Changes in 1.6.5 (2021-10-14) + +🙌 Improvements + +- Upgrade MatrixKit version ([v0.16.7](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.16.7)). + + +## Changes in 1.6.4 (2021-10-12) + +🙌 Improvements + +- Upgrade MatrixKit version ([v0.16.6](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.16.6)). + +🐛 Bugfixes + +- RoomVC: Fix a crash when previewing a room. ([#4982](https://github.com/vector-im/element-ios/issues/4982)) + + ## Changes in 1.6.2 (2021-10-08) 🙌 Improvements diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 90034d66f..6dab241e0 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.6.3 -CURRENT_PROJECT_VERSION = 1.6.3 +MARKETING_VERSION = 1.6.6 +CURRENT_PROJECT_VERSION = 1.6.6 diff --git a/Podfile b/Podfile index 49dec32b5..b373a6f43 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.5' +$matrixKitVersion = '= 0.16.7' # $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' @@ -55,7 +56,6 @@ abstract_target 'RiotPods' do # Remove warnings from "bad" pods pod 'OLMKit', :inhibit_warnings => true pod 'zxcvbn-ios', :inhibit_warnings => true - pod 'HPGrowingTextView', :inhibit_warnings => true # Tools pod 'SwiftGen', '~> 6.3' @@ -73,6 +73,7 @@ abstract_target 'RiotPods' do pod 'SideMenu', '~> 6.5' pod 'DSWaveformImage', '~> 6.1.1' pod 'ffmpeg-kit-ios-audio', '~> 4.5' + pod 'GrowingTextView', '~> 0.7.2' pod 'FLEX', '~> 4.5.0', :configurations => ['Debug'] diff --git a/Podfile.lock b/Podfile.lock index 4a7bfb316..2077a2dab 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -58,29 +58,29 @@ PODS: - MatomoTracker (7.4.1): - MatomoTracker/Core (= 7.4.1) - MatomoTracker/Core (7.4.1) - - MatrixKit (0.16.5): + - MatrixKit (0.16.7): - Down (~> 0.11.0) - DTCoreText (~> 1.6.25) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixKit/Core (= 0.16.5) - - MatrixSDK (= 0.20.5) - - MatrixKit/Core (0.16.5): + - MatrixKit/Core (= 0.16.7) + - MatrixSDK (= 0.20.7) + - MatrixKit/Core (0.16.7): - Down (~> 0.11.0) - DTCoreText (~> 1.6.25) - HPGrowingTextView (~> 1.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.20.5) - - MatrixSDK (0.20.5): - - MatrixSDK/Core (= 0.20.5) - - MatrixSDK/Core (0.20.5): + - MatrixSDK (= 0.20.7) + - MatrixSDK (0.20.7): + - MatrixSDK/Core (= 0.20.7) + - MatrixSDK/Core (0.20.7): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - OLMKit (~> 3.2.5) - Realm (= 10.16.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.20.5): + - MatrixSDK/JingleCallStack (0.20.7): - JitsiMeetSDK (= 3.10.2) - MatrixSDK/Core - OLMKit (3.2.5): @@ -124,7 +124,7 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - MatomoTracker (~> 7.4.1) - - MatrixKit (= 0.16.5) + - MatrixKit (= 0.16.7) - MatrixSDK - MatrixSDK/JingleCallStack - OLMKit @@ -204,8 +204,8 @@ SPEC CHECKSUMS: LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b MatomoTracker: 24a846c9d3aa76933183fe9d47fd62c9efa863fb - MatrixKit: a37efb94bb7c53b5dc912f0fd35971861b6c28bf - MatrixSDK: 417fac309f510b5f8ac121ba8abe3b897953e1ce + MatrixKit: d0346f60c7d0723066f6a3e94ebee789edc1f580 + MatrixSDK: 1d7a64d1e25f746e35157a68374b4282b5581188 OLMKit: 9fb4799c4a044dd2c06bda31ec31a12191ad30b5 ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d Realm: b6027801398f3743fc222f096faa85281b506e6c @@ -219,6 +219,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: e189a08f2a6f081d6eb0f57aaa898833f27a9adb +PODFILE CHECKSUM: 3c829592a4e938c0248c7eb66e1aa9c4493b2334 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 0ff445f39..bad6db43d 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 b6f7f626c..83b830e6c 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 e53eed47b..db36f708a 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/Managers/URLPreviews/URLPreviewService.swift b/Riot/Managers/URLPreviews/URLPreviewService.swift index e731c27db..f45988b0b 100644 --- a/Riot/Managers/URLPreviews/URLPreviewService.swift +++ b/Riot/Managers/URLPreviews/URLPreviewService.swift @@ -15,6 +15,7 @@ // import Foundation +import AFNetworking enum URLPreviewServiceError: Error { case missingResponse @@ -74,7 +75,10 @@ class URLPreviewService: NSObject { success(previewData) } - }, failure: failure) + }, failure: { error in + self.checkForDisabledAPI(in: error) + failure(error) + }) } /// Removes any cached preview data that has expired. @@ -82,9 +86,11 @@ class URLPreviewService: NSObject { store.removeExpiredItems() } - /// Deletes all cached preview data and closed previews from the store. + /// Deletes all cached preview data and closed previews from the store, + /// re-enabling URL previews if they have been disabled by `checkForDisabledAPI`. func clearStore() { store.deleteAll() + MXKAppSettings.standard().enableBubbleComponentLinkDetection = true } /// Store the `eventId` and `roomId` of a closed preview. @@ -156,4 +162,20 @@ class URLPreviewService: NSObject { return components?.url ?? url } + + /// Checks an error returned from `MXRestClient` to see whether the previews API + /// has been disabled on the homeserver. If this is true, link detection will be disabled + /// to prevent further requests being made and stop any previews loaders being presented. + private func checkForDisabledAPI(in error: Error?) { + // The error we're looking for is a generic 404 and not a matrix error. + guard + !MXError.isMXError(error), + let response = MXHTTPOperation.urlResponse(fromError: error) + else { return } + + if response.statusCode == 404 { + MXLog.debug("[URLPreviewService] Disabling link detection as homeserver does not support URL previews.") + MXKAppSettings.standard().enableBubbleComponentLinkDetection = false + } + } } diff --git a/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift b/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift index f3cf06e13..a0eab4c13 100644 --- a/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift +++ b/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift @@ -41,20 +41,16 @@ final class HomeserverConfigurationBuilder: NSObject { } // Encryption configuration - if let vectorWellKnownEncryptionConfig = vectorWellKnownEncryptionConfiguration { - isE2EEByDefaultEnabled = vectorWellKnownEncryptionConfig.isE2EEByDefaultEnabled - } else { - // Enable E2EE by default when there is no value - isE2EEByDefaultEnabled = true - } + // Enable E2EE by default when there is no value + isE2EEByDefaultEnabled = vectorWellKnownEncryptionConfiguration?.isE2EEByDefaultEnabled ?? true // Jitsi configuration let jitsiServerURL: URL let hardcodedJitsiServerURL: URL = BuildSettings.jitsiServerUrl - if let vectorWellKnownJitsiConfig = vectorWellKnownJitsiConfiguration { - jitsiPreferredDomain = vectorWellKnownJitsiConfig.preferredDomain - jitsiServerURL = self.jitsiServerURL(from: jitsiPreferredDomain) ?? hardcodedJitsiServerURL + if let preferredDomain = vectorWellKnownJitsiConfiguration?.preferredDomain { + jitsiPreferredDomain = preferredDomain + jitsiServerURL = self.jitsiServerURL(from: preferredDomain) ?? hardcodedJitsiServerURL } else { guard let hardcodedJitsiDomain = hardcodedJitsiServerURL.host else { fatalError("[HomeserverConfigurationBuilder] Fail to get Jitsi domain from hardcoded Jitsi URL") diff --git a/Riot/Model/WellKnown/VectorWellKnown.swift b/Riot/Model/WellKnown/VectorWellKnown.swift index 2a44c0c19..dd548d6b2 100644 --- a/Riot/Model/WellKnown/VectorWellKnown.swift +++ b/Riot/Model/WellKnown/VectorWellKnown.swift @@ -45,7 +45,7 @@ extension VectorWellKnown: Decodable { struct VectorWellKnownEncryptionConfiguration: Decodable { /// Indicate if E2EE is enabled by default - let isE2EEByDefaultEnabled: Bool + let isE2EEByDefaultEnabled: Bool? enum CodingKeys: String, CodingKey { case isE2EEByDefaultEnabled = "default" @@ -56,5 +56,5 @@ struct VectorWellKnownEncryptionConfiguration: Decodable { struct VectorWellKnownJitsiConfiguration: Decodable { /// Default Jitsi server - let preferredDomain: String + let preferredDomain: String? } 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..c86695012 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) { @@ -4131,82 +4127,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 790b26afd..d15bbdb0a 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 cf6077adf..a38edf5c1 100644 --- a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m +++ b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m @@ -126,11 +126,17 @@ if (roomCellData.spaceChildInfo) { - [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.spaceChildInfo.avatarUrl displayName:roomCellData.spaceChildInfo.displayName mediaManager:roomCellData.recentsDataSource.mxSession.mediaManager]; + [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.spaceChildInfo.avatarUrl + roomId:roomCellData.spaceChildInfo.childRoomId + displayName:roomCellData.spaceChildInfo.displayName + mediaManager:roomCellData.recentsDataSource.mxSession.mediaManager]; } else { - [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.roomSummary.avatar displayName:roomCellData.roomSummary.displayname mediaManager:roomCellData.roomSummary.mxSession.mediaManager]; + [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.roomSummary.avatar + roomId:roomCellData.roomSummary.roomId + displayName:roomCellData.roomSummary.displayname + mediaManager:roomCellData.roomSummary.mxSession.mediaManager]; } } else diff --git a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift index 504504312..3e9ad98b4 100644 --- a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift +++ b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift @@ -41,10 +41,22 @@ class VectorHostingController: UIHostingController { override func viewDidLoad() { super.viewDidLoad() + + self.view.backgroundColor = .clear + self.registerThemeServiceDidChangeThemeNotification() self.update(theme: self.theme) } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // Fixes weird iOS 15 bug where the view no longer grows its enclosing host + if #available(iOS 15.0, *) { + self.view.invalidateIntrinsicContentSize() + } + } + private func registerThemeServiceDidChangeThemeNotification() { NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) } @@ -54,8 +66,6 @@ class VectorHostingController: UIHostingController { } private func update(theme: Theme) { - self.view.backgroundColor = theme.headerBackgroundColor - if let navigationBar = self.navigationController?.navigationBar { theme.applyStyle(onNavigationBar: navigationBar) } 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 bb0b4879a..8bc80d428 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 @@ -245,6 +246,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 f8986da55..26fba7ec0 100644 --- a/Riot/Modules/Home/Views/RoomCollectionViewCell.m +++ b/Riot/Modules/Home/Views/RoomCollectionViewCell.m @@ -132,11 +132,17 @@ if (roomCellData.roomSummary) { - [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.roomSummary.avatar displayName:roomCellData.roomSummary.displayname mediaManager:roomCellData.roomSummary.mxSession.mediaManager]; + [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.roomSummary.avatar + roomId:roomCellData.roomSummary.roomId + displayName:roomCellData.roomSummary.displayname + mediaManager:roomCellData.roomSummary.mxSession.mediaManager]; } else { - [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.spaceChildInfo.avatarUrl displayName:roomCellData.spaceChildInfo.displayName mediaManager:roomCellData.recentsDataSource.mxSession.mediaManager]; + [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.spaceChildInfo.avatarUrl + roomId:roomCellData.spaceChildInfo.childRoomId + displayName:roomCellData.spaceChildInfo.displayName + mediaManager:roomCellData.recentsDataSource.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/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index ba19a08fe..a71e6d72e 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -1117,6 +1117,8 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat }); } failure:^(NSError * _Nullable error) { + MXStrongifyAndReturnIfNil(self); + MXLogDebug(@"[RoomBubbleCellData] Failed to get url preview") // Remove the loading URLPreviewView, indicate that the layout needs refreshing and send a notification for refresh diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 7f94a8c2f..9817f503d 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -377,6 +377,7 @@ const CGFloat kTypingCellHeight = 24; urlPreviewView = [URLPreviewView instantiate]; urlPreviewView.preview = component.urlPreviewData; urlPreviewView.delegate = self; + urlPreviewView.tag = index; [temporaryViews addObject:urlPreviewView]; @@ -416,6 +417,7 @@ const CGFloat kTypingCellHeight = 24; reactionsView = [BubbleReactionsView new]; reactionsView.viewModel = bubbleReactionsViewModel; + reactionsView.tag = index; [reactionsView updateWithTheme:ThemeService.shared.theme]; bubbleReactionsViewModel.viewModelDelegate = 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 7fc46819a..d32da8258 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -137,7 +137,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @interface RoomViewController () + RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate> { // The preview header @@ -249,6 +249,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, strong) VoiceMessageController *voiceMessageController; @property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter; +@property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator; +@property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView; + @end @implementation RoomViewController @@ -410,6 +413,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. @@ -449,6 +455,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self userInterfaceThemeDidChange]; }]; + [self userInterfaceThemeDidChange]; // Observe URL preview updates. @@ -1016,6 +1023,12 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [VoiceMessageMediaServiceProvider.sharedProvider setCurrentRoomSummary:dataSource.room.summary]; _voiceMessageController.roomId = dataSource.roomId; + + _userSuggestionCoordinator = [[UserSuggestionCoordinatorBridge alloc] initWithMediaManager:self.roomDataSource.mxSession.mediaManager + room:dataSource.room]; + _userSuggestionCoordinator.delegate = self; + + [self setupUserSuggestionView]; } - (void)onRoomDataSourceReady @@ -2209,6 +2222,27 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } } +- (void)setupUserSuggestionView +{ + if(!self.isViewLoaded) { + MXLogError(@"Failed setting up user suggestions. View not loaded."); + return; + } + + UIViewController *suggestionsViewController = self.userSuggestionCoordinator.toPresentable; + [suggestionsViewController.view setTranslatesAutoresizingMaskIntoConstraints:NO]; + + [self addChildViewController:suggestionsViewController]; + [self.userSuggestionContainerView addSubview:suggestionsViewController.view]; + + [NSLayoutConstraint activateConstraints:@[[suggestionsViewController.view.topAnchor constraintEqualToAnchor:self.userSuggestionContainerView.topAnchor], + [suggestionsViewController.view.leadingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.leadingAnchor], + [suggestionsViewController.view.trailingAnchor constraintEqualToAnchor:self.userSuggestionContainerView.trailingAnchor], + [suggestionsViewController.view.bottomAnchor constraintEqualToAnchor:self.userSuggestionContainerView.bottomAnchor],]]; + + [suggestionsViewController didMoveToParentViewController:self]; +} + #pragma mark - Jitsi - (void)showJitsiCallWithWidget:(Widget*)widget @@ -4197,6 +4231,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { [self cancelEventSelection]; } + +- (void)roomInputToolbarViewDidChangeTextMessage:(MXKRoomInputToolbarView *)toolbarView +{ + [self.userSuggestionCoordinator processTextMessage:toolbarView.textMessage]; +} #pragma mark - MXKRoomMemberDetailsViewControllerDelegate @@ -6514,4 +6553,22 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [[LegacyAppDelegate theDelegate] openSpaceWithId:spaceId]; } +#pragma mark - UserSuggestionCoordinatorBridgeDelegate + +- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator + didRequestMentionForMember:(MXRoomMember *)member + textTrigger:(NSString *)textTrigger +{ + if (textTrigger.length) { + NSString *textMessage = [self.inputToolbarView textMessage]; + textMessage = [textMessage stringByReplacingOccurrencesOfString:textTrigger + withString:@"" + options:NSBackwardsSearch | NSAnchoredSearch + range:NSMakeRange(0, textMessage.length)]; + [self.inputToolbarView setTextMessage:textMessage]; + } + + [self mention:member]; +} + @end diff --git a/Riot/Modules/Room/RoomViewController.xib b/Riot/Modules/Room/RoomViewController.xib index 5b1a0a909..1c8f83913 100644 --- a/Riot/Modules/Room/RoomViewController.xib +++ b/Riot/Modules/Room/RoomViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -32,6 +32,7 @@ + @@ -136,14 +137,14 @@