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 ce2055130..966ee4efa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,40 @@ +## Changes in 1.6.6 (2021-10-21) + +✨ Features + +- M10.4.1 Home space data filtering ([#4570](https://github.com/vector-im/element-ios/issues/4570)) +- Implemented message forwarding from within the main application. Updated the share extension designs. ([#5009](https://github.com/vector-im/element-ios/issues/5009)) + +🙌 Improvements + +- Settings: Refresh the appearance of headers and footers, with a few minor tweaks to the organisation. ([#5011](https://github.com/vector-im/element-ios/pull/5011)) +- Upgrade MatrixKit version ([v0.16.9](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.16.9)). +- RecentsDataSource: Refactorings for lazy loading room summaries. ([#4384](https://github.com/vector-im/element-ios/issues/4384)) +- Contacts Access: Request access via a button tap in the new Find Your Contacts footer instead of doing it automatically. ([#4484](https://github.com/vector-im/element-ios/issues/4484)) +- Navigation: Create RoomCoordinator. ([#4734](https://github.com/vector-im/element-ios/issues/4734)) +- Navigation: Enable room stacking. ([#4834](https://github.com/vector-im/element-ios/issues/4834)) +- SwiftUI: Add FramePreferenceKey for use in ViewFrameReader. ([#4974](https://github.com/vector-im/element-ios/issues/4974)) +- URL Previews: Stop requesting URL previews if the feature has been disabled on the homeserver. ([#5002](https://github.com/vector-im/element-ios/issues/5002)) +- VectorWellKnown: Make all properties optional. ([#5008](https://github.com/vector-im/element-ios/issues/5008)) + +🐛 Bugfixes + +- Message Composer: Pasting images from Safari now pastes the image and not its URL. ([#2076](https://github.com/vector-im/element-ios/issues/2076)) +- Fixed private space invite should use lock icon instead of planet ([#4886](https://github.com/vector-im/element-ios/issues/4886)) +- Room Lists: Fix generated avatar colours not matching Element Web. ([#4978](https://github.com/vector-im/element-ios/issues/4978)) +- Contacts Sync: Move call to validateSyncLocalContactsState into MatrixKit. ([#4989](https://github.com/vector-im/element-ios/issues/4989)) +- Timeline: Selecting a message now correctly selects any reactions and URL previews too. ([#4992](https://github.com/vector-im/element-ios/issues/4992)) + +🧱 Build + +- Build: Update to Xcode 12.5 in the Fastfile and macOS 11 in the GitHub actions. ([#4998](https://github.com/vector-im/element-ios/pull/4998)) + +Others + +- Replaced deprecated HPGrowingTextView with GrowingTextView. ([#4976](https://github.com/vector-im/element-ios/issues/4976)) +- Move new issues into incoming column and move X-Needs-Info into Need info column on the issue triage board ([#5012](https://github.com/vector-im/element-ios/issues/5012)) + + ## Changes in 1.6.5 (2021-10-14) 🙌 Improvements diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 79d20fa80..6dab241e0 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.6.5 -CURRENT_PROJECT_VERSION = 1.6.5 +MARKETING_VERSION = 1.6.6 +CURRENT_PROJECT_VERSION = 1.6.6 diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 321fb8b30..bb1b25a0a 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -281,6 +281,9 @@ final class BuildSettings: NSObject { static let roomScreenAllowStickerAction: Bool = true static let roomScreenAllowFilesAction: Bool = true + /// Allow split view detail view stacking + static let allowSplitViewDetailsScreenStacking: Bool = true + // MARK: - Room Contextual Menu static let roomContextualMenuShowMoreOptionForMessages: Bool = true diff --git a/DesignKit/Source/FontsUIkit.swift b/DesignKit/Source/FontsUIkit.swift index b39f15f1b..3067d03a1 100644 --- a/DesignKit/Source/FontsUIkit.swift +++ b/DesignKit/Source/FontsUIkit.swift @@ -20,7 +20,7 @@ import UIKit /** ObjC class for holding fonts for use in UIKit. */ -@objc public class FontsUIKit: NSObject, Fonts { +@objcMembers public class FontsUIKit: NSObject, Fonts { public var largeTitle: UIFont diff --git a/Podfile b/Podfile index 171dce6b7..dec1aaefb 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.7' +$matrixKitVersion = '= 0.16.9' # $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/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/Images.xcassets/Spaces/space_private_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/Contents.json new file mode 100644 index 000000000..6bb7945f7 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "space_private_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "space_private_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "space_private_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon.png b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon.png new file mode 100644 index 000000000..10bc95ab8 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon.png differ diff --git a/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@2x.png b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@2x.png new file mode 100644 index 000000000..cae6344e1 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@3x.png b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@3x.png new file mode 100644 index 000000000..eaf48592a Binary files /dev/null and b/Riot/Assets/Images.xcassets/Spaces/space_private_icon.imageset/space_private_icon@3x.png differ diff --git a/Riot/Assets/SharedImages.xcassets/Contents.json b/Riot/Assets/SharedImages.xcassets/Contents.json index da4a164c9..73c00596a 100644 --- a/Riot/Assets/SharedImages.xcassets/Contents.json +++ b/Riot/Assets/SharedImages.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/Contents.json b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/Contents.json new file mode 100644 index 000000000..35812152a --- /dev/null +++ b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "radio-button-default.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "radio-button-default@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "radio-button-default@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default.png b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default.png new file mode 100644 index 000000000..84e419079 Binary files /dev/null and b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default.png differ diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@2x.png b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@2x.png new file mode 100644 index 000000000..7e6083bc3 Binary files /dev/null and b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@2x.png differ diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@3x.png b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@3x.png new file mode 100644 index 000000000..316a8eab7 Binary files /dev/null and b/Riot/Assets/SharedImages.xcassets/radio-button-default.imageset/radio-button-default@3x.png differ diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/Contents.json b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/Contents.json new file mode 100644 index 000000000..a69d70fe6 --- /dev/null +++ b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "radio-button-selected.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "radio-button-selected@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "radio-button-selected@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected.png b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected.png new file mode 100644 index 000000000..6a744d6be Binary files /dev/null and b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected.png differ diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@2x.png b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@2x.png new file mode 100644 index 000000000..67c3bbd64 Binary files /dev/null and b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@2x.png differ diff --git a/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@3x.png b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@3x.png new file mode 100644 index 000000000..a4cd21452 Binary files /dev/null and b/Riot/Assets/SharedImages.xcassets/radio-button-selected.imageset/radio-button-selected@3x.png differ diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 244995f29..7dd14568c 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -1483,3 +1483,4 @@ "space_tag" = "Space"; "open" = "Öffnen"; "settings_links" = "LINKS"; +"find_your_contacts_footer" = "Dies kann jederzeit in den Einstellungen deaktiviert werden."; diff --git a/Riot/Assets/en.lproj/InfoPlist.strings b/Riot/Assets/en.lproj/InfoPlist.strings index 468d42cce..6d3378a2a 100644 --- a/Riot/Assets/en.lproj/InfoPlist.strings +++ b/Riot/Assets/en.lproj/InfoPlist.strings @@ -18,6 +18,6 @@ "NSCameraUsageDescription" = "The camera is used to take photos and videos, make video calls."; "NSPhotoLibraryUsageDescription" = "The photo library is used to send photos and videos."; "NSMicrophoneUsageDescription" = "Element needs to access your microphone to make and receive calls, take videos, and record voice messages."; -"NSContactsUsageDescription" = "To discover contacts already using Matrix, Element can send email addresses and phone numbers in your address book to your chosen Matrix identity server. Where supported, personal data is hashed before sending - please check your identity server's privacy policy for more details."; +"NSContactsUsageDescription" = "Element will show your contacts so you can invite them to chat."; "NSCalendarsUsageDescription" = "See your scheduled meetings in the app."; "NSFaceIDUsageDescription" = "Face ID is used to access your app."; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 32c50a8e7..844626362 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"; @@ -353,6 +360,7 @@ Tap the + to start adding people."; "room_event_action_redact" = "Remove"; "room_event_action_more" = "More"; "room_event_action_share" = "Share"; +"room_event_action_forward" = "Forward"; "room_event_action_permalink" = "Permalink"; "room_event_action_view_source" = "View Source"; "room_event_action_view_decrypted_source" = "View Decrypted Source"; @@ -449,7 +457,6 @@ Tap the + to start adding people."; "settings_report_bug" = "Report bug"; "settings_clear_cache" = "Clear cache"; "settings_config_home_server" = "Homeserver is %@"; -"settings_config_identity_server" = "Identity server is %@"; "settings_config_user_id" = "Logged in as %@"; "settings_user_settings" = "USER SETTINGS"; @@ -462,9 +469,10 @@ 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_about" = "ABOUT"; "settings_labs" = "LABS"; "settings_flair" = "Show flair where allowed"; "settings_devices" = "SESSIONS"; @@ -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."; @@ -619,6 +628,7 @@ Tap the + to start adding people."; "settings_discovery_no_identity_server" = "You are not currently using an identity server. To be discoverable by existing contacts you known, add one."; "settings_discovery_terms_not_signed" = "Agree to the identity server (%@) Terms of Service to allow yourself to be discoverable by email address or phone number."; +"settings_discovery_accept_terms" = "Accept Identity Server Terms"; "settings_discovery_three_pids_management_information_part1" = "Manage which email addresses or phone numbers other users can use to discover you and use to invite you to rooms. Add or remove email addresses or phone numbers from this list in "; "settings_discovery_three_pids_management_information_part2" = "User Settings"; "settings_discovery_three_pids_management_information_part3" = "."; @@ -1020,18 +1030,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 %@"; @@ -1718,6 +1731,7 @@ Tap the + to start adding people."; "spaces_coming_soon_detail" = "This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with Element on your computer."; "space_participants_action_remove" = "Remove from this space"; "space_participants_action_ban" = "Ban from this space"; +"space_home_show_all_rooms" = "Show all rooms"; "space_private_join_rule" = "Private space"; "space_public_join_rule" = "Public space"; diff --git a/Riot/Assets/et.lproj/InfoPlist.strings b/Riot/Assets/et.lproj/InfoPlist.strings index 238ddd486..25a5d9479 100644 --- a/Riot/Assets/et.lproj/InfoPlist.strings +++ b/Riot/Assets/et.lproj/InfoPlist.strings @@ -3,5 +3,5 @@ "NSPhotoLibraryUsageDescription" = "Fotogaleriid kasutatakse fotode ja videote saatmiseks teistele kasutajatele."; "NSMicrophoneUsageDescription" = "Kõnede tegemiseks, videote ja häälsõnumite salvestamiseks vajab Element ligipääsu sinu seadme mikrofonile."; "NSCalendarsUsageDescription" = "Vaata päevakavasse lisatud koosolekuid vastvast rakendusest."; -"NSContactsUsageDescription" = "Selleks, et leida Matrixi võrgu kasutajaid, võib Element saata sinu aadressiraamatus leiduvad e-posti aadressid ja telefoninumbrid sinu valitud Matrixi isikutuvastusserverile. Kui server seda toetab, siis andmed muudetakse enne saatmist räsideks - täpsema teabe leiad oma isikutuvastusserveri privaatsuspoliitikast."; +"NSContactsUsageDescription" = "Element näitab sulle tuttavaid, kellega saad alustada vestlust."; "NSFaceIDUsageDescription" = "Ligipääsuks sinu rakendusele on kasutusel Face ID."; diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index da076b099..e33a6047c 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -691,10 +691,10 @@ "user_verification_sessions_list_user_trust_level_unknown_title" = "Teadmata olek"; "user_verification_sessions_list_information" = "Sõnumid selle kasutajaga selles jututoas on läbivalt krüptitud ning kolmandad osapooled ei saa neid lugeda."; "user_verification_sessions_list_table_title" = "Sessioonid"; -"user_verification_sessions_list_session_trusted" = "Usaldusväärsed"; -"user_verification_sessions_list_session_untrusted" = "Ei ole usaldusväärsed"; -"user_verification_session_details_trusted_title" = "Usaldusväärsed"; -"user_verification_session_details_untrusted_title" = "Ei ole usaldusväärsed"; +"user_verification_sessions_list_session_trusted" = "Usaldusväärne"; +"user_verification_sessions_list_session_untrusted" = "Ei ole usaldusväärne"; +"user_verification_session_details_trusted_title" = "Usaldusväärne"; +"user_verification_session_details_untrusted_title" = "Ei ole usaldusväärne"; // Chat creation "room_creation_title" = "Uus vestlus"; "room_creation_account" = "Kasutajakonto"; @@ -710,7 +710,7 @@ "room_creation_keep_private" = "Jäta privaatseks"; "room_creation_make_private" = "Muuda privaatseks"; "room_creation_wait_for_creation" = "Jututuba on just loomisel. Palun oota üks hetk."; -"room_creation_invite_another_user" = "Otsi või kutsu uut kasutajat tema kasutajatunnuse, nime või e-posti aadressi alusel"; +"room_creation_invite_another_user" = "Kasutajatunnus, nimi või e-posti aadress"; "room_creation_error_invite_user_by_email_without_identity_server" = "Ühtegi isikutuvastusserverit pole seadistatud ning sul ei ole võimalik lisada kasutajaid e-posti aadressi alusel."; "room_recents_low_priority_section" = "VÄHETÄHTIS"; "room_recents_server_notice_section" = "SÜSTEEMSED TEATED"; @@ -757,7 +757,7 @@ "settings_integrations" = "LÕIMINGUD"; "settings_user_interface" = "KASUTAJALIIDES"; "settings_ignored_users" = "EIRATUD KASUTAJAD"; -"settings_contacts" = "KONTAKTID SIIN SEADMES"; +"settings_contacts" = "KONTAKTID SEADMES"; "settings_advanced" = "KEERUKAMAD SEADISTUSED"; "settings_other" = "MUUD SEADISTUSED"; "settings_labs" = "KATSED"; @@ -916,7 +916,7 @@ "user_verification_start_information_part2" = " võrreldes selleks üheks korraks loodud koodi mõlemas seadmes."; "user_verification_start_waiting_partner" = "Ootan vastust kasutajalt %@…"; "user_verification_start_additional_information" = "Turvalisuse mõttes on oluline, et teed seda nii, et kas olete üheskoos või kasutate suhtluskanalit, mida mõlemad usaldate."; -"user_verification_sessions_list_user_trust_level_trusted_title" = "Usaldusväärsed"; +"user_verification_sessions_list_user_trust_level_trusted_title" = "Usaldusväärne"; "user_verification_session_details_information_trusted_current_user" = "Kuna sina oled selle sessiooni verifitseerinud, siis see sessioon on krüptitud sõnumite saatmiseks usaldusväärne:"; "user_verification_session_details_information_trusted_other_user_part1" = "See sessioon on krüptitud sõnumite saatmiseks usaldusväärne, sest "; "user_verification_session_details_information_trusted_other_user_part2" = " verifitseeris selle:"; @@ -1443,3 +1443,27 @@ "room_recents_suggested_rooms_section" = "SOOVITATUD JUTUTOAD"; "done" = "Valmis"; "open" = "Ava"; +"settings_contacts_enable_sync_description" = "Sellega võimaldad oma isikutuvastusserveril lubada sind leida."; + +// Service terms +"service_terms_modal_title_message" = "Jätkamiseks palun nõustu kasutustingimustega"; +"settings_phone_contacts" = "KONTAKTID NUTISEADMES"; +"settings_contacts_enable_sync" = "Otsi kontakte"; +"service_terms_modal_information_description_integration_manager" = "Lõiminguhalduri alusel saad lisada ja kasutada kolmandate osapoolte loodud lisavõimalusi."; +"service_terms_modal_information_description_identity_server" = "Isikutuvastusserver võimaldab sul telefoninumbri või e-posti aadressi alusel leida Matrix'i kasutajaid."; +"service_terms_modal_information_title_integration_manager" = "Lõiminguhaldur"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Isikutuvastusserver"; +"service_terms_modal_description_integration_manager" = "Sellega tekib sul võimalus kasutada roboteid, sõnumisildu, vidinaid ja kleepsupakke."; +"service_terms_modal_description_identity_server" = "Sellega annad teistele inimestele võimaluse sind leida, kui nende aadressiraamatus on sinu telefoninumber või e-posti aadress."; +"service_terms_modal_table_header_integration_manager" = "LÕIMINGUHALDURI TINGIMUSED"; +"service_terms_modal_table_header_identity_server" = "ISIKUTUVASTUSSERVERI TINGIMUSED"; +"service_terms_modal_footer" = "Selle võimaluse saad alati seadistustest välja lülitada."; +"find_your_contacts_identity_service_error" = "Ei õnnestu leida isikutuvastusserverit."; +"find_your_contacts_footer" = "Selle võimaluse saad alati seadistustest välja lülitada."; +"find_your_contacts_message" = "Las %@ näitab sulle kontakte ja nii saad tuttavatega kiiresti vestlema asuda."; +"find_your_contacts_button_title" = "Otsi kontakte"; +"find_your_contacts_title" = "Alusta kontaktide looendist"; +"contacts_address_book_permission_denied_alert_message" = "Palun luba seadistustest aadressiraamatu lugemine."; +"contacts_address_book_permission_denied_alert_title" = "Kontaktid pole kasutusel"; diff --git a/Riot/Assets/fa.lproj/Vector.strings b/Riot/Assets/fa.lproj/Vector.strings index 69aecaf03..06e5d76d2 100644 --- a/Riot/Assets/fa.lproj/Vector.strings +++ b/Riot/Assets/fa.lproj/Vector.strings @@ -17,7 +17,7 @@ "back" = "بازگشت"; "store_full_description" = "المنت یک پیام‌رسان جدید و ابزاری برای همکاری افراد است که:\n\n۱. امکانات کنترلی لازم برای حفاظت از حریم خصوصی را در اختیار شما قرار می‌دهد.\n۲. امکان برقراری ارتباط با هر کسی را بر بستر شبکه‌ی ماتریکس و حتی فراتر از آن، امکان برقراری ارتباط با برنامه‌های دیگر نظیر Slack را در اختیار شما قرار می‌دهد.\n۳. شما را در برابر تبلیغات، کندوکاو داده‌هایتان، در پشتی و همچنین یک زیست‌بوم بسته و محصور محافظت می‌کند.\n۴. شما را از طریق رمزنگاری سرتاسری و همچنین امضاء متقابل برای تائيد هویت دیگران، امن می‌کند.\n\nالمنت یک پیام‌رسان و ابزار ارتباطی کاملا متفاوت است، چرا که از معماری غیرمتمرکز بهره برده و متن‌باز است.\n\nالمنت امکان استقرار محلی - یا انتخاب هر میزبان دلخواهی - را به شما داده و از این طریق حریم خصوصی، مالکیت و کنترل داده‌ها و گفتگوهایتان برای شما به ارمغان می‌آورد. همچنین دسترسی به یک شبکه‌ی باز را برای شما فراهم کرده، به طوری که مجبور نیستید فقط با کاربران المنت به گفتگو و صحبت بپردازید. در کنار همه‌ی این‌ها، بسیار امن است.\n\nپشتوانه‌ی قابلیت‌های بالا، استفاده از ماتریکس است - یک استاندارد برای ارتباطات غیرمحدود و متمرکز.\n\nالمنت به شما اختیار می‌دهد سرور گفتگو‌های خود را انتخاب کنید. در برنامه المنت، به طرق مختلف می‌توانید سرور مورد نظر خود را انتخاب کنید:\n\n۱. ساختن یک حساب‌کاربری رایگان بر روی سرور عمومی matrix.org\n۲. استقرار محلی و راه‌اندازی سرور بر روی سخت‌افزار خودتان و ایجاد حساب کاربری بر روی آن\n۳. ایجاد حساب کاربری بر روی یک سرور دلخواه از طریق عضویت در پلتفورم استقرار Element Matrix Services\n\nچرا المنت گزینه‌ی جذابی است؟\n\nمالک حقیقی داده‌های خود باشید: شما تصمیم بگیرید داده‌ها و پیام‌هایتان کجا ذخیره شوند. المنت مانند برخی اَبَرشرکت ها، در داده‌های شما کاوش نکرده و آن‌ها را در اختیار شخص ثالثی قرار نمی‌دهد.\n\nپیام‌رسانی و ارتباطات باز: شما می‌توانید با هر کسی بر بستر ماتریکس ارتباط بگیرید، فارغ از اینکه از کدام کلاینت ماتریکسی استفاده می‌کنند؛ حتی فراتر، شما می‌توانید افراد بر بستر سازوکارهای پیام‌رسانی دیگر نظیر Slack ،XMPP و یا IRC نیز ارتباط برقرار نمائید.\n\nفوق‌العاده امن: رمزنگاری سرتاسری واقعی (فقط افرادی که در حال گفتگو هستند امکان رمزگشایی پیام‌ها را دارند)، به همراه قابلیت امضاء متقابل برای تائید هویت دستگاه و هویت طرف‌های گفتگو.\n\nپکیج ارتباطی کامل: پیام‌رسانی، تماس‌های صوتی و تصویری، ارسال فایل، به اشتراک‌گذاری صفحه نمایش و یک طیف گسترده‌ای از یکپارچه‌سازی‌ها، بات‌ها و ابزارک‌ها. اتاق‌ها و فضاهای کاری مختلف بسازید و برای به سرانجام رسیدن امور، در ارتباط باشید.\n\nحاضر در همه جا: هرجا و هر زمان در دسترس بوده و پیام‌های خود را به صورت همگام‌سازی‌شده بر روی دستگاه‌های مختلف در اختیار داشته باشید."; // String for App Store -"store_short_description" = "چت/تماس صوتی مبتنی بر اینترنت امن غیرمتمرکز"; +"store_short_description" = "پیامرسان/تماس صوتی تصویری اینترنتی امن مبتنی بر بستر غیرمتمرکز"; "auth_missing_password" = "لطفا رمز عبور را وارد نمایید"; "auth_invalid_phone" = "شماره تماس وارد شده بنظر اشتباه است"; "auth_invalid_email" = "آدرس پست الکترونیکی وارد شده بنظر اشتباه است"; diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index e46331558..6ff0e4091 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -62,7 +62,7 @@ "auth_missing_phone" = "Numéro de téléphone manquant"; "auth_missing_email_or_phone" = "Adresse e-mail ou numéro de téléphone manquant"; "auth_password_dont_match" = "Les mots de passe ne correspondent pas"; -"auth_username_in_use" = "L’identifiant est déjà utilisé"; +"auth_username_in_use" = ""; "auth_forgot_password" = "Mot de passe oublié ?"; "auth_use_server_options" = "Utiliser un serveur personnalisé (avancé)"; "auth_email_validation_message" = "Merci de vérifier vos e-mails pour continuer l’inscription"; diff --git a/Riot/Assets/hu.lproj/InfoPlist.strings b/Riot/Assets/hu.lproj/InfoPlist.strings index f874c48d5..0edf4d429 100644 --- a/Riot/Assets/hu.lproj/InfoPlist.strings +++ b/Riot/Assets/hu.lproj/InfoPlist.strings @@ -2,6 +2,6 @@ "NSCameraUsageDescription" = "A kamera fényképek, videók készítéséhez és videóhívásokhoz lesz használva."; "NSPhotoLibraryUsageDescription" = "A fénykép galéria fényképek és videók küldéséhez lesz használva."; "NSMicrophoneUsageDescription" = "A hívás indításához és fogadásához, videó és hangüzenet felvételéhez az Elementnek hozzáférési engedélyre van szüksége a mikrofonhoz."; -"NSContactsUsageDescription" = "Az olyan ismerősök felderítéséhez akik már használják a Matrixot, Elementet el tudja küldeni a címjegyzékben található e-mail címeket és telefonszámokat az általad választott Matrix azonosítási szervernek. Ahol lehetséges a személyes adatok hash-elve lesznek - kérlek ellenőrizd az azonosítási szervered adatvédelmi szabályait."; +"NSContactsUsageDescription" = "Element megmutatja a névjegyzéket, hogy beszélgetésbe meghívhasd őket."; "NSCalendarsUsageDescription" = "Nézd meg a találkozóidat az alkalmazásban."; "NSFaceIDUsageDescription" = "Arc felismerés használata az alkalmazás eléréséhez."; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index d18f8a407..ed2f5b3a5 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -72,7 +72,7 @@ "auth_phone_in_use" = "Ez a telefonszám már használatban van"; "auth_untrusted_id_server" = "Az azonosító szerver megbízhatatlan"; "auth_password_dont_match" = "A jelszavak nem egyeznek meg"; -"auth_username_in_use" = "A felhasználói név használatban"; +"auth_username_in_use" = "A felhasználónév foglalt"; "auth_forgot_password" = "Elfelejtetted a jelszót?"; "auth_email_not_found" = "E-mail küldési hiba: Az e-mail cím nem található"; "auth_use_server_options" = "Egyedi szerver beállítások használata (haladó)"; @@ -105,7 +105,7 @@ "room_creation_keep_private" = "Maradjon zárt"; "room_creation_make_private" = "Legyen nyilvános"; "room_creation_wait_for_creation" = "A szoba elkészítése folyamatban van. Kérlek várj."; -"room_creation_invite_another_user" = "Keresés / meghívás felhasználói azonosítás, név vagy e-mail cím alapján"; +"room_creation_invite_another_user" = "Felhasználói azonosító, név vagy e-mail"; // Room recents "room_recents_directory_section" = "SZOBA KÖNYVTÁR"; "room_recents_favourites_section" = "KEDVENCEK"; @@ -281,7 +281,7 @@ "settings_calls_settings" = "HÍVÁSOK"; "settings_user_interface" = "FELHASZNÁLÓI FELÜLET"; "settings_ignored_users" = "FIGYELMEN KÍVÜL HAGYOTT FELHASZNÁLÓK"; -"settings_contacts" = "HELYI KAPCSOLATOK"; +"settings_contacts" = "ESZKÖZ NÉVJEGYZÉK"; "settings_advanced" = "HALADÓ"; "settings_other" = "MÁS"; "settings_labs" = "LABOR"; @@ -863,7 +863,7 @@ "settings_calls_stun_server_fallback_description" = "Másodlagos hívást segítő szerver (%@) engedélyezése ha a matrix szervered nem ajánl fel másikat (az IP címed a hívás ideje alatt meg lesz osztva)."; "settings_devices_description" = "Az munkamenet nyilvános neve látható azoknál az embereknél akikkel beszélgetsz"; "settings_discovery_no_identity_server" = "Jelenleg nem használsz azonosítási szervert. Ahhoz, hogy az ismerősök megtalálhassanak adj hozzá egyet."; -"settings_discovery_terms_not_signed" = "Egyetértés az Azonosítási Szerver (%@) Felhasználási Feltételeivel, hogy e-mail címmel vagy telefonszámmal megtalálható lehess."; +"settings_discovery_terms_not_signed" = "Egyetértés az azonosítási szerver (%@) Felhasználási Feltételeivel, hogy e-mail címmel vagy telefonszámmal megtalálható lehess."; "settings_discovery_three_pids_management_information_part1" = "E-mail címek és telefonszámok beállítása amivel más felhasználók megtalálhatnak és meghívhatna szobákba. E-mail cím és telefonszám hozzáadása és törlése a listából itt: "; "settings_discovery_three_pids_management_information_part2" = "Felhasználói Beállítások"; "settings_discovery_three_pids_management_information_part3" = "."; @@ -880,7 +880,7 @@ "settings_identity_server_no_is" = "Azonosítási szerver nincs beállítva"; "settings_identity_server_no_is_description" = "Jelenleg nem használsz azonosítási szervert. Ahhoz, hogy megtalálhass másokat és az ismerősök megtalálhassanak adj hozzá egyet alább."; // Identity server settings -"identity_server_settings_title" = "Azonosítási Szerver"; +"identity_server_settings_title" = "Azonosítási szerver"; "identity_server_settings_description" = "Jelenleg ezt használod: %@, hogy megtalálj másokat és megtalálhassanak ismerősök."; "identity_server_settings_no_is_description" = "Jelenleg nem használsz azonosítási szervert. Ahhoz, hogy megtalálj másokat és megtalálhassanak ismerősök adj meg egyet alább."; "identity_server_settings_place_holder" = "Adj meg egy azonosítási szervet"; @@ -1506,3 +1506,27 @@ "room_recents_suggested_rooms_section" = "JAVASOLT SZOBÁK"; "done" = "Kész"; "open" = "Megnyitás"; +"find_your_contacts_title" = "Kezdjük a kapcsolataid felsorolásával"; +"service_terms_modal_information_description_integration_manager" = "Az integrációs menedzser lehetővé teszi harmadik féltől származó lehetőségek használatát."; +"service_terms_modal_information_description_identity_server" = "Az azonosítási szerver lehetővé teszi, hogy megtaláld a kapcsolataidat azzal, hogy megnézi regisztráltak-e már az ő telefonszámaival vagy e-mail címeivel."; +"service_terms_modal_information_title_integration_manager" = "Integrációs Menedzser"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Azonosító szerver"; +"service_terms_modal_description_integration_manager" = "Ez lehetővé teszi, hogy botokat, hidakat, kisalkalmazásokat vagy matricákat használj."; +"service_terms_modal_description_identity_server" = "Ez lehetővé teszi másnak, akinek a telefonjába el van mentve a telefonszámod vagy e-mail címed, hogy megtaláljon."; +"service_terms_modal_table_header_integration_manager" = "INTEGRÁCIÓS MENEDZSER FELTÉTELEK"; +"service_terms_modal_table_header_identity_server" = "AZONOSÍTÁSI SZERVER FELTÉTELEK"; +"settings_contacts_enable_sync" = "Kapcsolatok megkeresése"; +"find_your_contacts_button_title" = "Kapcsolatok megkeresése"; +"find_your_contacts_message" = "Engedd meg, hogy %@ megjelenítse a névjegyzéket, hogy gyorsan elkezdhess beszélgetni azokkal akiket a legjobban ismersz."; +"service_terms_modal_footer" = "Bármikor letiltható a beállításokban."; + +// Service terms +"service_terms_modal_title_message" = "A folytatáshoz fogadd el a felhasználási feltételeket alább"; +"settings_contacts_enable_sync_description" = "Az azonosítási szerveredet fogja használni, hogy összekössön az ismerőseiddel és megtalálhassanak."; +"settings_phone_contacts" = "TELEFON NÉVJEGYZÉK"; +"find_your_contacts_identity_service_error" = "Az azonosítási szerverhez nem sikerült csatlakozni."; +"find_your_contacts_footer" = "Bármikor letiltható a beállításokban."; +"contacts_address_book_permission_denied_alert_message" = "A névjegyzék engedélyezéséhez lépj be az eszköz beállításokba."; +"contacts_address_book_permission_denied_alert_title" = "Névjegyzék letiltva"; diff --git a/Riot/Assets/id.lproj/InfoPlist.strings b/Riot/Assets/id.lproj/InfoPlist.strings new file mode 100644 index 000000000..e74ebc244 --- /dev/null +++ b/Riot/Assets/id.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ + + +"NSContactsUsageDescription" = "Element akan menampilkan kontak Anda supaya Anda bisa mengundang mereka ke obrolan."; +// Permissions usage explanations +"NSCameraUsageDescription" = "Kamera digunakan untuk mengambil foto dan video, dan melakukan panggilan video."; +"NSFaceIDUsageDescription" = "Face ID digunakan untuk mengakses aplikasi Anda."; +"NSCalendarsUsageDescription" = "Lihat pertemuan yang sudah dijadwalkan di aplikasi."; +"NSMicrophoneUsageDescription" = "Element membutuhkan akses ke mikrofon Anda untuk melakukan dan menerima panggilan, mengambil video, dan merekam pesan suara."; +"NSPhotoLibraryUsageDescription" = "Galeri digunakan untuk mengirim foto dan video."; diff --git a/Riot/Assets/id.lproj/Localizable.strings b/Riot/Assets/id.lproj/Localizable.strings new file mode 100644 index 000000000..679a5cbb8 --- /dev/null +++ b/Riot/Assets/id.lproj/Localizable.strings @@ -0,0 +1,167 @@ + + + +/** Key verification **/ + +"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@ ingin memverifikasi"; + +/* Group call from user, CallKit caller name */ +"GROUP_CALL_FROM_USER" = "%@ (Panggilan grup)"; + +/* A user added a Jitsi call to a room */ +"GROUP_CALL_STARTED" = "Panggilan grup dimulai"; + +/* Incoming named video conference invite from a specific person */ +"VIDEO_CONF_NAMED_FROM_USER" = "Panggilan video grup dari %@: '%@'"; + +/* Incoming named voice conference invite from a specific person */ +"VOICE_CONF_NAMED_FROM_USER" = "Panggilan grup dari %@: '%@'"; + +/* Incoming unnamed video conference invite from a specific person */ +"VIDEO_CONF_FROM_USER" = "Panggilan video grup dari %@"; + +/* Incoming unnamed voice conference invite from a specific person */ +"VOICE_CONF_FROM_USER" = "Panggilan grup dari %@"; + +/* Incoming one-to-one video call */ +"VIDEO_CALL_FROM_USER" = "Panggilan video dari %@"; + +/** Calls **/ + +/* Incoming one-to-one voice call */ +"VOICE_CALL_FROM_USER" = "Panggilan dari %@"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ memperbarui profilnya"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ mengubah avatarnya"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ mengubah namanya"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ mengubah namanya ke %@"; + +/* A user has invited you to a named room */ +"USER_INVITE_TO_NAMED_ROOM" = "%@ mengundang Anda ke %@"; + +/* A user has invited you to an (unamed) group chat */ +"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ mengundang Anda ke obrolan grup"; + +/** Invites **/ + +/* A user has invited you to a chat */ +"USER_INVITE_TO_CHAT" = "%@ mengundang Anda untuk mengobrol"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ mengirim sebuah reaksi"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ mereaksi %@"; + +/* Look, stuff's happened, alright? Just open the app. */ +"MSGS_IN_TWO_PLUS_ROOMS" = "%@ pesan baru di %@, %@ dan %@ lainnya"; + +/* Multiple messages in two rooms */ +"MSGS_IN_TWO_ROOMS" = "%@ pesan baru di %@ dan %@"; + +/* Multiple unread messages from two plus people (ie. for 4+ people: 'others' replaces the third person) */ +"MSGS_FROM_TWO_PLUS_USERS" = "%@ pesan baru dari %@, %@ dan %@ lainnya"; + +/* Multiple unread messages from three people */ +"MSGS_FROM_THREE_USERS" = "%@ pesan baru dari %@, %@ dan %@"; + +/* Multiple unread messages from two people */ +"MSGS_FROM_TWO_USERS" = "%@ pesan baru dari %@ dan %@"; + +/* Multiple unread messages from a specific person, not referencing a room */ +"MSGS_FROM_USER" = "%@ pesan baru di %@"; + +/** Coalesced messages **/ + +/* Multiple unread messages in a room */ +"UNREAD_IN_ROOM" = "%@ pesan baru di %@"; + +/* New message with hidden content due to PIN enabled */ +"MESSAGE_PROTECTED" = "Pesan Baru"; + +/* New message indicator on a room */ +"MESSAGE_IN_X" = "Pesan di %@"; + +/* New message indicator from a DM */ +"MESSAGE_FROM_X" = "Pesan dari %@"; + +/** Notification messages **/ + +/* New message indicator on unknown room */ +"MESSAGE" = "Pesan"; + +/* Sticker from a specific person, not referencing a room. */ +"STICKER_FROM_USER" = "%@ mengirim sebuah stiker"; + +/** Single, end-to-end encrypted messages (ie. we don't know what they say) */ + +/* New message from a specific person, not referencing a room */ +"MSG_FROM_USER" = "%@ mengirim sebuah pesan"; + +/* New image message from a specific person in a named room. */ +"IMAGE_FROM_USER_IN_ROOM" = "%@ mengirim gambar %@ di %@"; + +/** Media Messages **/ + +/* New image message from a specific person, not referencing a room. */ +"PICTURE_FROM_USER" = "%@ mengirim sebuah gambar"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ mengirim sebuah video"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ mengirim sebuah file audio %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ mengirim sebuah pesan suara"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ mengirim sebuah file %@"; + +/* A single unread message in a room */ +"SINGLE_UNREAD_IN_ROOM" = "Anda menerima sebuah pesan di %@"; + +/* A single unread message */ +"SINGLE_UNREAD" = "Anda menerima sebuah pesan"; + +/* New action message from a specific person in a named room. */ +"ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; + +/* New message from a specific person in a named room. Content included. */ +"MSG_FROM_USER_IN_ROOM_WITH_CONTENT" = "%@ di %@: %@"; + +/* New action message from a specific person, not referencing a room. */ +"ACTION_FROM_USER" = "* %@ %@"; + +/** Single, unencrypted messages (where we can include the content */ + +/* New message from a specific person, not referencing a room. Content included. */ +"MSG_FROM_USER_WITH_CONTENT" = "%@: %@"; + +/* New message from a specific person in a named room */ +"MSG_FROM_USER_IN_ROOM" = "%@ mengirim di %@"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ membalas di %@"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ membalas"; + +/** Titles **/ + +/* Message title for a specific person in a named room */ +"MSG_FROM_USER_IN_ROOM_TITLE" = "%@ di %@"; +/** General **/ + +"NOTIFICATION" = "Notifikasi"; diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index f61661288..aaeece68f 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -35,7 +35,7 @@ "close" = "Tutup"; "sending" = "Mengirim"; "send_to" = "Kirim ke %@"; -"rename" = "Ubah nama"; +"rename" = "Ubah Nama"; "later" = "Nanti"; "active_call_details" = "Panggilan Aktif (%@)"; "active_call" = "Panggilan Aktif"; @@ -47,21 +47,21 @@ "decline" = "Tolak"; "join" = "Bergabung"; "save" = "Simpan"; -"cancel" = "Batal"; +"cancel" = "Batalkan"; "off" = "Nonaktif"; "on" = "Aktif"; -"retry" = "Coba lagi"; +"retry" = "Coba Lagi"; "invite" = "Undang"; "remove" = "Hapus"; "leave" = "Tinggalkan"; "start" = "Mulai"; "create" = "Buat"; -"continue" = "Lanjut"; +"continue" = "Lanjutkan"; "back" = "Kembali"; "next" = "Selanjutnya"; // Actions -"view" = "Lihat"; +"view" = "Tampilkan"; "warning" = "Peringatan"; "title_groups" = "Komunitas-komunitas"; "title_rooms" = "Ruangan-ruangan"; @@ -75,7 +75,7 @@ "auth_email_not_found" = "Gagal mengirim surel: Alamat email ini tidak ditemukan"; "auth_forgot_password_error_no_configured_identity_server" = "Tidak ada server identitas yang dikonfigurasikan: tambahkan satu untuk mengatur ulang kata sandi Anda."; "auth_forgot_password" = "Lupa kata sandi?"; -"auth_username_in_use" = "Nama pengguna sudah dipakai"; +"auth_username_in_use" = "Nama pengguna telah dipakai"; "auth_password_dont_match" = "Kata sandi tidak cocok"; "auth_untrusted_id_server" = "Server identitas tidak dipercaya"; "auth_phone_is_required" = "Tidak ada server identitas yang dikonfigurasi sehingga Anda tidak dapat menambahkan nomor telepon untuk mengatur ulang kata sandi Anda di masa depan."; @@ -102,6 +102,510 @@ "joined" = "Bergabung"; "collapse" = "tutup"; "store_promotional_text" = "Aplikasi perpesanan dan kolaborasi yang menjaga privasi, pada jaringan terbuka. Terdesentralisasi untuk Anda kendali. Tidak ada penambangan data, tidak ada backdoor dan tidak ada akses pihak ketiga."; -"store_full_description" = "Element adalah aplikasi messenger dan kolaborasi tipe baru yang:\n\n1. Menempatkan Anda dalam kendali untuk mempertahankan privasi Anda\n2. Memungkinkan Anda berkomunikasi dengan siapa pun di jaringan Matrix, dan bahkan di luar dengan mengintegrasikan dengan aplikasi seperti Slack\n3. Melindungi Anda dari iklan, menambangan data, backdoor, dan taman berdinding\n4. Mengamankan Anda melalui enkripsi ujung-ke-ujung, dengan penandatanganan-silang untuk memverifikasi orang lain\n\nElement benar-benar berbeda dari aplikasi perpesanan dan kolaborasi lain karena terdesentralisasi dan sumber terbuka.\n\nElement memungkinkan Anda host sendiri - atau memilih host - sehingga Anda memiliki privasi, kepemilikan, dan kontrol data dan percakapan Anda. Ini memberi Anda akses ke jaringan terbuka; jadi Anda tidak hanya terjebak berbicara dengan pengguna Element. Itu sangat aman.\n\nElement dapat melakukan semua ini karena beroperasi pada Matrix - standar untuk komunikasi terdesentralisasi terbuka.\n\nElement menempatkan Anda dalam kendali dengan membiarkan Anda memilih siapa yang meng-host percakapan Anda. Dari aplikasi Element, Anda dapat memilih untuk meng-host dengan cara yang berbeda:\n\n1. Dapatkan akun gratis pada server publik matrix.org\n2. Host sendiri akun Anda dengan menjalankan server pada perangkat keras Anda sendiri\n3. Mendaftar untuk akun di server khusus dengan hanya berlangganan platform hosting Element Matrix Services\n\nMengapa memilih Element?\n\nMILIKI DATA ANDA: Anda memutuskan di mana harus menyimpan data dan pesan Anda. Anda memilikinya dan mengendalikannya, bukan perusahaan besar yang menambang data Anda atau memberikan akses ke pihak ketiga.\n\nPESAN DAN KOLABORASI TERBUKA: Anda dapat mengobrol dengan orang lain di jaringan Matrix, jika mereka menggunakan Element atau aplikasi Matrix lain, dan bahkan jika mereka menggunakan sistem perpesanan seperti Slack, IRC atau XMPP.\n\nSUPER-AMAN: Enkripsi ujung-ke-ujung (hanya mereka yang dalam percakapan dapat mendekripsi pesan), dan penandatanganan-silang untuk memverifikasi perangkat peserta percakapan.\n\nKOMUNIKASI LENGKAP: Pesan, panggilan suara dan video, berbagi file, berbagi layar dan banyak integrasi, bot dan widget. Buat ruangan, komunitas, tetap terhubung dan selesaikan hal-hal.\n\nDI MANA PUN ANDA BERADA: Tetap berkomunikasi di mana pun Anda berada dengan riwayat pesan yang sepenuhnya disinkronkan di semua perangkat Anda dan di web di https://element.io/app."; +"store_full_description" = "Element adalah aplikasi messenger dan kolaborasi tipe baru yang:\n\n1. Menempatkan Anda dalam kendali untuk mempertahankan privasi Anda\n2. Memungkinkan Anda berkomunikasi dengan siapa pun di jaringan Matrix, dan bahkan di luar dengan mengintegrasikan dengan aplikasi seperti Slack\n3. Melindungi Anda dari iklan, menambangan data, backdoor, dan taman berdinding\n4. Mengamankan Anda melalui enkripsi ujung-ke-ujung, dengan penandatanganan-silang untuk memverifikasi orang lain\n\nElement benar-benar berbeda dari aplikasi perpesanan dan kolaborasi lain karena terdesentralisasi dan sumber terbuka.\n\nElement memungkinkan Anda host sendiri - atau memilih host - sehingga Anda memiliki privasi, kepemilikan, dan kontrol data dan percakapan Anda. Ini memberi Anda akses ke jaringan terbuka; jadi Anda tidak hanya terjebak berbicara dengan pengguna Element. Itu sangat aman.\n\nElement dapat melakukan semua ini karena beroperasi pada Matrix - standar untuk komunikasi terdesentralisasi terbuka.\n\nElement menempatkan Anda dalam kendali dengan membiarkan Anda memilih siapa yang meng-host percakapan Anda. Dari aplikasi Element, Anda dapat memilih untuk meng-host dengan cara yang berbeda:\n\n1. Dapatkan akun gratis pada server publik matrix.org\n2. Host sendiri akun Anda dengan menjalankan server pada perangkat keras Anda sendiri\n3. Mendaftar untuk akun di server khusus dengan hanya berlangganan platform hosting Element Matrix Services\n\nMengapa memilih Element?\n\nMILIKI DATA ANDA: Anda memutuskan di mana harus menyimpan data dan pesan Anda. Anda memilikinya dan mengendalikannya, bukan perusahaan besar yang menambang data Anda atau memberikan akses ke pihak ketiga.\n\nPESAN DAN KOLABORASI TERBUKA: Anda dapat mengobrol dengan orang lain di jaringan Matrix, jika mereka menggunakan Element atau aplikasi Matrix lain, dan bahkan jika mereka menggunakan sistem perpesanan seperti Slack, IRC atau XMPP.\n\nSUPER-AMAN: Enkripsi ujung-ke-ujung (hanya mereka yang dalam percakapan dapat mendekripsi pesan), dan penandatanganan-silang untuk memverifikasi perangkat peserta percakapan.\n\nKOMUNIKASI LENGKAP: Pesan, panggilan suara dan video, berbagi file, berbagi layar dan banyak integrasi, bot dan widget. Buat ruangan, komunitas, tetap terhubung dan selesaikan hal-hal.\n\nDI MANA PUN ANDA BERADA: Tetap berkomunikasi di mana pun Anda berada dengan riwayat pesan yang sepenuhnya disinkronkan di semua perangkat Anda dan di web di https://app.element.io/."; // String for App Store "store_short_description" = "Obrolan/VoIP terdesentralisasi aman"; + + +// Room Details +"room_details_title" = "Detail Ruangan"; +"security_settings_secure_backup_setup" = "Siapkan"; +"security_settings_secure_backup" = "CADANGAN AMAN"; +"security_settings_crypto_sessions" = "SESI SAYA"; +"settings_discovery_three_pid_details_title_email" = "Kelola email"; +"settings_discovery_three_pids_management_information_part2" = "Pengaturan Pengguna"; +"settings_key_backup_delete_confirmation_prompt_title" = "Hapus Cadangan"; +"settings_key_backup_button_delete" = "Hapus Cadangan"; +"settings_key_backup_info_algorithm" = "Algoritma: %@"; +"settings_crypto_export" = "Ekspor kunci"; +"settings_crypto_device_key" = "\nKunci sesi:\n"; +"settings_crypto_device_id" = "\nID Sesi: "; +"settings_crypto_device_name" = "Nama sesi: "; +"settings_add_3pid_invalid_password_message" = "Kredential tidak valid"; +"settings_confirm_password" = "konfirmasi kata sandi"; +"settings_new_password" = "kata sandi baru"; +"settings_old_password" = "kata sandi lama"; +"settings_third_party_notices" = "Pemberitahuan Pihak Ketiga"; +"settings_privacy_policy" = "Kebijakan Privasi"; +"settings_version" = "Versi %@"; +"settings_labs_e2e_encryption" = "Enkripsi Ujung-ke-Ujung"; +"settings_contacts_phonebook_country" = "Negara buku telepon"; +"settings_integrations_allow_button" = "Kelola integrasi"; +"settings_enable_callkit" = "Panggilan yang diintegrasi"; +"settings_night_mode" = "Mode Malam"; +"settings_change_password" = "Ubah kata sandi"; +"settings_first_name" = "Nama Depan"; +"settings_display_name" = "Nama Tampilan"; +"settings_profile_picture" = "Gambar Profil"; +"settings_sign_out" = "Keluar"; +"settings_deactivate_account" = "NONAKTIFKAN AKUN"; +"settings_key_backup" = "CADANGAN KUNCI"; +"settings_contacts" = "KONTAK PERANGKAT"; +"settings_ignored_users" = "PENGGUNA YANG DIABAIKAN"; +"settings_user_interface" = "ANTARMUKA PENGGUNA"; +"settings_identity_server_settings" = "SERVER IDENTITAS"; +"settings_user_settings" = "PENGATURAN PENGGUNA"; +"settings_clear_cache" = "Bersihkan cache"; +"room_event_action_report" = "Laporkan konten"; +"settings_report_bug" = "Laporkan bug"; +"room_preview_try_join_an_unknown_room_default" = "sebuah ruangan"; +"room_title_one_member" = "1 anggota"; +"room_title_members" = "%@ anggota"; +"room_title_invite_members" = "Undang anggota"; + +// Room Title +"room_title_new_room" = "Ruangan baru"; +"unknown_devices_title" = "Sesi tidak diketahui"; +"unknown_devices_answer_anyway" = "Jawab Saja"; +"unknown_devices_call_anyway" = "Panggil Saja"; +"unknown_devices_send_anyway" = "Kirim Saja"; +"room_open_dialpad" = "Papan nomor"; +"room_place_voice_call" = "Panggilan suara"; +"room_accessibility_hangup" = "Jeda"; +"room_accessibility_video_call" = "Panggilan Video"; +"room_message_edits_history_title" = "Editan pesan"; +"room_action_send_file" = "Kirim file"; +"room_action_send_sticker" = "Kirim stiker"; +"room_event_action_reaction_history" = "Sejarah reaksi"; +"room_event_action_reaction_show_less" = "Tampilkan lebih sedikit"; +"room_event_action_reaction_show_all" = "Tampilkan semua"; +"room_event_action_view_encryption" = "Informasi enkripsi"; +"room_event_action_cancel_download" = "Batalkan Pengunduhan"; +"room_event_action_cancel_send" = "Batalkan Pengiriman"; +"room_event_action_view_source" = "Tampilkan Sumber"; +"room_prompt_cancel" = "batalkan semuanya"; +"room_prompt_resend" = "Kirim ulang semuanya"; +"room_participants_action_security_status_complete_security" = "Selesaikan keamanan"; +"room_participants_action_set_admin" = "Buat sebagai admin"; +"room_participants_action_set_moderator" = "Buat sebagai moderator"; +"room_participants_action_section_direct_chats" = "Pesan langsung"; +"room_participants_action_section_admin_tools" = "Alat admin"; +"room_participants_invite_malformed_id_title" = "Kesalahan dalam Mengundang"; +"room_participants_filter_room_members_for_dm" = "Filter anggota"; +"room_participants_leave_prompt_title" = "Tinggalkan ruangan"; +"room_participants_multi_participants" = "%d anggota"; +"room_participants_one_participant" = "1 anggota"; +"room_participants_add_participant" = "Tambahkan anggota"; +"contacts_user_directory_section" = "DIREKTORI PENGGUNA"; + +// Contacts +"contacts_address_book_section" = "KONTAK LOKAL"; +"directory_cell_description" = "%tu ruangan"; + +// Directory +"directory_cell_title" = "Jelajahi direktori"; +"search_no_result" = "Tidak ada hasil"; +"people_no_conversation" = "Tidak ada obrolan"; +"room_recents_join_room" = "Bergabung ke ruangan"; +"room_recents_create_empty_room" = "Buat ruangan"; +"room_recents_start_chat_with" = "Mulai obrolan"; +"room_recents_server_notice_section" = "PEMBERITAHUAN SISTEM"; +"room_recents_low_priority_section" = "PRIORITAS RENDAH"; +"room_recents_no_conversation" = "Tidak ada ruangan"; + +// Room recents +"room_recents_directory_section" = "DIREKTORI RUANGAN"; +"room_creation_make_private" = "Buat privat"; +"room_creation_keep_private" = "Tetap buat privat"; +"room_creation_make_public" = "Buat publik"; + +// Chat creation +"room_creation_title" = "Obrolan Baru"; + +// Social login + +"social_login_list_title_continue" = "Lanjut dengan"; +"auth_softlogout_clear_data_sign_out" = "Keluar"; +"side_menu_action_feedback" = "Masukan"; +"side_menu_action_help" = "Bantuan"; +"side_menu_action_settings" = "Pengaturan"; + +// Mark: - User avatar view + +"user_avatar_view_accessibility_label" = "avatar"; + +// Mark: Avatar + +"space_avatar_view_accessibility_label" = "avatar"; +"space_tag" = "space"; +"spaces_suggested_room" = "Disarankan"; +"spaces_left_panel_title" = "Space"; +"spaces_home_space_title" = "Beranda"; +"pin_protection_settings_section_header" = "PIN"; +"room_notifs_settings_cancel_action" = "Batalkan"; +"room_notifs_settings_done_action" = "Selesai"; +"room_notifs_settings_none" = "Tidak Ada"; +"room_details_notifs" = "Notifikasi"; +"security_settings_secure_backup_reset" = "Atur Ulang"; +"settings_messages_containing_keywords" = "Keyword"; +"settings_messages_containing_at_room" = "@room"; +"settings_notifications" = "NOTIFIKASI"; +"settings_links" = "TAUTAN"; +"auth_softlogout_sign_in" = "Masuk"; +"space_beta_announce_badge" = "BETA"; +"room_intro_cell_information_dm_sentence1_part3" = ". "; +"room_intro_cell_information_room_sentence1_part3" = ". "; + +// Mark: - Room avatar view + +"room_avatar_view_accessibility_label" = "avatar"; +"call_transfer_error_title" = "Kesalahan"; +"call_transfer_contacts_all" = "Semua"; +"call_transfer_contacts_recent" = "Baru Ini"; +"call_transfer_users" = "Pengguna"; + +// MARK: - Call Transfer +"call_transfer_title" = "Pindahkan"; +"room_info_list_section_other" = "Lainnya"; +"create_room_placeholder_address" = "#ruangantest:matrix.org"; +"create_room_placeholder_topic" = "Topik"; +"create_room_placeholder_name" = "Nama"; +"biometrics_cant_unlocked_alert_message_retry" = "Coba lagi"; +"pin_protection_reset_alert_action_reset" = "Atur Ulang"; +"pin_protection_choose_pin_welcome_after_register" = "Selamat datang."; +"secrets_reset_reset_action" = "Atur Ulang"; +"secrets_setup_recovery_passphrase_confirm_passphrase_title" = "Konfirmasi"; +"secrets_setup_recovery_passphrase_validate_action" = "Selesai"; +"security_settings_secure_backup_info_checking" = "Memeriksa…"; +"secrets_setup_recovery_key_done_action" = "Selesai"; +"secrets_setup_recovery_key_export_action" = "Simpan"; +"secrets_recovery_with_key_recovery_key_title" = "Masukkan"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part3" = "."; +"secrets_recovery_with_passphrase_passphrase_title" = "Masukkan"; + +// Session details + +"user_verification_session_details_trusted_title" = "Dipercayai"; +"user_verification_sessions_list_session_trusted" = "Dipercayai"; +"user_verification_sessions_list_table_title" = "Sesi"; +"user_verification_sessions_list_user_trust_level_unknown_title" = "Tidak Diketahui"; +"user_verification_sessions_list_user_trust_level_warning_title" = "Peringatan"; + +// Sessions list + +"user_verification_sessions_list_user_trust_level_trusted_title" = "Dipercayai"; +"key_verification_tile_conclusion_done_title" = "Terverifikasi"; +"key_verification_tile_request_incoming_approval_decline" = "Tolak"; +"key_verification_tile_request_incoming_approval_accept" = "Terima"; +"key_verification_tile_request_status_expired" = "Kedaluwarsa"; + +// MARK: - Key Verification + +"key_verification_bootstrap_not_setup_title" = "Kesalahan"; + +// MARK: Reaction history +"reaction_history_title" = "Reaksi"; +"emoji_picker_flags_category" = "Bendera"; +"emoji_picker_symbols_category" = "Simbol"; +"emoji_picker_objects_category" = "Benda"; +"emoji_picker_activity_category" = "Aktifitas"; + +// MARK: Emoji picker +"emoji_picker_title" = "Reaksi"; +"device_verification_emoji_pin" = "Pin"; +"device_verification_emoji_folder" = "Map"; +"device_verification_emoji_headphones" = "Headphone"; +"device_verification_emoji_anchor" = "Jangkar"; +"device_verification_emoji_bell" = "Lonceng"; +"device_verification_emoji_trumpet" = "Terompet"; +"device_verification_emoji_guitar" = "Gitar"; +"device_verification_emoji_ball" = "Bola"; +"device_verification_emoji_trophy" = "Piala"; +"device_verification_emoji_rocket" = "Roket"; +"device_verification_emoji_aeroplane" = "Pesawat"; +"device_verification_emoji_bicycle" = "Sepeda"; +"device_verification_emoji_train" = "Kereta Api"; +"device_verification_emoji_flag" = "Bendera"; +"device_verification_emoji_telephone" = "Telepon"; +"device_verification_emoji_hammer" = "Palu"; +"device_verification_emoji_lock" = "Gembok"; +"device_verification_emoji_key" = "Kunci"; +"device_verification_emoji_scissors" = "Gunting"; +"device_verification_emoji_paperclip" = "Klip Kertas"; +"device_verification_emoji_pencil" = "Pensil"; +"device_verification_emoji_book" = "Buku"; +"device_verification_emoji_gift" = "Hadiah"; +"device_verification_emoji_clock" = "Jam"; +"device_verification_emoji_hourglass" = "Jam Pasir"; +"device_verification_emoji_umbrella" = "Payung"; +"device_verification_emoji_santa" = "Santa"; +"device_verification_emoji_spanner" = "Kunci (Bengkel)"; +"device_verification_emoji_glasses" = "Kacamata"; +"device_verification_emoji_hat" = "Topi"; +"device_verification_emoji_robot" = "Robot"; +"device_verification_emoji_smiley" = "Senyuman"; +"device_verification_emoji_heart" = "Hati"; +"device_verification_emoji_cake" = "Kue"; +"device_verification_emoji_pizza" = "Pizza"; +"device_verification_emoji_corn" = "Jagung"; +"device_verification_emoji_strawberry" = "Stroberi"; +"device_verification_emoji_apple" = "Apel"; +"device_verification_emoji_banana" = "Pisang"; +"device_verification_emoji_fire" = "Api"; +"device_verification_emoji_cloud" = "Awan"; +"device_verification_emoji_moon" = "Bulan"; +"device_verification_emoji_globe" = "Bola Dunia"; +"device_verification_emoji_mushroom" = "Jamur"; +"device_verification_emoji_cactus" = "Kaktus"; +"device_verification_emoji_tree" = "Pohon"; +"device_verification_emoji_flower" = "Bunga"; +"device_verification_emoji_butterfly" = "Kupu-Kupu"; +"device_verification_emoji_octopus" = "Gurita"; +"device_verification_emoji_fish" = "Ikan"; +"device_verification_emoji_turtle" = "Kura-Kura"; +"device_verification_emoji_penguin" = "Penguin"; +"device_verification_emoji_rooster" = "Ayam"; +"device_verification_emoji_panda" = "Panda"; +"device_verification_emoji_rabbit" = "Kelinci"; +"device_verification_emoji_elephant" = "Gajah"; +"device_verification_emoji_pig" = "Babi"; +"device_verification_emoji_unicorn" = "Unicorn"; +"device_verification_emoji_horse" = "Kuda"; +"device_verification_emoji_lion" = "Singa"; +"device_verification_emoji_cat" = "Kucing"; + +// MARK: Emoji +"device_verification_emoji_dog" = "Anjing"; + +// MARK: Verified + +// Device + +"device_verification_verified_title" = "Terverifikasi!"; +"key_verification_manually_verify_device_validate_action" = "Verifikasi"; +"key_verification_self_verify_unverified_sessions_alert_validate_action" = "Tampilkan"; +"key_verification_self_verify_current_session_alert_validate_action" = "Verifikasi"; +"device_verification_self_verify_alert_validate_action" = "Verifikasi"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_backup_action" = "Cadangan"; +"key_backup_recover_done_action" = "Selesai"; +"key_backup_recover_from_recovery_key_recovery_key_title" = "Masukkan"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "."; +"key_backup_recover_from_passphrase_passphrase_title" = "Masukkan"; +"key_backup_setup_success_from_passphrase_done_action" = "Selesai"; + +// Success + +"key_backup_setup_success_title" = "Berhasil!"; +"key_backup_setup_passphrase_confirm_passphrase_valid" = "Hebat!"; +"key_backup_setup_passphrase_passphrase_valid" = "Hebat!"; +"key_backup_setup_passphrase_confirm_passphrase_title" = "Konfirmasi"; +"key_backup_setup_passphrase_passphrase_title" = "Masukkan"; +"key_backup_setup_intro_manual_export_info" = "(Lanjutan)"; +"key_backup_setup_skip_alert_skip_action" = "Lewat"; +"deactivate_account_forget_messages_information_part2_emphasize" = "Peringatan"; +"service_terms_modal_decline_button" = "Tolak"; +"service_terms_modal_accept_button" = "Terima"; + +// Widget Picker +"widget_picker_title" = "Integrasi"; +"widget_menu_refresh" = "Muat Ulang"; +"bug_report_send" = "Kirim"; +"e2e_key_backup_wrong_version_button_settings" = "Pengaturan"; +"call_actions_unhold" = "Lanjutkan"; +"large_badge_value_k_format" = "%.1fK"; +"yesterday" = "Kemarin"; +"today" = "Sekarang"; +"you" = "Anda"; + +// Others +"or" = "atau"; +"event_formatter_group_call_leave" = "Tinggalkan"; +"event_formatter_group_call_join" = "Bergabung"; +"event_formatter_call_retry" = "Coba Lagi"; +"event_formatter_call_answer" = "Jawab"; +"event_formatter_call_decline" = "Tolak"; +"event_formatter_message_edited_mention" = "(diedit)"; +"directory_server_placeholder" = "matrix.org"; + +// Directory +"directory_title" = "Direktori"; +"media_picker_select" = "PIlih"; +"media_picker_library" = "Perpustakaan"; +"receipt_status_read" = "Baca: "; +"group_participants_invited_section" = "DIUNDANG"; +"group_participants_invite_prompt_title" = "Konfirmasi"; +"group_participants_remove_prompt_title" = "Konfirmasi"; +"group_details_rooms" = "Ruangan"; +"group_details_people" = "Orang"; +"group_details_home" = "Beranda"; +"room_details_advanced_room_id_for_dm" = "ID:"; +"room_details_advanced_section" = "Lanjutan"; +"room_details_addresses_section" = "Alamat"; +"room_details_history_section_anyone" = "Siapa Saja"; +"room_details_favourite_tag" = "Favorit"; +"room_details_topic" = "Topik"; +"room_details_room_name_for_dm" = "Nama"; +"room_details_photo_for_dm" = "Foto"; +"room_details_settings" = "Pengaturan"; +"room_details_integrations" = "Integrasi"; +"room_details_files" = "Unggahan"; +"room_details_people" = "Anggota"; +"room_details_title_for_dm" = "Detail"; +"identity_server_settings_alert_disconnect_button" = "Putuskan"; +"identity_server_settings_disconnect" = "Putuskan"; +"identity_server_settings_change" = "Ubah"; +"identity_server_settings_add" = "Tambahkan"; +"security_settings_advanced" = "LANJUTAN"; +"security_settings_cryptography" = "KRIPTOGRAFI"; +"security_settings_crosssigning_reset" = "Atur Ulang"; +"security_settings_crosssigning" = "TANDA TANGAN SILANG"; + +// Security settings +"security_settings_title" = "Keamanan"; +"settings_discovery_three_pid_details_revoke_action" = "Hapus"; +"settings_discovery_three_pid_details_share_action" = "Bagikan"; +"settings_discovery_three_pids_management_information_part3" = "."; +"settings_copyright" = "Hak Cipta"; +"settings_ui_theme_black" = "Hitam"; +"settings_ui_theme_dark" = "Gelap"; +"settings_ui_theme_light" = "Terang"; +"settings_ui_theme_auto" = "Otomatis"; +"settings_ui_theme" = "Tema"; +"settings_ui_language" = "Bahasa"; +"settings_security" = "KEAMANAN"; +"settings_three_pids_management_information_part3" = "."; +"settings_three_pids_management_information_part2" = "Penemuan"; +"settings_phone_number" = "Nomor telepon"; +"settings_email_address" = "Email"; +"settings_remove_prompt_title" = "Konfirmasi"; +"settings_surname" = "Nama belakang"; +"settings_cryptography" = "KRIPTOGRAFI"; +"settings_devices" = "SESI"; +"settings_labs" = "UJI COBA"; +"settings_other" = "LAINNYA"; +"settings_advanced" = "LANJUTAN"; +"settings_integrations" = "INTEGRASI"; +"settings_discovery_settings" = "PENEMUAN"; +"settings_calls_settings" = "PANGGILAN"; + +// Settings +"settings_title" = "Pengaturan"; +"media_type_accessibility_sticker" = "Stiker"; +"media_type_accessibility_file" = "File"; +"media_type_accessibility_location" = "Lokasi"; +"media_type_accessibility_video" = "Video"; +"media_type_accessibility_audio" = "Audio"; +"media_type_accessibility_image" = "Gambar"; +"room_join_group_call" = "Gabung"; +"room_accessibility_call" = "Panggilan"; +"room_accessibility_upload" = "Unggah"; +"room_accessibility_integrations" = "Integrasi"; +"room_accessibility_search" = "Cari"; +"room_resource_limit_exceeded_message_contact_1" = " Mohon "; +"room_action_reply" = "Balas"; +"room_event_action_edit" = "Edit"; +"room_event_action_reply" = "Balas"; +"room_event_action_delete" = "Hapus"; +"room_event_action_resend" = "Kirim Ulang"; +"room_event_action_save" = "Simpan"; +"room_event_action_permalink" = "Tautan"; +"room_event_action_share" = "Bagikan"; +"room_event_action_more" = "Lainnya"; +"room_event_action_redact" = "Hapus"; +"room_event_action_quote" = "Kutip"; +"room_event_action_copy" = "Salin"; +"room_ongoing_conference_call_close" = "Tutup"; +"room_message_editing" = "Mengedit"; +"room_member_power_level_short_custom" = "Kustom"; +"room_member_power_level_short_moderator" = "Moderator"; +"room_member_power_level_short_admin" = "Admin"; +"room_participants_action_security_status_warning" = "Peringatan"; +"room_participants_action_security_status_verify" = "Verifikasi"; +"room_participants_action_security_status_verified" = "Terverifikasi"; +"room_participants_action_mention" = "Sebutan"; +"room_participants_action_unban" = "Hilangkan cekalan"; +"room_participants_action_invite" = "Undangan"; +"room_participants_action_section_security" = "Keamanan"; +"room_participants_action_section_other" = "Opsi"; +"room_participants_action_section_devices" = "Sesi"; +"room_participants_ago" = "lalu"; +"room_participants_now" = "sekarang"; +"room_participants_idle" = "Idle"; +"room_participants_unknown" = "Tidak diketahui"; +"room_participants_offline" = "Offline"; +"room_participants_online" = "Online"; +"room_participants_invited_section" = "DIUNDANG"; +"room_participants_invite_prompt_title" = "Konfirmasi"; +"room_participants_remove_prompt_title" = "Konfirmasi"; +"room_participants_leave_prompt_title_for_dm" = "Tinggalkan"; + +// Chat participants +"room_participants_title" = "Anggota"; +"search_in_progress" = "Mencari…"; +"search_default_placeholder" = "Cari"; +"search_files" = "File"; +"search_people" = "Orang"; +"search_messages" = "Pesan"; + +// Search +"search_rooms" = "Ruangan"; +"group_section" = "KOMUNITAS"; + +// Groups tab +"group_invite_section" = "UNDANGAN"; +"rooms_empty_view_title" = "Ruangan"; +"people_empty_view_title" = "Orang"; +"secrets_setup_recovery_key_loading" = "Memuat…"; +"key_verification_tile_request_status_waiting" = "Menunggu…"; +"device_verification_self_verify_start_waiting" = "Menunggu…"; +"event_formatter_call_ringing" = "Berdering…"; +"event_formatter_call_connecting" = "Menghubungkan…"; +"security_settings_crypto_sessions_loading" = "Memuat sesi…"; +"settings_key_backup_info_checking" = "Memeriksa…"; +"unknown_devices_verify" = "Verifikasi…"; +"room_participants_security_loading" = "Memuat…"; +"room_participants_action_security_status_loading" = "Memuat…"; +"directory_searching_title" = "Mencari direktori…"; +"room_details_advanced_room_id" = "ID Ruangan:"; +"room_details_banned_users_section" = "Pengguna yang dicekal"; +"room_details_flair_invalid_id_prompt_title" = "Format tidak valid"; +"room_details_history_section_prompt_title" = "Peringatan privasi"; +"room_details_direct_chat" = "Pesan Langsung"; +"room_details_mute_notifs" = "Bisukan notifikasi"; +"room_details_low_priority_tag" = "Prioritas rendah"; +"room_details_room_name" = "Nama Ruangan"; +"room_details_photo" = "Foto Ruangan"; +"room_details_search" = "Cari ruangan"; +"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "Putuskan saja"; + +// Identity server settings +"identity_server_settings_title" = "Server identitas"; +"manage_session_not_trusted" = "Tidak dipercayai"; +"manage_session_name" = "Nama sesi"; +"manage_session_info" = "INFORMASI SESI"; + +// Manage session +"manage_session_title" = "Kelola sesi"; +"security_settings_complete_security_alert_title" = "Keamanan lengkap"; +"security_settings_crosssigning_complete_security" = "Keamanan lengkap"; +"security_settings_crosssigning_bootstrap" = "Siapkan"; +"security_settings_backup" = "CADANGAN PESAN"; +"security_settings_secure_backup_delete" = "Hapus Cadangan"; +"people_conversation_section" = "OBROLAN"; + +// People tab +"people_invites_section" = "UNDANGAN"; +"room_recents_invites_section" = "UNDANGAN"; +"room_recents_conversations_section" = "RUANGAN"; +"room_recents_people_section" = "ORANG"; +"room_recents_favourites_section" = "FAVORIT"; +"room_creation_privacy" = "Privasi"; +"room_creation_appearance_name" = "Nama"; +"room_creation_appearance" = "Tampilan"; +"room_creation_account" = "Akun"; +"social_login_list_title_sign_up" = "Atau"; +"social_login_list_title_sign_in" = "Atau"; +"auth_reset_password_error_is_required" = "Tidak ada server identitas yang diatur: tambahkan di pengaturan server untuk mengatur ulang kata sandi Anda."; +"auth_reset_password_error_not_found" = "Alamat email Anda terlihat tidak diasosiasikan dengan sebuah ID Matrix di homeserver ini."; +"auth_reset_password_error_unauthorized" = "Gagal untuk memverifikasi alamat email: pastikan Anda membuka tautan yang ada di email"; +"auth_reset_password_next_step_button" = "Saya telah memverifikasi alamat email saya"; +"auth_reset_password_email_validation_message" = "Sebuah email telah dikirim ke %@. Setelah Anda membuka tautan yang ada, klik di bawah."; +"auth_reset_password_missing_password" = "Sebuah kata sandi baru harus dimasukkan."; +"auth_reset_password_missing_email" = "Alamat email yang tertaut dengan akun Anda harus dimasukkan."; +"auth_reset_password_message" = "Untuk mengatur ulang kata sandi Anda, masukkan alamat email yang tertaut ke akun Anda:"; +"auth_recaptcha_message" = "Homeserver ini ingin memastikan bahwa Anda bukan sebuah robot"; +"auth_msisdn_validation_error" = "Tidak dapat memverifikasi nomor telepon."; +"auth_msisdn_validation_message" = "Kami telah mengirim sebuah SMS dengan kode aktivasi. Silakan masukkan kodenya di bawah."; +"auth_msisdn_validation_title" = "Menunggu Verifikasi"; +"done" = "Selesai"; +"open" = "Buka"; diff --git a/Riot/Assets/it.lproj/InfoPlist.strings b/Riot/Assets/it.lproj/InfoPlist.strings index 8e49c5075..6382ef48e 100644 --- a/Riot/Assets/it.lproj/InfoPlist.strings +++ b/Riot/Assets/it.lproj/InfoPlist.strings @@ -2,6 +2,6 @@ "NSCameraUsageDescription" = "La fotocamera viene utilizzata per scattare fotografie, registrare video ed eseguire videochiamate."; "NSPhotoLibraryUsageDescription" = "La libreria fotografica viene utilizzata per inviare foto e video."; "NSMicrophoneUsageDescription" = "Element ha bisogno di accedere al microfono per effettuare e ricevere chiamate, registrare video e messaggi vocali."; -"NSContactsUsageDescription" = "Per scoprire i contatti che già usano Matrix, Element può inviare gli indirizzi email e i numeri di telefono della tua rubrica al server identità che hai scelto. Se supportato, viene fatto un hash dei dati personali prima dell'invio - controlla la politica sulla privacy del tuo server di identità per maggiori informazioni."; +"NSContactsUsageDescription" = "Element mostrerà i tuoi contatti così da poterli invitare in chat."; "NSCalendarsUsageDescription" = "Vedi le tue riunioni programmate nell'app."; "NSFaceIDUsageDescription" = "Face ID viene usato per accedere all'app."; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 70d0cad7a..aa161e9da 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -109,7 +109,7 @@ "room_creation_keep_private" = "Mantieni privata"; "room_creation_make_private" = "Rendi privata"; "room_creation_wait_for_creation" = "Una stanza è già in fase di creazione. Per favore attendi."; -"room_creation_invite_another_user" = "Cerca / invita per ID utente, nome o email"; +"room_creation_invite_another_user" = "ID utente, nome o email"; // Room recents "room_recents_directory_section" = "ELENCO STANZE"; "room_recents_favourites_section" = "PREFERITI"; @@ -295,7 +295,7 @@ "settings_calls_settings" = "CHIAMATE"; "settings_user_interface" = "INTERFACCIA UTENTE"; "settings_ignored_users" = "UTENTI IGNORATI"; -"settings_contacts" = "CONTATTI LOCALI"; +"settings_contacts" = "CONTATTI DEL DISPOSITIVO"; "settings_advanced" = "AVANZATE"; "settings_other" = "ALTRO"; "settings_labs" = "LABORATORIO"; @@ -892,7 +892,7 @@ "widget_menu_remove" = "Rimuovi per tutti"; "settings_integrations" = "INTEGRAZIONI"; "settings_integrations_allow_button" = "Gestisci le integrazioni"; -"settings_integrations_allow_description" = "Usa un Integration Manager (%@) per gestire bot, bridge, widget e pacchetti di sticker.\n\nGli Integration Manager possono ricevere dati di configurazione, modificare widget, mandare inviti alle stanze e modificare permessi a tuo nome."; +"settings_integrations_allow_description" = "Usa un gestore di integrazioni (%@) per gestire bot, bridge, widget e pacchetti di sticker.\n\nI gestori di integrazioni possono ricevere dati di configurazione, modificare widget, mandare inviti alle stanze e modificare permessi a tuo nome."; "widget_integration_manager_disabled" = "Devi attivare il gestore di integrazioni nelle impostazioni"; // Room widget permissions "room_widget_permission_title" = "Carica widget"; @@ -1477,3 +1477,27 @@ "room_recents_suggested_rooms_section" = "STANZE CONSIGLIATE"; "done" = "Fatto"; "open" = "Apri"; +"service_terms_modal_information_description_integration_manager" = "Un gestore di integrazioni ti permette di aggiungere funzioni da terze parti."; +"service_terms_modal_information_description_identity_server" = "Un server d'identità ti aiuta a trovare i tuoi contatti, cercando il loro numero di telefono o l'indirizzo email, per vedere se hanno già un account."; +"service_terms_modal_information_title_integration_manager" = "Gestore di integrazioni"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Server d'identità"; +"service_terms_modal_description_integration_manager" = "Ciò ti permetterà di usare bot, bridge, widget e pacchetti di sticker."; +"service_terms_modal_description_identity_server" = "Ciò aiuterà qualcuno a trovarti se ha il tuo numero di telefono o l'email salvati nei suoi contatti."; +"service_terms_modal_table_header_integration_manager" = "TERMINI DEL GESTORE DI INTEGRAZIONI"; +"service_terms_modal_table_header_identity_server" = "TERMINI DEL SERVER D'IDENTITÀ"; +"service_terms_modal_footer" = "Può essere disattivato in qualsiasi momento nelle impostazioni."; + +// Service terms +"service_terms_modal_title_message" = "Per continuare, accetta i termini e condizioni sottostanti"; +"settings_contacts_enable_sync_description" = "Verrà usato il tuo server d'identità per connetterti ai tuoi contatti e per aiutarli a trovarti."; +"settings_contacts_enable_sync" = "Trova i tuoi contatti"; +"settings_phone_contacts" = "CONTATTI DEL TELEFONO"; +"find_your_contacts_identity_service_error" = "Impossibile connettersi al server d'identità."; +"find_your_contacts_footer" = "Può essere disattivato in qualsiasi momento dalle impostazioni."; +"find_your_contacts_button_title" = "Trova i tuoi contatti"; +"find_your_contacts_message" = "Lascia che %@ mostri i tuoi contatti per poter iniziare subito a chattare con chi conosci già."; +"find_your_contacts_title" = "Inizia elencando i tuoi contatti"; +"contacts_address_book_permission_denied_alert_message" = "Per attivare i contatti, vai nelle impostazioni del tuo dispositivo."; +"contacts_address_book_permission_denied_alert_title" = "Contatti disattivati"; diff --git a/Riot/Assets/pt_BR.lproj/InfoPlist.strings b/Riot/Assets/pt_BR.lproj/InfoPlist.strings index 6449e513c..bb44cdb98 100644 --- a/Riot/Assets/pt_BR.lproj/InfoPlist.strings +++ b/Riot/Assets/pt_BR.lproj/InfoPlist.strings @@ -2,6 +2,6 @@ "NSCameraUsageDescription" = "A câmera é usada para tirar fotos e vídeos, fazer chamadas de vídeo."; "NSPhotoLibraryUsageDescription" = "A biblioteca de fotos é usada para enviar fotos e vídeos."; "NSMicrophoneUsageDescription" = "Element precisa acessar seu microfone para fazer e receber chamadas, tirar vídeos, e gravar mensagens de voz."; -"NSContactsUsageDescription" = "Para descobrir contatos já usando Matrix, Element pode enviar endereços de email e números de telefone em seu livro de endereços para seu servidor de identidade Matrix escolhido. Onde suportado, dados pessoais são hashados antes do envio - por favor cheque a política de privacidade de seu servidor de identidade para mais detalhes."; +"NSContactsUsageDescription" = "Element vai mostrar seus contatos para que você possa convidá-los a conversar."; "NSCalendarsUsageDescription" = "Ver suas reuniões agendadas no app."; "NSFaceIDUsageDescription" = "Face ID é usada para acessar seu app."; diff --git a/Riot/Assets/pt_BR.lproj/Localizable.strings b/Riot/Assets/pt_BR.lproj/Localizable.strings index aed9c682c..165ee183e 100644 --- a/Riot/Assets/pt_BR.lproj/Localizable.strings +++ b/Riot/Assets/pt_BR.lproj/Localizable.strings @@ -33,7 +33,7 @@ /* Look, stuff's happened, alright? Just open the app. */ "MSGS_IN_TWO_PLUS_ROOMS" = "%@ novas mensagens em %@, %@ e outras"; /* A user has invited you to a chat */ -"USER_INVITE_TO_CHAT" = "%@ tem conviado você para fazer chat"; +"USER_INVITE_TO_CHAT" = "%@ tem conviado você para conversar"; /* A user has invited you to an (unamed) group chat */ "USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ tem convidado você para um chat de grupo"; /* A user has invited you to a named room */ diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index a2d68de2b..77ab52883 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -100,7 +100,7 @@ "room_creation_keep_private" = "Manter privado"; "room_creation_make_private" = "Fazer privado"; "room_creation_wait_for_creation" = "Uma sala já está sendo criada. Por favor espere."; -"room_creation_invite_another_user" = "Buscar / convidar por ID de usuária(o), Nome ou email"; +"room_creation_invite_another_user" = "ID de usuária(o), nome ou email"; "room_recents_favourites_section" = "FAVORITOS"; "room_recents_people_section" = "PESSOAS"; "room_recents_conversations_section" = "SALAS"; @@ -288,7 +288,7 @@ "settings_calls_settings" = "CHAMADAS"; "settings_user_interface" = "INTERFACE DE USUÁRIA(O)"; "settings_ignored_users" = "USUÁRIAS(OS) IGNORADAS(OS)"; -"settings_contacts" = "CONTATOS LOCAIS"; +"settings_contacts" = "CONTATOS DE DISPOSITIVO"; "settings_advanced" = "AVANÇADAS"; "settings_other" = "OUTRAS"; "settings_labs" = "LABS"; @@ -556,7 +556,7 @@ "rerequest_keys_alert_message" = "Por favor lance %@ num outro dispositivo que possa decriptar a mensagem para que ele possa enviar as chaves para esta sessão."; // String for App Store "store_short_description" = "Chat/VoIP descentralizado e seguro"; -"store_full_description" = "Element is um novo tipo de mensageiro e app de colaboração que:\n\n1. Põe você em controle para preservar sua privacidade\n2. Permite você se comunicar com qualquer pessoa na rede Matrix, e até além ao integrar-se com apps como Slack\n3. Protege você de publicidade, datamining, backdoors e jardins murados\n4. Assegura você através de encriptação ponta-a-pontam com assinatura cruzada para verificar ouras(os)\n\nElement é completamente diferente de outros apps de mensageria e colaboração porque ele é descentralizado e open source.\n\nElement permite você auto-hospedar - ou escolher um host - para que você tenha privacidade, propriedade e controle de seus dados e conversas. Ele dá a você acesso a uma rede aberta; então você não está simplesmente preso falando com outras(os) usuárias(os) Element somente. E ele é muito seguro.\n\nElement é capaz de fazer tudo isso porque ele opera em Matrix - o padrão para comunicação aberta e descentralizada.\n\nElement põe você em controle ao permitir você escolher quem hospeda suas conversas. De seu app Element, você pode escolher hospedar de diferentes maneiras:\n\n1. Pegar uma conta grátis no servidor público matrix.org\n2. Auto-hospedar sua conta ao rodar um servidor em seu próprio hardware\n3. Registrar-se para uma conta num servidor personalizado ao simplesmente assinar a plataforma de hospedagem Element Matrix Services\n\nPorquê escolher Element?\n\nTENHA POSSE DE SEUS DADOS: Você decide onde manter seus dados e mensagens. Você os possui e controla, não alguma MEGACORP que mina seus dados ou dá acesso a terceiros.\n\nMENSAGERIA E COLABORAÇÃO ABERTOS: Você pode fazer chat com qualquer outra pessoa na rede Matrix, caso ela esteja usando Element ou um outro app Matrix, e mesmo se ela estiver usando um sistema de mensageria diferente, do tipo de Slack, IRC ou XMPP.\n\nSUPER-SEGURO: Encriptação ponta-a-ponta real (somente aquelas/es na conversa podem decriptar mensagens), e assinatura cruzada para verificar os dispositivos de participantes de conversa.\n\nCOMUNICAÇÃO COMPLETA: Mensageria, chamadas de voz e vídeo, compartilhamento de arquivo, compartilhamento de tela e um monte de integrações, bots e widgets. Construa salas, comunidades, fique em contato e tenha as coisas feitas.\n\nEM TODO LUGAR ONDE VOCÊ ESTEJA: Fique em contato onde quer que você esteja com histórico de mensagem sincronizado por todos os dispositivos e na web em https://element.io/app."; +"store_full_description" = "Element is um novo tipo de mensageiro e app de colaboração que:\n\n1. Põe você em controle para preservar sua privacidade\n2. Permite você se comunicar com qualquer pessoa na rede Matrix, e até além ao integrar-se com apps como Slack\n3. Protege você de publicidade, datamining, backdoors e jardins murados\n4. Assegura você através de encriptação ponta-a-pontam com assinatura cruzada para verificar ouras(os)\n\nElement é completamente diferente de outros apps de mensageria e colaboração porque ele é descentralizado e open source.\n\nElement permite você auto-hospedar - ou escolher um host - para que você tenha privacidade, propriedade e controle de seus dados e conversas. Ele dá a você acesso a uma rede aberta; então você não está simplesmente preso falando com outras(os) usuárias(os) Element somente. E ele é muito seguro.\n\nElement é capaz de fazer tudo isso porque ele opera em Matrix - o padrão para comunicação aberta e descentralizada.\n\nElement põe você em controle ao permitir você escolher quem hospeda suas conversas. De seu app Element, você pode escolher hospedar de diferentes maneiras:\n\n1. Pegar uma conta grátis no servidor público matrix.org\n2. Auto-hospedar sua conta ao rodar um servidor em seu próprio hardware\n3. Registrar-se para uma conta num servidor personalizado ao simplesmente assinar a plataforma de hospedagem Element Matrix Services\n\nPorquê escolher Element?\n\nTENHA POSSE DE SEUS DADOS: Você decide onde manter seus dados e mensagens. Você os possui e controla, não alguma MEGACORP que mina seus dados ou dá acesso a terceiros.\n\nMENSAGERIA E COLABORAÇÃO ABERTOS: Você pode conversar com qualquer outra pessoa na rede Matrix, caso ela esteja usando Element ou um outro app Matrix, e mesmo se ela estiver usando um sistema de mensageria diferente, do tipo de Slack, IRC ou XMPP.\n\nSUPER-SEGURO: Encriptação ponta-a-ponta real (somente aquelas/es na conversa podem decriptar mensagens), e assinatura cruzada para verificar os dispositivos de participantes de conversa.\n\nCOMUNICAÇÃO COMPLETA: Mensageria, chamadas de voz e vídeo, compartilhamento de arquivo, compartilhamento de tela e um monte de integrações, bots e widgets. Construa salas, comunidades, fique em contato e tenha as coisas feitas.\n\nEM TODO LUGAR ONDE VOCÊ ESTEJA: Fique em contato onde quer que você esteja com histórico de mensagem sincronizado por todos os dispositivos e na web em https://element.io/app."; "auth_login_single_sign_on" = "Fazer Sign In"; "auth_autodiscover_invalid_response" = "Resposta de descoberta de servidorcasa inválida"; "room_message_unable_open_link_error_message" = "Incapaz de abrir o link."; @@ -720,7 +720,7 @@ "settings_three_pids_management_information_part3" = "."; "settings_security" = "SEGURANÇA"; "settings_integrations_allow_button" = "Gerenciar integrações"; -"settings_integrations_allow_description" = "Use um Gerenciador de Integrações (%@) para gerenciar bots, bridges, widgets e pacotes de stickers.\n\nGerenciadores de Integrações recebem dados de configuração, e podem modificar widgets, enviar convites de sala e definir níveis de poder em seu nome."; +"settings_integrations_allow_description" = "Use um gerenciador de integrações (%@) para gerenciar bots, bridges, widgets e pacotes de stickers.\n\nGerenciadores de integrações recebem dados de configuração, e podem modificar widgets, enviar convites de sala e definir níveis de poder em seu nome."; "settings_add_3pid_password_title_email" = "Adicionar endereço de email"; "settings_discovery_three_pids_management_information_part2" = "Configurações de Usuária(o)"; "settings_discovery_three_pids_management_information_part3" = "."; @@ -779,7 +779,7 @@ "room_participants_action_security_status_warning" = "Aviso"; "settings_three_pids_management_information_part1" = "Gerencie quais endereços de email ou números de telefone você pode usar para fazer login ou recuperar sua conta aqui. Controle quem pode encontrar você em "; "settings_calls_stun_server_fallback_button" = "Permitir servidor fallback de assistência de chamadas"; -"settings_discovery_terms_not_signed" = "Concorde com os Termos de Serviço do Servidor de Identidade (%@) para permitir que você mesma(o) seja descobertável por endereço de email ou número de telefone."; +"settings_discovery_terms_not_signed" = "Concorde com os Termos de Serviço do servidor de identidade (%@) para permitir que você mesma(o) seja descobertável por endereço de email ou número de telefone."; "settings_discovery_three_pids_management_information_part1" = "Gerencie quais endereços de email ou números de telefone outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você para salas. Adicione ou remova endereços de email ou números de telefone desta lista em "; "settings_discovery_three_pid_details_title_email" = "Gerenciar email"; "settings_discovery_three_pid_details_information_email" = "Gerencie preferências para este endereço de email, que outras(os) usuárias(os) podem usar para descobrir você e usar para convidar você para salas. Adicione ou remova endereços de email em Contas."; @@ -794,7 +794,7 @@ "identity_server_settings_disconnect_info" = "Desconectar-se de seu servidor de identidade vai significar que você não vai ser descobertável por outras(os) usuárias(os) e ser capaz de convidar outras(os) por email ou telefone."; "identity_server_settings_alert_disconnect_still_sharing_3pid" = "Você ainda está compartilhando seus dados pessoais no servidor de identidade %@.\n\nNós recomendamos que você remova seus endereços de email e números de telefone do servidor de identidade antes de se desconectar."; "call_no_stun_server_error_title" = "Chamada falhou devido a servidor malconfigurado"; -"widget_integration_manager_disabled" = "Você precisa habilitar Gerenciador de Integrações em configurações"; +"widget_integration_manager_disabled" = "Você precisa habilitar gerenciador de integrações em configurações"; "service_terms_modal_description_for_identity_server_1" = "Encontrar outras(os) por telefone ou email"; "service_terms_modal_description_for_identity_server_2" = "Ser encontrada(o) por telefone ou email"; "device_verification_self_verify_wait_additional_information" = "Isto funciona com %@ e outros clientes Matrix capazes de assinatura cruzada."; @@ -885,7 +885,7 @@ "manage_session_not_trusted" = "Não confiada"; "manage_session_sign_out" = "Fazer signout desta sessão"; // Identity server settings -"identity_server_settings_title" = "Servidor de Identidade"; +"identity_server_settings_title" = "Servidor de identidade"; "identity_server_settings_description" = "Vvocê está atualmente usando %@ para descobrir e ser descobertável por contatos existentes que você conhece."; "identity_server_settings_no_is_description" = "Você não está atualmente usando um servidor de identidade. Para descobrir e ser descobertável por contatos existentes, adicione um acima."; "identity_server_settings_place_holder" = "Entrar um servidor de identidade"; @@ -1474,3 +1474,27 @@ "room_recents_suggested_rooms_section" = "SALAS SUGERIDAS"; "done" = "Feito"; "open" = "Abrir"; +"service_terms_modal_information_description_integration_manager" = "Um gerenciador de integrações deixa você adicionar funcionalidades de terceiros."; +"service_terms_modal_information_description_identity_server" = "Um servidor de identidade ajuda você a encontrar seus contatos, ao buscar o número de telefone ou endereço de email deles, para ver se eles já têm uma conta."; +"service_terms_modal_information_title_integration_manager" = "Gerenciador de Integrações"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Servidor de Identidade"; +"service_terms_modal_description_integration_manager" = "Isto vai permitir você usar bots, bridges, widgets e pacotes de stickers."; +"service_terms_modal_description_identity_server" = "Isto vai permitir alguém encontrar você se ela/ele tem seu número de telefone ou email salvo nos contatos de telefone dela/dele."; +"service_terms_modal_table_header_integration_manager" = "TERMOS DE GERENCIADOR DE INTEGRAÇÕES"; +"service_terms_modal_table_header_identity_server" = "TERMOS DE SERVIDOR DE IDENTIDADE"; +"service_terms_modal_footer" = "Isto pode ser desabilitado a qualquer hora em configurações."; + +// Service terms +"service_terms_modal_title_message" = "Para continuar, aceite os termos e condições abaixo"; +"settings_contacts_enable_sync_description" = "Isto vai usar seu servidor de identidade para conectar você com seus contatos, e ajudá-los a encontrar você."; +"settings_contacts_enable_sync" = "Encontre seus contatos"; +"settings_phone_contacts" = "CONTATOS DE TELEFONE"; +"find_your_contacts_identity_service_error" = "Incapaz de se conectar ao servidor de identidade."; +"find_your_contacts_footer" = "Isto pode ser desabilitado a qualquer hora a partir de configurações."; +"find_your_contacts_button_title" = "Encontre seus contatos"; +"find_your_contacts_message" = "Deixe %@ mostrar seus contatos para que você possa rapidamente começar a conversar com aqueles que você conhece melhor."; +"find_your_contacts_title" = "Comece por listar seus contatos"; +"contacts_address_book_permission_denied_alert_message" = "Para habilitar contatos, vá para as configurações de seu dispositivo."; +"contacts_address_book_permission_denied_alert_title" = "Contatos desabilitados"; diff --git a/Riot/Assets/ru.lproj/InfoPlist.strings b/Riot/Assets/ru.lproj/InfoPlist.strings index 5edbd7a0c..9608ff9a5 100644 --- a/Riot/Assets/ru.lproj/InfoPlist.strings +++ b/Riot/Assets/ru.lproj/InfoPlist.strings @@ -1,7 +1,7 @@ // Permissions usage explanations -"NSCameraUsageDescription" = "Камера используется для съемки фотографий и видеороликов, а также для видеозвонков."; -"NSPhotoLibraryUsageDescription" = "Галерея используется для отправки фотографий и видео."; +"NSCameraUsageDescription" = "Камера используется для съемки фото и видео, совершения видеозвонков."; +"NSPhotoLibraryUsageDescription" = "Галерея используется для отправки фото и видео."; "NSMicrophoneUsageDescription" = "Element необходим доступ к вашему микрофону, чтобы совершать и принимать звонки, снимать видео и записывать голосовые сообщения."; -"NSContactsUsageDescription" = "Чтобы обнаружить контакты, уже использующие Matrix, Element может отправлять адреса электронной почты и номера телефонов из адресной книги на выбранный вами сервер идентификации Matrix. Если поддерживается, то личные данные перед отправкой хэшируются - пожалуйста, ознакомьтесь с политикой конфиденциальности вашего сервера идентификации для получения более подробной информации."; -"NSCalendarsUsageDescription" = "Ознакомьтесь со своими запланированными встречами в приложении."; +"NSContactsUsageDescription" = "Element покажет ваши контакты, чтобы вы могли пригласить их в чат."; +"NSCalendarsUsageDescription" = "Просматривайте запланированные встречи в приложении."; "NSFaceIDUsageDescription" = "Face ID используется для доступа к вашему приложению."; diff --git a/Riot/Assets/ru.lproj/Vector.strings b/Riot/Assets/ru.lproj/Vector.strings index e46438442..ad004e9e0 100644 --- a/Riot/Assets/ru.lproj/Vector.strings +++ b/Riot/Assets/ru.lproj/Vector.strings @@ -95,7 +95,7 @@ "room_creation_keep_private" = "Оставить приватным"; "room_creation_make_private" = "Сделать приватным"; "room_creation_wait_for_creation" = "Комната уже создана. Подождите."; -"room_creation_invite_another_user" = "Поиск / приглашение по идентификатору пользователя, имени или адресу электронной почты"; +"room_creation_invite_another_user" = "Идентификатор пользователя, имя или электронная почта"; // Room recents "room_recents_directory_section" = "КАТАЛОГ КОМНАТ"; "room_recents_favourites_section" = "ИЗБРАННЫЕ"; @@ -237,7 +237,7 @@ "settings_notifications_settings" = "НАСТРОЙКИ УВЕДОМЛЕНИЙ"; "settings_user_interface" = "ПОЛЬЗОВАТЕЛЬСКИЙ ИНТЕРФЕЙС"; "settings_ignored_users" = "ИГНОРИРУЕМЫЕ ПОЛЬЗОВАТЕЛИ"; -"settings_contacts" = "ЛОКАЛЬНЫЕ КОНТАКТЫ"; +"settings_contacts" = "КОНТАКТЫ УСТРОЙСТВА"; "settings_advanced" = "ДОПОЛНИТЕЛЬНО"; "settings_other" = "ДРУГИЕ"; "settings_labs" = "ЛАБОРАТОРИЯ"; @@ -1420,20 +1420,20 @@ "event_formatter_call_has_ended_with_time" = "Вызов закончен • %@"; "settings_notifications" = "УВЕДОМЛЕНИЯ"; "version_check_modal_action_title_deprecated" = "Узнайте, как"; -"version_check_modal_subtitle_deprecated" = "Мы работали над улучшением Element для более быстрой и совершенной работы. К сожалению, ваша текущая версия iOS не совместима с некоторыми из этих исправлений и больше не будет поддерживаться.\nМы советуем вам обновить свою операционную систему, чтобы использовать Element в полной мере."; +"version_check_modal_subtitle_deprecated" = "Мы работали над улучшением %@ для более быстрой и совершенной работы. К сожалению, ваша текущая версия iOS не совместима с некоторыми из этих исправлений и больше не будет поддерживаться.\nМы советуем вам обновить свою операционную систему, чтобы использовать %@ в полной мере."; "version_check_modal_title_deprecated" = "Мы больше не поддерживаем iOS %@"; "version_check_modal_action_title_supported" = "Понятно"; -"version_check_modal_subtitle_supported" = "Мы работали над улучшением Element для более быстрой и совершенной работы. К сожалению, ваша текущая версия iOS не совместима с некоторыми из этих исправлений и больше не будет поддерживаться.\nМы советуем вам обновить свою операционную систему, чтобы использовать Element в полной мере."; +"version_check_modal_subtitle_supported" = "Мы работали над улучшением %@ для более быстрой и совершенной работы. К сожалению, ваша текущая версия iOS не совместима с некоторыми из этих исправлений и больше не будет поддерживаться.\nМы советуем вам обновить свою операционную систему, чтобы использовать %@ в полной мере."; "version_check_modal_title_supported" = "Мы прекращаем поддержку iOS %@"; -"version_check_banner_subtitle_deprecated" = "Мы больше не поддерживаем Element на iOS %@. Чтобы продолжать использовать Element в полной мере, мы советуем вам обновить версию iOS."; +"version_check_banner_subtitle_deprecated" = "Мы больше не поддерживаем %@ на iOS %@. Чтобы продолжать использовать %@ в полной мере, мы советуем вам обновить версию iOS."; "version_check_banner_title_deprecated" = "Мы больше не поддерживаем iOS %@"; -"version_check_banner_subtitle_supported" = "В ближайшее время мы прекращаем поддержку Element на iOS %@. Чтобы продолжать использовать Element в полной мере, мы советуем вам обновить вашу версию iOS."; +"version_check_banner_subtitle_supported" = "В ближайшее время мы прекращаем поддержку %@ на iOS %@. Чтобы продолжать использовать %@ в полной мере, мы советуем вам обновить версию iOS."; // Mark: - Version check "version_check_banner_title_supported" = "Мы прекращаем поддержку iOS %@"; "settings_show_url_previews_description" = "Предварительный просмотр будет осуществляться только в незашифрованных комнатах."; -"settings_show_url_previews" = "Показывать встроенные предварительные просмотры URL-адресов"; +"settings_show_url_previews" = "Предварительный просмотр веб-сайта"; "settings_mentions_and_keywords_encryption_notice" = "Вы не будете получать уведомления об упоминаниях и ключевых словах в зашифрованных комнатах на мобильных устройствах."; "settings_new_keyword" = "Добавить новое ключевое слово"; "settings_your_keywords" = "Ваши ключевые слова"; @@ -1487,3 +1487,27 @@ "settings_links" = "ССЫЛКИ"; "done" = "Готово"; "open" = "Открыть"; +"service_terms_modal_information_description_integration_manager" = "Менеджер интеграции позволяет добавлять функции от сторонних производителей."; +"service_terms_modal_information_description_identity_server" = "Сервер идентификации помогает найти контакты, просматривая номер телефона или адрес электронной почты, чтобы узнать, есть ли у них учетная запись."; +"service_terms_modal_information_title_integration_manager" = "Менеджер интеграции"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Сервер идентификации"; +"service_terms_modal_description_integration_manager" = "Это позволит вам использовать ботов, мосты, виджеты и пакеты стикеров."; +"service_terms_modal_description_identity_server" = "Это позволит найти вас, если у них есть ваш номер телефона или электронная почта, сохраненные в контактах на его телефоне."; +"service_terms_modal_table_header_integration_manager" = "УСЛОВИЯ МЕНЕДЖЕРА ИНТЕГРАЦИИ"; +"service_terms_modal_table_header_identity_server" = "УСЛОВИЯ СЕРВЕРА ИДЕНТИФИКАЦИИ"; +"service_terms_modal_footer" = "Это можно отключить в любое время в настройках."; + +// Service terms +"service_terms_modal_title_message" = "Чтобы продолжить, примите приведенные ниже положения и условия"; +"settings_contacts_enable_sync_description" = "Это позволит использовать ваш сервер идентификации для связи с вашими контактами и поможет им найти вас."; +"settings_contacts_enable_sync" = "Поиск контактов"; +"settings_phone_contacts" = "КОНТАКТЫ ТЕЛЕФОНА"; +"find_your_contacts_identity_service_error" = "Невозможно подключиться к серверу идентификации."; +"find_your_contacts_footer" = "Это можно отключить в настройках в любое время."; +"find_your_contacts_button_title" = "Поиск контактов"; +"find_your_contacts_message" = "Пусть %@ покажет ваши контакты, так вы сразу начнёте общаться с теми, кого вы хорошо знаете."; +"find_your_contacts_title" = "Начните с составления списка контактов"; +"contacts_address_book_permission_denied_alert_message" = "Для включения контактов, перейдите в настройки устройства."; +"contacts_address_book_permission_denied_alert_title" = "Контакты отключены"; diff --git a/Riot/Assets/sq.lproj/InfoPlist.strings b/Riot/Assets/sq.lproj/InfoPlist.strings index f5a1bd783..ee2f2697a 100644 --- a/Riot/Assets/sq.lproj/InfoPlist.strings +++ b/Riot/Assets/sq.lproj/InfoPlist.strings @@ -2,6 +2,6 @@ "NSCameraUsageDescription" = "Kamera përdoret për të bërë foto dhe regjistruar video, dhe për të bërë thirrje video."; "NSPhotoLibraryUsageDescription" = "Fototeka përdoret për të dërguar foto dhe video."; "NSMicrophoneUsageDescription" = "Element-it i duhet të përdorë mikrofonin tuaj për të bërë dhe marrë thirrje, për të regjistruar video, dhe për të regjistruar mesazhe zanorë."; -"NSContactsUsageDescription" = "Për zbulim kontaktesh që përdorin tashmë Matrix-in, Element-i mund të dërgojë adresa email dhe numra telefonash nga libri juaj i adresave te shërbyesi juaj i zgjedhur Matrix i identiteteve. Kur kjo mbulohet, të dhënat personale fshehtëzohen, para se të dërgohen - ju lutemi, për më tepër hollësi, shihni rregulla privatësie të shërbyesit tuaj të identiteteve."; +"NSContactsUsageDescription" = "Element-i do të shfaqë kontaktet tuaja, që kështu të mund t’i ftoni për të biseduar."; "NSCalendarsUsageDescription" = "Shihini te aplikacioni takimet tuaja të planifikuara."; "NSFaceIDUsageDescription" = "Face ID përdoret që të hyni në aplikacionin tuaj."; diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index 0f1043ea5..221b56ec4 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -61,7 +61,7 @@ "auth_phone_in_use" = "Ky numër telefoni është tashmë në përdorim"; "auth_untrusted_id_server" = "Shërbyesi i identiteteve s’është i besuar"; "auth_password_dont_match" = "Fjalëkalimet s’përputhen"; -"auth_username_in_use" = "Emër përdoruesi në përdorim"; +"auth_username_in_use" = "Emër përdoruesi i përdorur"; "auth_forgot_password" = "Harruat fjalëkalimin?"; "auth_email_not_found" = "S’u arrit të dërgohej email: Kjo adresë email s’u gjet"; "auth_email_validation_message" = "Ju lutemi, që të vazhdojë regjistrimi, kontrolloni email-in tuaj"; @@ -220,7 +220,7 @@ "settings_calls_settings" = "THIRRJE"; "settings_user_interface" = "NDËRFAQE PËRDORUESI"; "settings_ignored_users" = "PËRDORUES TË SHPËRFILLUR"; -"settings_contacts" = "KONTAKTE VENDORE"; +"settings_contacts" = "KONTAKTE PAJISJEJE"; "settings_advanced" = "TË MËTEJSHME"; "settings_other" = "TË TJERA"; "settings_devices" = "SESIONE"; @@ -397,7 +397,7 @@ "auth_use_server_options" = "Përdor mundësi vetjake shërbyesi (e përparuar)"; "auth_add_email_and_phone_warning" = "Regjistrimi me email dhe me numër telefoni njëherazi nuk mbulohet ende, deri sa të ketë API. Do të merret parasysh vetëm numri i telefonit. Email-in tuaj mund ta shtoni te profili juaj, te rregullimet."; "room_creation_appearance_picture" = "Foto fjalosjeje (në daçi)"; -"room_creation_invite_another_user" = "Kërkoni / ftoni sipas ID-je Përdoruesi, Emri ose email-i"; +"room_creation_invite_another_user" = "ID Përdoruesi, emër ose email"; "room_recents_favourites_section" = "TË PARAPALQYERA"; "room_recents_server_notice_section" = "SINJALIZIME SISTEMI"; "room_recents_join_room_title" = "Hyni në një dhomë"; @@ -854,7 +854,7 @@ "settings_add_3pid_invalid_password_message" = "Kredenciale të pavlefshme"; "settings_devices_description" = "Emri publik i një sesioni është i dukshëm për persona me të cilët komunikoni"; "settings_discovery_no_identity_server" = "S’po përdorni ndonjë shërbyes identitetesh. Që të jeni i zbulueshëm nga kontakte ekzistuese që njihni, shtoni një të tillë."; -"settings_discovery_terms_not_signed" = "Pajtohuni me Kushtet e Shërbimit të Shërbyesit të Identiteteve që t’i lejoni vetes të jeni i zbulueshëm përmes adrese email ose numri telefoni."; +"settings_discovery_terms_not_signed" = "Pajtohuni me Kushtet e Shërbimit të shërbyesit të identiteteve (%@), që t’i lejoni vetes të jeni i zbulueshëm përmes adrese email ose numri telefoni."; "settings_discovery_three_pids_management_information_part1" = "Administroni cilat adresa email ose numra telefonash mund të përdorin përdoruesit e tjerë për t’ju zbuluar dhe ftuar në dhoma. Shtoni ose hiqni prej kësaj liste adresa email ose numra telefonash "; "settings_discovery_three_pids_management_information_part2" = "Rregullime Përdoruesi"; "settings_discovery_three_pids_management_information_part3" = "."; @@ -871,7 +871,7 @@ "settings_identity_server_no_is" = "S’ka të formësuar shërbyes identitetesh"; "settings_identity_server_no_is_description" = "S’po përdorni ndonjë shërbyes identitetesh. Që të zbuloni dhe të jeni i zbulueshëm nga kontakte ekzistuese që njihni, shtoni një më sipër."; // Identity server settings -"identity_server_settings_title" = "Shërbyes Identitetesh"; +"identity_server_settings_title" = "Shërbyes identitetesh"; "identity_server_settings_description" = "Po përdorni %@ që të zbuloni dhe të jeni i zbulueshëm nga kontakte ekzistuese që dini."; "identity_server_settings_no_is_description" = "S’po përdorni ndonjë shërbyes identitetesh. Që të zbuloni dhe të jeni i zbulueshëm nga kontakte ekzistuese, shtoni një më sipër."; "identity_server_settings_place_holder" = "Jepni një shërbyes identitetesh"; @@ -909,12 +909,12 @@ "accessibility_checkbox_label" = "kutizë"; "settings_integrations" = "INTEGRIME"; "settings_integrations_allow_button" = "Administroni integrime"; -"settings_integrations_allow_description" = "Përdorni një Përgjegjës Integrimesh (%@) që të administroni robotë, ura, widget-e dhe paketa ngjitësish.\n\nPërgjegjësit e Integrimeve marrin të dhëna formësimi dhe mund të ndryshojnë widget-e, të dërgojnë ftesa për në dhoma dhe të caktojnë shkallë pushteti në emrin tuaj."; +"settings_integrations_allow_description" = "Përdorni një përgjegjës integrimesh (%@) që të administroni robotë, ura, widget-e dhe paketa ngjitësish.\n\nPërgjegjësit e integrimeve marrin të dhëna formësimi dhe mund të ndryshojnë widget-e, të dërgojnë ftesa për në dhoma dhe të caktojnë shkallë pushteti në emrin tuaj."; "widget_menu_refresh" = "Rifreskoje"; "widget_menu_open_outside" = "Hape në shfletues"; "widget_menu_revoke_permission" = "Shfuqizo hyrje për mua"; "widget_menu_remove" = "Hiqe për këdo"; -"widget_integration_manager_disabled" = "Lypset të aktivizoni Përgjegjës Integrimesh te rregullimet"; +"widget_integration_manager_disabled" = "Lypset të aktivizoni përgjegjës integrimesh te rregullimet"; "widget_picker_manage_integrations" = "Administroni integrime…"; // Room widget permissions "room_widget_permission_title" = "Ngarko Widget"; @@ -1492,3 +1492,29 @@ "settings_links" = "LIDHJE"; "room_recents_suggested_rooms_section" = "DHOMA TË SUGJERUARA"; "done" = "U bë"; +"spaces_home_space_title" = "Kreu"; +"service_terms_modal_information_description_integration_manager" = "Një përgjegjës integrimesh ju lejon të shtoni veçori prej palësh të treta."; +"service_terms_modal_information_description_identity_server" = "Një shërbyes identitetesh ju ndihmon të gjeni kontaktet tuaja, duke kërkuar numrat e telefonave ose adresat email të tyre, për të parë nëse kanë tashmë një llogari."; +"service_terms_modal_information_title_integration_manager" = "Përgjegjës Integrimesh"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Shërbyes Identitetesh"; +"service_terms_modal_description_integration_manager" = "Kjo do t’ju lejojë të përdorni robotë, ura, widget-e dhe paketa ngjitësish."; +"service_terms_modal_description_identity_server" = "Kjo do t’i lejojë dikujt t’ju gjejë, nëse ka ruajtur në kontaktet e telefonit të vet numrin tuaj të telefonit ose email-in tuaj."; +"service_terms_modal_table_header_integration_manager" = "KUSHTE PËRGJEGJËSI INTEGRIMESH"; +"service_terms_modal_table_header_identity_server" = "KUSHTE SHËRBYESI IDENTITETESH"; +"service_terms_modal_footer" = "Kjo mund të çaktivizohet kurdo që nga rregullimet."; + +// Service terms +"service_terms_modal_title_message" = "Që të vazhdohet, pranoni termat dhe kushtet më poshtë"; +"security_settings_secure_backup_reset" = "Riktheji te parazgjedhjet"; +"settings_contacts_enable_sync_description" = "Kjo do të përdorë shërbyesin tuaj të identiteteve për t’ju lidhur me kontaktet tuaja dhe për t’i ndihmuar ata t’ju gjejnë."; +"settings_contacts_enable_sync" = "Gjeni kontaktet tuaja"; +"settings_phone_contacts" = "KONTAKTE TELEFONI"; +"find_your_contacts_identity_service_error" = "S’arrihet të lidhet me shërbyesin e identiteteve."; +"find_your_contacts_footer" = "Kjo mund të çaktivizohet kurdo që nga rregullimet."; +"find_your_contacts_button_title" = "Gjeni kontakte tuajt"; +"find_your_contacts_message" = "Lejojeni %@ të shfaqë kontaktet tuaja, që kështu të mund të filloni shpejt e shpejt të bisedoni me ata që ju njohin më mirë."; +"find_your_contacts_title" = "Fillojani duke shfaqur kontaktet tuaja"; +"contacts_address_book_permission_denied_alert_message" = "Që të aktivizoni kontakte, kaloni te rregullimet e pajisjes tua."; +"contacts_address_book_permission_denied_alert_title" = "Kontaktet u çaktivizuan"; diff --git a/Riot/Assets/sv.lproj/InfoPlist.strings b/Riot/Assets/sv.lproj/InfoPlist.strings index d00b2d989..45ccee26b 100644 --- a/Riot/Assets/sv.lproj/InfoPlist.strings +++ b/Riot/Assets/sv.lproj/InfoPlist.strings @@ -3,5 +3,5 @@ // Permissions usage explanations "NSCameraUsageDescription" = "Kameran används för att ta bilder och videor, och ringa videosamtal."; "NSMicrophoneUsageDescription" = "Element behöver åtkomst till din mikrofon för att kunna ringa och ta emot samtal samt spela in video och röstmeddelanden."; -"NSContactsUsageDescription" = "För att upptäcka kontakter som redan använder Matrix kan Element skicka e-postadresser och telefonnummer i din adressbok till din valda Matrix-identitetsserver. Där det stöds hashas personuppgifter innan de skickas - kontrollera din identitetsservers integritetspolicy för mer information."; +"NSContactsUsageDescription" = "Element kommer att visa dina kontakter så du kan bjuda in dem att chatta."; "NSFaceIDUsageDescription" = "Face ID används för att komma åt appen."; 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/Assets/uk.lproj/InfoPlist.strings b/Riot/Assets/uk.lproj/InfoPlist.strings index 6973b0ae6..1923cbcbc 100644 --- a/Riot/Assets/uk.lproj/InfoPlist.strings +++ b/Riot/Assets/uk.lproj/InfoPlist.strings @@ -2,6 +2,6 @@ "NSCameraUsageDescription" = "Камера використовується для знімків фото і відео, а також для відео-викликів."; "NSPhotoLibraryUsageDescription" = "Фотографії використовуються для надсилання фото і відео."; "NSMicrophoneUsageDescription" = "Element потребує доступу до вашого мікрофона, щоб здійснювати та отримувати виклики, знімати відео та записувати голосові повідомлення."; -"NSContactsUsageDescription" = "Щоб показати, які з ваших контактів вже використовують Matrix, Element може надіслати адреси електронної пошти і номери телефонів з вашої адресної книги до вашого ідентифікаційного сервера Matrix. При наявності підтримки, перед надсиланням створюється хеш особистих даних. Для докладних відомостей ознайомтеся з політикою приватності свого ідентифікаційного сервера."; +"NSContactsUsageDescription" = "Element покаже ваші контакти, щоб ви могли запросити їх до бесіди."; "NSCalendarsUsageDescription" = "Переглядайте свої заплановані зустрічі в додатку."; "NSFaceIDUsageDescription" = "Face ID використовується для доступу до вашого додатку."; diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index d91f1d708..a573557ce 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -139,7 +139,7 @@ "client_web_name" = "Element Web"; "client_ios_name" = "Element iOS"; "auth_login_single_sign_on" = "Увійти"; -"room_creation_invite_another_user" = "Пошук / запрошення за ID користувача, іменем або е-поштою"; +"room_creation_invite_another_user" = "ID користувача, ім'я або е-пошта"; "room_creation_error_invite_user_by_email_without_identity_server" = "Сервер ідентифікації не налаштовано, тому ви не можете додати учасника з е-поштою."; // Room recents "room_recents_directory_section" = "КАТАЛОГ КІМНАТ"; @@ -381,7 +381,7 @@ "secrets_recovery_with_passphrase_lost_passphrase_action_part3" = "."; "key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "."; "settings_discovery_three_pids_management_information_part3" = "."; -"settings_contacts" = "ЛОКАЛЬНІ КОНТАКТИ"; +"settings_contacts" = "КОНТАКТИ ПРИСТРОЮ"; "settings_ignored_users" = "НЕХТУВАНІ КОРИСТУВАЧІ"; "settings_user_interface" = "КОРИСТУВАЦЬКИЙ ІНТЕРФЕЙС"; "settings_integrations" = "ІНТЕГРАЦІЇ"; diff --git a/Riot/Assets/zh_Hans.lproj/InfoPlist.strings b/Riot/Assets/zh_Hans.lproj/InfoPlist.strings index b4daf9c55..8133fc8ee 100644 --- a/Riot/Assets/zh_Hans.lproj/InfoPlist.strings +++ b/Riot/Assets/zh_Hans.lproj/InfoPlist.strings @@ -2,6 +2,6 @@ "NSCameraUsageDescription" = "摄像头权限用于拍摄照片、录制视频或进行视频聊天。"; "NSPhotoLibraryUsageDescription" = "照片库访问权限用于发送图片与视频。"; "NSMicrophoneUsageDescription" = "Element 需要访问您的麦克风才能拨打和接听电话、拍摄视频和录制语音消息。"; -"NSContactsUsageDescription" = "为了发现已在使用 Matrix 的联系人,Element 可以把你地址簿里的邮箱地址和电话号码发送到你所选择的 Matrix 身份认证服务器。如果支持的话,个人数据在发送前会被哈希处理——请检查你的身份认证服务器的隐私政策以获取详细信息。"; +"NSContactsUsageDescription" = "Element 将显示您的联系人,以便您可以邀请他们聊天。"; "NSCalendarsUsageDescription" = "在此应用中查看你计划的会议。"; "NSFaceIDUsageDescription" = "Face ID 权限用于访问您的应用。"; diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index c36ccb9ec..12ee6b01a 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -61,7 +61,7 @@ "auth_missing_phone" = "缺少电话号码"; "auth_missing_email_or_phone" = "缺少电子邮箱地址或电话号码"; "auth_password_dont_match" = "密码不匹配"; -"auth_username_in_use" = "用户名已被占用"; +"auth_username_in_use" = "用户名被占用"; "auth_forgot_password" = "忘记密码?"; "auth_use_server_options" = "使用自定义服务器选项(高级)"; "auth_email_validation_message" = "请检查我们发给您的电子邮件以继续注册"; @@ -93,7 +93,7 @@ "room_creation_keep_private" = "保持私密"; "room_creation_make_private" = "使其变成私密"; "room_creation_wait_for_creation" = "聊天室已创建。请稍候。"; -"room_creation_invite_another_user" = "通过用户 ID、名称或电子邮件进行搜索/邀请"; +"room_creation_invite_another_user" = "用户 ID、名称或电子邮件"; // Room recents "room_recents_directory_section" = "聊天室目录"; "room_recents_favourites_section" = "收藏夹"; @@ -245,7 +245,7 @@ "settings_user_settings" = "用户设置"; "settings_notifications_settings" = "通知设置"; "settings_ignored_users" = "已忽略用户"; -"settings_contacts" = "本地联系人"; +"settings_contacts" = "设备联系人"; "settings_advanced" = "高级"; "settings_other" = "其他"; "settings_labs" = "实验室"; @@ -1521,3 +1521,27 @@ "room_recents_suggested_rooms_section" = "建议的聊天室"; "done" = "完成"; "open" = "打开"; +"service_terms_modal_information_description_integration_manager" = "集成管理器允许您添加来自第三方的功能。"; +"service_terms_modal_information_description_identity_server" = "身份服务器通过查找电话号码或电子邮件地址帮助您找到联系人,看看他们是否已经有一个帐户。"; +"service_terms_modal_information_title_integration_manager" = "集成管理器"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "身份服务器"; +"service_terms_modal_description_integration_manager" = "这将允许您使用机器人、桥、小部件和贴纸包。"; +"service_terms_modal_description_identity_server" = "如果有人在电话联系人中保存了你的电话号码或电子邮件,就可以找到你。"; +"service_terms_modal_table_header_integration_manager" = "集成管理器条款"; +"service_terms_modal_table_header_identity_server" = "身份服务器条款"; +"service_terms_modal_footer" = "你可以随时在设置中禁用它。"; + +// Service terms +"service_terms_modal_title_message" = "如需继续,请接受以下条款和条件"; +"settings_contacts_enable_sync_description" = "这将使用您的身份服务器连接您和您的联系人,并帮助他们找到您。"; +"settings_contacts_enable_sync" = "寻找联系人"; +"settings_phone_contacts" = "电话联系人"; +"find_your_contacts_identity_service_error" = "无法连接到身份服务器。"; +"find_your_contacts_footer" = "你可以随时从设置中禁用它。"; +"find_your_contacts_button_title" = "寻找联系人"; +"find_your_contacts_message" = "让 %@ 显示你的联系人,以便你可以快速开始和你最了解的人聊天。"; +"find_your_contacts_title" = "从列出你的联系人开始"; +"contacts_address_book_permission_denied_alert_message" = "要启用联系人,请转到设备设置。"; +"contacts_address_book_permission_denied_alert_title" = "联系人被禁用"; 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/MXRoom+Riot.h b/Riot/Categories/MXRoom+Riot.h index dcf4860a3..42a6a6c01 100644 --- a/Riot/Categories/MXRoom+Riot.h +++ b/Riot/Categories/MXRoom+Riot.h @@ -19,13 +19,6 @@ #import "UserEncryptionTrustLevel.h" -typedef NS_ENUM(NSUInteger, RoomSentStatus) -{ - RoomSentStatusOk, - RoomSentStatusSentFailed, - RoomSentStatusSentFailedDueToUnknownDevices -}; - /** Define a `MXRoom` category at Riot level. */ @@ -51,9 +44,6 @@ typedef NS_ENUM(NSUInteger, RoomSentStatus) */ @property (nonatomic) id notificationCenterDidUpdateObserver; -/// Check if all messages have been sent. -@property (nonatomic, readonly) RoomSentStatus sentStatus; - /** Update the room tag. diff --git a/Riot/Categories/MXRoom+Riot.m b/Riot/Categories/MXRoom+Riot.m index 273f86a82..5bef645be 100644 --- a/Riot/Categories/MXRoom+Riot.m +++ b/Riot/Categories/MXRoom+Riot.m @@ -656,30 +656,4 @@ return objc_getAssociatedObject(self, @selector(notificationCenterDidUpdateObserver)); } -#pragma mark - Unread messages - -- (RoomSentStatus)sentStatus -{ - RoomSentStatus status = RoomSentStatusOk; - NSArray *outgoingMsgs = self.outgoingMessages; - - for (MXEvent *event in outgoingMsgs) - { - if (event.sentState == MXEventSentStateFailed) - { - status = RoomSentStatusSentFailed; - - // Check if the error is due to unknown devices - if ([event.sentError.domain isEqualToString:MXEncryptingErrorDomain] - && event.sentError.code == MXEncryptingErrorUnknownDeviceCode) - { - status = RoomSentStatusSentFailedDueToUnknownDevices; - break; - } - } - } - - return status; -} - @end 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/NSArray+Element.h b/Riot/Categories/NSArray+Element.h new file mode 100644 index 000000000..a71f8f714 --- /dev/null +++ b/Riot/Categories/NSArray+Element.h @@ -0,0 +1,43 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSArray (Element) + +/// Returns an array containing the results of mapping the given closure over the array's elements. +/// @param transform A mapping closure. `transform` accepts an element of this array as its parameter +/// and returns a transformed value of the same or of a different type. +/// @return An array containing the transformed elements of this array. +- (NSArray *)vc_map:(id (^)(id obj))transform; + +/// Returns an array containing the non-nil results of mapping the given closure over the array's elements. +/// @param transform A mapping closure. `transform` accepts an element of this array as its parameter +/// and returns a nullable transformed value of the same or of a different type. +/// @return An array of the non-nil results of calling `transform` with each element of the array. +- (NSArray *)vc_compactMap:(id _Nullable (^)(id obj))transform; + +/// Returns an array containing the concatenated results of mapping the given closure over the array's elements. +/// @param transform A mapping closure. `transform` accepts an element of this array as its parameter +/// and returns an array.. +/// @return The resulting flattened array. +- (NSArray *)vc_flatMap:(NSArray* (^)(id obj))transform; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Riot/Categories/NSArray+Element.m b/Riot/Categories/NSArray+Element.m new file mode 100644 index 000000000..c5b37d01d --- /dev/null +++ b/Riot/Categories/NSArray+Element.m @@ -0,0 +1,55 @@ +// +// 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 "NSArray+Element.h" + +@implementation NSArray (Element) + +- (NSArray *)vc_map:(id (^)(id obj))transform +{ + NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; + [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) + { + [result addObject:transform(obj)]; + }]; + return result; +} + +- (NSArray *)vc_compactMap:(id _Nullable (^)(id obj))transform +{ + NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; + [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) + { + id mappedObject = transform(obj); + if (mappedObject) + { + [result addObject:mappedObject]; + } + }]; + return result; +} + +- (NSArray *)vc_flatMap:(NSArray* (^)(id obj))transform +{ + NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; + [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) + { + [result addObjectsFromArray:transform(obj)]; + }]; + return result; +} + +@end diff --git a/Riot/Categories/UIViewController.swift b/Riot/Categories/UIViewController.swift index 8245d9a26..e1c16760e 100644 --- a/Riot/Categories/UIViewController.swift +++ b/Riot/Categories/UIViewController.swift @@ -103,4 +103,16 @@ extension UIViewController { return fabImageView } + + /// Set leftBarButtonItem with split view display mode button if there is no leftBarButtonItem defined and splitViewController exists. + /// To be Used when view controller is displayed as detail controller in split view. + func vc_setupDisplayModeLeftBarButtonItemIfNeeded() { + guard let splitViewController = self.splitViewController, self.navigationItem.leftBarButtonItem == nil else { + return + } + + // If there is no leftBarButtonItem defined, + // set split view display mode button as left bar button item + self.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem + } } diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 2af5367f5..cd4a5b92e 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") @@ -192,6 +195,7 @@ internal enum Asset { internal static let spaceMenuLeave = ImageAsset(name: "space_menu_leave") internal static let spaceMenuMembers = ImageAsset(name: "space_menu_members") internal static let spaceMenuRooms = ImageAsset(name: "space_menu_rooms") + internal static let spacePrivateIcon = ImageAsset(name: "space_private_icon") internal static let spaceRoomIcon = ImageAsset(name: "space_room_icon") internal static let spaceTypeIcon = ImageAsset(name: "space_type_icon") internal static let spaceUserIcon = ImageAsset(name: "space_user_icon") @@ -207,6 +211,8 @@ internal enum Asset { internal static let cancel = ImageAsset(name: "cancel") internal static let e2eVerified = ImageAsset(name: "e2e_verified") internal static let horizontalLogo = ImageAsset(name: "horizontal_logo") + internal static let radioButtonDefault = ImageAsset(name: "radio-button-default") + internal static let radioButtonSelected = ImageAsset(name: "radio-button-selected") } } // swiftlint:enable identifier_name line_length nesting type_body_length type_name @@ -222,6 +228,7 @@ internal struct ImageAsset { internal typealias Image = UIImage #endif + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *) internal var image: Image { let bundle = BundleToken.bundle #if os(iOS) || os(tvOS) @@ -233,13 +240,25 @@ internal struct ImageAsset { let image = Image(named: name) #endif guard let result = image else { - fatalError("Unable to load image named \(name).") + fatalError("Unable to load image asset named \(name).") } return result } + + #if os(iOS) || os(tvOS) + @available(iOS 8.0, tvOS 9.0, *) + internal func image(compatibleWith traitCollection: UITraitCollection) -> Image { + let bundle = BundleToken.bundle + guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else { + fatalError("Unable to load image asset named \(name).") + } + return result + } + #endif } internal extension ImageAsset.Image { + @available(iOS 8.0, tvOS 9.0, watchOS 2.0, *) @available(macOS, deprecated, message: "This initializer is unsafe on macOS, please use the ImageAsset.image property") convenience init!(asset: ImageAsset) { diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index d82e45c6f..54ca11b35 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") } @@ -2859,6 +2887,10 @@ public class VectorL10n: NSObject { public static var roomEventActionEdit: String { return VectorL10n.tr("Vector", "room_event_action_edit") } + /// Forward + public static var roomEventActionForward: String { + return VectorL10n.tr("Vector", "room_event_action_forward") + } /// Reason for kicking this user public static var roomEventActionKickPromptReason: String { return VectorL10n.tr("Vector", "room_event_action_kick_prompt_reason") @@ -4011,37 +4043,53 @@ 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") + } + /// ABOUT + public static var settingsAbout: String { + return VectorL10n.tr("Vector", "settings_about") } /// Invalid credentials public static var settingsAdd3pidInvalidPasswordMessage: String { @@ -4103,10 +4151,6 @@ public class VectorL10n: NSObject { public static func settingsConfigHomeServer(_ p1: String) -> String { return VectorL10n.tr("Vector", "settings_config_home_server", p1) } - /// Identity server is %@ - public static func settingsConfigIdentityServer(_ p1: String) -> String { - return VectorL10n.tr("Vector", "settings_config_identity_server", p1) - } /// No build info public static var settingsConfigNoBuildInfo: String { return VectorL10n.tr("Vector", "settings_config_no_build_info") @@ -4127,13 +4171,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 { @@ -4195,6 +4243,10 @@ public class VectorL10n: NSObject { public static var settingsDirectMessages: String { return VectorL10n.tr("Vector", "settings_direct_messages") } + /// Accept Identity Server Terms + public static var settingsDiscoveryAcceptTerms: String { + return VectorL10n.tr("Vector", "settings_discovery_accept_terms") + } /// An error occured. Please retry. public static var settingsDiscoveryErrorMessage: String { return VectorL10n.tr("Vector", "settings_discovery_error_message") @@ -4543,6 +4595,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") @@ -4855,6 +4911,10 @@ public class VectorL10n: NSObject { public static var spaceFeatureUnavailableTitle: String { return VectorL10n.tr("Vector", "space_feature_unavailable_title") } + /// Show all rooms + public static var spaceHomeShowAllRooms: String { + return VectorL10n.tr("Vector", "space_home_show_all_rooms") + } /// Ban from this space public static var spaceParticipantsActionBan: String { return VectorL10n.tr("Vector", "space_participants_action_ban") diff --git a/Riot/Managers/AppInfo/BuildInfo.m b/Riot/Managers/AppInfo/BuildInfo.m index 0853efe94..a1dbffe14 100644 --- a/Riot/Managers/AppInfo/BuildInfo.m +++ b/Riot/Managers/AppInfo/BuildInfo.m @@ -16,7 +16,11 @@ #import "BuildInfo.h" +#ifdef IS_SHARE_EXTENSION +#import "RiotShareExtension-Swift.h" +#else #import "Riot-Swift.h" +#endif #define MAKE_STRING(x) #x #define MAKE_NS_STRING(x) @MAKE_STRING(x) diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 1872db213..9e1ccd05b 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -22,10 +22,13 @@ final class RiotSettings: NSObject { // MARK: - Constants - private enum UserDefaultsKeys { + public enum UserDefaultsKeys { static let enableCrashReport = "enableCrashReport" static let notificationsShowDecryptedContent = "showDecryptedContent" static let allowStunServerFallback = "allowStunServerFallback" + static let pinRoomsWithMissedNotificationsOnHome = "pinRoomsWithMissedNotif" + static let pinRoomsWithUnreadMessagesOnHome = "pinRoomsWithUnread" + static let showAllRoomsInHomeSpace = "showAllRoomsInHomeSpace" } static let shared = RiotSettings() @@ -79,11 +82,11 @@ final class RiotSettings: NSObject { var showDecryptedContentInNotifications /// Indicate if rooms with missed notifications should be displayed first on home screen. - @UserDefault(key: "pinRoomsWithMissedNotif", defaultValue: false, storage: defaults) + @UserDefault(key: UserDefaultsKeys.pinRoomsWithMissedNotificationsOnHome, defaultValue: false, storage: defaults) var pinRoomsWithMissedNotificationsOnHome /// Indicate if rooms with unread messages should be displayed first on home screen. - @UserDefault(key: "pinRoomsWithUnread", defaultValue: false, storage: defaults) + @UserDefault(key: UserDefaultsKeys.pinRoomsWithUnreadMessagesOnHome, defaultValue: false, storage: defaults) var pinRoomsWithUnreadMessagesOnHome /// Indicate to show Not Safe For Work public rooms. @@ -140,6 +143,9 @@ final class RiotSettings: NSObject { @UserDefault(key: "roomsAllowToJoinPublicRooms", defaultValue: BuildSettings.roomsAllowToJoinPublicRooms, storage: defaults) var roomsAllowToJoinPublicRooms + @UserDefault(key: UserDefaultsKeys.showAllRoomsInHomeSpace, defaultValue: false, storage: defaults) + var showAllRoomsInHomeSpace + // MARK: - Room Screen @UserDefault(key: "roomScreenAllowVoIPForDirectRoom", defaultValue: BuildSettings.roomScreenAllowVoIPForDirectRoom, storage: defaults) @@ -227,9 +233,6 @@ final class RiotSettings: NSObject { @UserDefault(key: "settingsScreenShowChangePassword", defaultValue: BuildSettings.settingsScreenShowChangePassword, storage: defaults) var settingsScreenShowChangePassword - @UserDefault(key: "settingsScreenShowInviteFriends", defaultValue: BuildSettings.settingsScreenShowInviteFriends, storage: defaults) - var settingsScreenShowInviteFriends - @UserDefault(key: "settingsScreenShowEnableStunServerFallback", defaultValue: BuildSettings.settingsScreenShowEnableStunServerFallback, storage: defaults) var settingsScreenShowEnableStunServerFallback 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.h b/Riot/Modules/Application/LegacyAppDelegate.h index ddf33c28b..6a3e2c3be 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.h +++ b/Riot/Modules/Application/LegacyAppDelegate.h @@ -31,6 +31,9 @@ @protocol LegacyAppDelegateDelegate; @class CallBar; @class CallPresenter; +@class RoomNavigationParameters; +@class RoomPreviewNavigationParameters; +@class UniversalLinkParameters; #pragma mark - Notifications /** @@ -204,15 +207,24 @@ UINavigationControllerDelegate #pragma mark - Matrix Room handling // Show a room and jump to the given event if event id is not nil otherwise go to last messages. -- (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession restoreInitialDisplay:(BOOL)restoreInitialDisplay completion:(void (^)(void))completion; +- (void)showRoomWithParameters:(RoomNavigationParameters*)parameters completion:(void (^)(void))completion; -- (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession restoreInitialDisplay:(BOOL)restoreInitialDisplay; +- (void)showRoomWithParameters:(RoomNavigationParameters*)parameters; +// Restore display and show the room - (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession; // Creates a new direct chat with the provided user id - (void)createDirectChatWithUserId:(NSString*)userId completion:(void (^)(void))completion; +// Show room preview +- (void)showRoomPreviewWithParameters:(RoomPreviewNavigationParameters*)parameters completion:(void (^)(void))completion; + +- (void)showRoomPreviewWithParameters:(RoomPreviewNavigationParameters*)parameters; + +// Restore display and show the room preview +- (void)showRoomPreview:(RoomPreviewData*)roomPreviewData; + // Reopen an existing direct room with this userId or creates a new one (if it doesn't exist) - (void)startDirectChatWithUserId:(NSString*)userId completion:(void (^)(void))completion; @@ -241,6 +253,14 @@ UINavigationControllerDelegate */ - (BOOL)handleUniversalLinkURL:(NSURL*)universalLinkURL; +/** + Process universal link. + + @param parameters the universal link parameters. + @return YES in case of processing success. + */ +- (BOOL)handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters; + /** Extract params from the URL fragment part (after '#') of a vector.im Universal link: diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index d0b5217ff..6b9816643 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) { @@ -1240,8 +1236,24 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni return [self handleUniversalLinkFragment:fragment fromURL:nil]; } + - (BOOL)handleUniversalLinkFragment:(NSString*)fragment fromURL:(NSURL*)universalLinkURL + { + ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:YES stackAboveVisibleViews:NO]; + + UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithFragment:fragment universalLinkURL:universalLinkURL presentationParameters:presentationParameters]; + + return [self handleUniversalLinkWithParameters:parameters]; +} + +- (BOOL)handleUniversalLinkWithParameters:(UniversalLinkParameters*)universalLinkParameters +{ + NSString *fragment = universalLinkParameters.fragment; + NSURL *universalLinkURL = universalLinkParameters.universalLinkURL; + ScreenPresentationParameters *screenPresentationParameters = universalLinkParameters.presentationParameters; + BOOL restoreInitialDisplay = screenPresentationParameters.restoreInitialDisplay; + BOOL continueUserActivity = NO; MXKAccountManager *accountManager = [MXKAccountManager sharedManager]; @@ -1342,26 +1354,23 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if (room.summary.roomType == MXRoomTypeSpace) { - [self restoreInitialDisplay:^{ - self.spaceDetailPresenter = [SpaceDetailPresenter new]; - self.spaceDetailPresenter.delegate = self; - [self.spaceDetailPresenter presentForSpaceWithId:room.roomId from:self.masterNavigationController sourceView:nil session:account.mxSession animated:YES]; - }]; + SpaceNavigationParameters *spaceNavigationParameters = [[SpaceNavigationParameters alloc] initWithRoomId:room.roomId mxSession:account.mxSession presentationParameters:screenPresentationParameters]; + + [self showSpaceWithParameters:spaceNavigationParameters]; } else { // Open the room page - [self showRoom:roomId andEventId:eventId withMatrixSession:account.mxSession]; + RoomNavigationParameters *roomNavigationParameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId eventId:eventId mxSession:account.mxSession presentationParameters: screenPresentationParameters]; + + [self showRoomWithParameters:roomNavigationParameters]; } continueUserActivity = YES; } else { - // We will display something but we need to do some requests before. - // So, come back to the home VC and show its loading wheel while processing - [self restoreInitialDisplay:^{ - + void(^findRoom)(void) = ^{ if ([_masterTabBarController.selectedViewController isKindOfClass:MXKActivityHandlingViewController.class]) { MXKActivityHandlingViewController *homeViewController = (MXKActivityHandlingViewController*)_masterTabBarController.selectedViewController; @@ -1402,7 +1411,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { universalLinkFragmentPendingRoomAlias = @{roomId: roomIdOrAlias}; - [self handleUniversalLinkFragment:newUniversalLinkFragment fromURL:universalLinkURL]; + UniversalLinkParameters *newParameters = [[UniversalLinkParameters alloc] initWithFragment:newUniversalLinkFragment universalLinkURL:universalLinkURL presentationParameters:screenPresentationParameters]; + + [self handleUniversalLinkWithParameters:newParameters]; } else { @@ -1440,7 +1451,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if (notif.object == account.mxSession && account.mxSession.state == MXSessionStateRunning) { MXLogDebug(@"[AppDelegate] Universal link: The session is running. Retry the link"); - [self handleUniversalLinkFragment:fragment fromURL:universalLinkURL]; + [self handleUniversalLinkWithParameters:universalLinkParameters]; } } }]; @@ -1459,26 +1470,44 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni roomPreviewData.viaServers = queryParams[@"via"]; } + RoomPreviewNavigationParameters *roomPreviewNavigationParameters = [[RoomPreviewNavigationParameters alloc] initWithPreviewData:roomPreviewData presentationParameters:screenPresentationParameters]; + [account.mxSession.matrixRestClient roomSummaryWith:roomIdOrAlias via:roomPreviewData.viaServers success:^(MXPublicRoom *room) { if ([room.roomTypeString isEqualToString:MXRoomTypeStringSpace]) { [homeViewController stopActivityIndicator]; - self.spaceDetailPresenter = [SpaceDetailPresenter new]; - self.spaceDetailPresenter.delegate = self; - [self.spaceDetailPresenter presentForSpaceWithPublicRoom:room from:self.masterNavigationController sourceView:nil session:account.mxSession animated:YES]; + SpacePreviewNavigationParameters *spacePreviewNavigationParameters = [[SpacePreviewNavigationParameters alloc] initWithPublicRoom:room mxSession:account.mxSession presentationParameters:screenPresentationParameters]; + + [self showSpacePreviewWithParameters:spacePreviewNavigationParameters]; } else { - [self peekInRoomWithId:roomIdOrAlias forPreviewData:roomPreviewData params:pathParams]; + [self peekInRoomWithNavigationParameters:roomPreviewNavigationParameters pathParams:pathParams]; } } failure:^(NSError *error) { - [self peekInRoomWithId:roomIdOrAlias forPreviewData:roomPreviewData params:pathParams]; + [self peekInRoomWithNavigationParameters:roomPreviewNavigationParameters pathParams:pathParams]; }]; } } - }]; + }; + + + // We will display something but we need to do some requests before. + // So, come back to the home VC and show its loading wheel while processing + + if (restoreInitialDisplay) + { + [self restoreInitialDisplay:^{ + findRoom(); + }]; + } + else + { + findRoom(); + } + // Let's say we are handling the case continueUserActivity = YES; @@ -1498,7 +1527,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if ([universalLinkFragmentPending isEqualToString:fragment]) { MXLogDebug(@"[AppDelegate] Universal link: The user is now logged in. Retry the link"); - [self handleUniversalLinkFragment:fragment fromURL:universalLinkURL]; + [self handleUniversalLinkWithParameters:universalLinkParameters]; } }]; } @@ -1526,7 +1555,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Create the contact related to this member MXKContact *contact = [[MXKContact alloc] initMatrixContactWithDisplayName:displayName andMatrixID:userId]; - [self showContact:contact]; + [self showContact:contact presentationParameters:screenPresentationParameters]; continueUserActivity = YES; } @@ -1545,7 +1574,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } // Display the group details - [self showGroup:group withMatrixSession:account.mxSession]; + [self showGroup:group withMatrixSession:account.mxSession presentationParamters:screenPresentationParameters]; continueUserActivity = YES; } @@ -1563,7 +1592,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if ([universalLinkFragmentPending isEqualToString:fragment]) { MXLogDebug(@"[AppDelegate] Universal link: The user is now logged in. Retry the link"); - [self handleUniversalLinkFragment:fragment fromURL:universalLinkURL]; + [self handleUniversalLinkWithParameters:universalLinkParameters]; } }]; } @@ -1581,7 +1610,10 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Unknown command: Do nothing except coming back to the main screen MXLogDebug(@"[AppDelegate] Universal link: TODO: Do not know what to do with the link arguments: %@", pathParams); - [self popToHomeViewControllerAnimated:NO completion:nil]; + if (restoreInitialDisplay) + { + [self popToHomeViewControllerAnimated:NO completion:nil]; + } } return continueUserActivity; @@ -1605,8 +1637,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } -- (void)peekInRoomWithId:(NSString*)roomIdOrAlias forPreviewData:(RoomPreviewData *)roomPreviewData params:(NSArray *)pathParams +- (void)peekInRoomWithNavigationParameters:(RoomPreviewNavigationParameters*)presentationParameters pathParams:(NSArray *)pathParams { + RoomPreviewData *roomPreviewData = presentationParameters.previewData; + NSString *roomIdOrAlias = presentationParameters.roomId; + // Is it a link to an event of a room? // If yes, the event will be displayed once the room is joined roomPreviewData.eventId = (pathParams.count >= 3) ? pathParams[2] : nil; @@ -1628,7 +1663,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } self->universalLinkFragmentPendingRoomAlias = nil; - [self showRoomPreview:roomPreviewData]; + [self showRoomPreviewWithParameters:presentationParameters]; }]; } @@ -2786,8 +2821,17 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } -- (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession restoreInitialDisplay:(BOOL)restoreInitialDisplay completion:(void (^)(void))completion +- (void)showRoomWithParameters:(RoomNavigationParameters*)parameters { + [self showRoomWithParameters:parameters completion:nil]; +} + +- (void)showRoomWithParameters:(RoomNavigationParameters*)parameters completion:(void (^)(void))completion +{ + NSString *roomId = parameters.roomId; + MXSession *mxSession = parameters.mxSession; + BOOL restoreInitialDisplay = parameters.presentationParameters.restoreInitialDisplay; + if (roomId && mxSession) { MXRoom *room = [mxSession roomWithRoomId:roomId]; @@ -2809,8 +2853,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni void (^selectRoom)(void) = ^() { // Select room to display its details (dispatch this action in order to let TabBarController end its refresh) - [self.masterTabBarController selectRoomWithId:roomId andEventId:eventId inMatrixSession:mxSession completion:^{ - + + [self.masterTabBarController selectRoomWithParameters:parameters completion:^{ // Remove delivered notifications for this room [self.pushNotificationService removeDeliveredNotificationsWithRoomId:roomId completion:nil]; @@ -2833,23 +2877,124 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } -- (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession restoreInitialDisplay:(BOOL)restoreInitialDisplay -{ - [self showRoom:roomId andEventId:eventId withMatrixSession:mxSession restoreInitialDisplay:restoreInitialDisplay completion:nil]; -} - - (void)showRoom:(NSString*)roomId andEventId:(NSString*)eventId withMatrixSession:(MXSession*)mxSession { - [self showRoom:roomId andEventId:eventId withMatrixSession:mxSession restoreInitialDisplay:YES completion:nil]; + // Ask to restore initial display + ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:YES]; + + RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId + eventId:eventId mxSession:mxSession presentationParameters:presentationParameters]; + + [self showRoomWithParameters:parameters]; +} + +- (void)showRoomPreviewWithParameters:(RoomPreviewNavigationParameters*)parameters completion:(void (^)(void))completion +{ + void (^showRoomPreview)(void) = ^() { + [self.masterTabBarController selectRoomPreviewWithParameters:parameters completion:completion]; + }; + + if (parameters.presentationParameters.restoreInitialDisplay) + { + [self restoreInitialDisplay:^{ + showRoomPreview(); + }]; + } + else + { + showRoomPreview(); + } +} + +- (void)showRoomPreviewWithParameters:(RoomPreviewNavigationParameters*)parameters +{ + [self showRoomPreviewWithParameters:parameters completion:nil]; } - (void)showRoomPreview:(RoomPreviewData*)roomPreviewData { - [self restoreInitialDisplay:^{ - - [_masterTabBarController showRoomPreview:roomPreviewData]; - - }]; + // Ask to restore initial display + ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:YES]; + + RoomPreviewNavigationParameters *parameters = [[RoomPreviewNavigationParameters alloc] initWithPreviewData:roomPreviewData presentationParameters:presentationParameters]; + + [self showRoomPreviewWithParameters:parameters]; +} + +- (void)showSpacePreviewWithParameters:(SpacePreviewNavigationParameters*)parameters +{ + UIViewController *presentingViewController; + UIView *sourceView; + + if (parameters.presentationParameters.presentingViewController) + { + presentingViewController = parameters.presentationParameters.presentingViewController; + sourceView = parameters.presentationParameters.sourceView; + } + else + { + presentingViewController = self.masterNavigationController; + } + + self.spaceDetailPresenter = [SpaceDetailPresenter new]; + self.spaceDetailPresenter.delegate = self; + + void(^showSpace)(void) = ^{ + [self.spaceDetailPresenter presentForSpaceWithPublicRoom:parameters.publicRoom + from:presentingViewController + sourceView:sourceView + session:parameters.mxSession + animated:YES]; + }; + + if (parameters.presentationParameters.restoreInitialDisplay) + { + [self restoreInitialDisplay:^{ + showSpace(); + }]; + } + else + { + showSpace(); + } +} + +- (void)showSpaceWithParameters:(SpaceNavigationParameters*)parameters +{ + UIViewController *presentingViewController; + UIView *sourceView; + + if (parameters.presentationParameters.presentingViewController) + { + presentingViewController = parameters.presentationParameters.presentingViewController; + sourceView = parameters.presentationParameters.sourceView; + } + else + { + presentingViewController = self.masterNavigationController; + } + + self.spaceDetailPresenter = [SpaceDetailPresenter new]; + self.spaceDetailPresenter.delegate = self; + + void(^showSpace)(void) = ^{ + [self.spaceDetailPresenter presentForSpaceWithId:parameters.roomId + from:presentingViewController + sourceView:sourceView + session:parameters.mxSession + animated:YES]; + }; + + if (parameters.presentationParameters.restoreInitialDisplay) + { + [self restoreInitialDisplay:^{ + showSpace(); + }]; + } + else + { + showSpace(); + } } - (void)setVisibleRoomId:(NSString *)roomId @@ -2964,25 +3109,43 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni #pragma mark - Contacts handling -- (void)showContact:(MXKContact*)contact +- (void)showContact:(MXKContact*)contact presentationParameters:(ScreenPresentationParameters*)presentationParameters { - [self restoreInitialDisplay:^{ - - [self.masterTabBarController selectContact:contact]; - - }]; + void(^showContact)(void) = ^{ + [self.masterTabBarController selectContact:contact withPresentationParameters:presentationParameters]; + }; + + if (presentationParameters.restoreInitialDisplay) + { + [self restoreInitialDisplay:^{ + showContact(); + }]; + } + else + { + showContact(); + } } #pragma mark - Matrix Groups handling -- (void)showGroup:(MXGroup*)group withMatrixSession:(MXSession*)mxSession +- (void)showGroup:(MXGroup*)group withMatrixSession:(MXSession*)mxSession presentationParamters:(ScreenPresentationParameters*)presentationParameters { - [self restoreInitialDisplay:^{ - + void(^showGroup)(void) = ^{ // Select group to display its details (dispatch this action in order to let TabBarController end its refresh) - [_masterTabBarController selectGroup:group inMatrixSession:mxSession]; - - }]; + [self.masterTabBarController selectGroup:group inMatrixSession:mxSession presentationParameters:presentationParameters]; + }; + + if (presentationParameters.restoreInitialDisplay) + { + [self restoreInitialDisplay:^{ + showGroup(); + }]; + } + else + { + showGroup(); + } } - (void)promptForStunServerFallback @@ -4131,82 +4294,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/Application/ScreenNavigation/RoomNavigationParameters.swift b/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift new file mode 100644 index 000000000..7308a5701 --- /dev/null +++ b/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift @@ -0,0 +1,50 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Navigation parameters to display a room with a provided identifier in a specific matrix session. +@objcMembers +class RoomNavigationParameters: NSObject { + + // MARK: - Properties + + /// The room identifier + let roomId: String + + /// If not nil, the room will be opened on this event. + let eventId: String? + + /// The Matrix session in which the room should be available. + let mxSession: MXSession + + /// Screen presentation parameters. + let presentationParameters: ScreenPresentationParameters + + // MARK: - Setup + + init(roomId: String, + eventId: String?, + mxSession: MXSession, + presentationParameters: ScreenPresentationParameters) { + self.roomId = roomId + self.eventId = eventId + self.mxSession = mxSession + self.presentationParameters = presentationParameters + + super.init() + } +} diff --git a/Riot/Modules/Application/ScreenNavigation/RoomPreviewNavigationParameters.swift b/Riot/Modules/Application/ScreenNavigation/RoomPreviewNavigationParameters.swift new file mode 100644 index 000000000..e59d2687f --- /dev/null +++ b/Riot/Modules/Application/ScreenNavigation/RoomPreviewNavigationParameters.swift @@ -0,0 +1,39 @@ +// +// 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 + +/// Navigation parameters to display a preview of a room that is unknown for the user. +/// This room can come from an email invitation link or a simple link to a room. +@objcMembers +class RoomPreviewNavigationParameters: RoomNavigationParameters { + + // MARK: - Properties + + /// The data for the room preview + let previewData: RoomPreviewData + + // MARK: - Setup + + init(previewData: RoomPreviewData, presentationParameters: ScreenPresentationParameters) { + self.previewData = previewData + + super.init(roomId: previewData.roomId, + eventId: previewData.eventId, + mxSession: previewData.mxSession, + presentationParameters: presentationParameters) + } +} diff --git a/Riot/Modules/Application/ScreenNavigation/ScreenPresentationParameters.swift b/Riot/Modules/Application/ScreenNavigation/ScreenPresentationParameters.swift new file mode 100644 index 000000000..6b7666eb7 --- /dev/null +++ b/Riot/Modules/Application/ScreenNavigation/ScreenPresentationParameters.swift @@ -0,0 +1,65 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Screen presentation parameters used when a universal link is triggered +@objcMembers +class ScreenPresentationParameters: NSObject { + + // MARK: - Properties + + /// Indicate to pop to home and restore initial view hierarchy + let restoreInitialDisplay: Bool + + /// Indicate to stack above visible views + /// If this variable is set to true `restoreInitialDisplay` should be set to false to have effect + let stackAboveVisibleViews: Bool + + /// The object that triggers the universal link action. + let sender: AnyObject? + + /// The view containing the anchor rectangle for the popover. Useful for iPad if a universlink trigger a pop over. + let sourceView: UIView? + + /// The view controller from which the universal link is triggered. `nil` if triggered from some other kind of object. + var presentingViewController: UIViewController? { + return self.sender as? UIViewController + } + + // MARK: - Properties + + init(restoreInitialDisplay: Bool, + stackAboveVisibleViews: Bool, + sender: AnyObject?, + sourceView: UIView?) { + self.restoreInitialDisplay = restoreInitialDisplay + self.stackAboveVisibleViews = stackAboveVisibleViews + self.sender = sender + self.sourceView = sourceView + + super.init() + } + + convenience init(restoreInitialDisplay: Bool, stackAboveVisibleViews: Bool) { + self.init(restoreInitialDisplay: restoreInitialDisplay, stackAboveVisibleViews: stackAboveVisibleViews, sender: nil, sourceView: nil) + } + + /// In this initializer `stackAboveVisibleViews` is set to false` + convenience init(restoreInitialDisplay: Bool) { + self.init(restoreInitialDisplay: restoreInitialDisplay, stackAboveVisibleViews: false, sender: nil, sourceView: nil) + } +} diff --git a/Riot/Modules/Application/ScreenNavigation/SpaceNavigationParameters.swift b/Riot/Modules/Application/ScreenNavigation/SpaceNavigationParameters.swift new file mode 100644 index 000000000..f335e56bf --- /dev/null +++ b/Riot/Modules/Application/ScreenNavigation/SpaceNavigationParameters.swift @@ -0,0 +1,45 @@ +// +// 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 + +/// Navigation parameters to display a space with a provided identifier in a specific matrix session. +@objcMembers +class SpaceNavigationParameters: NSObject { + + // MARK: - Properties + + /// The room identifier + let roomId: String + + /// The Matrix session in which the room should be available. + let mxSession: MXSession + + /// Screen presentation parameters. + let presentationParameters: ScreenPresentationParameters + + // MARK: - Setup + + init(roomId: String, + mxSession: MXSession, + presentationParameters: ScreenPresentationParameters) { + self.roomId = roomId + self.mxSession = mxSession + self.presentationParameters = presentationParameters + + super.init() + } +} diff --git a/Riot/Modules/Application/ScreenNavigation/SpacePreviewNavigationParameters.swift b/Riot/Modules/Application/ScreenNavigation/SpacePreviewNavigationParameters.swift new file mode 100644 index 000000000..b24ab1781 --- /dev/null +++ b/Riot/Modules/Application/ScreenNavigation/SpacePreviewNavigationParameters.swift @@ -0,0 +1,39 @@ +// +// 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 + +/// Navigation parameters to display a preview of a space that is unknown for the user. +@objcMembers +class SpacePreviewNavigationParameters: SpaceNavigationParameters { + + // MARK: - Properties + + /// The data for the room preview + let publicRoom: MXPublicRoom + + // MARK: - Setup + + init(publicRoom: MXPublicRoom, + mxSession: MXSession, + presentationParameters: ScreenPresentationParameters) { + self.publicRoom = publicRoom + + super.init(roomId: publicRoom.roomId, + mxSession: mxSession, + presentationParameters: presentationParameters) + } +} diff --git a/Riot/Modules/Common/Models/Section.swift b/Riot/Modules/Common/Models/Section.swift index 29eb40500..8b7131208 100644 --- a/Riot/Modules/Common/Models/Section.swift +++ b/Riot/Modules/Common/Models/Section.swift @@ -21,7 +21,35 @@ final class Section: NSObject { let tag: Int var rows: [Row] - var headerTitle: String? + var attributedHeaderTitle: NSAttributedString? + var attributedFooterTitle: NSAttributedString? + + var headerTitle: String? { + get { + attributedHeaderTitle?.string + } + set { + guard let newValue = newValue else { + attributedHeaderTitle = nil + return + } + + attributedHeaderTitle = NSAttributedString(string: newValue) + } + } + var footerTitle: String? { + get { + attributedFooterTitle?.string + } + set { + guard let newValue = newValue else { + attributedFooterTitle = nil + return + } + + attributedFooterTitle = NSAttributedString(string: newValue) + } + } init(withTag tag: Int) { self.tag = tag diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h index a12e72d67..585453317 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.h @@ -19,20 +19,22 @@ #import "PublicRoomsDirectoryDataSource.h" +@protocol RecentsListServiceProtocol; +@class DiscussionsCount; @class MXSpace; /** List the different modes used to prepare the recents data source. Each mode corresponds to an application tab: Home, Favourites, People and Rooms. + Used as the tag of UITableView, starting from 1 in order to avoid collision with default tag of UIView. */ -typedef enum : NSUInteger +typedef NS_ENUM(NSInteger, RecentsDataSourceMode) { - RecentsDataSourceModeHome, + RecentsDataSourceModeHome = 1, RecentsDataSourceModeFavourites, RecentsDataSourceModePeople, RecentsDataSourceModeRooms - -} RecentsDataSourceMode; +}; /** List the different secure backup banners that could be displayed. @@ -76,17 +78,40 @@ extern NSString *const kRecentsDataSourceTapOnDirectoryServerChange; @property (nonatomic) NSInteger serverNoticeSection; @property (nonatomic) NSInteger suggestedRoomsSection; -@property (nonatomic, readonly) NSArray* invitesCellDataArray; -@property (nonatomic, readonly) NSArray* favoriteCellDataArray; -@property (nonatomic, readonly) NSArray* peopleCellDataArray; -@property (nonatomic, readonly) NSArray* conversationCellDataArray; -@property (nonatomic, readonly) NSArray* lowPriorityCellDataArray; -@property (nonatomic, readonly) NSArray* serverNoticeCellDataArray; -@property (nonatomic, readonly) NSArray* suggestedRoomCellDataArray; +@property (nonatomic, readonly) NSInteger totalVisibleItemCount; + +/** + Counts for favorited rooms. + */ +@property (nonatomic, readonly) DiscussionsCount *favoriteMissedDiscussionsCount; + +/** + Counts for direct rooms. + */ +@property (nonatomic, readonly) DiscussionsCount *directMissedDiscussionsCount; + +/** + Counts for group rooms. + */ +@property (nonatomic, readonly) DiscussionsCount *groupMissedDiscussionsCount; @property (nonatomic, readonly) SecureBackupBannerDisplay secureBackupBannerDisplay; @property (nonatomic, readonly) CrossSigningBannerDisplay crossSigningBannerDisplay; +@property (nonatomic, readonly) id recentsListService; + ++ (instancetype)new NS_UNAVAILABLE; +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMatrixSession:(MXSession*)mxSession NS_UNAVAILABLE; + +/** + Initializer + @param mxSession session instance + @param recentsListService service instance + */ +- (instancetype)initWithMatrixSession:(MXSession*)mxSession + recentsListService:(id)recentsListService; + /** Set the delegate by specifying the selected display mode. */ @@ -163,44 +188,4 @@ extern NSString *const kRecentsDataSourceTapOnDirectoryServerChange; */ - (void)moveRoomCell:(MXRoom*)room from:(NSIndexPath*)oldPath to:(NSIndexPath*)newPath success:(void (^)(void))moveSuccess failure:(void (^)(NSError *error))moveFailure; -/** - The current number of the favourite rooms with missed notifications. - */ -@property (nonatomic, readonly) NSUInteger missedFavouriteDiscussionsCount; - -/** - The current number of the favourite rooms with unread highlighted messages. - */ -@property (nonatomic, readonly) NSUInteger missedHighlightFavouriteDiscussionsCount; - -/** - The current number of the direct chats with missed notifications, including the invites. - */ -@property (nonatomic, readonly) NSUInteger missedDirectDiscussionsCount; - -/** - The current number of the direct chats with unread highlighted messages. - */ -@property (nonatomic, readonly) NSUInteger missedHighlightDirectDiscussionsCount; - -/** - The current number of the direct chats with unsent messages. - */ -@property (nonatomic, readonly) NSUInteger unsentMessagesDirectDiscussionsCount; - -/** - The current number of the group chats with missed notifications, including the invites. - */ -@property (nonatomic, readonly) NSUInteger missedGroupDiscussionsCount; - -/** - The current number of the group chats with unread highlighted messages. - */ -@property (nonatomic, readonly) NSUInteger missedHighlightGroupDiscussionsCount; - -/** - The current number of the group chats with unsent messages. - */ -@property (nonatomic, readonly) NSUInteger unsentMessagesGroupDiscussionsCount; - @end diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index a219ab3c9..9942f883f 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -24,6 +24,7 @@ #import "MXRoom+Riot.h" #import "MXSession+Riot.h" +#import "NSArray+Element.h" #import "Riot-Swift.h" @@ -40,9 +41,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSourceTapOnDirectoryServerChange"; -@interface RecentsDataSource() +@interface RecentsDataSource() { - RecentsDataSourceState *state; dispatch_queue_t processingQueue; NSInteger shrinkedSectionsBitMask; @@ -56,6 +56,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou @property (nonatomic, assign, readwrite) SecureBackupBannerDisplay secureBackupBannerDisplay; @property (nonatomic, assign, readwrite) CrossSigningBannerDisplay crossSigningBannerDisplay; +@property (nonatomic, readwrite) id recentsListService; + @property (nonatomic, strong) CrossSigningService *crossSigningService; @end @@ -64,10 +66,10 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou @synthesize directorySection, invitesSection, favoritesSection, peopleSection, conversationSection, lowPrioritySection, serverNoticeSection, suggestedRoomsSection, secureBackupBannerSection, crossSigningBannerSection; @synthesize hiddenCellIndexPath, droppingCellIndexPath, droppingCellBackGroundView; -- (instancetype)init +- (instancetype)initWithMatrixSession:(MXSession *)mxSession + recentsListService:(id)theRecentsListService { - self = [super init]; - if (self) + if (self = [super initWithMatrixSession:mxSession]) { processingQueue = dispatch_queue_create("RecentsDataSource", DISPATCH_QUEUE_SERIAL); @@ -87,6 +89,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou [self registerCellDataClass:RecentCellData.class forCellIdentifier:kMXKRecentCellIdentifier]; [self registerSpaceServiceDidBuildGraphNotification]; + self.recentsListService = theRecentsListService; + [self.recentsListService addDelegate:self]; } return self; } @@ -112,69 +116,53 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou #pragma mark - Properties -- (NSArray *)invitesCellDataArray +- (NSArray> *)invitesCellDataArray { - return state.invitesCellDataArray; + return self.recentsListService.invitedRoomListData.rooms; } -- (NSArray *)favoriteCellDataArray +- (NSArray> *)favoriteCellDataArray { - return state.favoriteCellDataArray; + return self.recentsListService.favoritedRoomListData.rooms; } -- (NSArray *)peopleCellDataArray +- (NSArray> *)peopleCellDataArray { - return state.peopleCellDataArray; + return self.recentsListService.peopleRoomListData.rooms; } -- (NSArray *)conversationCellDataArray +- (NSArray> *)conversationCellDataArray { - return state.conversationCellDataArray; + return self.recentsListService.conversationRoomListData.rooms; } -- (NSArray *)lowPriorityCellDataArray +- (NSArray> *)lowPriorityCellDataArray { - return state.lowPriorityCellDataArray; + return self.recentsListService.lowPriorityRoomListData.rooms; } -- (NSArray *)serverNoticeCellDataArray +- (NSArray> *)serverNoticeCellDataArray { - return state.serverNoticeCellDataArray; + return self.recentsListService.serverNoticeRoomListData.rooms; } -- (NSArray *)suggestedRoomCellDataArray +- (NSArray> *)suggestedRoomCellDataArray { - return state.suggestedRoomCellDataArray; + return self.recentsListService.suggestedRoomListData.rooms; } -- (NSUInteger)missedFavouriteDiscussionsCount +- (NSInteger)totalVisibleItemCount { - return state.favouriteMissedDiscussionsCount.count; -} -- (NSUInteger)missedHighlightFavouriteDiscussionsCount -{ - return state.favouriteMissedDiscussionsCount.highlightCount; + return self.recentsListService.totalVisibleItemCount; } -- (NSUInteger)missedDirectDiscussionsCount +- (DiscussionsCount *)favoriteMissedDiscussionsCount { - return state.directMissedDiscussionsCount.count; -} -- (NSUInteger)missedHighlightDirectDiscussionsCount -{ - return state.directMissedDiscussionsCount.highlightCount; + return self.recentsListService.favoritedMissedDiscussionsCount; } -- (NSUInteger)missedGroupDiscussionsCount +- (DiscussionsCount *)directMissedDiscussionsCount { - return state.groupMissedDiscussionsCount.count; -} -- (NSUInteger)groupMissedDiscussionsCount -{ - return state.favouriteMissedDiscussionsCount.highlightCount; + return self.recentsListService.peopleMissedDiscussionsCount; } -- (NSUInteger)unsentMessagesDirectDiscussionsCount +- (DiscussionsCount *)groupMissedDiscussionsCount { - return state.unsentMessagesDirectDiscussionsCount; -} -- (NSUInteger)unsentMessagesGroupDiscussionsCount -{ - return state.unsentMessagesGroupDiscussionsCount; + return self.recentsListService.conversationMissedDiscussionsCount; } #pragma mark - @@ -201,8 +189,13 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou } [self updateSecureBackupBanner]; - [self forceRefresh]; [self refreshCrossSigningBannerDisplay]; + [self.recentsListService updateMode:_recentsDataSourceMode]; +} + +- (void)setCurrentSpace:(MXSpace *)currentSpace +{ + [self.recentsListService updateSpace:currentSpace]; } - (UIView *)viewForStickyHeaderInSection:(NSInteger)section withFrame:(CGRect)frame @@ -449,10 +442,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou // Refresh is disabled during drag&drop animation" if (!self.droppingCellIndexPath) { - [self refreshRoomsSection:^{ - // And inform the delegate about the update - [self.delegate dataSource:self didCellChange:nil]; - }]; + [self.recentsListService refresh]; } } @@ -502,13 +492,13 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou favoritesSection = sectionsCount++; } - if (_recentsDataSourceMode == RecentsDataSourceModeHome) + if (self.peopleCellDataArray.count > 0 || _recentsDataSourceMode == RecentsDataSourceModeHome) { peopleSection = sectionsCount++; } // Keep visible the main rooms section even if it is empty, except on favourites screen. - if (_recentsDataSourceMode != RecentsDataSourceModeFavourites) + if (self.conversationCellDataArray.count > 0 || _recentsDataSourceMode == RecentsDataSourceModeHome) { conversationSection = sectionsCount++; } @@ -696,61 +686,54 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou - (UIView *)badgeViewForHeaderTitleInHomeSection:(NSInteger)section { // Prepare a badge to display the total of missed notifications in this section. - NSUInteger count = 0; - NSArray *sectionArray; + id counts = nil; UIView *missedNotifAndUnreadBadgeBgView = nil; if (section == favoritesSection) { - sectionArray = self.favoriteCellDataArray; + counts = self.recentsListService.favoritedRoomListData.counts; } else if (section == peopleSection) { - sectionArray = self.peopleCellDataArray; + counts = self.recentsListService.peopleRoomListData.counts; } else if (section == conversationSection) { - sectionArray = self.conversationCellDataArray; + counts = self.recentsListService.conversationRoomListData.counts; } else if (section == lowPrioritySection) { - sectionArray = self.lowPriorityCellDataArray; + counts = self.recentsListService.lowPriorityRoomListData.counts; } else if (section == serverNoticeSection) { - sectionArray = self.serverNoticeCellDataArray; + counts = self.recentsListService.serverNoticeRoomListData.counts; } else if (section == suggestedRoomsSection) { - sectionArray = self.suggestedRoomCellDataArray; + counts = self.recentsListService.suggestedRoomListData.counts; } - BOOL highlight = NO; - for (id cellData in sectionArray) - { - count += cellData.notificationCount; - highlight |= (cellData.highlightCount > 0); - } - - if (count) + if (counts.numberOfNotifications) { UILabel *missedNotifAndUnreadBadgeLabel = [[UILabel alloc] init]; missedNotifAndUnreadBadgeLabel.textColor = ThemeService.shared.theme.baseTextPrimaryColor; missedNotifAndUnreadBadgeLabel.font = [UIFont boldSystemFontOfSize:14]; - if (count > 1000) + if (counts.numberOfNotifications > 1000) { - CGFloat value = count / 1000.0; + CGFloat value = counts.numberOfNotifications / 1000.0; missedNotifAndUnreadBadgeLabel.text = [VectorL10n largeBadgeValueKFormat:value]; } else { - missedNotifAndUnreadBadgeLabel.text = [NSString stringWithFormat:@"%tu", count]; + missedNotifAndUnreadBadgeLabel.text = [NSString stringWithFormat:@"%tu", counts.numberOfNotifications]; } [missedNotifAndUnreadBadgeLabel sizeToFit]; CGFloat bgViewWidth = missedNotifAndUnreadBadgeLabel.frame.size.width + 18; + BOOL highlight = counts.numberOfHighlights > 0; missedNotifAndUnreadBadgeBgView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, bgViewWidth, 20)]; [missedNotifAndUnreadBadgeBgView.layer setCornerRadius:10]; missedNotifAndUnreadBadgeBgView.backgroundColor = highlight ? ThemeService.shared.theme.noticeColor : ThemeService.shared.theme.noticeSecondaryColor; @@ -954,7 +937,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou - (id)cellDataAtIndexPath:(NSIndexPath *)indexPath { - id cellData = nil; + id summary = nil; NSUInteger cellDataIndex = indexPath.row; NSInteger tableSection = indexPath.section; @@ -972,53 +955,57 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou { if (cellDataIndex < self.favoriteCellDataArray.count) { - cellData = self.favoriteCellDataArray[cellDataIndex]; + summary = self.favoriteCellDataArray[cellDataIndex]; } } else if (tableSection == peopleSection) { if (cellDataIndex < self.peopleCellDataArray.count) { - cellData = self.peopleCellDataArray[cellDataIndex]; + summary = self.peopleCellDataArray[cellDataIndex]; } } else if (tableSection== conversationSection) { if (cellDataIndex < self.conversationCellDataArray.count) { - cellData = self.conversationCellDataArray[cellDataIndex]; + summary = self.conversationCellDataArray[cellDataIndex]; } } else if (tableSection == lowPrioritySection) { if (cellDataIndex < self.lowPriorityCellDataArray.count) { - cellData = self.lowPriorityCellDataArray[cellDataIndex]; + summary = self.lowPriorityCellDataArray[cellDataIndex]; } } else if (tableSection == serverNoticeSection) { if (cellDataIndex < self.serverNoticeCellDataArray.count) { - cellData = self.serverNoticeCellDataArray[cellDataIndex]; + summary = self.serverNoticeCellDataArray[cellDataIndex]; } } else if (tableSection == invitesSection) { if (cellDataIndex < self.invitesCellDataArray.count) { - cellData = self.invitesCellDataArray[cellDataIndex]; + summary = self.invitesCellDataArray[cellDataIndex]; } } else if (tableSection == suggestedRoomsSection) { if (cellDataIndex < self.suggestedRoomCellDataArray.count) { - cellData = self.suggestedRoomCellDataArray[cellDataIndex]; + summary = self.suggestedRoomCellDataArray[cellDataIndex]; } } - return cellData; + if (summary) + { + return [[MXKRecentCellData alloc] initWithRoomSummary:summary dataSource:self]; + } + return nil; } - (CGFloat)cellHeightAtIndexPath:(NSIndexPath *)indexPath @@ -1065,32 +1052,15 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou #pragma mark - -- (NSInteger)cellIndexPosWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)matrixSession within:(NSArray*)cellDataArray +- (NSInteger)cellIndexPosWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)matrixSession within:(NSArray> *)summaries { - if (roomId && matrixSession && cellDataArray.count) + if (!roomId || !matrixSession || !summaries.count || self.mxSession != matrixSession) { - for (int index = 0; index < cellDataArray.count; index++) - { - id cellDataStoring = cellDataArray[index]; - - if (cellDataStoring.roomSummary) - { - if ([roomId isEqualToString:cellDataStoring.roomSummary.roomId] && (matrixSession == cellDataStoring.roomSummary.room.mxSession)) - { - return index; - } - } - else if (cellDataStoring.spaceChildInfo) - { - if ([roomId isEqualToString:cellDataStoring.spaceChildInfo.name]) - { - return index; - } - } - } + return NSNotFound; } - - return NSNotFound; + return [summaries indexOfObjectPassingTest:^BOOL(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + return [obj.roomId isEqualToString:roomId]; + }]; } - (NSIndexPath*)cellIndexPathWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)matrixSession @@ -1206,427 +1176,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou return indexPath; } - #pragma mark - MXKDataSourceDelegate -- (void)refreshRoomsSection:(void (^)(void))onComplete -{ - if (displayedRecentsDataSourceArray.count > 0) - { - // FIXME manage multi accounts - MXKSessionRecentsDataSource *recentsDataSource = displayedRecentsDataSourceArray[0]; - - NSMutableArray> *cells = [NSMutableArray new]; - NSInteger count = recentsDataSource.numberOfCells; - - for (NSUInteger index = 0; index < count; index++) - { - id cell = [recentsDataSource cellDataAtIndex:index]; - [cells addObject:cell]; - } - - MXWeakify(self); - [self computeStateAsyncWithCells:cells recentsDataSourceMode:self.recentsDataSourceMode matrixSession:recentsDataSource.mxSession onComplete:^(RecentsDataSourceState *newState) { - MXStrongifyAndReturnIfNil(self); - - self->state = newState; - onComplete(); - }]; - } - else - { - onComplete(); - } -} - -- (void)computeStateAsyncWithCells:(NSArray> *)cells - recentsDataSourceMode:(RecentsDataSourceMode)recentsDataSourceMode - matrixSession:(MXSession*)mxSession - onComplete:(void (^)(RecentsDataSourceState *newState))onComplete -{ - dispatch_async(processingQueue, ^{ - RecentsDataSourceState *newState = [RecentsDataSource computeStateWithCells:cells recentsDataSourceMode:recentsDataSourceMode matrixSession:mxSession]; - dispatch_async(dispatch_get_main_queue(), ^{ - onComplete(newState); - }); - }); -} - -+ (RecentsDataSourceState *)computeStateWithCells:(NSArray> *)cells - recentsDataSourceMode:(RecentsDataSourceMode)recentsDataSourceMode - matrixSession:(MXSession*)mxSession -{ - NSDate *startDate = [NSDate date]; - - NSMutableArray> *invitesCellDataArray = [NSMutableArray new]; - NSMutableArray> *favoriteCellDataArray = [NSMutableArray new]; - NSMutableArray> *peopleCellDataArray = [NSMutableArray new]; - NSMutableArray> *conversationCellDataArray = [NSMutableArray new]; - NSMutableArray> *lowPriorityCellDataArray = [NSMutableArray new]; - NSMutableArray> *serverNoticeCellDataArray = [NSMutableArray new]; - NSMutableArray> *suggestedRoomCellDataArray = [NSMutableArray new]; - - MissedDiscussionsCount *favouriteMissedDiscussionsCount = [MissedDiscussionsCount new]; - MissedDiscussionsCount *directMissedDiscussionsCount = [MissedDiscussionsCount new]; - MissedDiscussionsCount *groupMissedDiscussionsCount = [MissedDiscussionsCount new]; - NSUInteger unsentMessagesDirectDiscussionsCount = 0; - NSUInteger unsentMessagesGroupDiscussionsCount = 0; - - for (id recentCellDataStoring in cells) - { - MXRoom* room = recentCellDataStoring.roomSummary.room; - - if (recentsDataSourceMode == RecentsDataSourceModeHome) - { - if (room.accountData.tags[kMXRoomTagServerNotice]) - { - [serverNoticeCellDataArray addObject:recentCellDataStoring]; - } - else if (room.accountData.tags[kMXRoomTagFavourite]) - { - [favoriteCellDataArray addObject:recentCellDataStoring]; - } - else if (room.accountData.tags[kMXRoomTagLowPriority]) - { - [lowPriorityCellDataArray addObject:recentCellDataStoring]; - } - else if (room.summary.membership == MXMembershipInvite) - { - if (room.summary.roomType != MXRoomTypeSpace && !MXSDKOptions.sharedInstance.autoAcceptRoomInvites) - { - [invitesCellDataArray addObject:recentCellDataStoring]; - } - } - else if (room.isDirect) - { - [peopleCellDataArray addObject:recentCellDataStoring]; - } - else if (recentCellDataStoring.isSuggestedRoom && recentCellDataStoring.spaceChildInfo.roomType != MXRoomTypeSpace) - { - MXRoomSummary *roomSummary = [mxSession roomSummaryWithRoomId:recentCellDataStoring.spaceChildInfo.childRoomId]; - if (!roomSummary.isJoined) - { - [suggestedRoomCellDataArray addObject:recentCellDataStoring]; - } - } - else - { - // Hide spaces from home (keep space invites) - if (room.summary.roomType != MXRoomTypeSpace) - { - [conversationCellDataArray addObject:recentCellDataStoring]; - } - } - } - else if (recentsDataSourceMode == RecentsDataSourceModeFavourites) - { - // Keep only the favourites rooms. - if (room.accountData.tags[kMXRoomTagFavourite]) - { - [favoriteCellDataArray addObject:recentCellDataStoring]; - } - } - else if (recentsDataSourceMode == RecentsDataSourceModePeople) - { - // Keep only the direct rooms which are not low priority - if (room.isDirect && !room.accountData.tags[kMXRoomTagLowPriority]) - { - if (room.summary.membership == MXMembershipInvite) - { - if (!MXSDKOptions.sharedInstance.autoAcceptRoomInvites) - { - [invitesCellDataArray addObject:recentCellDataStoring]; - } - - } - else - { - [conversationCellDataArray addObject:recentCellDataStoring]; - } - } - } - else if (recentsDataSourceMode == RecentsDataSourceModeRooms) - { - if (recentCellDataStoring.isSuggestedRoom && recentCellDataStoring.spaceChildInfo.roomType != MXRoomTypeSpace) - { - MXRoomSummary *roomSummary = [mxSession roomSummaryWithRoomId:recentCellDataStoring.spaceChildInfo.childRoomId]; - BOOL isJoined = roomSummary.membership == MXMembershipJoin || roomSummary.membershipTransitionState == MXMembershipTransitionStateJoined; - if (!isJoined) - { - [suggestedRoomCellDataArray addObject:recentCellDataStoring]; - } - } - // Consider only non direct rooms. - else if (!room.isDirect) - { - // Keep only the invites, the favourites and the rooms without tag and room type different from space - if (room.summary.membership == MXMembershipInvite) - { - if (room.summary.roomType != MXRoomTypeSpace && !MXSDKOptions.sharedInstance.autoAcceptRoomInvites) - { - [invitesCellDataArray addObject:recentCellDataStoring]; - } - } - else if ((!room.accountData.tags.count || room.accountData.tags[kMXRoomTagFavourite]) && room.summary.roomType != MXRoomTypeSpace) - { - [conversationCellDataArray addObject:recentCellDataStoring]; - } - } - } - - // Update missed conversations counts - NSUInteger notificationCount = recentCellDataStoring.roomSummary.notificationCount; - - // Ignore the regular notification count if the room is in 'mentions only" mode at the Riot level. - if (room.isMentionsOnly) - { - // Only the highlighted missed messages must be considered here. - notificationCount = recentCellDataStoring.roomSummary.highlightCount; - } - - if (notificationCount) - { - if (room.accountData.tags[kMXRoomTagFavourite]) - { - favouriteMissedDiscussionsCount.count ++; - - if (recentCellDataStoring.roomSummary.highlightCount) - { - favouriteMissedDiscussionsCount.highlightCount ++; - } - } - - if (room.isDirect) - { - directMissedDiscussionsCount.count ++; - - if (recentCellDataStoring.roomSummary.highlightCount) - { - directMissedDiscussionsCount.highlightCount ++; - } - } - else if (!room.accountData.tags.count || room.accountData.tags[kMXRoomTagFavourite]) - { - groupMissedDiscussionsCount.count ++; - - if (recentCellDataStoring.roomSummary.highlightCount) - { - groupMissedDiscussionsCount.highlightCount ++; - } - } - } - else if (room.summary.membership == MXMembershipInvite) - { - if (room.isDirect) - { - directMissedDiscussionsCount.count ++; - } - else - { - groupMissedDiscussionsCount.highlightCount ++; - } - } - - if (room.sentStatus != RoomSentStatusOk) - { - if (room.isDirect) - { - unsentMessagesDirectDiscussionsCount ++; - } - else - { - unsentMessagesGroupDiscussionsCount ++; - } - } - } - - if (recentsDataSourceMode == RecentsDataSourceModeHome) - { - BOOL pinMissedNotif = RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome; - BOOL pinUnread = RiotSettings.shared.pinRoomsWithUnreadMessagesOnHome; - NSComparator comparator = nil; - - if (pinMissedNotif) - { - // Sort each rooms collection by considering first the rooms with some missed notifs, the rooms with unread, then the others. - comparator = ^NSComparisonResult(id recentCellData1, id recentCellData2) { - - if (recentCellData1.spaceChildInfo && !recentCellData2.spaceChildInfo) - { - return NSOrderedDescending; - } - if (recentCellData2.spaceChildInfo && !recentCellData1.spaceChildInfo) - { - return NSOrderedAscending; - } - - if (recentCellData1.roomSummary.room.sentStatus != RoomSentStatusOk - && recentCellData2.roomSummary.room.sentStatus == RoomSentStatusOk) - { - return NSOrderedAscending; - } - - if (recentCellData2.roomSummary.room.sentStatus != RoomSentStatusOk - && recentCellData1.roomSummary.room.sentStatus == RoomSentStatusOk) - { - return NSOrderedDescending; - } - - if (recentCellData1.highlightCount) - { - if (recentCellData2.highlightCount) - { - return NSOrderedSame; - } - else - { - return NSOrderedAscending; - } - } - else if (recentCellData2.highlightCount) - { - return NSOrderedDescending; - } - else if (recentCellData1.notificationCount) - { - if (recentCellData2.notificationCount) - { - return NSOrderedSame; - } - else - { - return NSOrderedAscending; - } - } - else if (recentCellData2.notificationCount) - { - return NSOrderedDescending; - } - else if (pinUnread) - { - if (recentCellData1.hasUnread) - { - if (recentCellData2.hasUnread) - { - return NSOrderedSame; - } - else - { - return NSOrderedAscending; - } - } - else if (recentCellData2.hasUnread) - { - return NSOrderedDescending; - } - } - - return NSOrderedSame; - }; - } - else if (pinUnread) - { - // Sort each rooms collection by considering first the rooms with some unread messages then the others. - comparator = ^NSComparisonResult(id recentCellData1, id recentCellData2) { - - if (recentCellData1.spaceChildInfo && !recentCellData2.spaceChildInfo) - { - return NSOrderedDescending; - } - if (recentCellData2.spaceChildInfo && !recentCellData1.spaceChildInfo) - { - return NSOrderedAscending; - } - - if (recentCellData1.roomSummary.room.sentStatus != RoomSentStatusOk - && recentCellData2.roomSummary.room.sentStatus == RoomSentStatusOk) - { - return NSOrderedAscending; - } - - if (recentCellData2.roomSummary.room.sentStatus != RoomSentStatusOk - && recentCellData1.roomSummary.room.sentStatus == RoomSentStatusOk) - { - return NSOrderedDescending; - } - - if (recentCellData1.hasUnread) - { - if (recentCellData2.hasUnread) - { - return NSOrderedSame; - } - else - { - return NSOrderedAscending; - } - } - else if (recentCellData2.hasUnread) - { - return NSOrderedDescending; - } - - return NSOrderedSame; - }; - } - - if (comparator) - { - // Sort the rooms collections - [favoriteCellDataArray sortUsingComparator:comparator]; - [peopleCellDataArray sortUsingComparator:comparator]; - [conversationCellDataArray sortUsingComparator:comparator]; - [lowPriorityCellDataArray sortUsingComparator:comparator]; - [serverNoticeCellDataArray sortUsingComparator:comparator]; - } - } - else if (favoriteCellDataArray.count > 0 && recentsDataSourceMode == RecentsDataSourceModeFavourites) - { - // Sort them according to their tag order - [favoriteCellDataArray sortUsingComparator:^NSComparisonResult(id recentCellData1, id recentCellData2) { - - return [mxSession compareRoomsByTag:kMXRoomTagFavourite room1:recentCellData1.roomSummary.room room2:recentCellData2.roomSummary.room]; - - }]; - } - else if (conversationCellDataArray.count > 0 && (recentsDataSourceMode == RecentsDataSourceModeRooms || recentsDataSourceMode == RecentsDataSourceModePeople)) - { - [conversationCellDataArray sortUsingComparator:^NSComparisonResult(id recentCellData1, id recentCellData2) { - - if (recentCellData1.roomSummary.room.sentStatus != RoomSentStatusOk - && recentCellData2.roomSummary.room.sentStatus == RoomSentStatusOk) - { - return NSOrderedAscending; - } - - if (recentCellData2.roomSummary.room.sentStatus != RoomSentStatusOk - && recentCellData1.roomSummary.room.sentStatus == RoomSentStatusOk) - { - return NSOrderedDescending; - } - - return NSOrderedAscending; - }]; - } - - MXLogDebug(@"[RecentsDataSource] refreshRoomsSections: Done in %.0fms", [[NSDate date] timeIntervalSinceDate:startDate] * 1000); - MXLogDebug(@"[Spaces] refreshRoomsSections with %ld suggested room", suggestedRoomCellDataArray.count); - - return [[RecentsDataSourceState alloc] - initWithInvitesCellDataArray:invitesCellDataArray - favoriteCellDataArray:favoriteCellDataArray - peopleCellDataArray:peopleCellDataArray - conversationCellDataArray:conversationCellDataArray - lowPriorityCellDataArray:lowPriorityCellDataArray - serverNoticeCellDataArray:serverNoticeCellDataArray - suggestedRoomCellDataArray:suggestedRoomCellDataArray - favouriteMissedDiscussionsCount:favouriteMissedDiscussionsCount - directMissedDiscussionsCount:directMissedDiscussionsCount - groupMissedDiscussionsCount:groupMissedDiscussionsCount - unsentMessagesDirectDiscussionsCount:unsentMessagesDirectDiscussionsCount - unsentMessagesGroupDiscussionsCount:unsentMessagesGroupDiscussionsCount]; -} - - (void)dataSource:(MXKDataSource*)dataSource didCellChange:(id)changes { // Refresh is disabled during drag&drop animation @@ -1646,12 +1197,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou // 1 - call [super thisNewMethod] // 2 - call [self refreshRoomsSections] - // refresh the sections - [self refreshRoomsSection:^{ - // Call super to keep update readyRecentsDataSourceArray. - [super dataSource:dataSource didCellChange:changes]; - }]; - + // Call super to keep update readyRecentsDataSourceArray. + [super dataSource:dataSource didCellChange:changes]; } #pragma mark - Drag & Drop handling @@ -1720,6 +1267,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou [publicRoomsTriggerTimer invalidate]; publicRoomsTriggerTimer = nil; + + [self.recentsListService stop]; } #pragma mark - Override MXKRecentsDataSource @@ -1727,11 +1276,13 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou - (void)searchWithPatterns:(NSArray *)patternsList { [super searchWithPatterns:patternsList]; + + NSString *searchPattern = [patternsList componentsJoinedByString:@" "]; + + [self.recentsListService updateQuery:searchPattern]; if (_publicRoomsDirectoryDataSource) { - NSString *searchPattern = [patternsList componentsJoinedByString:@" "]; - // Do not send a /publicRooms request for every keystroke // Let user finish typing [publicRoomsTriggerTimer invalidate]; @@ -1878,4 +1429,12 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou [self hideCrossSigningBannerWithDisplay:self.crossSigningBannerDisplay]; } +#pragma mark - RecentsListServiceDelegate + +- (void)serviceDidChangeData:(id)service +{ + // TODO: Update only updated sections + [self.delegate dataSource:self didCellChange:nil]; +} + @end diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSourceState.swift b/Riot/Modules/Common/Recents/DataSources/RecentsDataSourceState.swift deleted file mode 100644 index 1cadbec71..000000000 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSourceState.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -/// The state for a room list screens. -@objcMembers -class RecentsDataSourceState: NSObject { - - // MARK: - Properties - - // MARK: Cells - let invitesCellDataArray: [MXKRecentCellDataStoring] - let favoriteCellDataArray: [MXKRecentCellDataStoring] - let peopleCellDataArray: [MXKRecentCellDataStoring] - let conversationCellDataArray: [MXKRecentCellDataStoring] - let lowPriorityCellDataArray: [MXKRecentCellDataStoring] - let serverNoticeCellDataArray: [MXKRecentCellDataStoring] - let suggestedRoomCellDataArray: [MXKRecentCellDataStoring] - - // MARK: Notifications counts - let favouriteMissedDiscussionsCount: MissedDiscussionsCount - let directMissedDiscussionsCount: MissedDiscussionsCount - let groupMissedDiscussionsCount: MissedDiscussionsCount - - // MARK: Unsent counts - let unsentMessagesDirectDiscussionsCount: UInt - let unsentMessagesGroupDiscussionsCount: UInt - - - // MARK: - Setup - init(invitesCellDataArray: [MXKRecentCellDataStoring], - favoriteCellDataArray: [MXKRecentCellDataStoring], - peopleCellDataArray: [MXKRecentCellDataStoring], - conversationCellDataArray: [MXKRecentCellDataStoring], - lowPriorityCellDataArray: [MXKRecentCellDataStoring], - serverNoticeCellDataArray: [MXKRecentCellDataStoring], - suggestedRoomCellDataArray: [MXKRecentCellDataStoring], - favouriteMissedDiscussionsCount: MissedDiscussionsCount, - directMissedDiscussionsCount: MissedDiscussionsCount, - groupMissedDiscussionsCount: MissedDiscussionsCount, - unsentMessagesDirectDiscussionsCount: UInt, - unsentMessagesGroupDiscussionsCount: UInt) { - self.invitesCellDataArray = invitesCellDataArray - self.favoriteCellDataArray = favoriteCellDataArray - self.peopleCellDataArray = peopleCellDataArray - self.conversationCellDataArray = conversationCellDataArray - self.lowPriorityCellDataArray = lowPriorityCellDataArray - self.serverNoticeCellDataArray = serverNoticeCellDataArray - self.suggestedRoomCellDataArray = suggestedRoomCellDataArray - self.favouriteMissedDiscussionsCount = favouriteMissedDiscussionsCount - self.directMissedDiscussionsCount = directMissedDiscussionsCount - self.groupMissedDiscussionsCount = groupMissedDiscussionsCount - self.unsentMessagesDirectDiscussionsCount = unsentMessagesDirectDiscussionsCount - self.unsentMessagesGroupDiscussionsCount = unsentMessagesGroupDiscussionsCount - super.init() - } -} - - -/// Noticiations counts per section -@objcMembers -class MissedDiscussionsCount: NSObject { - /// Regular notifications - var count: UInt = 0 - - /// Mentions like notications - var highlightCount: UInt = 0 -} diff --git a/Riot/Modules/Common/Recents/Model/DiscussionsCount.swift b/Riot/Modules/Common/Recents/Model/DiscussionsCount.swift new file mode 100644 index 000000000..ee80cc52f --- /dev/null +++ b/Riot/Modules/Common/Recents/Model/DiscussionsCount.swift @@ -0,0 +1,60 @@ +// +// 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 + +/// Noticiations counts per section +@objcMembers +public class DiscussionsCount: NSObject { + /// Number of notified rooms with regular notifications + public let numberOfNotified: Int + + /// Number of highlighted rooms with mentions like notications + public let numberOfHighlighted: Int + + /// Number of rooms that have unsent messages in it + public let numberOfUnsent: Int + + /// Flag indicating is there any unsent + public var hasUnsent: Bool { + return numberOfUnsent > 0 + } + + /// Flag indicating is there any highlight + public var hasHighlight: Bool { + return numberOfHighlighted > 0 + } + + public static let zero: DiscussionsCount = DiscussionsCount(numberOfNotified: 0, + numberOfHighlighted: 0, + numberOfUnsent: 0) + + public init(numberOfNotified: Int, + numberOfHighlighted: Int, + numberOfUnsent: Int) { + self.numberOfNotified = numberOfNotified + self.numberOfHighlighted = numberOfHighlighted + self.numberOfUnsent = numberOfUnsent + super.init() + } + + public init(withRoomListDataCounts counts: MXRoomListDataCounts) { + self.numberOfNotified = counts.numberOfNotifiedRooms + self.numberOfHighlighted = counts.numberOfHighlightedRooms + counts.numberOfInvitedRooms + self.numberOfUnsent = counts.numberOfUnsentRooms + super.init() + } +} diff --git a/Riot/Modules/Common/Recents/CellData/RecentCellData.h b/Riot/Modules/Common/Recents/Model/RecentCellData.h similarity index 100% rename from Riot/Modules/Common/Recents/CellData/RecentCellData.h rename to Riot/Modules/Common/Recents/Model/RecentCellData.h diff --git a/Riot/Modules/Common/Recents/CellData/RecentCellData.m b/Riot/Modules/Common/Recents/Model/RecentCellData.m similarity index 73% rename from Riot/Modules/Common/Recents/CellData/RecentCellData.m rename to Riot/Modules/Common/Recents/Model/RecentCellData.m index 45926d388..be2d88d47 100644 --- a/Riot/Modules/Common/Recents/CellData/RecentCellData.m +++ b/Riot/Modules/Common/Recents/Model/RecentCellData.m @@ -27,10 +27,8 @@ #endif @implementation RecentCellData -// trick to hide the mother class property as it is readonly one. -// self.roomDisplayname returns this value instead of the mother class. -@synthesize roomDisplayname; +// Adds K handling to super implementation - (NSString*)notificationCountStringValue { NSString *stringValue; @@ -49,26 +47,29 @@ return stringValue; } +// Adds mentions-only handling to super implementation - (NSUInteger)notificationCount { + MXRoom *room = [self.mxSession roomWithRoomId:self.roomSummary.roomId]; // Ignore the regular notification count if the room is in 'mentions only" mode at the Riot level. - if (self.roomSummary.room.isMentionsOnly) + if (room.isMentionsOnly) { // Only the highlighted missed messages must be considered here. - return self.roomSummary.highlightCount; + return super.highlightCount; } - return self.roomSummary.notificationCount; + return super.notificationCount; } -- (void)update +// Adds "Empty Room" case to super implementation +- (NSString *)roomDisplayname { - [super update]; - roomDisplayname = self.spaceChildInfo ? self.spaceChildInfo.name: self.roomSummary.displayname; - if (!roomDisplayname.length) + NSString *result = [super roomDisplayname]; + if (!result.length) { - roomDisplayname = [MatrixKitL10n roomDisplaynameEmptyRoom]; + result = [MatrixKitL10n roomDisplaynameEmptyRoom]; } + return result; } @end diff --git a/Riot/Modules/Common/Recents/RecentsViewController+RoomInvite.swift b/Riot/Modules/Common/Recents/RecentsViewController+RoomInvite.swift index 32bf4dada..681a24874 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController+RoomInvite.swift +++ b/Riot/Modules/Common/Recents/RecentsViewController+RoomInvite.swift @@ -18,8 +18,9 @@ import Foundation extension RecentsViewController { - @objc func canShowRoomPreview(for room: MXRoom) -> Bool { - let membershipTransitionState = room.summary.membershipTransitionState + @objc + func canShowRoomPreview(for summary: MXRoomSummaryProtocol) -> Bool { + let membershipTransitionState = summary.membershipTransitionState // NOTE: For the moment do not offer the possibility to show room preview when invitation action is in progress diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 790b26afd..f47f08a9d 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]; @@ -856,16 +856,34 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro } } -- (void)dispayRoomWithRoomId:(NSString*)roomId inMatrixSession:(MXSession*)matrixSession +- (void)showRoomWithRoomId:(NSString*)roomId inMatrixSession:(MXSession*)matrixSession { // Avoid multiple openings of rooms self.userInteractionEnabled = NO; + + // Do not stack views when showing room + ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; - [[AppDelegate theDelegate] showRoom:roomId andEventId:nil withMatrixSession:matrixSession restoreInitialDisplay:NO completion:^{ + RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId + eventId:nil + mxSession:matrixSession + presentationParameters:presentationParameters]; + + [[AppDelegate theDelegate] showRoomWithParameters:parameters completion:^{ self.userInteractionEnabled = YES; }]; } +- (void)showRoomPreviewWithData:(RoomPreviewData*)roomPreviewData +{ + // Do not stack views when showing room + ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO sender:nil sourceView:nil]; + + RoomPreviewNavigationParameters *parameters = [[RoomPreviewNavigationParameters alloc] initWithPreviewData:roomPreviewData presentationParameters:presentationParameters]; + + [[AppDelegate theDelegate] showRoomPreviewWithParameters:parameters]; +} + // Disable UI interactions in this screen while we are going to open another screen. // Interactions on reset on viewWillAppear. - (void)setUserInteractionEnabled:(BOOL)userInteractionEnabled @@ -901,7 +919,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro { id cellDataStoring = (id )cellData; - if (cellDataStoring.roomSummary.room.summary.membership != MXMembershipInvite) + if (cellDataStoring.roomSummary.membership != MXMembershipInvite) { return RecentTableViewCell.class; } @@ -939,7 +957,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro } // Display the room preview - [self dispayRoomWithRoomId:invitedRoom.roomId inMatrixSession:invitedRoom.mxSession]; + [self showRoomWithRoomId:invitedRoom.roomId inMatrixSession:invitedRoom.mxSession]; } else if ([actionIdentifier isEqualToString:kInviteRecentTableViewCellAcceptButtonPressed]) { @@ -1451,18 +1469,17 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro id cellData = [self.dataSource cellDataAtIndexPath:indexPath]; // Retrieve the invited room - MXRoom* invitedRoom = cellData.roomSummary.room; - if (invitedRoom.summary.roomType == MXRoomTypeSpace) + if (cellData.roomSummary.roomType == MXRoomTypeSpace) { // Indicates that spaces are not supported [self showSpaceInviteNotAvailable]; } // Check if can show preview for the invited room - else if ([self canShowRoomPreviewFor:invitedRoom]) + else if ([self canShowRoomPreviewFor:cellData.roomSummary]) { // Display the room preview - [self dispayRoomWithRoomId:invitedRoom.roomId inMatrixSession:invitedRoom.mxSession]; + [self showRoomWithRoomId:cellData.roomIdentifier inMatrixSession:cellData.mxSession]; } else { @@ -1982,7 +1999,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro if ([self.recentsDataSource.publicRoomsDirectoryDataSource.mxSession roomWithRoomId:publicRoom.roomId]) { // Open the public room - [[AppDelegate theDelegate] showRoom:publicRoom.roomId andEventId:nil withMatrixSession:self.recentsDataSource.publicRoomsDirectoryDataSource.mxSession restoreInitialDisplay:NO]; + [self showRoomWithRoomId:publicRoom.roomId + inMatrixSession:self.recentsDataSource.publicRoomsDirectoryDataSource.mxSession]; } else { @@ -1996,14 +2014,15 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro // Try to get more information about the room before opening its preview [roomPreviewData peekInRoom:^(BOOL succeeded) { [self stopActivityIndicator]; - - [[AppDelegate theDelegate].masterTabBarController showRoomPreview:roomPreviewData]; + + [self showRoomPreviewWithData:roomPreviewData]; }]; } else { RoomPreviewData *roomPreviewData = [[RoomPreviewData alloc] initWithPublicRoom:publicRoom andSession:self.recentsDataSource.publicRoomsDirectoryDataSource.mxSession]; - [[AppDelegate theDelegate].masterTabBarController showRoomPreview:roomPreviewData]; + + [self showRoomPreviewWithData:roomPreviewData]; } } } @@ -2069,7 +2088,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro - (void)recentListViewController:(MXKRecentListViewController *)recentListViewController didSelectRoom:(NSString *)roomId inMatrixSession:(MXSession *)matrixSession { - [self dispayRoomWithRoomId:roomId inMatrixSession:matrixSession]; + [self showRoomWithRoomId:roomId inMatrixSession:matrixSession]; } - (void)recentListViewController:(MXKRecentListViewController *)recentListViewController didSelectSuggestedRoom:(MXSpaceChildInfo *)childInfo @@ -2080,7 +2099,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro [previewData peekInRoom:^(BOOL succeeded) { MXStrongifyAndReturnIfNil(self); [self stopActivityIndicator]; - [[AppDelegate theDelegate].masterTabBarController showRoomPreview:previewData]; + [self showRoomPreviewWithData:previewData]; }]; } @@ -2123,7 +2142,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro - (void)createRoomCoordinatorBridgePresenterDelegate:(CreateRoomCoordinatorBridgePresenter *)coordinatorBridgePresenter didCreateNewRoom:(MXRoom *)room { [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - [[AppDelegate theDelegate] showRoom:room.roomId andEventId:nil withMatrixSession:self.mainSession restoreInitialDisplay:NO]; + [self showRoomWithRoomId:room.roomId inMatrixSession:self.mainSession]; }]; coordinatorBridgePresenter = nil; } @@ -2217,7 +2236,13 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro - (BOOL)shouldShowEmptyView { - return NO; + // Do not present empty screen while searching + if (self.recentsDataSource.searchPatternsList.count) + { + return NO; + } + + return self.recentsDataSource.totalVisibleItemCount == 0; } #pragma mark - RoomsDirectoryCoordinatorBridgePresenterDelegate @@ -2252,7 +2277,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro { // Room is known show it directly [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - [[AppDelegate theDelegate] showRoom:room.roomId andEventId:nil withMatrixSession:self.mainSession restoreInitialDisplay:NO]; + [self showRoomWithRoomId:room.roomId + inMatrixSession:self.mainSession]; }]; coordinatorBridgePresenter = nil; } @@ -2280,7 +2306,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro if (succeeded) { [coordinatorBridgePresenter dismissWithAnimated:YES completion:^{ - [[AppDelegate theDelegate].masterTabBarController showRoomPreview:roomPreviewData]; + + [self showRoomPreviewWithData:roomPreviewData]; }]; self.roomsDirectoryCoordinatorBridgePresenter = nil; } else { diff --git a/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift b/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift new file mode 100644 index 000000000..cda8f716d --- /dev/null +++ b/Riot/Modules/Common/Recents/Service/MatrixSDK/RecentsListService.swift @@ -0,0 +1,510 @@ +// +// 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 + +@objcMembers +public class RecentsListService: NSObject, RecentsListServiceProtocol { + + private weak var session: MXSession? + public private(set) var mode: RecentsDataSourceMode + public private(set) var query: String? + public private(set) var space: MXSpace? + + // MARK: - Fetchers + + private var invitedRoomListDataFetcher: MXRoomListDataFetcher? + private var favoritedRoomListDataFetcher: MXRoomListDataFetcher? + private var directRoomListDataFetcher: MXRoomListDataFetcher? { + switch mode { + case .home: + return directRoomListDataFetcherForHome + case .people: + return directRoomListDataFetcherForPeople + default: + return nil + } + } + private var conversationRoomListDataFetcher: MXRoomListDataFetcher? { + switch mode { + case .home: + return conversationRoomListDataFetcherForHome + case .rooms: + return conversationRoomListDataFetcherForRooms + default: + return nil + } + } + private var lowPriorityRoomListDataFetcher: MXRoomListDataFetcher? + private var serverNoticeRoomListDataFetcher: MXRoomListDataFetcher? + private var suggestedRoomListDataFetcher: MXRoomListDataFetcher? + + private var conversationRoomListDataFetcherForHome: MXRoomListDataFetcher? + private var conversationRoomListDataFetcherForRooms: MXRoomListDataFetcher? + private var directRoomListDataFetcherForHome: MXRoomListDataFetcher? + private var directRoomListDataFetcherForPeople: MXRoomListDataFetcher? + + // MARK: - Private + + private var fetcherTypesForMode: [RecentsDataSourceMode: FetcherTypes] = [ + .home: [.invited, .favorited, .directHome, .conversationHome, .lowPriority, .serverNotice, .suggested], + .favourites: [.favorited], + .people: [.directPeople], + .rooms: [.conversationRooms, .suggested] + ] + + private var allFetchers: [MXRoomListDataFetcher] { + var result: [MXRoomListDataFetcher] = [] + if let fetcher = invitedRoomListDataFetcher { + result.append(fetcher) + } + if let fetcher = favoritedRoomListDataFetcher { + result.append(fetcher) + } + if let fetcher = directRoomListDataFetcherForHome { + result.append(fetcher) + } + if let fetcher = directRoomListDataFetcherForPeople { + result.append(fetcher) + } + if let fetcher = conversationRoomListDataFetcherForHome { + result.append(fetcher) + } + if let fetcher = conversationRoomListDataFetcherForRooms { + result.append(fetcher) + } + if let fetcher = lowPriorityRoomListDataFetcher { + result.append(fetcher) + } + if let fetcher = serverNoticeRoomListDataFetcher { + result.append(fetcher) + } + if let fetcher = suggestedRoomListDataFetcher { + result.append(fetcher) + } + return result + } + + private var visibleFetchers: [MXRoomListDataFetcher] { + guard let fetcherTypes = fetcherTypesForMode[mode] else { + return [] + } + var result: [MXRoomListDataFetcher] = [] + if let fetcher = invitedRoomListDataFetcher, fetcherTypes.contains(.invited) { + result.append(fetcher) + } + if let fetcher = favoritedRoomListDataFetcher, fetcherTypes.contains(.favorited) { + result.append(fetcher) + } + if let fetcher = directRoomListDataFetcherForHome, fetcherTypes.contains(.directHome) { + result.append(fetcher) + } + if let fetcher = directRoomListDataFetcherForPeople, fetcherTypes.contains(.directPeople) { + result.append(fetcher) + } + if let fetcher = conversationRoomListDataFetcherForHome, fetcherTypes.contains(.conversationHome) { + result.append(fetcher) + } + if let fetcher = conversationRoomListDataFetcherForRooms, fetcherTypes.contains(.conversationRooms) { + result.append(fetcher) + } + if let fetcher = lowPriorityRoomListDataFetcher, fetcherTypes.contains(.lowPriority) { + result.append(fetcher) + } + if let fetcher = serverNoticeRoomListDataFetcher, fetcherTypes.contains(.serverNotice) { + result.append(fetcher) + } + if let fetcher = suggestedRoomListDataFetcher, + fetcherTypes.contains(.suggested) { + result.append(fetcher) + } + return result + } + + private var hideInvitedSection: Bool { + return MXSDKOptions.sharedInstance().autoAcceptRoomInvites + } + + private var showAllRoomsInHomeSpace: Bool { + return RiotSettings.shared.showAllRoomsInHomeSpace + } + + // swiftlint:disable weak_delegate + private let multicastDelegate: MXMulticastDelegate = MXMulticastDelegate() + // swiftlint:enable weak_delegate + + private var sortOptions: MXRoomListDataSortOptions { + switch mode { + case .home: + let pinMissed = RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome + let pinUnread = RiotSettings.shared.pinRoomsWithUnreadMessagesOnHome + return MXRoomListDataSortOptions(missedNotificationsFirst: pinMissed, + unreadMessagesFirst: pinUnread) + default: + return MXRoomListDataSortOptions(missedNotificationsFirst: false, + unreadMessagesFirst: false) + } + } + + // MARK: - Public API + + public convenience init(withSession session: MXSession) { + self.init(withSession: session, + mode: .home, + query: nil, + space: nil) + } + + private init(withSession session: MXSession, + mode: RecentsDataSourceMode, + query: String?, + space: MXSpace?) { + self.session = session + self.mode = mode + self.query = query + self.space = space + super.init() + createFetchers() + addRiotSettingsObserver() + } + + // MARK: - View Data + + public var invitedRoomListData: MXRoomListData? { + guard shouldShowInvited else { return nil } + return invitedRoomListDataFetcher?.data + } + public var favoritedRoomListData: MXRoomListData? { + guard shouldShowFavorited else { return nil } + return favoritedRoomListDataFetcher?.data + } + public var peopleRoomListData: MXRoomListData? { + guard shouldShowDirect else { return nil } + return directRoomListDataFetcher?.data + } + public var conversationRoomListData: MXRoomListData? { + guard shouldShowConversation else { return nil } + return conversationRoomListDataFetcher?.data + } + public var lowPriorityRoomListData: MXRoomListData? { + guard shouldShowLowPriority else { return nil } + return lowPriorityRoomListDataFetcher?.data + } + public var serverNoticeRoomListData: MXRoomListData? { + guard shouldShowServerNotice else { return nil } + return serverNoticeRoomListDataFetcher?.data + } + public var suggestedRoomListData: MXRoomListData? { + guard shouldShowSuggested else { return nil } + return suggestedRoomListDataFetcher?.data + } + + public var favoritedMissedDiscussionsCount: DiscussionsCount { + guard let data = favoritedRoomListDataFetcher?.data else { + return .zero + } + return DiscussionsCount(withRoomListDataCounts: data.counts) + } + + public var peopleMissedDiscussionsCount: DiscussionsCount { + guard let data = directRoomListDataFetcherForPeople?.data else { + return .zero + } + return DiscussionsCount(withRoomListDataCounts: data.counts) + } + + public var conversationMissedDiscussionsCount: DiscussionsCount { + guard let data = conversationRoomListDataFetcherForRooms?.data else { + return .zero + } + return DiscussionsCount(withRoomListDataCounts: data.counts) + } + + public var totalVisibleItemCount: Int { + return visibleFetchers.reduce(0, { $0 + ($1.data?.counts.numberOfRooms ?? 0) }) + } + + public func updateMode(_ mode: RecentsDataSourceMode) { + self.mode = mode + if let fetcher = favoritedRoomListDataFetcher { + updateFavoritedFetcher(fetcher, for: mode) + } + allFetchers.forEach({ notifyDataChange(on: $0) }) + } + + public func updateQuery(_ query: String?) { + self.query = query + visibleFetchers.forEach({ $0.fetchOptions.filterOptions.query = query }) + } + + public func updateSpace(_ space: MXSpace?) { + self.space = space + allFetchers.forEach({ $0.fetchOptions.filterOptions.space = space }) + } + + public func refresh() { + allFetchers.forEach({ $0.fetchOptions.sortOptions = sortOptions }) + allFetchers.forEach({ $0.fetchOptions.filterOptions.showAllRoomsInHomeSpace = showAllRoomsInHomeSpace }) + } + + public func stop() { + removeRiotSettingsObserver() + removeAllDelegates() + allFetchers.forEach({ $0.stop() }) + } + + // MARK: - Delegate + + public func addDelegate(_ delegate: RecentsListServiceDelegate) { + multicastDelegate.addDelegate(delegate) + } + + public func removeDelegate(_ delegate: RecentsListServiceDelegate) { + multicastDelegate.removeDelegate(delegate) + } + + public func removeAllDelegates() { + multicastDelegate.removeAllDelegates() + } + + // MARK: - Riot Settings Observer + + private func addRiotSettingsObserver() { + NotificationCenter.default.addObserver(self, + selector: #selector(userDefaultsUpdated(_:)), + name: .userDefaultValueUpdated, + object: nil) + } + + private func removeRiotSettingsObserver() { + NotificationCenter.default.removeObserver(self, + name: .userDefaultValueUpdated, + object: nil) + } + + @objc + private func userDefaultsUpdated(_ notification: Notification) { + guard let key = notification.object as? String else { + return + } + switch key { + case RiotSettings.UserDefaultsKeys.pinRoomsWithMissedNotificationsOnHome, + RiotSettings.UserDefaultsKeys.pinRoomsWithUnreadMessagesOnHome: + refresh() + case RiotSettings.UserDefaultsKeys.showAllRoomsInHomeSpace: + refresh() + default: + break + } + } + + // MARK: - Private + + private var shouldShowInvited: Bool { + return fetcherTypesForMode[mode]?.contains(.invited) ?? false + } + + private var shouldShowFavorited: Bool { + return fetcherTypesForMode[mode]?.contains(.favorited) ?? false + } + + private var shouldShowDirect: Bool { + switch mode { + case .home: + return fetcherTypesForMode[mode]?.contains(.directHome) ?? false + case .people: + return fetcherTypesForMode[mode]?.contains(.directPeople) ?? false + default: + return false + } + } + + private var shouldShowConversation: Bool { + switch mode { + case .home: + return fetcherTypesForMode[mode]?.contains(.conversationHome) ?? false + case .rooms: + return fetcherTypesForMode[mode]?.contains(.conversationRooms) ?? false + default: + return false + } + } + + private var shouldShowLowPriority: Bool { + return fetcherTypesForMode[mode]?.contains(.lowPriority) ?? false + } + + private var shouldShowServerNotice: Bool { + return fetcherTypesForMode[mode]?.contains(.serverNotice) ?? false + } + + private var shouldShowSuggested: Bool { + return fetcherTypesForMode[mode]?.contains(.suggested) ?? false + } + + private func createCommonRoomListDataFetcher(withDataTypes dataTypes: MXRoomSummaryDataTypes = [], + onlySuggested: Bool = false, + paginate: Bool = true) -> MXRoomListDataFetcher { + guard let session = session else { + fatalError("Session deallocated") + } + let filterOptions = MXRoomListDataFilterOptions(dataTypes: dataTypes, + onlySuggested: onlySuggested, + query: query, + space: space, + showAllRoomsInHomeSpace: showAllRoomsInHomeSpace) + + let fetchOptions = MXRoomListDataFetchOptions(filterOptions: filterOptions, + sortOptions: sortOptions, + async: false) + let fetcher = session.roomListDataManager.fetcher(withOptions: fetchOptions) + if paginate { + fetcher.addDelegate(self) + fetcher.paginate() + } + return fetcher + } + + private func createDirectRoomListDataFetcherForHome() -> MXRoomListDataFetcher { + let fetcher = createCommonRoomListDataFetcher(withDataTypes: [.direct], paginate: false) + updateDirectFetcher(fetcher, for: .home) + fetcher.addDelegate(self) + fetcher.paginate() + return fetcher + } + + private func createDirectRoomListDataFetcherForPeople() -> MXRoomListDataFetcher { + let fetcher = createCommonRoomListDataFetcher(withDataTypes: [.direct], paginate: false) + updateDirectFetcher(fetcher, for: .people) + fetcher.addDelegate(self) + fetcher.paginate() + return fetcher + } + + private func createConversationRoomListDataFetcherForHome() -> MXRoomListDataFetcher { + let fetcher = createCommonRoomListDataFetcher(withDataTypes: [], paginate: false) + updateConversationFetcher(fetcher, for: .home) + fetcher.addDelegate(self) + fetcher.paginate() + return fetcher + } + + private func createConversationRoomListDataFetcherForRooms() -> MXRoomListDataFetcher { + let fetcher = createCommonRoomListDataFetcher(withDataTypes: [], paginate: false) + updateConversationFetcher(fetcher, for: .rooms) + fetcher.addDelegate(self) + fetcher.paginate() + return fetcher + } + + private func createFetchers() { + if !hideInvitedSection { + invitedRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.invited]) + } + favoritedRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.favorited]) + directRoomListDataFetcherForHome = createDirectRoomListDataFetcherForHome() + directRoomListDataFetcherForPeople = createDirectRoomListDataFetcherForPeople() + conversationRoomListDataFetcherForHome = createConversationRoomListDataFetcherForHome() + conversationRoomListDataFetcherForRooms = createConversationRoomListDataFetcherForRooms() + lowPriorityRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.lowPriority]) + serverNoticeRoomListDataFetcher = createCommonRoomListDataFetcher(withDataTypes: [.serverNotice]) + suggestedRoomListDataFetcher = createCommonRoomListDataFetcher(onlySuggested: true) + } + + private func updateDirectFetcher(_ fetcher: MXRoomListDataFetcher, for mode: RecentsDataSourceMode) { + switch mode { + case .home: + fetcher.fetchOptions.filterOptions.notDataTypes = [.invited, .lowPriority] + case .people: + fetcher.fetchOptions.filterOptions.notDataTypes = [.lowPriority] + default: + break + } + } + + private func updateFavoritedFetcher(_ fetcher: MXRoomListDataFetcher, for mode: RecentsDataSourceMode) { + switch mode { + case .home: + fetcher.fetchOptions.sortOptions = sortOptions + case .favourites: + let newSortOptions = sortOptions + newSortOptions.favoriteTag = true + fetcher.fetchOptions.sortOptions = newSortOptions + default: + break + } + } + + private func updateConversationFetcher(_ fetcher: MXRoomListDataFetcher, for mode: RecentsDataSourceMode) { + var notDataTypes: MXRoomSummaryDataTypes = [.hidden, .conferenceUser, .direct, .lowPriority, .serverNotice, .space] + switch mode { + case .home: + notDataTypes.insert([.invited, .favorited]) + fetcher.fetchOptions.filterOptions.notDataTypes = notDataTypes + case .rooms: + if hideInvitedSection { + notDataTypes.insert(.invited) + } + fetcher.fetchOptions.filterOptions.notDataTypes = notDataTypes + default: + break + } + } + + private func notifyDataChange(on fetcher: MXRoomListDataFetcher) { + multicastDelegate.invoke({ $0.serviceDidChangeData(self) }) + } + + deinit { + stop() + } + +} + +// MARK: - MXRoomListDataFetcherDelegate + +extension RecentsListService: MXRoomListDataFetcherDelegate { + + public func fetcherDidChangeData(_ fetcher: MXRoomListDataFetcher) { + notifyDataChange(on: fetcher) + } + +} + +// MARK: - FetcherTypes + +private struct FetcherTypes: OptionSet { + typealias RawValue = Int + let rawValue: RawValue + + init(rawValue: RawValue) { + self.rawValue = rawValue + } + + static let invited = FetcherTypes(rawValue: 1 << 0) + static let favorited = FetcherTypes(rawValue: 1 << 1) + static let directHome = FetcherTypes(rawValue: 1 << 2) + static let directPeople = FetcherTypes(rawValue: 1 << 3) + static let conversationHome = FetcherTypes(rawValue: 1 << 4) + static let conversationRooms = FetcherTypes(rawValue: 1 << 5) + static let lowPriority = FetcherTypes(rawValue: 1 << 6) + static let serverNotice = FetcherTypes(rawValue: 1 << 7) + static let suggested = FetcherTypes(rawValue: 1 << 8) + + static let none: FetcherTypes = [] + static let all: FetcherTypes = [ + .invited, .favorited, .directHome, .directPeople, .conversationHome, .conversationRooms, .lowPriority, .serverNotice, .suggested] +} diff --git a/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift b/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift new file mode 100644 index 000000000..6be38a676 --- /dev/null +++ b/Riot/Modules/Common/Recents/Service/Mock/MockRecentsListService.swift @@ -0,0 +1,216 @@ +// +// 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 + +@objcMembers +public class MockRecentsListService: NSObject, RecentsListServiceProtocol { + + private var rooms: [MockRoomSummary] + + private var _invitedRoomListData: MXRoomListData? + private var _favoritedRoomListData: MXRoomListData? + private var _peopleRoomListData: MXRoomListData? + private var _conversationRoomListData: MXRoomListData? + private var _lowPriorityRoomListData: MXRoomListData? + private var _serverNoticeRoomListData: MXRoomListData? + + // swiftlint:disable weak_delegate + private let multicastDelegate: MXMulticastDelegate = MXMulticastDelegate() + // swiftlint:enable weak_delegate + + public init(withRooms rooms: [MockRoomSummary]) { + self.rooms = rooms + + var invited: [MockRoomSummary] = [] + var favorited: [MockRoomSummary] = [] + var people: [MockRoomSummary] = [] + var conversation: [MockRoomSummary] = [] + var lowPriority: [MockRoomSummary] = [] + var serverNotice: [MockRoomSummary] = [] + + rooms.forEach { summary in + if summary.isTyped(.invited) { + invited.append(summary) + } + if summary.isTyped(.favorited) { + favorited.append(summary) + } + if summary.isTyped(.direct) { + people.append( summary) + } + if !summary.isTyped([.direct, + .invited, + .favorited, + .lowPriority, + .serverNotice]) { + conversation.append(summary) + } + if summary.isTyped(.lowPriority) { + lowPriority.append(summary) + } + if summary.isTyped(.serverNotice) { + serverNotice.append(summary) + } + } + _invitedRoomListData = MockRoomListData(withRooms: invited) + _favoritedRoomListData = MockRoomListData(withRooms: favorited) + _peopleRoomListData = MockRoomListData(withRooms: people) + _conversationRoomListData = MockRoomListData(withRooms: conversation) + _lowPriorityRoomListData = MockRoomListData(withRooms: lowPriority) + _serverNoticeRoomListData = MockRoomListData(withRooms: serverNotice) + + super.init() + } + + public static func generate(withNumberOfRooms numberOfRooms: Int) -> MockRecentsListService { + var rooms: [MockRoomSummary] = [] + for i in 0.. Bool { + return (dataTypes.rawValue & types.rawValue) != 0 + } + + public var sentStatus: MXRoomSummarySentStatus = .ok + + public var spaceChildInfo: MXSpaceChildInfo? + + public var parentSpaceIds: Set = [] + + public init(withRoomId roomId: String) { + self.roomId = roomId + super.init() + } + + public static func generate() -> MockRoomSummary { + return generate(withTypes: []) + } + + public static func generateDirect() -> MockRoomSummary { + return generate(withTypes: .direct) + } + + public static func generate(withTypes types: MXRoomSummaryDataTypes) -> MockRoomSummary { + guard let random = MXTools.generateSecret() else { + fatalError("Room id cannot be created") + } + let result = MockRoomSummary(withRoomId: "!\(random):matrix.org") + result.dataTypes = types + if types.contains(.invited) { + result.membership = .invite + result.membershipTransitionState = .invited + } + return result + } + +} diff --git a/Riot/Modules/Common/Recents/Service/RecentsListServiceDelegate.swift b/Riot/Modules/Common/Recents/Service/RecentsListServiceDelegate.swift new file mode 100644 index 000000000..a2afaf599 --- /dev/null +++ b/Riot/Modules/Common/Recents/Service/RecentsListServiceDelegate.swift @@ -0,0 +1,25 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@objc +public protocol RecentsListServiceDelegate: AnyObject { + + /// Delegate method to be called when service data updated + /// - Parameter service: service object + func serviceDidChangeData(_ service: RecentsListServiceProtocol) +} diff --git a/Riot/Modules/Common/Recents/Service/RecentsListServiceProtocol.swift b/Riot/Modules/Common/Recents/Service/RecentsListServiceProtocol.swift new file mode 100644 index 000000000..154cf2e21 --- /dev/null +++ b/Riot/Modules/Common/Recents/Service/RecentsListServiceProtocol.swift @@ -0,0 +1,102 @@ +// +// 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 +public protocol RecentsListServiceProtocol { + + // MARK: - Properties + + /// Current mode + var mode: RecentsDataSourceMode { get } + + /// Query to filter rooms + var query: String? { get } + + /// Current space + var space: MXSpace? { get } + + // MARK: - Data + + /// Invited rooms for current mode + var invitedRoomListData: MXRoomListData? { get } + + /// Favorited rooms for current mode + var favoritedRoomListData: MXRoomListData? { get } + + /// Direct rooms for current mode + var peopleRoomListData: MXRoomListData? { get } + + /// Rooms for current mode + var conversationRoomListData: MXRoomListData? { get } + + /// Low priority rooms for current mode + var lowPriorityRoomListData: MXRoomListData? { get } + + /// Server notice rooms for current mode + var serverNoticeRoomListData: MXRoomListData? { get } + + /// Suggested rooms for current mode + var suggestedRoomListData: MXRoomListData? { get } + + // MARK: Discussion counts + + /// Counts for favorite screen + var favoritedMissedDiscussionsCount: DiscussionsCount { get } + + /// Counts for people screen + var peopleMissedDiscussionsCount: DiscussionsCount { get } + + /// Counts for rooms screen + var conversationMissedDiscussionsCount: DiscussionsCount { get } + + /// Total number of rooms visible in one screen. Can be used to display an empty view + var totalVisibleItemCount: Int { get } + + // MARK: - Methods + + /// Upte mode function + /// - Parameter mode: new mode + func updateMode(_ mode: RecentsDataSourceMode) + + /// Update query to filter rooms + /// - Parameter query: new query + func updateQuery(_ query: String?) + + /// Update current space + /// - Parameter space: new space + func updateSpace(_ space: MXSpace?) + + /// Refresh recents + func refresh() + + /// Stop service. Do not use after stopping. + func stop() + + // MARK: - Delegate + + /// Add delegate instance for the service + /// - Parameter delegate: new delegate + func addDelegate(_ delegate: RecentsListServiceDelegate) + + /// Remove given delegate instance + /// - Parameter delegate: delegate to be removed + func removeDelegate(_ delegate: RecentsListServiceDelegate) + + /// Remove all delegates + func removeAllDelegates() +} diff --git a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m index cf6077adf..0d540399b 100644 --- a/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m +++ b/Riot/Modules/Common/Recents/Views/RecentTableViewCell.m @@ -76,7 +76,7 @@ self.lastEventDate.text = roomCellData.lastEventDate; // Manage lastEventAttributedTextMessage optional property - if (!roomCellData.spaceChildInfo && [roomCellData respondsToSelector:@selector(lastEventAttributedTextMessage)]) + if (!roomCellData.roomSummary.spaceChildInfo && [roomCellData respondsToSelector:@selector(lastEventAttributedTextMessage)]) { // Force the default text color for the last message (cancel highlighted message color) NSMutableAttributedString *lastEventDescription = [[NSMutableAttributedString alloc] initWithAttributedString:roomCellData.lastEventAttributedTextMessage]; @@ -88,7 +88,7 @@ self.lastEventDescription.text = roomCellData.lastEventTextMessage; } - self.unsentImageView.hidden = roomCellData.roomSummary.room.sentStatus == RoomSentStatusOk; + self.unsentImageView.hidden = roomCellData.roomSummary.sentStatus == MXRoomSummarySentStatusOk; self.lastEventDecriptionLabelTrailingConstraint.constant = self.unsentImageView.hidden ? 10 : 30; // Notify unreads and bing @@ -124,14 +124,10 @@ self.roomTitle.font = [UIFont systemFontOfSize:17 weight:UIFontWeightMedium]; } - if (roomCellData.spaceChildInfo) - { - [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.spaceChildInfo.avatarUrl 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.avatarUrl + roomId:roomCellData.roomIdentifier + displayName:roomCellData.roomDisplayname + mediaManager:roomCellData.mxSession.mediaManager]; } else { diff --git a/Riot/Modules/Common/SectionFooters/SectionFooterView.swift b/Riot/Modules/Common/SectionFooters/SectionFooterView.swift new file mode 100644 index 000000000..92c36e88c --- /dev/null +++ b/Riot/Modules/Common/SectionFooters/SectionFooterView.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 UIKit +import Reusable + +/// A subclass of `UITableViewHeaderFooterView` that conforms to `Themable` +/// to create a consistent looking custom footer inside of the app. If using gesture +/// recognizers on the view, be aware that these will be automatically removed on reuse. +@objcMembers +class SectionFooterView: UITableViewHeaderFooterView, NibLoadable, Themable { + + // MARK: - Properties + + static var defaultReuseIdentifier: String { + String(describing: Self.self) + } + + static var nib: UINib { + // Copy paste from NibReusable in order to expose to ObjC + UINib(nibName: String(describing: self), bundle: Bundle(for: self)) + } + + /// The amount to inset the footer label on its leading side, relative to the safe area insets. + var leadingInset: CGFloat { + get { footerLabelLeadingConstraint.constant } + set { footerLabelLeadingConstraint.constant = newValue } + } + + /// The text label added in the xib file. Using our own label was necessary due to the behaviour + /// on iOS 12-14 where any customisation to the existing text label is wiped out after being + /// set in `tableView:viewForFooterInSection`. This behaviour is fixed in iOS 15. + @IBOutlet private weak var footerLabel: UILabel! + /// The label's leading constraint, relative to the safe area insets. + @IBOutlet private weak var footerLabelLeadingConstraint: NSLayoutConstraint! + + // MARK: - Public + + override func prepareForReuse() { + super.prepareForReuse() + + for recognizer in gestureRecognizers ?? [] { + removeGestureRecognizer(recognizer) + } + } + + func update(theme: Theme) { + footerLabel.textColor = theme.colors.secondaryContent + footerLabel.font = theme.fonts.subheadline + footerLabel.numberOfLines = 0 + } + + /// Update the footer with new text. + func update(withText text: String) { + footerLabel.text = text + } + + /// Update the footer with attributed text. Be sure to call this after calling `update(theme:)` + /// otherwise any color or font attributes will be wiped out by the theme. + func update(withAttributedText attributedText: NSAttributedString) { + footerLabel.attributedText = attributedText + } +} diff --git a/Riot/Modules/Common/SectionFooters/SectionFooterView.xib b/Riot/Modules/Common/SectionFooters/SectionFooterView.xib new file mode 100644 index 000000000..1dbf29848 --- /dev/null +++ b/Riot/Modules/Common/SectionFooters/SectionFooterView.xib @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/RiotShareExtension/Modules/Share/ShareViewController.h b/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinatorParameters.swift similarity index 59% rename from RiotShareExtension/Modules/Share/ShareViewController.h rename to Riot/Modules/Communities/TabDetail/GroupDetailsCoordinatorParameters.swift index 74e7af78a..8ff866d7a 100644 --- a/RiotShareExtension/Modules/Share/ShareViewController.h +++ b/Riot/Modules/Communities/TabDetail/GroupDetailsCoordinatorParameters.swift @@ -1,5 +1,7 @@ +// File created from ScreenTemplate +// $ createScreen.sh Communities GroupDetails /* - Copyright 2017 Aram Sargsyan + Copyright 2021 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,9 +16,14 @@ limitations under the License. */ -#import -#import +import Foundation -@interface ShareViewController : MXKViewController - -@end +/// 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/DeepLink/UniversalLinkParameters.swift b/Riot/Modules/DeepLink/UniversalLinkParameters.swift new file mode 100644 index 000000000..611988232 --- /dev/null +++ b/Riot/Modules/DeepLink/UniversalLinkParameters.swift @@ -0,0 +1,55 @@ +// +// 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 + +/// Parameters describing a universal link +@objcMembers +class UniversalLinkParameters: NSObject { + + // MARK: - Properties + + /// The unprocessed the universal link URL + let universalLinkURL: URL + + /// The fragment part of the universal link + let fragment: String + + /// Presentation parameters + let presentationParameters: ScreenPresentationParameters + + // MARK: - Setup + + init(fragment: String, + universalLinkURL: URL, + presentationParameters: ScreenPresentationParameters) { + self.fragment = fragment + self.universalLinkURL = universalLinkURL + self.presentationParameters = presentationParameters + + super.init() + } + + convenience init?(universalLinkURL: URL, + presentationParameters: ScreenPresentationParameters) { + + guard let fixedURL = Tools.fixURL(withSeveralHashKeys: universalLinkURL), let fragment = fixedURL.fragment else { + return nil + } + + self.init(fragment: fragment, universalLinkURL: universalLinkURL, presentationParameters: presentationParameters) + } +} diff --git a/Riot/Modules/Favorites/FavouritesViewController.m b/Riot/Modules/Favorites/FavouritesViewController.m index e5bad0b6f..2d2eca941 100644 --- a/Riot/Modules/Favorites/FavouritesViewController.m +++ b/Riot/Modules/Favorites/FavouritesViewController.m @@ -142,21 +142,4 @@ } } -- (BOOL)shouldShowEmptyView -{ - // Do not present empty screen while searching - if (recentsDataSource.searchPatternsList.count) - { - return NO; - } - - return [self totalItemCounts] == 0; -} - -// Total items to display on the screen -- (NSUInteger)totalItemCounts -{ - return recentsDataSource.favoriteCellDataArray.count; -} - @end diff --git a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m index 958e2cb24..05043e47b 100644 --- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m @@ -139,6 +139,20 @@ } } +- (void)showRoomWithId:(NSString*)roomId + andEventId:(NSString*)eventId + inMatrixSession:(MXSession*)session +{ + ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; + + RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId + eventId:eventId + mxSession:session + presentationParameters:presentationParameters]; + + [[AppDelegate theDelegate] showRoomWithParameters:parameters]; +} + #pragma mark - MXKDataSourceDelegate - (Class)cellViewClassForCellData:(MXKCellData*)cellData @@ -188,9 +202,9 @@ [tableView deselectRowAtIndexPath:indexPath animated:YES]; // Make the master tabBar view controller open the RoomViewController - [[AppDelegate theDelegate].masterTabBarController selectRoomWithId:cellData.roomId - andEventId:_selectedEvent.eventId - inMatrixSession:self.mainSession]; + [self showRoomWithId:cellData.roomId + andEventId:_selectedEvent.eventId + inMatrixSession:self.mainSession]; // Reset the selected event. HomeViewController got it when here _selectedEvent = nil; diff --git a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m index 0dbc1cfcb..1050bbd1b 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m @@ -146,6 +146,20 @@ } } +- (void)showRoomWithId:(NSString*)roomId + andEventId:(NSString*)eventId + inMatrixSession:(MXSession*)session +{ + ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; + + RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId + eventId:eventId + mxSession:session + presentationParameters:presentationParameters]; + + [[AppDelegate theDelegate] showRoomWithParameters:parameters]; +} + #pragma mark - MXKDataSourceDelegate - (Class)cellViewClassForCellData:(MXKCellData*)cellData @@ -239,9 +253,9 @@ [tableView deselectRowAtIndexPath:indexPath animated:YES]; // Make the master tabBar view controller open the RoomViewController - [[AppDelegate theDelegate].masterTabBarController selectRoomWithId:cellData.roomId - andEventId:_selectedEvent.eventId - inMatrixSession:cellData.mxSession]; + [self showRoomWithId:cellData.roomId + andEventId:_selectedEvent.eventId + inMatrixSession:cellData.mxSession]; // Reset the selected event. HomeViewController got it when here _selectedEvent = nil; diff --git a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m index 1de1f5b4b..2f6e791d7 100644 --- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m +++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m @@ -193,7 +193,7 @@ if ([dataSource.mxSession roomWithRoomId:publicRoom.roomId]) { // Open the public room. - [self openRoomWithId:publicRoom.roomId inMatrixSession:dataSource.mxSession]; + [self showRoomWithId:publicRoom.roomId inMatrixSession:dataSource.mxSession]; } else { @@ -209,13 +209,13 @@ [self stopActivityIndicator]; - [[AppDelegate theDelegate].masterTabBarController showRoomPreview:roomPreviewData]; + [self showRoomPreviewWithData:roomPreviewData]; }]; } else { RoomPreviewData *roomPreviewData = [[RoomPreviewData alloc] initWithPublicRoom:publicRoom andSession:dataSource.mxSession]; - [[AppDelegate theDelegate].masterTabBarController showRoomPreview:roomPreviewData]; + [self showRoomPreviewWithData:roomPreviewData]; } } @@ -232,9 +232,23 @@ #pragma mark - Private methods -- (void)openRoomWithId:(NSString*)roomId inMatrixSession:(MXSession*)mxSession -{ - [[AppDelegate theDelegate] showRoom:roomId andEventId:nil withMatrixSession:mxSession restoreInitialDisplay:NO]; +- (void)showRoomWithId:(NSString*)roomId inMatrixSession:(MXSession*)mxSession +{ + ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; + + RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId + eventId:nil + mxSession:mxSession + presentationParameters:presentationParameters]; + [[AppDelegate theDelegate] showRoomWithParameters:parameters]; +} + +- (void)showRoomPreviewWithData:(RoomPreviewData*)roomPreviewData +{ + ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; + + RoomPreviewNavigationParameters *parameters = [[RoomPreviewNavigationParameters alloc] initWithPreviewData:roomPreviewData presentationParameters:presentationParameters]; + [[AppDelegate theDelegate] showRoomPreviewWithParameters:parameters]; } - (void)refreshCurrentSelectedCell:(BOOL)forceVisible @@ -243,7 +257,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..03375e8b9 100644 --- a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m +++ b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m @@ -91,6 +91,7 @@ [titles addObject:[VectorL10n searchPeople]]; peopleSearchViewController = [ContactsTableViewController contactsTableViewController]; peopleSearchViewController.contactsTableViewControllerDelegate = self; + peopleSearchViewController.disableFindYourContactsFooter = YES; [viewControllers addObject:peopleSearchViewController]; // add Files tab @@ -229,7 +230,9 @@ if (mainSession) { // Init the recents data source - recentsDataSource = [[UnifiedSearchRecentsDataSource alloc] initWithMatrixSession:mainSession]; + RecentsListService *recentsListService = [[RecentsListService alloc] initWithSession:mainSession]; + recentsDataSource = [[UnifiedSearchRecentsDataSource alloc] initWithMatrixSession:mainSession + recentsListService:recentsListService]; [recentsViewController displayList:recentsDataSource]; // Init the search for messages @@ -245,6 +248,7 @@ // Init the search for people peopleSearchDataSource = [[ContactsDataSource alloc] initWithMatrixSession:mainSession]; + peopleSearchDataSource.showLocalContacts = NO; peopleSearchDataSource.areSectionsShrinkable = YES; peopleSearchDataSource.displaySearchInputInContactsList = YES; peopleSearchDataSource.contactCellAccessoryImage = [[UIImage imageNamed: @"disclosure_icon"] vc_tintedImageUsingColor:ThemeService.shared.theme.textSecondaryColor];; diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index cd3281eef..411add2bb 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -378,8 +378,8 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - if ((indexPath.section == recentsDataSource.conversationSection && !recentsDataSource.conversationCellDataArray.count) - || (indexPath.section == recentsDataSource.peopleSection && !recentsDataSource.peopleCellDataArray.count) + if ((indexPath.section == recentsDataSource.conversationSection && !recentsDataSource.recentsListService.conversationRoomListData.counts.numberOfRooms) + || (indexPath.section == recentsDataSource.peopleSection && !recentsDataSource.recentsListService.peopleRoomListData.counts.numberOfRooms) || (indexPath.section == recentsDataSource.secureBackupBannerSection) || (indexPath.section == recentsDataSource.crossSigningBannerSection) ) @@ -470,8 +470,8 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - if ((indexPath.section == recentsDataSource.conversationSection && !recentsDataSource.conversationCellDataArray.count) - || (indexPath.section == recentsDataSource.peopleSection && !recentsDataSource.peopleCellDataArray.count)) + if ((indexPath.section == recentsDataSource.conversationSection && !recentsDataSource.recentsListService.conversationRoomListData.counts.numberOfRooms) + || (indexPath.section == recentsDataSource.peopleSection && !recentsDataSource.recentsListService.peopleRoomListData.counts.numberOfRooms)) { return [recentsDataSource cellHeightAtIndexPath:indexPath]; } @@ -601,7 +601,7 @@ UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onCollectionViewCellTap:)]; [cell addGestureRecognizer:tapGesture]; - if ([cellData.roomSummary.roomId isEqualToString:editedRoomId]) + if ([cellData.roomIdentifier isEqualToString:editedRoomId]) { cell.editionArrowView.hidden = NO; } @@ -631,11 +631,14 @@ if (renderedCellData.isSuggestedRoom) { - [self.delegate recentListViewController:self didSelectSuggestedRoom:renderedCellData.spaceChildInfo]; + [self.delegate recentListViewController:self + didSelectSuggestedRoom:renderedCellData.roomSummary.spaceChildInfo]; } else { - [self.delegate recentListViewController:self didSelectRoom:renderedCellData.roomSummary.roomId inMatrixSession:renderedCellData.roomSummary.room.mxSession]; + [self.delegate recentListViewController:self + didSelectRoom:renderedCellData.roomIdentifier + inMatrixSession:renderedCellData.mxSession]; } } @@ -924,19 +927,7 @@ } // Otherwise check the number of items to display - return [self totalItemCounts] == 0; -} - -// Total items to display on the screen -- (NSUInteger)totalItemCounts -{ - return recentsDataSource.invitesCellDataArray.count - + recentsDataSource.favoriteCellDataArray.count - + recentsDataSource.peopleCellDataArray.count - + recentsDataSource.conversationCellDataArray.count - + recentsDataSource.lowPriorityCellDataArray.count - + recentsDataSource.serverNoticeCellDataArray.count - + recentsDataSource.suggestedRoomCellDataArray.count; + return recentsDataSource.totalVisibleItemCount == 0; } #pragma mark - SpaceMembersCoordinatorBridgePresenterDelegate diff --git a/Riot/Modules/Home/Views/RoomCollectionViewCell.m b/Riot/Modules/Home/Views/RoomCollectionViewCell.m index f8986da55..a030b9a6f 100644 --- a/Riot/Modules/Home/Views/RoomCollectionViewCell.m +++ b/Riot/Modules/Home/Views/RoomCollectionViewCell.m @@ -101,8 +101,8 @@ } // Notify unreads and bing - if (roomCellData.roomSummary.room.summary.membership == MXMembershipInvite - || roomCellData.roomSummary.room.sentStatus != RoomSentStatusOk) + if (roomCellData.roomSummary.membership == MXMembershipInvite + || roomCellData.roomSummary.sentStatus != MXRoomSummarySentStatusOk) { self.badgeLabel.hidden = NO; self.badgeLabel.badgeColor = ThemeService.shared.theme.noticeColor; @@ -130,14 +130,10 @@ } - if (roomCellData.roomSummary) - { - [self.roomAvatar vc_setRoomAvatarImageWith:roomCellData.roomSummary.avatar 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.avatarUrl + roomId:roomCellData.roomIdentifier + displayName:roomCellData.roomDisplayname + mediaManager:roomCellData.mxSession.mediaManager]; } } @@ -176,11 +172,7 @@ - (NSString*)roomId { - if (roomCellData) - { - return roomCellData.spaceChildInfo ? roomCellData.spaceChildInfo.childRoomId : roomCellData.roomSummary.roomId; - } - return nil; + return roomCellData.roomIdentifier; } @end 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/People/PeopleViewController.m b/Riot/Modules/People/PeopleViewController.m index 1072e7742..ed9aaca76 100644 --- a/Riot/Modules/People/PeopleViewController.m +++ b/Riot/Modules/People/PeopleViewController.m @@ -165,24 +165,6 @@ } } -- (BOOL)shouldShowEmptyView -{ - // Do not present empty screen while searching - if (recentsDataSource.searchPatternsList.count) - { - return NO; - } - - return [self totalItemCounts] == 0; -} - -// Total items to display on the screen -- (NSUInteger)totalItemCounts -{ - return recentsDataSource.invitesCellDataArray.count - + recentsDataSource.conversationCellDataArray.count; -} - #pragma mark - SpaceMembersCoordinatorBridgePresenterDelegate - (void)spaceMembersCoordinatorBridgePresenterDelegateDidComplete:(SpaceMembersCoordinatorBridgePresenter *)coordinatorBridgePresenter diff --git a/Riot/Modules/People/Views/InviteRecentTableViewCell+ButtonViewsUpdate.swift b/Riot/Modules/People/Views/InviteRecentTableViewCell+ButtonViewsUpdate.swift index 7c5ce4825..27d319da7 100644 --- a/Riot/Modules/People/Views/InviteRecentTableViewCell+ButtonViewsUpdate.swift +++ b/Riot/Modules/People/Views/InviteRecentTableViewCell+ButtonViewsUpdate.swift @@ -26,8 +26,8 @@ extension InviteRecentTableViewCell { } /// Update buttons according to current MXMembershipChangeState of the room - @objc func updateButtonViews(with room: MXRoom) { - let membershipTransitionState = room.summary.membershipTransitionState + @objc func updateButtonViews(with summary: MXRoomSummaryProtocol) { + let membershipTransitionState = summary.membershipTransitionState var joinButtonIsLoading = false var leaveButtonIsLoading = false diff --git a/Riot/Modules/People/Views/InviteRecentTableViewCell.m b/Riot/Modules/People/Views/InviteRecentTableViewCell.m index aa691a871..738c8cb4a 100644 --- a/Riot/Modules/People/Views/InviteRecentTableViewCell.m +++ b/Riot/Modules/People/Views/InviteRecentTableViewCell.m @@ -87,7 +87,7 @@ NSString *const kInviteRecentTableViewCellRoomKey = @"kInviteRecentTableViewCell { if (self.delegate) { - MXRoom *room = roomCellData.roomSummary.room; + MXRoom *room = [roomCellData.mxSession roomWithRoomId:roomCellData.roomIdentifier]; if (room) { @@ -99,16 +99,11 @@ NSString *const kInviteRecentTableViewCellRoomKey = @"kInviteRecentTableViewCell - (void)render:(MXKCellData *)cellData { [super render:cellData]; - - MXRoom *room = roomCellData.roomSummary.room; - if (room.roomId) - { - [self updateViewsWithRoom:room showPreviewButton:NO]; - } + [self updateViewsWithRoom:roomCellData.roomSummary showPreviewButton:NO]; } -- (void)updateViewsWithRoom:(MXRoom*)room showPreviewButton:(BOOL)showPreviewButton +- (void)updateViewsWithRoom:(id)room showPreviewButton:(BOOL)showPreviewButton { NSString *rightButtonTitle; 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..dd3ca7d2f --- /dev/null +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -0,0 +1,238 @@ +// 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, handleUniversalLinkWith parameters: UniversalLinkParameters) -> Bool { + return AppDelegate.theDelegate().handleUniversalLink(with: parameters) + } +} 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+Spaces.swift b/Riot/Modules/Room/RoomViewController+Spaces.swift deleted file mode 100644 index f85197cd8..000000000 --- a/Riot/Modules/Room/RoomViewController+Spaces.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -/// this extension is temprorary and implements navigation to the Space bootom sheet. This should be moved to an universal link flow coordinator -extension RoomViewController { - @objc func handleSpaceUniversalLink(with url: URL) { - let url = Tools.fixURL(withSeveralHashKeys: url) - - var pathParamsObjc: NSArray? - var queryParamsObjc: NSMutableDictionary? - AppDelegate.theDelegate().parseUniversalLinkFragment(url?.fragment, outPathParams: &pathParamsObjc, outQueryParams: &queryParamsObjc) - - // Sanity check - guard let pathParams = pathParamsObjc as? [String], pathParams.count > 0 else { - MXLog.error("[RoomViewController] Universal link: Error: No path parameters") - return - } - - var roomIdOrAliasParam: String? - var eventIdParam: String? - var userIdParam: String? - var groupIdParam: String? - - // Check permalink to room or event - if pathParams[0] == "room" && pathParams.count >= 2 { - - // The link is the form of "/room/[roomIdOrAlias]" or "/room/[roomIdOrAlias]/[eventId]" - roomIdOrAliasParam = pathParams[1] - - // Is it a link to an event of a room? - eventIdParam = pathParams.count >= 3 ? pathParams[2] : nil - - } else if pathParams[0] == "group" && pathParams.count >= 2 { - - // The link is the form of "/group/[groupId]" - groupIdParam = pathParams[1] - - } else if (pathParams[0].hasPrefix("#") || pathParams[0].hasPrefix("!")) && pathParams.count >= 1 { - - // The link is the form of "/#/[roomIdOrAlias]" or "/#/[roomIdOrAlias]/[eventId]" - // Such links come from matrix.to permalinks - roomIdOrAliasParam = pathParams[0] - eventIdParam = pathParams.count >= 2 ? pathParams[1] : nil - - } else if pathParams[0] == "user" && pathParams.count == 2 { // Check permalink to a user - // The link is the form of "/user/userId" - userIdParam = pathParams[1] - } else if pathParams[0].hasPrefix("@") && pathParams.count == 1 { - // The link is the form of "/#/[userId]" - // Such links come from matrix.to permalinks - userIdParam = pathParams[0] - } - - guard let roomIdOrAlias = roomIdOrAliasParam else { - AppDelegate.theDelegate().handleUniversalLinkURL(url) - return - } - - self.startActivityIndicator() - - var viaServers: [String] = [] - if let queryParams = queryParamsObjc as? [String: Any], let via = queryParams["via"] as? [String] { - viaServers = via - } - - if roomIdOrAlias.hasPrefix("#") { - self.mainSession.matrixRestClient.roomId(forRoomAlias: roomIdOrAlias) { [weak self] response in - guard let self = self else { - return - } - - guard let roomId = response.value else { - self.stopActivityIndicator() - - if response.error != nil { - let errorMessage = VectorL10n.roomDoesNotExist(roomIdOrAlias) - AppDelegate.theDelegate().showAlert(withTitle: nil, message: errorMessage) - } - return - } - - self.requestSummaryAndShowSpaceDetail(forRoomWithId: roomId, via: viaServers, from: url) - } - } else { - self.requestSummaryAndShowSpaceDetail(forRoomWithId: roomIdOrAlias, via: viaServers, from: url) - } - } - - private func requestSummaryAndShowSpaceDetail(forRoomWithId roomId: String, via: [String], from url: URL?) { - if self.mainSession.spaceService.getSpace(withId: roomId) != nil { - self.stopActivityIndicator() - self.showSpaceDetail(withId: roomId) - return - } - - self.mainSession.matrixRestClient.roomSummary(with: roomId, via: via) { [weak self] response in - guard let self = self else { - return - } - - self.stopActivityIndicator() - - guard let publicRoom = response.value, publicRoom.roomTypeString == MXRoomTypeString.space.rawValue else { - AppDelegate.theDelegate().handleUniversalLinkURL(url) - return - } - - self.showSpaceDetail(with: publicRoom) - } - } - -} diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 7fa73286a..5016dec0f 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -28,6 +28,7 @@ #import "UIViewController+RiotSearch.h" @class BadgeLabel; +@class UniversalLinkParameters; @protocol RoomViewControllerDelegate; NS_ASSUME_NONNULL_BEGIN @@ -86,10 +87,6 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification; */ - (void)displayRoomPreview:(RoomPreviewData*)roomPreviewData; -- (void)showSpaceDetailWithPublicRoom:(MXPublicRoom *)publicRoom; - -- (void)showSpaceDetailWithId:(NSString *)spaceId; - /** Action used to handle some buttons. */ @@ -170,26 +167,14 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification; - (void)roomViewControllerPreviewDidTapCancel:(RoomViewController *)roomViewController; /** - Handle the fragment of a universal link. + Process universal link. @param roomViewController the `RoomViewController` instance. - @param fragment the fragment part of the universal link. - @param universalLinkURL the unprocessed the universal link URL (optional). - @return true to indicate that the fragment has been handled, or false when the fragment is not supported. - */ -- (BOOL)roomViewController:(RoomViewController *)roomViewController -handleUniversalLinkFragment:(NSString*)fragment - fromURL:(nullable NSURL*)universalLinkURL; - -/** - Process universal link. - - @param roomViewController the `RoomViewController` instance. - @param universalLinkURL the universal link URL. + @param parameters the universal link parameters. @return YES in case of processing success. */ - (BOOL)roomViewController:(RoomViewController *)roomViewController - handleUniversalLinkURL:(NSURL*)universalLinkURL; +handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters; @end diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 7fc46819a..5b4ad0c2b 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -16,6 +16,8 @@ limitations under the License. */ +@import MobileCoreServices; + #import "RoomViewController.h" #import "RoomDataSource.h" @@ -106,6 +108,7 @@ #import "AvatarGenerator.h" #import "Tools.h" #import "WidgetManager.h" +#import "ShareManager.h" #import "GBDeviceInfo_iOS.h" @@ -137,7 +140,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @interface RoomViewController () + RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate> { // The preview header @@ -249,6 +252,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, strong) VoiceMessageController *voiceMessageController; @property (nonatomic, strong) SpaceDetailPresenter *spaceDetailPresenter; +@property (nonatomic, strong) ShareManager *shareManager; + +@property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator; +@property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView; + @end @implementation RoomViewController @@ -410,6 +418,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 +460,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self userInterfaceThemeDidChange]; }]; + [self userInterfaceThemeDidChange]; // Observe URL preview updates. @@ -1016,6 +1028,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 @@ -2184,31 +2202,57 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; return [[AppDelegate theDelegate] showAlertWithTitle:title message:message]; } +- (ScreenPresentationParameters*)buildUniversalLinkPresentationParameters +{ + return [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:BuildSettings.allowSplitViewDetailsScreenStacking sender:self sourceView:nil]; +} + - (BOOL)handleUniversalLinkURL:(NSURL*)universalLinkURL { - if (self.delegate) - { - return [self.delegate roomViewController:self handleUniversalLinkURL:universalLinkURL]; - } - else - { - [self handleSpaceUniversalLinkWith:universalLinkURL]; - return YES; - } + UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithUniversalLinkURL:universalLinkURL presentationParameters:[self buildUniversalLinkPresentationParameters]]; + return [self handleUniversalLinkWithParameters:parameters]; } - + - (BOOL)handleUniversalLinkFragment:(NSString*)fragment fromURL:(NSURL*)universalLinkURL +{ + UniversalLinkParameters *parameters = [[UniversalLinkParameters alloc] initWithFragment:fragment + universalLinkURL:universalLinkURL presentationParameters:[self buildUniversalLinkPresentationParameters]]; + return [self handleUniversalLinkWithParameters:parameters]; +} + +- (BOOL)handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters { if (self.delegate) { - return [self.delegate roomViewController:self handleUniversalLinkFragment:fragment fromURL:universalLinkURL]; + return [self.delegate roomViewController:self handleUniversalLinkWithParameters:parameters]; } else { - return [[AppDelegate theDelegate] handleUniversalLinkFragment:fragment fromURL:universalLinkURL]; + return [[AppDelegate theDelegate] handleUniversalLinkWithParameters:parameters]; } } +- (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 @@ -2360,10 +2404,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Set a default title view class without handling tap gesture (Let [self refreshRoomTitle] refresh this view correctly). [self setRoomTitleViewClass:RoomTitleView.class]; - - // Remove details icon - RoomTitleView *roomTitleView = (RoomTitleView*)self.titleView; - + // Remove the shadow image used to hide the bottom border of the navigation bar when the preview header is displayed [mainNavigationController.navigationBar setShadowImage:nil]; [mainNavigationController.navigationBar setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault]; @@ -3156,6 +3197,23 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } + [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + self.shareManager = [[ShareManager alloc] initWithShareItemProvider:[[SimpleShareItemProvider alloc] initWithTextMessage:selectedComponent.textMessage] + type:ShareManagerTypeForward]; + + MXWeakify(self); + [self.shareManager setCompletionCallback:^(ShareManagerResult result) { + MXStrongifyAndReturnIfNil(self); + [attachment onShareEnded]; + [self dismissViewControllerAnimated:YES completion:nil]; + self.shareManager = nil; + }]; + + [self presentViewController:self.shareManager.mainViewController animated:YES completion:nil]; + }]]; + if (!isJitsiCallEvent) { [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionQuote] @@ -3209,6 +3267,29 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } else // Add action for attachment { + if (attachment.type == MXKAttachmentTypeFile || + attachment.type == MXKAttachmentTypeImage || + attachment.type == MXKAttachmentTypeVideo || + attachment.type == MXKAttachmentTypeVoiceMessage) { + + [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + self.shareManager = [[ShareManager alloc] initWithShareItemProvider:[[SimpleShareItemProvider alloc] initWithAttachment:attachment] + type:ShareManagerTypeForward]; + + MXWeakify(self); + [self.shareManager setCompletionCallback:^(ShareManagerResult result) { + MXStrongifyAndReturnIfNil(self); + [attachment onShareEnded]; + [self dismissViewControllerAnimated:YES completion:nil]; + self.shareManager = nil; + }]; + + [self presentViewController:self.shareManager.mainViewController animated:YES completion:nil]; + }]]; + } + if (BuildSettings.messageDetailsAllowSave) { if (attachment.type == MXKAttachmentTypeImage || attachment.type == MXKAttachmentTypeVideo) @@ -3306,7 +3387,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self cancelEventSelection]; + [self startActivityIndicator]; + [attachment prepareShare:^(NSURL *fileURL) { + [self stopActivityIndicator]; __strong __typeof(weakSelf)self = weakSelf; self->documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:fileURL]; @@ -3321,10 +3405,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } } failure:^(NSError *error) { - - //Alert user [self showError:error]; - + [self stopActivityIndicator]; }]; // Start animation in case of download during attachment preparing @@ -4197,6 +4279,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { [self cancelEventSelection]; } + +- (void)roomInputToolbarViewDidChangeTextMessage:(MXKRoomInputToolbarView *)toolbarView +{ + [self.userSuggestionCoordinator processTextMessage:toolbarView.textMessage]; +} #pragma mark - MXKRoomMemberDetailsViewControllerDelegate @@ -5031,14 +5118,14 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; -(BOOL)checkUnsentMessages { - RoomSentStatus sentStatus = RoomSentStatusOk; + MXRoomSummarySentStatus sentStatus = MXRoomSummarySentStatusOk; if ([self.activitiesView isKindOfClass:RoomActivitiesView.class]) { - sentStatus = self.roomDataSource.room.sentStatus; + sentStatus = self.roomDataSource.room.summary.sentStatus; - if (sentStatus != RoomSentStatusOk) + if (sentStatus != MXRoomSummarySentStatusOk) { - NSString *notification = sentStatus == RoomSentStatusSentFailedDueToUnknownDevices ? + NSString *notification = sentStatus == MXRoomSummarySentStatusSentFailedDueToUnknownDevices ? [VectorL10n roomUnsentMessagesUnknownDevicesNotification] : [VectorL10n roomUnsentMessagesNotification]; @@ -5109,7 +5196,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } } - return sentStatus != RoomSentStatusOk; + return sentStatus != MXRoomSummarySentStatusOk; } - (void)eventDidChangeSentState:(NSNotification *)notif @@ -6481,20 +6568,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]; } -- (void)showSpaceDetailWithPublicRoom:(MXPublicRoom *)publicRoom -{ - self.spaceDetailPresenter = [SpaceDetailPresenter new]; - self.spaceDetailPresenter.delegate = self; - [self.spaceDetailPresenter presentForSpaceWithPublicRoom:publicRoom from:self sourceView:nil session:self.mainSession animated:YES]; -} - -- (void)showSpaceDetailWithId:(NSString *)spaceId -{ - self.spaceDetailPresenter = [SpaceDetailPresenter new]; - self.spaceDetailPresenter.delegate = self; - [self.spaceDetailPresenter presentForSpaceWithId:spaceId from:self sourceView:nil session:self.mainSession animated:YES]; -} - #pragma mark - SpaceDetailPresenterDelegate - (void)spaceDetailPresenterDidComplete:(SpaceDetailPresenter *)presenter @@ -6514,4 +6587,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 @@