diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml index f2fe6125c1..2c932c52fe 100644 --- a/.github/workflows/release-alpha.yml +++ b/.github/workflows/release-alpha.yml @@ -90,11 +90,9 @@ jobs: - name: Build Ad-hoc release and send it to Diawi run: bundle exec fastlane alpha env: - # Automatically bypass 2FA upgrade if possible on Apple account. - SPACESHIP_SKIP_2FA_UPGRADE: true - APPLE_ID: ${{ secrets.FASTLANE_USER }} - FASTLANE_USER: ${{ secrets.FASTLANE_USER }} - FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }} + APPSTORECONNECT_KEY_ID: ${{ secrets.APPSTORECONNECT_KEY_ID }} + APPSTORECONNECT_KEY_ISSUER_ID: ${{ secrets.APPSTORECONNECT_KEY_ISSUER_ID }} + APPSTORECONNECT_KEY_CONTENT: ${{ secrets.APPSTORECONNECT_KEY_CONTENT }} DIAWI_API_TOKEN: ${{ secrets.DIAWI_API_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml index 632e8b5382..a5cd493289 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-move-labelled.yml @@ -59,8 +59,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -83,8 +83,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -107,8 +107,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -130,8 +130,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -152,8 +152,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -175,8 +175,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -198,8 +198,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!,$contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } diff --git a/.github/workflows/triage-review-requests.yml b/.github/workflows/triage-review-requests.yml index 8306cffefc..295a18fdf6 100644 --- a/.github/workflows/triage-review-requests.yml +++ b/.github/workflows/triage-review-requests.yml @@ -58,8 +58,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!, $contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } @@ -72,7 +72,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} add_product_pr_to_project: - name: Move PRs asking for design review to the design board + name: Move PRs asking for product review to the product board runs-on: ubuntu-latest steps: - uses: octokit/graphql-action@v2.x @@ -125,8 +125,8 @@ jobs: headers: '{"GraphQL-Features": "projects_next_graphql"}' query: | mutation add_to_project($projectid:ID!, $contentid:ID!) { - addProjectNextItem(input:{projectId:$projectid contentId:$contentid}) { - projectNextItem { + addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) { + item { id } } diff --git a/CHANGES.md b/CHANGES.md index b40812f791..15c1ca2e63 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,48 @@ +## Changes in 1.9.12 (2022-11-15) + +✨ Features + +- Threads: added support to read receipts (MSC3771) ([#6663](https://github.com/vector-im/element-ios/issues/6663)) +- Threads: added support to notifications count (MSC3773) ([#6664](https://github.com/vector-im/element-ios/issues/6664)) +- Threads: added support to labs flag for read receipts ([#7029](https://github.com/vector-im/element-ios/issues/7029)) +- Threads: notification count in main timeline including un participated threads ([#7038](https://github.com/vector-im/element-ios/issues/7038)) +- Unverified sessions alert. ([#7056](https://github.com/vector-im/element-ios/issues/7056)) +- Labs: Rich-text editor: enable translations between Markdown and HTML when toggling text formatting ([#7061](https://github.com/vector-im/element-ios/issues/7061)) + +🙌 Improvements + +- Add informational sheets for user's session states. ([#6992](https://github.com/vector-im/element-ios/pull/6992)) +- Add the sign out option in the menu in the session overview. ([#7001](https://github.com/vector-im/element-ios/pull/7001)) +- Add show/hide sessions' ip address in the new session manager. ([#7028](https://github.com/vector-im/element-ios/pull/7028)) +- Updated GBDeviceInfo pod. ([#7051](https://github.com/vector-im/element-ios/pull/7051)) +- Improve device manager code coverage. ([#7065](https://github.com/vector-im/element-ios/pull/7065)) +- Initial sync: Remove 10s wait on failed initial sync ([#7068](https://github.com/vector-im/element-ios/pull/7068)) +- Upgrade MatrixSDK version ([v0.24.3](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.3)). +- Labs: Rich text-editor - Add support for plain text mode ([#6980](https://github.com/vector-im/element-ios/issues/6980)) + +🐛 Bugfixes + +- Prevent autolayout crashes when showing toast notifications ([#7046](https://github.com/vector-im/element-ios/pull/7046)) +- Fixed timeline layout issues for reactions and attachments ([#7064](https://github.com/vector-im/element-ios/pull/7064)) +- Rich Text Composer: Voice Dictation is supported (only plain text can be dictated). ([#6945](https://github.com/vector-im/element-ios/issues/6945)) +- Rich Text Composer dismisses the keyboard when sending custom iOS emojis as images, like the normal composer. ([#6946](https://github.com/vector-im/element-ios/issues/6946)) +- Fixed IRC-style message and commands support in Rich text editor ([#6962](https://github.com/vector-im/element-ios/issues/6962)) +- Fixed the missing keystrokes issue on the Rich Text Editor ([#7005](https://github.com/vector-im/element-ios/issues/7005)) +- Fixed the long press deleting issue skipping some text on the Rich Text Editor ([#7006](https://github.com/vector-im/element-ios/issues/7006)) +- Hide push toggles for http pushers when there is no server support. ([#7022](https://github.com/vector-im/element-ios/issues/7022)) +- Synchronise composer and toolbar resizing animation duration for smoother height updates. ([#7025](https://github.com/vector-im/element-ios/issues/7025)) +- Device Manager: Session list item is not tappable everywhere. ([#7035](https://github.com/vector-im/element-ios/issues/7035)) +- Labs: Rich-text editor - Fix text formatting enabled inconsistent state ([#7052](https://github.com/vector-im/element-ios/issues/7052)) +- Labs: Rich-text editor - Fix text formatting switch losing the current content of the composer ([#7054](https://github.com/vector-im/element-ios/issues/7054)) +- Threads: removed "unread_thread_notifications" from sync filters for server that doesn't support MSC3773 ([#7066](https://github.com/vector-im/element-ios/issues/7066)) +- Poll not usable after logging out and back in. ([#7070](https://github.com/vector-im/element-ios/issues/7070)) +- Threads: Display number of unread messages above threads button ([#7076](https://github.com/vector-im/element-ios/issues/7076)) + +🚧 In development 🚧 + +- Device Manager: Multi-session sign out. ([#6963](https://github.com/vector-im/element-ios/issues/6963)) + + ## Changes in 1.9.11 (2022-11-08) 🐛 Bugfixes diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index f0fe260115..ffb7d901a1 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.9.11 -CURRENT_PROJECT_VERSION = 1.9.11 +MARKETING_VERSION = 1.9.12 +CURRENT_PROJECT_VERSION = 1.9.12 diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index aa0de1873d..d6b022948b 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -437,4 +437,7 @@ final class BuildSettings: NSObject { static let qrLoginEnableDisplayingQRs = false static let rendezvousServerBaseURL = URL(string: "https://rendezvous.lab.element.dev/")! + + // MARK: - Alerts + static let showUnverifiedSessionsAlert = true } diff --git a/Gemfile.lock b/Gemfile.lock index 6590decc42..53de8296b9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,7 @@ GEM specs: CFPropertyList (3.0.5) rexml - activesupport (6.1.6.1) + activesupport (6.1.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -17,20 +17,20 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.621.0) - aws-sdk-core (3.134.0) + aws-partitions (1.657.0) + aws-sdk-core (3.166.0) aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.525.0) - aws-sigv4 (~> 1.1) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.58.0) - aws-sdk-core (~> 3, >= 3.127.0) + aws-sdk-kms (1.59.0) + aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.114.0) - aws-sdk-core (~> 3, >= 3.127.0) + aws-sdk-s3 (1.117.1) + aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) - aws-sigv4 (1.5.1) + aws-sigv4 (1.5.2) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) claide (1.1.0) @@ -85,9 +85,9 @@ GEM dotenv (2.8.1) emoji_regex (3.2.3) escape (0.0.4) - ethon (0.15.0) + ethon (0.16.0) ffi (>= 1.15.0) - excon (0.92.4) + excon (0.93.1) faraday (1.10.2) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -117,7 +117,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.209.1) + fastlane (2.210.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -159,7 +159,8 @@ GEM fastlane-plugin-brew (0.1.1) fastlane-plugin-diawi (2.1.0) rest-client (>= 2.0.0) - fastlane-plugin-sentry (1.12.2) + fastlane-plugin-sentry (1.14.0) + os (~> 1.1, >= 1.1.4) fastlane-plugin-versioning (0.5.1) fastlane-plugin-xcodegen (1.1.0) fastlane-plugin-brew (~> 0.1.1) @@ -167,9 +168,9 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.25.0) - google-apis-core (>= 0.7, < 2.a) - google-apis-core (0.7.0) + google-apis-androidpublisher_v3 (0.31.0) + google-apis-core (>= 0.9.1, < 2.a) + google-apis-core (0.9.1) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -178,27 +179,27 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.13.0) - google-apis-core (>= 0.7, < 2.a) - google-apis-playcustomapp_v1 (0.10.0) - google-apis-core (>= 0.7, < 2.a) - google-apis-storage_v1 (0.17.0) - google-apis-core (>= 0.7, < 2.a) + google-apis-iamcredentials_v1 (0.16.0) + google-apis-core (>= 0.9.1, < 2.a) + google-apis-playcustomapp_v1 (0.12.0) + google-apis-core (>= 0.9.1, < 2.a) + google-apis-storage_v1 (0.19.0) + google-apis-core (>= 0.9.0, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.2.0) - google-cloud-storage (1.38.0) + google-cloud-errors (1.3.0) + google-cloud-storage (1.44.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.17.0) + google-apis-storage_v1 (~> 0.19.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.2.0) + googleauth (1.3.0) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -214,7 +215,7 @@ GEM concurrent-ruby (~> 1.0) jmespath (1.6.1) json (2.6.2) - jwt (2.4.1) + jwt (2.5.0) memoist (0.16.2) mime-types (3.4.1) mime-types-data (~> 3.2015) @@ -230,7 +231,7 @@ GEM nap (1.1.0) naturally (2.2.1) netrc (0.11.0) - nokogiri (1.13.8) + nokogiri (1.13.9) mini_portile2 (~> 2.8.0) racc (~> 1.4) optparse (0.1.1) @@ -302,7 +303,7 @@ GEM rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) - zeitwerk (2.6.0) + zeitwerk (2.6.6) PLATFORMS ruby diff --git a/Podfile b/Podfile index d89c32f611..154c119d42 100644 --- a/Podfile +++ b/Podfile @@ -16,7 +16,7 @@ use_frameworks! # - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK 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 -$matrixSDKVersion = '= 0.24.2' +$matrixSDKVersion = '= 0.24.3' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } @@ -66,7 +66,7 @@ end abstract_target 'RiotPods' do - pod 'GBDeviceInfo', '~> 6.6.0' + pod 'GBDeviceInfo', '~> 7.1.0' pod 'Reusable', '~> 4.1' pod 'KeychainAccess', '~> 4.2.2' pod 'WeakDictionary', '~> 2.0' diff --git a/Podfile.lock b/Podfile.lock index 0def666833..d9ebb08f44 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -40,9 +40,9 @@ PODS: - DTFoundation/Core - FLEX (4.5.0) - FlowCommoniOS (1.12.2) - - GBDeviceInfo (6.6.0): - - GBDeviceInfo/Core (= 6.6.0) - - GBDeviceInfo/Core (6.6.0) + - GBDeviceInfo (7.1.0): + - GBDeviceInfo/Core (= 7.1.0) + - GBDeviceInfo/Core (7.1.0) - GZIP (1.3.0) - Introspect (0.1.4) - JitsiMeetSDK (5.0.2) @@ -55,9 +55,9 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixSDK (0.24.2): - - MatrixSDK/Core (= 0.24.2) - - MatrixSDK/Core (0.24.2): + - MatrixSDK (0.24.3): + - MatrixSDK/Core (= 0.24.3) + - MatrixSDK/Core (0.24.3): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) @@ -65,9 +65,9 @@ PODS: - OLMKit (~> 3.2.5) - Realm (= 10.27.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/CryptoSDK (0.24.2): + - MatrixSDK/CryptoSDK (0.24.3): - MatrixSDKCrypto (= 0.1.5) - - MatrixSDK/JingleCallStack (0.24.2): + - MatrixSDK/JingleCallStack (0.24.3): - JitsiMeetSDK (= 5.0.2) - MatrixSDK/Core - MatrixSDKCrypto (0.1.5) @@ -117,13 +117,13 @@ DEPENDENCIES: - DTCoreText (~> 1.6.25) - FLEX (~> 4.5.0) - FlowCommoniOS (~> 1.12.0) - - GBDeviceInfo (~> 6.6.0) + - GBDeviceInfo (~> 7.1.0) - Introspect (~> 0.1) - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.24.2) - - MatrixSDK/JingleCallStack (= 0.24.2) + - MatrixSDK (= 0.24.3) + - MatrixSDK/JingleCallStack (= 0.24.3) - OLMKit - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) @@ -209,7 +209,7 @@ SPEC CHECKSUMS: DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536 FLEX: e51461dd6f0bfb00643c262acdfea5d5d12c596b FlowCommoniOS: ca92071ab526dc89905495a37844fd7e78d1a7f2 - GBDeviceInfo: ed0db16230d2fa280e1cbb39a5a7f60f6946aaec + GBDeviceInfo: 5d62fa85bdcce3ed288d83c28789adf1173e4376 GZIP: 416858efbe66b41b206895ac6dfd5493200d95b3 Introspect: b62c4dd2063072327c21d618ef2bedc3c87bc366 JitsiMeetSDK: edcac8e2b92ee0c7f3e75bd0aefefbe9faccfc93 @@ -220,7 +220,7 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: 1b64384084050652fcffafdf8641200f1ab25060 + MatrixSDK: caa83fd5fa63872295bf868afbd334e0ca67dda2 MatrixSDKCrypto: dcab554bc7157cad31c01fc1137cf5acb01959a4 OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f @@ -241,6 +241,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 96a971e076c61e54ae5bb7bf30ecba80563eeacf +PODFILE CHECKSUM: 154d5d74ff0efd23cb8c97e9ee2a000103270e09 COCOAPODS: 1.11.2 diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index ef28187e4f..816ccb018f 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,7 +23,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "state" : { - "revision" : "d5ef7054fb43924d5b92d5d627347ca2bc333717" + "revision" : "2469f27b7e1e51aaa135e09f9005eb10fda686e6" } }, { diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Contents.json new file mode 100644 index 0000000000..dbee5479fb --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "action_formatting_disabled.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "action_formatting_disabled@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "action_formatting_disabled@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled.png new file mode 100644 index 0000000000..f7ef2b190a Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@2x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@2x.png new file mode 100644 index 0000000000..270dd75d22 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@3x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@3x.png new file mode 100644 index 0000000000..9394656ff4 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/Contents.json new file mode 100644 index 0000000000..198b65f6e3 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "action_formatting_enabled.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "action_formatting_enabled@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "action_formatting_enabled@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled.png new file mode 100644 index 0000000000..06798aaa73 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled@2x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled@2x.png new file mode 100644 index 0000000000..b2aec071e1 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled@3x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled@3x.png new file mode 100644 index 0000000000..414aa321fe Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_enabled.imageset/action_formatting_enabled@3x.png differ diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_live.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_live.imageset/Contents.json new file mode 100644 index 0000000000..9a7c051043 --- /dev/null +++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_live.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "voice_broadcast_tile_live.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_live.imageset/voice_broadcast_tile_live.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_live.imageset/voice_broadcast_tile_live.svg new file mode 100644 index 0000000000..5728fc8d46 --- /dev/null +++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_live.imageset/voice_broadcast_tile_live.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_mic.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_mic.imageset/Contents.json new file mode 100644 index 0000000000..eeeba86aef --- /dev/null +++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_mic.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "voice_broadcast_tile_mic.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_mic.imageset/voice_broadcast_tile_mic.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_mic.imageset/voice_broadcast_tile_mic.svg new file mode 100644 index 0000000000..2f3deca13b --- /dev/null +++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_tile_mic.imageset/voice_broadcast_tile_mic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 5e7c7dee16..db34abd415 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -1628,7 +1628,7 @@ "room_event_action_view_in_room" = "Im Raum anzeigen"; "location_sharing_open_open_street_maps" = "In OpenStreetMap öffnen"; "onboarding_use_case_title" = "Mit wem wirst du am meisten schreiben?"; -"onboarding_use_case_message" = "Wir helfen dir, dich zu verbinden"; +"onboarding_use_case_message" = "Wir helfen dir, dich zu vernetzen"; "onboarding_use_case_personal_messaging" = "Freunde und Familie"; "onboarding_use_case_work_messaging" = "Teams"; "onboarding_use_case_community_messaging" = "Gemeinschaften"; @@ -2638,7 +2638,7 @@ // Send Media Actions "wysiwyg_composer_start_action_media_picker" = "Fotobibliothek"; -"settings_labs_enable_wysiwyg_composer" = "Probiere den Rich-Text-Editor aus (bald auch mit Plain-Text-Modus)"; +"settings_labs_enable_wysiwyg_composer" = "Probiere den Textverarbeitungs-Editor aus"; "wysiwyg_composer_start_action_voice_broadcast" = "Sprachübertragung"; "voice_broadcast_already_in_progress_message" = "Du zeichnest bereits eine Sprachübertragung auf. Bitte beende die laufende Übertragung, um eine neue zu beginnen."; "voice_broadcast_blocked_by_someone_else_message" = "Jemand anderes nimmt bereits eine Sprachübertragung auf. Warte auf das Ende der Übertragung, bevor du eine neue startest."; @@ -2651,3 +2651,22 @@ "deselect_all" = "Alle abwählen"; "user_other_session_menu_select_sessions" = "Sitzungen auswählen"; "user_other_session_selected_count" = "%@ ausgewählt"; +"user_other_session_menu_sign_out_sessions" = "Von %@ Sitzungen abmelden"; +"manage_session_sign_out_other_sessions" = "Von allen anderen Sitzungen abmelden"; +"user_sessions_hide_location_info" = "IP-Adresse ausblenden"; +"user_sessions_show_location_info" = "IP-Adresse anzeigen"; +"voice_broadcast_tile" = "Sprachübertragung"; +"voice_broadcast_live" = "Live"; +"user_session_verified_session_description" = "Auf verifizierte Sitzungen kannst du überall mit Element zugreifen, wenn du deine Passphrase eingegeben oder Element mit einer anderen Sitzung verifiziert hast.\n\nDies bedeutet, dass du alle Schlüssel zum Entsperren deiner verschlüsselten Nachrichten hast und anderen bestätigst, dieser Sitzung zu vertrauen."; +"user_session_got_it" = "Verstanden"; +"user_session_rename_session_description" = "Andere Nutzer in Direktnachrichten und Räumen, in denen du dich befindest, können eine vollständige Liste deiner Sitzungen einsehen.\n\nDies gibt ihnen die Sicherheit, dass sie auch wirklich mit dir kommunizieren. Allerdings bedeutet es auch, dass sie die Sitzungsnamen sehen können, die du hier angibst."; +"user_session_rename_session_title" = "Sitzungen umbenennen"; +"user_session_inactive_session_description" = "Inaktive Sitzungen sind jene, die du einige Zeit nicht mehr verwendet hast, die aber weiterhin Verschlüsselungs-Schlüssel erhalten.\n\nDas Löschen von inaktiven Sitzungen verbessert Sicherheit und Leistung, und hilft dir zu erkennen, ob eine neue Sitzung verdächtig ist."; +"user_session_inactive_session_title" = "Inaktive Sitzungen"; +"user_session_unverified_session_description" = "Nicht verifizierte Sitzungen sind jene, die angemeldet, aber nicht quer signiert sind.\n\nDu solltest besonders sicherstellen, dass du diese Sitzungen erkennst, da sie auf eine nicht autorisierte Nutzung deines Kontos hindeuten könnten."; +"user_session_unverified_session_title" = "Nicht verifizierte Sitzungen"; +"user_session_verified_session_title" = "Verifizierte Sitzungen"; +"key_verification_alert_body" = "Überprüfe sie, um ein sicheres Konto gewährleisten zu können."; + +// Unverified sessions +"key_verification_alert_title" = "Du hast nicht verifizierte Sitzungen"; diff --git a/Riot/Assets/en.lproj/Untranslated.strings b/Riot/Assets/en.lproj/Untranslated.strings index 6d9320f4a1..9ff00a53dc 100644 --- a/Riot/Assets/en.lproj/Untranslated.strings +++ b/Riot/Assets/en.lproj/Untranslated.strings @@ -19,4 +19,3 @@ // MARK: Onboarding Personalization WIP "image_picker_action_files" = "Choose from files"; -"voice_broadcast_in_timeline_title" = "Voice broadcast detected (under active development)"; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 1f04c58a2f..fa68764c3e 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -797,7 +797,7 @@ Tap the + to start adding people."; "settings_labs_enable_new_session_manager" = "New session manager"; "settings_labs_enable_new_client_info_feature" = "Record the client name, version, and url to recognise sessions more easily in session manager"; "settings_labs_enable_new_app_layout" = "New Application Layout"; -"settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor (plain text mode coming soon)"; +"settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor"; "settings_labs_enable_voice_broadcast" = "Voice broadcast (under active development)"; "settings_version" = "Version %@"; @@ -936,7 +936,7 @@ Tap the + to start adding people."; "manage_session_not_trusted" = "Not trusted"; "manage_session_sign_out" = "Sign out of this session"; "manage_session_rename" = "Rename session"; - +"manage_session_sign_out_other_sessions" = "Sign out of all other sessions"; // User sessions management "user_sessions_settings" = "Manage sessions"; @@ -1548,9 +1548,8 @@ Tap the + to start adding people."; "key_verification_self_verify_current_session_alert_validate_action" = "Verify"; // Unverified sessions - -"key_verification_self_verify_unverified_sessions_alert_title" = "Review where you're logged in"; -"key_verification_self_verify_unverified_sessions_alert_message" = "Verify all your sessions to ensure your account & messages are safe."; +"key_verification_alert_title" = "You have unverified sessions"; +"key_verification_alert_body" = "Review to ensure your account is safe."; "key_verification_self_verify_unverified_sessions_alert_validate_action" = "Review"; // MARK: Self verification wait @@ -2195,6 +2194,8 @@ Tap the + to start adding people."; "voice_broadcast_blocked_by_someone_else_message" = "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one."; "voice_broadcast_already_in_progress_message" = "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one."; "voice_broadcast_playback_loading_error" = "Unable to play this voice broadcast."; +"voice_broadcast_live" = "Live"; +"voice_broadcast_tile" = "Voice broadcast"; // Mark: - Version check @@ -2421,6 +2422,8 @@ To enable access, tap Settings> Location and select Always"; "user_sessions_overview_other_sessions_section_title" = "Other sessions"; "user_sessions_overview_other_sessions_section_info" = "For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore."; +"user_sessions_show_location_info" = "Show IP address"; +"user_sessions_hide_location_info" = "Hide IP address"; "user_sessions_overview_current_session_section_title" = "Current session"; "user_sessions_overview_link_device" = "Link a device"; @@ -2444,6 +2447,15 @@ To enable access, tap Settings> Location and select Always"; "user_other_session_verified_additional_info" = "This session is ready for secure messaging."; "user_session_push_notifications" = "Push notifications"; "user_session_push_notifications_message" = "When turned on, this session will receive push notifications."; +"user_session_got_it" = "Got it"; +"user_session_verified_session_title" = "Verified sessions"; +"user_session_verified_session_description" = "Verified sessions are anywhere you are using Element after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session."; +"user_session_unverified_session_title" = "Unverified session"; +"user_session_unverified_session_description" = "Unverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account."; +"user_session_inactive_session_title" = "Inactive sessions"; +"user_session_inactive_session_description" = "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.\n\nRemoving inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious."; +"user_session_rename_session_title" = "Renaming sessions"; +"user_session_rename_session_description" = "Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here."; "user_other_session_security_recommendation_title" = "Security recommendation"; "user_other_session_unverified_sessions_header_subtitle" = "Verify your sessions for enhanced secure messaging or sign out from those you don’t recognize or use anymore."; @@ -2462,6 +2474,7 @@ To enable access, tap Settings> Location and select Always"; "user_other_session_clear_filter" = "Clear filter"; "user_other_session_selected_count" = "%@ selected"; "user_other_session_menu_select_sessions" = "Select sessions"; +"user_other_session_menu_sign_out_sessions" = "Sign out of %@ sessions"; // First item is client name and second item is session display name "user_session_name" = "%@: %@"; diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 1dd7c1b42a..fd122e8474 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2575,7 +2575,7 @@ "manage_session_name_info" = "Palun arvesta, et sessioonide nimed on näha ka kõikidele osapooltele, kellega sa suhtled. %@"; "manage_session_name_hint" = "Sinu enda kirjutatud sessiooninimede alusel on sul oma seadmeid lihtsam ära tunda."; "settings_labs_enable_voice_broadcast" = "Ringhäälingukõne (aktiivses arenduses)"; -"settings_labs_enable_wysiwyg_composer" = "Proovi vormindatud teksti alusel töötavat tekstitoimetit (varsti lisandub ka vormindamata teksti režiim)"; +"settings_labs_enable_wysiwyg_composer" = "Proovi vormindatud teksti alusel töötavat tekstitoimetit"; "authentication_qr_login_failure_retry" = "Proovi uuesti"; "authentication_qr_login_failure_request_timed_out" = "Sidumine ei lõppenud etteantud aja jooksul."; "authentication_qr_login_failure_request_denied" = "Teine seade lükkas päringu tagasi."; @@ -2589,3 +2589,22 @@ "deselect_all" = "Eemalda kõik valikud"; "user_other_session_menu_select_sessions" = "Vali sessioonid"; "user_other_session_selected_count" = "%@ valitud"; +"user_other_session_menu_sign_out_sessions" = "Logi välja %@'st sessioonist"; +"user_sessions_hide_location_info" = "Peida IP-aadress"; +"user_sessions_show_location_info" = "Näita IP-aadressi"; +"manage_session_sign_out_other_sessions" = "Logi välja kõikidest oma muudest sessioonidest"; +"voice_broadcast_tile" = "Ringhäälingukõne"; +"voice_broadcast_live" = "Otse eetris"; +"user_session_rename_session_description" = "Teised osapooled nii otsesõnumites kui jututubades näevad sinu kõikide sessioonide loendit.\n\nSee tähendab, et nad võivad uskuda, et tegemist on tõesti sinuga. Samal ajal näevad ka siin sisestatud sessiooninime."; +"user_session_unverified_session_description" = "Verifitseerimata sessioonid on sellised, kuhu sa oled oma kasutajanime ja salasõnaga sisse loginud, kuid mille puhul on risttunnustamine tegemata.\n\nPalun kontrolli, et need on sulle teadaolevad sessioonid. Vastasel korral võib olla tegemist sinu kasutajakonto lubamatu kasutamisega."; +"user_session_rename_session_title" = "Sessioonide nimede muutmine"; +"user_session_inactive_session_description" = "Mitteaktiivsed sessioonid on sellised, mida sa pole mõnda aega kasutanud, aga nad saavad jätkuvalt pärida sinu krüptovõtmeid.\n\nSelliste sessioonide eemaldamine parandab jõudlust ja turvalisust ning sul on lihtsam märgata, kui loendisse tekib midagi kahtlast."; +"user_session_inactive_session_title" = "Mitteaktiivsed sessioonid"; +"user_session_unverified_session_title" = "Verifitseerimata sessioon"; +"user_session_verified_session_description" = "Verifitseeritud sessioonideks loetakse Element'is või mõnes muus Matrix'i rakenduses selliseid sessioone, kus sa kas oled sisestanud oma salafraasi või tuvastanud end mõne teise oma verifitseeritud sessiooni abil.\n\nSee tähendab, et selles sessioonis on ka kõik vajalikud võtmed krüptitud sõnumite lugemiseks ja teistele kasutajatele kinnitamiseks, et sa usaldad seda sessiooni."; +"user_session_verified_session_title" = "Verifitseeritud sessioonid"; +"user_session_got_it" = "Selge lugu"; +"key_verification_alert_body" = "Tagamaks, et su konto on sinu kontrolli all, vaata andmed üle."; + +// Unverified sessions +"key_verification_alert_title" = "Sul on verifitseerimata sessioone"; diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index bb02d69cd3..4f371613b8 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -2605,3 +2605,13 @@ "authentication_qr_login_start_subtitle" = "Utilisez l’appareil photo de cet appareil pour scanner le QR code affiché sur votre autre appareil :"; "authentication_qr_login_start_title" = "Scanner le QR code"; "authentication_login_with_qr" = "Se connecter avec un QR code"; +"key_verification_alert_body" = "Passez les en revue pour vous assurer que votre compte n’est pas compromis."; + +// Unverified sessions +"key_verification_alert_title" = "Vous avez des sessions non-vérifiées"; +"manage_session_sign_out_other_sessions" = "Déconnecter toutes les autres sessions"; +"manage_session_name_info_link" = "Plus"; +/* The placeholder will be replaces with manage_session_name_info_link */ +"manage_session_name_info" = "Gardez en tête que les noms des sessions sont aussi visibles par les personnes avec qui vous communiquez. %@"; +"manage_session_name_hint" = "Personnaliser les noms des sessions peut vous aider à reconnaître vos appareils plus facilement."; +"settings_labs_enable_wysiwyg_composer" = "Essayez le compositeur de messages visuel"; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 1f72aab670..78e52a6fd8 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2597,7 +2597,7 @@ /* The placeholder will be replaces with manage_session_name_info_link */ "manage_session_name_info" = "Fontos, hogy a munkamenet neve a kommunikációban résztvevők számára látható. %@"; "manage_session_name_hint" = "Az egyedi munkamenet név segíthet az eszköz könnyebb felismerésében."; -"settings_labs_enable_wysiwyg_composer" = "Próbálja ki az új szövegbevitelt (hamarosan érkezik a sima szöveges üzemmód)"; +"settings_labs_enable_wysiwyg_composer" = "Próbálja ki az új szövegbevitelt"; "settings_labs_enable_new_client_info_feature" = "Kliens neve, verziója és url felvétele a munkamenet könnyebb azonosításához a munkamenet kezelőben"; "settings_labs_enable_new_session_manager" = "Új munkamenet kezelő"; "authentication_qr_login_failure_retry" = "Próbáld újra"; @@ -2637,3 +2637,22 @@ "deselect_all" = "Semmit nem jelöl ki"; "user_other_session_menu_select_sessions" = "Munkamenetek kiválasztása"; "user_other_session_selected_count" = "%@ kiválasztva"; +"user_other_session_menu_sign_out_sessions" = "Kijelentkezés %@ munkamenetből"; +"user_sessions_hide_location_info" = "IP címek elrejtése"; +"user_sessions_show_location_info" = "IP címek megjelenítése"; +"manage_session_sign_out_other_sessions" = "Kijelentkezés minden más munkamenetből"; +"user_session_verified_session_description" = "Mindenhol ellenőrzött munkamenetek vannak ahol Elementet használva megadtad a jelmondatodat vagy egy másik már hitelesített munkamenetből megerősítetted az identitásodat.\n\nEz azt jelenti, hogy a titkosított üzenetek visszafejtéséhez rendelkezel a kulcsokkal és megerősíted a többiek felé, hogy megbízol a munkamenetben."; + +// Unverified sessions +"key_verification_alert_title" = "Ellenőrizetlen bejelentkezései vannak"; +"user_session_rename_session_description" = "Más felhasználók akikkel közvetlenül vagy szobában beszélgetsz látják a teljes listát a munkameneteidről.\n\nEzzel ők biztosak lehetnek abban, hogy ténylegesen veled beszélgetnek. Ez azt is jelenti, hogy látják a munkamenet nevét amit itt megadsz."; +"user_session_rename_session_title" = "Munkamenet átnevezése"; +"user_session_inactive_session_description" = "Az inaktív munkamenetek azok amiket egy ideje nem használtál, de továbbra is megkapják a titkosítási kulcsokat.\n\nA nem aktív munkamenetek törlésével növelhető a biztonság és a sebesség valamint könnyebb lesz felismerni a gyanús munkameneteket."; +"user_session_inactive_session_title" = "Nem aktív munkamenetek"; +"user_session_unverified_session_description" = "Az ellenőrizetlen munkamenetek azok amikre a felhasználói neveddel és jelszavaddal léptek be de nem lett ellenőrizve.\n\nMindenképpen győződj meg arról, hogy felismered ezeket a munkameneteket mert lehet, hogy illetéktelenül használják a fiókodat."; +"user_session_unverified_session_title" = "Ellenőrizetlen munkamenet"; +"user_session_verified_session_title" = "Hitelesített munkamenetek"; +"user_session_got_it" = "Értem"; +"voice_broadcast_tile" = "Hang közvetítés"; +"voice_broadcast_live" = "Élő"; +"key_verification_alert_body" = "Tekintsd át, hogy meggyőződj arról, hogy a fiókod biztonságban van."; diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index be31a8bdb3..8894d91b41 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2831,7 +2831,7 @@ /* The placeholder will be replaces with manage_session_name_info_link */ "manage_session_name_info" = "Harap diketahui bahwa nama sesi juga terlihat ke orang-orang yang Anda berkomunikasi. %@"; "manage_session_name_hint" = "Nama sesi khusus dapat membantu Anda mengenal perangkat Anda dengan lebih mudah."; -"settings_labs_enable_wysiwyg_composer" = "Coba editor teks kaya (mode teks biasa akan datang)"; +"settings_labs_enable_wysiwyg_composer" = "Coba editor teks kaya"; "wysiwyg_composer_start_action_voice_broadcast" = "Siaran suara"; "voice_broadcast_playback_loading_error" = "Tidak dapat memainkan siaran suara ini."; "voice_broadcast_already_in_progress_message" = "Anda saat ini merekam sebuah siaran suara. Mohon akhiri siaran suara Anda saat ini untuk memulai yang baru."; @@ -2844,3 +2844,22 @@ "deselect_all" = "Batalkan Semua Pilihan"; "user_other_session_menu_select_sessions" = "Pilih sesi"; "user_other_session_selected_count" = "%@ dipilih"; +"user_other_session_menu_sign_out_sessions" = "Keluar dari %@ sesi"; +"manage_session_sign_out_other_sessions" = "Keluarkan semua sesi lain"; +"user_sessions_hide_location_info" = "Sembunyikan alamat IP"; +"user_sessions_show_location_info" = "Tampilkan alamat IP"; +"voice_broadcast_tile" = "Siaran suara"; +"voice_broadcast_live" = "Langsung"; +"user_session_rename_session_description" = "Pengguna lain dalam pesan langsung dan ruangan yang Anda bergabung dapat melihat daftar sesi Anda yang lengkap.\n\nIni memberikan mereka kepastian bahwa mereka berbicara dengan Anda, tetapi ini juga berarti bahwa mereka dapat melihat nama sesi yang Anda masukkan di sini."; +"user_session_rename_session_title" = "Mengubah nama sesi"; +"user_session_inactive_session_description" = "Sesi yang tidak aktif adalah sesi yang Anda tidak gunakan dalam beberapa waktu, tetapi mereka masih mendapatkan kunci enkripsi.\n\nMenghapus sesi yang sudah tidak aktif meningkatkan keamanan dan performa, dan membuatnya lebih mudah untuk mengenal jika sebuah sesi baru."; +"user_session_inactive_session_title" = "Sesi tidak aktif"; +"user_session_unverified_session_description" = "Sesi yang belum diverifikasi adalah sesi yang telah masuk dengan kredensial Anda tetapi belum diverifikasi secara silang.\n\nAnda seharusnya yakin bahwa Anda mengenal sesi ini karena mereka bisa saja berarti seseorang menggunakan akun Anda."; +"user_session_unverified_session_title" = "Sesi belum diverifikasi"; +"user_session_verified_session_title" = "Sesi terverifikasi"; +"user_session_verified_session_description" = "Sesi terverifikasi ada di mana pun Anda menggunakan Element setelah memasukkan frasa sandi atau mengonfirmasi identitas Anda dengan sesi terverifikasi lainnya.\n\nIni berarti Anda memiliki semua kunci yang diperlukan untuk membuka kunci pesan terenkripsi dan mengonfirmasi kepada pengguna lain bahwa Anda memercayai sesi ini."; +"user_session_got_it" = "Mengerti"; +"key_verification_alert_body" = "Periksa untuk memastikan akun Anda aman."; + +// Unverified sessions +"key_verification_alert_title" = "Anda punya sesi yang belum diverifikasi"; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 0637f0fe9a..bc2e62e7c2 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2604,7 +2604,7 @@ /* The placeholder will be replaces with manage_session_name_info_link */ "manage_session_name_info" = "Ricorda che i nomi di sessione sono anche visibili alle persone con cui comunichi. %@"; "manage_session_name_hint" = "I nomi di sessione personalizzati possono aiutarti a riconoscere i tuoi dispositivi più facilmente."; -"settings_labs_enable_wysiwyg_composer" = "Prova l'editor in rich text (il testo semplice è in arrivo)"; +"settings_labs_enable_wysiwyg_composer" = "Prova l'editor in rich text"; "settings_labs_enable_voice_broadcast" = "Trasmissione vocale (in sviluppo attivo)"; "wysiwyg_composer_start_action_voice_broadcast" = "Trasmissione vocale"; "voice_broadcast_playback_loading_error" = "Impossibile avviare questa trasmissione vocale."; @@ -2617,3 +2617,22 @@ "deselect_all" = "Deseleziona tutti"; "user_other_session_menu_select_sessions" = "Seleziona sessioni"; "user_other_session_selected_count" = "%@ selezionate"; +"user_other_session_menu_sign_out_sessions" = "Disconnetti da %@ sessioni"; +"user_sessions_hide_location_info" = "Nascondi indirizzo IP"; +"user_sessions_show_location_info" = "Mostra indirizzo IP"; +"manage_session_sign_out_other_sessions" = "Disconnetti da tutte le altre sessioni"; +"user_session_rename_session_description" = "Gli altri utenti nei messaggi diretti e nelle stanze in cui entri, possono vedere una lista completa delle tue sessioni.\n\nIn questo modo hanno la certezza che stanno parlando davvero con te, ma significa anche che possono vedere il nome della sessione che inserisci qui."; +"user_session_rename_session_title" = "Rinominare le sessioni"; +"user_session_inactive_session_description" = "Le sessioni inattive sono quelle che non usi da un po' di tempo, ma che continuano a ricevere chiavi di crittografia.\n\nLa rimozione di sessioni inattive migliora la sicurezza e le prestazioni, e ti rende più facile capire se una sessione nuova è sospetta."; +"user_session_inactive_session_title" = "Sessioni inattive"; +"user_session_unverified_session_description" = "Le sessioni non verificate sono quelle in cui è stato fatto l'accesso con le tue credenziali, ma che non sono state verificate.\n\nDovresti essere particolarmente sicuro di riconoscere queste sessioni dato che potrebbero rappresentare un uso non autorizzato del tuo account."; +"user_session_unverified_session_title" = "Sessione non verificata"; +"user_session_verified_session_description" = "Le sessioni verificate sono ovunque usi Element dopo l'inserimento della password o la conferma della tua identità con un'altra sessione verificata.\n\nCiò significa che hai tutte le chiavi necessarie per sbloccare i tuoi messaggi cifrati e per confermare agli altri utenti che ti fidi di questa sessione."; +"user_session_verified_session_title" = "Sessioni verificate"; +"user_session_got_it" = "Capito"; +"voice_broadcast_tile" = "Trasmissione vocale"; +"voice_broadcast_live" = "In diretta"; +"key_verification_alert_body" = "Controlla per assicurarti che l'account sia sicuro."; + +// Unverified sessions +"key_verification_alert_title" = "Hai sessioni non verificate"; diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index df4ef10d1c..88f0760eee 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -25,7 +25,7 @@ "preview" = "プレビュー"; "camera" = "カメラ"; "voice" = "音声"; -"video" = "映像"; +"video" = "動画"; "active_call" = "通話開始"; "active_call_details" = "通話開始(%@)"; "later" = "後で"; @@ -133,8 +133,8 @@ "search_in_progress" = "検索しています…"; // Directory "directory_cell_title" = "ルーム一覧を見る"; -"directory_cell_description" = "%tu ルーム"; -"directory_search_results_title" = "ルーム一覧検索結果"; +"directory_cell_description" = "%tuつのルーム"; +"directory_search_results_title" = "ルーム一覧の検索結果"; "directory_searching_title" = "ルーム一覧を検索しています…"; "directory_search_fail" = "一覧を取得できませんでした"; // Contacts @@ -492,13 +492,13 @@ // Group Details "group_details_title" = "コミュニティーの詳細"; "group_details_home" = "ホーム"; -"group_details_people" = "人々"; +"group_details_people" = "連絡先"; "group_details_rooms" = "ルーム"; // Group Home -"group_home_one_member_format" = "1メンバー"; -"group_home_multi_members_format" = "%tuメンバー"; -"group_home_one_room_format" = "1ルーム"; -"group_home_multi_rooms_format" = "%tuルーム"; +"group_home_one_member_format" = "1名のメンバー"; +"group_home_multi_members_format" = "%tu名のメンバー"; +"group_home_one_room_format" = "1つのルーム"; +"group_home_multi_rooms_format" = "%tuつのルーム"; "group_invitation_format" = "%@がこのコミュニティーにあなたを招待しました"; // Group participants "group_participants_add_participant" = "参加者を追加"; @@ -529,7 +529,7 @@ "e2e_room_key_request_ignore_request" = "要求を無視"; // GDPR "gdpr_consent_not_given_alert_message" = "%@ホームサーバーを引き続き使用するには、利用規約を確認して同意する必要があります。"; -"gdpr_consent_not_given_alert_review_now_action" = "今レビュー"; +"gdpr_consent_not_given_alert_review_now_action" = "確認"; "deactivate_account_title" = "無効なアカウント"; "deactivate_account_informations_part1" = "これにより、アカウントは永久に使用できなくなります。ログインすることはできず、誰も同じユーザーIDを再登録することはできません。これにより、あなたのアカウントは参加している全てのルームから退去し、あなたのIDサーバーからアカウントの詳細が削除されます。 "; "deactivate_account_informations_part2_emphasize" = "この動作は元に戻せません。"; @@ -563,7 +563,7 @@ // MARK: - Room Info -"room_info_list_one_member" = "1人のメンバー"; +"room_info_list_one_member" = "1名のメンバー"; "create_room_placeholder_address" = "#testroom:matrix.org"; "create_room_section_header_address" = "アドレス"; "create_room_show_in_directory" = "ルーム一覧に掲載"; @@ -1541,7 +1541,7 @@ "room_intro_cell_information_room_without_topic_sentence2_part1" = "トピックを追加"; "security_settings_secure_backup_restore" = "バックアップから復元"; "settings_device_notifications" = "端末の通知"; -"settings_contacts_enable_sync" = "連絡先を探す"; +"settings_contacts_enable_sync" = "連絡先を検索"; "settings_analytics_and_crash_data" = "クラッシュ・分析データを送信"; "settings_ui_theme_picker_message_invert_colours" = "「自動」は端末の色の「反転」設定を使用します"; "settings_ui_theme_picker_message_match_system_theme" = "「自動」は端末と同じ外観を自動で使用します"; @@ -1587,7 +1587,7 @@ "biometrics_usage_reason" = "アプリを開くには認証が必要です"; "settings_sending_media" = "画像と動画の送信"; "invite_friends_share_text" = "%@ での連絡先: %@"; -"side_menu_action_invite_friends" = "友だちを招待する"; +"side_menu_action_invite_friends" = "友だちを招待"; "call_more_actions_change_audio_device" = "オーディオデバイスを変更"; "call_more_actions_dialpad" = "ダイヤルパッド"; "onboarding_splash_login_button_title" = "既にアカウントを持っています"; @@ -1670,8 +1670,8 @@ "sign_out_existing_key_backup_alert_title" = "サインアウトしてよろしいですか?"; "find_your_contacts_message" = "%@ であなたの連絡先を表示し、知人とのチャットを素早く始めます。"; "find_your_contacts_footer" = "この設定はいつでも無効にできます"; -"find_your_contacts_button_title" = "連絡先を検索する"; -"find_your_contacts_title" = "連絡先をリストアップする"; +"find_your_contacts_button_title" = "連絡先を検索"; +"find_your_contacts_title" = "連絡先をリストアップ"; // Mark: - Room avatar view @@ -1679,7 +1679,7 @@ // MARK: - Invite friends -"invite_friends_action" = "友だちを %@ に招待する"; +"invite_friends_action" = "友だちを %@ に招待"; "call_transfer_error_title" = "エラー"; "home_context_menu_mark_as_read" = "既読にする"; "home_context_menu_normal_priority" = "通常優先度"; @@ -1690,14 +1690,14 @@ // MARK: - Call Transfer "call_transfer_title" = "転送"; "room_info_back_button_title" = "ルーム情報"; -"create_room_processing" = "ルーム作成中"; +"create_room_processing" = "ルームを作成しています"; "call_transfer_users" = "ユーザー"; "home_context_menu_notifications" = "通知"; "home_context_menu_make_dm" = "連絡先に移動"; "home_context_menu_make_room" = "ルームに移動"; "leave_space_title" = "%@ を退出"; "room_participants_leave_success" = "ルームを退出しました"; -"room_participants_leave_processing" = "退出中"; +"room_participants_leave_processing" = "退出しています"; "event_formatter_group_call_leave" = "退出"; "home_context_menu_leave" = "退出"; @@ -1756,15 +1756,15 @@ "room_notifs_settings_notify_me_for" = "以下の場合に通知"; "room_details_access_row_title" = "アクセス"; "room_details_notifs" = "通知"; -"location_sharing_invalid_power_level_title" = "ライブ現在地共有に必要な権限がありません"; -"settings_labs_enable_live_location_sharing" = "ライブ現在地共有 - 現在地を共有します (開発中の機能、位置情報が一時的にルームの履歴に残ります)"; +"location_sharing_invalid_power_level_title" = "位置情報(ライブ)の共有に必要な権限がありません"; +"settings_labs_enable_live_location_sharing" = "位置情報(ライブ)の共有 - 現在の位置情報を共有(開発中の機能。位置情報が一時的にルームの履歴に残ります)"; "event_formatter_message_deleted" = "削除済みのメッセージ"; "home_context_menu_unfavourite" = "お気に入りから削除"; "home_context_menu_favourite" = "お気に入り"; "all_chats_user_menu_settings" = "ユーザー設定"; "all_chats_edit_layout_show_filters" = "フィルターを表示"; "all_chats_edit_layout_show_recents" = "最近使用したものを表示"; -"all_chats_edit_layout_alphabetical_order" = "A-Z で並び替え"; +"all_chats_edit_layout_alphabetical_order" = "アルファベット順で並び替え"; "all_chats_edit_layout_activity_order" = "アクティビティで並び替え"; "space_selector_create_space" = "スペースを作成"; "space_selector_empty_view_information" = "スペースは、ルームや連絡先をグループ化する方法です。以下からスペースを作成できます。"; @@ -1776,20 +1776,26 @@ // Mark: - All Chats "all_chats_title" = "全てのチャット"; -"location_sharing_live_loading" = "ライブ現在地共有を読み込み中..."; +"location_sharing_live_loading" = "位置情報(ライブ)を読み込んでいます…"; "location_sharing_live_list_item_stop_sharing_action" = "停止"; "location_sharing_live_list_item_current_user_display_name" = "あなた"; "live_location_sharing_banner_stop" = "停止"; -"live_location_sharing_ended" = "ライブ現在地共有が終了"; -"live_location_sharing_banner_title" = "ライブ現在地共有が有効"; +"live_location_sharing_ended" = "位置情報(ライブ)が終了しました"; +"live_location_sharing_banner_title" = "位置情報(ライブ)が有効です"; "location_sharing_invalid_authorization_settings" = "設定"; // MARK: Live location sharing -"location_sharing_live_share_title" = "ライブ現在地共有"; -"service_terms_modal_decline_button" = "拒否する"; -"service_terms_modal_accept_button" = "同意する"; +"location_sharing_live_share_title" = "位置情報(ライブ)を共有"; +"service_terms_modal_decline_button" = "拒否"; +"service_terms_modal_accept_button" = "同意"; "service_terms_modal_description_identity_server" = "この操作により、端末の連絡先にあなたの電話番号や電子メールを保存している人があなたを検索できるようになります。"; // Service terms "service_terms_modal_title_message" = "続行するには、以下の利用規約に同意してください"; +"service_terms_modal_information_title_integration_manager" = "インテグレーションマネージャー"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "IDサーバー"; +"location_sharing_invalid_power_level_message" = "位置情報(ライブ)の共有には適切な権限が必要です。"; +"location_sharing_live_error" = "位置情報(ライブ)のエラー"; diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index 297ff79b09..0b3fd58685 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -62,7 +62,7 @@ "auth_optional_phone_placeholder" = "Telefoonnummer (optioneel)"; "auth_phone_placeholder" = "Telefoonnummer"; "auth_repeat_password_placeholder" = "Wachtwoord herhalen"; -"auth_repeat_new_password_placeholder" = "Bevestig uw nieuwe Matrix account wachtwoord"; +"auth_repeat_new_password_placeholder" = "Bevestig je nieuwe Matrix account wachtwoord"; "auth_invalid_login_param" = "Onjuiste inlognaam en/of wachtwoord"; "auth_invalid_user_name" = "Inlognamen mogen alleen letters, cijfers, punten, koppeltekens en onderstrepingstekens bevatten"; "auth_invalid_password" = "Het wachtwoord is te kort (min 6)"; @@ -80,20 +80,20 @@ "auth_username_in_use" = "Inlognaam al in gebruik"; "auth_forgot_password" = "Matrix account wachtwoord vergeten?"; "auth_use_server_options" = "Aangepaste serverinstellingen gebruiken (geavanceerd)"; -"auth_email_validation_message" = "Bekijk uw e-mail om verder te gaan met de registratie"; +"auth_email_validation_message" = "Bekijk je e-mail om verder te gaan met de registratie"; "auth_msisdn_validation_title" = "Verificatie in afwachting"; "auth_msisdn_validation_message" = "We hebben een sms met een activatiecode gestuurd. Voer deze code hieronder in."; "auth_msisdn_validation_error" = "Kan het telefoonnummer niet verifiëren."; -"auth_recaptcha_message" = "Deze server wil er graag zeker van zijn dat u geen robot bent"; -"auth_reset_password_message" = "Voer het e-mailadres dat met uw account verbonden is in om uw Matrix account wachtwoord opnieuw in te stellen:"; -"auth_reset_password_missing_email" = "Het e-mailadres dat met uw account verbonden is moet ingevoerd worden."; +"auth_recaptcha_message" = "Deze server wil er graag zeker van zijn dat je geen robot bent"; +"auth_reset_password_message" = "Voer het e-mailadres dat met je account verbonden is in om je Matrix account wachtwoord opnieuw in te stellen:"; +"auth_reset_password_missing_email" = "Het e-mailadres dat met je account verbonden is moet ingevoerd worden."; "auth_reset_password_missing_password" = "Er moet een nieuw wachtwoord ingevoerd worden."; -"auth_reset_password_email_validation_message" = "Er is een e-mail naar %@ gestuurd. Klik hieronder zodra u de koppeling erin hebt gevolgd."; +"auth_reset_password_email_validation_message" = "Er is een e-mail naar %@ gestuurd. Klik hieronder zodra je de koppeling erin hebt gevolgd."; "auth_reset_password_next_step_button" = "Ik heb mijn e-mailadres geverifieerd"; -"auth_reset_password_error_unauthorized" = "Verifiëren van e-mailadres is mislukt: wees er zeker van dat u op de koppeling in de e-mail hebt getikt"; -"auth_reset_password_error_not_found" = "Het ziet er niet naar uit dat uw e-mailadres met een Matrix-ID is verbonden op deze server."; -"auth_reset_password_success_message" = "Uw Matrix account wachtwoord is opnieuw ingesteld.\n\nU bent op alle apparaten afgemeld en u zult geen pushmeldingen meer ontvangen. Om meldingen weer in te schakelen, meldt u zich op elk apparaat opnieuw aan."; -"auth_add_email_and_phone_warning" = "Registratie met e-mailadres en telefoonnummer tegelijkertijd wordt, totdat de API bestaat, nog niet ondersteund. Alleen het telefoonnummer zal worden gebruikt. U kunt uw e-mailadres later aan uw profiel in de instellingen toevoegen."; +"auth_reset_password_error_unauthorized" = "Verifiëren van e-mailadres is mislukt: wees er zeker van dat je op de koppeling in de e-mail hebt getikt"; +"auth_reset_password_error_not_found" = "Het ziet er niet naar uit dat je e-mailadres met een Matrix-ID is verbonden op deze server."; +"auth_reset_password_success_message" = "Jouw Matrix account wachtwoord is opnieuw ingesteld.\n\nJe bent op alle apparaten afgemeld en je zal geen pushmeldingen meer ontvangen. Om meldingen weer in te schakelen, meldt je jezelf op elk apparaat opnieuw aan."; +"auth_add_email_and_phone_warning" = "Registratie met e-mailadres en telefoonnummer tegelijkertijd wordt, totdat de API bestaat, nog niet ondersteund. Alleen het telefoonnummer zal worden gebruikt. Je kan je e-mailadres later aan je profiel in de instellingen toevoegen."; // Chat creation "room_creation_title" = "Nieuwe chat"; "room_creation_account" = "Account"; @@ -105,7 +105,7 @@ "room_creation_public_room" = "Dit gesprek is publiek"; "room_creation_make_public" = "Publiek maken"; "room_creation_make_public_prompt_title" = "Dit gesprek publiek maken?"; -"room_creation_make_public_prompt_msg" = "Weet u zeker dat u dit gesprek publiek wilt maken? Iedereen kan uw berichten lezen en aan het gesprek deelnemen."; +"room_creation_make_public_prompt_msg" = "Weet je zeker dat je dit gesprek publiek wilt maken? Iedereen kan jouw berichten lezen en aan het gesprek deelnemen."; "room_creation_keep_private" = "Privé houden"; "room_creation_make_private" = "Privé maken"; "room_creation_wait_for_creation" = "Er wordt al een kamer aangemaakt. Even geduld."; @@ -150,19 +150,19 @@ "contacts_address_book_matrix_users_toggle" = "Alleen Matrix-personen"; "contacts_address_book_no_contact" = "Geen lokale contacten"; "contacts_address_book_permission_required" = "Toestemming vereist voor toegang tot de lokale contacten"; -"contacts_address_book_permission_denied" = "U heeft %@ geen toegang tot uw lokale contacten verleend"; +"contacts_address_book_permission_denied" = "Je hebt %@ geen toegang tot je lokale contacten verleend"; // Chat participants "room_participants_title" = "Deelnemers"; "room_participants_add_participant" = "Deelnemer toevoegen"; "room_participants_one_participant" = "1 deelnemer"; "room_participants_multi_participants" = "%d deelnemers"; "room_participants_leave_prompt_title" = "Kamer verlaten"; -"room_participants_leave_prompt_msg" = "Weet u zeker dat u de kamer wilt verlaten?"; +"room_participants_leave_prompt_msg" = "Weet je zeker dat je de kamer wil verlaten?"; "room_participants_remove_prompt_title" = "Bevestiging"; -"room_participants_remove_prompt_msg" = "Weet u zeker dat u %@ uit dit gesprek wilt verwijderen?"; +"room_participants_remove_prompt_msg" = "Weet je zeker dat je %@ uit dit gesprek wil verwijderen?"; "room_participants_remove_third_party_invite_msg" = "Verwijderen van uitnodigingen door derde partijen wordt, totdat de API bestaat, nog niet ondersteund"; "room_participants_invite_prompt_title" = "Bevestiging"; -"room_participants_invite_prompt_msg" = "Weet u zeker dat u %@ in dit gesprek wilt uitnodigen?"; +"room_participants_invite_prompt_msg" = "Weet je zeker dat je %@ in dit gesprek wil uitnodigen?"; "room_participants_filter_room_members" = "Kamerleden filteren"; "room_participants_invite_another_user" = "Zoeken/uitnodigen met persoon-ID, naam of e-mailadres"; "room_participants_invite_malformed_id_title" = "Uitnodigingsfout"; @@ -219,7 +219,7 @@ "room_event_action_view_source" = "Bron weergeven"; "room_event_action_report" = "Inhoud melden"; "room_event_action_report_prompt_reason" = "Reden voor het melden van deze inhoud"; -"room_event_action_report_prompt_ignore_user" = "Wilt u alle berichten van deze persoon verbergen?"; +"room_event_action_report_prompt_ignore_user" = "Wil je alle berichten van deze persoon verbergen?"; "room_event_action_save" = "Opslaan"; "room_event_action_resend" = "Opnieuw versturen"; "room_event_action_delete" = "Verwijderen"; @@ -229,7 +229,7 @@ "room_warning_about_encryption" = "Eind-tot-eindversleuteling is in bèta en kan onbetrouwbaar zijn.\n\nHet is beter om het nog niet met gevoelige gegevens te vertrouwen.\n\nApparaten kunnen de geschiedenis van vóór ze de kamer betraden nog niet ontsleutelen.\n\nVersleutelde berichten zullen niet zichtbaar zijn op cliënten die nog geen versleuteling ondersteunen."; // Unknown devices "unknown_devices_alert_title" = "Kamer bevat onbekende sessies"; -"unknown_devices_alert" = "Deze kamer bevat onbekende apparaten die niet geverifieerd zijn.\nDit betekent dat er geen garantie is dat de apparaten bij de personen horen waarbij ze beweren te horen.\nWe raden u aan om bij elk apparaat door het verificatieproces heen te gaan voordat u verdergaat, maar u kunt het bericht ook zonder verificatie opnieuw versturen."; +"unknown_devices_alert" = "Deze kamer bevat onbekende apparaten die niet geverifieerd zijn.\nDit betekent dat er geen garantie is dat de apparaten bij de personen horen waarbij ze beweren te horen.\nWe raden je aan om bij elk apparaat door het verificatieproces heen te gaan voordat je verdergaat, maar je kan het bericht ook zonder verificatie opnieuw versturen."; "unknown_devices_send_anyway" = "Alsnog versturen"; "unknown_devices_call_anyway" = "Alsnog bellen"; "unknown_devices_answer_anyway" = "Alsnog beantwoorden"; @@ -243,10 +243,10 @@ "room_title_members" = "%@ leden"; "room_title_one_member" = "1 lid"; // Room Preview -"room_preview_invitation_format" = "U bent door %@ uitgenodigd om deze kamer toe te treden"; +"room_preview_invitation_format" = "Je bent door %@ uitgenodigd om deze kamer toe te treden"; "room_preview_subtitle" = "Dit is een voorvertoning van deze kamer. Kamerinteracties zijn uitgeschakeld."; -"room_preview_unlinked_email_warning" = "Deze uitnodiging is naar %@ verstuurd, maar dat is niet geassocieerd met deze account. U kunt zich met een andere account aanmelden, of dit e-mailadres aan deze account toevoegen."; -"room_preview_try_join_an_unknown_room" = "U probeert toegang te verkrijgen tot %@. Zou u willen toetreden om aan het gesprek deel te nemen?"; +"room_preview_unlinked_email_warning" = "Deze uitnodiging is naar %@ verstuurd, maar dat is niet geassocieerd met deze account. Je kan met een andere account aanmelden, of dit e-mailadres aan deze account toevoegen."; +"room_preview_try_join_an_unknown_room" = "Je probeert toegang te verkrijgen tot %@. Zou je willen toetreden om aan het gesprek deel te nemen?"; "room_preview_try_join_an_unknown_room_default" = "een kamer"; // Settings "settings_title" = "Instellingen"; @@ -268,17 +268,17 @@ "settings_devices" = "APPARATEN"; "settings_cryptography" = "CRYPTOGRAFIE"; "settings_sign_out" = "Uitloggen"; -"settings_sign_out_confirmation" = "Weet u het zeker?"; -"settings_sign_out_e2e_warn" = "U zult uw sleutels voor eind-tot-eindversleuteling kwijtraken. Dat betekent dat u op dit apparaat geen oude berichten in versleutelde gesprekken meer zult kunnen lezen."; +"settings_sign_out_confirmation" = "Weet je het zeker?"; +"settings_sign_out_e2e_warn" = "Je zal je sleutels voor eind-tot-eindversleuteling kwijtraken. Dat betekent dat je op dit apparaat geen oude berichten in versleutelde gesprekken meer zal kunnen lezen."; "settings_profile_picture" = "Profielfoto"; "settings_display_name" = "Weergavenaam"; "settings_first_name" = "Voornaam"; "settings_surname" = "Achternaam"; "settings_remove_prompt_title" = "Bevestiging"; -"settings_remove_email_prompt_msg" = "Weet u zeker dat u het e-mailadres %@ wilt verwijderen?"; -"settings_remove_phone_prompt_msg" = "Weet u zeker dat u het telefoonnummer %@ wilt verwijderen?"; +"settings_remove_email_prompt_msg" = "Weet je zeker dat je het e-mailadres %@ wil verwijderen?"; +"settings_remove_phone_prompt_msg" = "Weet je zeker dat je het telefoonnummer %@ wil verwijderen?"; "settings_email_address" = "E-mailadres"; -"settings_email_address_placeholder" = "Voer uw e-mailadres in"; +"settings_email_address_placeholder" = "Voer je e-mailadres in"; "settings_add_email_address" = "E-mailadres toevoegen"; "settings_phone_number" = "Telefoon"; "settings_add_phone_number" = "Telefoonnummer toevoegen"; @@ -286,7 +286,7 @@ "settings_night_mode" = "Nachtmodus"; "settings_fail_to_update_profile" = "Bijwerken van profiel is mislukt"; "settings_enable_push_notif" = "Meldingen op dit apparaat"; -"settings_global_settings_info" = "Globale meldingsinstellingen zijn beschikbaar op uw %@-webcliënt"; +"settings_global_settings_info" = "Globale meldingsinstellingen zijn beschikbaar op je %@-webcliënt"; "settings_pin_rooms_with_missed_notif" = "Kamers met gemiste meldingen vastprikken"; "settings_pin_rooms_with_unread" = "Kamers met ongelezen berichten vastprikken"; //"settings_enable_all_notif" = "Alle notificaties aanzetten"; @@ -301,7 +301,7 @@ "settings_contacts_discover_matrix_users" = "Gebruik e-mailadressen en telefoonnummers om gebruikers te vinden"; "settings_contacts_phonebook_country" = "Land voor telefoonboek"; "settings_labs_e2e_encryption" = "Eind-tot-eind-versleuteling"; -"settings_labs_e2e_encryption_prompt_message" = "Om het opzetten van de versleuteling af te ronden moet u zich opnieuw aanmelden."; +"settings_labs_e2e_encryption_prompt_message" = "Om het opzetten van de versleuteling af te ronden moet je jezelf opnieuw aanmelden."; "settings_version" = "Versie %@"; "settings_olm_version" = "Olm-versie %@"; "settings_copyright" = "Copyright"; @@ -313,7 +313,7 @@ "settings_new_password" = "Nieuw wachtwoord"; "settings_confirm_password" = "Wachtwoord bevestigen"; "settings_fail_to_update_password" = "Bijwerken van Matrix account wachtwoord is mislukt"; -"settings_password_updated" = "Uw Matrix account wachtwoord is bijgewerkt"; +"settings_password_updated" = "Jouw Matrix account wachtwoord is bijgewerkt"; "settings_crypto_device_name" = "Apparaatnaam: "; "settings_crypto_device_id" = "\nApparaats-ID: "; "settings_crypto_device_key" = "\nApparaatssleutel:\n"; @@ -350,7 +350,7 @@ "room_details_addresses_invalid_address_prompt_title" = "Ongeldig bijnaamformaat"; "room_details_addresses_invalid_address_prompt_msg" = "%@ is geen geldig formaat voor een bijnaam"; "room_details_addresses_disable_main_address_prompt_title" = "Hoofdadreswaarschuwing"; -"room_details_addresses_disable_main_address_prompt_msg" = "U heeft geen hoofdadres opgegeven. Het standaardhoofdadres voor deze kamer zal willekeurig gekozen worden"; +"room_details_addresses_disable_main_address_prompt_msg" = "Je hebt geen hoofdadres opgegeven. Het standaardhoofdadres voor deze kamer zal willekeurig gekozen worden"; "room_details_banned_users_section" = "Verbannen personen"; "room_details_advanced_section" = "Geavanceerd"; "room_details_advanced_room_id" = "Kamer-ID:"; @@ -370,7 +370,7 @@ "room_details_fail_to_remove_room_aliases" = "Verwijderen van kameradressen is mislukt"; "room_details_fail_to_update_room_canonical_alias" = "Bijwerken van hoofdadres is mislukt"; "room_details_fail_to_enable_encryption" = "Inschakelen van versleuteling in deze kamer is mislukt"; -"room_details_save_changes_prompt" = "Wilt u de wijzigingen opslaan?"; +"room_details_save_changes_prompt" = "Wil je de wijzigingen opslaan?"; "room_details_set_main_address" = "Instellen als hoofdadres"; "room_details_unset_main_address" = "Niet instellen als hoofdadres"; "room_details_copy_room_id" = "Kamer-ID kopiëren"; @@ -388,13 +388,13 @@ "directory_server_placeholder" = "matrix.org"; // Others "or" = "of"; -"you" = "U"; +"you" = "Jij"; "today" = "Vandaag"; "yesterday" = "Gisteren"; "network_offline_prompt" = "Het ziet er naar uit dat de internetverbinding offline is."; "public_room_section_title" = "Publieke kamers (op %@):"; -"bug_report_prompt" = "De app is de vorige keer gecrasht. Wilt u een crashrapport indienen?"; -"rage_shake_prompt" = "Het ziet er naar uit dat u de telefoon in frustratie schudt. Wilt u een foutmelding indienen?"; +"bug_report_prompt" = "De app is de vorige keer gecrasht. Wil je een crashrapport indienen?"; +"rage_shake_prompt" = "Het ziet er naar uit dat je de telefoon in frustratie schudt. Wil je een foutmelding indienen?"; "camera_access_not_granted" = "%@ heeft geen toestemming om de camera te gebruiken, pas de privacy-instellingen aan"; "large_badge_value_k_format" = "%.1fK"; // Call @@ -402,17 +402,17 @@ "call_incoming_video_prompt" = "Inkomende video-oproep van %@"; // No VoIP support "no_voip_title" = "Inkomende oproep"; -"no_voip" = "%@ belt u %@ maar ondersteunt nog geen oproepen.\nU kunt deze melding negeren en vanaf een ander apparaat opnemen, of de oproep afwijzen."; +"no_voip" = "%@ belt je %@ maar ondersteunt nog geen oproepen.\nJe kan deze melding negeren en vanaf een ander apparaat opnemen, of de oproep afwijzen."; // Crash report // Crypto -"e2e_enabling_on_app_update" = "%@ ondersteunt nu eind-tot-eind-versleuteling, maar u moet zich opnieuw aanmelden om het in te schakelen.\n\nU kunt dit nu of later doen vanuit de app-instellingen."; -"e2e_need_log_in_again" = "U moet zich opnieuw aanmelden om sleutels voor eind-tot-eind-versleuteling te genereren voor dit apparaat, en om de publieke sleutel naar uw server te sturen.\nDit is eenmalig; excuses voor het ongemak."; +"e2e_enabling_on_app_update" = "%@ ondersteunt nu eind-tot-eind-versleuteling, maar je moet opnieuw aanmelden om het in te schakelen.\n\nJe kan dit nu of later doen vanuit de app-instellingen."; +"e2e_need_log_in_again" = "Je moet opnieuw aanmelden om sleutels voor eind-tot-eind-versleuteling te genereren voor dit apparaat, en om de publieke sleutel naar je server te sturen.\nDit is eenmalig; excuses voor het ongemak."; // Bug report "bug_report_title" = "Foutmelding"; -"bug_report_description" = "Beschrijf de foutmelding. Wat heeft u gedaan? Wat verwachtte u dat er zou gebeuren? Wat is er echt gebeurd?"; +"bug_report_description" = "Beschrijf de foutmelding. Wat heb je gedaan? Wat verwachtte je dat er zou gebeuren? Wat is er echt gebeurd?"; "bug_crash_report_title" = "Crashrapport"; -"bug_crash_report_description" = "Beschrijf wat u deed vóór de crash:"; -"bug_report_logs_description" = "Om problemen te onderzoeken worden logboeken van deze app met de foutmelding meegestuurd. Als u liever alleen bovenstaande tekst stuurt, haal dan het vinkje weg:"; +"bug_crash_report_description" = "Beschrijf wat je deed vóór de crash:"; +"bug_report_logs_description" = "Om problemen te onderzoeken worden logboeken van deze app met de foutmelding meegestuurd. Als je liever alleen bovenstaande tekst stuurt, haal dan het vinkje weg:"; "bug_report_send_logs" = "Logboeken versturen"; "bug_report_send_screenshot" = "Schermafbeelding versturen"; "bug_report_progress_zipping" = "Logboeken worden verzameld"; @@ -436,12 +436,12 @@ "auth_identity_server_placeholder" = "URL (bv. https://vector.im)"; "room_ongoing_conference_call_with_close" = "Er is een vergadergesprek gaande. Neem deel met %@ of %@. %@ het."; "room_ongoing_conference_call_close" = "Sluiten"; -"room_conference_call_no_power" = "U heeft toestemming nodig om vergaderingen in deze kamer te beheren"; +"room_conference_call_no_power" = "Je hebt toestemming nodig om vergaderingen in deze kamer te beheren"; "settings_labs_create_conference_with_jitsi" = "Maak vergadergesprekken met jitsi"; "call_already_displayed" = "Er is al een oproep aan de gang."; "call_jitsi_error" = "Deelnemen aan het vergadergesprek is mislukt."; // Widget -"widget_no_power_to_manage" = "U heeft toestemming nodig om widgets in deze kamer te beheren"; +"widget_no_power_to_manage" = "Je hebt toestemming nodig om widgets in deze kamer te beheren"; "widget_creation_failure" = "Aanmaken van widget is mislukt"; "send_to" = "Stuur naar %@"; "sending" = "Wordt verstuurd"; @@ -451,7 +451,7 @@ "settings_calls_settings" = "OPROEPEN"; "settings_show_decrypted_content" = "Ontsleutelde inhoud tonen"; "settings_enable_callkit" = "Geïntegreerde oproepen"; -"settings_callkit_info" = "Ontvang inkomende oproepen op uw toegangsscherm. Geef uw %@-oproepen weer in de gespreksgeschiedenis van het systeem. Als iCloud ingeschakeld is zal deze geschiedenis met Apple gedeeld worden."; +"settings_callkit_info" = "Ontvang inkomende oproepen op je toegangsscherm. Geef je %@-oproepen weer in de gespreksgeschiedenis van het systeem. Als iCloud ingeschakeld is zal deze geschiedenis met Apple gedeeld worden."; "settings_ui_theme" = "Thema"; "settings_ui_theme_auto" = "Automatisch"; "settings_ui_theme_light" = "Licht"; @@ -470,22 +470,22 @@ "call_incoming_voice" = "Inkomende oproep…"; "call_incoming_video" = "Inkomende video-oproep…"; // Widget Integration Manager -"widget_integration_need_to_be_able_to_invite" = "U moet personen kunnen uitnodigen om dat te kunnen doen."; +"widget_integration_need_to_be_able_to_invite" = "Je moet personen kunnen uitnodigen om dat te kunnen doen."; "widget_integration_unable_to_create" = "Kan widget niet aanmaken."; "widget_integration_failed_to_send_request" = "Versturen van verzoek is mislukt."; "widget_integration_room_not_recognised" = "Deze kamer wordt niet herkend."; "widget_integration_positive_power_level" = "Het machtsniveau moet een positief geheel getal zijn."; -"widget_integration_must_be_in_room" = "U zit niet in deze kamer."; -"widget_integration_no_permission_in_room" = "U heeft geen toestemming om dat in deze kamer te doen."; +"widget_integration_must_be_in_room" = "Je zit niet in deze kamer."; +"widget_integration_no_permission_in_room" = "Je hebt geen toestemming om dat in deze kamer te doen."; "widget_integration_missing_room_id" = "kamer_id ontbreekt in het verzoek."; "widget_integration_missing_user_id" = "user_id ontbreekt in het verzoek."; "widget_integration_room_not_visible" = "Kamer %@ is niet zichtbaar."; // Share extension -"share_extension_auth_prompt" = "Meld u aan in de hoofdapp om inhoud te delen"; +"share_extension_auth_prompt" = "Meldt je aan in de hoofdapp om inhoud te delen"; "share_extension_failed_to_encrypt" = "Versturen is mislukt. Controleer in de hoofdapp de versleutelingsinstellingen van deze kamer"; // Room key request dialog "e2e_room_key_request_title" = "Versleutelingssleutelverzoek"; -"e2e_room_key_request_message" = "Uw ongeverifieerde apparaat ‘%@’ vraagt naar versleutelingssleutels."; +"e2e_room_key_request_message" = "Je ongeverifieerde apparaat ‘%@’ vraagt naar versleutelingssleutels."; "e2e_room_key_request_start_verification" = "Verificatie beginnen…"; "e2e_room_key_request_share_without_verifying" = "Delen zonder te verifiëren"; "e2e_room_key_request_ignore_request" = "Verzoek negeren"; @@ -493,7 +493,7 @@ // Groups tab "group_invite_section" = "UITNODIGINGEN"; "group_section" = "GEMEENSCHAPPEN"; -"room_do_not_have_permission_to_post" = "U heeft geen toestemming om in deze kamer te plaatsen"; +"room_do_not_have_permission_to_post" = "Je hebt geen toestemming om in deze kamer te plaatsen"; "settings_flair" = "Badge weergeven waar toegestaan"; "room_details_flair_section" = "Badge voor gemeenschappen weergeven"; "room_details_new_flair_placeholder" = "Nieuwe gemeenschaps-ID toevoegen (bv. +foo%@)"; @@ -510,15 +510,15 @@ "group_home_multi_members_format" = "%tu leden"; "group_home_one_room_format" = "1 kamer"; "group_home_multi_rooms_format" = "%tu kamers"; -"group_invitation_format" = "%@ heeft u uitgenodigd om tot deze gemeenschap toe te treden"; +"group_invitation_format" = "%@ heeft je uitgenodigd om tot deze gemeenschap toe te treden"; // Group participants "group_participants_add_participant" = "Deelnemer toevoegen"; "group_participants_leave_prompt_title" = "Groep verlaten"; -"group_participants_leave_prompt_msg" = "Weet u zeker dat u de groep wilt verlaten?"; +"group_participants_leave_prompt_msg" = "Weet je zeker dat je de groep wil verlaten?"; "group_participants_remove_prompt_title" = "Bevestiging"; -"group_participants_remove_prompt_msg" = "Weet u zeker dat u %@ uit deze groep wilt verwijderen?"; +"group_participants_remove_prompt_msg" = "Weet je zeker dat je %@ uit deze groep wil verwijderen?"; "group_participants_invite_prompt_title" = "Bevestiging"; -"group_participants_invite_prompt_msg" = "Weet u zeker dat u %@ in deze groep wilt uitnodigen?"; +"group_participants_invite_prompt_msg" = "Weet je zeker dat je %@ in deze groep wilt uitnodigen?"; "group_participants_filter_members" = "Gemeenschapsleden filteren"; "group_participants_invite_another_user" = "Zoeken/uitnodigen met persoon-ID of naam"; "group_participants_invite_malformed_id_title" = "Uitnodigingsfout"; @@ -526,11 +526,11 @@ "group_participants_invited_section" = "UITGENODIGD"; // Group rooms "group_rooms_filter_rooms" = "Gemeenschapskamers filteren"; -"e2e_room_key_request_message_new_device" = "U heeft een nieuw apparaat ‘%@’ toegevoegd, dat vraagt naar versleutelingssleutels."; +"e2e_room_key_request_message_new_device" = "Je hebt een nieuw apparaat ‘%@’ toegevoegd, dat vraagt naar versleutelingssleutels."; "room_event_action_kick_prompt_reason" = "Reden voor het verwijderen van deze persoon"; "room_event_action_ban_prompt_reason" = "Reden voor het verbannen van deze persoon"; // GDPR -"gdpr_consent_not_given_alert_message" = "Om de %@-server te blijven gebruiken moet u de algemene voorwaarden lezen en er mee akkoord gaan."; +"gdpr_consent_not_given_alert_message" = "Om de %@-server te blijven gebruiken moet je de algemene voorwaarden lezen en er mee akkoord gaan."; "gdpr_consent_not_given_alert_review_now_action" = "Nu lezen"; "room_action_send_photo_or_video" = "Foto of video versturen"; "room_action_send_sticker" = "Sticker versturen"; @@ -541,27 +541,27 @@ "rerequest_keys_alert_message" = "Start %@ op een ander apparaat dat het bericht kan ontsleutelen, zodat het de sleutels kan sturen naar dit apparaat."; "settings_deactivate_my_account" = "Account permanent deactiveren"; "event_formatter_rerequest_keys_part1_link" = "Versleutelingssleutels opnieuw aanvragen"; -"event_formatter_rerequest_keys_part2" = " van uw andere apparaten."; -"widget_sticker_picker_no_stickerpacks_alert" = "U heeft momenteel geen stickerpakketten ingeschakeld."; -"widget_sticker_picker_no_stickerpacks_alert_add_now" = "Wilt u er nu een paar toevoegen?"; +"event_formatter_rerequest_keys_part2" = " van je andere apparaten."; +"widget_sticker_picker_no_stickerpacks_alert" = "Je hebt momenteel geen stickerpakketten ingeschakeld."; +"widget_sticker_picker_no_stickerpacks_alert_add_now" = "Wil je er nu een paar toevoegen?"; "deactivate_account_title" = "Account deactiveren"; -"deactivate_account_informations_part1" = "Dit zal uw account voorgoed onbruikbaar maken. U zult zich niet meer kunnen aanmelden, en niemand anders zal zich met dezelfde persoon-ID kunnen registreren. Dit zal er voor zorgen dat uw account alle kamers verlaat waar deze momenteel lid van is, en het verwijdert uw gegevens van de identiteitsserver. "; +"deactivate_account_informations_part1" = "Dit zal je account voorgoed onbruikbaar maken. Je zal niet meer kunnen aanmelden, en niemand anders zal zich met dezelfde persoon-ID kunnen registreren. Dit zal er voor zorgen dat je account alle kamers verlaat waar deze momenteel lid van is, en het verwijdert je gegevens van de identiteitsserver. "; "deactivate_account_informations_part2_emphasize" = "Deze actie is onomkeerbaar."; -"deactivate_account_informations_part3" = "\n\nHet deactiveren van uw account "; -"deactivate_account_informations_part4_emphasize" = "zal er standaard niet voor zorgen dat de berichten die u heeft verzonden worden vergeten. "; -"deactivate_account_informations_part5" = "Als u wilt dat wij de berichten vergeten, vinkt u het vakje hieronder aan.\n\nDe zichtbaarheid van berichten in Matrix is gelijkaardig aan e-mails. Het vergeten van uw berichten betekent dat berichten die u heeft verstuurd niet meer gedeeld worden met nieuwe of ongeregistreerde gebruikers, maar geregistreerde personen die al toegang hebben tot deze berichten zullen alsnog toegang hebben tot hun eigen kopie ervan."; +"deactivate_account_informations_part3" = "\n\nHet deactiveren van je account "; +"deactivate_account_informations_part4_emphasize" = "zal er standaard niet voor zorgen dat de berichten die je hebt verzonden worden vergeten. "; +"deactivate_account_informations_part5" = "Als je wil dat wij de berichten vergeten, vink je het vakje hieronder aan.\n\nDe zichtbaarheid van berichten in Matrix is gelijkaardig aan e-mails. Het vergeten van je berichten betekent dat berichten die je hebt verstuurd niet meer gedeeld worden met nieuwe of ongeregistreerde gebruikers, maar geregistreerde personen die al toegang hebben tot deze berichten zullen alsnog toegang hebben tot hun eigen kopie ervan."; "deactivate_account_forget_messages_information_part1" = "Vergeet alle berichten die ik heb verstuurd wanneer mijn account gedeactiveerd is ("; "deactivate_account_forget_messages_information_part3" = ": dit zal er voor zorgen dat toekomstige personen een onvolledig beeld krijgen van gesprekken)"; "deactivate_account_validate_action" = "Account deactiveren"; "deactivate_account_password_alert_title" = "Account deactiveren"; -"deactivate_account_password_alert_message" = "Voer uw Matrix account wachtwoord in om verder te gaan"; +"deactivate_account_password_alert_message" = "Voer je Matrix account wachtwoord in om verder te gaan"; "room_event_action_view_decrypted_source" = "Ontsleutelde bron weergeven"; "room_message_reply_to_placeholder" = "Stuur een antwoord (onversleuteld)…"; "encrypted_room_message_reply_to_placeholder" = "Stuur een versleuteld antwoord…"; "room_message_reply_to_short_placeholder" = "Stuur een antwoord…"; // String for App Store "store_short_description" = "Veilig en gedecentraliseerd chatten en bellen"; -"store_full_description" = "Element is een nieuw type messenger en samenwerkings app die:\n\n1. U de controle geeft om uw privacy te behouden\n2. U laat communiceren met iedereen in het Matrix-netwerk, en zelfs daarbuiten door integratie met apps zoals Slack\n3. Beschermt u tegen reclame, datamining, achterdeurtjes en ommuurde netwerken\n4. Beveiligt u door eind-tot-eind versleuteling, met kruislings ondertekenen om anderen te verifiëren\n\nElement is compleet anders dan andere messengers en samenwerkings-apps, omdat het gedecentraliseerd en open source is.\n\nMet Element kunt u zelf hosten - of een host kiezen - zodat u privacy, eigendom en controle heeft over uw gegevens en gesprekken. Het geeft u toegang tot een open netwerk; u zit dus niet vast aan het praten met alleen andere Element-personen. En het is zeer veilig.\n\nElement is hiertoe in staat omdat het werkt op basis van Matrix - de standaard voor open, gedecentraliseerde communicatie. \n\nElement geeft u de controle door u te laten kiezen wie uw gesprekken host. Vanuit de Element app kunt u kiezen om op verschillende manieren te hosten:\n\n1. Neem een gratis account op de publieke server matrix.org\n2. Host het zelf, uw account door draait op uw eigen server\n3. Laat ons het hosten, meld u aan voor een account op een aangepaste server bij het Element Matrix Services hosting platform\n\nWaarom kiest u voor Element?\n\nEIGENAAR VAN UW GEGEVENS: U bepaalt waar uw gegevens en berichten worden bewaard. U bent de eigenaar en heeft de controle, niet een of andere MEGACORP die uw gegevens mijnt of toegang geeft aan derden.\n\nOPEN MESSAGING EN SAMENWERKING: U kunt met iedereen in het Matrix-netwerk chatten, of ze nu Element of een andere Matrix-app gebruiken, en zelfs als ze een ander messaging-systeem gebruiken zoals Slack, IRC of XMPP.\n\nSUPER-VEILIG: Echte eind-tot-eind versleuteling (alleen degenen in de conversatie kunnen berichten ontsleutelen), en kruislings ondertekenen om de apparaten van gespreksdeelnemers te verifiëren.\n\nCOMPLETE COMMUNICATIE: Berichten, spraak- en videogesprekken, bestandsdeling, schermdeling en een heleboel integraties, bots en widgets. Bouw kamers, Spaces, blijf in contact en krijg het gedaan.\n\nOVERAL WAAR U BENT: Blijf in contact waar u ook bent met volledig gesynchroniseerde berichtgeschiedenis op al uw apparaten en op het web op https://element.io/app."; +"store_full_description" = "Element is een nieuw type messenger en samenwerkings app die:\n\n1. Jou de controle geeft om je privacy te behouden\n2. Jou laat communiceren met iedereen in het Matrix-netwerk, en zelfs daarbuiten door integratie met apps zoals Slack\n3. Jou beschermt tegen reclame, datamining, achterdeurtjes en ommuurde netwerken\n4. Jou beveiligt door eind-tot-eind versleuteling, met kruislings ondertekenen om anderen te verifiëren\n\nElement is compleet anders dan andere messengers en samenwerkings-apps, omdat het gedecentraliseerd en open source is.\n\nMet Element kan je zelf hosten - of een host kiezen - zodat je privacy, eigendom en controle hebt over je gegevens en gesprekken. Het geeft je toegang tot een open netwerk; je zit dus niet vast aan het praten met alleen andere Element-personen. En het is zeer veilig.\n\nElement is hiertoe in staat omdat het werkt op basis van Matrix - de standaard voor open, gedecentraliseerde communicatie. \n\nElement geeft je de controle door je te laten kiezen wie jouw gesprekken host. Vanuit de Element app kan je kiezen om op verschillende manieren te hosten:\n\n1. Neem een gratis account op de publieke server matrix.org\n2. Host het zelf, jouw account door draait op je eigen server\n3. Laat ons het hosten, meld je aan voor een account op een aangepaste server bij het Element Matrix Services hosting platform\n\nWaarom kies je voor Element?\n\nEIGENAAR VAN JE GEGEVENS: Jij bepaalt waar je gegevens en berichten worden bewaard. Jij bent de eigenaar en hebt de controle, niet een of andere MEGACORP die je gegevens mijnt of toegang geeft aan derden.\n\nOPEN MESSAGING EN SAMENWERKING: Jij kunt met iedereen in het Matrix-netwerk chatten, of ze nu Element of een andere Matrix-app gebruiken, en zelfs als ze een ander messaging-systeem gebruiken zoals Slack, IRC of XMPP.\n\nSUPER-VEILIG: Echte eind-tot-eind versleuteling (alleen degenen in de conversatie kunnen berichten ontsleutelen), en kruislings ondertekenen om de apparaten van gespreksdeelnemers te verifiëren.\n\nCOMPLETE COMMUNICATIE: Berichten, spraak- en videogesprekken, bestandsdeling, schermdeling en een heleboel integraties, bots en widgets. Bouw kamers, Spaces, blijf in contact en krijg het gedaan.\n\nOVERAL WAAR U BENT: Blijf in contact waar je ook bent met volledig gesynchroniseerde berichtgeschiedenis op al je apparaten en op het web op https://element.io/app."; "auth_login_single_sign_on" = "Aanmelden met enkele aanmelding"; "auth_accept_policies" = "Lees het beleid van deze homeserver en kies om te aanvaarden:"; "auth_autodiscover_invalid_response" = "Ongeldig server-ontdekkings-antwoord"; @@ -572,7 +572,7 @@ "room_predecessor_information" = "Deze kamer is een voortzetting van een eerdere kamer."; "room_predecessor_link" = "Tik hier om oudere berichten te zien."; "room_resource_limit_exceeded_message_contact_1" = " Gelieve "; -"room_resource_limit_exceeded_message_contact_2_link" = "contact op te nemen met uw dienstbeheerder"; +"room_resource_limit_exceeded_message_contact_2_link" = "contact op te nemen met je dienstbeheerder"; "room_resource_limit_exceeded_message_contact_3" = " om deze dienst te blijven gebruiken."; "room_resource_usage_limit_reached_message_1_default" = "Deze server heeft één van zijn bronlimieten overschreden, dus "; "room_resource_usage_limit_reached_message_1_monthly_active_user" = "Deze server heeft zijn limiet voor maandelijks actieve personen overschreden, dus "; @@ -581,14 +581,14 @@ "settings_key_backup" = "SLEUTELBACK-UP"; "settings_labs_room_members_lazy_loading" = "Gespreksleden lui laden"; "settings_labs_room_members_lazy_loading_error_message" = "Uw thuisserver ondersteunt het lui laden van gespreksleden nog niet. Probeer het later opnieuw."; -"settings_key_backup_info" = "Versleutelde berichten worden beveiligd met eind-tot-eind-versleuteling. Enkel de ontvanger(s) en u hebben de sleutels om deze berichten te lezen."; +"settings_key_backup_info" = "Versleutelde berichten worden beveiligd met eind-tot-eind-versleuteling. Enkel de ontvanger(s) en jij hebben de sleutels om deze berichten te lezen."; "settings_key_backup_info_checking" = "Bezig met controleren…"; -"settings_key_backup_info_none" = "Uw sleutels worden niet geback-upt vanaf dit apparaat."; -"settings_key_backup_info_signout_warning" = "Maak een back-up van uw sleutels voordat u zich afmeldt om ze niet te verliezen."; +"settings_key_backup_info_none" = "Jouw sleutels worden niet geback-upt vanaf dit apparaat."; +"settings_key_backup_info_signout_warning" = "Maak een back-up van je sleutels voordat je jezelf afmeldt om ze niet te verliezen."; "settings_key_backup_info_version" = "Sleutelback-upversie: %@"; "settings_key_backup_info_algorithm" = "Algoritme: %@"; -"settings_key_backup_info_valid" = "Dit apparaat maakt een back-up van uw sleutels."; -"settings_key_backup_info_not_valid" = "Dit apparaat maakt geen back-up van uw sleutels, maar u heeft wel een bestaande back-up waarvan u kunt herstellen, en u vanaf dan nieuwe sleutels aan kunt toevoegen."; +"settings_key_backup_info_valid" = "Dit apparaat maakt een back-up van je sleutels."; +"settings_key_backup_info_not_valid" = "Dit apparaat maakt geen back-up van je sleutels, maar je hebt wel een bestaande back-up waarvan je kan herstellen, en je vanaf dan nieuwe sleutels aan kan toevoegen."; "settings_key_backup_info_progress" = "Back-up van %@ sleutels wordt gemaakt…"; "settings_key_backup_info_progress_done" = "Alle sleutels zijn geback-upt"; "settings_key_backup_info_trust_signature_unknown" = "De back-up heeft een ondertekening van apparaat met ID: %@"; @@ -601,25 +601,25 @@ "settings_key_backup_button_restore" = "Herstellen uit back-up"; "settings_key_backup_button_delete" = "Back-up verwijderen"; "settings_key_backup_delete_confirmation_prompt_title" = "Back-up verwijderen"; -"settings_key_backup_delete_confirmation_prompt_msg" = "Weet u het zeker? Als uw sleutels niet correct geback-upt zijn, zult u uw versleutelde berichten verliezen."; +"settings_key_backup_delete_confirmation_prompt_msg" = "Weet je het zeker? Als je sleutels niet correct geback-upt zijn, zal je jouw versleutelde berichten verliezen."; "homeserver_connection_lost" = "Kon geen verbinding maken met de server."; "room_does_not_exist" = "%@ bestaat niet"; // Key backup wrong version "e2e_key_backup_wrong_version_title" = "Nieuwe sleutelback-up"; -"e2e_key_backup_wrong_version" = "Er is een nieuwe sleutelback-up voor versleutelde berichten gedetecteerd.\n\nIndien deze niet van u komt, stel dan een nieuw veiligheidswachtwoord in in de instellingen."; +"e2e_key_backup_wrong_version" = "Er is een nieuwe sleutelback-up voor versleutelde berichten gedetecteerd.\n\nIndien deze niet van jou komt, stel dan een nieuw veiligheidswachtwoord in in de instellingen."; "e2e_key_backup_wrong_version_button_settings" = "Instellingen"; "e2e_key_backup_wrong_version_button_wasme" = "Ik was het"; "key_backup_setup_title" = "Sleutelback-up"; -"key_backup_setup_skip_alert_title" = "Weet u het zeker?"; -"key_backup_setup_skip_alert_message" = "U verliest mogelijk uw versleutelde berichten als u zich afmeldt of uw apparaat verliest."; +"key_backup_setup_skip_alert_title" = "Weet je het zeker?"; +"key_backup_setup_skip_alert_message" = "Je verliest mogelijk je versleutelde berichten als je afmeldt of je apparaat verliest."; "key_backup_setup_skip_alert_skip_action" = "Overslaan"; -"key_backup_setup_intro_title" = "Verlies nooit uw versleutelde berichten"; -"key_backup_setup_intro_info" = "Berichten in versleutelde kamers worden versleuteld met eind-tot-eind-versleuteling. Enkel de ontvanger(s) en u hebben de sleutels om deze berichten te lezen.\n\nMaak een veilige back-up van uw sleutels om deze niet te verliezen."; +"key_backup_setup_intro_title" = "Verlies nooit je versleutelde berichten"; +"key_backup_setup_intro_info" = "Berichten in versleutelde kamers worden versleuteld met eind-tot-eind-versleuteling. Enkel de ontvanger(s) en jij hebben de sleutels om deze berichten te lezen.\n\nMaak een veilige back-up van je sleutels om deze niet te verliezen."; "key_backup_setup_intro_setup_action_without_existing_backup" = "Begin sleutelback-up te gebruiken"; "key_backup_setup_intro_manual_export_info" = "(Geavanceerd)"; "key_backup_setup_intro_manual_export_action" = "Sleutels handmatig exporteren"; -"key_backup_setup_passphrase_title" = "Beveilig uw back-up met een veiligheidswachtwoord"; -"key_backup_setup_passphrase_info" = "We bewaren een versleutelde kopie van uw sleutels op onze server. Bescherm uw back-up met een veiligheidswachtwoord om deze veilig te houden.\n\nVoor maximale beveiliging zou dit moeten verschillen van uw Matrix account wachtwoord."; +"key_backup_setup_passphrase_title" = "Beveilig je back-up met een veiligheidswachtwoord"; +"key_backup_setup_passphrase_info" = "We bewaren een versleutelde kopie van je sleutels op onze server. Bescherm je back-up met een veiligheidswachtwoord om deze veilig te houden.\n\nVoor maximale beveiliging zou dit moeten verschillen van je Matrix account wachtwoord."; "key_backup_setup_passphrase_passphrase_title" = "Invoeren"; "key_backup_setup_passphrase_passphrase_placeholder" = "Wachtwoord invoeren"; "key_backup_setup_passphrase_passphrase_valid" = "Top!"; @@ -629,50 +629,50 @@ "key_backup_setup_passphrase_confirm_passphrase_valid" = "Top!"; "key_backup_setup_passphrase_confirm_passphrase_invalid" = "Wachtwoorden komen niet overeen"; "key_backup_setup_passphrase_set_passphrase_action" = "Wachtwoord instellen"; -"key_backup_setup_passphrase_setup_recovery_key_info" = "Of beveilig uw back-up met een veiligheidssleutel en bewaar deze op een veilige plaats."; +"key_backup_setup_passphrase_setup_recovery_key_info" = "Of beveilig je back-up met een veiligheidssleutel en bewaar deze op een veilige plaats."; "key_backup_setup_passphrase_setup_recovery_key_action" = "(Geavanceerd) Instellen met veiligheidssleutel"; "key_backup_setup_success_title" = "Klaar!"; // Success from passphrase -"key_backup_setup_success_from_passphrase_info" = "Er wordt een back-up van uw sleutels gemaakt.\n\nUw veiligheidssleutel is een veiligheidsnet - u kunt deze gebruiken om de toegang tot uw versleutelde berichten te herstellen als u uw wachtwoord zou vergeten.\n\nBewaar uw veiligheidssleutel op een heel veilige plaats, zoals een wachtwoordbeheerder (of een kluis)."; +"key_backup_setup_success_from_passphrase_info" = "Er wordt een back-up van je sleutels gemaakt.\n\nJe veiligheidssleutel is een veiligheidsnet - je kan deze gebruiken om de toegang tot je versleutelde berichten te herstellen als je jouw wachtwoord zou vergeten.\n\nBewaar je veiligheidssleutel op een heel veilige plaats, zoals een wachtwoordbeheerder (of een kluis)."; "key_backup_setup_success_from_passphrase_save_recovery_key_action" = "Veiligheidssleutel opslaan"; "key_backup_setup_success_from_passphrase_done_action" = "Klaar"; // Success from recovery key -"key_backup_setup_success_from_recovery_key_info" = "Er wordt een back-up van uw sleutels gemaakt.\n\nMaak een kopie van deze veiligheidssleutel en bewaar deze op een veilige plaats."; +"key_backup_setup_success_from_recovery_key_info" = "Er wordt een back-up van je sleutels gemaakt.\n\nMaak een kopie van deze veiligheidssleutel en bewaar deze op een veilige plaats."; "key_backup_setup_success_from_recovery_key_recovery_key_title" = "Veiligheidssleutel"; "key_backup_setup_success_from_recovery_key_make_copy_action" = "Maak een kopie"; "key_backup_setup_success_from_recovery_key_made_copy_action" = "Ik heb een kopie gemaakt"; "key_backup_recover_title" = "Versleutelde berichten"; "key_backup_recover_invalid_passphrase_title" = "Onjuist veiligheidswachtwoord"; -"key_backup_recover_invalid_passphrase" = "De back-up kon niet ontsleuteld worden met dit wachtwoord: controleer of u het veiligheidswachtwoord juist hebt ingevoerd."; +"key_backup_recover_invalid_passphrase" = "De back-up kon niet ontsleuteld worden met dit wachtwoord: controleer of je het veiligheidswachtwoord juist hebt ingevoerd."; "key_backup_recover_invalid_recovery_key_title" = "Veiligheidssleutel komt niet overeen"; -"key_backup_recover_invalid_recovery_key" = "De back-up kon niet ontsleuteld worden met deze sleutel: controleer of u de juiste veiligheidssleutel hebt ingevoerd."; -"key_backup_recover_from_passphrase_info" = "Gebruik uw veiligheidswachtwoord om uw versleutelde berichtengeschiedenis te ontgrendelen"; +"key_backup_recover_invalid_recovery_key" = "De back-up kon niet ontsleuteld worden met deze sleutel: controleer of je de juiste veiligheidssleutel hebt ingevoerd."; +"key_backup_recover_from_passphrase_info" = "Gebruik je veiligheidswachtwoord om je versleutelde berichtengeschiedenis te ontgrendelen"; "key_backup_recover_from_passphrase_passphrase_title" = "Invoeren"; "key_backup_recover_from_passphrase_passphrase_placeholder" = "Wachtwoord invoeren"; "key_backup_recover_from_passphrase_recover_action" = "Geschiedenis ontgrendelen"; -"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Veiligheidswachtwoord vergeten? Dan kunt u "; -"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "uw veiligheidssleutel gebruiken"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Veiligheidswachtwoord vergeten? Dan kan je "; +"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "je veiligheidssleutel gebruiken"; "key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "."; -"key_backup_recover_from_recovery_key_info" = "Gebruik uw veiligheidssleutel om uw versleutelde berichtengeschiedenis te ontgrendelen"; +"key_backup_recover_from_recovery_key_info" = "Gebruik je veiligheidssleutel om je versleutelde berichtengeschiedenis te ontgrendelen"; "key_backup_recover_from_recovery_key_recovery_key_title" = "Invoeren"; "key_backup_recover_from_recovery_key_recovery_key_placeholder" = "Veiligheidssleutel invoeren"; "key_backup_recover_from_recovery_key_recover_action" = "Geschiedenis ontgrendelen"; -"key_backup_recover_from_recovery_key_lost_recovery_key_action" = "Veiligheidssleutel verloren? U kunt er een nieuwe aanmaken in de instellingen."; +"key_backup_recover_from_recovery_key_lost_recovery_key_action" = "Veiligheidssleutel verloren? Je kan een nieuwe aanmaken in de instellingen."; "key_backup_recover_success_info" = "Back-up hersteld!"; "key_backup_recover_done_action" = "Klaar"; "key_backup_setup_banner_title" = "Verlies nooit uw versleutelde berichten"; "key_backup_setup_banner_subtitle" = "Begin sleutelback-up te gebruiken"; "key_backup_recover_banner_title" = "Verlies nooit uw versleutelde berichten"; -"sign_out_existing_key_backup_alert_title" = "Weet u zeker dat u zich wilt afmelden?"; +"sign_out_existing_key_backup_alert_title" = "Weet je zeker dat je wil afmelden?"; "sign_out_existing_key_backup_alert_sign_out_action" = "Uitloggen"; -"sign_out_non_existing_key_backup_alert_title" = "Als u zich nu afmeldt, zult u de toegang tot uw versleutelde berichten verliezen"; +"sign_out_non_existing_key_backup_alert_title" = "Als je nu afmeldt, zal je de toegang tot je versleutelde berichten verliezen"; "sign_out_non_existing_key_backup_alert_setup_key_backup_action" = "Begin sleutelback-up te gebruiken"; "sign_out_non_existing_key_backup_alert_discard_key_backup_action" = "Ik wil mijn versleutelde berichten niet"; -"sign_out_non_existing_key_backup_sign_out_confirmation_alert_title" = "U zult uw versleutelde berichten verliezen"; -"sign_out_non_existing_key_backup_sign_out_confirmation_alert_message" = "U zult de toegang tot uw versleutelde berichten verliezen, tenzij u een back-up van uw sleutels maakt vooraleer u zich afmeldt."; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_title" = "Je zal je versleutelde berichten verliezen"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_message" = "Je zal de toegang tot je versleutelde berichten verliezen, tenzij je een back-up van je sleutels maakt voordat je afmeldt."; "sign_out_non_existing_key_backup_sign_out_confirmation_alert_sign_out_action" = "Uitloggen"; "sign_out_non_existing_key_backup_sign_out_confirmation_alert_backup_action" = "Back-up"; -"sign_out_key_backup_in_progress_alert_title" = "Sleutelback-up is bezig. Als u zich nu afmeldt zult u de toegang tot uw versleutelde berichten verliezen."; +"sign_out_key_backup_in_progress_alert_title" = "Sleutelback-up is bezig. Als je nu afmeldt zal je de toegang tot je versleutelde berichten verliezen."; "sign_out_key_backup_in_progress_alert_discard_key_backup_action" = "Ik wil mijn versleutelde berichten niet"; "sign_out_key_backup_in_progress_alert_cancel_action" = "Ik wacht wel"; "room_event_action_reply" = "Beantwoorden"; @@ -690,8 +690,8 @@ "device_verification_error_cannot_load_device" = "Kan apparaatsinformatie niet laden."; // Mark: Incoming "device_verification_incoming_title" = "Inkomend verificatieverzoek"; -"device_verification_incoming_description_1" = "Verifieer dit apparaat om het als vertrouwd te markeren. Door de apparaten van uw partners te vertrouwen hoeft u zich nog minder zorgen te maken over het gebruik van eind-tot-eind-versleutelde berichten."; -"device_verification_incoming_description_2" = "Dit apparaat verifiëren zal het als vertrouwd markeren, en het ook aan uw gesprekspartner als vertrouwd markeren."; +"device_verification_incoming_description_1" = "Verifieer dit apparaat om het als vertrouwd te markeren. Door de apparaten van je partners te vertrouwen hoef jij je nog minder zorgen te maken over het gebruik van eind-tot-eind-versleutelde berichten."; +"device_verification_incoming_description_2" = "Dit apparaat verifiëren zal het als vertrouwd markeren, en het ook aan je gesprekspartner als vertrouwd markeren."; // MARK: Start "device_verification_start_title" = "Verifieer door een korte tekenreeks te vergelijken"; "device_verification_start_wait_partner" = "Wachten op partner om te aanvaarden…"; @@ -778,16 +778,16 @@ "widget_integrations_server_failed_to_connect" = "Verbinden met integratieserver mislukt"; "device_verification_emoji_lock" = "Slot"; "close" = "Sluiten"; -"auth_softlogout_signed_out" = "U bent afgemeld"; +"auth_softlogout_signed_out" = "Je bent afgemeld"; "auth_softlogout_sign_in" = "Aanmelden"; -"auth_softlogout_reason" = "De beheerder van uw server (%1$@) heeft u van uw account %2$@ afgemeld (%3$@)."; -"auth_softlogout_recover_encryption_keys" = "Meld u aan om de versleutelingssleutels te herstellen die uitsluitend op dit apparaat worden opgeslagen. U heeft ze nodig om uw versleutelde berichten op al uw apparaten te kunnen lezen."; +"auth_softlogout_reason" = "De beheerder van je server (%1$@) heeft je van je account %2$@ afgemeld (%3$@)."; +"auth_softlogout_recover_encryption_keys" = "Meld je aan om de versleutelingssleutels te herstellen die uitsluitend op dit apparaat worden opgeslagen. Je hebt ze nodig om je versleutelde berichten op al je apparaten te kunnen lezen."; "auth_softlogout_clear_data" = "Persoonlijke gegevens wissen"; -"auth_softlogout_clear_data_message_1" = "Let op: uw persoonlijke gegevens (inclusief versleutelingssleutels) worden nog altijd op dit apparaat opgeslagen."; -"auth_softlogout_clear_data_message_2" = "Wis ze indien u dit apparaat niet meer gebruikt, of indien u zich wilt aanmelden met een andere account."; +"auth_softlogout_clear_data_message_1" = "Let op: je persoonlijke gegevens (inclusief versleutelingssleutels) worden nog altijd op dit apparaat opgeslagen."; +"auth_softlogout_clear_data_message_2" = "Wis ze indien je dit apparaat niet meer gebruikt, of indien je jezelf wilt aanmelden met een andere account."; "auth_softlogout_clear_data_button" = "Alle gegevens wissen"; -"auth_softlogout_clear_data_sign_out_title" = "Weet u het zeker?"; -"auth_softlogout_clear_data_sign_out_msg" = "Weet u zeker dat u alle gegevens die op dit moment op dit apparaat worden opgeslagen wilt wissen? Meld u opnieuw aan om toegang te verkrijgen tot uw accountgegevens en berichten."; +"auth_softlogout_clear_data_sign_out_title" = "Weet je het zeker?"; +"auth_softlogout_clear_data_sign_out_msg" = "Weet je zeker dat je alle gegevens die op dit moment op dit apparaat worden opgeslagen wilt wissen? Meld je opnieuw aan om toegang te verkrijgen tot je accountgegevens en berichten."; "auth_softlogout_clear_data_sign_out" = "Uitloggen"; "room_event_action_reaction_show_all" = "Alles tonen"; "room_event_action_reaction_show_less" = "Minder tonen"; @@ -800,8 +800,8 @@ // Image picker "image_picker_action_camera" = "Foto maken"; "image_picker_action_library" = "Kiezen uit mediatheek"; -"camera_unavailable" = "De camera is niet beschikbaar op uw apparaat"; -"photo_library_access_not_granted" = "%@ heeft geen toegang tot de fotobibliotheek, wijzig uw privacy-instellingen"; +"camera_unavailable" = "De camera is niet beschikbaar op jouw apparaat"; +"photo_library_access_not_granted" = "%@ heeft geen toegang tot de fotobibliotheek, wijzig je privacy-instellingen"; // MARK: File upload "file_upload_error_title" = "Bestand uploaden"; "file_upload_error_unsupported_file_type_message" = "Bestandstype niet ondersteund."; @@ -817,9 +817,9 @@ "emoji_picker_flags_category" = "Vlaggen"; // MARK: Reaction history "reaction_history_title" = "Reacties"; -"auth_forgot_password_error_no_configured_identity_server" = "Er is geen identiteitsserver geconfigureerd: voeg er een toe om uw Matrix account wachtwoord opnieuw in te stellen."; -"room_creation_error_invite_user_by_email_without_identity_server" = "Er is geen identiteitsserver geconfigureerd, dus u kunt geen deelnemers toevoegen via e-mail."; -"room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Er is geen identiteitsserver geconfigureerd, dus u kunt geen gesprek beginnen met een contact via e-mail."; +"auth_forgot_password_error_no_configured_identity_server" = "Er is geen identiteitsserver geconfigureerd: voeg er een toe om je Matrix account wachtwoord opnieuw in te stellen."; +"room_creation_error_invite_user_by_email_without_identity_server" = "Er is geen identiteitsserver geconfigureerd, dus je kan geen deelnemers toevoegen via e-mail."; +"room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Er is geen identiteitsserver geconfigureerd, dus je kan geen gesprek beginnen met een contact via e-mail."; // Service terms "service_terms_modal_title" = "Dienstvoorwaarden"; "service_terms_modal_message" = "Om door te gaan dient u de dienstvoorwaarden te aanvaarden."; @@ -827,8 +827,8 @@ "service_terms_modal_description_for_identity_server" = "Wees vindbaar voor anderen"; "service_terms_modal_description_for_integration_manager" = "Gebruik bots, bruggen, widgets en stickerpakketten"; // Errors -"error_user_already_logged_in" = "Het lijkt alsof u probeert te verbinden met een andere server. Wilt u zich afmelden?"; -"room_participants_remove_third_party_invite_prompt_msg" = "Weet u zeker dat u deze uitnodiging wilt intrekken?"; +"error_user_already_logged_in" = "Het lijkt alsof je probeert te verbinden met een andere server. Wil je jezelf afmelden?"; +"room_participants_remove_third_party_invite_prompt_msg" = "Weet je zeker dat je deze uitnodiging wil intrekken?"; "room_accessiblity_scroll_to_bottom" = "Scrollen naar onderen"; "room_accessibility_search" = "Zoeken"; "room_accessibility_integrations" = "Integraties"; @@ -847,34 +847,34 @@ // Accessibility "accessibility_checkbox_label" = "aanvinkvak"; "auth_add_email_message_2" = "Stel een e-mailadres in voor accountherstel en om later optioneel vindbaar te zijn voor mensen die u kennen."; -"auth_add_phone_message_2" = "Stel een telefoon in om later optioneel vindbaar te zijn voor mensen die u kennen."; -"auth_add_email_phone_message_2" = "Stel een e-mailadres in voor accountherstel. Gebruik later e-mail of telefoon om optioneel vindbaar te zijn voor mensen die u kennen."; -"auth_email_is_required" = "Er is geen identiteitsserver geconfigureerd, dus u kunt geen e-mailadres toevoegen om uw Matrix account wachtwoord in de toekomst opnieuw in te stellen."; -"auth_phone_is_required" = "Er is geen identiteitsserver geconfigureerd, dus u kunt geen telefoonnummer toevoegen om uw Matrix account wachtwoord in de toekomst opnieuw in te stellen."; -"auth_reset_password_error_is_required" = "Geen identiteitsserver ingesteld: voeg er één toe bij server opties om uw Matrix account wachtwoord te wijzigen."; +"auth_add_phone_message_2" = "Stel een telefoon in om later optioneel vindbaar te zijn voor mensen die jou kennen."; +"auth_add_email_phone_message_2" = "Stel een e-mailadres in voor accountherstel. Gebruik later e-mail of telefoon om optioneel vindbaar te zijn voor mensen die jou kennen."; +"auth_email_is_required" = "Er is geen identiteitsserver geconfigureerd, dus je kan geen e-mailadres toevoegen om je Matrix account wachtwoord in de toekomst opnieuw in te stellen."; +"auth_phone_is_required" = "Er is geen identiteitsserver geconfigureerd, dus je kan geen telefoonnummer toevoegen om je Matrix account wachtwoord in de toekomst opnieuw in te stellen."; +"auth_reset_password_error_is_required" = "Geen identiteitsserver ingesteld: voeg er één toe bij server opties om je Matrix account wachtwoord te wijzigen."; // MARK: - Secrets set up // Recovery Key -"secrets_setup_recovery_key_title" = "Sla uw veiligheidssleutel op"; +"secrets_setup_recovery_key_title" = "Sla je veiligheidssleutel op"; "secure_key_backup_setup_intro_use_security_key_info" = "Genereer een veiligheidssleutel, die op een veilige plek opgeslagen kan worden zoals een wachtwoordmanager."; "secure_key_backup_setup_intro_use_security_key_title" = "Gebruik een veiligheidssleutel"; -"secrets_setup_recovery_passphrase_summary_information" = "Onthoud uw veiligheidswachtwoord. Het kan gebruikt worden voor het ontgrendelen van uw versleutelde berichten en data."; -"secrets_setup_recovery_passphrase_summary_title" = "Sla uw veiligheidswachtwoord op"; -"secrets_setup_recovery_passphrase_confirm_information" = "Voer uw veiligheidswachtwoord opnieuw in om het te bevestigen."; -"secrets_setup_recovery_passphrase_information" = "Voer een veiligheidswachtwoord in die alleen u kent om uw gegevens te beveiligen op uw server."; +"secrets_setup_recovery_passphrase_summary_information" = "Onthoud je veiligheidswachtwoord. Het kan gebruikt worden voor het ontgrendelen van je versleutelde berichten en data."; +"secrets_setup_recovery_passphrase_summary_title" = "Sla je veiligheidswachtwoord op"; +"secrets_setup_recovery_passphrase_confirm_information" = "Voer je veiligheidswachtwoord opnieuw in om het te bevestigen."; +"secrets_setup_recovery_passphrase_information" = "Voer een veiligheidswachtwoord in die alleen jij kent om je gegevens te beveiligen op je server."; // Recovery passphrase "secrets_setup_recovery_passphrase_title" = "Veiligheidswachtwoord instellen"; -"secure_key_backup_setup_intro_use_security_passphrase_info" = "Voer een veiligheidswachtwoord in die alleen u weet, en genereer een veiligheidssleutel voor uw back-up."; +"secure_key_backup_setup_intro_use_security_passphrase_info" = "Voer een veiligheidswachtwoord in die alleen jij weet, en genereer een veiligheidssleutel voor je back-up."; "secure_key_backup_setup_intro_use_security_passphrase_title" = "Gebruik een veiligheidswachtwoord"; -"secrets_setup_recovery_key_storage_alert_message" = "✓ Print het en bewaar het op een veilige plek\n✓ Sla het op een USB-sleutel of back-upschijf op\n✓ Kopieer het naar uw persoonlijke cloud opslag"; -"room_intro_cell_information_multiple_dm_sentence2" = "Alleen u bent in dit gesprek, tenzij u iemand uitnodigd voor dit gesprek."; -"room_intro_cell_information_dm_sentence2" = "Alleen u twee zijn in dit gesprek, niemand anders kan deelnemen."; +"secrets_setup_recovery_key_storage_alert_message" = "✓ Print het en bewaar het op een veilige plek\n✓ Sla het op een USB-sleutel of back-upschijf op\n✓ Kopieer het naar je persoonlijke cloud opslag"; +"room_intro_cell_information_multiple_dm_sentence2" = "Alleen jij bent in dit gesprek, tenzij je iemand uitnodigt voor dit gesprek."; +"room_intro_cell_information_dm_sentence2" = "Alleen jullie twee zijn in dit gesprek, niemand anders kan deelnemen."; "room_intro_cell_information_dm_sentence1_part3" = ". "; -"room_intro_cell_information_dm_sentence1_part1" = "Dit is het begin van uw directe gesprek met "; +"room_intro_cell_information_dm_sentence1_part1" = "Dit is het begin van je directe gesprek met "; "room_intro_cell_information_room_without_topic_sentence2_part2" = " om personen te laten weten waar deze kamer over gaat."; "room_intro_cell_information_room_without_topic_sentence2_part1" = "Voeg een onderwerp toe"; "room_intro_cell_information_room_with_topic_sentence2" = "Onderwerp: %@"; @@ -894,7 +894,7 @@ // MARK: - Invite friends "invite_friends_action" = "Vrienden tot %@ uitnodigen"; -"favourites_empty_view_information" = "U kunt op een paar manieren favorieten maken - de snelste is om een gesprek ingedrukt te houden. Tik op de ster en ze verschijnen hier automatisch om veilig te bewaren."; +"favourites_empty_view_information" = "Je kan op een paar manieren favorieten maken - de snelste is om een gesprek ingedrukt te houden. Tik op de ster en ze verschijnen hier automatisch om veilig te bewaren."; // MARK: - Favourites @@ -949,11 +949,11 @@ "biometrics_cant_unlocked_alert_message_retry" = "Opnieuw proberen"; "biometrics_cant_unlocked_alert_message_login" = "Log opnieuw in"; "biometrics_cant_unlocked_alert_message_x" = "Gebruik %@ om te ontgrendelen of log opnieuw in en schakel %@ opnieuw in"; -"biometrics_cant_unlocked_alert_title" = "Kan uw app niet ontgrendelen"; -"biometrics_usage_reason" = "Authenticatie is nodig om toegang te krijgen tot uw app"; +"biometrics_cant_unlocked_alert_title" = "Kan je app niet ontgrendelen"; +"biometrics_usage_reason" = "Authenticatie is nodig om toegang te krijgen tot je app"; "biometrics_desetup_disable_button_title_x" = "%@ uitschakelen"; "biometrics_desetup_title_x" = "%@ uitschakelen"; -"biometrics_setup_subtitle" = "Bespaar uzelf tijd"; +"biometrics_setup_subtitle" = "Bespaar jezelf tijd"; "biometrics_setup_enable_button_title_x" = "%@ inschakelen"; "biometrics_setup_title_x" = "%@ inschakelen"; "biometrics_settings_enable_x" = "%@ inschakelen"; @@ -962,27 +962,27 @@ // MARK: - Biometrics Protection "biometrics_mode_touch_id" = "Touch ID"; -"pin_protection_kick_user_alert_message" = "Te veel fouten, u bent uitgelogd"; -"pin_protection_explanatory" = "Met een PIN beschermt u uw data zoals uw berichten en contacten. Zo heeft alleen u toegang na het invoeren van uw PIN bij het opstarten van de app."; +"pin_protection_kick_user_alert_message" = "Te veel fouten, je bent uitgelogd"; +"pin_protection_explanatory" = "Met een PIN beschermt je jouw data zoals je berichten en contacten. Zo heeft alleen jij toegang na het invoeren van je PIN bij het opstarten van de app."; "pin_protection_not_allowed_pin" = "Om beveiligingsredenen is deze PIN niet beschikbaar. Probeer een andere PIN"; "pin_protection_settings_change_pin" = "PIN wijzigen"; "pin_protection_settings_enable_pin" = "PIN inschakelen"; "pin_protection_settings_enabled_forced" = "PIN ingeschakeld"; -"pin_protection_settings_section_footer" = "Om uw PIN opnieuw in te stellen moet u straks opnieuw inloggen en een nieuwe PIN aanmaken."; +"pin_protection_settings_section_footer" = "Om je PIN opnieuw in te stellen moet je straks opnieuw inloggen en een nieuwe PIN aanmaken."; "pin_protection_settings_section_header" = "PIN"; "pin_protection_settings_section_header_with_biometrics" = "PIN en %@"; -"pin_protection_mismatch_too_many_times_error_message" = "Als u uw PIN bent vergeten, klik op de PIN vergeten-knop."; +"pin_protection_mismatch_too_many_times_error_message" = "Als je jouw PIN bent vergeten, klik op de PIN vergeten-knop."; "pin_protection_mismatch_error_message" = "Probeer het opnieuw"; "pin_protection_mismatch_error_title" = "PIN's zijn niet hetzelfde"; "pin_protection_reset_alert_action_reset" = "Opnieuw instellen"; -"pin_protection_reset_alert_message" = "Om uw PIN opnieuw in te stellen moet u strak opnieuw inloggen en een nieuwe PIN aanmaken"; +"pin_protection_reset_alert_message" = "Om je PIN opnieuw in te stellen moet je strak opnieuw inloggen en een nieuwe PIN aanmaken"; "pin_protection_reset_alert_title" = "PIN opnieuw instellen"; "pin_protection_forgot_pin" = "PIN vergeten"; -"pin_protection_enter_pin" = "Voor uw PIN in"; -"pin_protection_confirm_pin_to_change" = "Bevestig uw PIN om uw PIN te wijzigen"; -"pin_protection_confirm_pin_to_disable" = "Bevestig uw PIN om uw PIN uit te schakelen"; -"pin_protection_confirm_pin" = "Bevestig uw PIN"; -"pin_protection_choose_pin" = "Maak een PIN voor uw veiligheid"; +"pin_protection_enter_pin" = "Voer je PIN in"; +"pin_protection_confirm_pin_to_change" = "Bevestig je PIN om je PIN te wijzigen"; +"pin_protection_confirm_pin_to_disable" = "Bevestig je PIN om je PIN uit te schakelen"; +"pin_protection_confirm_pin" = "Bevestig je PIN"; +"pin_protection_choose_pin" = "Maak een PIN voor je veiligheid"; "pin_protection_choose_pin_welcome_after_register" = "Welkom."; // MARK: - PIN Protection @@ -990,12 +990,12 @@ "pin_protection_choose_pin_welcome_after_login" = "Welkom terug."; "major_update_done_action" = "Ik snap het"; "major_update_learn_more_action" = "Leer meer"; -"major_update_information" = "We zijn enthousiast om onze nieuwe naam aan te kondigen! Uw app is up-to-date en u bent ingelogd op uw account."; +"major_update_information" = "We zijn enthousiast om onze nieuwe naam aan te kondigen! Je app is up-to-date en je bent ingelogd op je account."; // MARK: - Major update "major_update_title" = "Riot is nu %@"; -"cross_signing_setup_banner_subtitle" = "Verifieer uw andere apparaat gemakkelijker"; +"cross_signing_setup_banner_subtitle" = "Verifieer je andere apparaat gemakkelijker"; // MARK: - Cross-signing @@ -1004,9 +1004,9 @@ "cross_signing_setup_banner_title" = "Versleuteling instellen"; "secrets_reset_authentication_message" = "Geef het Matrix account wachtwoord in om te bevestigen"; "secrets_reset_reset_action" = "Opnieuw instellen"; -"secrets_reset_warning_message" = "U zult opnieuw starten zonder geschiedenis, berichten, vertrouwde apparaten en vertrouwde personen."; -"secrets_reset_warning_title" = "Als u alles terugzet"; -"secrets_reset_information" = "Doe dit alleen, wanneer u geen ander apparaat heeft om dit apparaat mee te verifiëren."; +"secrets_reset_warning_message" = "Je zal opnieuw starten zonder geschiedenis, berichten, vertrouwde apparaten en vertrouwde personen."; +"secrets_reset_warning_title" = "Als je alles terugzet"; +"secrets_reset_information" = "Doe dit alleen, wanneer je geen ander apparaat hebt om dit apparaat mee te verifiëren."; // MARK: - Secrets reset @@ -1019,28 +1019,28 @@ "secrets_setup_recovery_key_done_action" = "Klaar"; "secrets_setup_recovery_key_export_action" = "Opslaan"; "secrets_setup_recovery_key_loading" = "Laden…"; -"secrets_setup_recovery_key_information" = "Bewaar uw veiligheidssleutel op een veilige plek. Deze kan gebruikt worden om uw versleutelde berichten en data te ontsleutelen."; -"secrets_recovery_with_key_invalid_recovery_key_message" = "Verifieer dat u de juiste veiligheidssleutel heeft ingevoerd."; +"secrets_setup_recovery_key_information" = "Bewaar je veiligheidssleutel op een veilige plek. Deze kan gebruikt worden om je versleutelde berichten en data te ontsleutelen."; +"secrets_recovery_with_key_invalid_recovery_key_message" = "Verifieer dat je de juiste veiligheidssleutel hebt ingevoerd."; "secrets_recovery_with_key_invalid_recovery_key_title" = "Geen toegang tot geheime opslag"; "secrets_recovery_with_key_recover_action" = "Gebruik sleutel"; "secrets_recovery_with_key_recovery_key_placeholder" = "Veiligheidssleutel invoeren"; "secrets_recovery_with_key_recovery_key_title" = "Invoeren"; -"secrets_recovery_with_key_information_verify_device" = "Gebruik uw veiligheidssleutel om dit apparaat te verifiëren."; -"secrets_recovery_with_key_information_default" = "Ontvang toegang tot uw versleutelde berichtengeschiedenis en kruislings ondertekenen voor het verifiëren van andere sessie door het invoeren van uw veiligheidssleutel."; +"secrets_recovery_with_key_information_verify_device" = "Gebruik je veiligheidssleutel om dit apparaat te verifiëren."; +"secrets_recovery_with_key_information_default" = "Ontvang toegang tot je versleutelde berichtengeschiedenis en kruislings ondertekenen voor het verifiëren van andere sessie door het invoeren van je veiligheidssleutel."; // Recover with key "secrets_recovery_with_key_title" = "Veiligheidssleutel"; -"secrets_recovery_with_passphrase_invalid_passphrase_message" = "Verifieer dat u het juiste veiligheidswachtwoord heeft ingevoerd."; +"secrets_recovery_with_passphrase_invalid_passphrase_message" = "Verifieer dat je het juiste veiligheidswachtwoord hebt ingevoerd."; "secrets_recovery_with_passphrase_invalid_passphrase_title" = "Geen toegang tot geheime opslag"; "secrets_recovery_with_passphrase_lost_passphrase_action_part3" = "."; -"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "uw veiligheidssleutel gebruiken"; -"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "Veiligheidswachtwoord vergeten? Dan kunt u "; +"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "je veiligheidssleutel gebruiken"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "Veiligheidswachtwoord vergeten? Dan kan je "; "secrets_recovery_with_passphrase_recover_action" = "Gebruik wachtwoord"; -"secrets_recovery_with_passphrase_passphrase_placeholder" = "Voer uw veiligheidswachtwoord in"; +"secrets_recovery_with_passphrase_passphrase_placeholder" = "Voer je veiligheidswachtwoord in"; "secrets_recovery_with_passphrase_passphrase_title" = "Invoeren"; -"secrets_recovery_with_passphrase_information_verify_device" = "Gebruik uw veiligheidswachtwoord om dit apparaat te verifiëren."; -"secrets_recovery_with_passphrase_information_default" = "Ontvang toegang tot uw versleutelde berichtengeschiedenis en kruislings ondertekenen voor het verifiëren van andere sessies door het invoeren van uw veiligheidswachtwoord."; +"secrets_recovery_with_passphrase_information_verify_device" = "Gebruik je veiligheidswachtwoord om dit apparaat te verifiëren."; +"secrets_recovery_with_passphrase_information_default" = "Ontvang toegang tot je versleutelde berichtengeschiedenis en kruislings ondertekenen voor het verifiëren van andere sessies door het invoeren van je veiligheidswachtwoord."; // Recover with passphrase @@ -1053,13 +1053,13 @@ "user_verification_session_details_verify_action_other_user" = "Handmatig verifiëren"; "user_verification_session_details_verify_action_current_user_manually" = "Handmatig middels een tekst"; "user_verification_session_details_verify_action_current_user" = "Interactief verifiëren"; -"user_verification_session_details_additional_information_untrusted_current_user" = "Als u zich niet heeft aangemeld bij deze sessie, is uw account wellicht geschonden."; +"user_verification_session_details_additional_information_untrusted_current_user" = "Als je niet bent aangemeld bij deze sessie, is je account wellicht geschonden."; "user_verification_session_details_additional_information_untrusted_other_user" = "Totdat deze persoon deze sessie vertrouwd, zijn berichten gelabeld met waarschuwingen. Een andere mogelijkheid is om de persoon handmatig te verifiëren."; "user_verification_session_details_information_untrusted_other_user" = " heeft zich in een nieuwe sessie aangemeld:"; "user_verification_session_details_information_untrusted_current_user" = "Verifieer deze sessie om het als vertrouwd te markeren en het toegang te geven tot versleutelde berichten:"; "user_verification_session_details_information_trusted_other_user_part2" = " verifieer het:"; "user_verification_session_details_information_trusted_other_user_part1" = "Deze sessie is vertrouwd voor veilige communicatie omdat "; -"user_verification_session_details_information_trusted_current_user" = "Deze sessie is vertrouwd voor veilige communicatie omdat u het heeft geverifieerd:"; +"user_verification_session_details_information_trusted_current_user" = "Deze sessie is vertrouwd voor veilige communicatie omdat je het hebt geverifieerd:"; "user_verification_session_details_untrusted_title" = "Niet vertrouwd"; // Session details @@ -1118,9 +1118,9 @@ "key_verification_tile_conclusion_done_title" = "Geverifieerd"; "key_verification_tile_request_incoming_approval_decline" = "Weigeren"; "key_verification_tile_request_incoming_approval_accept" = "Aanvaarden"; -"key_verification_tile_request_status_accepted" = "U heeft aanvaard"; +"key_verification_tile_request_status_accepted" = "Je hebt aanvaard"; "key_verification_tile_request_status_cancelled" = "%@ heeft geannuleerd"; -"key_verification_tile_request_status_cancelled_by_me" = "U heeft geannuleerd"; +"key_verification_tile_request_status_cancelled_by_me" = "Je hebt geannuleerd"; "key_verification_tile_request_status_expired" = "Verlopen"; "key_verification_tile_request_status_waiting" = "Wachten…"; "key_verification_tile_request_status_data_loading" = "Gegevens laden…"; @@ -1129,30 +1129,30 @@ // Tiles "key_verification_tile_request_incoming_title" = "Verificatieverzoek"; -"key_verification_bootstrap_not_setup_message" = "U moet kruislings ondertekenen eerst instellen."; +"key_verification_bootstrap_not_setup_message" = "Je moet kruislings ondertekenen eerst instellen."; // MARK: - Key Verification "key_verification_bootstrap_not_setup_title" = "Fout"; -"error_not_supported_on_mobile" = "U kunt dit niet doen vanaf de mobiele %@."; +"error_not_supported_on_mobile" = "Je kan dit niet doen vanaf de mobiele %@."; // Generic errors -"error_invite_3pid_with_no_identity_server" = "Voeg een identiteitsserver toe in uw instellingen, om per e-mail uit te kunnen nodigen."; +"error_invite_3pid_with_no_identity_server" = "Voeg een identiteitsserver toe in je instellingen, om per e-mail uit te kunnen nodigen."; // User "key_verification_verified_user_information" = "Berichten met deze personen zijn eind-tot-eind-versleuteld en kunnen niet door derde partijen gelezen worden."; -"key_verification_verified_this_session_information" = "U kunt nu veilig uw berichten op dit apparaat lezen. Andere personen weten dat zij u kunnen vertrouwen."; -"key_verification_verified_new_session_information" = "U kunt nu veilig uw berichten op uw andere apparaat lezen. Andere personen weten dat zij u kunnen vertrouwen."; -"key_verification_verified_other_session_information" = "U kunt nu veilig uw berichten in uw andere sessie lezen. Andere personen weten dat zij u kunnen vertrouwen."; +"key_verification_verified_this_session_information" = "Je kan nu veilig je berichten op dit apparaat lezen. Andere personen weten dat zij jou kunnen vertrouwen."; +"key_verification_verified_new_session_information" = "Je kan nu veilig je berichten op je andere apparaat lezen. Andere personen weten dat zij jou kunnen vertrouwen."; +"key_verification_verified_other_session_information" = "Je kan nu veilig je berichten in je andere sessie lezen. Andere personen weten dat zij jou kunnen vertrouwen."; "key_verification_verified_new_session_title" = "Nieuwe sessie geverifieerd!"; "key_verification_manually_verify_device_validate_action" = "Verifiëren"; "key_verification_manually_verify_device_additional_information" = "Als deze niet overeenkomen, dan wordt deze sessie mogelijk door iemand anders onderschept."; "key_verification_manually_verify_device_key_title" = "Sessiesleutel"; "key_verification_manually_verify_device_id_title" = "Sessie-ID"; "key_verification_manually_verify_device_name_title" = "Sessienaam"; -"key_verification_manually_verify_device_instruction" = "Om te verifiëren dat deze sessie vertrouwd kan worden, contacteert u de eigenaar via een andere methode (bv. persoonlijk of via een telefoontje) en vraagt u hem/haar of de sleutel die hij/zij ziet in zijn/haar Persoonsinstellingen van deze sessie overeenkomt met de sleutel hieronder:"; +"key_verification_manually_verify_device_instruction" = "Om te verifiëren dat deze sessie vertrouwd kan worden, contacteer je de eigenaar via een andere methode (bv. persoonlijk of via een telefoontje) en vraag je hen of de sleutel die hen zien in hun Persoonsinstellingen van deze sessie overeenkomt met de sleutel hieronder:"; // MARK: Manually Verify Device @@ -1166,22 +1166,22 @@ "key_verification_verify_sas_title_emoji" = "Vergelijk de emoji's"; "device_verification_self_verify_wait_recover_secrets_checking_availability" = "Controleren op andere verificatie mogelijkheden..."; -"device_verification_self_verify_wait_recover_secrets_additional_information" = "Wanneer u geen toegang meer heeft tot een bestaande sessie"; -"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Uw veiligheidswachtwoord of -sleutel gebruiken"; +"device_verification_self_verify_wait_recover_secrets_additional_information" = "Wanneer je geen toegang meer hebt tot een bestaande sessie"; +"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Je veiligheidswachtwoord of -sleutel gebruiken"; "device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Veiligheidssleutel gebruiken"; "device_verification_self_verify_wait_additional_information" = "Dit werkt met %@ en andere Matrix-apps die kruislings ondertekenen ondersteunen."; -"device_verification_self_verify_wait_information" = "Verifieer deze sessie vanaf een van uw andere sessies, om toegang te krijgen tot de versleutelde berichten.\n\nGebruik de laatste versie van %@ op uw andere apparaten:"; +"device_verification_self_verify_wait_information" = "Verifieer deze sessie vanaf een van je andere sessies, om toegang te krijgen tot de versleutelde berichten.\n\nGebruik de laatste versie van %@ op je andere apparaten:"; "device_verification_self_verify_wait_new_sign_in_title" = "Verifieer deze login"; // MARK: Self verification wait "device_verification_self_verify_wait_title" = "Beveiliging afronden"; "key_verification_self_verify_unverified_sessions_alert_validate_action" = "Nakijken"; -"key_verification_self_verify_unverified_sessions_alert_message" = "Controleer al uw sessies om zeker te zijn dat uw account & berichten veilig zijn."; +"key_verification_self_verify_unverified_sessions_alert_message" = "Controleer al je sessies om zeker te zijn dat je account & berichten veilig zijn."; // Unverified sessions -"key_verification_self_verify_unverified_sessions_alert_title" = "Kijk na waar u aangemeld bent"; +"key_verification_self_verify_unverified_sessions_alert_title" = "Kijk na waar je aangemeld bent"; "key_verification_self_verify_current_session_alert_validate_action" = "Verifiëren"; "key_verification_self_verify_current_session_alert_message" = "Mogelijk wantrouwen anderen het."; @@ -1189,43 +1189,43 @@ "key_verification_self_verify_current_session_alert_title" = "Deze sessie verifiëren"; "device_verification_self_verify_start_waiting" = "Wachten…"; -"device_verification_self_verify_start_information" = "Gebruik deze sessie om uw nieuwe sessie te verifiëren, waardoor deze laatste toegang verkrijgt tot versleutelde berichten."; +"device_verification_self_verify_start_information" = "Gebruik deze sessie om je nieuwe sessie te verifiëren, waardoor deze laatste toegang verkrijgt tot versleutelde berichten."; "device_verification_self_verify_start_verify_action" = "Verificatie starten"; "device_verification_self_verify_alert_validate_action" = "Verifiëren"; -"device_verification_self_verify_alert_message" = "Verifieer de nieuwe aanmelding op uw account: %@"; +"device_verification_self_verify_alert_message" = "Verifieer de nieuwe aanmelding op je account: %@"; // MARK: Self verification start // New login -"device_verification_self_verify_alert_title" = "Nieuwe aanmelding - was u dat?"; +"device_verification_self_verify_alert_title" = "Nieuwe aanmelding - was jij dat?"; "device_verification_security_advice_number" = "Vergelijk de cijfers en stel vast dat deze in dezelfde volgorde worden weergeven."; "device_verification_security_advice_emoji" = "Vergelijk de emoji's en stel vast dat deze in dezelfde volgorde worden weergeven."; "key_verification_user_title" = "Verifieer hen"; "key_verification_this_session_title" = "Deze sessie verifiëren"; -"key_verification_new_session_title" = "Verifieer uw nieuwe sessie"; +"key_verification_new_session_title" = "Verifieer je nieuwe sessie"; // MARK: - Device Verification "key_verification_other_session_title" = "Sessie verifiëren"; -"sign_out_non_existing_key_backup_alert_setup_secure_backup_action" = "Gebruik uw Veilige Back-up"; +"sign_out_non_existing_key_backup_alert_setup_secure_backup_action" = "Gebruik je Veilige Back-up"; // Recover from private key "key_backup_recover_from_private_key_info" = "Back-up wordt hersteld…"; -"secure_backup_setup_banner_subtitle" = "Bescherm uzelf tegen toegangsverlies van uw versleutelde berichten en data"; +"secure_backup_setup_banner_subtitle" = "Bescherm jezelf tegen toegangsverlies van je versleutelde berichten en data"; // Banner "secure_backup_setup_banner_title" = "Veilige Back-up"; -"secure_key_backup_setup_cancel_alert_message" = "Wanneer u nu afbreekt en de toegang tot uw sessies verliest, kunt u de versleutelde berichten en data kwijtraken.\n\nU kunt ook een back-up instellen en uw sleutels beheren in de Instellingen."; +"secure_key_backup_setup_cancel_alert_message" = "Wanneer je nu afbreekt en de toegang tot je sessies verliest, kan je de versleutelde berichten en data kwijtraken.\n\nJe kan ook een back-up instellen en je sleutels beheren in de Instellingen."; // Cancel -"secure_key_backup_setup_cancel_alert_title" = "Weet u het zeker?"; +"secure_key_backup_setup_cancel_alert_title" = "Weet je het zeker?"; "secure_key_backup_setup_existing_backup_error_delete_it" = "Verwijder het"; "secure_key_backup_setup_existing_backup_error_unlock_it" = "Ontsleutelen"; "secure_key_backup_setup_existing_backup_error_info" = "Ontgrendel het om de Veilige Back-up te gebruiken of verwijder het om een nieuwe berichten back-up te beginnen in de Veilige Back-up."; "secure_key_backup_setup_existing_backup_error_title" = "Er bestaat al een back-up voor berichten"; -"secure_key_backup_setup_intro_info" = "Bescherm uzelf tegen het verliezen van uw versleutelde berichten en data door uw versleutelingssleutels te back-uppen op onze server."; +"secure_key_backup_setup_intro_info" = "Bescherm jezelf tegen het verliezen van je versleutelde berichten en data door je versleutelingssleutels te back-uppen op onze server."; // MARK: Secure backup setup @@ -1241,10 +1241,10 @@ "service_terms_modal_decline_button" = "Weigeren"; "room_widget_permission_room_id_permission" = "Kamer-ID"; "room_widget_permission_widget_id_permission" = "Widget-ID"; -"room_widget_permission_theme_permission" = "Uw thema"; -"room_widget_permission_user_id_permission" = "Uw persoon-ID"; -"room_widget_permission_avatar_url_permission" = "Uw afbeelding-URL"; -"room_widget_permission_display_name_permission" = "Uw weergavenaam"; +"room_widget_permission_theme_permission" = "Jouw thema"; +"room_widget_permission_user_id_permission" = "Jouw persoon-ID"; +"room_widget_permission_avatar_url_permission" = "Jouw afbeelding-URL"; +"room_widget_permission_display_name_permission" = "Jouw weergavenaam"; "room_widget_permission_information_title" = "Dit gebruiken kan gegevens delen met %@:\n"; "room_widget_permission_webview_information_title" = "Dit gebruiken kan cookies toevoegen en gegevens delen met %@:\n"; "room_widget_permission_creator_info_title" = "Deze widget is toegevoegd door:"; @@ -1252,7 +1252,7 @@ // Room widget permissions "room_widget_permission_title" = "Widget laden"; "widget_picker_manage_integrations" = "Beheer integraties…"; -"widget_integration_manager_disabled" = "U moet integratiebeheer inschakelen in de instellingen"; +"widget_integration_manager_disabled" = "Je moet integratiebeheer inschakelen in de instellingen"; "widget_menu_remove" = "Verwijderen voor iedereen"; "widget_menu_revoke_permission" = "Toegang intrekken voor mij"; "widget_menu_open_outside" = "Openen in browser"; @@ -1260,15 +1260,15 @@ "bug_report_background_mode" = "Op de achtergrond doorgaan"; "call_actions_unhold" = "Hervatten"; "call_no_stun_server_error_use_fallback_button" = "Probeer %@ te gebruiken"; -"call_no_stun_server_error_message_2" = "U kunt ook de publieke server op %@ gebruiken, maar dit zal minder betrouwbaar zijn, en zal uw IP-adres met die server delen. U kunt dit ook beheren in de Instellingen"; -"call_no_stun_server_error_message_1" = "Vraag uw serverbeheerder %@ een TURN-server te configureren teneinde oproepen betrouwbaar te doen werken."; +"call_no_stun_server_error_message_2" = "Je kan ook de publieke server op %@ gebruiken, maar dit zal minder betrouwbaar zijn, en zal je IP-adres met die server delen. Je kan dit ook beheren in de Instellingen"; +"call_no_stun_server_error_message_1" = "Vraag je serverbeheerder %@ een TURN-server te configureren teneinde oproepen betrouwbaar te doen werken."; "call_no_stun_server_error_title" = "Oproep mislukt door verkeerd geconfigureerde server"; -"event_formatter_jitsi_widget_removed_by_you" = "U heeft een VoIP-vergadering verwijderd"; -"event_formatter_jitsi_widget_added_by_you" = "U heeft een VoIP-vergadering toegevoegd"; -"event_formatter_widget_removed_by_you" = "U heeft deze widget verwijderd: %@"; +"event_formatter_jitsi_widget_removed_by_you" = "Je hebt een VoIP-vergadering verwijderd"; +"event_formatter_jitsi_widget_added_by_you" = "Je hebt een VoIP-vergadering toegevoegd"; +"event_formatter_widget_removed_by_you" = "Je hebt deze widget verwijderd: %@"; // Events formatter with you -"event_formatter_widget_added_by_you" = "U heeft deze widget toegevoegd: %@"; +"event_formatter_widget_added_by_you" = "Je hebt deze widget toegevoegd: %@"; "event_formatter_call_back" = "Terugbellen"; "event_formatter_call_you_declined" = "Oproep geweigerd"; "event_formatter_call_you_currently_in" = "Actieve oproep"; @@ -1287,40 +1287,40 @@ "room_details_photo_for_dm" = "Foto"; "room_details_title_for_dm" = "Details"; "identity_server_settings_alert_error_invalid_identity_server" = "%@ is geen geldige identiteitsserver."; -"identity_server_settings_alert_error_terms_not_accepted" = "U moet de voorwaarden van %@ aanvaarden om deze als identiteitsserver in te stellen."; +"identity_server_settings_alert_error_terms_not_accepted" = "Je moet de voorwaarden van %@ aanvaarden om deze als identiteitsserver in te stellen."; "identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "Verbinding toch verbreken"; -"identity_server_settings_alert_disconnect_still_sharing_3pid" = "U deelt nog persoonlijke gegevens op de identiteitsserver %@.\n\nVoordat u de verbinding verbreekt wordt het u aangeraden uw e-mailadressen en telefoonnummers van de identiteitsserver te verwijderen."; +"identity_server_settings_alert_disconnect_still_sharing_3pid" = "Je deelt nog persoonlijke gegevens op de identiteitsserver %@.\n\nVoordat je de verbinding verbreekt wordt het je aangeraden je e-mailadressen en telefoonnummers van de identiteitsserver te verwijderen."; "identity_server_settings_alert_disconnect_button" = "Verbinding verbreken"; -"identity_server_settings_alert_disconnect" = "Wilt u de verbinding met de identiteitsserver %@ verbreken?"; +"identity_server_settings_alert_disconnect" = "Wil je de verbinding met de identiteitsserver %@ verbreken?"; "identity_server_settings_alert_disconnect_title" = "Verbinding met identiteitsserver verbreken"; "identity_server_settings_alert_change" = "Verbinding met identiteitsserver %1$@ verbreken en in plaats daarvan verbinden met %2$@?"; "identity_server_settings_alert_change_title" = "Identiteitsserver wisselen"; -"identity_server_settings_alert_no_terms" = "De door u gekozen identiteitsserver heeft geen dienstvoorwaarden. Ga alleen door, wanneer u de eigenaar van de server vertrouwd."; +"identity_server_settings_alert_no_terms" = "De door jou gekozen identiteitsserver heeft geen dienstvoorwaarden. Ga alleen door, wanneer je de eigenaar van de server vertrouwd."; "identity_server_settings_alert_no_terms_title" = "De identiteitsserver heeft geen dienstvoorwaarden"; "identity_server_settings_disconnect" = "Verbinding verbreken"; -"identity_server_settings_disconnect_info" = "De verbinding met uw identiteitsserver verbreken zal ertoe leiden dat u niet door andere mensen gevonden zal kunnen worden, en dat u anderen niet via e-mail of telefoon zal kunnen uitnodigen."; +"identity_server_settings_disconnect_info" = "De verbinding met je identiteitsserver verbreken zal ertoe leiden dat je niet door andere mensen gevonden zal kunnen worden, en dat je anderen niet via e-mail of telefoon zal kunnen uitnodigen."; "identity_server_settings_change" = "Wijzigen"; "identity_server_settings_add" = "Toevoegen"; "identity_server_settings_place_holder" = "Voer een identiteitsserver in"; -"identity_server_settings_no_is_description" = "U gebruikt momenteel geen identiteitsserver. Voeg er hierboven één toe om bekenden te kunnen vinden en voor hen vindbaar te zijn."; -"identity_server_settings_description" = "U gebruikt momenteel %@ om gevonden te kunnen worden en bestaande contacten te ontdekken."; +"identity_server_settings_no_is_description" = "Je gebruikt momenteel geen identiteitsserver. Voeg er hierboven één toe om bekenden te kunnen vinden en voor hen vindbaar te zijn."; +"identity_server_settings_description" = "Je gebruikt momenteel %@ om gevonden te kunnen worden en bestaande contacten te ontdekken."; // Identity server settings "identity_server_settings_title" = "Identiteitsserver"; // AuthenticatedSessionViewControllerFactory -"authenticated_session_flow_not_supported" = "Deze app ondersteunt de verificatiemethode op uw server niet."; +"authenticated_session_flow_not_supported" = "Deze app ondersteunt de verificatiemethode op jouw server niet."; "manage_session_sign_out" = "Deze sessie afmelden"; "manage_session_not_trusted" = "Niet vertrouwd"; -"manage_session_trusted" = "Door u vertrouwd"; +"manage_session_trusted" = "Door jou vertrouwd"; "manage_session_name" = "Sessienaam"; "manage_session_info" = "SESSIE INFO"; // Manage session "manage_session_title" = "Beheer sessie"; -"security_settings_user_password_description" = "Bevestig uw identiteit door uw Matrix account wachtwoord in te voeren"; +"security_settings_user_password_description" = "Bevestig je identiteit door je Matrix account wachtwoord in te voeren"; "security_settings_coming_soon" = "Sorry. Deze actie is nog niet beschikbaar in %@ iOS. Gebruik een andere Matrix-app om het in te stellen. %@ iOS zal het dan wel gebruiken."; -"security_settings_complete_security_alert_message" = "U moet de beveiliging van uw huidige sessie nog afronden."; +"security_settings_complete_security_alert_message" = "Je moet de beveiliging van je huidige sessie nog afronden."; "security_settings_complete_security_alert_title" = "Beveiliging afronden"; "security_settings_blacklist_unverified_devices_description" = "Verifieer alle sessies van een persoon om deze als vertrouwd te markeren en berichten naar te zenden."; "security_settings_blacklist_unverified_devices" = "Verzend nooit berichten naar niet-vertrouwde sessies"; @@ -1331,62 +1331,62 @@ "security_settings_crosssigning_reset" = "Reset"; "security_settings_crosssigning_bootstrap" = "Instellen"; "security_settings_crosssigning_info_ok" = "Kruiselings ondertekenen is klaar voor gebruik."; -"security_settings_crosssigning_info_trusted" = "Kruislings ondertekenen is ingeschakeld. U kunt andere personen en sessies verifiëren met kruislings ondertekenen, maar u kunt dit nog niet vanaf deze sessie doordat de versleutelingssleutel ontbreekt. Rond de beveiliging van deze sessie af."; -"security_settings_crosssigning_info_exists" = "Uw account heeft een kruislings ondertekenen ID, maar is nog niet geverifieerd door deze sessie. Rond de beveiliging van deze sessie af."; +"security_settings_crosssigning_info_trusted" = "Kruislings ondertekenen is ingeschakeld. Je kan andere personen en sessies verifiëren met kruislings ondertekenen, maar je kan dit nog niet vanaf deze sessie doordat de versleutelingssleutel ontbreekt. Rond de beveiliging van deze sessie af."; +"security_settings_crosssigning_info_exists" = "Jouw account heeft een kruislings ondertekenen ID, maar is nog niet geverifieerd door deze sessie. Rond de beveiliging van deze sessie af."; "security_settings_crosssigning_info_not_bootstrapped" = "Kruislings ondertekenen is nog niet ingesteld."; "security_settings_crosssigning" = "KRUISLINGS ONDERTEKENEN"; "security_settings_backup" = "BERICHTENBACK-UP"; "security_settings_secure_backup_delete" = "Back-up verwijderen"; "security_settings_secure_backup_synchronise" = "Synchroniseren"; "security_settings_secure_backup_setup" = "Instellen"; -"security_settings_secure_backup_description" = "Maak een back-up van uw versleutelingssleutel bij uw account data voor het geval u toegang verliest tot uw sessies. Uw sleutels zullen worden beveiligd met een unieke veiligheidssleutel."; +"security_settings_secure_backup_description" = "Maak een back-up van je versleutelingssleutel bij je account data voor het geval je toegang verliest tot je sessies. Jouw sleutels zullen worden beveiligd met een unieke veiligheidssleutel."; "security_settings_secure_backup" = "VEILIGE BACK-UP"; -"security_settings_crypto_sessions_description_2" = "Als u deze inlog niet herkent, verander uw Matrix account wachtwoord en reset uw Veilige Back-up."; +"security_settings_crypto_sessions_description_2" = "Als je deze inlog niet herkent, verander je Matrix account wachtwoord en reset je Veilige Back-up."; "security_settings_crypto_sessions_loading" = "Sessies laden…"; "security_settings_crypto_sessions" = "MIJN SESSIES"; // Security settings "security_settings_title" = "Beveiliging"; "settings_show_NSFW_public_rooms" = "NSFW publieke kamers weergeven"; -"settings_identity_server_no_is_description" = "U gebruikt momenteel geen identiteitsserver. Voeg er hierboven één toe om bekenden te kunnen vinden en voor hen vindbaar te zijn."; +"settings_identity_server_no_is_description" = "Je gebruikt momenteel geen identiteitsserver. Voeg er hierboven één toe om bekenden te kunnen vinden en voor hen vindbaar te zijn."; "settings_identity_server_no_is" = "Geen identiteitsserver geconfigureerd"; -"settings_identity_server_description" = "Met de hierboven ingestelde identiteitsserver kan u uw contacten vinden en bent u vindbaar voor uw contacten."; +"settings_identity_server_description" = "Met de hierboven ingestelde identiteitsserver kan je jouw contacten vinden en ben je vindbaar voor je contacten."; "settings_discovery_three_pid_details_enter_sms_code_action" = "SMS-activeringscode invoeren"; "settings_discovery_three_pid_details_cancel_email_validation_action" = "E-mailbevestiging afbreken"; "settings_discovery_three_pid_details_revoke_action" = "Intrekken"; "settings_discovery_three_pid_details_share_action" = "Delen"; -"settings_discovery_three_pid_details_information_phone_number" = "Beheer de voorkeuren voor dit telefoonnummer, dat andere personen kunnen gebruiken om u te vinden en u uit te nodigen tot gesprekken. Telefoonnummers toevoegen of verwijderen in Accounts."; +"settings_discovery_three_pid_details_information_phone_number" = "Beheer de voorkeuren voor dit telefoonnummer, dat andere personen kunnen gebruiken om je te vinden en je uit te nodigen tot gesprekken. Telefoonnummers toevoegen of verwijderen in Accounts."; "settings_discovery_three_pid_details_title_phone_number" = "Beheer telefoonnummer"; -"settings_discovery_three_pid_details_information_email" = "Beheer de voorkeuren voor dit e-mailadres, dat andere personen kunnen gebruiken om u te vinden en u uit te nodigen tot gesprekken. E-mailadressen toevoegen of verwijderen in Accounts."; +"settings_discovery_three_pid_details_information_email" = "Beheer de voorkeuren voor dit e-mailadres, dat andere personen kunnen gebruiken om je te vinden en je uit te nodigen tot gesprekken. E-mailadressen toevoegen of verwijderen in Accounts."; "settings_discovery_three_pid_details_title_email" = "E-mail beheren"; "settings_discovery_error_message" = "Er is een fout opgetreden. Probeer het opnieuw."; "settings_discovery_three_pids_management_information_part3" = "."; "settings_discovery_three_pids_management_information_part2" = "Persoonsinstellingen"; -"settings_discovery_three_pids_management_information_part1" = "Beheer e-mailadressen en telefoonnummers die andere personen kunnen gebruiken om u te vinden en u uit te nodigen voor kamers. E-mailadressen of telefoonnummers toevoegen of verwijderen van deze lijst kan in "; +"settings_discovery_three_pids_management_information_part1" = "Beheer e-mailadressen en telefoonnummers die andere personen kunnen gebruiken om je te vinden en je uit te nodigen voor kamers. E-mailadressen of telefoonnummers toevoegen of verwijderen van deze lijst kan in "; "settings_discovery_terms_not_signed" = "Aanvaard de voorwaarden van de identiteitsserver (%@) om vindbaar te zijn op e-mailadres of telefoonnummer."; -"settings_discovery_no_identity_server" = "U gebruikt momenteel geen identiteitsserver. Om door de u bekende contacten vindbaar te zijn, voeg er een toe."; -"settings_devices_description" = "De publieke naam van een sessie is zichtbaar voor de personen waarmee u communiceert"; +"settings_discovery_no_identity_server" = "Je gebruikt momenteel geen identiteitsserver. Om door de jou bekende contacten vindbaar te zijn, voeg er een toe."; +"settings_devices_description" = "De publieke naam van een sessie is zichtbaar voor de personen waarmee je communiceert"; "settings_add_3pid_invalid_password_message" = "Ongeldig wachtwoord"; -"settings_add_3pid_password_message" = "Geef uw Matrix account wachtwoord om verder te gaan"; +"settings_add_3pid_password_message" = "Geef je Matrix account wachtwoord om verder te gaan"; "settings_add_3pid_password_title_msidsn" = "Telefoonnummer toevoegen"; "settings_add_3pid_password_title_email" = "E-mailadres toevoegen"; "settings_integrations_allow_button" = "Beheer integraties"; -"settings_calls_stun_server_fallback_description" = "Sta de terugvalserver voor oproepbijstand %@ toe wanneer uw server er geen aanbiedt (uw IP-adres wordt gedeeld gedurende een oproep)."; +"settings_calls_stun_server_fallback_description" = "Sta de terugvalserver voor oproepbijstand %@ toe wanneer je server er geen aanbiedt (jouw IP-adres wordt gedeeld gedurende een oproep)."; "settings_calls_stun_server_fallback_button" = "Terugvalserver voor oproepen toestaan"; "settings_security" = "BEVEILIGING"; "settings_three_pids_management_information_part3" = "."; "settings_three_pids_management_information_part2" = "Ontdekken"; -"settings_three_pids_management_information_part1" = "Beheer hier de e-mailadressen en telefoonnummers die u kunt gebruiken om zich aan te melden, of om uw account te herstellen. Controleer wie u vinden kan in "; +"settings_three_pids_management_information_part1" = "Beheer hier de e-mailadressen en telefoonnummers die je kan gebruiken om je aan te melden, of om je account te herstellen. Controleer wie je vinden kan in "; "settings_integrations" = "INTEGRATIES"; "settings_identity_server_settings" = "IDENTITEITSSERVER"; "settings_discovery_settings" = "ONTDEKKEN"; -"external_link_confirmation_message" = "Deze koppeling %@ brengt u naar een andere website: %@\n\nWeet u zeker dat u door wilt gaan?"; +"external_link_confirmation_message" = "Deze koppeling %@ brengt u naar een andere website: %@\n\nWeet je zeker dat je door wil gaan?"; "external_link_confirmation_title" = "Controleer deze koppeling"; "room_open_dialpad" = "Kiestoetsen"; "room_place_voice_call" = "Audio-oproep"; -"room_event_action_delete_confirmation_message" = "Weet u zeker dat u alle niet verzonden berichten wilt verwijderen?"; +"room_event_action_delete_confirmation_message" = "Weet je zeker dat je alle niet verzonden berichten wil verwijderen?"; "room_event_action_delete_confirmation_title" = "Niet verzonden berichten verwijderen"; -"room_unsent_messages_cancel_message" = "Weet u zeker dat u alle niet verzonden berichten in deze kamer wilt verwijderen?"; +"room_unsent_messages_cancel_message" = "Weet je zeker dat je alle niet verzonden berichten in deze kamer wil verwijderen?"; "room_unsent_messages_cancel_title" = "Niet verzonden berichten verwijderen"; "room_member_power_level_short_custom" = "Aangepast"; "room_member_power_level_short_moderator" = "Mod"; @@ -1394,8 +1394,8 @@ "room_member_power_level_custom_in" = "Aangepast (%@) in %@"; "room_member_power_level_moderator_in" = "Moderator in %@"; "room_member_power_level_admin_in" = "Beheerder in %@"; -"room_participants_security_information_room_encrypted_for_dm" = "Berichten zijn hier eind-tot-eind versleuteld.\n\nUw berichten zijn met een digitale sleutel beveiligd, alleen u en de ontvanger hebben de unieke sleutels om deze berichten te ontgrendelen."; -"room_participants_security_information_room_encrypted" = "Berichten in deze kamer zijn eind-tot-eind versleuteld.\n\nUw berichten zijn met een sleutel beveiligd, alleen u en de ontvanger hebben de unieke sleutels om deze berichten te ontgrendelen."; +"room_participants_security_information_room_encrypted_for_dm" = "Berichten zijn hier eind-tot-eind versleuteld.\n\nJouw berichten zijn met een digitale sleutel beveiligd, alleen jij en de ontvanger hebben de unieke sleutels om deze berichten te ontgrendelen."; +"room_participants_security_information_room_encrypted" = "Berichten in deze kamer zijn eind-tot-eind versleuteld.\n\nJouw berichten zijn met een sleutel beveiligd, alleen jij en de ontvanger hebben de unieke sleutels om deze berichten te ontgrendelen."; "room_participants_security_information_room_not_encrypted_for_dm" = "Berichten zijn hier niet eind-tot-eind versleuteld."; "room_participants_security_information_room_not_encrypted" = "Berichten in deze kamer zijn niet eind-tot-eind versleuteld."; "room_participants_security_loading" = "Laden…"; @@ -1406,7 +1406,7 @@ "room_participants_action_security_status_verified" = "Geverifieerd"; "room_participants_action_section_security" = "Beveiliging"; "room_participants_filter_room_members_for_dm" = "Deelnemers filteren"; -"room_participants_leave_prompt_msg_for_dm" = "Weet u zeker dat u het gesprek wilt verlaten?"; +"room_participants_leave_prompt_msg_for_dm" = "Weet je zeker dat je het gesprek wil verlaten?"; "room_participants_leave_prompt_title_for_dm" = "Verlaten"; "contacts_address_book_no_identity_server" = "Geen identiteitsserver geconfigureerd"; "rooms_empty_view_information" = "Kamers zijn geschikt voor alle groepsgesprekken, privé of publiek. Klik op de + om de bestaande kamers te ontdekken of maak een nieuwe aan."; @@ -1434,7 +1434,7 @@ "more" = "Meer"; "switch" = "Wissel"; "joined" = "Toegetreden"; -"store_promotional_text" = "Privacy-beschermende chat- en samenwerkingsapp, op een open netwerk. Gedecentraliseerd, zodat u de controle hebt. Geen datamining, geen achterdeurtjes en geen toegang voor derden."; +"store_promotional_text" = "Privacy-beschermende chat- en samenwerkingsapp, op een open netwerk. Gedecentraliseerd, zodat jij de controle hebt. Geen datamining, geen achterdeurtjes en geen toegang voor derden."; "room_details_integrations" = "Integraties"; "room_details_search" = "Kamer doorzoeken"; "room_multiple_typing_notification" = "%@ en anderen"; @@ -1455,17 +1455,17 @@ "event_formatter_call_ringing" = "Bellen…"; "event_formatter_call_connecting" = "Verbinden…"; "settings_labs_enable_ringing_for_group_calls" = "Audiomelding voor groepsgesprek"; -"room_no_privileges_to_create_group_call" = "U moet een beheerder of moderator zijn om een gesprek te starten."; +"room_no_privileges_to_create_group_call" = "Je moet een beheerder of moderator zijn om een gesprek te starten."; "room_join_group_call" = "Deelnemen"; // Chat "room_slide_to_end_group_call" = "Schuif om voor iedereen de oproep te beëindigen"; -"space_beta_announce_information" = "Spaces zijn de nieuwe manier om kamers en personen te groeperen. Dit is nog niet beschikbaar op iOS, maar u kunt ze nu al gebruiken op Web en Desktop."; -"space_feature_unavailable_information" = "Spaces zijn de nieuwe manier om kamers en personen te groeperen. \n\nHet is binnenkort beschikbaar. U kan aan Spaces deelnemen via andere platformen en die kamers worden dan hier beschikbaar."; +"space_beta_announce_information" = "Spaces zijn de nieuwe manier om kamers en personen te groeperen. Dit is nog niet beschikbaar op iOS, maar je kan ze nu al gebruiken op Web en Desktop."; +"space_feature_unavailable_information" = "Spaces zijn de nieuwe manier om kamers en personen te groeperen. \n\nHet is binnenkort beschikbaar. Je kan aan Spaces deelnemen via andere platformen en die kamers worden dan hier beschikbaar."; "space_beta_announce_subtitle" = "De nieuwe versie van gemeenschappen"; "space_beta_announce_title" = "Spaces komen binnenkort"; "space_beta_announce_badge" = "BETA"; -"space_feature_unavailable_subtitle" = "Spaces zijn nog niet beschikbaar op iOS, maar u kunt het nu al testen op Web en Desktop"; +"space_feature_unavailable_subtitle" = "Spaces zijn nog niet beschikbaar op iOS, maar je kan het nu al testen op Web en Desktop"; // Mark: - Spaces @@ -1484,23 +1484,23 @@ // Mark: - User avatar view "user_avatar_view_accessibility_label" = "afbeelding"; -"secrets_recovery_with_key_information_unlock_secure_backup_with_key" = "Voor uw veiligheidssleutel in om door te gaan."; -"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "Voer uw veiligheidswachtwoord in om door te gaan."; +"secrets_recovery_with_key_information_unlock_secure_backup_with_key" = "Voer je veiligheidssleutel in om door te gaan."; +"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "Voer je veiligheidswachtwoord in om door te gaan."; // Success from secure backup -"key_backup_setup_success_from_secure_backup_info" = "Uw sleutels worden geback-upt."; +"key_backup_setup_success_from_secure_backup_info" = "Je sleutels worden geback-upt."; "security_settings_secure_backup_restore" = "Uit back-up herstellen"; "security_settings_secure_backup_reset" = "Opnieuw instellen"; -"security_settings_secure_backup_info_valid" = "Deze sessie maakt een back-up van uw sleutels."; +"security_settings_secure_backup_info_valid" = "Deze sessie maakt een back-up van je sleutels."; "security_settings_secure_backup_info_checking" = "Controleren…"; -"settings_ui_theme_picker_message_match_system_theme" = "'Automatisch' gebruikt uw apparaat thema instelling"; -"settings_ui_theme_picker_message_invert_colours" = "‘Automatisch’ gebruikt de instelling ‘Kleurweergave omkeren’ van uw apparaat"; +"settings_ui_theme_picker_message_match_system_theme" = "'Automatisch' gebruikt je apparaat thema instelling"; +"settings_ui_theme_picker_message_invert_colours" = "‘Automatisch’ gebruikt de instelling ‘Kleurweergave omkeren’ van je apparaat"; "room_recents_unknown_room_error_message" = "Deze kamer is niet gevonden. Controleer of het bestaat"; -"room_creation_dm_error" = "Uw direct gesprek kon niet aangemaakt worden. Controleer de personen die u wilt uitnodigen en probeer het opnieuw."; +"room_creation_dm_error" = "Je direct gesprek kon niet aangemaakt worden. Controleer de personen die je wil uitnodigen en probeer het opnieuw."; "key_verification_verify_qr_code_scan_code_other_device_action" = "Scan met dit apparaat"; "room_notifs_settings_encrypted_room_notice" = "Let op dat vermeldingen & trefwoorden-meldingen niet beschikbaar zijn in versleutelde kamers op mobiel."; "room_notifs_settings_account_settings" = "Accountinstellingen"; -"room_notifs_settings_manage_notifications" = "U kunt uw meldingen beheren in %@"; +"room_notifs_settings_manage_notifications" = "Je kan je meldingen beheren in %@"; "room_notifs_settings_cancel_action" = "Annuleren"; "room_notifs_settings_done_action" = "Klaar"; "room_notifs_settings_none" = "Geen"; @@ -1510,14 +1510,14 @@ // Room Notification Settings "room_notifs_settings_notify_me_for" = "Stuur een melding voor"; "room_details_notifs" = "Meldingen"; -"voice_message_stop_locked_mode_recording" = "Tik op uw opname om te stoppen of te luisteren"; +"voice_message_stop_locked_mode_recording" = "Tik op je opname om te stoppen of te luisteren"; "voice_message_remaining_recording_time" = "%@s is vertrokken"; // Mark: - Voice Messages "voice_message_release_to_send" = "Vasthouden om op te nemen, loslaten om te versturen"; "settings_labs_voice_messages" = "Spraakberichten"; -"settings_notifications_disabled_alert_message" = "Om in te schakelen, ga naar uw apparaatinstellingen."; +"settings_notifications_disabled_alert_message" = "Om in te schakelen, ga naar je apparaatinstellingen."; "settings_notifications_disabled_alert_title" = "Meldingen uitgeschakeld"; "settings_device_notifications" = "Apparaatmeldingen"; "event_formatter_call_missed_video" = "Video-oproep gemist"; @@ -1547,22 +1547,22 @@ "settings_default" = "Standaardnotificaties"; "settings_notifications" = "NOTIFICATIES"; "version_check_modal_action_title_deprecated" = "Ontdek hoe"; -"version_check_modal_subtitle_deprecated" = "We hebben gewerkt aan het verbeteren van %@ voor een snellere en meer gepolijste ervaring. Helaas is uw huidige iOS-versie niet geschikt voor sommige van deze verbeteringen en worden deze niet langer ondersteund.\nWe adviseren u om uw besturingssysteem te upgraden om %@ volledig te kunnen gebruiken."; +"version_check_modal_subtitle_deprecated" = "We hebben gewerkt aan het verbeteren van %@ voor een snellere en meer gepolijste ervaring. Helaas is je huidige iOS-versie niet geschikt voor sommige van deze verbeteringen en worden deze niet langer ondersteund.\nWe adviseren je om je besturingssysteem te upgraden om %@ volledig te kunnen gebruiken."; "version_check_modal_title_deprecated" = "We ondersteunen iOS %@ niet langer"; "version_check_modal_action_title_supported" = "Ik heb hem"; -"version_check_modal_subtitle_supported" = "We hebben gewerkt aan het verbeteren van %@ voor een snellere en meer gepolijste ervaring. Helaas is uw huidige iOS-versie niet geschikt voor een aantal van deze verbeteringen en zal deze niet langer worden ondersteund.\nWe adviseren u om uw besturingssysteem te upgraden om het volledige potentieel van %@ te kunnen benutten."; +"version_check_modal_subtitle_supported" = "We hebben gewerkt aan het verbeteren van %@ voor een snellere en meer gepolijste ervaring. Helaas is je huidige iOS-versie niet geschikt voor een aantal van deze verbeteringen en zal deze niet langer worden ondersteund.\nWe adviseren je om je besturingssysteem te upgraden om het volledige potentieel van %@ te kunnen benutten."; "version_check_modal_title_supported" = "We stoppen de ondersteuning voor iOS %@"; -"version_check_banner_subtitle_deprecated" = "We ondersteunen %@ niet langer op iOS %@. Om het volledige potentieel van %@ te blijven gebruiken, adviseren wij u om uw iOS-versie te upgraden."; +"version_check_banner_subtitle_deprecated" = "We ondersteunen %@ niet langer op iOS %@. Om het volledige potentieel van %@ te blijven gebruiken, adviseren wij je om je iOS-versie te upgraden."; "version_check_banner_title_deprecated" = "We ondersteunen iOS %@ niet langer"; -"version_check_banner_subtitle_supported" = "We zullen binnenkort de ondersteuning voor %@ op iOS %@ stoppen. Om het volledige potentieel van %@ te blijven gebruiken, adviseren wij u om uw iOS-versie te upgraden."; +"version_check_banner_subtitle_supported" = "We zullen binnenkort de ondersteuning voor %@ op iOS %@ stoppen. Om het volledige potentieel van %@ te blijven gebruiken, adviseren wij je om je iOS-versie te upgraden."; // Mark: - Version check "version_check_banner_title_supported" = "We stoppen de ondersteuning voor iOS %@"; -"settings_mentions_and_keywords_encryption_notice" = "U krijgt geen meldingen voor vermeldingen en trefwoorden in versleutelde kamers op mobiele telefoons."; +"settings_mentions_and_keywords_encryption_notice" = "Je krijgt geen meldingen voor vermeldingen en trefwoorden in versleutelde kamers op mobiele telefoons."; "settings_show_url_previews_description" = "Voorvertoningen worden alleen getoond in niet versleutelde kamers."; "settings_show_url_previews" = "Website-voorvertoning tonen"; -"settings_confirm_media_size_description" = "Wanneer dit aan staat wordt u elke keer gevraagd welk formaat afbeeldingen en video's u wilt versturen."; +"settings_confirm_media_size_description" = "Wanneer dit aan staat wordt je elke keer gevraagd welk formaat afbeeldingen en video's je wil versturen."; "settings_confirm_media_size" = "Bevestig afmeting bij versturen"; "settings_sending_media" = "AFBEELDINGEN EN VIDEO'S VERSTUREN"; "space_avatar_view_accessibility_hint" = "Space-afbeelding wijzigen"; @@ -1574,21 +1574,21 @@ "space_private_join_rule" = "Privé Space"; "space_participants_action_ban" = "Uit deze Space verbannen"; "space_participants_action_remove" = "Uit deze kamer verwijderen"; -"spaces_coming_soon_detail" = "Deze functies is nog niet klaar, maar er wordt aan gewerkt. Voor nu kan u dit wel doen met %@ via uw computer."; +"spaces_coming_soon_detail" = "Deze functies is nog niet klaar, maar er wordt aan gewerkt. Voor nu kan je dit wel doen met %@ via je computer."; "spaces_invites_coming_soon_title" = "Uitnodigen komt binnenkort"; "spaces_add_rooms_coming_soon_title" = "Kamers toevoegen komt binnenkort"; "spaces_coming_soon_title" = "Komt binnenkort"; -"spaces_no_member_found_detail" = "Zoekt u iemand niet in %@? Voorlopig kan u alleen een uitnodiging sturen via web of desktop."; -"spaces_no_room_found_detail" = "Sommige resultaten kunnen verborgen zijn doordat ze privé zijn. U heeft dan een uitnodiging nodig om deel te nemen."; -"spaces_empty_space_detail" = "Sommige kamers kunnen verborgen zijn doordat ze privé zijn. U heeft dan een uitnodiging nodig om deel te nemen."; +"spaces_no_member_found_detail" = "Zoek je iemand niet in %@? Voorlopig kan je alleen een uitnodiging sturen via web of desktop."; +"spaces_no_room_found_detail" = "Sommige resultaten kunnen verborgen zijn doordat ze privé zijn. Je hebt dan een uitnodiging nodig om deel te nemen."; +"spaces_empty_space_detail" = "Sommige kamers kunnen verborgen zijn doordat ze privé zijn. Je hebt dan een uitnodiging nodig om deel te nemen."; "spaces_no_result_found_title" = "Geen resultaten gevonden"; "spaces_empty_space_title" = "Deze Space heeft (nog) geen kamers"; "space_tag" = "space"; "spaces_explore_rooms" = "Kamers ontdekken"; "leave_space_and_all_rooms_action" = "Alle kamers en Spaces verlaten"; "leave_space_only_action" = "Geen kamers verlaten"; -"leave_space_message_admin_warning" = "U bent beheerder van deze Space, controleer of u de beheerrechten heeft overgedragen aan een andere deelnemer voordat u vertrekt."; -"leave_space_message" = "Weet u zeker dat u %@ wilt verlaten? Wilt u ook alle kamers en Spaces in deze Space verlaten?"; +"leave_space_message_admin_warning" = "Je bent beheerder van deze Space, controleer of je de beheerrechten hebt overgedragen aan een andere deelnemer voordat je vertrekt."; +"leave_space_message" = "Weet je zeker dat je %@ wil verlaten? Wil je ook alle kamers en Spaces in deze Space verlaten?"; "leave_space_title" = "%@ verlaten"; "spaces_left_panel_title" = "Spaces"; "spaces_home_space_title" = "Thuis"; @@ -1598,30 +1598,30 @@ "done" = "Klaar"; "open" = "Openen"; "space_home_show_all_rooms" = "Alle kamers tonen"; -"service_terms_modal_information_description_integration_manager" = "Met een integratiebeheerder kunt u functies van derden toevoegen."; -"service_terms_modal_information_description_identity_server" = "Een identiteitsserver helpt u uw contactpersonen te vinden, door hun telefoonnummer of e-mail op te zoeken, om te zien of zij al een account hebben."; +"service_terms_modal_information_description_integration_manager" = "Met een integratiebeheerder kan je functies van derden toevoegen."; +"service_terms_modal_information_description_identity_server" = "Een identiteitsserver helpt je jouw contactpersonen te vinden, door hun telefoonnummer of e-mail op te zoeken, om te zien of zij al een account hebben."; "service_terms_modal_information_title_integration_manager" = "Integratiebeheerder"; // Alert explaining what an identity server / integration manager is. "service_terms_modal_information_title_identity_server" = "Indentiteitsserver"; "service_terms_modal_description_integration_manager" = "Hiermee kunt u bots, bruggen, widgets en stickerpakketten gebruiken."; -"service_terms_modal_description_identity_server" = "Zo kan iemand u vinden als hij uw telefoonnummer of e-mail in zijn telefooncontacten heeft opgeslagen."; +"service_terms_modal_description_identity_server" = "Zo kan iemand je vinden als hij je telefoonnummer of e-mail in zijn telefooncontacten heeft opgeslagen."; "service_terms_modal_table_header_integration_manager" = "INTEGRATIEBEHEERDER VOORWAARDEN"; "service_terms_modal_table_header_identity_server" = "IDENTITEITSSERVER VOORWAARDEN"; -"service_terms_modal_footer" = "Dit kan elk moment worden uitgeschakeld in uw instellingen."; +"service_terms_modal_footer" = "Dit kan elk moment worden uitgeschakeld in je instellingen."; // Service terms "service_terms_modal_title_message" = "Om door te gaan, aanvaard de onderstaande voorwaarden"; -"settings_contacts_enable_sync_description" = "Dit zal uw identiteitsserver gebruiken om u te verbinden met uw contacten, en hen helpen u te vinden."; -"settings_contacts_enable_sync" = "Vind uw contacten"; +"settings_contacts_enable_sync_description" = "Dit zal je identiteitsserver gebruiken om je te verbinden met je contacten, en hen helpen jou te vinden."; +"settings_contacts_enable_sync" = "Vind je contacten"; "settings_phone_contacts" = "TELEFOON CONTACTEN"; "room_event_action_forward" = "Doorsturen"; "find_your_contacts_identity_service_error" = "Kan geen verbinding maken met de identiteitsserver."; "find_your_contacts_footer" = "Dit kan op elk moment worden uitgeschakeld via de instellingen."; -"find_your_contacts_button_title" = "Vind uw contacten"; -"find_your_contacts_message" = "Laat %@ uw contacten zien, zodat u snel kunt beginnen te chatten met degenen die u het beste kent."; -"find_your_contacts_title" = "Begin met een lijst van uw contacten"; -"contacts_address_book_permission_denied_alert_message" = "Om contacten in te schakelen, ga naar uw apparaatinstellingen."; +"find_your_contacts_button_title" = "Vind je contacten"; +"find_your_contacts_message" = "Laat %@ je contacten zien, zodat je snel kan beginnen te chatten met degenen die je het beste kent."; +"find_your_contacts_title" = "Begin met een lijst van je contacten"; +"contacts_address_book_permission_denied_alert_message" = "Om contacten in te schakelen, ga naar je apparaatinstellingen."; "contacts_address_book_permission_denied_alert_title" = "Contacten uitgeschakeld"; "poll_edit_form_add_option" = "Optie toevoegen"; "poll_edit_form_option_number" = "Optie %lu"; @@ -1642,7 +1642,7 @@ "poll_timeline_not_closed_subtitle" = "Probeer het opnieuw"; "poll_timeline_not_closed_title" = "Sluiten van de poll mislukt"; "poll_timeline_vote_not_registered_action" = "OK"; -"poll_timeline_vote_not_registered_subtitle" = "Sorry, uw stem is niet geregistreerd. Probeer het opnieuw"; +"poll_timeline_vote_not_registered_subtitle" = "Sorry, jouw stem is niet geregistreerd. Probeer het opnieuw"; "poll_timeline_vote_not_registered_title" = "Stem niet geregistreerd"; "poll_timeline_total_final_results" = "Uitslag gebaseerd op %lu stemmen"; "poll_timeline_total_final_results_one_vote" = "Uitslag gebaseerd op 1 stem"; @@ -1659,7 +1659,7 @@ "analytics_prompt_stop" = "Delen stoppen"; "analytics_prompt_yes" = "Ja, dat is prima"; "analytics_prompt_not_now" = "Niet nu"; -"analytics_prompt_point_3" = "U kunt dit op elk moment uitzetten in de instellingen"; +"analytics_prompt_point_3" = "Je kan dit op elk moment uitzetten in de instellingen"; /* Note: The word "don't" is formatted in bold */ "analytics_prompt_point_2" = "Wij delen geen informatie met derden"; /* Note: The word "don't" is formatted in bold */ @@ -1668,10 +1668,10 @@ /* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ "analytics_prompt_terms_upgrade" = "Lees al onze voorwaarden %@. Is dit akkoord?"; /* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ -"analytics_prompt_terms_new_user" = "U kunt al onze voorwaarden %@ lezen."; +"analytics_prompt_terms_new_user" = "Je kan al onze voorwaarden %@ lezen."; "analytics_prompt_terms_link_new_user" = "hier"; -"analytics_prompt_message_upgrade" = "U heeft eerder toestemming gegeven om anonieme gebruiksgegevens met ons te delen. Om beter te begrijpen hoe mensen meerdere apparaten gebruiken, genereren we nu een willekeurige identificatiecode die door uw apparaten wordt gedeeld."; -"analytics_prompt_message_new_user" = "Help ons bij het identificeren van problemen en het verbeteren van %@ door anonieme gebruiksgegevens te delen. Om te begrijpen hoe mensen meerdere apparaten gebruiken genereren we een willekeurige identificatie die we verspreiden over uw apparaten."; +"analytics_prompt_message_upgrade" = "Je hebt eerder toestemming gegeven om anonieme gebruiksgegevens met ons te delen. Om beter te begrijpen hoe mensen meerdere apparaten gebruiken, genereren we nu een willekeurige identificatiecode die door je apparaten wordt gedeeld."; +"analytics_prompt_message_new_user" = "Help ons bij het identificeren van problemen en het verbeteren van %@ door anonieme gebruiksgegevens te delen. Om te begrijpen hoe mensen meerdere apparaten gebruiken genereren we een willekeurige identificatie die we verspreiden over je apparaten."; // Analytics "analytics_prompt_title" = "Help %@ verbeteren"; @@ -1687,8 +1687,8 @@ "location_sharing_open_apple_maps" = "In Apple Maps openen"; "location_sharing_invalid_authorization_settings" = "Instellingen"; "location_sharing_invalid_authorization_not_now" = "Niet nu"; -"location_sharing_invalid_authorization_error_title" = "%@ heeft geen toegang tot uw locatie. U kan dit inschakelen via Instellingen > Locatie"; -"location_sharing_locating_user_error_title" = "%@ heeft geen toegang tot uw locatie. Probeer het later opnieuw."; +"location_sharing_invalid_authorization_error_title" = "%@ heeft geen toegang tot jouw locatie. Je kan dit inschakelen via Instellingen > Locatie"; +"location_sharing_locating_user_error_title" = "%@ heeft geen toegang tot jouw locatie. Probeer het later opnieuw."; "location_sharing_loading_map_error_title" = "%@ kan de kaart niet laden. Probeer het later opnieuw."; "location_sharing_share_action" = "Delen"; "location_sharing_close_action" = "Sluiten"; @@ -1699,26 +1699,26 @@ "ok" = "OK"; "settings_enable_room_message_bubbles" = "Berichtbubbels"; "onboarding_splash_page_4_message" = "Element is ook zeer geschikt voor op de werkvloer. Het wordt vertrouwd door 's werelds veiligste organisaties."; -"onboarding_splash_page_4_title_no_pun" = "Berichten voor uw team."; +"onboarding_splash_page_4_title_no_pun" = "Berichten voor jouw team."; "onboarding_splash_page_3_message" = "End-to-end versleuteld en geen telefoonnummer vereist. Geen advertenties of datamining."; "onboarding_splash_page_3_title" = "Veilig berichtenverkeer."; -"onboarding_splash_page_2_message" = "Kies waar u gesprekken worden gehouden, zodat u controle en onafhankelijkheid heeft. Verbonden via Matrix."; -"onboarding_splash_page_2_title" = "U heeft de controle."; -"onboarding_splash_page_1_message" = "Veilige en onafhankelijke communicatie die u dezelfde mate van privacy geeft als een persoonlijk gesprek in uw eigen huis."; -"onboarding_splash_page_1_title" = "Word eigenaar van uw gesprekken."; +"onboarding_splash_page_2_message" = "Kies waar je gesprekken worden gehouden, zodat je controle en onafhankelijkheid hebt. Verbonden via Matrix."; +"onboarding_splash_page_2_title" = "Jij hebt de controle."; +"onboarding_splash_page_1_message" = "Veilige en onafhankelijke communicatie die je dezelfde mate van privacy geeft als een persoonlijk gesprek in je eigen huis."; +"onboarding_splash_page_1_title" = "Word eigenaar van jouw gesprekken."; "onboarding_splash_login_button_title" = "Ik heb al een account"; // Onboarding "onboarding_splash_register_button_title" = "Account aanmaken"; -"poll_edit_form_poll_type_closed_description" = "Resultaten worden pas onthuld als u de poll beëindigt"; +"poll_edit_form_poll_type_closed_description" = "Resultaten worden pas onthuld als je de poll beëindigt"; "poll_edit_form_poll_type_closed" = "Gesloten poll"; "poll_edit_form_poll_type_open_description" = "Kiezers zien resultaten zodra ze hebben gestemd"; "poll_edit_form_poll_type_open" = "Poll openen"; "poll_edit_form_update_failure_subtitle" = "Probeer het opnieuw"; "poll_edit_form_update_failure_title" = "Kan poll niet bijwerken"; "poll_edit_form_poll_type" = "Poll type"; -"location_sharing_post_failure_subtitle" = "%@ kan uw locatie niet versturen. Probeer het later opnieuw."; -"location_sharing_post_failure_title" = "We konden uw locatie niet versturen"; +"location_sharing_post_failure_subtitle" = "%@ kan jouw locatie niet versturen. Probeer het later opnieuw."; +"location_sharing_post_failure_title" = "We konden je locatie niet versturen"; "home_context_menu_leave" = "Verlaten"; "home_context_menu_normal_priority" = "Normale prioriteit"; "home_context_menu_low_priority" = "Lage prioriteit"; @@ -1730,24 +1730,24 @@ "home_context_menu_make_room" = "Verplaatsen naar kamers"; "home_context_menu_make_dm" = "Verplaatsen naar personen"; "event_formatter_message_deleted" = "Bericht verwijderd"; -"settings_labs_enable_threads" = "Berichten met discussielijnen"; -"message_from_a_thread" = "Van een discussie"; -"threads_empty_show_all_threads" = "Toon alle discussies"; -"threads_empty_tip" = "Tip: Tik op een bericht en gebruik “Discussie” om er een te starten."; -"threads_empty_info_my" = "Reageer op een lopende discussie of tik op een bericht en gebruik “Discussie” om een nieuwe te starten."; -"threads_empty_info_all" = "Discussies helpen u gesprekken on-topic te houden en gemakkelijk bij te houden."; -"threads_empty_title" = "Houd discussies georganiseerd met discussielijnen"; -"threads_action_my_threads" = "Mijn discussies"; -"threads_action_all_threads" = "Alle discussies"; -"threads_title" = "Discussies"; -"thread_copy_link_to_thread" = "Kopieer link naar discussie"; +"settings_labs_enable_threads" = "Berichten met threads"; +"message_from_a_thread" = "Van een thread"; +"threads_empty_show_all_threads" = "Toon alle threads"; +"threads_empty_tip" = "Tip: Tik op een bericht en gebruik “Thread” om er een te starten."; +"threads_empty_info_my" = "Reageer op een lopende threads of tik op een bericht en gebruik “Thread” om een nieuwe te starten."; +"threads_empty_info_all" = "Threads helpen je gesprekken on-topic te houden en gemakkelijk bij te houden."; +"threads_empty_title" = "Houd discussies georganiseerd met threads"; +"threads_action_my_threads" = "Mijn threads"; +"threads_action_all_threads" = "Alle threads"; +"threads_title" = "Threads"; +"thread_copy_link_to_thread" = "Kopieer link naar thread"; // MARK: Threads -"room_thread_title" = "Discussies"; +"room_thread_title" = "Thread"; "room_accessibility_thread_more" = "Meer"; -"room_accessibility_threads" = "Discussies"; +"room_accessibility_threads" = "Threads"; "room_event_copy_link_info" = "Link gekopieerd naar klembord."; -"room_event_action_reply_in_thread" = "Discussie"; +"room_event_action_reply_in_thread" = "Thread"; "room_event_action_view_in_room" = "Bekijk in kamer"; @@ -1779,16 +1779,16 @@ "login_create_account" = "Account aanmaken:"; "login_server_url_placeholder" = "URL (bv. https://matrix.org)"; "login_home_server_title" = "Server-URL:"; -"login_home_server_info" = "Uw server slaat al uw gespreks- en accountgegevens op"; +"login_home_server_info" = "Je server slaat al je gespreks- en accountgegevens op"; "login_identity_server_title" = "Identiteitsserver-URL:"; "login_identity_server_info" = "Matrix verstrekt identiteitsservers om te achterhalen welke e-mailadressen enz. bij welke Matrix-ID’s horen. Tot nu toe bestaat alleen https://matrix.org."; "login_user_id_placeholder" = "Matrix-ID (bv. @jan:matrix.org of jan)"; "login_password_placeholder" = "Wachtwoord"; "login_optional_field" = "optioneel"; "login_display_name_placeholder" = "Weergavenaam (bv. Jan Janssens)"; -"login_email_info" = "Door een e-mailadres in te voeren kunnen andere personen u eenvoudiger op Matrix vinden, verder geeft het u een manier om uw wachtwoord in de toekomst te wijzigen."; +"login_email_info" = "Door een e-mailadres in te voeren kunnen andere personen je eenvoudiger op Matrix vinden, verder geeft het je een manier om je wachtwoord in de toekomst te wijzigen."; "login_email_placeholder" = "E-mailadres"; -"login_prompt_email_token" = "Voer uw e-mailadres-validatiebewijs in:"; +"login_prompt_email_token" = "Voer je e-mailadres-validatiebewijs in:"; "login_error_title" = "Aanmelden Mislukt"; "login_error_no_login_flow" = "Ophalen van authenticatie-informatie van deze server is mislukt"; "login_error_do_not_support_login_flows" = "Momenteel bieden we geen ondersteuning voor sommige of alle aanmeldingsmethoden van deze server"; @@ -1886,9 +1886,9 @@ "device_details_identifier" = "ID\n"; "device_details_last_seen" = "Laatst gezien\n"; "device_details_last_seen_format" = "%@ @ %@\n"; -"device_details_rename_prompt_message" = "De publieke naam van een sessie is zichtbaar voor de personen waarmee u communiceert"; +"device_details_rename_prompt_message" = "De publieke naam van een sessie is zichtbaar voor de personen waarmee je communiceert"; "device_details_delete_prompt_title" = "Authenticatie"; -"device_details_delete_prompt_message" = "Deze handeling vereist bijkomende authenticatie.\nVoer uw wachtwoord in om verder te gaan."; +"device_details_delete_prompt_message" = "Deze handeling vereist bijkomende authenticatie.\nVoer je wachtwoord in om verder te gaan."; // Encryption information "room_event_encryption_info_title" = "Informatie over eind-tot-eind-versleuteling\n\n"; "room_event_encryption_info_event" = "Gebeurtenisinformatie\n"; @@ -1914,15 +1914,15 @@ "room_event_encryption_info_block" = "Blokkeren"; "room_event_encryption_info_unblock" = "Deblokkeren"; "room_event_encryption_verify_title" = "Sessie verifiëren\n\n"; -"room_event_encryption_verify_message" = "Om te verifiëren dat deze sessie vertrouwd kan worden, neemt u contact op met de eigenaar van de sessie op een andere manier (bv. persoonlijk of door te bellen) en vraagt u hem/haar of de sleutel die hij/zij in de persoonsinstellingen ziet overeenkomt met de onderstaande sleutel:\n\n\tSessienaam: %@\n\tSessie-ID: %@\n\tSessiesleutel: %@\n\nAls het overeenkomt, klikt u hieronder op de knop ‘Verifiëren’. Als het niet overeenkomt, onderschept iemand anders deze sessie en drukt u in plaats daarvan op de knop ‘Blokkeren’.\n\nIn de toekomst zal dit verificatieproces verbeterd worden."; +"room_event_encryption_verify_message" = "Om te verifiëren dat deze sessie vertrouwd kan worden, neem je contact op met de eigenaar van de sessie op een andere manier (bv. persoonlijk of door te bellen) en vraag je hen of de sleutel die hun in de persoonsinstellingen zien overeenkomt met de onderstaande sleutel:\n\n\tSessienaam: %@\n\tSessie-ID: %@\n\tSessiesleutel: %@\n\nAls het overeenkomt, klik je hieronder op de knop ‘Verifiëren’. Als het niet overeenkomt, onderschept iemand anders deze sessie en druk je in plaats daarvan op de knop ‘Blokkeren’.\n\nIn de toekomst zal dit verificatieproces verbeterd worden."; "room_event_encryption_verify_ok" = "Verifiëren"; // Account "account_save_changes" = "Wijzigingen opslaan"; "account_link_email" = "E-mailadres koppelen"; "account_linked_emails" = "Gekoppelde e-mailadressen"; "account_email_validation_title" = "Verificatie in afwachting"; -"account_email_validation_message" = "Bekijk uw e-mail en open de koppeling erin. Wanneer dit gedaan is, tikt u op verder gaan."; -"account_email_validation_error" = "Kan het e-mailadres niet verifiëren. Bekijk uw e-mail en open de koppeling erin. Wanneer dit gedaan is, tikt u op verder gaan"; +"account_email_validation_message" = "Bekijk je e-mail en open de koppeling erin. Wanneer dit gedaan is, tik je op verder gaan."; +"account_email_validation_error" = "Kan het e-mailadres niet verifiëren. Bekijk je e-mail en open de koppeling erin. Wanneer dit gedaan is, tik je op verder gaan"; "account_msisdn_validation_title" = "Verificatie in afwachting"; "account_msisdn_validation_message" = "We hebben een sms met een activatiecode verstuurd. Voer deze code hieronder in."; "account_msisdn_validation_error" = "Kan het telefoonnummer niet verifiëren."; @@ -1945,28 +1945,28 @@ "room_please_select" = "Selecteer een gesprek"; "room_error_join_failed_title" = "Toetreden tot het gesprek is mislukt"; "room_error_join_failed_empty_room" = "Het is momenteel niet mogelijk om tot een leeg gesprek toe te treden."; -"room_error_name_edition_not_authorized" = "U bent niet bevoegd om de naam van dit gesprek te wijzigen"; -"room_error_topic_edition_not_authorized" = "U bent niet bevoegd om het onderwerp van dit gesprek te wijzigen"; +"room_error_name_edition_not_authorized" = "Je bent niet bevoegd om de naam van dit gesprek te wijzigen"; +"room_error_topic_edition_not_authorized" = "Je bent niet bevoegd om het onderwerp van dit gesprek te wijzigen"; "room_error_cannot_load_timeline" = "Laden van tijdslijn is mislukt"; "room_error_timeline_event_not_found_title" = "Laden van tijdslijnpositie is mislukt"; "room_error_timeline_event_not_found" = "De app heeft geprobeerd een specifiek punt in de tijdslijn van dit gesprek te laden, maar kon het niet vinden"; -"room_left" = "U heeft het gesprek verlaten"; -"room_no_power_to_create_conference_call" = "U heeft toestemming nodig om een vergadering in dit groepsgesprek te starten"; +"room_left" = "Je hebt het gesprek verlaten"; +"room_no_power_to_create_conference_call" = "Je hebt toestemming nodig om een vergadering in dit groepsgesprek te starten"; "room_no_conference_call_in_encrypted_rooms" = "Vergadergesprekken worden niet ondersteund in versleutelde gesprekken"; // Room members -"room_member_ignore_prompt" = "Weet u zeker dat u alle berichten van deze persoon wilt verbergen?"; -"room_member_power_level_prompt" = "U kunt deze veranderingen niet ongedaan maken aangezien u de persoon tot hetzelfde niveau als uzelf promoveert.\nWeet u het zeker?"; +"room_member_ignore_prompt" = "Weet je zeker dat je alle berichten van deze persoon wil verbergen?"; +"room_member_power_level_prompt" = "Je kan deze veranderingen niet ongedaan maken aangezien je de persoon tot hetzelfde niveau als jezelf promoveert.\nWeet je het zeker?"; // Attachment -"attachment_size_prompt" = "Wilt u het versturen als:"; +"attachment_size_prompt" = "Wil je het versturen als:"; "attachment_original" = "Werkelijke grootte (%@)"; "attachment_small" = "Klein (~%@)"; "attachment_medium" = "Middel (~%@)"; "attachment_large" = "Groot (~%@)"; "attachment_cancel_download" = "Download annuleren?"; "attachment_cancel_upload" = "Upload annuleren?"; -"attachment_multiselection_size_prompt" = "Wilt u afbeeldingen versturen als:"; +"attachment_multiselection_size_prompt" = "Wil je afbeeldingen versturen als:"; "attachment_multiselection_original" = "Werkelijke grootte"; -"attachment_e2e_keys_file_prompt" = "Dit bestand bevat versleutelingssleutels die uit een Matrix-client geëxporteerd zijn.\nWilt u de bestandsinhoud bekijken of de sleutels die het bevat importeren?"; +"attachment_e2e_keys_file_prompt" = "Dit bestand bevat versleutelingssleutels die uit een Matrix-client geëxporteerd zijn.\nWil je de bestandsinhoud bekijken of de sleutels die het bevat importeren?"; "attachment_e2e_keys_import" = "Bezig met importeren…"; // Contacts "contact_mx_users" = "Matrix personen"; @@ -1980,12 +1980,12 @@ "format_time_d" = "d"; // E2E import "e2e_import_room_keys" = "Gesprekssleutels importeren"; -"e2e_import_prompt" = "Dit proces maakt het mogelijk om versleutelingssleutels die u eerder had geëxporteerd vanaf een andere Matrix-cliënt te importeren. Daarna kunt u alle berichten ontsleutelen die de andere cliënt ook kon ontsleutelen.\nHet exporteerbestand is beschermd met een wachtwoord. Voer hier het wachtwoord in om het bestand te ontsleutelen."; +"e2e_import_prompt" = "Dit proces maakt het mogelijk om versleutelingssleutels die je eerder had geëxporteerd vanaf een andere Matrix-cliënt te importeren. Daarna kan je alle berichten ontsleutelen die de andere cliënt ook kon ontsleutelen.\nHet exporteerbestand is beschermd met een wachtwoord. Voer hier het wachtwoord in om het bestand te ontsleutelen."; "e2e_import" = "Importeren"; "e2e_passphrase_enter" = "Voer wachtwoord in"; // E2E export "e2e_export_room_keys" = "Gesprekssleutels exporteren"; -"e2e_export_prompt" = "Dit proces maakt het mogelijk om de sleutels voor berichten die u heeft ontvangen in versleutelde gesprekken te exporteren naar een lokaal bestand. Daarna kunt u het bestand in de toekomst in een andere Matrix-cliënt importeren, zodat die cliënt ook deze berichten zal kunnen ontsleutelen.\nHet geëxporteerde bestand zal iedereen die het kan lezen de mogelijkheid bieden om de versleutelde berichten die u kunt zien te ontsleutelen, dus wees voorzichtig en bewaar het op een veilige plaats."; +"e2e_export_prompt" = "Dit proces maakt het mogelijk om de sleutels voor berichten die je hebt ontvangen in versleutelde gesprekken te exporteren naar een lokaal bestand. Daarna kan je het bestand in de toekomst in een andere Matrix-cliënt importeren, zodat die cliënt ook deze berichten zal kunnen ontsleutelen.\nHet geëxporteerde bestand zal iedereen die het kan lezen de mogelijkheid bieden om de versleutelde berichten die je kan zien te ontsleutelen, dus wees voorzichtig en bewaar het op een veilige plaats."; "e2e_export" = "Exporteren"; "e2e_passphrase_confirm" = "Bevestig wachtwoord"; "e2e_passphrase_empty" = "Wachtwoord mag niet leeg zijn"; @@ -2000,15 +2000,15 @@ "private" = "Privé"; "public" = "Publiek"; "power_level" = "Machtsniveau"; -"network_error_not_reachable" = "Controleer uw netwerkverbinding"; +"network_error_not_reachable" = "Controleer je netwerkverbinding"; "user_id_placeholder" = "bv: @jan:server"; "ssl_homeserver_url" = "Server-URL: %@"; // Permissions "camera_access_not_granted_for_call" = "Video-oproepen vereisen toegang tot de camera, maar %@ heeft hier geen toestemming voor"; "microphone_access_not_granted_for_call" = "Oproepen vereisen toegang tot de camera, maar %@ heeft hier geen toestemming voor"; -"local_contacts_access_not_granted" = "Personen zoeken op basis van uw lokale contacten vereist toegang tot die contacten, maar %@ heeft hier geen toestemming voor"; +"local_contacts_access_not_granted" = "Personen zoeken op basis van je lokale contacten vereist toegang tot die contacten, maar %@ heeft hier geen toestemming voor"; "local_contacts_access_discovery_warning_title" = "Personen zoeken"; -"local_contacts_access_discovery_warning" = "Om contacten te vinden die Matrix al gebruiken, kan %@ de e-mailadressen en telefoonnummers in uw adresboek naar uw gekozen Matrix-identiteitsserver sturen. Waar ondersteund worden de persoonlijke gegevens gehasht vóór het versturen - bekijk het privacybeleid van uw identiteitsserver voor meer informatie."; +"local_contacts_access_discovery_warning" = "Om contacten te vinden die Matrix al gebruiken, kan %@ de e-mailadressen en telefoonnummers in je adresboek naar je gekozen Matrix-identiteitsserver sturen. Waar ondersteund worden de persoonlijke gegevens gehasht vóór het versturen - bekijk het privacybeleid van je identiteitsserver voor meer informatie."; // Country picker "country_picker_title" = "Kies een land"; /* -*- @@ -2107,7 +2107,7 @@ "notification_settings_disable_all" = "Alle meldingen uitschakelen"; "notification_settings_enable_notifications" = "Meldingen inschakelen"; "notification_settings_enable_notifications_warning" = "Alle meldingen zijn momenteel voor alle apparaten uitgeschakeld."; -"notification_settings_global_info" = "Meldingsinstellingen worden op uw account opgeslagen en gedeeld met alle cliënten die dat ondersteunen (inclusief bureaubladmeldingen).\n\nRegels worden in volgorde toegepast; de eerste regel die overeenkomt bepaalt de uitkomst van een bericht.\nDus: per-woord-meldingen zijn belangrijker dan per-gespreks-meldingen, die op hun beurt weer belangrijker zijn dan per-afzender-meldingen.\nVoor meerdere regels van hetzelfde type geldt dat de eerste in de lijst die overeenkomt de prioriteit heeft."; +"notification_settings_global_info" = "Meldingsinstellingen worden op je account opgeslagen en gedeeld met alle cliënten die dat ondersteunen (inclusief bureaubladmeldingen).\n\nRegels worden in volgorde toegepast; de eerste regel die overeenkomt bepaalt de uitkomst van een bericht.\nDus: per-woord-meldingen zijn belangrijker dan per-gespreks-meldingen, die op hun beurt weer belangrijker zijn dan per-afzender-meldingen.\nVoor meerdere regels van hetzelfde type geldt dat de eerste in de lijst die overeenkomt de prioriteit heeft."; "notification_settings_per_word_notifications" = "Per-woord-meldingen"; "notification_settings_per_word_info" = "Woorden komen niet hoofdlettergevoelig met elkaar overeen en kunnen een *-wildcard bevatten. Dus:\nfoo komt overeen met de tekenreeks ‘foo’, die omgeven wordt door woordscheidingstekens (zoals punctuatie en spaties, of het begin of einde van een regel).\nfoo* komt overeen met elk woord dat met ‘foo’ begint.\n*foo* komt overeen met elk woord dat de drie letters ‘foo’ bevat."; "notification_settings_always_notify" = "Altijd melden"; @@ -2146,7 +2146,7 @@ "ssl_remain_offline" = "Negeren"; "ssl_fingerprint_hash" = "Vingerafdruk (%@):"; "ssl_could_not_verify" = "Kan de identiteit van de externe server niet bepalen."; -"ssl_cert_not_trust" = "Dit kan betekenen dat iemand kwaadwillig uw verkeer onderschept, of dat uw telefoon het certificaat dat door de externe server wordt geleverd niet vertrouwt."; +"ssl_cert_not_trust" = "Dit kan betekenen dat iemand kwaadwillig je verkeer onderschept, of dat je telefoon het certificaat dat door de externe server wordt geleverd niet vertrouwt."; "ssl_cert_new_account_expl" = "Als de serverbeheerder heeft gezegd dat dit de bedoeling is, wees er dan zeker van dat de vingerafdruk hieronder overeenkomt met de vingerafdruk die door hen wordt geleverd."; "ssl_unexpected_existing_expl" = "Het certificaat is veranderd van één dat door uw telefoon werd vertrouwd naar een ander. Dit is HEEL ONGEBRUIKELIJK. Het wordt aangeraden om dit nieuwe certificaat NIET TE AANVAARDEN."; "ssl_expected_existing_expl" = "Het certificaat is veranderd van een vertrouwd naar een onvertrouwd certificaat. De server heeft misschien zijn certificaat vernieuwd. Contacteer de serverbeheerder voor de verwachte vingerafdruk."; @@ -2166,7 +2166,7 @@ "login_error_resource_limit_exceeded_title" = "Bronlimiet Overschreden"; "login_error_resource_limit_exceeded_message_default" = "Deze server heeft één of meerdere van zijn bronlimieten overschreden."; "login_error_resource_limit_exceeded_message_monthly_active_user" = "Deze server heeft zijn limiet voor maandelijks actieve personen bereikt."; -"login_error_resource_limit_exceeded_message_contact" = "\n\nGelieve contact op te nemen met uw dienstbeheerder om deze dienst te blijven gebruiken."; +"login_error_resource_limit_exceeded_message_contact" = "\n\nGelieve contact op te nemen met je dienstbeheerder om deze dienst te blijven gebruiken."; "login_error_resource_limit_exceeded_contact_button" = "Beheerder contacteren"; // Reply to message "message_reply_to_sender_sent_an_image" = "heeft een afbeelding gestuurd."; @@ -2181,67 +2181,67 @@ "notice_encryption_enabled_ok" = "%@ heeft eind-tot-eind-versleuteling ingeschakeld."; "notice_encryption_enabled_unknown_algorithm" = "%1$@ heeft eind-tot-eind-versleuteling ingeschakeld (onbekend algoritme %2$@)."; "notice_room_name_removed_for_dm" = "%@ heeft de naam verwijderd"; -"notice_room_ban_by_you" = "U heeft %@ verbannen"; -"notice_room_unban_by_you" = "U heeft %@ ontbannen"; -"notice_room_history_visible_to_members_from_joined_point_by_you_for_dm" = "U heeft toekomstige berichten voor iedereen zichtbaar gemaakt vanaf het moment dat zij deelnemen aan het gesprek."; -"notice_room_history_visible_to_members_from_joined_point_by_you" = "U heeft de toekomstige gespreksgeschiedenis zichtbaar gemaakt voor alle gespreksleden, vanaf het moment dat ze toegetreden zijn."; -"notice_room_history_visible_to_members_from_invited_point_by_you_for_dm" = "U heeft toekomstige berichten voor iedereen zichtbaar gemaakt vanaf het moment dat zij zijn uitgenodigd."; -"notice_room_history_visible_to_members_from_invited_point_by_you" = "U heeft de toekomstige gespreksgeschiedenis zichtbaar gemaakt voor alle gespreksleden, vanaf het moment dat ze uitgenodigd zijn."; -"notice_room_history_visible_to_members_by_you_for_dm" = "U heeft toekomstige berichten voor alle gespreksleden zichtbaar gemaakt."; -"notice_room_history_visible_to_members_by_you" = "U heeft de toekomstige gespreksgeschiedenis voor alle gespreksleden zichtbaar gemaakt."; -"notice_room_history_visible_to_anyone_by_you" = "U heeft de toekomstige gespreksgeschiedenis voor iedereen zichtbaar gemaakt."; -"notice_redaction_by_you" = "U heeft een gebeurtenis bewerkt (ID: %@)"; -"notice_encryption_enabled_unknown_algorithm_by_you" = "U heeft eind-tot-eind-versleuteling ingeschakeld (onbekend algoritme %@)."; -"notice_encryption_enabled_ok_by_you" = "U heeft eind-tot-eind-versleuteling ingeschakeld."; -"notice_room_created_by_you_for_dm" = "U bent toegetreden."; -"notice_room_created_by_you" = "U heeft de kamer aangemaakt en ingesteld."; -"notice_profile_change_redacted_by_you" = "U heeft uw profiel %@ bijgewerkt"; -"notice_event_redacted_by_you" = " door u"; -"notice_room_topic_removed_by_you" = "U heeft het onderwerp verwijderd"; -"notice_room_name_removed_by_you_for_dm" = "U heeft de naam verwijderd"; -"notice_room_name_removed_by_you" = "U heeft de gespreksnaam verwijderd"; -"notice_conference_call_request_by_you" = "U heeft een VoIP-vergadering aangevraagd"; -"notice_ended_video_call_by_you" = "U heeft opgehangen"; -"notice_answered_video_call_by_you" = "U heeft de oproep beantwoord"; -"notice_placed_video_call_by_you" = "U heeft een spraakoproep gestart"; -"notice_placed_voice_call_by_you" = "U heeft een spraakoproep gestart"; -"notice_room_name_changed_by_you_for_dm" = "U heeft de gespreksnaam veranderd naar %@."; -"notice_room_name_changed_by_you" = "U heeft de gespreksnaam veranderd naar %@."; -"notice_topic_changed_by_you" = "U heeft het onderwerp veranderd naar \"%@\"."; -"notice_display_name_removed_by_you" = "U heeft uw weergavenaam verwijderd"; -"notice_display_name_changed_from_by_you" = "U heeft uw weergavenaam veranderd van %@ naar %@"; -"notice_display_name_set_by_you" = "U heeft uw weergavenaam veranderd naar %@"; -"notice_avatar_url_changed_by_you" = "U heeft uw profielfoto veranderd"; -"notice_room_withdraw_by_you" = "U heeft %@'s uitnodiging teruggetrokken"; -"notice_room_kick_by_you" = "U heeft %@ verwijderd uit het gesprek"; -"notice_room_reject_by_you" = "U heeft de uitnodiging geweigerd"; -"notice_room_leave_by_you" = "U heeft het gesprek verlaten"; -"notice_room_join_by_you" = "U bent toegetreden"; -"notice_room_third_party_revoked_invite_by_you_for_dm" = "U heeft %@'s uitnodiging ingetrokken"; -"notice_room_third_party_revoked_invite_by_you" = "U heeft de uitnodiging aan %@ om aan het gesprek deel te nemen ingetrokken"; -"notice_room_third_party_registered_invite_by_you" = "U heeft de uitnodiging van %@ aanvaard"; -"notice_room_third_party_invite_by_you_for_dm" = "U heeft %@ uitgenodigd"; -"notice_room_third_party_invite_by_you" = "U heeft %@ uitgenodigd om aan het gesprek deel te nemen"; -"notice_room_invite_you" = "%@ heeft u uitgenodigd"; +"notice_room_ban_by_you" = "Je hebt %@ verbannen"; +"notice_room_unban_by_you" = "Je hebt %@ ontbannen"; +"notice_room_history_visible_to_members_from_joined_point_by_you_for_dm" = "Je hebt toekomstige berichten voor iedereen zichtbaar gemaakt vanaf het moment dat hun deelnemen aan het gesprek."; +"notice_room_history_visible_to_members_from_joined_point_by_you" = "Je hebt de toekomstige gespreksgeschiedenis zichtbaar gemaakt voor alle gespreksleden, vanaf het moment dat hun toegetreden zijn."; +"notice_room_history_visible_to_members_from_invited_point_by_you_for_dm" = "Je hebt toekomstige berichten voor iedereen zichtbaar gemaakt vanaf het moment dat hun zijn uitgenodigd."; +"notice_room_history_visible_to_members_from_invited_point_by_you" = "Je hebt de toekomstige gespreksgeschiedenis zichtbaar gemaakt voor alle gespreksleden, vanaf het moment dat ze uitgenodigd zijn."; +"notice_room_history_visible_to_members_by_you_for_dm" = "Je hebt toekomstige berichten voor alle gespreksleden zichtbaar gemaakt."; +"notice_room_history_visible_to_members_by_you" = "Je hebt de toekomstige gespreksgeschiedenis voor alle gespreksleden zichtbaar gemaakt."; +"notice_room_history_visible_to_anyone_by_you" = "Je hebt de toekomstige gespreksgeschiedenis voor iedereen zichtbaar gemaakt."; +"notice_redaction_by_you" = "Je hebt een gebeurtenis bewerkt (ID: %@)"; +"notice_encryption_enabled_unknown_algorithm_by_you" = "Je hebt eind-tot-eind-versleuteling ingeschakeld (onbekend algoritme %@)."; +"notice_encryption_enabled_ok_by_you" = "Je hebt eind-tot-eind-versleuteling ingeschakeld."; +"notice_room_created_by_you_for_dm" = "Je bent toegetreden."; +"notice_room_created_by_you" = "Je hebt de kamer aangemaakt en ingesteld."; +"notice_profile_change_redacted_by_you" = "Je hebt je profiel %@ bijgewerkt"; +"notice_event_redacted_by_you" = " door jou"; +"notice_room_topic_removed_by_you" = "Je hebt het onderwerp verwijderd"; +"notice_room_name_removed_by_you_for_dm" = "Je hebt de naam verwijderd"; +"notice_room_name_removed_by_you" = "Je hebt de gespreksnaam verwijderd"; +"notice_conference_call_request_by_you" = "Je hebt een VoIP-vergadering aangevraagd"; +"notice_ended_video_call_by_you" = "Je hebt opgehangen"; +"notice_answered_video_call_by_you" = "Je hebt de oproep beantwoord"; +"notice_placed_video_call_by_you" = "Je hebt een spraakoproep gestart"; +"notice_placed_voice_call_by_you" = "Je hebt een spraakoproep gestart"; +"notice_room_name_changed_by_you_for_dm" = "Je hebt de gespreksnaam veranderd naar %@."; +"notice_room_name_changed_by_you" = "Je hebt de gespreksnaam veranderd naar %@."; +"notice_topic_changed_by_you" = "Je hebt het onderwerp veranderd naar \"%@\"."; +"notice_display_name_removed_by_you" = "Je hebt je weergavenaam verwijderd"; +"notice_display_name_changed_from_by_you" = "Je hebt je weergavenaam veranderd van %@ naar %@"; +"notice_display_name_set_by_you" = "Je hebt je weergavenaam veranderd naar %@"; +"notice_avatar_url_changed_by_you" = "Je hebt je profielfoto veranderd"; +"notice_room_withdraw_by_you" = "Je hebt %@'s uitnodiging teruggetrokken"; +"notice_room_kick_by_you" = "Je hebt %@ verwijderd uit het gesprek"; +"notice_room_reject_by_you" = "Je hebt de uitnodiging geweigerd"; +"notice_room_leave_by_you" = "Je hebt het gesprek verlaten"; +"notice_room_join_by_you" = "Je bent toegetreden"; +"notice_room_third_party_revoked_invite_by_you_for_dm" = "Je hebt %@'s uitnodiging ingetrokken"; +"notice_room_third_party_revoked_invite_by_you" = "Je hebt de uitnodiging aan %@ om aan het gesprek deel te nemen ingetrokken"; +"notice_room_third_party_registered_invite_by_you" = "Je hebt de uitnodiging van %@ aanvaard"; +"notice_room_third_party_invite_by_you_for_dm" = "Je hebt %@ uitgenodigd"; +"notice_room_third_party_invite_by_you" = "Je hebt %@ uitgenodigd om aan het gesprek deel te nemen"; +"notice_room_invite_you" = "%@ heeft jou uitgenodigd"; // Notice Events with "You" -"notice_room_invite_by_you" = "U heeft %@ uitgenodigd"; +"notice_room_invite_by_you" = "Je hebt %@ uitgenodigd"; "notice_room_name_changed_for_dm" = "%@ heeft de gespreksnaam veranderd naar %@."; "notice_room_third_party_revoked_invite_for_dm" = "%@ heeft %@'s uitnodiging ingetrokken"; "notice_room_third_party_invite_for_dm" = "%@ heeft %@ uitgenodigd"; -"room_left_for_dm" = "U heeft het gesprek verlaten"; +"room_left_for_dm" = "Je hebt het gesprek verlaten"; "notice_room_history_visible_to_members_from_joined_point_for_dm" = "%@ heeft toekomstige berichten voor iedereen zichtbaar gemaakt vanaf het moment dat zij deelnemen aan het gesprek."; "notice_room_history_visible_to_members_from_invited_point_for_dm" = "%@ heeft toekomstige berichten voor iedereen zichtbaar gemaakt vanaf het moment dat zij zijn uitgenodigd."; "notice_room_history_visible_to_members_for_dm" = "%@ heeft toekomstige berichten voor alle gespreksleden zichtbaar gemaakt."; "notice_room_aliases_for_dm" = "De bijnamen zijn: %@"; -"notice_room_join_rule_public_by_you_for_dm" = "U maakte dit publiekelijk."; -"notice_room_join_rule_public_by_you" = "U heeft de kamer publiekelijk gemaakt."; +"notice_room_join_rule_public_by_you_for_dm" = "Je maakte dit publiekelijk."; +"notice_room_join_rule_public_by_you" = "Je hebt de kamer publiekelijk gemaakt."; "notice_room_join_rule_public_for_dm" = "%@ maakte dit publiekelijk."; "notice_room_join_rule_public" = "%@ heeft de kamer publiekelijk gemaakt."; "notice_room_created_for_dm" = "%@ is toegetreden."; "notice_room_power_level_intro_for_dm" = "Het machtsniveau van de gespreksleden is:"; -"notice_room_join_rule_invite_by_you_for_dm" = "U maakte dit gesprek alleen op uitnodiging."; -"notice_room_join_rule_invite_by_you" = "U heeft het toegangsbeleid gewijzigd naar alleen genodigden."; +"notice_room_join_rule_invite_by_you_for_dm" = "Je maakte dit gesprek alleen op uitnodiging."; +"notice_room_join_rule_invite_by_you" = "Je hebt het toegangsbeleid gewijzigd naar alleen genodigden."; // New "notice_room_join_rule_invite" = "%@ heeft het toegangsbeleid gewijzigd naar alleen genodigden."; "notice_room_join_rule_invite_for_dm" = "%@ heeft dit gesprek alleen op uitnodiging gemaakt."; @@ -2252,9 +2252,9 @@ "call_more_actions_change_audio_device" = "Audio-apparaat wisselen"; "call_more_actions_unhold" = "Hervatten"; "call_more_actions_hold" = "Vasthouden"; -"call_holded" = "U heeft de oproep in de wacht"; +"call_holded" = "Je hebt de oproep in de wacht"; "call_remote_holded" = "%@ heeft de oproep in de wacht"; -"notice_declined_video_call_by_you" = "U heeft de oproep afgewezen"; +"notice_declined_video_call_by_you" = "Je hebt de oproep afgewezen"; "notice_declined_video_call" = "%@ heeft de oproep afgewezen"; "resume_call" = "Hervatten"; "call_consulting_with_user" = "In de wacht bij %@"; @@ -2268,7 +2268,7 @@ "attachment_large_with_resolution" = "Groot %@ (~%@)"; "attachment_medium_with_resolution" = "Middel %@ (~%@)"; "attachment_small_with_resolution" = "Klein %@ (~%@)"; -"attachment_size_prompt_message" = "U kunt dit uitzetten in uw instellingen."; +"attachment_size_prompt_message" = "Je kan dit uitzetten in je instellingen."; "attachment_size_prompt_title" = "Bevestig de afmeting om te versturen"; "attachment_unsupported_preview_message" = "Dit bestandstype wordt niet ondersteund."; "attachment_unsupported_preview_title" = "Kan geen voorbeeld geven"; @@ -2277,14 +2277,14 @@ "room_invite_to_space_option_detail" = "Ze kunnen %@ ontdekken, maar zijn geen lid van %@."; "spaces_creation_post_process_creating_space" = "Space aanmaken"; "spaces_creation_cancel_title" = "Stoppen met het aanmaken van een space?"; -"spaces_creation_visibility_title" = "Wat voor soort space wilt u aanmaken?"; +"spaces_creation_visibility_title" = "Wat voor soort space wil je aanmaken?"; "spaces_create_space_title" = "Een space aanmaken"; "spaces_add_space_title" = "Space aanmaken"; "create_room_processing" = "Kamer aanmaken"; "ignore_user" = "Negeer persoon"; "spaces_creation_post_process_inviting_users" = "%@ personen uitnodigen"; "settings_labs_use_only_latest_user_avatar_and_name" = "Toon laatste avatar en naam voor persoon in berichtgeschiedenis"; -"room_preview_decline_invitation_options" = "Wilt u de uitnodiging afwijzen of deze persoon negeren?"; +"room_preview_decline_invitation_options" = "Wil je de uitnodiging afwijzen of deze persoon negeren?"; /* The placeholder string contains onboarding_use_case_skip_button as a tappable action */ "onboarding_use_case_not_sure_yet" = "Nog niet zeker? %@"; "notice_error_unformattable_event" = "** Kan bericht niet weergeven. Meld een bug a.u.b."; @@ -2298,7 +2298,7 @@ "location_sharing_live_share_title" = "Live locatie delen"; "location_sharing_open_open_street_maps" = "Openen in OpenStreetMap"; "side_menu_coach_message" = "Veeg naar rechts of tik om alle kamers te zien"; -"spaces_add_room_missing_permission_message" = "U heeft geen rechten om kamers aan deze space toe te voegen."; +"spaces_add_room_missing_permission_message" = "Je hebt geen rechten om kamers aan deze space toe te voegen."; "spaces_creation_in_one_space" = "in 1 space"; "spaces_creation_in_many_spaces" = "in %@ spaces"; "spaces_creation_in_spacename_plus_many" = "in %@ + %@ spaces"; @@ -2308,54 +2308,54 @@ "spaces_creation_post_process_creating_room" = "%@ aanmaken"; "spaces_creation_post_process_uploading_avatar" = "Avatar uploaden"; "spaces_creation_post_process_creating_space_task" = "%@ aanmaken"; -"spaces_creation_invite_by_username_message" = "U kunt ze later ook uitnodigen."; -"spaces_creation_invite_by_username_title" = "Nodig uw team uit"; +"spaces_creation_invite_by_username_message" = "Je kan ze later ook uitnodigen."; +"spaces_creation_invite_by_username_title" = "Nodig je team uit"; "spaces_creation_invite_by_username" = "Uitnodigen op inlognaam"; -"spaces_creation_add_rooms_message" = "Aangezien deze space alleen voor u is, zal niemand hiervan op de hoogte worden gesteld. U kunt later meer toevoegen."; -"spaces_creation_add_rooms_title" = "Wat wilt u toevoegen?"; -"spaces_creation_sharing_type_me_and_teammates_detail" = "Een privéruimte voor u en uw teamgenoten"; +"spaces_creation_add_rooms_message" = "Aangezien deze space alleen voor jou is, zal niemand hiervan op de hoogte worden gesteld. Je kan later meer toevoegen."; +"spaces_creation_add_rooms_title" = "Wat wil je toevoegen?"; +"spaces_creation_sharing_type_me_and_teammates_detail" = "Een privéruimte voor jou en je teamgenoten"; "spaces_creation_sharing_type_me_and_teammates_title" = "Ik en teamgenoten"; -"spaces_creation_sharing_type_just_me_detail" = "Een privé space om uw kamers te organiseren"; +"spaces_creation_sharing_type_just_me_detail" = "Een privé space om je kamers te organiseren"; "spaces_creation_sharing_type_just_me_title" = "Alleen ik"; -"spaces_creation_sharing_type_message" = "Zorg ervoor dat de juiste mensen toegang hebben tot %@. U kunt dit later wijzigen."; -"spaces_creation_sharing_type_title" = "Met wie werkt u samen?"; +"spaces_creation_sharing_type_message" = "Zorg ervoor dat de juiste mensen toegang hebben tot %@. Je kan dit later wijzigen."; +"spaces_creation_sharing_type_title" = "Met wie werk je samen?"; "spaces_creation_email_invites_email_title" = "E-mail"; -"spaces_creation_email_invites_message" = "U kunt ze later ook uitnodigen."; -"spaces_creation_email_invites_title" = "Nodig uw team uit"; +"spaces_creation_email_invites_message" = "Je kan ze later ook uitnodigen."; +"spaces_creation_email_invites_title" = "Nodig je team uit"; "spaces_creation_new_rooms_support" = "Ondersteuning"; "spaces_creation_new_rooms_random" = "Willekeurig"; "spaces_creation_new_rooms_general" = "Algemeen"; "spaces_creation_new_rooms_room_name_title" = "Kamer naam"; "spaces_creation_new_rooms_message" = "We zullen voor elk een kamer maken."; -"spaces_creation_new_rooms_title" = "Wat zijn enkele discussies die u zult hebben?"; -"spaces_creation_cancel_message" = "Uw voortgang gaat verloren."; -"spaces_creation_private_space_title" = "Uw privé space"; -"spaces_creation_public_space_title" = "Uw openbare ruimte"; +"spaces_creation_new_rooms_title" = "Wat zijn enkele discussies die je zal hebben?"; +"spaces_creation_cancel_message" = "Je voortgang gaat verloren."; +"spaces_creation_private_space_title" = "Jouw privé space"; +"spaces_creation_public_space_title" = "Jouw openbare ruimte"; "spaces_creation_address_already_exists" = "%@\nbestaat al"; "spaces_creation_address_invalid_characters" = "%@\nheeft ongeldige tekens"; -"spaces_creation_address_default_message" = "Uw ruimte is zichtbaar op\n%@"; +"spaces_creation_address_default_message" = "Je ruimte is zichtbaar op\n%@"; "spaces_creation_empty_room_name_error" = "Naam verplicht"; "spaces_creation_address" = "Adres"; -"spaces_creation_settings_message" = "Voeg wat details toe om het te laten opvallen. U kunt deze op elk moment wijzigen."; -"spaces_creation_footer" = "U kunt dit later wijzigen"; -"spaces_creation_visibility_message" = "Als u lid wilt worden van een bestaande ruimte, heeft u een uitnodiging nodig."; +"spaces_creation_settings_message" = "Voeg wat details toe om het te laten opvallen. Je kan deze op elk moment wijzigen."; +"spaces_creation_footer" = "Je kan dit later wijzigen"; +"spaces_creation_visibility_message" = "Als je lid wil worden van een bestaande ruimte, heb je een uitnodiging nodig."; // Mark: - Space Creation "spaces_creation_hint" = "Spaces zijn een nieuwe manier om kamers en mensen te groeperen."; -"space_settings_current_address_message" = "Uw space is zichtbaar op\n%@"; -"space_settings_update_failed_message" = "Kan space-instellingen niet updaten. Wilt u het opnieuw proberen?"; +"space_settings_current_address_message" = "Je space is zichtbaar op\n%@"; +"space_settings_update_failed_message" = "Kan space-instellingen niet updaten. Wil je het opnieuw proberen?"; "space_settings_access_section" = "Wie heeft toegang tot deze ruimte?"; "space_topic" = "Beschrijving"; "space_public_join_rule_detail" = "Open voor iedereen, het beste voor gemeenschappen"; "spaces_add_space" = "Space toevoegen"; "spaces_add_room" = "Ruimte toevoegen"; "spaces_invite_people" = "Mensen uitnodigen"; -"space_private_join_rule_detail" = "Alleen op uitnodiging, het beste voor uzelf of teams"; +"space_private_join_rule_detail" = "Alleen op uitnodiging, het beste voor jezelf of teams"; "spaces_explore_rooms_one_room" = "1 kamer"; "spaces_explore_rooms_room_number" = "%@ kamers"; -"space_invite_not_enough_permission" = "U bent niet gemachtigd om mensen voor deze space uit te nodigen"; -"room_invite_not_enough_permission" = "U bent niet gemachtigd om mensen voor deze chatruimte uit te nodigen"; +"space_invite_not_enough_permission" = "Je bent niet gemachtigd om mensen voor deze space uit te nodigen"; +"room_invite_not_enough_permission" = "Je bent niet gemachtigd om mensen voor deze chatruimte uit te nodigen"; "room_invite_to_room_option_detail" = "Ze zullen geen deel uitmaken van %@."; "room_invite_to_room_option_title" = "Alleen naar deze kamer"; @@ -2385,7 +2385,7 @@ "room_suggestion_settings_screen_nav_title" = "Kamer voorstellen"; "room_access_space_chooser_other_spaces_section_info" = "Dit zijn waarschijnlijk dingen waar andere beheerders van %@ deel van uitmaken."; "room_access_space_chooser_other_spaces_section" = "Andere spaces of kamers"; -"room_access_space_chooser_known_spaces_section" = "Spaces waarvan u weet dat ze %@ bevatten"; +"room_access_space_chooser_known_spaces_section" = "Spaces waarvan je weet dat ze %@ bevatten"; "room_access_settings_screen_setting_room_access" = "Toegang tot de kamer instellen"; "room_access_settings_screen_upgrade_alert_upgrading" = "Kamer upgraden"; "room_access_settings_screen_upgrade_alert_upgrade_button" = "Upgrade"; @@ -2394,7 +2394,7 @@ "room_access_settings_screen_public_message" = "Iedereen kan vinden en meedoen."; "room_access_settings_screen_edit_spaces" = "Bewerk spaces"; "room_access_settings_screen_upgrade_required" = "Upgrade vereist"; -"room_access_settings_screen_restricted_message" = "Laat iedereen in een ruimte zoeken en meedoen.\nU wordt gevraagd om te bevestigen welke spaces."; +"room_access_settings_screen_restricted_message" = "Laat iedereen in een ruimte zoeken en meedoen.\nJe wordt gevraagd om te bevestigen welke spaces."; "room_access_settings_screen_private_message" = "Alleen uitgenodigde mensen kunnen vinden en deelnemen."; "room_access_settings_screen_message" = "Bepaal wie %@ kan vinden en er lid van kan worden."; "room_access_settings_screen_title" = "Wie heeft toegang tot deze kamer?"; @@ -2408,25 +2408,25 @@ "threads_beta_cancel" = "Niet nu"; "threads_beta_enable" = "Probeer het"; "threads_beta_information_link" = "Leer meer"; -"threads_beta_information" = "Houd discussies georganiseerd met discussielijnen.\n\nDiscussies helpen u gesprekken on-topic te houden en gemakkelijk bij te houden. "; -"threads_beta_title" = "Discussies"; +"threads_beta_information" = "Houd discussies georganiseerd met threads.\n\nThreads helpen je gesprekken on-topic te houden en gemakkelijk bij te houden. "; +"threads_beta_title" = "Threads"; "threads_notice_done" = "Ik snap het"; -"threads_notice_information" = "Alle discussies die tijdens de experimentele periode zijn gemaakt, worden nu weergegeven als gewone antwoorden.

Dit is een eenmalige overgang, aangezien discussies nu deel uitmaken van de Matrix-specificatie."; -"threads_notice_title" = "Discussies niet langer experimenteel 🎉"; -"room_participants_invite_prompt_to_msg" = "Weet u zeker dat u %@ wilt uitnodigen voor %@?"; +"threads_notice_information" = "Alle threads die tijdens de experimentele periode zijn gemaakt, worden nu weergegeven als gewone antwoorden.

Dit is een eenmalige overgang, aangezien threads nu deel uitmaken van de Matrix-specificatie."; +"threads_notice_title" = "Threads niet langer experimenteel 🎉"; +"room_participants_invite_prompt_to_msg" = "Weet je zeker dat je %@ wilt uitnodigen voor %@?"; "room_participants_leave_success" = "Kamer verlaten"; "room_participants_leave_processing" = "Verlaten"; "search_filter_placeholder" = "Filter"; "onboarding_celebration_button" = "Laten we beginnen"; -"onboarding_celebration_message" = "Ga op elk gewenst moment naar de instellingen om uw profiel bij te werken"; +"onboarding_celebration_message" = "Ga op elk gewenst moment naar de instellingen om je profiel bij te werken"; "onboarding_celebration_title" = "Ziet er goed uit!"; "onboarding_avatar_accessibility_label" = "Profielfoto"; -"onboarding_avatar_message" = "Tijd om een gezicht aan uw naam te geven"; +"onboarding_avatar_message" = "Tijd om een gezicht aan je naam te geven"; "onboarding_avatar_title" = "Voeg een profielfoto toe"; -"onboarding_display_name_max_length" = "Uw weergavenaam mag niet langer zijn dan 256 tekens"; -"onboarding_display_name_hint" = "U kunt dit later wijzigen"; +"onboarding_display_name_max_length" = "Je weergavenaam mag niet langer zijn dan 256 tekens"; +"onboarding_display_name_hint" = "Je kan dit later wijzigen"; "onboarding_display_name_placeholder" = "Weergavenaam"; -"onboarding_display_name_message" = "Dit wordt weergegeven wanneer u berichten verzendt."; +"onboarding_display_name_message" = "Dit wordt weergegeven wanneer je berichten verzendt."; "onboarding_display_name_title" = "Kies een weergavenaam"; "onboarding_personalization_skip" = "Sla deze stap over"; "onboarding_personalization_save" = "Opslaan en doorgaan"; @@ -2435,7 +2435,7 @@ /* The placeholder string contains the user's matrix ID */ "onboarding_congratulations_title" = "Gefeliciteerd!"; "onboarding_use_case_existing_server_button" = "Verbinding maken met server"; -"onboarding_use_case_existing_server_message" = "Wilt u lid worden van een bestaande server?"; +"onboarding_use_case_existing_server_message" = "Wil je lid worden van een bestaande server?"; "onboarding_use_case_community_messaging" = "Gemeenschappen"; "onboarding_use_case_work_messaging" = "Teams"; "onboarding_use_case_personal_messaging" = "Vrienden en familie"; @@ -2453,7 +2453,7 @@ "stop" = "Stop"; "joining" = "Meedoen"; "location_sharing_live_list_item_stop_sharing_action" = "Stop"; -"location_sharing_live_list_item_current_user_display_name" = "U"; +"location_sharing_live_list_item_current_user_display_name" = "Jij"; "location_sharing_live_list_item_last_update_invalid" = "Onbekende laatste update"; "location_sharing_live_list_item_last_update" = "%@ geleden bijgewerkt"; "location_sharing_live_list_item_sharing_expired" = "Delen verlopen"; @@ -2461,15 +2461,15 @@ "location_sharing_live_viewer_title" = "Locatie"; "location_sharing_live_map_callout_title" = "Deel locatie"; "room_access_settings_screen_upgrade_alert_note" = "Houd er rekening mee dat bij het upgraden een nieuwe versie van de kamer wordt gemaakt. Alle huidige berichten blijven in deze gearchiveerde ruimte."; -"room_access_settings_screen_upgrade_alert_message_no_param" = "Iedereen in een bovenliggende space kan deze ruimte vinden en er lid van worden. Het is niet nodig om iedereen handmatig uit te nodigen. U kunt dit op elk moment wijzigen in de kamerinstellingen."; -"room_access_settings_screen_upgrade_alert_message" = "Iedereen in %@ kan deze ruimte vinden en er lid van worden - het is niet nodig om iedereen handmatig uit te nodigen. U kunt dit op elk moment wijzigen in de kamerinstellingen."; -"settings_presence_offline_mode_description" = "Indien ingeschakeld, verschijnt u altijd offline voor andere personen, zelfs wanneer u de toepassing gebruikt."; +"room_access_settings_screen_upgrade_alert_message_no_param" = "Iedereen in een bovenliggende space kan deze ruimte vinden en er lid van worden. Het is niet nodig om iedereen handmatig uit te nodigen. Je kan dit op elk moment wijzigen in de kamerinstellingen."; +"room_access_settings_screen_upgrade_alert_message" = "Iedereen in %@ kan deze ruimte vinden en er lid van worden - het is niet nodig om iedereen handmatig uit te nodigen. Je kan dit op elk moment wijzigen in de kamerinstellingen."; +"settings_presence_offline_mode_description" = "Indien ingeschakeld, verschijn je altijd offline voor andere personen, zelfs wanneer je de toepassing gebruikt."; "settings_presence_offline_mode" = "Offline modus"; "settings_presence" = "Aanwezigheid"; -"threads_discourage_information_2" = "\n\nWilt u toch threads inschakelen?"; -"threads_discourage_information_1" = "Uw server ondersteunt momenteel geen discussies, dus deze functie kan onbetrouwbaar zijn. Sommige berichten in een discussie zijn mogelijk niet betrouwbaar beschikbaar. "; +"threads_discourage_information_2" = "\n\nWil je toch threads inschakelen?"; +"threads_discourage_information_1" = "Jouw server ondersteunt momenteel geen threads, dus deze functie kan onbetrouwbaar zijn. Sommige berichten in een thread zijn mogelijk niet betrouwbaar beschikbaar. "; /* The placeholder string contains the user's matrix ID */ -"onboarding_congratulations_message" = "Uw account %@ is aangemaakt"; +"onboarding_congratulations_message" = "Jouw account %@ is aangemaakt"; "onboarding_use_case_skip_button" = "Sla deze vraag over"; "location_sharing_live_stop_sharing_progress" = "Stop locatie delen"; "location_sharing_live_stop_sharing_error" = "Kan locatie delen niet stoppen"; @@ -2477,7 +2477,7 @@ "location_sharing_live_timer_selector_long" = "voor 8 uur"; "location_sharing_live_timer_selector_medium" = "voor 1 uur"; "location_sharing_live_timer_selector_short" = "voor 15 minuten"; -"location_sharing_live_timer_selector_title" = "Kies hoe lang anderen uw nauwkeurige locatie kunnen zien."; +"location_sharing_live_timer_selector_title" = "Kies hoe lang anderen je nauwkeurige locatie kunnen zien."; "location_sharing_live_error" = "Live locatie error"; "location_sharing_live_loading" = "Live locatie wordt geladen..."; "location_sharing_live_timer_incoming" = "Live tot %@"; @@ -2485,11 +2485,11 @@ "confirm" = "Bevestigen"; "location_sharing_allow_background_location_cancel_action" = "Niet nu"; "location_sharing_allow_background_location_validate_action" = "Instellingen"; -"location_sharing_allow_background_location_message" = "Als u live locatie wilt delen, heeft Element locatietoegang nodig wanneer de app op de achtergrond is. Om toegang in te schakelen, tik je op Instellingen> Locatie en selecteert u Altijd"; +"location_sharing_allow_background_location_message" = "Als je live locatie wilt delen, heeft Element locatietoegang nodig wanneer de app op de achtergrond is. Om toegang in te schakelen, tik je op Instellingen> Locatie en selecteer je Altijd"; "location_sharing_allow_background_location_title" = "Toegang te verlenen"; "settings_labs_enable_live_location_sharing" = "Live locatie delen - deel huidige locatie (actieve ontwikkeling, en tijdelijk, locaties blijven bestaan in kamergeschiedenis)"; /* The %@ placeholder will be replaced with the integration manager's URL. */ -"settings_integrations_allow_description" = "Gebruik een integratiebeheerder (%@) om bots, bruggen, widgets en stickerpakketten te beheren.\n\nIntegratiebeheerders ontvangen configuratiedata en kunnen widgets aanpassen, kameruitnodigingen versturen en bestuursniveaus instellen namens u."; +"settings_integrations_allow_description" = "Gebruik een integratiebeheerder (%@) om bots, bruggen, widgets en stickerpakketten te beheren.\n\nIntegratiebeheerders ontvangen configuratiedata en kunnen widgets aanpassen, kameruitnodigingen versturen en bestuursniveaus instellen namens jou."; "settings_ui_show_redactions_in_room_history" = "Toon een aanduiding voor verwijderde berichten"; // MARK: Reactions @@ -2504,19 +2504,19 @@ // Mark: Leave space "leave_space_action" = "Verlaat space"; -"spaces_feature_not_available" = "Deze functie is hier niet beschikbaar. Voor nu kunt u dit doen met %@ op uw computer."; +"spaces_feature_not_available" = "Deze functie is hier niet beschikbaar. Voor nu kan je dit doen met %@ op uw computer."; "home_context_menu_mark_as_read" = "Markeer als gelezen"; "settings_timeline" = "TIJDLIJN"; "room_accessibility_record_voice_message_hint" = "Dubbeltik en houd vast om op te nemen."; "room_accessibility_record_voice_message" = "Spraakbericht opnemen"; "message_reply_to_sender_sent_their_live_location" = "Live locatie."; "location_sharing_live_lab_promotion_activation" = "Live locatie delen inschakelen"; -"location_sharing_live_lab_promotion_text" = "Let op: dit is een labfunctie met een tijdelijke implementatie waarmee de geschiedenis van uw gedeelde locatie permanent zichtbaar is voor andere mensen in de kamer."; +"location_sharing_live_lab_promotion_text" = "Let op: dit is een labfunctie met een tijdelijke implementatie waarmee de geschiedenis van je gedeelde locatie permanent zichtbaar is voor andere mensen in de kamer."; "location_sharing_live_lab_promotion_title" = "Live locatie delen"; "location_sharing_map_credits_title" = "© Auteursrecht"; "room_info_back_button_title" = "Kamer Informatie"; -"network_offline_message" = "U bent offline, controleer uw verbinding."; -"network_offline_title" = "U bent offline"; +"network_offline_message" = "Je bent offline, controleer je verbinding."; +"network_offline_title" = "Je bent offline"; "password_validation_error_contain_symbol" = "Bevat een symbool."; "password_validation_error_contain_number" = "Bevat een cijfer."; "password_validation_error_contain_uppercase_letter" = "Bevat een hoofdletter."; @@ -2528,8 +2528,8 @@ "password_validation_error_header" = "Het opgegeven wachtwoord voldoet niet aan de onderstaande criteria:"; // MARK: Password Validation -"password_validation_info_header" = "Uw wachtwoord moet aan de onderstaande criteria voldoen:"; -"authentication_recaptcha_title" = "Bent u een mens?"; +"password_validation_info_header" = "Je wachtwoord moet aan de onderstaande criteria voldoen:"; +"authentication_recaptcha_title" = "Ben je een mens?"; "authentication_terms_policy_url_error" = "Kan het geselecteerde beleid niet vinden. Probeer het later opnieuw."; "authentication_terms_message" = "Lees de voorwaarden en het beleid van %@ a.u.b."; "authentication_terms_title" = "Privacy beleid"; @@ -2537,11 +2537,11 @@ "authentication_verify_msisdn_waiting_button" = "Code nogmaals versturen"; /* The placeholder will show the phone number that was entered. */ "authentication_verify_msisdn_waiting_message" = "Er is een code verzonden naar %@"; -"authentication_verify_msisdn_waiting_title" = "Verifieer uw telefoonnummer"; +"authentication_verify_msisdn_waiting_title" = "Verifieer je telefoonnummer"; "authentication_verify_msisdn_otp_text_field_placeholder" = "Bevestigingscode"; "authentication_verify_msisdn_text_field_placeholder" = "Telefoonnummer"; -"authentication_verify_msisdn_input_message" = "%@ moet uw account verifiëren"; -"authentication_verify_msisdn_input_title" = "Vul uw telefoonnummer in"; +"authentication_verify_msisdn_input_message" = "%@ moet je account verifiëren"; +"authentication_verify_msisdn_input_title" = "Vul je telefoonnummer in"; "authentication_choose_password_submit_button" = "Wachtwoord opnieuw instellen"; "authentication_choose_password_signout_all_devices" = "Uitloggen op alle apparaten"; "authentication_choose_password_text_field_placeholder" = "Nieuw Wachtwoord"; @@ -2550,41 +2550,41 @@ "authentication_forgot_password_waiting_button" = "Email opnieuw verzenden"; /* The placeholder will show the email address that was entered. */ "authentication_forgot_password_waiting_message" = "Volg de instructies die naar %@ zijn gestuurd"; -"authentication_forgot_password_waiting_title" = "Controleer uw e-mail."; +"authentication_forgot_password_waiting_title" = "Controleer je e-mail."; "authentication_forgot_password_text_field_placeholder" = "E-mailadres"; -"authentication_forgot_password_input_message" = "%@ stuurt uw een verificatie link"; -"authentication_forgot_password_input_title" = "Vul uw e-mailadres in"; +"authentication_forgot_password_input_message" = "%@ stuurt je een verificatie link"; +"authentication_forgot_password_input_title" = "Vul je e-mailadres in"; "authentication_verify_email_waiting_button" = "Email opnieuw verzenden"; "authentication_verify_email_waiting_hint" = "Geen e-mail ontvangen?"; /* The placeholder will show the email address that was entered. */ "authentication_verify_email_waiting_message" = "Volg de instructies die naar %@ gestuurd zijn"; -"authentication_verify_email_waiting_title" = "Verifieer uw e-mailadres."; +"authentication_verify_email_waiting_title" = "Verifieer je e-mailadres."; "authentication_verify_email_text_field_placeholder" = "E-mailadres"; -"authentication_verify_email_input_message" = "%@ moet uw account verifiëren"; -"authentication_verify_email_input_title" = "Vul uw e-mailadres in"; -"authentication_cancel_flow_confirmation_message" = "Uw account is nog niet aangemaakt. Het registratieproces stoppen?"; +"authentication_verify_email_input_message" = "%@ moet je account verifiëren"; +"authentication_verify_email_input_title" = "Vul je e-mailadres in"; +"authentication_cancel_flow_confirmation_message" = "Jouw account is nog niet aangemaakt. Het registratieproces stoppen?"; "authentication_server_selection_generic_error" = "Kan geen server vinden op deze URL, controleer of deze correct is."; "authentication_server_selection_server_url" = "Server URL"; -"authentication_server_selection_register_message" = "Wat is het adres van uw server? Dit is als een thuis voor al uw gegevens"; -"authentication_server_selection_register_title" = "Selecteer uw server"; -"authentication_server_selection_login_message" = "Wat is het adres van uw server?"; +"authentication_server_selection_register_message" = "Wat is het adres van je server? Dit is als een thuis voor al je gegevens"; +"authentication_server_selection_register_title" = "Selecteer je server"; +"authentication_server_selection_login_message" = "Wat is het adres van je server?"; "authentication_server_selection_login_title" = "Verbinding maken met server"; -"authentication_server_info_title" = "Waar uw gesprekken zullen leven"; +"authentication_server_info_title" = "Waar je gesprekken zullen leven"; "authentication_login_forgot_password" = "Wachtwoord vergeten"; "authentication_login_username" = "Inlognaam / E-mailadres / Telefoon"; "authentication_login_title" = "Welkom terug!"; "authentication_registration_password_footer" = "Moet 8 tekens of meer zijn"; /* The placeholder will show the full Matrix ID that has been entered. */ -"authentication_registration_username_footer_available" = "Anderen kunnen u %@ ontdekken"; -"authentication_registration_username_footer" = "U kunt dit later niet meer wijzigen"; +"authentication_registration_username_footer_available" = "Anderen kunnen je %@ ontdekken"; +"authentication_registration_username_footer" = "Je kan dit later niet meer wijzigen"; "authentication_registration_username" = "Inlognaam"; // MARK: Authentication "authentication_registration_title" = "Maak een account aan"; -"authentication_server_info_title_login" = "Waar uw gesprekken leven"; -"location_sharing_invalid_power_level_message" = "U moet de juiste rechten hebben om de live locatie in deze kamer te delen."; -"location_sharing_invalid_power_level_title" = "U heeft geen toestemming om de live locatie te delen"; -"authentication_choose_password_not_verified_message" = "Controleer uw inbox"; +"authentication_server_info_title_login" = "Waar je gesprekken leven"; +"location_sharing_invalid_power_level_message" = "Je moet de juiste rechten hebben om de live locatie in deze kamer te delen."; +"location_sharing_invalid_power_level_title" = "Je hebt geen toestemming om de live locatie te delen"; +"authentication_choose_password_not_verified_message" = "Controleer je inbox"; "authentication_choose_password_not_verified_title" = "E-mailadres niet geverifieerd"; // MARK: User sessions management @@ -2600,28 +2600,28 @@ // Mark: - Space Selector "space_selector_title" = "Mijn spaces"; -"room_invites_empty_view_information" = "Hier verschijnen uw uitnodigingen."; +"room_invites_empty_view_information" = "Hier verschijnen je uitnodigingen."; // Mark: - Room invites "room_invites_empty_view_title" = "Niets nieuws."; "all_chats_onboarding_try_it" = "Probeer het uit"; "all_chats_onboarding_title" = "Wat is nieuw"; -"all_chats_onboarding_page_message3" = "Tik op uw profiel om ons te laten weten wat u ervan vindt."; +"all_chats_onboarding_page_message3" = "Tik op je profiel om ons te laten weten wat je ervan vindt."; "all_chats_onboarding_page_title3" = "Geef feedback"; -"all_chats_onboarding_page_message2" = "Krijg sneller en gemakkelijker toegang tot uw Spaces (linksonder) dan ooit tevoren."; +"all_chats_onboarding_page_message2" = "Krijg sneller en gemakkelijker toegang tot je Spaces (linksonder) dan ooit tevoren."; "all_chats_onboarding_page_title2" = "Toegang tot spaces"; -"all_chats_onboarding_page_message1" = "Om uw Element te vereenvoudigen, zijn tabbladen nu optioneel. Beheer ze met behulp van het menu rechtsboven."; +"all_chats_onboarding_page_message1" = "Om je Element te vereenvoudigen, zijn tabbladen nu optioneel. Beheer ze met behulp van het menu rechtsboven."; "all_chats_onboarding_page_title1" = "Welkom bij de nieuwe weergave!"; "all_chats_edit_menu_space_settings" = "Space instellingen"; "all_chats_edit_menu_leave_space" = "Verlaat %@"; "all_chats_user_menu_settings" = "Gebruikersinstellingen"; "room_recents_recently_viewed_section" = "Recent bekeken"; -"all_chats_nothing_found_placeholder_message" = "Probeer uw zoekopdracht aan te passen."; +"all_chats_nothing_found_placeholder_message" = "Probeer je zoekopdracht aan te passen."; "all_chats_nothing_found_placeholder_title" = "Niets gevonden."; -"all_chats_empty_unreads_placeholder_message" = "Dit is waar uw ongelezen berichten zullen verschijnen, als u er een aantal hebt."; -"all_chats_empty_list_placeholder_title" = "U bent helemaal bij."; -"all_chats_empty_view_information" = "De alles-in-één beveiligde chat-app voor teams, vrienden en organisaties. Maak een chat of sluit u aan bij een bestaande chatruimte om aan de slag te gaan."; +"all_chats_empty_unreads_placeholder_message" = "Dit is waar je ongelezen berichten zullen verschijnen, als je er een aantal hebt."; +"all_chats_empty_list_placeholder_title" = "Je bent helemaal bij."; +"all_chats_empty_view_information" = "De alles-in-één beveiligde chat-app voor teams, vrienden en organisaties. Maak een chat of sluit je aan bij een bestaande chatruimte om aan de slag te gaan."; "all_chats_empty_space_information" = "Spaces zijn een nieuwe manier om kamers en mensen te groeperen. Voeg een bestaande kamer toe of maak een nieuwe aan met de knop rechtsonder."; "all_chats_empty_view_title" = "%@\nziet er een beetje leeg uit."; "all_chats_all_filter" = "Alles"; @@ -2630,9 +2630,9 @@ "all_chats_edit_layout_show_filters" = "Filters tonen"; "all_chats_edit_layout_show_recents" = "Recente tonen"; "all_chats_edit_layout_sorting_options_title" = "Berichten sorteren op"; -"all_chats_edit_layout_pin_spaces_title" = "Zet uw spaces vast"; -"all_chats_edit_layout_add_filters_message" = "Filter uw berichten automatisch in de categorieën van uw keuze"; -"all_chats_edit_layout_add_filters_title" = "Filter uw berichten"; +"all_chats_edit_layout_pin_spaces_title" = "Zet je spaces vast"; +"all_chats_edit_layout_add_filters_message" = "Filter je berichten automatisch in de categorieën van jouw keuze"; +"all_chats_edit_layout_add_filters_title" = "Filter je berichten"; "all_chats_edit_layout_add_section_message" = "Secties vastprikken op Thuis voor gemakkelijke toegang"; "all_chats_edit_layout_add_section_title" = "Sectie toevoegen aan thuis"; "all_chats_edit_layout_unreads" = "Ongelezen"; @@ -2644,7 +2644,7 @@ "all_chats_title" = "Alle gesprekken"; "spaces_subspace_creation_visibility_message" = "De gecreëerde space wordt toegevoegd aan %@."; -"spaces_subspace_creation_visibility_title" = "Welk type subspace wilt u maken?"; +"spaces_subspace_creation_visibility_title" = "Welk type subspace wil je maken?"; "spaces_explore_rooms_format" = "Ontdek %@"; "spaces_create_subspace_title" = "Maak een subspace"; "spaces_add_subspace_title" = "Maak space binnen %@"; @@ -2721,17 +2721,17 @@ "user_other_session_filter_menu_verified" = "Geverifieerd"; "user_other_session_filter_menu_all" = "Alle sessies"; "user_other_session_filter" = "Filter"; -"user_other_session_verified_sessions_header_subtitle" = "Voor de beste beveiliging logt u uit bij elke sessie die u niet meer herkent of gebruikt."; -"user_other_session_current_session_details" = "Uw huidige sessie"; -"user_other_session_unverified_sessions_header_subtitle" = "Verifieer uw sessies voor verbeterde beveiligde berichtenuitwisseling of meld u af bij sessies die u niet meer herkent of gebruikt."; +"user_other_session_verified_sessions_header_subtitle" = "Voor de beste beveiliging log je uit bij elke sessie die je niet meer herkent of gebruikt."; +"user_other_session_current_session_details" = "Jouw huidige sessie"; +"user_other_session_unverified_sessions_header_subtitle" = "Verifieer je sessies voor verbeterde beveiligde berichtenuitwisseling of meld je af bij sessies die je niet meer herkent of gebruikt."; "user_other_session_security_recommendation_title" = "Beveiligingsaanbeveling"; "user_session_push_notifications_message" = "Indien ingeschakeld, ontvangt deze sessie pushmeldingen."; "user_session_push_notifications" = "Pushmeldingen"; "user_other_session_verified_additional_info" = "Deze sessie is klaar voor beveiligde berichtenuitwisseling."; -"user_other_session_unverified_additional_info" = "Verifieer of meld u af bij deze sessie voor de beste beveiliging en betrouwbaarheid."; -"user_session_verification_unknown_additional_info" = "Verifieer uw huidige sessie om de verificatiestatus van deze sessie weer te geven."; -"user_session_unverified_additional_info" = "Verifieer uw huidige sessie voor verbeterde beveiligde berichtenuitwisseling."; -"user_session_verified_additional_info" = "Uw huidige sessie is klaar voor beveiligde berichtenuitwisseling."; +"user_other_session_unverified_additional_info" = "Verifieer of meld je af bij deze sessie voor de beste beveiliging en betrouwbaarheid."; +"user_session_verification_unknown_additional_info" = "Verifieer je huidige sessie om de verificatiestatus van deze sessie weer te geven."; +"user_session_unverified_additional_info" = "Verifieer je huidige sessie voor verbeterde beveiligde berichtenuitwisseling."; +"user_session_verified_additional_info" = "Je huidige sessie is klaar voor beveiligde berichtenuitwisseling."; "user_session_learn_more" = "Meer lezen"; "user_session_view_details" = "Bekijk details"; "user_session_verify_action" = "Sessie verifiëren"; @@ -2744,13 +2744,13 @@ "user_sessions_view_all_action" = "Alles bekijken (%d)"; "user_sessions_overview_link_device" = "Een apparaat koppelen"; "user_sessions_overview_current_session_section_title" = "Huidige sessie"; -"user_sessions_overview_other_sessions_section_info" = "Voor de beste beveiliging verifieert u uw sessies en meldt u zich af bij elke sessie die u niet meer herkent of gebruikt."; +"user_sessions_overview_other_sessions_section_info" = "Voor de beste beveiliging verifieer je jouw sessies en meld jij je af bij elke sessie die je niet meer herkent of gebruikt."; "user_sessions_overview_other_sessions_section_title" = "Andere sessies"; -"user_sessions_overview_security_recommendations_inactive_info" = "Overweeg om u af te melden bij oude sessies (90 dagen of ouder) die u niet meer gebruikt."; +"user_sessions_overview_security_recommendations_inactive_info" = "Overweeg om je af te melden bij oude sessies (90 dagen of ouder) die je niet meer gebruikt."; "user_sessions_overview_security_recommendations_inactive_title" = "Inactieve sessies"; -"user_sessions_overview_security_recommendations_unverified_info" = "Verifieer of meld u af bij niet geverifieerde sessies."; +"user_sessions_overview_security_recommendations_unverified_info" = "Verifieer of meld je af bij niet geverifieerde sessies."; "user_sessions_overview_security_recommendations_unverified_title" = "Niet geverifieerde sessies"; -"user_sessions_overview_security_recommendations_section_info" = "Verbeter uw accountbeveiliging door deze aanbevelingen op te volgen."; +"user_sessions_overview_security_recommendations_section_info" = "Verbeter je accountbeveiliging door deze aanbevelingen op te volgen."; "user_sessions_overview_security_recommendations_section_title" = "Beveiligingsaanbevelingen"; // MARK: User sessions management @@ -2759,13 +2759,13 @@ "user_sessions_default_session_display_name" = "%@ iOS"; "all_chats_user_menu_accessibility_label" = "Gebruikersmenu"; "voice_broadcast_playback_loading_error" = "Kan deze spraakuitzending niet afspelen."; -"voice_broadcast_already_in_progress_message" = "U neemt al een spraakuitzending op. Beëindig uw huidige spraakuitzending om een nieuwe te starten."; +"voice_broadcast_already_in_progress_message" = "Je neemt al een spraakuitzending op. Beëindig je huidige spraakuitzending om een nieuwe te starten."; "voice_broadcast_blocked_by_someone_else_message" = "Iemand anders neemt al een spraakuitzending op. Wacht tot hun spraakuitzending is afgelopen om een nieuwe te starten."; -"voice_broadcast_permission_denied_message" = "U heeft niet de vereiste rechten om een spraakuitzending in deze kamer te starten. Neem contact op met een kamer beheerder om uw machtigingen te upgraden."; +"voice_broadcast_permission_denied_message" = "Je hebt niet de vereiste rechten om een spraakuitzending in deze kamer te starten. Neem contact op met een kamer beheerder om je machtigingen te upgraden."; // Mark: - Voice broadcast "voice_broadcast_unauthorized_title" = "Kan geen nieuwe spraakuitzending starten"; -"sign_out_confirmation_message" = "Weet u zeker dat u zich wilt afmelden?"; +"sign_out_confirmation_message" = "Weet je zeker dat je wil afmelden?"; // MARK: Sign out warning @@ -2773,35 +2773,39 @@ "manage_session_rename" = "Sessie hernoemen"; "manage_session_name_info_link" = "Lees meer"; /* The placeholder will be replaces with manage_session_name_info_link */ -"manage_session_name_info" = "Houd er rekening mee dat sessienamen ook zichtbaar zijn voor mensen met wie u communiceert. %@"; -"manage_session_name_hint" = "Met aangepaste sessienamen kunt u uw apparaten gemakkelijker herkennen."; +"manage_session_name_info" = "Houd er rekening mee dat sessienamen ook zichtbaar zijn voor mensen met wie je communiceert. %@"; +"manage_session_name_hint" = "Met aangepaste sessienamen kan je jouw apparaten gemakkelijker herkennen."; "settings_labs_enable_voice_broadcast" = "Voice-uitzending (in actieve ontwikkeling)"; "settings_labs_enable_wysiwyg_composer" = "Probeer de rich-text-editor (platte tekst-modus komt binnenkort)"; "settings_labs_enable_new_session_manager" = "Nieuwe sessiemanager"; -"room_first_message_placeholder" = "Stuur uw eerste bericht…"; +"room_first_message_placeholder" = "Stuur je eerste bericht…"; "authentication_qr_login_failure_retry" = "Probeer het nog eens"; "authentication_qr_login_failure_request_timed_out" = "De koppeling is niet binnen de vereiste tijd voltooid."; "authentication_qr_login_failure_request_denied" = "Het verzoek is geweigerd op het andere apparaat."; "authentication_qr_login_failure_invalid_qr" = "QR-code is ongeldig."; "authentication_qr_login_failure_title" = "Koppelen mislukt"; -"authentication_qr_login_loading_signed_in" = "U bent nu aangemeld op uw andere apparaat."; +"authentication_qr_login_loading_signed_in" = "Je bent nu aangemeld op je andere apparaat."; "authentication_qr_login_loading_waiting_signin" = "Wachten tot het apparaat zich aanmeldt."; "authentication_qr_login_loading_connecting_device" = "Verbinden met apparaat"; -"authentication_qr_login_confirm_alert" = "Zorg ervoor dat u de herkomst van deze code kent. Door apparaten te koppelen, geeft u iemand volledige toegang tot uw account."; -"authentication_qr_login_confirm_subtitle" = "Controleer of de onderstaande code overeenkomt met uw andere apparaat:"; +"authentication_qr_login_confirm_alert" = "Zorg ervoor dat je de herkomst van deze code kent. Door apparaten te koppelen, geef je iemand volledige toegang tot je account."; +"authentication_qr_login_confirm_subtitle" = "Controleer of de onderstaande code overeenkomt met je andere apparaat:"; "authentication_qr_login_confirm_title" = "Beveiligde verbinding tot stand gebracht"; "authentication_qr_login_scan_subtitle" = "Positioneer de QR-code in het vierkant hieronder"; "authentication_qr_login_scan_title" = "Scan QR-code"; "authentication_qr_login_display_step2" = "Selecteer 'Aanmelden met QR-code'"; -"authentication_qr_login_display_step1" = "Open Element op uw andere apparaat"; -"authentication_qr_login_display_subtitle" = "Scan de onderstaande QR-code met uw apparaat dat is uitgelogd."; +"authentication_qr_login_display_step1" = "Open Element op je andere apparaat"; +"authentication_qr_login_display_subtitle" = "Scan de onderstaande QR-code met je apparaat dat is uitgelogd."; "authentication_qr_login_display_title" = "Een apparaat koppelen"; "authentication_qr_login_start_display_qr" = "QR-code weergeven op dit apparaat"; "authentication_qr_login_start_need_alternative" = "Een alternatieve methode nodig?"; "authentication_qr_login_start_step4" = "Selecteer 'Toon QR-code op dit apparaat'"; "authentication_qr_login_start_step3" = "Selecteer 'Een apparaat koppelen'"; "authentication_qr_login_start_step2" = "Ga naar Instellingen -> Beveiliging en privacy"; -"authentication_qr_login_start_step1" = "Open Element op uw andere apparaat"; -"authentication_qr_login_start_subtitle" = "Gebruik de camera op dit apparaat om de QR-code te scannen die op uw andere apparaat wordt weergegeven:"; +"authentication_qr_login_start_step1" = "Open Element op je andere apparaat"; +"authentication_qr_login_start_subtitle" = "Gebruik de camera op dit apparaat om de QR-code te scannen die op je andere apparaat wordt weergegeven:"; "authentication_qr_login_start_title" = "Scan QR-code"; "authentication_login_with_qr" = "Log in met QR-code"; +"user_other_session_menu_sign_out_sessions" = "Bij %@ sessies afmelden"; +"manage_session_sign_out_other_sessions" = "Bij alle andere sessies afmelden"; +"user_sessions_hide_location_info" = "Verberg IP-adres"; +"user_sessions_show_location_info" = "Toon IP-adres"; diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index 3741d98419..7e2cc90f2a 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -2605,7 +2605,7 @@ /* The placeholder will be replaces with manage_session_name_info_link */ "manage_session_name_info" = "Por favor esteja ciente que nomes de sessões também são visíveis a pessoas com quem você se comunica. %@"; "manage_session_name_hint" = "Nomes de sessões personalizados podem ajudar você a reconhecer seus dispositivos mais facilmente."; -"settings_labs_enable_wysiwyg_composer" = "Experimente o editor de texto rico (modo de texto puro vindo em breve)"; +"settings_labs_enable_wysiwyg_composer" = "Experimente o editor de texto rico"; "wysiwyg_composer_start_action_voice_broadcast" = "Broadcast de voz"; "voice_broadcast_playback_loading_error" = "Incapaz de tocar este broadcast de voz."; "voice_broadcast_already_in_progress_message" = "Você já está gravando um broadcast de voz. Por favor termine seu broadcast de voz atual para começar um novo."; @@ -2618,3 +2618,9 @@ "deselect_all" = "Desselecionar Todas(os)"; "user_other_session_menu_select_sessions" = "Selecionar sessões"; "user_other_session_selected_count" = "%@ selecionadas"; +"user_other_session_menu_sign_out_sessions" = "Fazer signout de %@ sessões"; +"user_sessions_hide_location_info" = "Esconder endereço de IP"; +"user_sessions_show_location_info" = "Mostrar endereço de IP"; +"manage_session_sign_out_other_sessions" = "Fazer signout de todas as outras sessões"; +"voice_broadcast_tile" = "Broadcast de voz"; +"voice_broadcast_live" = "Ao vivo"; diff --git a/Riot/Assets/ru.lproj/Vector.strings b/Riot/Assets/ru.lproj/Vector.strings index 7680e9765a..c02dfd82d9 100644 --- a/Riot/Assets/ru.lproj/Vector.strings +++ b/Riot/Assets/ru.lproj/Vector.strings @@ -2131,7 +2131,7 @@ "authentication_recaptcha_title" = "Вы человек?"; /* The placeholder will show the homeserver's domain */ "authentication_terms_message" = "Пожалуйста прочитайте условия и политику %@"; -"authentication_terms_title" = "Политика сервера"; +"authentication_terms_title" = "Политика приватности"; "authentication_verify_msisdn_invalid_phone_number" = "Недействительный номер телефона"; "authentication_verify_msisdn_waiting_button" = "Отправить код повторно"; /* The placeholder will show the phone number that was entered. */ @@ -2158,3 +2158,13 @@ "user_other_session_current_session_details" = "Текущая сессия"; "user_other_session_filter_menu_all" = "Все сессии"; "wysiwyg_composer_start_action_stickers" = "Наклейки"; +"authentication_login_with_qr" = "Войти при помощи QR-кода"; +"onboarding_avatar_message" = "Время добавить лицо к имени"; +"onboarding_congratulations_home_button" = "На главную"; +"invite_to" = "Пригласить в %@"; +"authentication_qr_login_start_step3" = "Выберите «Соеденить устройство»"; +"authentication_qr_login_start_step2" = "Зайдите в Настройки -> Безопасность и Приватность"; +"authentication_qr_login_start_step1" = "Откройте Element на вашем другом устройстве"; +"authentication_qr_login_start_subtitle" = "Используйте камеру на этом устройстве, чтобы сканировать QR-код, отображённый на вашем другом устройстве:"; +"authentication_qr_login_start_title" = "Сканировать QR-код"; +"authentication_terms_policy_url_error" = "Не получилось найти выбранные правила. Пожалуйста, попробуйте снова позже."; diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index ba65a69cb5..9f1cc6b65d 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2827,7 +2827,7 @@ /* The placeholder will be replaces with manage_session_name_info_link */ "manage_session_name_info" = "Uvedomte si, že názvy relácií sú viditeľné aj pre ľudí, s ktorými komunikujete. %@"; "manage_session_name_hint" = "Vlastné názvy relácií vám pomôžu ľahšie rozpoznať vaše zariadenia."; -"settings_labs_enable_wysiwyg_composer" = "Vyskúšajte rozšírený textový editor (čistý textový režim sa objaví čoskoro)"; +"settings_labs_enable_wysiwyg_composer" = "Vyskúšajte rozšírený textový editor"; "wysiwyg_composer_start_action_voice_broadcast" = "Hlasové vysielanie"; "voice_broadcast_already_in_progress_message" = "Už nahrávate hlasové vysielanie. Ukončite aktuálne hlasové vysielanie a spustite nové."; "voice_broadcast_blocked_by_someone_else_message" = "Niekto iný už nahráva hlasové vysielanie. Počkajte, kým sa skončí jeho hlasové vysielanie, a potom spustite nové."; @@ -2840,3 +2840,22 @@ "deselect_all" = "Zrušiť výber všetkých"; "user_other_session_selected_count" = "%@ vybratých"; "user_other_session_menu_select_sessions" = "Vyberte relácie"; +"user_other_session_menu_sign_out_sessions" = "Odhlásiť sa z %@ relácií"; +"user_sessions_hide_location_info" = "Skryť IP adresu"; +"user_sessions_show_location_info" = "Zobraziť IP adresu"; +"manage_session_sign_out_other_sessions" = "Odhlásiť zo všetkých ostatných relácií"; +"voice_broadcast_tile" = "Hlasové vysielanie"; +"voice_broadcast_live" = "Naživo"; +"user_session_rename_session_description" = "Ostatní používatelia v priamych správach a miestnostiach, do ktorých sa pripojíte, si môžu pozrieť úplný zoznam vašich relácií.\n\nTo im poskytuje istotu, že komunikujú naozaj s vami, ale zároveň to znamená, že vidia názov relácie, ktorý sem zadáte."; +"user_session_rename_session_title" = "Premenovanie relácií"; +"user_session_inactive_session_description" = "Neaktívne relácie sú relácie, ktoré ste určitý čas nepoužívali, ale naďalej dostávajú šifrovacie kľúče.\n\nOdstránenie neaktívnych relácií zvyšuje bezpečnosť a výkon a uľahčuje identifikáciu podozrivých nových relácií."; +"user_session_inactive_session_title" = "Neaktívne relácie"; +"user_session_unverified_session_description" = "Neoverené relácie sú relácie, do ktorých ste sa prihlásili pomocou svojich prístupových údajov, ale ktoré neboli krížovo overené.\n\nMali by ste si byť obzvlášť istí, že tieto relácie poznáte, pretože by mohli predstavovať neoprávnené použitie vášho konta."; +"user_session_unverified_session_title" = "Neoverená relácia"; +"user_session_verified_session_description" = "Overené relácie sú všade tam, kde používate Element po zadaní svojho prístupového hesla alebo po potvrdení svojej totožnosti inou overenou reláciou.\n\nTo znamená, že máte všetky kľúče potrebné na odomknutie zašifrovaných správ a potvrdenie pre ostatných používateľov, že tejto relácii dôverujete."; +"user_session_verified_session_title" = "Overené relácie"; +"user_session_got_it" = "Rozumiem"; +"key_verification_alert_body" = "Skontrolujte, či je vaše konto bezpečné."; + +// Unverified sessions +"key_verification_alert_title" = "Máte neoverené relácie"; diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index 280ad600cf..847de8e424 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -2362,7 +2362,7 @@ "authentication_terms_policy_url_error" = "S’arrihet të gjendet rregulli i përzgjedhur. Ju lutemi, riprovoni."; /* The placeholder will show the homeserver's domain */ "authentication_terms_message" = "Ju lutemi, lexoni kushte dhe rregulla të %@"; -"authentication_terms_title" = "Rregulla shërbyesi"; +"authentication_terms_title" = "Rregulla privatësie"; "authentication_verify_msisdn_invalid_phone_number" = "Numër telefoni i pavlefshëm"; "authentication_verify_msisdn_waiting_button" = "Ridërgomëni kodin"; /* The placeholder will show the phone number that was entered. */ @@ -2417,3 +2417,216 @@ // MARK: Authentication "authentication_registration_title" = "Krijoni llogarinë tuaj"; +"all_chats_onboarding_page_message3" = "Prekni profilin tuaj që të na bëni të ditur se ç’mendoni."; +"all_chats_edit_layout_add_section_message" = "Fiksoni ndarje te kreu, për hyrje të lehtë në ta"; +"room_event_encryption_info_key_authenticity_not_guaranteed" = "S’mund të garantohet mirëfilltësia e këtij mesazhi të fshehtëzuar në këtë pajisje."; +"deselect_all" = "Shpërzgjidhi Krejt"; +"wysiwyg_composer_format_action_strikethrough" = "Apliko format me të nënvizuara"; +"wysiwyg_composer_format_action_underline" = "Apliko format me të hequravije"; +"wysiwyg_composer_format_action_italic" = "Apliko format me të pjerrta"; + +// Formatting Actions +"wysiwyg_composer_format_action_bold" = "Apliko format me të trasha"; +"wysiwyg_composer_start_action_voice_broadcast" = "Transmetim zanor"; +"wysiwyg_composer_start_action_text_formatting" = "Formatim Teksti"; +"wysiwyg_composer_start_action_camera" = "Kamerë"; +"wysiwyg_composer_start_action_location" = "Vendndodhje"; +"wysiwyg_composer_start_action_polls" = "Pyetësorë"; +"wysiwyg_composer_start_action_attachments" = "Bashkëngjitje"; +"wysiwyg_composer_start_action_stickers" = "Ngjitës"; + + +// Mark: - WYSIWYG Composer + +// Send Media Actions +"wysiwyg_composer_start_action_media_picker" = "Fototekë"; +"user_session_overview_session_details_button_title" = "Hollësi sesioni"; +"user_session_overview_session_title" = "Sesion"; +"user_session_overview_current_session_title" = "Sesioni i tanishëm"; +"user_session_details_application_url" = "URL"; +"user_session_details_application_version" = "Version"; +"user_session_details_application_name" = "Emër"; +"user_session_details_device_os" = "Sistem Operativ"; +"user_session_details_device_browser" = "Shfletues"; +"user_session_details_device_model" = "Model"; +"user_session_details_device_ip_location" = "Venndodhje IP-je"; +"user_session_details_device_ip_address" = "Adresë IP"; +"user_session_details_last_activity" = "Veprimtaria e fundit"; +"user_session_details_session_section_footer" = "Kopjoni çfarëdo të dhëne duke prekur mbi të dhe duke e mbajtur të shtypur."; +"user_session_details_session_id" = "ID Sesioni"; +"user_session_details_session_name" = "Emër sesioni"; +"user_session_details_device_section_header" = "Pajisje"; +"user_session_details_application_section_header" = "Aplikacion"; +"user_session_details_session_section_header" = "Sesion"; +"user_session_details_title" = "Hollësi sesioni"; +"device_type_name_unknown" = "E panjohur"; +"device_type_name_mobile" = "Celular"; +"device_type_name_web" = "Web"; +"device_type_name_desktop" = "Desktop"; +"device_name_unknown" = "Klient i panjohur"; +"user_inactive_session_item_with_date" = "Joaktiv për 90 e ca ditë (%@)"; +"user_inactive_session_item" = "Joaktiv për 90 e ca ditë"; +"user_session_item_details_last_activity" = "Veprimtaria e fundit %@"; + +/* %1$@ will be the verification state and %2$@ will be user_session_item_details_verification_unknown or user_other_session_current_session_details */ +"user_session_item_details" = "%1$@ · %2$@"; +// First item is client name and second item is session display name +"user_session_name" = "%@: %@"; +"user_other_session_menu_select_sessions" = "Përzgjidhni sesione"; +"user_other_session_selected_count" = "%@ të përzgjedhur"; +"user_other_session_clear_filter" = "Spastroje filtrin"; +"user_other_session_no_unverified_sessions" = "S’u gjetën sesione të paverifikuar."; +"user_other_session_no_verified_sessions" = "S’u gjetën sesione të verifikuar."; +"user_other_session_no_inactive_sessions" = "S’u gjetën sesione joaktive."; +"user_other_session_filter_menu_inactive" = "Joaktiv"; +"user_other_session_filter_menu_unverified" = "I paverifikuar"; +"user_other_session_filter_menu_verified" = "I verifikuar"; +"user_other_session_filter_menu_all" = "Krejt sesionet"; +"user_other_session_filter" = "Filtroji"; +"user_other_session_verified_sessions_header_subtitle" = "Për sigurinë më të mirë, dilni nga çfarëdo sesioni që nuk e njihni apo përdorni më."; +"user_other_session_current_session_details" = "Sesioni juaj i tanishëm"; +"user_other_session_unverified_sessions_header_subtitle" = "Verifikoni sesionet tuaj, për shkëmbim më të sigurt mesazhesh, ose dilni prej atyre që nuk i njihni, apo përdorni më."; +"user_other_session_security_recommendation_title" = "Rekomandim sigurie"; +"user_session_push_notifications_message" = "Kur aktivizohet, ky sesion do të marrë njoftime push."; +"user_session_push_notifications" = "Njoftime Push"; +"user_other_session_verified_additional_info" = "Ky sesion është gati për shkëmbim të sigurt mesazhesh."; +"user_other_session_unverified_additional_info" = "Për sigurinë dhe besueshmërinë më të mirë, verifikojeni, ose dilni nga ky sesion."; +"user_session_verification_unknown_additional_info" = "Verifikoni sesionin tuaj të tanishëm, që të shfaqni gjendjen e verifikimit të këtij sesioni."; +"user_session_unverified_additional_info" = "Verifikoni sesionin tuaj të tanishëm, për shkëmbim më të sigurt të mesazheve."; +"user_session_verified_additional_info" = "Sesioni juaj i tanishëm është gati për shkëmbim të sigurt mesazhesh."; +"user_session_learn_more" = "Mësoni më tepër"; +"user_session_view_details" = "Shihni hollësitë"; +"user_session_verify_action" = "Verifiko sesion"; +"user_session_verification_unknown_short" = "I panjohur"; +"user_session_unverified_short" = "I paverifikuar"; +"user_session_verified_short" = "I verifikuar"; +"user_session_verification_unknown" = "Gjendje e panjohur verifikimi"; +"user_session_unverified" = "Sesion i paverifikuar"; +"user_session_verified" = "Sesion i verifikuar"; +"user_sessions_view_all_action" = "Shihini krejt (%d)"; +"user_sessions_overview_link_device" = "Lidhni një pajisje"; +"user_sessions_overview_current_session_section_title" = "Sesioni i tanishëm"; +"user_sessions_overview_other_sessions_section_info" = "Për sigurinë më të mirë, verifikoni sesionet tuaja dhe dilni nga çfarëdo sesioni që s’e njihni, ose s’e përdorni më."; +"user_sessions_overview_other_sessions_section_title" = "Sesione të tjerë"; +"user_sessions_overview_security_recommendations_inactive_info" = "Shihni mundësinë e daljes nga sesione të vjetër (90 ditë ose më tepër) të cilët s’i përdorni më."; +"user_sessions_overview_security_recommendations_inactive_title" = "Sesione joaktivë"; +"user_sessions_overview_security_recommendations_unverified_info" = "Verifikojini, ose dilni nga sesione të paverifikuar."; +"user_sessions_overview_security_recommendations_unverified_title" = "Sesione të paverifikuar"; +"user_sessions_overview_security_recommendations_section_info" = "Përmirësoni sigurinë e llogarisë tuaj duke ndjekur këto rekomandime."; +"user_sessions_overview_security_recommendations_section_title" = "Rekomandime sigurie"; +"user_sessions_overview_title" = "Sesione"; + +// MARK: User sessions management + +// Parameter is the application display name (e.g. "Element") +"user_sessions_default_session_display_name" = "%@ iOS"; +"location_sharing_map_loading_error" = "S’arrihet të ngarkohet hartë\nKy shërbyes Home s’është formësuar të shfaqë harta"; +"space_invite_nav_title" = "Ftesë për në hapësirë"; +"space_detail_nav_title" = "Hollësi hapësire"; +"space_selector_create_space" = "Krijo Hapësirë"; +"space_selector_empty_view_information" = "Hapësirat janë një mënyrë e re për të grupuar dhoma dhe njerëz. Që t’ia filloni, krijoni një hapësirë."; +"space_selector_empty_view_title" = "Ende pa hapësira."; + +// Mark: - Space Selector + +"space_selector_title" = "Hapësirat e mia"; +"room_invites_empty_view_information" = "Ky është vendi ku shfaqen ftesat tuaja."; + +// Mark: - Room invites + +"room_invites_empty_view_title" = "S’ka gjë të re."; +"all_chats_onboarding_try_it" = "Provojeni"; +"all_chats_onboarding_title" = "Ç’ka të re"; +"all_chats_onboarding_page_title3" = "Jepni Përshtypje"; +"all_chats_onboarding_page_message2" = "Hyni në Hapësirat tuaja (poshtë djathtas) më shpejt dhe më kollaj se kurrë më parë."; +"all_chats_onboarding_page_title2" = "Hyni Në Hapësira"; +"all_chats_onboarding_page_message1" = "Që të thjeshtohet Element-i juaj, skedat tanimë janë opsionale. Administrojini duke përdorur menunë djathtas në krye."; +"all_chats_onboarding_page_title1" = "Mirë se vini te një pamje e re!"; +"all_chats_edit_menu_space_settings" = "Rregullime hapësire"; +"all_chats_edit_menu_leave_space" = "Braktise %@"; +"all_chats_user_menu_settings" = "Rregullime përdoruesi"; +"all_chats_user_menu_accessibility_label" = "Menu përdoruesi"; +"room_recents_recently_viewed_section" = "Parë së fundi"; +"all_chats_nothing_found_placeholder_message" = "Provoni të përimtoni kërkimin tuaj."; +"all_chats_nothing_found_placeholder_title" = "S’u gjet gjë."; +"all_chats_empty_unreads_placeholder_message" = "Ky është vendi ku do të shfaqen mesazhet tuaj të palexuar, kur të ketë të tillë."; +"all_chats_empty_view_information" = "Aplikacioni “all-in-one” i fjalosjeve të siguruara, për ekipe, shokë dhe ente. Që t’ia filloni, krijoni një fjalosje, ose hyni në një dhomë ekzistuese."; +"all_chats_empty_space_information" = "Hapësirat janë një mënyrë e re për të grupuar dhoma dhe persona. Shtoni një dhomë ekzistuese, ose krijoni një të re, duke përdorur butonin poshtë djathtas."; +"all_chats_empty_view_title" = "%s\nduket paksa si i zbrazët."; +"all_chats_all_filter" = "Krejt"; +"all_chats_edit_layout_alphabetical_order" = "Renditi si A-Z"; +"all_chats_edit_layout_activity_order" = "Renditi sipas veprimtarish"; +"all_chats_edit_layout_show_filters" = "Shfaq filtra"; +"all_chats_edit_layout_show_recents" = "Shfaq të freskëta"; +"all_chats_edit_layout_sorting_options_title" = "Renditi mesazhet sipas"; +"all_chats_edit_layout_pin_spaces_title" = "Fiksoni hapësirat tuaja"; +"all_chats_edit_layout_add_filters_title" = "Filtroni mesazhet tuaja"; +"all_chats_edit_layout_add_section_title" = "Shtoni ndarje te kreu"; +"all_chats_edit_layout_unreads" = "Të palexuara"; +"all_chats_edit_layout_recents" = "Së fundi"; +"all_chats_edit_layout" = "Parapëlqime skeme grafike"; +"all_chats_section_title" = "Fjalosje"; + +// Mark: - All Chats + +"all_chats_title" = "Krejt fjalosjet"; +"voice_broadcast_playback_loading_error" = "S’arrihet të luhet ky transmetim zanor."; +"voice_broadcast_already_in_progress_message" = "Po incizoni tashmë një transmetim zanor. Ju lutemi, që të nisni një të ri, përfundoni transmetimin tuaj zanor të tanishëm."; +"voice_broadcast_blocked_by_someone_else_message" = "Dikush tjetër është duke incizuar një transmetim zanor. Që të nisni një të ri, prisni të përfundojë incizimi zanor i tij."; +"voice_broadcast_permission_denied_message" = "S’keni lejet e domosdoshme që të nisni një transmetim zanor në këtë dhomë. Lidhuni me një përgjegjës dhome që të përmirësoni lejet tuaja."; + +// Mark: - Voice broadcast +"voice_broadcast_unauthorized_title" = "S’niset dot një transmetim zanor i ri"; +"spaces_subspace_creation_visibility_message" = "Hapësira e krijuar do të shtohet te %@."; +"spaces_subspace_creation_visibility_title" = "Ç’lloj nënhapësire doni të krijoni?"; +"spaces_explore_rooms_format" = "Eksploroni %@"; +"spaces_create_subspace_title" = "Krijoni një nënhapësirë"; +"spaces_add_subspace_title" = "Krijoni hapësirë brenda %@"; +"sign_out_confirmation_message" = "Jeni i sigurt se doni të dilni?"; + +// MARK: Sign out warning + +"sign_out" = "Dilni"; + +// User sessions management +"user_sessions_settings" = "Administroni sesione"; +"manage_session_rename" = "Riemërtoni sesionin"; +"manage_session_name_info_link" = "Mësoni më tepër"; +/* The placeholder will be replaces with manage_session_name_info_link */ +"manage_session_name_info" = "Ju lutemi, kini parasysh se emrat e sesioneve janë të dukshëm edhe për personat me të cilët komunikoni.%@"; +"manage_session_name_hint" = "Emra vetjakë sesionesh mund t’ju ndihmojnë të njihni më kollaj pajisjet tuaja."; +"settings_labs_enable_voice_broadcast" = "Aktivizoni transmetim zanor (nën zhvillim aktiv)"; +"settings_labs_enable_wysiwyg_composer" = "Provoni përpunuesin e teksteve të pasur (për tekst të thjeshtë vjen së shpejti)"; +"settings_labs_enable_new_app_layout" = "Skemë e Re Aplikacioni"; +"settings_labs_enable_new_client_info_feature" = "Regjistro emrin, versionin dhe URL-në e klientit, për të dalluar më kollaj sesionit te përgjegjës sesionesh"; +"settings_labs_enable_new_session_manager" = "Përgjegjës i ri sesionesh"; +"room_first_message_placeholder" = "Dërgoni mesazhin tuaj të parë…"; +"authentication_qr_login_failure_retry" = "Riprovoni"; +"authentication_qr_login_failure_request_timed_out" = "Lidhja s’u plotësua brenda kohës së domosdoshme."; +"authentication_qr_login_failure_request_denied" = "Kërkesa u hodh poshtë në pajisjen tjetër."; +"authentication_qr_login_failure_invalid_qr" = "Kodi QR është i pavlefshëm."; +"authentication_qr_login_failure_title" = "Lidhja dështoi"; +"authentication_qr_login_loading_signed_in" = "Tani jeni i futur te pajisja juaj tjetër."; +"authentication_qr_login_loading_waiting_signin" = "Po pritet që të bëhet hyrja te pajisja."; +"authentication_qr_login_loading_connecting_device" = "Po lidhet me pajisjen"; +"authentication_qr_login_confirm_alert" = "Ju lutemi, sigurohuni se e dini origjinën e këtij kodi. Duke lidhur pajisje, do t’i jepni dikujt hyrje të plotë në llogarinë tuaj."; +"authentication_qr_login_confirm_subtitle" = "Ripohoni se kodi më poshtë përkon me atë në pajisjen tuaj tjetër:"; +"authentication_qr_login_confirm_title" = "U vendos lidhje e siguruar"; +"authentication_qr_login_scan_subtitle" = "Vendoseni kodin QR te kuadrati më poshtë"; +"authentication_qr_login_scan_title" = "Skanoni kodin QR"; +"authentication_qr_login_display_step2" = "Përzgjidhni “Hyni me kod QR”"; +"authentication_qr_login_display_step1" = "Hapeni Element-in në pajisjen tuaj tjetër"; +"authentication_qr_login_display_subtitle" = "Skanoni kodin QR më poshtë me pajisjen ku është bërë dalja."; +"authentication_qr_login_display_title" = "Lidhni një pajisje"; +"authentication_qr_login_start_display_qr" = "Shfaq kod QR te kjo pajisje"; +"authentication_qr_login_start_need_alternative" = "Ju duhet një metodë alternative?"; +"authentication_qr_login_start_step4" = "Përzgjidhni “Shfaq kod QR në këtë pajisje”"; +"authentication_qr_login_start_step3" = "Përzgjidhni “Lidhni një pajisje”"; +"authentication_qr_login_start_step2" = "Kaloni te Rregullime -> Siguri & Privatësi"; +"authentication_qr_login_start_step1" = "Hapeni Element-in në pajisjen tuaj tjetër"; +"authentication_qr_login_start_subtitle" = "Përdorni kamerën në këtë pajisje që të skanoni kodin QR të shfaqur në pajisjen tuaj tjetër:"; +"authentication_qr_login_start_title" = "Skanoni kodin QR"; +"authentication_login_with_qr" = "Hyni me kod QR"; +"invite_to" = "Ftojeni te %@"; +"all_chats_empty_list_placeholder_title" = "S’ka gjë tjetër për të parë."; +"all_chats_edit_layout_add_filters_message" = "Filtroni automatikisht mesazhet tuaj në kategori që caktoni vetë"; diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index d1d1d7d7cc..a38d76c6fe 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -2829,16 +2829,35 @@ /* The placeholder will be replaces with manage_session_name_info_link */ "manage_session_name_info" = "Зауважте, що назви сеансів також видно людям, з якими ви спілкуєтесь. %@"; "manage_session_name_hint" = "Власні назви сеансів допоможуть вам легше розпізнавати ваші пристрої."; -"settings_labs_enable_wysiwyg_composer" = "Спробуйте розширений текстовий редактор (незабаром з'явиться режим звичайного тексту)"; -"wysiwyg_composer_start_action_voice_broadcast" = "Голосові повідомлення"; -"voice_broadcast_playback_loading_error" = "Неможливо відтворити це голосове повідомлення."; -"voice_broadcast_already_in_progress_message" = "Ви вже записуєте голосове повідомлення. Завершіть поточну трансляцію, щоб розпочати нову."; -"voice_broadcast_blocked_by_someone_else_message" = "Хтось інший вже записує голосове повідомлення. Зачекайте, поки закінчиться трансляція, щоб розпочати нову."; -"voice_broadcast_permission_denied_message" = "Ви не маєте необхідних дозволів для початку трансляції голосового повідомлення в цій кімнаті. Зверніться до адміністратора кімнати, щоб оновити ваші дозволи."; +"settings_labs_enable_wysiwyg_composer" = "Спробуйте розширений текстовий редактор"; +"wysiwyg_composer_start_action_voice_broadcast" = "Голосові трансляції"; +"voice_broadcast_playback_loading_error" = "Неможливо відтворити цю голосову трансляцію."; +"voice_broadcast_already_in_progress_message" = "Ви вже записуєте голосову трансляцію. Завершіть поточну трансляцію, щоб розпочати нову."; +"voice_broadcast_blocked_by_someone_else_message" = "Хтось інший вже записує голосову трансляцію. Зачекайте, поки вона завершиться, щоб розпочати нову."; +"voice_broadcast_permission_denied_message" = "Ви не маєте необхідних дозволів для початку голосової трансляції в цій кімнаті. Зверніться до адміністратора кімнати, щоб оновити ваші дозволи."; // Mark: - Voice broadcast -"voice_broadcast_unauthorized_title" = "Не вдалося розпочати трансляцію нового голосового повідомлення"; -"settings_labs_enable_voice_broadcast" = "Голосові повідомлення (в активній розробці)"; +"voice_broadcast_unauthorized_title" = "Не вдалося розпочати нову голосову трансляцію"; +"settings_labs_enable_voice_broadcast" = "Голосові трансляції (в активній розробці)"; "deselect_all" = "Скасувати вибір усіх"; "user_other_session_menu_select_sessions" = "Вибрати сеанси"; "user_other_session_selected_count" = "Вибрано %@"; +"user_other_session_menu_sign_out_sessions" = "Вийти з %@ сеансів"; +"manage_session_sign_out_other_sessions" = "Вийти з усіх інших сеансів"; +"user_sessions_hide_location_info" = "Сховати IP-адресу"; +"user_sessions_show_location_info" = "Показати IP-адресу"; +"voice_broadcast_tile" = "Голосові трансляції"; +"voice_broadcast_live" = "Наживо"; +"user_session_rename_session_description" = "Інші користувачі в особистих повідомленнях і кімнатах, до яких ви приєдналися, можуть переглядати повний список ваших сеансів.\n\nЦе дає їм впевненість у тому, що вони дійсно розмовляють з вами, а також означає, що вони можуть бачити назву сеансу, яку ви вводите сюди."; +"user_session_rename_session_title" = "Перейменовані сеанси"; +"user_session_inactive_session_description" = "Неактивні сеанси — це сеанси, які ви не використовували протягом певного часу, але вони продовжують отримувати ключі шифрування.\n\nВилучення неактивних сеансів покращує безпеку та швидкодію, а також полегшує визначення підозрілих нових сеансів."; +"user_session_inactive_session_title" = "Неактивні сеанси"; +"user_session_unverified_session_description" = "Не звірені сеанси — це сеанси, до яких ви ввійшли за допомогою ваших облікових даних, але не пройшли перехресну перевірку.\n\nВи повинні бути особливо впевнені, що розпізнаєте ці сеанси, оскільки вони можуть означати несанкціоноване використовування вашого облікового запису."; +"user_session_unverified_session_title" = "Не звірений сеанс"; +"user_session_verified_session_description" = "Звірені сеанси — це будь-який пристрій, на якому ви використовуєте Element після введення парольної фрази або підтвердження вашої особи за допомогою іншого звіреного сеансу.\n\nЦе означає, що ви маєте всі ключі, необхідні для розблокування ваших зашифрованих повідомлень і підтвердження іншим користувачам, що ви довіряєте цьому сеансу."; +"user_session_verified_session_title" = "Звірені сеанси"; +"user_session_got_it" = "Зрозуміло"; +"key_verification_alert_body" = "Перегляньте їх, щоб переконатися, що ваш обліковий запис у безпеці."; + +// Unverified sessions +"key_verification_alert_title" = "У вас є не звірені сеанси"; diff --git a/Riot/Categories/Date+Calculation.swift b/Riot/Categories/Date+Calculation.swift new file mode 100644 index 0000000000..fc68c99d57 --- /dev/null +++ b/Riot/Categories/Date+Calculation.swift @@ -0,0 +1,25 @@ +// +// Copyright 2022 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 Date { + + func daysBetween(date: Date) -> Int { + let components = Calendar.current.dateComponents([.day], from: self, to: date) + return components.day ?? 0 + } +} diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m index fe01f6887d..a7bfd69f14 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m @@ -623,6 +623,10 @@ - (CGRect)componentFrameInContentViewForIndex:(NSInteger)componentIndex selectedComponentHeight = roomBubbleTableViewCell.frame.size.height - selectedComponentPositionY; } + // Force the textView used underneath to layout its frame properly + [roomBubbleTableViewCell setNeedsLayout]; + [roomBubbleTableViewCell layoutIfNeeded]; + selectedComponenContentViewYOffset = roomBubbleTableViewCell.messageTextView.frame.origin.y; } diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 9221744276..94352b0be0 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -197,6 +197,8 @@ internal class Asset: NSObject { internal static let peopleFloatingAction = ImageAsset(name: "people_floating_action") internal static let actionCamera = ImageAsset(name: "action_camera") internal static let actionFile = ImageAsset(name: "action_file") + internal static let actionFormattingDisabled = ImageAsset(name: "action_formatting_disabled") + internal static let actionFormattingEnabled = ImageAsset(name: "action_formatting_enabled") internal static let actionLive = ImageAsset(name: "action_live") internal static let actionLocation = ImageAsset(name: "action_location") internal static let actionMediaLibrary = ImageAsset(name: "action_media_library") @@ -343,6 +345,8 @@ internal class Asset: NSObject { internal static let voiceBroadcastRecord = ImageAsset(name: "voice_broadcast_record") internal static let voiceBroadcastRecordPause = ImageAsset(name: "voice_broadcast_record_pause") internal static let voiceBroadcastStop = ImageAsset(name: "voice_broadcast_stop") + internal static let voiceBroadcastTileLive = ImageAsset(name: "voice_broadcast_tile_live") + internal static let voiceBroadcastTileMic = ImageAsset(name: "voice_broadcast_tile_mic") internal static let launchScreenLogo = ImageAsset(name: "launch_screen_logo") } @objcMembers diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 6c82fd2666..36091cc12f 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2923,6 +2923,14 @@ public class VectorL10n: NSObject { public static var keyBackupSetupTitle: String { return VectorL10n.tr("Vector", "key_backup_setup_title") } + /// Review to ensure your account is safe. + public static var keyVerificationAlertBody: String { + return VectorL10n.tr("Vector", "key_verification_alert_body") + } + /// You have unverified sessions + public static var keyVerificationAlertTitle: String { + return VectorL10n.tr("Vector", "key_verification_alert_title") + } /// You need to bootstrap cross-signing first. public static var keyVerificationBootstrapNotSetupMessage: String { return VectorL10n.tr("Vector", "key_verification_bootstrap_not_setup_message") @@ -3007,14 +3015,6 @@ public class VectorL10n: NSObject { public static var keyVerificationSelfVerifyCurrentSessionAlertValidateAction: String { return VectorL10n.tr("Vector", "key_verification_self_verify_current_session_alert_validate_action") } - /// Verify all your sessions to ensure your account & messages are safe. - public static var keyVerificationSelfVerifyUnverifiedSessionsAlertMessage: String { - return VectorL10n.tr("Vector", "key_verification_self_verify_unverified_sessions_alert_message") - } - /// Review where you're logged in - public static var keyVerificationSelfVerifyUnverifiedSessionsAlertTitle: String { - return VectorL10n.tr("Vector", "key_verification_self_verify_unverified_sessions_alert_title") - } /// Review public static var keyVerificationSelfVerifyUnverifiedSessionsAlertValidateAction: String { return VectorL10n.tr("Vector", "key_verification_self_verify_unverified_sessions_alert_validate_action") @@ -3647,6 +3647,10 @@ public class VectorL10n: NSObject { public static var manageSessionSignOut: String { return VectorL10n.tr("Vector", "manage_session_sign_out") } + /// Sign out of all other sessions + public static var manageSessionSignOutOtherSessions: String { + return VectorL10n.tr("Vector", "manage_session_sign_out_other_sessions") + } /// Manage session public static var manageSessionTitle: String { return VectorL10n.tr("Vector", "manage_session_title") @@ -7543,7 +7547,7 @@ public class VectorL10n: NSObject { public static var settingsLabsEnableVoiceBroadcast: String { return VectorL10n.tr("Vector", "settings_labs_enable_voice_broadcast") } - /// Try out the rich text editor (plain text mode coming soon) + /// Try out the rich text editor public static var settingsLabsEnableWysiwygComposer: String { return VectorL10n.tr("Vector", "settings_labs_enable_wysiwyg_composer") } @@ -8679,6 +8683,10 @@ public class VectorL10n: NSObject { public static var userOtherSessionMenuSelectSessions: String { return VectorL10n.tr("Vector", "user_other_session_menu_select_sessions") } + /// Sign out of %@ sessions + public static func userOtherSessionMenuSignOutSessions(_ p1: String) -> String { + return VectorL10n.tr("Vector", "user_other_session_menu_sign_out_sessions", p1) + } /// No inactive sessions found. public static var userOtherSessionNoInactiveSessions: String { return VectorL10n.tr("Vector", "user_other_session_no_inactive_sessions") @@ -8779,6 +8787,18 @@ public class VectorL10n: NSObject { public static var userSessionDetailsTitle: String { return VectorL10n.tr("Vector", "user_session_details_title") } + /// Got it + public static var userSessionGotIt: String { + return VectorL10n.tr("Vector", "user_session_got_it") + } + /// Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.\n\nRemoving inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious. + public static var userSessionInactiveSessionDescription: String { + return VectorL10n.tr("Vector", "user_session_inactive_session_description") + } + /// Inactive sessions + public static var userSessionInactiveSessionTitle: String { + return VectorL10n.tr("Vector", "user_session_inactive_session_title") + } /// %1$@ · %2$@ public static func userSessionItemDetails(_ p1: String, _ p2: String) -> String { return VectorL10n.tr("Vector", "user_session_item_details", p1, p2) @@ -8815,6 +8835,14 @@ public class VectorL10n: NSObject { public static var userSessionPushNotificationsMessage: String { return VectorL10n.tr("Vector", "user_session_push_notifications_message") } + /// Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here. + public static var userSessionRenameSessionDescription: String { + return VectorL10n.tr("Vector", "user_session_rename_session_description") + } + /// Renaming sessions + public static var userSessionRenameSessionTitle: String { + return VectorL10n.tr("Vector", "user_session_rename_session_title") + } /// Unverified session public static var userSessionUnverified: String { return VectorL10n.tr("Vector", "user_session_unverified") @@ -8823,6 +8851,14 @@ public class VectorL10n: NSObject { public static var userSessionUnverifiedAdditionalInfo: String { return VectorL10n.tr("Vector", "user_session_unverified_additional_info") } + /// Unverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account. + public static var userSessionUnverifiedSessionDescription: String { + return VectorL10n.tr("Vector", "user_session_unverified_session_description") + } + /// Unverified session + public static var userSessionUnverifiedSessionTitle: String { + return VectorL10n.tr("Vector", "user_session_unverified_session_title") + } /// Unverified public static var userSessionUnverifiedShort: String { return VectorL10n.tr("Vector", "user_session_unverified_short") @@ -8847,6 +8883,14 @@ public class VectorL10n: NSObject { public static var userSessionVerifiedAdditionalInfo: String { return VectorL10n.tr("Vector", "user_session_verified_additional_info") } + /// Verified sessions are anywhere you are using Element after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session. + public static var userSessionVerifiedSessionDescription: String { + return VectorL10n.tr("Vector", "user_session_verified_session_description") + } + /// Verified sessions + public static var userSessionVerifiedSessionTitle: String { + return VectorL10n.tr("Vector", "user_session_verified_session_title") + } /// Verified public static var userSessionVerifiedShort: String { return VectorL10n.tr("Vector", "user_session_verified_short") @@ -8863,6 +8907,10 @@ public class VectorL10n: NSObject { public static func userSessionsDefaultSessionDisplayName(_ p1: String) -> String { return VectorL10n.tr("Vector", "user_sessions_default_session_display_name", p1) } + /// Hide IP address + public static var userSessionsHideLocationInfo: String { + return VectorL10n.tr("Vector", "user_sessions_hide_location_info") + } /// Current session public static var userSessionsOverviewCurrentSessionSectionTitle: String { return VectorL10n.tr("Vector", "user_sessions_overview_current_session_section_title") @@ -8911,6 +8959,10 @@ public class VectorL10n: NSObject { public static var userSessionsSettings: String { return VectorL10n.tr("Vector", "user_sessions_settings") } + /// Show IP address + public static var userSessionsShowLocationInfo: String { + return VectorL10n.tr("Vector", "user_sessions_show_location_info") + } /// View all (%d) public static func userSessionsViewAllAction(_ p1: Int) -> String { return VectorL10n.tr("Vector", "user_sessions_view_all_action", p1) @@ -9071,6 +9123,10 @@ public class VectorL10n: NSObject { public static var voiceBroadcastBlockedBySomeoneElseMessage: String { return VectorL10n.tr("Vector", "voice_broadcast_blocked_by_someone_else_message") } + /// Live + public static var voiceBroadcastLive: String { + return VectorL10n.tr("Vector", "voice_broadcast_live") + } /// You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions. public static var voiceBroadcastPermissionDeniedMessage: String { return VectorL10n.tr("Vector", "voice_broadcast_permission_denied_message") @@ -9079,6 +9135,10 @@ public class VectorL10n: NSObject { public static var voiceBroadcastPlaybackLoadingError: String { return VectorL10n.tr("Vector", "voice_broadcast_playback_loading_error") } + /// Voice broadcast + public static var voiceBroadcastTile: String { + return VectorL10n.tr("Vector", "voice_broadcast_tile") + } /// Can't start a new voice broadcast public static var voiceBroadcastUnauthorizedTitle: String { return VectorL10n.tr("Vector", "voice_broadcast_unauthorized_title") diff --git a/Riot/Generated/UntranslatedStrings.swift b/Riot/Generated/UntranslatedStrings.swift index 1f417c7701..f273877eba 100644 --- a/Riot/Generated/UntranslatedStrings.swift +++ b/Riot/Generated/UntranslatedStrings.swift @@ -14,10 +14,6 @@ public extension VectorL10n { static var imagePickerActionFiles: String { return VectorL10n.tr("Untranslated", "image_picker_action_files") } - /// Voice broadcast detected (under active development) - static var voiceBroadcastInTimelineTitle: String { - return VectorL10n.tr("Untranslated", "voice_broadcast_in_timeline_title") - } } // swiftlint:enable function_parameter_count identifier_name line_length type_body_length diff --git a/Riot/Managers/Settings/RiotSettings+Publisher.swift b/Riot/Managers/Settings/RiotSettings+Publisher.swift index f1e6f17cb6..cd37da82eb 100644 --- a/Riot/Managers/Settings/RiotSettings+Publisher.swift +++ b/Riot/Managers/Settings/RiotSettings+Publisher.swift @@ -18,12 +18,9 @@ import Foundation import Combine extension RiotSettings { - - @available(iOS 13.0, *) func publisher(for key: String) -> AnyPublisher { - return NotificationCenter.default.publisher(for: .userDefaultValueUpdated) + NotificationCenter.default.publisher(for: .userDefaultValueUpdated) .filter({ $0.object as? String == key }) .eraseToAnyPublisher() } - } diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 2e885b1cf8..8e96eb1a76 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -32,6 +32,7 @@ final class RiotSettings: NSObject { static let showAllRoomsInHomeSpace = "showAllRoomsInHomeSpace" static let enableUISIAutoReporting = "enableUISIAutoReporting" static let enableLiveLocationSharing = "enableLiveLocationSharing" + static let showIPAddressesInSessionsManager = "showIPAddressesInSessionsManager" } static let shared = RiotSettings() @@ -175,6 +176,13 @@ final class RiotSettings: NSObject { /// Flag indicating if the wysiwyg composer feature is enabled @UserDefault(key: "enableWysiwygComposer", defaultValue: false, storage: defaults) var enableWysiwygComposer + + @UserDefault(key: "enableWysiwygTextFormatting", defaultValue: true, storage: defaults) + var enableWysiwygTextFormatting + + /// Flag indicating if the IP addresses should be shown in the new device manager + @UserDefault(key: UserDefaultsKeys.showIPAddressesInSessionsManager, defaultValue: false, storage: defaults) + var showIPAddressesInSessionsManager /// Flag indicating if the voice broadcast feature is enabled @UserDefault(key: "enableVoiceBroadcast", defaultValue: false, storage: defaults) @@ -195,9 +203,6 @@ final class RiotSettings: NSObject { @UserDefault(key: "hideVerifyThisSessionAlert", defaultValue: false, storage: defaults) var hideVerifyThisSessionAlert - @UserDefault(key: "hideReviewSessionsAlert", defaultValue: false, storage: defaults) - var hideReviewSessionsAlert - @UserDefault(key: "matrixApps", defaultValue: false, storage: defaults) var matrixApps diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 63d2afe504..3558a4a4d5 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -638,6 +638,8 @@ - (void)applicationDidBecomeActive:(UIApplication *)application [self.pushNotificationService applicationDidBecomeActive]; [self configurePinCodeScreenFor:application createIfRequired:NO]; + + [self checkCrossSigningForSession:self.mxSessions.firstObject]; } - (void)configurePinCodeScreenFor:(UIApplication *)application @@ -2193,6 +2195,10 @@ - (void)logoutSendingRequestServer:(BOOL)sendLogoutServerRequest // Reset analytics [Analytics.shared reset]; + [[[ReviewSessionAlertSnoozeController alloc] init] clearSnooze]; + + [TimelinePollProvider.shared reset]; + #ifdef MX_CALL_STACK_ENDPOINT // Erase all created certificates and private keys by MXEndpointCallStack for (MXKAccount *account in MXKAccountManager.sharedManager.accounts) diff --git a/Riot/Modules/CrossSigning/CrossSigningService.swift b/Riot/Modules/CrossSigning/CrossSigningService.swift index c4773581b3..a4df60aaae 100644 --- a/Riot/Modules/CrossSigning/CrossSigningService.swift +++ b/Riot/Modules/CrossSigning/CrossSigningService.swift @@ -78,7 +78,7 @@ final class CrossSigningService: NSObject { func setupCrossSigningRequest() -> AuthenticatedEndpointRequest { let path = "\(kMXAPIPrefixPathUnstable)/keys/device_signing/upload" - return AuthenticatedEndpointRequest(path: path, httpMethod: "POST") + return AuthenticatedEndpointRequest(path: path, httpMethod: "POST", params: [:]) } /// Setup cross-signing without authentication. Useful when a grace period is enabled. diff --git a/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m b/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m index f2f2cd4064..1488377f5f 100644 --- a/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m +++ b/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m @@ -152,7 +152,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath]; // Finalize cell view customization here - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *bubbleCell = (MXKRoomBubbleTableViewCell*)cell; diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index de7504084a..fed2030d9c 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -60,7 +60,7 @@ class AllChatsViewController: HomeViewController { private let tableViewPaginationThrottler = MXThrottler(minimumDelay: 0.1) - private var reviewSessionAlertHasBeenDisplayed: Bool = false + private let reviewSessionAlertSnoozeController = ReviewSessionAlertSnoozeController() private var bannerView: UIView? { didSet { @@ -72,9 +72,7 @@ class AllChatsViewController: HomeViewController { private var isOnboardingCoordinatorPreparing: Bool = false private var allChatsOnboardingCoordinatorBridgePresenter: AllChatsOnboardingCoordinatorBridgePresenter? - - private var currentAlert: UIAlertController? - + @IBOutlet private var toolbar: UIToolbar! private var isToolbarHidden: Bool = false { didSet { @@ -821,12 +819,13 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { /// - Parameters: /// - session: the matrix session. func presentVerifyCurrentSessionAlertIfNeeded(with session: MXSession) { - guard !RiotSettings.shared.hideVerifyThisSessionAlert, !reviewSessionAlertHasBeenDisplayed, !isOnboardingInProgress else { + guard !RiotSettings.shared.hideVerifyThisSessionAlert, + !isOnboardingInProgress, + presentedViewController == nil, + viewIfLoaded?.window != nil else { return } - reviewSessionAlertHasBeenDisplayed = true - // Force verification if required by the HS configuration guard !session.vc_homeserverConfiguration().encryption.isSecureBackupRequired else { MXLog.debug("[AllChatsViewController] presentVerifyCurrentSessionAlertIfNeededWithSession: Force verification of the device") @@ -842,21 +841,16 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { /// - Parameters: /// - session: the matrix session. func presentReviewUnverifiedSessionsAlertIfNeeded(with session: MXSession) { - guard !RiotSettings.shared.hideReviewSessionsAlert, !reviewSessionAlertHasBeenDisplayed else { + guard BuildSettings.showUnverifiedSessionsAlert, + !reviewSessionAlertSnoozeController.isSnoozed(), + presentedViewController == nil, + viewIfLoaded?.window != nil else { return } - + let devices = mainSession.crypto.devices(forUser: mainSession.myUserId).values - var userHasOneUnverifiedDevice = false - for device in devices { - if !device.trustLevel.isCrossSigningVerified { - userHasOneUnverifiedDevice = true - break - } - } - + let userHasOneUnverifiedDevice = devices.contains(where: {!$0.trustLevel.isCrossSigningVerified}) if userHasOneUnverifiedDevice { - reviewSessionAlertHasBeenDisplayed = true presentReviewUnverifiedSessionsAlert(with: session) } } @@ -968,8 +962,6 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { private func presentVerifyCurrentSessionAlert(with session: MXSession) { MXLog.debug("[AllChatsViewController] presentVerifyCurrentSessionAlertWithSession") - currentAlert?.dismiss(animated: true, completion: nil) - let alert = UIAlertController(title: VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertTitle, message: VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertMessage, preferredStyle: .alert) @@ -989,16 +981,13 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { })) self.present(alert, animated: true) - currentAlert = alert } private func presentReviewUnverifiedSessionsAlert(with session: MXSession) { MXLog.debug("[AllChatsViewController] presentReviewUnverifiedSessionsAlert") - currentAlert?.dismiss(animated: true, completion: nil) - - let alert = UIAlertController(title: VectorL10n.keyVerificationSelfVerifyUnverifiedSessionsAlertTitle, - message: VectorL10n.keyVerificationSelfVerifyUnverifiedSessionsAlertMessage, + let alert = UIAlertController(title: VectorL10n.keyVerificationAlertTitle, + message: VectorL10n.keyVerificationAlertBody, preferredStyle: .alert) alert.addAction(UIAlertAction(title: VectorL10n.keyVerificationSelfVerifyUnverifiedSessionsAlertValidateAction, @@ -1007,14 +996,11 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { self.showSettingsSecurityScreen(with: session) })) - alert.addAction(UIAlertAction(title: VectorL10n.later, style: .cancel)) - - alert.addAction(UIAlertAction(title: VectorL10n.doNotAskAgain, style: .destructive, handler: { action in - RiotSettings.shared.hideReviewSessionsAlert = true + alert.addAction(UIAlertAction(title: VectorL10n.later, style: .cancel, handler: { [weak self] _ in + self?.reviewSessionAlertSnoozeController.snooze() })) present(alert, animated: true) - currentAlert = alert } private func showSettingsSecurityScreen(with session: MXSession) { @@ -1030,7 +1016,12 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { settingsViewController.loadViewIfNeeded() AppDelegate.theDelegate().restoreInitialDisplay { - self.navigationController?.viewControllers = [self, settingsViewController, securityViewController] + if RiotSettings.shared.enableNewSessionManager { + self.navigationController?.viewControllers = [self, settingsViewController] + settingsViewController.showUserSessionsFlow() + } else { + self.navigationController?.viewControllers = [self, settingsViewController, securityViewController] + } } } @@ -1053,9 +1044,7 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { } private func resetReviewSessionsFlags() { - reviewSessionAlertHasBeenDisplayed = false RiotSettings.shared.hideVerifyThisSessionAlert = false - RiotSettings.shared.hideReviewSessionsAlert = false } private func presentOnboardingFlow() { diff --git a/Riot/Modules/Home/AllChats/ReviewSessionAlertSnoozeController.swift b/Riot/Modules/Home/AllChats/ReviewSessionAlertSnoozeController.swift new file mode 100644 index 0000000000..e87dedd34d --- /dev/null +++ b/Riot/Modules/Home/AllChats/ReviewSessionAlertSnoozeController.swift @@ -0,0 +1,49 @@ +// +// Copyright 2022 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 +class ReviewSessionAlertSnoozeController: NSObject { + + private let userDefaults: UserDefaults + private let snoozeDateKey = "ReviewSessionAlertSnoozeController_snoozeDateKey" + private let minDaysBetweenAlerts = 7 + + // for Objective-C + convenience override init() { + self.init(userDefaults: UserDefaults.standard) + } + + init(userDefaults: UserDefaults = UserDefaults.standard) { + self.userDefaults = userDefaults + } + + func isSnoozed() -> Bool { + guard let lastDisplayedDate = userDefaults.object(forKey: snoozeDateKey) as? Date else { + return false + } + return lastDisplayedDate.daysBetween(date: Date()) <= minDaysBetweenAlerts + } + + func snooze() { + userDefaults.set(Date(), forKey: snoozeDateKey) + } + + func clearSnooze() { + userDefaults.removeObject(forKey: snoozeDateKey) + } +} diff --git a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.h b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.h index fd28f9939a..679e805f8f 100644 --- a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.h +++ b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.h @@ -359,11 +359,6 @@ typedef BOOL (^MXKAccountOnCertificateChange)(MXKAccount *mxAccount, NSData *cer - (void)resetDeviceId; #pragma mark - Sync filter -/** - Check if the homeserver supports room members lazy loading. - @param completion the check result. - */ -- (void)supportLazyLoadOfRoomMembers:(void (^)(BOOL supportLazyLoadOfRoomMembers))completion; /** Call this method at an appropriate time to attempt dehydrating to a new backup device diff --git a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m index 97cc2fb5c3..8b60e2fbe1 100644 --- a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m +++ b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m @@ -55,9 +55,6 @@ @interface MXKAccount () // We will notify user only once on session failure BOOL notifyOpenSessionFailure; - // The timer used to postpone server sync on failure - NSTimer* initialServerSyncTimer; - // Reachability observer id reachabilityObserver; @@ -934,9 +931,6 @@ - (void)closeSession:(BOOL)clearStore sessionStateObserver = nil; } - [initialServerSyncTimer invalidate]; - initialServerSyncTimer = nil; - if (userUpdateListener) { [mxSession.myUser removeListener:userUpdateListener]; @@ -1136,8 +1130,6 @@ - (void)pauseInBackgroundTask // Cancel pending actions [[NSNotificationCenter defaultCenter] removeObserver:reachabilityObserver]; reachabilityObserver = nil; - [initialServerSyncTimer invalidate]; - initialServerSyncTimer = nil; MXLogDebug(@"[MXKAccount] Pause is delayed due to the session state: %@", [MXTools readableSessionState: mxSession.state]); } @@ -1627,8 +1619,6 @@ - (void)launchInitialServerSync // Cancel potential reachability observer and pending action [[NSNotificationCenter defaultCenter] removeObserver:reachabilityObserver]; reachabilityObserver = nil; - [initialServerSyncTimer invalidate]; - initialServerSyncTimer = nil; // Sanity check if (!mxSession || (mxSession.state != MXSessionStateStoreDataReady && mxSession.state != MXSessionStateInitialSyncFailed)) @@ -1694,9 +1684,8 @@ - (void)launchInitialServerSync if (networkReachabilityManager.isReachable) { - // The problem is not the network - // Postpone a new attempt in 10 sec - self->initialServerSyncTimer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(launchInitialServerSync) userInfo:self repeats:NO]; + // If we have network, we retry immediately, otherwise the server may clear any cache it has computed thus far + [self launchInitialServerSync]; } else { @@ -2088,14 +2077,15 @@ - (void)backgroundSync:(unsigned int)timeout success:(void (^)(void))success fai #pragma mark - Sync filter -- (void)supportLazyLoadOfRoomMembers:(void (^)(BOOL supportLazyLoadOfRoomMembers))completion +- (void)supportLazyLoadOfRoomMembersWithMatrixVersion:(MXMatrixVersions *)matrixVersions + completion:(void (^)(BOOL supportLazyLoadOfRoomMembers))completion { void(^onUnsupportedLazyLoadOfRoomMembers)(NSError *) = ^(NSError *error) { completion(NO); }; // Check if the server supports LL sync filter - MXFilterJSONModel *filter = [self syncFilterWithLazyLoadOfRoomMembers:YES]; + MXFilterJSONModel *filter = [self syncFilterWithLazyLoadOfRoomMembers:YES supportsNotificationsForThreads:NO]; [mxSession.store filterIdForFilter:filter success:^(NSString * _Nullable filterId) { if (filterId) @@ -2106,8 +2096,8 @@ - (void)supportLazyLoadOfRoomMembers:(void (^)(BOOL supportLazyLoadOfRoomMembers else { // Check the Matrix versions supported by the HS - [self.mxSession supportedMatrixVersions:^(MXMatrixVersions *matrixVersions) { - + if (matrixVersions) + { if (matrixVersions.supportLazyLoadMembers) { // The HS supports LL @@ -2117,8 +2107,11 @@ - (void)supportLazyLoadOfRoomMembers:(void (^)(BOOL supportLazyLoadOfRoomMembers { onUnsupportedLazyLoadOfRoomMembers(nil); } - - } failure:onUnsupportedLazyLoadOfRoomMembers]; + } + else + { + completion(NO); + } } } failure:onUnsupportedLazyLoadOfRoomMembers]; } @@ -2133,28 +2126,42 @@ - (void)buildSyncFilter:(void (^)(MXFilterJSONModel *syncFilter))completion // Check settings BOOL syncWithLazyLoadOfRoomMembersSetting = [MXKAppSettings standardAppSettings].syncWithLazyLoadOfRoomMembers; - if (syncWithLazyLoadOfRoomMembersSetting) - { - // Check if the server supports LL sync filter before enabling it - [self supportLazyLoadOfRoomMembers:^(BOOL supportLazyLoadOfRoomMembers) { + void(^buildSyncFilter)(MXMatrixVersions *) = ^(MXMatrixVersions *matrixVersions) { + BOOL supportsNotificationsForThreads = matrixVersions ? matrixVersions.supportsNotificationsForThreads : NO; + + if (syncWithLazyLoadOfRoomMembersSetting) + { + // Check if the server supports LL sync filter before enabling it + [self supportLazyLoadOfRoomMembersWithMatrixVersion:matrixVersions completion:^(BOOL supportLazyLoadOfRoomMembers) { + - if (supportLazyLoadOfRoomMembers) - { - completion([self syncFilterWithLazyLoadOfRoomMembers:YES]); - } - else - { - // No support from the HS - // Disable the setting. That will avoid to make a request at every startup - [MXKAppSettings standardAppSettings].syncWithLazyLoadOfRoomMembers = NO; - completion([self syncFilterWithLazyLoadOfRoomMembers:NO]); - } - }]; - } - else - { - completion([self syncFilterWithLazyLoadOfRoomMembers:NO]); - } + if (supportLazyLoadOfRoomMembers) + { + completion([self syncFilterWithLazyLoadOfRoomMembers:YES + supportsNotificationsForThreads:supportsNotificationsForThreads]); + } + else + { + // No support from the HS + // Disable the setting. That will avoid to make a request at every startup + [MXKAppSettings standardAppSettings].syncWithLazyLoadOfRoomMembers = NO; + completion([self syncFilterWithLazyLoadOfRoomMembers:NO + supportsNotificationsForThreads:supportsNotificationsForThreads]); + } + }]; + } + else + { + completion([self syncFilterWithLazyLoadOfRoomMembers:NO supportsNotificationsForThreads:supportsNotificationsForThreads]); + } + }; + + [mxSession supportedMatrixVersions:^(MXMatrixVersions *matrixVersions) { + buildSyncFilter(matrixVersions); + } failure:^(NSError *error) { + MXLogWarning(@"[MXAccount] buildSyncFilter: failed to get supported versions: %@", error); + buildSyncFilter(nil); + }]; } /** @@ -2163,7 +2170,7 @@ - (void)buildSyncFilter:(void (^)(MXFilterJSONModel *syncFilter))completion @param syncWithLazyLoadOfRoomMembers enable LL support. @return the sync filter to use. */ -- (MXFilterJSONModel *)syncFilterWithLazyLoadOfRoomMembers:(BOOL)syncWithLazyLoadOfRoomMembers +- (MXFilterJSONModel *)syncFilterWithLazyLoadOfRoomMembers:(BOOL)syncWithLazyLoadOfRoomMembers supportsNotificationsForThreads:(BOOL)supportsNotificationsForThreads { MXFilterJSONModel *syncFilter; NSUInteger limit = 10; @@ -2198,11 +2205,11 @@ - (MXFilterJSONModel *)syncFilterWithLazyLoadOfRoomMembers:(BOOL)syncWithLazyLoa // Set that limit in the filter if (syncWithLazyLoadOfRoomMembers) { - syncFilter = [MXFilterJSONModel syncFilterForLazyLoadingWithMessageLimit:limit]; + syncFilter = [MXFilterJSONModel syncFilterForLazyLoadingWithMessageLimit:limit unreadThreadNotifications:supportsNotificationsForThreads]; } else { - syncFilter = [MXFilterJSONModel syncFilterWithMessageLimit:limit]; + syncFilter = [MXFilterJSONModel syncFilterWithMessageLimit:limit unreadThreadNotifications:supportsNotificationsForThreads]; } // TODO: We could extend the filter to match other settings (self.showAllEventsInRoomHistory, diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index d7bc678227..d315f7afc6 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -2358,15 +2358,61 @@ - (void)didReceiveReceiptEvent:(MXEvent *)receiptEvent roomState:(MXRoomState *) // Update cell data we have received a read receipt for NSArray *readEventIds = receiptEvent.readReceiptEventIds; - for (NSString* eventId in readEventIds) + if (RiotSettings.shared.enableThreads) { - MXKRoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:eventId]; - if (cellData) + NSArray *readThreadIds = receiptEvent.readReceiptThreadIds; + for (int i = 0 ; i < readEventIds.count ; i++) { + NSString *eventId = readEventIds[i]; + MXKRoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:eventId]; + if (cellData) + { + if ([readThreadIds[i] isEqualToString:kMXEventUnthreaded]) + { + // Unthreaded RR must be propagated through all threads. + [self.mxSession.threadingService allThreadsInRoomWithId:self.roomId onlyParticipated:NO completion:^(NSArray> *threads) { + NSMutableArray *threadIds = [NSMutableArray arrayWithObject:kMXEventTimelineMain]; + for (id thread in threads) + { + [threadIds addObject:thread.id]; + } + + for (NSString *threadId in threadIds) + { + @synchronized(self->bubbles) + { + dispatch_group_enter(dispatchGroup); + [self addReadReceiptsForEvent:eventId threadId:threadId inCellDatas:self->bubbles startingAtCellData:cellData completion:^{ + dispatch_group_leave(dispatchGroup); + }]; + } + } + }]; + } + else + { + NSString *threadId = readThreadIds[i]; + @synchronized(self->bubbles) + { + dispatch_group_enter(dispatchGroup); + [self addReadReceiptsForEvent:eventId threadId:threadId inCellDatas:self->bubbles startingAtCellData:cellData completion:^{ + dispatch_group_leave(dispatchGroup); + }]; + } + } + } + } + } + else + { + // If + for (NSString *eventId in readEventIds) + { + MXKRoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:eventId]; @synchronized(self->bubbles) { dispatch_group_enter(dispatchGroup); - [self addReadReceiptsForEvent:eventId inCellDatas:self->bubbles startingAtCellData:cellData completion:^{ + [self addReadReceiptsForEvent:eventId threadId:kMXEventTimelineMain inCellDatas:self->bubbles startingAtCellData:cellData completion:^{ dispatch_group_leave(dispatchGroup); }]; } @@ -3512,7 +3558,10 @@ - (void)processQueuedEvents:(void (^)(NSUInteger addedHistoryCellNb, NSUInteger @autoreleasepool { dispatch_group_enter(dispatchGroup); - [self addReadReceiptsForEvent:queuedEvent.event.eventId inCellDatas:self->bubblesSnapshot startingAtCellData:self->eventIdToBubbleMap[queuedEvent.event.eventId] completion:^{ + [self addReadReceiptsForEvent:queuedEvent.event.eventId + threadId:queuedEvent.event.threadId + inCellDatas:self->bubblesSnapshot + startingAtCellData:self->eventIdToBubbleMap[queuedEvent.event.eventId] completion:^{ dispatch_group_leave(dispatchGroup); }]; } @@ -3667,16 +3716,22 @@ Add the read receipts of an event into the timeline (which is in array of cell d If the event is not displayed, read receipts will be added to a previous displayed message. @param eventId the id of the event. + @param threadId the Id of the thread related of the event. @param cellDatas the working array of cell datas. @param cellData the original cell data the event belongs to. + @param completion completion block */ -- (void)addReadReceiptsForEvent:(NSString*)eventId inCellDatas:(NSArray>*)cellDatas startingAtCellData:(id)cellData completion:(void (^)(void))completion +- (void)addReadReceiptsForEvent:(NSString*)eventId + threadId:(NSString *)threadId + inCellDatas:(NSArray>*)cellDatas + startingAtCellData:(id)cellData + completion:(void (^)(void))completion { if (self.showBubbleReceipts) { if (self.room) { - [self.room getEventReceipts:eventId sorted:YES completion:^(NSArray * _Nonnull readReceipts) { + [self.room getEventReceipts:eventId threadId:threadId sorted:YES completion:^(NSArray * _Nonnull readReceipts) { if (readReceipts.count) { NSInteger cellDataIndex = [cellDatas indexOfObject:cellData]; @@ -3686,6 +3741,14 @@ - (void)addReadReceiptsForEvent:(NSString*)eventId inCellDatas:(NSArray *)readReceipts forEvent:(NSStri } } +/** + Clear all potential duplicated RR with same user ID within a given list of cell data. + + This is needed for client with threads disabled in order to clean threaded RRs. + + @param cellDatas the working array of cell datas. + */ +- (void)clearDuplicatedReadReceiptsInCellDatas:(NSArray>*)cellDatas +{ + NSMutableSet *seenUserIds = [NSMutableSet set]; + for (id cellData in cellDatas.reverseObjectEnumerator) + { + if ([cellData isKindOfClass:MXKRoomBubbleCellData.class]) + { + MXKRoomBubbleCellData *roomBubbleCellData = (MXKRoomBubbleCellData*)cellData; + + for (MXKRoomBubbleComponent *component in roomBubbleCellData.bubbleComponents) + { + if (component.attributedTextMessage) + { + if (roomBubbleCellData.readReceipts[component.event.eventId]) + { + NSArray *currentReadReceipts = roomBubbleCellData.readReceipts[component.event.eventId]; + NSMutableArray *newReadReceipts = [NSMutableArray array]; + for (MXReceiptData *readReceipt in currentReadReceipts) + { + if (![seenUserIds containsObject:readReceipt.userId]) + { + [newReadReceipts addObject:readReceipt]; + [seenUserIds addObject:readReceipt.userId]; + } + } + [self updateCellData:roomBubbleCellData withReadReceipts:newReadReceipts forEventId:component.event.eventId]; + } + } + } + } + } +} #pragma mark - UITableViewDataSource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index 94f7346aa3..8b3a49a5fd 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -105,6 +105,8 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) */ @property(nonatomic) NSInteger componentIndexOfSentMessageTick; +@property(nonatomic, strong) NSString *voiceBroadcastState; + /** Indicate that both the text message layout and any additional content height are no longer valid and should be recomputed before presentation in a bubble cell. This could be due diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index adcd6692e8..712604203d 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -186,23 +186,45 @@ - (instancetype)initWithEvent:(MXEvent *)event andRoomState:(MXRoomState *)roomS else if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) { VoiceBroadcastInfo *voiceBroadcastInfo = [VoiceBroadcastInfo modelFromJSON: event.content]; + + // Check if the state event corresponds to the beginning of a voice broadcast if ([VoiceBroadcastInfo isStartedFor:voiceBroadcastInfo.state]) { - // This state event corresponds to the beginning of a voice broadcast - // Check whether this is a local live broadcast to display it with the recorder view or not - // Note: Because of race condition, the voiceBroadcastService may be running without id here (the sync response may be received before - // the success of the event sending), in that case, we will display a recorder view by default to let the user be able to stop a potential record. - if ([event.sender isEqualToString: self.mxSession.myUserId] && - [voiceBroadcastInfo.deviceId isEqualToString:self.mxSession.myDeviceId] && - self.mxSession.voiceBroadcastService != nil && - ([event.eventId isEqualToString: self.mxSession.voiceBroadcastService.voiceBroadcastInfoEventId] || - self.mxSession.voiceBroadcastService.voiceBroadcastInfoEventId == nil)) + // Retrieve the most recent voice broadcast info. + MXEvent *lastVoiceBroadcastInfoEvent = [roomDataSource.roomState stateEventsWithType:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType].lastObject; + if (event.originServerTs > lastVoiceBroadcastInfoEvent.originServerTs) + { + lastVoiceBroadcastInfoEvent = event; + } + + VoiceBroadcastInfo *lastVoiceBroadcastInfo = [VoiceBroadcastInfo modelFromJSON: lastVoiceBroadcastInfoEvent.content]; + + // Handle the specific case where the state event is a started voice broadcast (the voiceBroadcastId is the event id itself). + if (!lastVoiceBroadcastInfo.voiceBroadcastId) + { + lastVoiceBroadcastInfo.voiceBroadcastId = lastVoiceBroadcastInfoEvent.eventId; + } + + // Check if the voice broadcast is still alive. + if ([lastVoiceBroadcastInfo.voiceBroadcastId isEqualToString:event.eventId] && ![VoiceBroadcastInfo isStoppedFor:lastVoiceBroadcastInfo.state]) { - self.tag = RoomBubbleCellDataTagVoiceBroadcastRecord; + // Check whether this broadcast is sent from the currrent session to display it with the recorder view or not. + if ([event.stateKey isEqualToString:self.mxSession.myUserId] && + [voiceBroadcastInfo.deviceId isEqualToString:self.mxSession.myDeviceId]) + { + self.tag = RoomBubbleCellDataTagVoiceBroadcastRecord; + } + else + { + self.tag = RoomBubbleCellDataTagVoiceBroadcastPlayback; + } + + self.voiceBroadcastState = lastVoiceBroadcastInfo.state; } else { self.tag = RoomBubbleCellDataTagVoiceBroadcastPlayback; + self.voiceBroadcastState = VoiceBroadcastInfo.stoppedValue; } } else @@ -213,8 +235,9 @@ - (instancetype)initWithEvent:(MXEvent *)event andRoomState:(MXRoomState *)roomS { // This state event corresponds to the end of a voice broadcast // Force the tag of the potential cellData which corresponds to the started event to switch the display from recorder to listener - id bubbleData = [roomDataSource cellDataOfEventWithEventId:voiceBroadcastInfo.eventId]; + RoomBubbleCellData *bubbleData = [roomDataSource cellDataOfEventWithEventId:voiceBroadcastInfo.voiceBroadcastId]; bubbleData.tag = RoomBubbleCellDataTagVoiceBroadcastPlayback; + bubbleData.voiceBroadcastState = VoiceBroadcastInfo.stoppedValue; } } self.collapsable = NO; diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 8426238188..5d76bd5f68 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -395,7 +395,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N id cellDecorator = [RoomTimelineConfiguration shared].currentStyle.cellDecorator; // Finalize cell view customization here - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *bubbleCell = (MXKRoomBubbleTableViewCell*)cell; [self resetAccessibilityForCell:bubbleCell]; diff --git a/Riot/Modules/Room/DataSources/ThreadDataSource.swift b/Riot/Modules/Room/DataSources/ThreadDataSource.swift index 3f69f7a041..603ae8862b 100644 --- a/Riot/Modules/Room/DataSources/ThreadDataSource.swift +++ b/Riot/Modules/Room/DataSources/ThreadDataSource.swift @@ -27,8 +27,8 @@ public class ThreadDataSource: RoomDataSource { public override func finalizeInitialization() { super.finalizeInitialization() - showReadMarker = false - showBubbleReceipts = false + showReadMarker = true + showBubbleReceipts = true showTypingRow = false NotificationCenter.default.addObserver(self, @@ -42,22 +42,6 @@ public class ThreadDataSource: RoomDataSource { object: nil) } - public override var showReadMarker: Bool { - get { - return false - } set { - _ = newValue - } - } - - public override var showBubbleReceipts: Bool { - get { - return false - } set { - _ = newValue - } - } - public override class func load(withRoomId roomId: String!, initialEventId: String!, threadId: String!, diff --git a/Riot/Modules/Room/MXKRoomViewController.m b/Riot/Modules/Room/MXKRoomViewController.m index b0f547bc44..de6a691f4c 100644 --- a/Riot/Modules/Room/MXKRoomViewController.m +++ b/Riot/Modules/Room/MXKRoomViewController.m @@ -368,6 +368,8 @@ - (void)viewDidAppear:(BOOL)animated // Mark all messages as read when the room is displayed [self.roomDataSource.room.summary markAllAsReadLocally]; + + [self updateCurrentEventIdAtTableBottom:YES]; } - (void)viewWillDisappear:(BOOL)animated @@ -1857,7 +1859,7 @@ - (void)triggerInitialBackPagination CGFloat localPositionOfEvent = 0.0; - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; @@ -2301,7 +2303,7 @@ - (BOOL)reloadBubblesTable:(BOOL)useBottomAnchor invalidateBubblesCellDataCache: CGFloat eventBottomPosition = eventTopPosition + cell.frame.size.height; // Compute accurate event positions in case of bubble with multiple components - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; NSArray *bubbleComponents = roomBubbleTableViewCell.bubbleData.bubbleComponents; @@ -2497,7 +2499,10 @@ - (void)updateCurrentEventIdAtTableBottom:(BOOL)acknowledge updateReadMarker = (currentReadMarkerEvent && (currentReadMarkerEvent.originServerTs <= component.event.originServerTs)); } - [roomDataSource.room acknowledgeEvent:component.event andUpdateReadMarker:updateReadMarker]; + if (self.navigationController.viewControllers.lastObject == self) + { + [roomDataSource.room acknowledgeEvent:component.event andUpdateReadMarker:updateReadMarker]; + } } break; } @@ -2599,11 +2604,11 @@ - (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)ac roomDataSource.showBubblesDateTime = !roomDataSource.showBubblesDateTime; MXLogDebug(@" -> Turn %@ cells date", roomDataSource.showBubblesDateTime ? @"ON" : @"OFF"); } - else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnAttachmentView] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnAttachmentView] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { [self showAttachmentInCell:(MXKRoomBubbleTableViewCell *)cell]; } - else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnProgressView] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnProgressView] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; @@ -2714,7 +2719,7 @@ - (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)ac } } } - else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnEvent] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellLongPressOnEvent] && [cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { [self dismissKeyboard]; @@ -3084,7 +3089,7 @@ - (void)selectAllTextMessageInCell:(id)cell return; } - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; selectedText = roomBubbleTableViewCell.bubbleData.textMessage; @@ -3623,7 +3628,7 @@ - (void)showAttachmentInCell:(UITableViewCell*)cell // Keep here the image view used to display the attachment in the selected cell. // Note: Only `MXKRoomBubbleTableViewCell` and `MXKSearchTableViewCell` are supported for the moment. - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { self.openedAttachmentImageView = ((MXKRoomBubbleTableViewCell *)cell).attachmentView.imageView; } @@ -3801,7 +3806,7 @@ - (void)showAttachmentInCell:(UITableViewCell*)cell }]; - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { // Start animation in case of download MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 7da32359f9..af49e8f6ef 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -46,6 +46,10 @@ extern NSNotificationName const RoomCallTileTappedNotification; Notification string used to indicate group call tile tapped in a room. Notification object will be the `RoomBubbleCellData` object. */ extern NSNotificationName const RoomGroupCallTileTappedNotification; +/** + Duration for the composer resize animation. + */ +extern NSTimeInterval const kResizeComposerAnimationDuration; @interface RoomViewController : MXKRoomViewController diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 2149ecaa42..a0834940a7 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -599,6 +599,7 @@ - (void)viewWillDisappear:(BOOL)animated [VoiceMessageMediaServiceProvider.sharedProvider pauseAllServices]; [VoiceBroadcastRecorderProvider.shared pauseRecording]; + [VoiceBroadcastPlaybackProvider.shared pausePlaying]; // Stop the loading indicator even if the session is still in progress [self stopLoadingUserIndicator]; @@ -3237,30 +3238,30 @@ - (RoomTimelineCellIdentifier)cellIdentifierForCellData:(MXKCellData*)cellData a { if (bubbleData.isPaginationFirstBubble) { - cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastWithPaginationTitle; + cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithPaginationTitle; } else if (bubbleData.shouldHideSenderInformation) { - cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastWithoutSenderInfo; + cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithoutSenderInfo; } else { - cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcast; + cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceBroadcastPlayback; } } else { if (bubbleData.isPaginationFirstBubble) { - cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithPaginationTitle; + cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithPaginationTitle; } else if (bubbleData.shouldHideSenderInformation) { - cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithoutSenderInfo; + cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithoutSenderInfo; } else { - cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcast; + cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlayback; } } } @@ -5115,7 +5116,9 @@ - (void)roomInputToolbarViewShowSendMediaActions:(MXKRoomInputToolbarView *)tool [actionItems addObject:@(ComposerCreateActionCamera)]; } - self.composerCreateActionListBridgePresenter = [[ComposerCreateActionListBridgePresenter alloc] initWithActions:actionItems]; + self.composerCreateActionListBridgePresenter = [[ComposerCreateActionListBridgePresenter alloc] initWithActions:actionItems + wysiwygEnabled:RiotSettings.shared.enableWysiwygComposer + textFormattingEnabled:RiotSettings.shared.enableWysiwygTextFormatting]; self.composerCreateActionListBridgePresenter.delegate = self; [self.composerCreateActionListBridgePresenter presentFrom:self animated:YES]; } @@ -5268,7 +5271,7 @@ - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)ce } } - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell; if (roomBubbleTableViewCell.readMarkerView) @@ -5918,17 +5921,13 @@ - (void)refreshActivitiesViewDisplay { if (self.roomDataSource.room) { - // Retrieve the unread messages count - NSUInteger unreadCount = self.roomDataSource.room.summary.localUnreadEventCount; + // Retrieve the unread messages count on the current thread + NSUInteger unreadCount = [self.mainSession.store + localUnreadEventCount:self.roomDataSource.room.roomId + threadId:self.roomDataSource.threadId ?: kMXEventTimelineMain + withTypeIn:self.mainSession.unreadEventTypes]; - if (!self.roomDataSource.threadId) - { - self.scrollToBottomBadgeLabel.text = unreadCount ? [NSString stringWithFormat:@"%lu", unreadCount] : nil; - } - else - { - self.scrollToBottomBadgeLabel.text = nil; - } + self.scrollToBottomBadgeLabel.text = unreadCount ? [NSString stringWithFormat:@"%lu", unreadCount] : nil; self.scrollToBottomHidden = NO; } else @@ -6526,7 +6525,7 @@ - (void)refreshJumpToLastUnreadBannerDisplay if (self.roomDataSource.isLive && !self.roomDataSource.isPeeking && self.roomDataSource.showReadMarker && self.roomDataSource.room.accountData.readMarkerEventId) { UITableViewCell *cell = [self.bubblesTableView visibleCells].firstObject; - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell*)cell; // Check whether the read marker is inside the first displayed cell. @@ -6764,8 +6763,8 @@ - (void)presentReviewUnverifiedSessionsAlert [currentAlert dismissViewControllerAnimated:NO completion:nil]; - UIAlertController *alert = [UIAlertController alertControllerWithTitle:[VectorL10n keyVerificationSelfVerifyUnverifiedSessionsAlertTitle] - message:[VectorL10n keyVerificationSelfVerifyUnverifiedSessionsAlertMessage] + UIAlertController *alert = [UIAlertController alertControllerWithTitle:[VectorL10n keyVerificationAlertTitle] + message:[VectorL10n keyVerificationAlertBody] preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:[VectorL10n keyVerificationSelfVerifyUnverifiedSessionsAlertValidateAction] @@ -7453,31 +7452,27 @@ - (void)updateThreadListBarButtonItem:(UIBarButtonItem *)barButtonItem with:(MXT MXThreadNotificationsCount *notificationsCount = [service notificationsCountForRoom:self.roomDataSource.roomId]; - if (notificationsCount.numberOfHighlightedThreads > 0) - { - [button setImage:AssetImages.threadsIconRedDot.image - forState:UIControlStateNormal]; - button.contentEdgeInsets = kThreadListBarButtonItemContentInsetsDot; - } - else if (notificationsCount.numberOfNotifiedThreads > 0) - { - if (ThemeService.shared.isCurrentThemeDark) - { - [button setImage:AssetImages.threadsIconGrayDotDark.image - forState:UIControlStateNormal]; - } - else - { - [button setImage:AssetImages.threadsIconGrayDotLight.image - forState:UIControlStateNormal]; - } - button.contentEdgeInsets = kThreadListBarButtonItemContentInsetsDot; - } - else + UIImage *buttonIcon = [AssetImages.threadsIcon.image vc_resizedWith:kThreadListBarButtonItemImageSize]; + [button setImage:buttonIcon forState:UIControlStateNormal]; + button.contentEdgeInsets = kThreadListBarButtonItemContentInsetsNoDot; + + if (notificationsCount.notificationsNumber > 0) { - [button setImage:[AssetImages.threadsIcon.image vc_resizedWith:kThreadListBarButtonItemImageSize] - forState:UIControlStateNormal]; - button.contentEdgeInsets = kThreadListBarButtonItemContentInsetsNoDot; + BadgeLabel *badgeLabel = [[BadgeLabel alloc] init]; + badgeLabel.text = notificationsCount.notificationsNumber > 99 ? @"99+" : [NSString stringWithFormat:@"%lu", notificationsCount.notificationsNumber]; + id theme = ThemeService.shared.theme; + badgeLabel.font = theme.fonts.caption1SB; + badgeLabel.textColor = theme.colors.navigation; + badgeLabel.badgeColor = notificationsCount.numberOfHighlightedThreads ? theme.colors.alert : theme.colors.secondaryContent; + [button addSubview:badgeLabel]; + + [badgeLabel layoutIfNeeded]; + + badgeLabel.translatesAutoresizingMaskIntoConstraints = NO; + [badgeLabel.centerYAnchor constraintEqualToAnchor:button.centerYAnchor + constant:badgeLabel.bounds.size.height - buttonIcon.size.height / 2].active = YES; + [badgeLabel.centerXAnchor constraintEqualToAnchor:button.centerXAnchor + constant:badgeLabel.bounds.size.width + buttonIcon.size.width / 2].active = YES; } if (replaceIndex == NSNotFound) @@ -8062,6 +8057,11 @@ - (void)composerCreateActionListBridgePresenterDelegateDidComplete:(ComposerCrea }]; } +- (void)composerCreateActionListBridgePresenterDelegateDidToggleTextFormatting:(ComposerCreateActionListBridgePresenter *)coordinatorBridgePresenter enabled:(BOOL)enabled +{ + [self togglePlainTextMode]; +} + - (void)composerCreateActionListBridgePresenterDidDismissInteractively:(ComposerCreateActionListBridgePresenter *)coordinatorBridgePresenter { self.composerCreateActionListBridgePresenter = nil; diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index 7bbc6812cd..7ac56f0d23 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -84,7 +84,7 @@ extension RoomViewController { "event_id": eventModified.eventId ]) }) - } else { + } else if !self.send(asIRCStyleCommandIfPossible: rawTextMsg) { roomDataSource.sendFormattedTextMessage(rawTextMsg, html: htmlMsg) { response in switch response { case .success: @@ -149,6 +149,11 @@ extension RoomViewController { } } } + + @objc func togglePlainTextMode() { + RiotSettings.shared.enableWysiwygTextFormatting.toggle() + wysiwygInputToolbar?.textFormattingEnabled.toggle() + } } // MARK: - Private Helpers diff --git a/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m b/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m index 9c0530a7ba..2790df0908 100644 --- a/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m +++ b/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m @@ -131,7 +131,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath]; // Finalize cell view customization here - if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) + if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class] && ![cell isKindOfClass:MXKRoomEmptyBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *bubbleCell = (MXKRoomBubbleTableViewCell*)cell; diff --git a/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h b/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h index 3348df0e66..5a91d01e35 100644 --- a/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h +++ b/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h @@ -170,13 +170,13 @@ typedef NS_ENUM(NSUInteger, RoomTimelineCellIdentifier) { // - Voice broadcast // -- Incoming - RoomTimelineCellIdentifierIncomingVoiceBroadcast, - RoomTimelineCellIdentifierIncomingVoiceBroadcastWithoutSenderInfo, - RoomTimelineCellIdentifierIncomingVoiceBroadcastWithPaginationTitle, + RoomTimelineCellIdentifierIncomingVoiceBroadcastPlayback, + RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithoutSenderInfo, + RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithPaginationTitle, // -- Outgoing - RoomTimelineCellIdentifierOutgoingVoiceBroadcast, - RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithoutSenderInfo, - RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithPaginationTitle, + RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlayback, + RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithoutSenderInfo, + RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithPaginationTitle, // - Voice broadcast recorder RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder, diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m b/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m index c747476eee..4cfb03de1f 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m @@ -135,12 +135,12 @@ - (void)registerFileWithoutThumbnailCellsForTableView:(UITableView*)tableView - (void)registerVoiceBroadcastCellsForTableView:(UITableView*)tableView { // Incoming - [tableView registerClass:VoiceBroadcastIncomingBubbleCell.class forCellReuseIdentifier:VoiceBroadcastIncomingBubbleCell.defaultReuseIdentifier]; - [tableView registerClass:VoiceBroadcastIncomingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceBroadcastIncomingWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [tableView registerClass:VoiceBroadcastIncomingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceBroadcastIncomingWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackIncomingBubbleCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackIncomingBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackIncomingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackIncomingWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackIncomingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackIncomingWithPaginationTitleBubbleCell.defaultReuseIdentifier]; // Outgoing - [tableView registerClass:VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; - [tableView registerClass:VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackOutgoingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackOutgoingWithPaginationTitleBubbleCell.defaultReuseIdentifier]; } - (void)registerVoiceBroadcastRecorderCellsForTableView:(UITableView*)tableView @@ -311,17 +311,17 @@ - (void)registerVoiceBroadcastRecorderCellsForTableView:(UITableView*)tableView }; } -- (NSDictionary*)voiceBroadcastCellsMapping +- (NSDictionary*)voiceBroadcastPlaybackCellsMapping { return @{ // Incoming - @(RoomTimelineCellIdentifierIncomingVoiceBroadcast) : VoiceBroadcastIncomingBubbleCell.class, - @(RoomTimelineCellIdentifierIncomingVoiceBroadcastWithoutSenderInfo) : VoiceBroadcastIncomingWithoutSenderInfoBubbleCell.class, - @(RoomTimelineCellIdentifierIncomingVoiceBroadcastWithPaginationTitle) : VoiceBroadcastIncomingWithPaginationTitleBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceBroadcastPlayback) : VoiceBroadcastPlaybackIncomingBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithoutSenderInfo) : VoiceBroadcastPlaybackIncomingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithPaginationTitle) : VoiceBroadcastPlaybackIncomingWithPaginationTitleBubbleCell.class, // Outgoing - @(RoomTimelineCellIdentifierOutgoingVoiceBroadcast) : VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell.class, - @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithoutSenderInfo) : VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell.class, - @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithPaginationTitle) : VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlayback) : VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithoutSenderInfo) : VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithPaginationTitle) : VoiceBroadcastPlaybackOutgoingWithPaginationTitleBubbleCell.class, }; } diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingBubbleCell.swift similarity index 92% rename from Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingBubbleCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingBubbleCell.swift index f46acbae1a..fda3dbd6dd 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingBubbleCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastIncomingBubbleCell: VoiceBroadcastBubbleCell, BubbleIncomingRoomCellProtocol { +class VoiceBroadcastPlaybackIncomingBubbleCell: VoiceBroadcastPlaybackBubbleCell, BubbleIncomingRoomCellProtocol { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingWithPaginationTitleBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingWithPaginationTitleBubbleCell.swift similarity index 87% rename from Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingWithPaginationTitleBubbleCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingWithPaginationTitleBubbleCell.swift index 6bbb10d9af..979ccd27f8 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingWithPaginationTitleBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingWithPaginationTitleBubbleCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastIncomingWithPaginationTitleBubbleCell: VoiceBroadcastIncomingBubbleCell { +class VoiceBroadcastPlaybackIncomingWithPaginationTitleBubbleCell: VoiceBroadcastPlaybackIncomingBubbleCell { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastWithoutSenderInfoPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingWithoutSenderInfoBubbleCell.swift similarity index 87% rename from Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastWithoutSenderInfoPlainCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingWithoutSenderInfoBubbleCell.swift index 6f3ec9110a..7a99e2ecba 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastWithoutSenderInfoPlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Incoming/VoiceBroadcastPlaybackIncomingWithoutSenderInfoBubbleCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastWithoutSenderInfoPlainCell: VoiceBroadcastPlainCell { +class VoiceBroadcastPlaybackIncomingWithoutSenderInfoBubbleCell: VoiceBroadcastPlaybackIncomingBubbleCell { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Outgoing/VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Outgoing/VoiceBroadcastPlaybackOutgoingWithPaginationTitleBubbleCell.swift similarity index 85% rename from Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Outgoing/VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Outgoing/VoiceBroadcastPlaybackOutgoingWithPaginationTitleBubbleCell.swift index 72f69e4d77..34bf806700 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Outgoing/VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Outgoing/VoiceBroadcastPlaybackOutgoingWithPaginationTitleBubbleCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastOutgoingWithPaginationTitleBubbleCell: VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell { +class VoiceBroadcastPlaybackOutgoingWithPaginationTitleBubbleCell: VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Outgoing/VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Outgoing/VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell.swift similarity index 92% rename from Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Outgoing/VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Outgoing/VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell.swift index b149647b60..3616469e93 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Outgoing/VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/Outgoing/VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastOutgoingWithoutSenderInfoBubbleCell: VoiceBroadcastBubbleCell, BubbleOutgoingRoomCellProtocol { +class VoiceBroadcastPlaybackOutgoingWithoutSenderInfoBubbleCell: VoiceBroadcastPlaybackBubbleCell, BubbleOutgoingRoomCellProtocol { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/VoiceBroadcastBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackBubbleCell.swift similarity index 96% rename from Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/VoiceBroadcastBubbleCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackBubbleCell.swift index 67db62e889..2de4341db6 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/VoiceBroadcastBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackBubbleCell.swift @@ -16,7 +16,7 @@ import UIKit -class VoiceBroadcastBubbleCell: VoiceBroadcastPlainCell { +class VoiceBroadcastPlaybackBubbleCell: VoiceBroadcastPlaybackPlainCell { // MARK: - Properties @@ -95,7 +95,7 @@ class VoiceBroadcastBubbleCell: VoiceBroadcastPlainCell { } // MARK: - RoomCellTimestampDisplayable -extension VoiceBroadcastBubbleCell: TimestampDisplayable { +extension VoiceBroadcastPlaybackBubbleCell: TimestampDisplayable { func addTimestampView(_ timestampView: UIView) { guard let messageBubbleBackgroundView = self.getBubbleBackgroundView() else { diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainBubbleCell.swift new file mode 100644 index 0000000000..365a159564 --- /dev/null +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainBubbleCell.swift @@ -0,0 +1,37 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class VoiceBroadcastPlaybackPlainBubbleCell: VoiceBroadcastPlaybackBubbleCell { + + override func setupViews() { + super.setupViews() + + // TODO: VB update margins attributes + let leftMargin: CGFloat = BubbleRoomCellLayoutConstants.incomingBubbleBackgroundMargins.left + BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.left + let rightMargin: CGFloat = 15 + BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.right + + roomCellContentView?.innerContentViewLeadingConstraint.constant = leftMargin + roomCellContentView?.innerContentViewTrailingConstraint.constant = rightMargin + } + + override func update(theme: Theme) { + super.update(theme: theme) + + self.bubbleBackgroundColor = theme.roomCellIncomingBubbleBackgroundColor + } +} diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainBubbleCell.swift new file mode 100644 index 0000000000..69f94e8fad --- /dev/null +++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainBubbleCell.swift @@ -0,0 +1,37 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class VoiceBroadcastRecorderPlainBubbleCell: VoiceBroadcastRecorderBubbleCell { + + override func setupViews() { + super.setupViews() + + // TODO: VB update margins attributes + let leftMargin: CGFloat = BubbleRoomCellLayoutConstants.incomingBubbleBackgroundMargins.left + BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.left + let rightMargin: CGFloat = 15 + BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.right + + roomCellContentView?.innerContentViewLeadingConstraint.constant = leftMargin + roomCellContentView?.innerContentViewTrailingConstraint.constant = rightMargin + } + + override func update(theme: Theme) { + super.update(theme: theme) + + self.bubbleBackgroundColor = theme.roomCellIncomingBubbleBackgroundColor + } +} diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainCell.swift similarity index 76% rename from Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastPlainCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainCell.swift index 14c602c4cd..8987cb1de5 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastPlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainCell.swift @@ -16,19 +16,22 @@ import Foundation -class VoiceBroadcastPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCellReadMarkerDisplayable { +class VoiceBroadcastPlaybackPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCellReadMarkerDisplayable { private var event: MXEvent? override func render(_ cellData: MXKCellData!) { super.render(cellData) - + guard let contentView = roomCellContentView?.innerContentView, let bubbleData = cellData as? RoomBubbleCellData, let event = bubbleData.events.last, let voiceBroadcastContent = VoiceBroadcastInfo(fromJSON: event.content), voiceBroadcastContent.state == VoiceBroadcastInfo.State.started.rawValue, - let controller = VoiceBroadcastPlaybackProvider.shared.buildVoiceBroadcastPlaybackVCForEvent(event, senderDisplayName: bubbleData.senderDisplayName) else { + let controller = VoiceBroadcastPlaybackProvider.shared.buildVoiceBroadcastPlaybackVCForEvent(event, + senderDisplayName: bubbleData.senderDisplayName, + voiceBroadcastState: bubbleData.voiceBroadcastState) + else { return } @@ -54,4 +57,4 @@ class VoiceBroadcastPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable } } -extension VoiceBroadcastPlainCell: RoomCellThreadSummaryDisplayable {} +extension VoiceBroadcastPlaybackPlainCell: RoomCellThreadSummaryDisplayable {} diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastWithPaginationTitlePlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackWithPaginationTitlePlainCell.swift similarity index 88% rename from Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastWithPaginationTitlePlainCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackWithPaginationTitlePlainCell.swift index fa3c3bc500..09f0bcff53 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastWithPaginationTitlePlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackWithPaginationTitlePlainCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastWithPaginationTitlePlainCell: VoiceBroadcastPlainCell { +class VoiceBroadcastPlaybackWithPaginationTitlePlainCell: VoiceBroadcastPlaybackPlainBubbleCell { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackWithoutSenderInfoPlainCell.swift similarity index 88% rename from Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingWithoutSenderInfoBubbleCell.swift rename to Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackWithoutSenderInfoPlainCell.swift index 4f123da7d5..41f98f81ad 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Incoming/VoiceBroadcastIncomingWithoutSenderInfoBubbleCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackWithoutSenderInfoPlainCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastIncomingWithoutSenderInfoBubbleCell: VoiceBroadcastIncomingBubbleCell { +class VoiceBroadcastPlaybackWithoutSenderInfoPlainCell: VoiceBroadcastPlaybackPlainBubbleCell { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithPaginationTitlePlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithPaginationTitlePlainCell.swift index 4247f306c9..5c0bb91432 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithPaginationTitlePlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithPaginationTitlePlainCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastRecorderWithPaginationTitlePlainCell: VoiceBroadcastRecorderPlainCell { +class VoiceBroadcastRecorderWithPaginationTitlePlainCell: VoiceBroadcastRecorderPlainBubbleCell { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithoutSenderInfoPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithoutSenderInfoPlainCell.swift index 172b10aee8..797ab8c57f 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithoutSenderInfoPlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithoutSenderInfoPlainCell.swift @@ -16,7 +16,7 @@ import Foundation -class VoiceBroadcastRecorderWithoutSenderInfoPlainCell: VoiceBroadcastRecorderPlainCell { +class VoiceBroadcastRecorderWithoutSenderInfoPlainCell: VoiceBroadcastRecorderPlainBubbleCell { override func setupViews() { super.setupViews() diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h index b1e85a6212..21d8b11bc1 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h @@ -56,7 +56,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSDictionary*)locationCellsMapping; -- (NSDictionary*)voiceBroadcastCellsMapping; +- (NSDictionary*)voiceBroadcastPlaybackCellsMapping; - (NSDictionary*)voiceBroadcastRecorderCellsMapping; diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m index 4813b539d9..83b835579a 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m @@ -276,14 +276,14 @@ - (void)registerFileWithoutThumbnailCellsForTableView:(UITableView*)tableView - (void)registerVoiceBroadcastCellsForTableView:(UITableView*)tableView { - [tableView registerClass:VoiceBroadcastPlainCell.class forCellReuseIdentifier:VoiceBroadcastPlainCell.defaultReuseIdentifier]; - [tableView registerClass:VoiceBroadcastWithoutSenderInfoPlainCell.class forCellReuseIdentifier:VoiceBroadcastWithoutSenderInfoPlainCell.defaultReuseIdentifier]; - [tableView registerClass:VoiceBroadcastWithPaginationTitlePlainCell.class forCellReuseIdentifier:VoiceBroadcastWithPaginationTitlePlainCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackPlainBubbleCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackPlainBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackWithoutSenderInfoPlainCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackWithoutSenderInfoPlainCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastPlaybackWithPaginationTitlePlainCell.class forCellReuseIdentifier:VoiceBroadcastPlaybackWithPaginationTitlePlainCell.defaultReuseIdentifier]; } - (void)registerVoiceBroadcastRecorderCellsForTableView:(UITableView*)tableView { - [tableView registerClass:VoiceBroadcastRecorderPlainCell.class forCellReuseIdentifier:VoiceBroadcastRecorderPlainCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceBroadcastRecorderPlainBubbleCell.class forCellReuseIdentifier:VoiceBroadcastRecorderPlainBubbleCell.defaultReuseIdentifier]; [tableView registerClass:VoiceBroadcastRecorderWithoutSenderInfoPlainCell.class forCellReuseIdentifier:VoiceBroadcastRecorderWithoutSenderInfoPlainCell.defaultReuseIdentifier]; [tableView registerClass:VoiceBroadcastRecorderWithPaginationTitlePlainCell.class forCellReuseIdentifier:VoiceBroadcastRecorderWithPaginationTitlePlainCell.defaultReuseIdentifier]; } @@ -346,8 +346,8 @@ - (void)registerVoiceBroadcastRecorderCellsForTableView:(UITableView*)tableView NSDictionary *locationCellsMapping = [self locationCellsMapping]; [cellClasses addEntriesFromDictionary:locationCellsMapping]; - NSDictionary *voiceBroadcastCellsMapping = [self voiceBroadcastCellsMapping]; - [cellClasses addEntriesFromDictionary:voiceBroadcastCellsMapping]; + NSDictionary *voiceBroadcastPlaybackCellsMapping = [self voiceBroadcastPlaybackCellsMapping]; + [cellClasses addEntriesFromDictionary:voiceBroadcastPlaybackCellsMapping]; NSDictionary *voiceBroadcastRecorderCellsMapping = [self voiceBroadcastRecorderCellsMapping]; [cellClasses addEntriesFromDictionary:voiceBroadcastRecorderCellsMapping]; @@ -574,17 +574,17 @@ - (void)registerVoiceBroadcastRecorderCellsForTableView:(UITableView*)tableView }; } -- (NSDictionary*)voiceBroadcastCellsMapping +- (NSDictionary*)voiceBroadcastPlaybackCellsMapping { return @{ // Incoming - @(RoomTimelineCellIdentifierIncomingVoiceBroadcast) : VoiceBroadcastPlainCell.class, - @(RoomTimelineCellIdentifierIncomingVoiceBroadcastWithoutSenderInfo) : VoiceBroadcastWithoutSenderInfoPlainCell.class, - @(RoomTimelineCellIdentifierIncomingVoiceBroadcastWithPaginationTitle) : VoiceBroadcastWithPaginationTitlePlainCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceBroadcastPlayback) : VoiceBroadcastPlaybackPlainBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithoutSenderInfo) : VoiceBroadcastPlaybackWithoutSenderInfoPlainCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceBroadcastPlaybackWithPaginationTitle) : VoiceBroadcastPlaybackWithPaginationTitlePlainCell.class, // Outoing - @(RoomTimelineCellIdentifierOutgoingVoiceBroadcast) : VoiceBroadcastPlainCell.class, - @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithoutSenderInfo) : VoiceBroadcastWithoutSenderInfoPlainCell.class, - @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithPaginationTitle) : VoiceBroadcastWithPaginationTitlePlainCell.class + @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlayback) : VoiceBroadcastPlaybackPlainBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithoutSenderInfo) : VoiceBroadcastPlaybackWithoutSenderInfoPlainCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastPlaybackWithPaginationTitle) : VoiceBroadcastPlaybackWithPaginationTitlePlainCell.class }; } @@ -592,7 +592,7 @@ - (void)registerVoiceBroadcastRecorderCellsForTableView:(UITableView*)tableView { return @{ // Outoing - @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder) : VoiceBroadcastRecorderPlainCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder) : VoiceBroadcastRecorderPlainBubbleCell.class, @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithoutSenderInfo) : VoiceBroadcastRecorderWithoutSenderInfoPlainCell.class, @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithPaginationTitle) : VoiceBroadcastRecorderWithPaginationTitlePlainCell.class }; diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index f886276fd6..e6fad8e094 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -37,7 +37,9 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private var heightConstraint: NSLayoutConstraint! private var hostingViewController: VectorHostingController! private var wysiwygViewModel = WysiwygComposerViewModel(textColor: ThemeService.shared().theme.colors.primaryContent) - private var viewModel: ComposerViewModelProtocol = ComposerViewModel(initialViewState: ComposerViewState()) + private var viewModel: ComposerViewModelProtocol = ComposerViewModel( + initialViewState: ComposerViewState(textFormattingEnabled: RiotSettings.shared.enableWysiwygTextFormatting, + bindings: ComposerBindings(focused: false))) // MARK: Public @@ -66,11 +68,14 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp viewModel.callback = { [weak self] result in self?.handleViewModelResult(result) } + wysiwygViewModel.plainTextMode = !RiotSettings.shared.enableWysiwygTextFormatting inputAccessoryViewForKeyboard = UIView(frame: .zero) - let composer = Composer(viewModel: viewModel.context, + let composer = Composer( + viewModel: viewModel.context, wysiwygViewModel: wysiwygViewModel, + resizeAnimationDuration: Double(kResizeComposerAnimationDuration), sendMessageAction: { [weak self] content in guard let self = self else { return } self.sendWysiwygMessage(content: content) @@ -97,6 +102,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp subView.trailingAnchor.constraint(equalTo: self.trailingAnchor), subView.bottomAnchor.constraint(equalTo: self.bottomAnchor) ]) + cancellables = [ hostingViewController.heightPublisher .removeDuplicates() @@ -121,6 +127,10 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp self.backgroundColor = .clear } + override func dismissKeyboard() { + self.viewModel.dismissKeyboard() + } + // MARK: - Private private func updateToolbarHeight(wysiwygHeight: CGFloat) { @@ -129,7 +139,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } private func sendWysiwygMessage(content: WysiwygComposerContent) { - delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: content.html, withRawText: content.plainText) + delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: content.html, withRawText: content.markdown) } private func showSendMediaActions() { @@ -206,6 +216,20 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp updatePlaceholderText() } } + + /// Whether text formatting is currently enabled in the composer. + var textFormattingEnabled: Bool { + get { + self.viewModel.textFormattingEnabled + } + set { + self.viewModel.textFormattingEnabled = newValue + self.wysiwygViewModel.plainTextMode = !newValue + if !newValue { + self.wysiwygViewModel.maximised = false + } + } + } /// Add the voice message toolbar to the composer /// - Parameter voiceMessageToolbarView: the voice message toolbar UIView diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift index 46e06cfeda..231773f2b5 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift @@ -72,6 +72,10 @@ class VoiceMessageAudioPlayer: NSObject { return audioPlayer.items() } + var currentUrl: URL? { + return (audioPlayer?.currentItem?.asset as? AVURLAsset)?.url + } + private(set) var isStopped = true deinit { diff --git a/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountService.swift b/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountService.swift index 911b1daa76..db91179c60 100644 --- a/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountService.swift +++ b/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountService.swift @@ -47,7 +47,7 @@ enum DeactivateAccountServiceError: Error { @objcMembers class DeactivateAccountService: NSObject { private let session: MXSession private let uiaService: UserInteractiveAuthenticationService - private let request = AuthenticatedEndpointRequest(path: "\(kMXAPIPrefixPathR0)/account/deactivate", httpMethod: "POST") + private let request = AuthenticatedEndpointRequest(path: "\(kMXAPIPrefixPathR0)/account/deactivate", httpMethod: "POST", params: [:]) /// The authentication session's ID if interactive authentication has begun, otherwise `nil`. private var sessionID: String? diff --git a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m index bab229a54d..806e10cffc 100644 --- a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m +++ b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m @@ -673,7 +673,7 @@ - (void)removeDevice NSString *title = [VectorL10n deviceDetailsDeletePromptTitle]; NSString *message = [VectorL10n deviceDetailsDeletePromptMessage]; - AuthenticatedEndpointRequest *deleteDeviceRequest = [[AuthenticatedEndpointRequest alloc] initWithPath:[NSString stringWithFormat:@"%@/devices/%@", kMXAPIPrefixPathR0, [MXTools encodeURIComponent:device.deviceId]] httpMethod:@"DELETE"]; + AuthenticatedEndpointRequest *deleteDeviceRequest = [[AuthenticatedEndpointRequest alloc] initWithPath:[NSString stringWithFormat:@"%@/devices/%@", kMXAPIPrefixPathR0, [MXTools encodeURIComponent:device.deviceId]] httpMethod:@"DELETE" params:[[NSDictionary alloc] init]]; ReauthenticationCoordinatorParameters *coordinatorParameters = [[ReauthenticationCoordinatorParameters alloc] initWithSession:self.mainSession presenter:self title:title message:message authenticatedEndpointRequest:deleteDeviceRequest]; diff --git a/Riot/Modules/Settings/SettingsViewController.h b/Riot/Modules/Settings/SettingsViewController.h index dfe764538e..3e99593fc0 100644 --- a/Riot/Modules/Settings/SettingsViewController.h +++ b/Riot/Modules/Settings/SettingsViewController.h @@ -20,5 +20,6 @@ + (instancetype)instantiate; +- (void)showUserSessionsFlow; @end diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index 4bdab4abd6..a9c810c93f 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -903,7 +903,7 @@ - (void)presentVerifyCurrentSessionAlertWithSession:(MXSession*)session - (void)presentReviewUnverifiedSessionsAlertIfNeededWithSession:(MXSession*)session { - if (RiotSettings.shared.hideReviewSessionsAlert || self.reviewSessionAlertHasBeenDisplayed) + if (self.reviewSessionAlertHasBeenDisplayed) { return; } @@ -934,8 +934,8 @@ - (void)presentReviewUnverifiedSessionsAlertWithSession:(MXSession*)session [currentAlert dismissViewControllerAnimated:NO completion:nil]; - UIAlertController *alert = [UIAlertController alertControllerWithTitle:[VectorL10n keyVerificationSelfVerifyUnverifiedSessionsAlertTitle] - message:[VectorL10n keyVerificationSelfVerifyUnverifiedSessionsAlertMessage] + UIAlertController *alert = [UIAlertController alertControllerWithTitle:[VectorL10n keyVerificationAlertTitle] + message:[VectorL10n keyVerificationAlertBody] preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:[VectorL10n keyVerificationSelfVerifyUnverifiedSessionsAlertValidateAction] @@ -948,13 +948,6 @@ - (void)presentReviewUnverifiedSessionsAlertWithSession:(MXSession*)session style:UIAlertActionStyleCancel handler:nil]]; - [alert addAction:[UIAlertAction actionWithTitle:[VectorL10n doNotAskAgain] - style:UIAlertActionStyleDestructive - handler:^(UIAlertAction * action) { - RiotSettings.shared.hideReviewSessionsAlert = YES; - }]]; - - [self presentViewController:alert animated:YES completion:nil]; currentAlert = alert; @@ -975,7 +968,6 @@ - (void)resetReviewSessionsFlags { self.reviewSessionAlertHasBeenDisplayed = NO; RiotSettings.shared.hideVerifyThisSessionAlert = NO; - RiotSettings.shared.hideReviewSessionsAlert = NO; } #pragma mark - UITabBarDelegate diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift index de9cb89de4..29ae93b020 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift @@ -210,7 +210,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { return eventFormatter.attributedString(from: message.replyStrippedVersion, with: roomState, andLatestRoomState: nil, - error: formatterError).vc_byRemovingLinks + error: formatterError)?.vc_byRemovingLinks } private func lastMessageTextAndTime(forThread thread: MXThreadProtocol) -> (NSAttributedString?, String?) { diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadModel.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadModel.swift index 6667d5bfc5..7e89556fec 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadModel.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadModel.swift @@ -35,7 +35,7 @@ enum ThreadNotificationStatus { init(withThread thread: MXThreadProtocol) { if thread.highlightCount > 0 { self = .highlighted - } else if thread.isParticipated && thread.notificationCount > 0 { + } else if thread.notificationCount > 0 { self = .notified } else { self = .none diff --git a/Riot/Modules/Threads/ThreadsCoordinator.swift b/Riot/Modules/Threads/ThreadsCoordinator.swift index bc0a3608a6..9e09cacf5f 100644 --- a/Riot/Modules/Threads/ThreadsCoordinator.swift +++ b/Riot/Modules/Threads/ThreadsCoordinator.swift @@ -68,10 +68,6 @@ final class ThreadsCoordinator: NSObject, ThreadsCoordinatorProtocol { // Detect when view controller has been dismissed by gesture when presented modally (not in full screen). self.navigationRouter.toPresentable().presentationController?.delegate = self - guard parameters.threadId == nil else { - return - } - if self.navigationRouter.modules.isEmpty == false { self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in self?.remove(childCoordinator: rootCoordinator) diff --git a/Riot/Modules/UserInteractiveAuthentication/AuthenticatedEndpointRequest.swift b/Riot/Modules/UserInteractiveAuthentication/AuthenticatedEndpointRequest.swift index f8827427af..a6e1078083 100644 --- a/Riot/Modules/UserInteractiveAuthentication/AuthenticatedEndpointRequest.swift +++ b/Riot/Modules/UserInteractiveAuthentication/AuthenticatedEndpointRequest.swift @@ -22,10 +22,11 @@ class AuthenticatedEndpointRequest: NSObject { let path: String let httpMethod: String - - init(path: String, httpMethod: String) { + let params: [String: Any] + init(path: String, httpMethod: String, params: [String: Any]) { self.path = path self.httpMethod = httpMethod + self.params = params super.init() } } @@ -37,6 +38,15 @@ extension AuthenticatedEndpointRequest { /// - Parameter deviceID: The device ID that is to be deleted. static func deleteDevice(_ deviceID: String) -> AuthenticatedEndpointRequest { let path = String(format: "%@/devices/%@", kMXAPIPrefixPathR0, MXTools.encodeURIComponent(deviceID)) - return AuthenticatedEndpointRequest(path: path, httpMethod: "DELETE") + return AuthenticatedEndpointRequest(path: path, httpMethod: "DELETE", params: [:]) + } +} + +extension AuthenticatedEndpointRequest { + /// Create an authenticated request on `_matrix/client/r0/delete_devices`. + /// - Parameter deviceIDs: IDs for devices that is to be deleted. + static func deleteDevices(_ deviceIDs: [String]) -> AuthenticatedEndpointRequest { + let path = String(format: "%@/delete_devices", kMXAPIPrefixPathR0) + return AuthenticatedEndpointRequest(path: path, httpMethod: "POST", params: ["devices": deviceIDs]) } } diff --git a/Riot/Modules/UserInteractiveAuthentication/UserInteractiveAuthenticationService.swift b/Riot/Modules/UserInteractiveAuthentication/UserInteractiveAuthenticationService.swift index 56319d3e64..7d7c709c54 100644 --- a/Riot/Modules/UserInteractiveAuthentication/UserInteractiveAuthenticationService.swift +++ b/Riot/Modules/UserInteractiveAuthentication/UserInteractiveAuthenticationService.swift @@ -131,7 +131,7 @@ final class UserInteractiveAuthenticationService: NSObject { success: @escaping (MXAuthenticationSession?) -> Void, failure: @escaping (Error) -> Void) -> MXHTTPOperation { // Get the authentication flow required for this API - return self.session.matrixRestClient.authSessionForRequest(withMethod: request.httpMethod, path: request.path, parameters: [:], success: { [weak self] (authenticationSession) in + return self.session.matrixRestClient.authSessionForRequest(withMethod: request.httpMethod, path: request.path, parameters: request.params, success: { [weak self] (authenticationSession) in guard let self = self else { return } diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift index 1de0229049..fb90d834de 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift @@ -110,7 +110,7 @@ public class VoiceBroadcastAggregator { guard let event = roomState?.stateEvents(with: .custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType))?.last, event.stateKey == self.voiceBroadcastSenderId, let voiceBroadcastInfo = VoiceBroadcastInfo(fromJSON: event.content), - (event.eventId == self.voiceBroadcastStartEventId || voiceBroadcastInfo.eventId == self.voiceBroadcastStartEventId), + (event.eventId == self.voiceBroadcastStartEventId || voiceBroadcastInfo.voiceBroadcastId == self.voiceBroadcastStartEventId), let state = VoiceBroadcastInfo.State(rawValue: voiceBroadcastInfo.state) else { return } diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastBuilder.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastBuilder.swift index e27f5258a8..5bd472f6f7 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastBuilder.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastBuilder.swift @@ -27,20 +27,25 @@ struct VoiceBroadcastBuilder { var voiceBroadcast = VoiceBroadcast() - voiceBroadcast.chunks = Set(events.compactMap { event in + let chunks = Set(events.compactMap { event in buildChunk(event: event, mediaManager: mediaManager, voiceBroadcastStartEventId: voiceBroadcastStartEventId) }) + voiceBroadcast.chunks = chunks + voiceBroadcast.duration = chunks.reduce(0) { $0 + $1.duration} + return voiceBroadcast } func buildChunk(event: MXEvent, mediaManager: MXMediaManager, voiceBroadcastStartEventId: String) -> VoiceBroadcastChunk? { guard let attachment = MXKAttachment(event: event, andMediaManager: mediaManager), let chunkInfo = event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] as? [String: UInt], - let sequence = chunkInfo[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkSequence] else { + let sequence = chunkInfo[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkSequence], + let audio = event.content[kMXMessageContentKeyExtensibleAudioMSC1767] as? [String: UInt], + let duration = audio[kMXMessageContentKeyExtensibleAudioDuration] else { return nil } - return VoiceBroadcastChunk(voiceBroadcastInfoEventId: voiceBroadcastStartEventId, sequence: sequence, attachment: attachment) + return VoiceBroadcastChunk(voiceBroadcastInfoEventId: voiceBroadcastStartEventId, sequence: sequence, attachment: attachment, duration: duration) } } diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastChunk.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastChunk.swift index 1d974d791c..8c18fc6029 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastChunk.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastChunk.swift @@ -20,13 +20,16 @@ public class VoiceBroadcastChunk: NSObject { public private(set) var voiceBroadcastInfoEventId: String public private(set) var sequence: UInt public private(set) var attachment: MXKAttachment + public private(set) var duration: UInt public init(voiceBroadcastInfoEventId: String, sequence: UInt, - attachment: MXKAttachment) { + attachment: MXKAttachment, + duration: UInt) { self.voiceBroadcastInfoEventId = voiceBroadcastInfoEventId self.sequence = sequence self.attachment = attachment + self.duration = duration } public static func == (lhs: VoiceBroadcastChunk, rhs: VoiceBroadcastChunk) -> Bool { diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h index 36b963e475..71781d9276 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h @@ -32,15 +32,12 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) NSInteger chunkLength; /// The event id of the started voice broadcast info state event. -@property (nonatomic, strong, nullable) NSString* eventId; - -/// The event used to build the MXBeaconInfo. -@property (nonatomic, readonly, nullable) MXEvent *originalEvent; +@property (nonatomic, strong, nullable) NSString* voiceBroadcastId; - (instancetype)initWithDeviceId:(NSString *)deviceId state:(NSString *)state chunkLength:(NSInteger)chunkLength - eventId:(NSString *)eventId; + voiceBroadcastId:(NSString *)voiceBroadcastId; @end diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m index 51a50876c5..eaaaa9047c 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m @@ -22,14 +22,14 @@ @implementation VoiceBroadcastInfo - (instancetype)initWithDeviceId:(NSString *)deviceId state:(NSString *)state chunkLength:(NSInteger)chunkLength - eventId:(NSString *)eventId + voiceBroadcastId:(NSString *)voiceBroadcastId { if (self = [super init]) { _deviceId = deviceId; _state = state; _chunkLength = chunkLength; - _eventId = eventId; + _voiceBroadcastId = voiceBroadcastId; } return self; @@ -55,7 +55,7 @@ + (id)modelFromJSON:(NSDictionary *)JSONDictionary MXJSONModelSetInteger(chunkLength, JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkLength]); } - NSString *eventId; + NSString *voiceBroadcastId; if (JSONDictionary[kMXEventRelationRelatesToKey]) { MXEventContentRelatesTo *relatesTo; @@ -63,11 +63,11 @@ + (id)modelFromJSON:(NSDictionary *)JSONDictionary if (relatesTo && [relatesTo.relationType isEqualToString:MXEventRelationTypeReference]) { - eventId = relatesTo.eventId; + voiceBroadcastId = relatesTo.eventId; } } - return [[VoiceBroadcastInfo alloc] initWithDeviceId:deviceId state:state chunkLength:chunkLength eventId:eventId]; + return [[VoiceBroadcastInfo alloc] initWithDeviceId:deviceId state:state chunkLength:chunkLength voiceBroadcastId:voiceBroadcastId]; } - (NSDictionary *)JSONDictionary @@ -78,8 +78,8 @@ - (NSDictionary *)JSONDictionary JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyState] = self.state; - if (_eventId) { - MXEventContentRelatesTo *relatesTo = [[MXEventContentRelatesTo alloc] initWithRelationType:MXEventRelationTypeReference eventId:_eventId]; + if (_voiceBroadcastId) { + MXEventContentRelatesTo *relatesTo = [[MXEventContentRelatesTo alloc] initWithRelationType:MXEventRelationTypeReference eventId:_voiceBroadcastId]; JSONDictionary[kMXEventRelationRelatesToKey] = relatesTo.JSONDictionary; } else { diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift index 3515a5b59a..b2bc1afe42 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift @@ -35,4 +35,20 @@ extension VoiceBroadcastInfo { @objc static func isStopped(for name: String) -> Bool { return name == State.stopped.rawValue } + + @objc static func startedValue() -> String { + return State.started.rawValue + } + + @objc static func pausedValue() -> String { + return State.paused.rawValue + } + + @objc static func resumedValue() -> String { + return State.resumed.rawValue + } + + @objc static func stoppedValue() -> String { + return State.stopped.rawValue + } } diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastModels.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastModels.swift index 138af9e32d..4b36bea73f 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastModels.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastModels.swift @@ -24,4 +24,5 @@ public enum VoiceBroadcastKind { public struct VoiceBroadcast { var chunks: Set = [] var kind: VoiceBroadcastKind = .player + var duration: UInt = 0 } diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift index 81cbc51aff..e6d6171a8a 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift @@ -23,15 +23,18 @@ public class VoiceBroadcastService: NSObject { // MARK: - Properties - public private(set) var voiceBroadcastInfoEventId: String? public let room: MXRoom + public private(set) var voiceBroadcastId: String? public private(set) var state: VoiceBroadcastInfo.State + // Mechanism to process one call of sendVoiceBroadcastInfo() at a time + private let asyncTaskQueue: MXAsyncTaskQueue // MARK: - Setup public init(room: MXRoom, state: VoiceBroadcastInfo.State) { self.room = room self.state = state + self.asyncTaskQueue = MXAsyncTaskQueue(label: "VoiceBroadcastServiceQueueEventSerialQueue-" + MXTools.generateSecret()) } // MARK: - Constants @@ -43,14 +46,13 @@ public class VoiceBroadcastService: NSObject { /// Start a voice broadcast. /// - Parameters: /// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success. - /// - Returns: a `MXHTTPOperation` instance. - func startVoiceBroadcast(completion: @escaping (MXResponse) -> Void) -> MXHTTPOperation? { - return sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.started) { [weak self] response in + func startVoiceBroadcast(completion: @escaping (MXResponse) -> Void) { + sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.started) { [weak self] response in guard let self = self else { return } switch response { case .success((let eventIdResponse)): - self.voiceBroadcastInfoEventId = eventIdResponse + self.voiceBroadcastId = eventIdResponse completion(.success(eventIdResponse)) case .failure(let error): completion(.failure(error)) @@ -61,25 +63,22 @@ public class VoiceBroadcastService: NSObject { /// Pause a voice broadcast. /// - Parameters: /// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success. - /// - Returns: a `MXHTTPOperation` instance. - func pauseVoiceBroadcast(completion: @escaping (MXResponse) -> Void) -> MXHTTPOperation? { - return sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.paused, completion: completion) + func pauseVoiceBroadcast(completion: @escaping (MXResponse) -> Void) { + sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.paused, completion: completion) } /// resume a voice broadcast. /// - Parameters: /// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success. - /// - Returns: a `MXHTTPOperation` instance. - func resumeVoiceBroadcast(completion: @escaping (MXResponse) -> Void) -> MXHTTPOperation? { - return sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.resumed, completion: completion) + func resumeVoiceBroadcast(completion: @escaping (MXResponse) -> Void) { + sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.resumed, completion: completion) } /// stop a voice broadcast info. /// - Parameters: /// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success. - /// - Returns: a `MXHTTPOperation` instance. - func stopVoiceBroadcast(completion: @escaping (MXResponse) -> Void) -> MXHTTPOperation? { - return sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.stopped, completion: completion) + func stopVoiceBroadcast(completion: @escaping (MXResponse) -> Void) { + sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.stopped, completion: completion) } func getState() -> String { @@ -104,19 +103,17 @@ public class VoiceBroadcastService: NSObject { func sendChunkOfVoiceBroadcast(audioFileLocalURL: URL, mimeType: String?, duration: UInt, - samples: [Float]?, sequence: UInt, success: @escaping ((String?) -> Void), failure: @escaping ((Error?) -> Void)) { - guard let voiceBroadcastInfoEventId = self.voiceBroadcastInfoEventId else { + guard let voiceBroadcastId = self.voiceBroadcastId else { return failure(VoiceBroadcastServiceError.notStarted) } self.room.sendChunkOfVoiceBroadcast(localURL: audioFileLocalURL, - voiceBroadcastInfoEventId: voiceBroadcastInfoEventId, + voiceBroadcastId: voiceBroadcastId, mimeType: mimeType, duration: duration, - samples: samples, sequence: sequence, success: success, failure: failure) @@ -124,46 +121,71 @@ public class VoiceBroadcastService: NSObject { // MARK: - Private - private func sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State, completion: @escaping (MXResponse) -> Void) -> MXHTTPOperation? { + private func allowedStates(from state: VoiceBroadcastInfo.State) -> [VoiceBroadcastInfo.State] { + switch state { + case .started: + return [.paused, .stopped] + case .paused: + return [.resumed, .stopped] + case .resumed: + return [.paused, .stopped] + case .stopped: + return [.started] + } + } + + private func sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State, completion: @escaping (MXResponse) -> Void) { guard let userId = self.room.mxSession.myUserId else { completion(.failure(VoiceBroadcastServiceError.missingUserId)) - return nil + return } - let stateKey = userId - - let voiceBroadcastInfo = VoiceBroadcastInfo() - - voiceBroadcastInfo.deviceId = self.room.mxSession.myDeviceId - - voiceBroadcastInfo.state = state.rawValue - - if state != VoiceBroadcastInfo.State.started { - guard let voiceBroadcastInfoEventId = self.voiceBroadcastInfoEventId else { - completion(.failure(VoiceBroadcastServiceError.notStarted)) - return nil + asyncTaskQueue.async { (taskCompleted) in + guard self.allowedStates(from: self.state).contains(state) else { + MXLog.warning("[VoiceBroadcastService] sendVoiceBroadcastInfo: unexpected state change \(self.state) -> \(state)") + completion(.failure(VoiceBroadcastServiceError.unexpectedState)) + taskCompleted() + return } - voiceBroadcastInfo.eventId = voiceBroadcastInfoEventId - } else { - voiceBroadcastInfo.chunkLength = BuildSettings.voiceBroadcastChunkLength - } - - guard let stateEventContent = voiceBroadcastInfo.jsonDictionary() as? [String: Any] else { - completion(.failure(VoiceBroadcastServiceError.unknown)) - return nil - } - - return self.room.sendStateEvent(.custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType), - content: stateEventContent, stateKey: stateKey) { [weak self] response in - guard let self = self else { return } + let stateKey = userId - switch response { - case .success(let object): - self.state = state - completion(.success(object)) - case .failure(let error): - completion(.failure(error)) + let voiceBroadcastInfo = VoiceBroadcastInfo() + + voiceBroadcastInfo.deviceId = self.room.mxSession.myDeviceId + + voiceBroadcastInfo.state = state.rawValue + + if state != VoiceBroadcastInfo.State.started { + guard let voiceBroadcastId = self.voiceBroadcastId else { + completion(.failure(VoiceBroadcastServiceError.notStarted)) + taskCompleted() + return + } + + voiceBroadcastInfo.voiceBroadcastId = voiceBroadcastId + } else { + voiceBroadcastInfo.chunkLength = BuildSettings.voiceBroadcastChunkLength + } + + guard let stateEventContent = voiceBroadcastInfo.jsonDictionary() as? [String: Any] else { + completion(.failure(VoiceBroadcastServiceError.unknown)) + taskCompleted() + return + } + + self.room.sendStateEvent(.custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType), + content: stateEventContent, stateKey: stateKey) { [weak self] response in + guard let self = self else { return } + + switch response { + case .success(let object): + self.state = state + completion(.success(object)) + case .failure(let error): + completion(.failure(error)) + } + taskCompleted() } } } @@ -176,10 +198,8 @@ extension VoiceBroadcastService { /// - Parameters: /// - success: A closure called when the operation is complete. /// - failure: A closure called when the operation fails. - /// - Returns: a `MXHTTPOperation` instance. - @discardableResult - @objc public func startVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) -> MXHTTPOperation? { - return self.startVoiceBroadcast { response in + @objc public func startVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) { + self.startVoiceBroadcast { response in switch response { case .success(let object): success(object) @@ -193,10 +213,8 @@ extension VoiceBroadcastService { /// - Parameters: /// - success: A closure called when the operation is complete. /// - failure: A closure called when the operation fails. - /// - Returns: a `MXHTTPOperation` instance. - @discardableResult - @objc public func pauseVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) -> MXHTTPOperation? { - return self.pauseVoiceBroadcast { response in + @objc public func pauseVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) { + self.pauseVoiceBroadcast { response in switch response { case .success(let object): success(object) @@ -210,10 +228,8 @@ extension VoiceBroadcastService { /// - Parameters: /// - success: A closure called when the operation is complete. /// - failure: A closure called when the operation fails. - /// - Returns: a `MXHTTPOperation` instance. - @discardableResult - @objc public func resumeVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) -> MXHTTPOperation? { - return self.resumeVoiceBroadcast { response in + @objc public func resumeVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) { + self.resumeVoiceBroadcast { response in switch response { case .success(let object): success(object) @@ -227,10 +243,8 @@ extension VoiceBroadcastService { /// - Parameters: /// - success: A closure called when the operation is complete. /// - failure: A closure called when the operation fails. - /// - Returns: a `MXHTTPOperation` instance. - @discardableResult - @objc public func stopVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) -> MXHTTPOperation? { - return self.stopVoiceBroadcast { response in + @objc public func stopVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) { + self.stopVoiceBroadcast { response in switch response { case .success(let object): success(object) @@ -247,7 +261,7 @@ extension MXRoom { /// Send a voice broadcast to the room. /// - Parameters: /// - localURL: the local filesystem path of the file to send. - /// - voiceBroadcastInfoEventId: The id of the voice broadcast info event. + /// - voiceBroadcastId: The event id of the started voice broadcast info state event /// - mimeType: (optional) the mime type of the file. Defaults to `audio/ogg`. /// - duration: the length of the voice message in milliseconds /// - samples: an array of floating point values normalized to [0, 1] @@ -257,19 +271,15 @@ extension MXRoom { /// - failure: A closure called when the operation fails. /// - Returns: a `MXHTTPOperation` instance. @nonobjc @discardableResult func sendChunkOfVoiceBroadcast(localURL: URL, - voiceBroadcastInfoEventId: String, + voiceBroadcastId: String, mimeType: String?, duration: UInt, - samples: [Float]?, threadId: String? = nil, sequence: UInt, success: @escaping ((String?) -> Void), failure: @escaping ((Error?) -> Void)) -> MXHTTPOperation? { - let boxedSamples = samples?.compactMap { NSNumber(value: $0) } - - guard let relatesTo = MXEventContentRelatesTo(relationType: MXEventRelationTypeReference, - eventId: voiceBroadcastInfoEventId).jsonDictionary() as? [String: Any] else { + eventId: voiceBroadcastId).jsonDictionary() as? [String: Any] else { failure(VoiceBroadcastServiceError.unknown) return nil } @@ -281,7 +291,7 @@ extension MXRoom { VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType: sequenceValue], mimeType: mimeType, duration: duration, - samples: boxedSamples, + samples: nil, threadId: threadId, localEcho: nil, success: success, diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastServiceError.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastServiceError.swift index 55d0820fac..70f3851e09 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastServiceError.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastServiceError.swift @@ -21,6 +21,7 @@ public enum VoiceBroadcastServiceError: Int, Error { case missingUserId case roomNotFound case notStarted + case unexpectedState case unknown } diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 80efe2f996..2a1fe4879f 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -273,7 +273,8 @@ - (NSAttributedString *)unsafeAttributedStringFromEvent:(MXEvent *)event return [self renderString:displayText forEvent:event]; } } else if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) { - MXLogDebug(@"VB incoming build string") + // do not show voice broadcast info in the timeline + return nil; } } diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 3b48903646..1d7f5cf074 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -845,7 +845,7 @@ class NotificationService: UNNotificationServiceExtension { return } - mxRestClient.sendReadReceipt(toRoom: roomId, forEvent: eventId) { response in + mxRestClient.sendReadReceipt(toRoom: roomId, forEvent: eventId, threadId: event.threadId) { response in if response.isSuccess { MXLog.debug("[NotificationService] sendReadReceipt: Read receipt send successfully.") } else if let error = response.error { diff --git a/RiotSwiftUI/Modules/Common/Extensions/View.swift b/RiotSwiftUI/Modules/Common/Extensions/View.swift new file mode 100644 index 0000000000..2ab99f8844 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Extensions/View.swift @@ -0,0 +1,24 @@ +// +// Copyright 2022 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 SwiftUI + +extension View { + func hideKeyboard() { + UIApplication.shared.vc_closeKeyboard() + } +} diff --git a/RiotSwiftUI/Modules/Common/InfoSheet/Coordinator/InfoSheetCoordinator.swift b/RiotSwiftUI/Modules/Common/InfoSheet/Coordinator/InfoSheetCoordinator.swift new file mode 100644 index 0000000000..6fb7fc1103 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/InfoSheet/Coordinator/InfoSheetCoordinator.swift @@ -0,0 +1,91 @@ +// +// 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 CommonKit +import SwiftUI + +struct InfoSheetCoordinatorParameters { + let title: String + let description: String + let action: InfoSheet.Action + let parentSize: CGSize? +} + +final class InfoSheetCoordinator: Coordinator, Presentable { + private let parameters: InfoSheetCoordinatorParameters + private let infoSheetHostingController: UIViewController + private var infoSheetViewModel: InfoSheetViewModelProtocol + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: ((InfoSheetViewModelResult) -> Void)? + + init(parameters: InfoSheetCoordinatorParameters) { + self.parameters = parameters + + let viewModel = InfoSheetViewModel(title: parameters.title, description: parameters.description, action: parameters.action) + let view = InfoSheet(viewModel: viewModel.context) + infoSheetViewModel = viewModel + let controller = VectorHostingController(rootView: view) + infoSheetHostingController = controller + setupPresentation(of: controller) + } + + // MARK: - Public + + func start() { + MXLog.debug("[InfoSheetCoordinator] did start.") + infoSheetViewModel.completion = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[InfoSheetCoordinator] InfoSheetViewModel did complete with result: \(result).") + self.completion?(result) + } + } + + func toPresentable() -> UIViewController { + infoSheetHostingController + } +} + +private extension InfoSheetCoordinator { + // The bottom sheet should be presented with the content intrinsic height as for design requirement + // We can do it easily just on iOS 16+ + func setupPresentation(of viewController: VectorHostingController) { + let cornerRadius: CGFloat = 24 + + guard + #available(iOS 16, *), + let parentSize = parameters.parentSize, + let presentationController = viewController.sheetPresentationController + else { + viewController.bottomSheetPreferences = .init(cornerRadius: cornerRadius) + return + } + + let intrisincSize = viewController.view.systemLayoutSizeFitting(.init(width: parentSize.width, height: 0), + withHorizontalFittingPriority: .defaultHigh, + verticalFittingPriority: .defaultLow) + + presentationController.preferredCornerRadius = cornerRadius + presentationController.prefersGrabberVisible = true + presentationController.detents = [ + .custom { context in + min(context.maximumDetentValue, intrisincSize.height) + }, + .large() + ] + } +} diff --git a/RiotSwiftUI/Modules/Common/InfoSheet/InfoSheetModels.swift b/RiotSwiftUI/Modules/Common/InfoSheet/InfoSheetModels.swift new file mode 100644 index 0000000000..0652bfe83e --- /dev/null +++ b/RiotSwiftUI/Modules/Common/InfoSheet/InfoSheetModels.swift @@ -0,0 +1,33 @@ +// +// 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. +// + +// MARK: View model + +enum InfoSheetViewModelResult { + case actionTriggered +} + +// MARK: View + +struct InfoSheetViewState: BindableState { + let title: String + let description: String + let action: InfoSheet.Action +} + +enum InfoSheetViewAction { + case actionTriggered +} diff --git a/RiotSwiftUI/Modules/Common/InfoSheet/InfoSheetViewModel.swift b/RiotSwiftUI/Modules/Common/InfoSheet/InfoSheetViewModel.swift new file mode 100644 index 0000000000..c586864951 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/InfoSheet/InfoSheetViewModel.swift @@ -0,0 +1,36 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +typealias InfoSheetViewModelType = StateStoreViewModel + +class InfoSheetViewModel: InfoSheetViewModelType, InfoSheetViewModelProtocol { + var completion: ((InfoSheetViewModelResult) -> Void)? + + init(title: String, description: String, action: InfoSheet.Action) { + super.init(initialViewState: InfoSheetViewState(title: title, description: description, action: action)) + } + + // MARK: - Public + + override func process(viewAction: InfoSheetViewAction) { + switch viewAction { + case .actionTriggered: + completion?(.actionTriggered) + } + } +} diff --git a/RiotSwiftUI/Modules/Common/InfoSheet/InfoSheetViewModelProtocol.swift b/RiotSwiftUI/Modules/Common/InfoSheet/InfoSheetViewModelProtocol.swift new file mode 100644 index 0000000000..97bff28470 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/InfoSheet/InfoSheetViewModelProtocol.swift @@ -0,0 +1,22 @@ +// +// 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 InfoSheetViewModelProtocol { + var completion: ((InfoSheetViewModelResult) -> Void)? { get set } + var context: InfoSheetViewModelType.Context { get } +} diff --git a/RiotSwiftUI/Modules/Common/InfoSheet/MockInfoSheetScreenState.swift b/RiotSwiftUI/Modules/Common/InfoSheet/MockInfoSheetScreenState.swift new file mode 100644 index 0000000000..62d86a681a --- /dev/null +++ b/RiotSwiftUI/Modules/Common/InfoSheet/MockInfoSheetScreenState.swift @@ -0,0 +1,57 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +/// Using an enum for the screen allows you define the different state cases with +/// the relevant associated data for each case. +enum MockInfoSheetScreenState: MockScreenState, CaseIterable { + // A case for each state you want to represent + // with specific, minimal associated data that will allow you + // mock that screen. + case sheet(title: String, subtitle: String, action: InfoSheet.Action) + + /// The associated screen + var screenType: Any.Type { + InfoSheet.self + } + + /// A list of screen state definitions + static var allCases: [MockInfoSheetScreenState] { + // Each of the presence statuses + [.sheet(title: VectorL10n.userSessionVerifiedSessionTitle, subtitle: VectorL10n.userSessionVerifiedSessionDescription, action: .init(text: VectorL10n.userSessionGotIt, action: { }))] + } + + /// Generate the view struct for the screen state. + var screenView: ([Any], AnyView) { + let model: (title: String, subtitle: String, action: InfoSheet.Action) + + switch self { + case let .sheet(title, subtitle, action): + model = (title, subtitle, action) + } + let viewModel = InfoSheetViewModel(title: model.title, description: model.subtitle, action: model.action) + + // can simulate service and viewModel actions here if needs be. + + return ( + [model, viewModel], + AnyView(InfoSheet(viewModel: viewModel.context) + .addDependency(MockAvatarService.example)) + ) + } +} diff --git a/RiotSwiftUI/Modules/Common/InfoSheet/View/InfoSheet.swift b/RiotSwiftUI/Modules/Common/InfoSheet/View/InfoSheet.swift new file mode 100644 index 0000000000..86f6cdd5dd --- /dev/null +++ b/RiotSwiftUI/Modules/Common/InfoSheet/View/InfoSheet.swift @@ -0,0 +1,86 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct InfoSheet: View { + struct Action { + let text: String + let action: () -> Void + } + + @Environment(\.theme) var theme: ThemeSwiftUI + private let viewModel: InfoSheetViewModel.Context + + init(viewModel: InfoSheetViewModel.Context) { + self.viewModel = viewModel + } + + var body: some View { + let padding: CGFloat = 16 + VStack(spacing: 24) { + VStack(spacing: 18) { + Text(viewModel.viewState.title) + .font(theme.fonts.headline) + .foregroundColor(theme.colors.primaryContent) + .accessibilityIdentifier(viewModel.viewState.title) + .padding([.leading, .trailing], padding) + + Rectangle() + .foregroundColor(theme.colors.system) + .frame(height: 1) + + Text(viewModel.viewState.description) + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + .accessibilityIdentifier(viewModel.viewState.description) + .padding([.leading, .trailing], padding) + .fixedSize(horizontal: false, vertical: true) + } + .layoutPriority(1) + + Button { + viewModel.viewState.action.action() + viewModel.send(viewAction: .actionTriggered) + } + label: { + Text(viewModel.viewState.action.text) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.background) + .frame(height: 46) + .frame(maxWidth: .infinity) + .accessibilityIdentifier(viewModel.viewState.action.text) + } + .background(theme.colors.accent) + .cornerRadius(8) + .padding([.leading, .trailing], padding) + .frame(maxHeight: .infinity, alignment: .bottom) + } + .padding(.bottom, padding) + .padding(.top, 32) + .frame(maxWidth: .infinity) + .background(theme.colors.background.ignoresSafeArea(edges: .bottom)) + } +} + +// MARK: - Previews + +struct InfoSheet_Previews: PreviewProvider { + static let stateRenderer = MockInfoSheetScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/Common/Util/InlineTextButton.swift b/RiotSwiftUI/Modules/Common/Util/InlineTextButton.swift index e5673b12c4..107b3ee1b2 100644 --- a/RiotSwiftUI/Modules/Common/Util/InlineTextButton.swift +++ b/RiotSwiftUI/Modules/Common/Util/InlineTextButton.swift @@ -39,10 +39,11 @@ struct InlineTextButton: View { /// - mainText: The main text that shouldn't appear tappable. This must contain a single `%@` placeholder somewhere within. /// - tappableText: The tappable text that will be substituted into the `%@` placeholder. /// - action: The action to perform when tapping the button. - internal init(_ mainText: String, tappableText: String, action: @escaping () -> Void) { + /// - alwaysCallAction: If true calls the action on tap action even if the `tappableText` isn't found inside the `mainText` + init(_ mainText: String, tappableText: String, alwaysCallAction: Bool = true, action: @escaping () -> Void) { guard let range = mainText.range(of: "%@") else { components = [StringComponent(string: Substring(mainText), isTinted: false)] - self.action = action + self.action = alwaysCallAction ? action : { } return } @@ -69,8 +70,13 @@ struct InlineTextButton: View { func makeBody(configuration: Configuration) -> some View { components.reduce(Text("")) { lastValue, component in - lastValue + Text(component.string) - .foregroundColor(component.isTinted ? .accentColor.opacity(configuration.isPressed ? 0.2 : 1) : nil) + var text: Text = .init(component.string) + + if component.isTinted { + text = text.foregroundColor(.accentColor.opacity(configuration.isPressed ? 0.2 : 1)) + } + + return lastValue + text } } } diff --git a/RiotSwiftUI/Modules/Common/Util/SearchBar.swift b/RiotSwiftUI/Modules/Common/Util/SearchBar.swift index 63b3863b99..fb05eff952 100644 --- a/RiotSwiftUI/Modules/Common/Util/SearchBar.swift +++ b/RiotSwiftUI/Modules/Common/Util/SearchBar.swift @@ -64,7 +64,7 @@ struct SearchBar: View { Button(action: { self.isEditing = false self.text = "" - UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + self.hideKeyboard() }) { Text(VectorL10n.cancel) .font(theme.fonts.body) diff --git a/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift index dd1b27d3e2..ed10aa7a6d 100644 --- a/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift +++ b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift @@ -33,7 +33,7 @@ import Foundation /// It provides a nice layer of consistency and also safety. As we are not passing the `ViewModel` to the view directly, shortcuts/hacks /// can't be made into the `ViewModel`. @dynamicMemberLookup -class ViewModelContext: ObservableObject { +final class ViewModelContext: ObservableObject { // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListBridgePresenter.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListBridgePresenter.swift index 41b79334d8..6d2a08f0fe 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListBridgePresenter.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListBridgePresenter.swift @@ -18,6 +18,7 @@ import Foundation @objc protocol ComposerCreateActionListBridgePresenterDelegate { func composerCreateActionListBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: ComposerCreateActionListBridgePresenter, action: ComposerCreateAction) + func composerCreateActionListBridgePresenterDelegateDidToggleTextFormatting(_ coordinatorBridgePresenter: ComposerCreateActionListBridgePresenter, enabled: Bool) func composerCreateActionListBridgePresenterDidDismissInteractively(_ coordinatorBridgePresenter: ComposerCreateActionListBridgePresenter) } @@ -34,6 +35,8 @@ final class ComposerCreateActionListBridgePresenter: NSObject { // MARK: Private private let actions: [ComposerCreateAction] + private let wysiwygEnabled: Bool + private let textFormattingEnabled: Bool private var coordinator: ComposerCreateActionListCoordinator? // MARK: Public @@ -42,10 +45,12 @@ final class ComposerCreateActionListBridgePresenter: NSObject { // MARK: - Setup - init(actions: [Int]) { + init(actions: [Int], wysiwygEnabled: Bool, textFormattingEnabled: Bool) { self.actions = actions.compactMap { ComposerCreateAction(rawValue: $0) } + self.wysiwygEnabled = wysiwygEnabled + self.textFormattingEnabled = textFormattingEnabled super.init() } @@ -57,12 +62,16 @@ final class ComposerCreateActionListBridgePresenter: NSObject { // } func present(from viewController: UIViewController, animated: Bool) { - let composerCreateActionListCoordinator = ComposerCreateActionListCoordinator(actions: actions) + let composerCreateActionListCoordinator = ComposerCreateActionListCoordinator(actions: actions, + wysiwygEnabled: wysiwygEnabled, + textFormattingEnabled: textFormattingEnabled) composerCreateActionListCoordinator.callback = { [weak self] action in guard let self = self else { return } switch action { case .done(let composeAction): self.delegate?.composerCreateActionListBridgePresenterDelegateDidComplete(self, action: composeAction) + case .toggleTextFormatting(let enabled): + self.delegate?.composerCreateActionListBridgePresenterDelegateDidToggleTextFormatting(self, enabled: enabled) case .cancel: self.delegate?.composerCreateActionListBridgePresenterDidDismissInteractively(self) } diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift index fcc05c1f2d..cb52281eb8 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift @@ -19,6 +19,7 @@ import SwiftUI /// Actions returned by the coordinator callback enum ComposerCreateActionListCoordinatorAction { case done(ComposerCreateAction) + case toggleTextFormatting(Bool) case cancel } @@ -39,8 +40,11 @@ final class ComposerCreateActionListCoordinator: NSObject, Coordinator, Presenta // MARK: - Setup - init(actions: [ComposerCreateAction]) { - viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: actions)) + init(actions: [ComposerCreateAction], wysiwygEnabled: Bool, textFormattingEnabled: Bool) { + viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState( + actions: actions, + wysiwygEnabled: wysiwygEnabled, + bindings: ComposerCreateActionListBindings(textFormattingEnabled: textFormattingEnabled))) view = ComposerCreateActionList(viewModel: viewModel.context) let hostingVC = VectorHostingController(rootView: view) hostingVC.bottomSheetPreferences = VectorHostingBottomSheetPreferences( @@ -61,6 +65,8 @@ final class ComposerCreateActionListCoordinator: NSObject, Coordinator, Presenta switch result { case .done(let action): self.callback?(.done(action)) + case .toggleTextFormatting(let enabled): + self.callback?(.toggleTextFormatting(enabled)) } } } diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/MockComposerCreateActionListScreenState.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/MockComposerCreateActionListScreenState.swift index 31d5b94872..cb1a53b886 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/MockComposerCreateActionListScreenState.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/MockComposerCreateActionListScreenState.swift @@ -33,7 +33,10 @@ enum MockComposerCreateActionListScreenState: MockScreenState, CaseIterable { case .fullList: actions = ComposerCreateAction.allCases } - let viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: actions)) + let viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState( + actions: actions, + wysiwygEnabled: true, + bindings: ComposerCreateActionListBindings(textFormattingEnabled: true))) return ( [viewModel], diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift index 457cc612af..6c42041b7c 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift @@ -21,11 +21,15 @@ import Foundation enum ComposerCreateActionListViewAction { // The user selected an action case selectAction(ComposerCreateAction) + // The user toggled the text formatting action + case toggleTextFormatting(Bool) } enum ComposerCreateActionListViewModelResult: Equatable { // The user selected an action and is done with the screen case done(ComposerCreateAction) + // The user toggled the text formatting setting but might not be done with the screen + case toggleTextFormatting(Bool) } // MARK: View @@ -33,6 +37,13 @@ enum ComposerCreateActionListViewModelResult: Equatable { struct ComposerCreateActionListViewState: BindableState { /// The list of composer create actions to display to the user let actions: [ComposerCreateAction] + let wysiwygEnabled: Bool + + var bindings: ComposerCreateActionListBindings +} + +struct ComposerCreateActionListBindings { + var textFormattingEnabled: Bool } @objc enum ComposerCreateAction: Int { diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Test/Unit/ComposerCreateActionListTests.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Test/Unit/ComposerCreateActionListTests.swift index 33258467b3..35532a2124 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Test/Unit/ComposerCreateActionListTests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Test/Unit/ComposerCreateActionListTests.swift @@ -23,7 +23,13 @@ class ComposerCreateActionListTests: XCTestCase { var context: ComposerCreateActionListViewModel.Context! override func setUpWithError() throws { - viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState(actions: ComposerCreateAction.allCases)) + viewModel = ComposerCreateActionListViewModel( + initialViewState: ComposerCreateActionListViewState( + actions: ComposerCreateAction.allCases, + wysiwygEnabled: true, + bindings: ComposerCreateActionListBindings(textFormattingEnabled: true) + ) + ) context = viewModel.context } diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift index dbc484372f..7f2733e2b2 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift @@ -22,11 +22,17 @@ struct ComposerCreateActionList: View { // MARK: Private @Environment(\.theme) private var theme: ThemeSwiftUI + + private var textFormattingIcon: String { + viewModel.textFormattingEnabled + ? Asset.Images.actionFormattingEnabled.name + : Asset.Images.actionFormattingDisabled.name + } // MARK: Public @ObservedObject var viewModel: ComposerCreateActionListViewModel.Context - + var body: some View { VStack { VStack(alignment: .leading) { @@ -48,6 +54,29 @@ struct ComposerCreateActionList: View { .padding(.horizontal, 16) .padding(.vertical, 12) } + if viewModel.viewState.wysiwygEnabled { + SeparatorLine() + HStack(spacing: 16) { + Image(textFormattingIcon) + .renderingMode(.template) + .foregroundColor(theme.colors.accent) + Text(VectorL10n.wysiwygComposerStartActionTextFormatting) + .foregroundColor(theme.colors.primaryContent) + .font(theme.fonts.body) + .accessibilityIdentifier("textFormatting") + Spacer() + Toggle("", isOn: $viewModel.textFormattingEnabled) + .toggleStyle(ComposerToggleActionStyle()) + .labelsHidden() + .onChange(of: viewModel.textFormattingEnabled) { isOn in + viewModel.send(viewAction: .toggleTextFormatting(isOn)) + } + } + .contentShape(Rectangle()) + .padding(.horizontal, 16) + .padding(.vertical, 12) + + } } .padding(.top, 8) Spacer() @@ -63,3 +92,35 @@ struct ComposerCreateActionList_Previews: PreviewProvider { stateRenderer.screenGroup() } } + +struct ComposerToggleActionStyle: ToggleStyle { + @Environment(\.theme) private var theme + + func makeBody(configuration: Configuration) -> some View { + HStack { + Rectangle() + .foregroundColor(.clear) + .frame(width: 50, height: 30, alignment: .center) + .overlay( + Rectangle() + .foregroundColor(configuration.isOn + ? theme.colors.accent.opacity(0.5) + : theme.colors.primaryContent.opacity(0.25)) + .cornerRadius(7) + .padding(.all, 8) + ) + .overlay( + Circle() + .foregroundColor(configuration.isOn + ? theme.colors.accent + : theme.colors.background) + .padding(.all, 3) + .offset(x: configuration.isOn ? 11 : -11, y: 0) + .shadow(radius: configuration.isOn ? 0.0 : 2.0) + .animation(Animation.linear(duration: 0.1)) + + ).cornerRadius(20) + .onTapGesture { configuration.isOn.toggle() } + } + } +} diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/ViewModel/ComposerCreateActionListViewModel.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/ViewModel/ComposerCreateActionListViewModel.swift index bd063b1b25..93fa9950bf 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/ViewModel/ComposerCreateActionListViewModel.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/ViewModel/ComposerCreateActionListViewModel.swift @@ -35,6 +35,8 @@ class ComposerCreateActionListViewModel: ComposerCreateActionListViewModelType, switch viewAction { case .selectAction(let action): callback?(.done(action)) + case .toggleTextFormatting(let enabled): + callback?(.toggleTextFormatting(enabled)) } } } diff --git a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift index 48d7df0545..c0602ab035 100644 --- a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift +++ b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift @@ -29,14 +29,15 @@ enum MockComposerScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { let viewModel: ComposerViewModel + let bindings = ComposerBindings(focused: false) switch self { - case .send: viewModel = ComposerViewModel(initialViewState: ComposerViewState()) - case .edit: viewModel = ComposerViewModel(initialViewState: ComposerViewState(sendMode: .edit)) - case .reply: viewModel = ComposerViewModel(initialViewState: ComposerViewState(eventSenderDisplayName: "TestUser", sendMode: .reply)) + case .send: viewModel = ComposerViewModel(initialViewState: ComposerViewState(textFormattingEnabled: true, bindings: bindings)) + case .edit: viewModel = ComposerViewModel(initialViewState: ComposerViewState(sendMode: .edit, textFormattingEnabled: true, bindings: bindings)) + case .reply: viewModel = ComposerViewModel(initialViewState: ComposerViewState(eventSenderDisplayName: "TestUser", sendMode: .reply, textFormattingEnabled: true, bindings: bindings)) } - let wysiwygviewModel = WysiwygComposerViewModel(minHeight: 20, maxHeight: 360) + let wysiwygviewModel = WysiwygComposerViewModel(minHeight: 20, maxCompressedHeight: 360) viewModel.callback = { [weak viewModel, weak wysiwygviewModel] result in guard let viewModel = viewModel else { return } @@ -54,7 +55,11 @@ enum MockComposerScreenState: MockScreenState, CaseIterable { [viewModel, wysiwygviewModel], AnyView(VStack { Spacer() - Composer(viewModel: viewModel.context, wysiwygViewModel: wysiwygviewModel, sendMessageAction: { _ in }, showSendMediaActions: { }) + Composer(viewModel: viewModel.context, + wysiwygViewModel: wysiwygviewModel, + resizeAnimationDuration: 0.1, + sendMessageAction: { _ in }, + showSendMediaActions: { }) }.frame( minWidth: 0, maxWidth: .infinity, diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift index 0f8ad1fdc7..c4293eafcd 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift @@ -19,12 +19,15 @@ import Foundation struct ComposerViewState: BindableState { var eventSenderDisplayName: String? var sendMode: ComposerSendMode = .send + var textFormattingEnabled: Bool var placeholder: String? + + var bindings: ComposerBindings } extension ComposerViewState { var shouldDisplayContext: Bool { - return sendMode == .edit || sendMode == .reply + sendMode == .edit || sendMode == .reply } var contextDescription: String? { @@ -45,3 +48,7 @@ extension ComposerViewState { } } } + +struct ComposerBindings { + var focused: Bool +} diff --git a/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift b/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift index c80bea8191..aae6e1682d 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift @@ -45,6 +45,42 @@ final class ComposerUITests: MockScreenTestCase { XCTAssertTrue(maximiseButton.exists) } + // This test requires "connect hardware keyboard" to be off on the simulator + // And may not work on the CI + func testFastTyping() throws { + app.goToScreenWithIdentifier(MockComposerScreenState.send.title) + let text = "fast typing test" + let wysiwygTextView = app.textViews.allElementsBoundByIndex[0] + XCTAssertTrue(wysiwygTextView.exists) + wysiwygTextView.tap() + sleep(1) + wysiwygTextView.typeText(text) + let options = XCTExpectedFailure.Options() + options.isStrict = false + XCTExpectFailure("Test may fail on CI", options: options) + let value = wysiwygTextView.value as? String + XCTAssert(value == text, "Text view value is: \(value ?? "nil")") + } + + // This test requires "connect hardware keyboard" to be off on the simulator + // And may not work on the CI + func testLongPressDelete() throws { + app.goToScreenWithIdentifier(MockComposerScreenState.send.title) + let text = "test1 test2 test3 test4 test5 test6 test7" + let wysiwygTextView = app.textViews.allElementsBoundByIndex[0] + XCTAssertTrue(wysiwygTextView.exists) + wysiwygTextView.tap() + sleep(1) + wysiwygTextView.typeText(text) + sleep(1) + app.keys["delete"].press(forDuration: 10.0) + let options = XCTExpectedFailure.Options() + options.isStrict = false + XCTExpectFailure("Test may fail on CI", options: options) + let value = wysiwygTextView.value as? String + XCTAssert(value == "", "Text view value is: \(value ?? "nil")") + } + func testReplyMode() throws { app.goToScreenWithIdentifier(MockComposerScreenState.reply.title) diff --git a/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift b/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift index 5f16cfa42f..073c6f357e 100644 --- a/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,8 @@ final class ComposerViewModelTests: XCTestCase { var context: ComposerViewModel.Context! override func setUpWithError() throws { - viewModel = ComposerViewModel(initialViewState: ComposerViewState()) + viewModel = ComposerViewModel(initialViewState: ComposerViewState(textFormattingEnabled: true, + bindings: ComposerBindings(focused: false))) context = viewModel.context } @@ -69,4 +70,10 @@ final class ComposerViewModelTests: XCTestCase { viewModel.placeholder = "Placeholder Test" XCTAssert(context.viewState.placeholder == "Placeholder Test") } + + func testDimissKeyboard() { + viewModel.state.bindings.focused = true + viewModel.dismissKeyboard() + XCTAssert(context.viewState.bindings.focused == false) + } } diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift index 624c846386..f255c25a54 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift @@ -23,14 +23,20 @@ struct Composer: View { // MARK: Private + @ObservedObject private var viewModel: ComposerViewModelType.Context + @ObservedObject private var wysiwygViewModel: WysiwygComposerViewModel + private let resizeAnimationDuration: Double + + private let sendMessageAction: (WysiwygComposerContent) -> Void + private let showSendMediaActions: () -> Void + @Environment(\.theme) private var theme: ThemeSwiftUI - @State private var focused = false @State private var isActionButtonShowing = false private let horizontalPadding: CGFloat = 12 private let borderHeight: CGFloat = 40 - private let minTextViewHeight: CGFloat = 20 + private var minTextViewHeight: CGFloat = 22 private var verticalPadding: CGFloat { (borderHeight - minTextViewHeight) / 2 } @@ -60,69 +66,60 @@ struct Composer: View { } private var borderColor: Color { - focused ? theme.colors.quarterlyContent : theme.colors.quinaryContent + viewModel.focused ? theme.colors.quarterlyContent : theme.colors.quinaryContent } private var formatItems: [FormatItem] { FormatType.allCases.map { type in FormatItem( type: type, - active: wysiwygViewModel.reversedActions.contains(type.composerAction), - disabled: wysiwygViewModel.disabledActions.contains(type.composerAction) + active: wysiwygViewModel.actionStates[type.composerAction] == .reversed, + disabled: wysiwygViewModel.actionStates[type.composerAction] == .disabled ) } } - - // MARK: Public - - @ObservedObject var viewModel: ComposerViewModelType.Context - @ObservedObject var wysiwygViewModel: WysiwygComposerViewModel - - let sendMessageAction: (WysiwygComposerContent) -> Void - let showSendMediaActions: () -> Void - - var body: some View { - VStack(spacing: 8) { - let rect = RoundedRectangle(cornerRadius: cornerRadius) - VStack(spacing: 12) { - if viewModel.viewState.shouldDisplayContext { - HStack { - if let imageName = viewModel.viewState.contextImageName { - Image(imageName) - .foregroundColor(theme.colors.tertiaryContent) - } - if let contextDescription = viewModel.viewState.contextDescription { - Text(contextDescription) - .accessibilityIdentifier("contextDescription") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(theme.colors.secondaryContent) - } - Spacer() - Button { - viewModel.send(viewAction: .cancel) - } label: { - Image(Asset.Images.inputCloseIcon.name) - .foregroundColor(theme.colors.tertiaryContent) - } - .accessibilityIdentifier("cancelButton") + + private var composerContainer: some View { + let rect = RoundedRectangle(cornerRadius: cornerRadius) + return VStack(spacing: 12) { + if viewModel.viewState.shouldDisplayContext { + HStack { + if let imageName = viewModel.viewState.contextImageName { + Image(imageName) + .foregroundColor(theme.colors.tertiaryContent) } - .padding(.top, 8) - .padding(.horizontal, horizontalPadding) + if let contextDescription = viewModel.viewState.contextDescription { + Text(contextDescription) + .accessibilityIdentifier("contextDescription") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(theme.colors.secondaryContent) + } + Spacer() + Button { + viewModel.send(viewAction: .cancel) + } label: { + Image(Asset.Images.inputCloseIcon.name) + .foregroundColor(theme.colors.tertiaryContent) + } + .accessibilityIdentifier("cancelButton") } - HStack(alignment: .top, spacing: 0) { - WysiwygComposerView( - focused: $focused, - content: wysiwygViewModel.content, - replaceText: wysiwygViewModel.replaceText, - select: wysiwygViewModel.select, - didUpdateText: wysiwygViewModel.didUpdateText - ) - .tintColor(theme.colors.accent) - .placeholder(viewModel.viewState.placeholder, color: theme.colors.tertiaryContent) - .frame(height: wysiwygViewModel.idealHeight) - .onAppear { + .padding(.top, 8) + .padding(.horizontal, horizontalPadding) + } + HStack(alignment: .top, spacing: 0) { + WysiwygComposerView( + focused: $viewModel.focused, + viewModel: wysiwygViewModel + ) + .tintColor(theme.colors.accent) + .placeholder(viewModel.viewState.placeholder, color: theme.colors.tertiaryContent) + .frame(height: wysiwygViewModel.idealHeight) + .onAppear { + if wysiwygViewModel.isContentEmpty { wysiwygViewModel.setup() } + } + if viewModel.viewState.textFormattingEnabled { Button { wysiwygViewModel.maximised.toggle() } label: { @@ -135,63 +132,101 @@ struct Composer: View { .padding(.leading, 12) .padding(.trailing, 4) } - .padding(.horizontal, horizontalPadding) - .padding(.top, topPadding) - .padding(.bottom, verticalPadding) } - .clipShape(rect) - .overlay(rect.stroke(borderColor, lineWidth: 1)) - .animation(.easeInOut(duration: 0.1), value: wysiwygViewModel.idealHeight) .padding(.horizontal, horizontalPadding) - .padding(.top, 8) - .onTapGesture { - if !focused { - focused = true - } + .padding(.top, topPadding) + .padding(.bottom, verticalPadding) + } + .clipShape(rect) + .overlay(rect.stroke(borderColor, lineWidth: 1)) + .animation(.easeInOut(duration: resizeAnimationDuration), value: wysiwygViewModel.idealHeight) + .padding(.top, 8) + .onTapGesture { + if viewModel.focused { + viewModel.focused = true } - HStack(spacing: 0) { - Button { - showSendMediaActions() - } label: { - Image(Asset.Images.startComposeModule.name) - .resizable() - .foregroundColor(theme.colors.tertiaryContent) - .frame(width: 14, height: 14) - } - .frame(width: 36, height: 36) - .background(Circle().fill(theme.colors.system)) - .padding(.trailing, 8) - .accessibilityLabel(VectorL10n.create) - FormattingToolbar(formatItems: formatItems) { type in - wysiwygViewModel.apply(type.action) + } + } + + private var sendMediaButton: some View { + return Button { + showSendMediaActions() + } label: { + Image(Asset.Images.startComposeModule.name) + .resizable() + .foregroundColor(theme.colors.tertiaryContent) + .frame(width: 14, height: 14) + } + .frame(width: 36, height: 36) + .background(Circle().fill(theme.colors.system)) + .padding(.trailing, 8) + .accessibilityLabel(VectorL10n.create) + } + + private var sendButton: some View { + return Button { + sendMessageAction(wysiwygViewModel.content) + wysiwygViewModel.clearContent() + } label: { + if viewModel.viewState.sendMode == .edit { + Image(Asset.Images.saveIcon.name) + } else { + Image(Asset.Images.sendIcon.name) + } + } + .frame(width: 36, height: 36) + .padding(.leading, 8) + .isHidden(!isActionButtonShowing) + .accessibilityIdentifier(actionButtonAccessibilityIdentifier) + .accessibilityLabel(VectorL10n.send) + .onChange(of: wysiwygViewModel.isContentEmpty) { isEmpty in + viewModel.send(viewAction: .contentDidChange(isEmpty: isEmpty)) + withAnimation(.easeInOut(duration: 0.15)) { + isActionButtonShowing = !isEmpty + } + } + } + + // MARK: Public + + init( + viewModel: ComposerViewModelType.Context, + wysiwygViewModel: WysiwygComposerViewModel, + resizeAnimationDuration: Double, + sendMessageAction: @escaping (WysiwygComposerContent) -> Void, + showSendMediaActions: @escaping () -> Void) { + self.viewModel = viewModel + self.wysiwygViewModel = wysiwygViewModel + self.resizeAnimationDuration = resizeAnimationDuration + self.sendMessageAction = sendMessageAction + self.showSendMediaActions = showSendMediaActions + } + + var body: some View { + VStack(spacing: 8) { + HStack(alignment: .bottom, spacing: 0) { + if !viewModel.viewState.textFormattingEnabled { + sendMediaButton } - .frame(height: 44) - Spacer() - Button { - sendMessageAction(wysiwygViewModel.content) - wysiwygViewModel.clearContent() - } label: { - if viewModel.viewState.sendMode == .edit { - Image(Asset.Images.saveIcon.name) - } else { - Image(Asset.Images.sendIcon.name) - } + composerContainer + if !viewModel.viewState.textFormattingEnabled { + sendButton } - .frame(width: 36, height: 36) - .padding(.leading, 8) - .isHidden(!isActionButtonShowing) - .accessibilityIdentifier(actionButtonAccessibilityIdentifier) - .accessibilityLabel(VectorL10n.send) - .onChange(of: wysiwygViewModel.isContentEmpty) { isEmpty in - viewModel.send(viewAction: .contentDidChange(isEmpty: isEmpty)) - withAnimation(.easeInOut(duration: 0.15)) { - isActionButtonShowing = !isEmpty + } + if viewModel.viewState.textFormattingEnabled { + HStack(alignment: .center, spacing: 0) { + sendMediaButton + FormattingToolbar(formatItems: formatItems) { type in + wysiwygViewModel.apply(type.action) } + .frame(height: 44) + Spacer() + sendButton } } - .padding(.horizontal, 12) - .padding(.bottom, 4) } + .padding(.horizontal, horizontalPadding) + .padding(.bottom, 4) } } diff --git a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift index 1e44ed0491..8ad3ebd272 100644 --- a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift +++ b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift @@ -35,6 +35,15 @@ final class ComposerViewModel: ComposerViewModelType, ComposerViewModelProtocol state.sendMode = newValue } } + + var textFormattingEnabled: Bool { + get { + state.textFormattingEnabled + } + set { + state.textFormattingEnabled = newValue + } + } var eventSenderDisplayName: String? { get { @@ -64,4 +73,8 @@ final class ComposerViewModel: ComposerViewModelType, ComposerViewModelProtocol callback?(.contentDidChange(isEmpty: isEmpty)) } } + + func dismissKeyboard() { + state.bindings.focused = false + } } diff --git a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift index 70d943dc76..a1674ff4d5 100644 --- a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift @@ -20,6 +20,9 @@ protocol ComposerViewModelProtocol { var context: ComposerViewModelType.Context { get } var callback: ((ComposerViewModelResult) -> Void)? { get set } var sendMode: ComposerSendMode { get set } + var textFormattingEnabled: Bool { get set } var eventSenderDisplayName: String? { get set } var placeholder: String? { get set } + + func dismissKeyboard() } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift index 31fb638490..a11cb3a2e7 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift @@ -16,14 +16,22 @@ import Foundation -class TimelinePollProvider { +@objcMembers +class TimelinePollProvider: NSObject { static let shared = TimelinePollProvider() - var session: MXSession? + var session: MXSession? { + willSet { + guard let currentSession = self.session else { return } + + if currentSession != newValue { + // Clear all stored coordinators on new session + coordinatorsForEventIdentifiers.removeAll() + } + } + } var coordinatorsForEventIdentifiers = [String: TimelinePollCoordinator]() - private init() { } - /// Create or retrieve the poll timeline coordinator for this event and return /// a view to be displayed in the timeline func buildTimelinePollVCForEvent(_ event: MXEvent) -> UIViewController? { @@ -49,4 +57,8 @@ class TimelinePollProvider { func timelinePollCoordinatorForEventIdentifier(_ eventIdentifier: String) -> TimelinePollCoordinator? { coordinatorsForEventIdentifiers[eventIdentifier] } + + func reset() { + coordinatorsForEventIdentifiers.removeAll() + } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift index 4184f0d63e..d353e2f557 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift @@ -48,7 +48,7 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable { let voiceBroadcastAggregator = try VoiceBroadcastAggregator(session: parameters.session, room: parameters.room, voiceBroadcastStartEventId: parameters.voiceBroadcastStartEvent.eventId, voiceBroadcastState: parameters.voiceBroadcastState) - let details = VoiceBroadcastPlaybackDetails(senderDisplayName: parameters.senderDisplayName) + let details = VoiceBroadcastPlaybackDetails(senderDisplayName: parameters.senderDisplayName, avatarData: parameters.room.avatarData) viewModel = VoiceBroadcastPlaybackViewModel(details: details, mediaServiceProvider: VoiceMessageMediaServiceProvider.sharedProvider, cacheManager: VoiceMessageAttachmentCacheManager.sharedManager, @@ -61,7 +61,9 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable { func start() { } func toPresentable() -> UIViewController { - VectorHostingController(rootView: VoiceBroadcastPlaybackView(viewModel: viewModel.context)) + let view = VoiceBroadcastPlaybackView(viewModel: viewModel.context) + .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + return VectorHostingController(rootView: view) } func canEndVoiceBroadcast() -> Bool { @@ -74,4 +76,8 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable { } func endVoiceBroadcast() {} + + func pausePlaying() { + viewModel.context.send(viewAction: .pause) + } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift index 5167a23647..29b6252dfc 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift @@ -16,17 +16,26 @@ import Foundation -class VoiceBroadcastPlaybackProvider { - static let shared = VoiceBroadcastPlaybackProvider() +@objc class VoiceBroadcastPlaybackProvider: NSObject { + @objc static let shared = VoiceBroadcastPlaybackProvider() - var session: MXSession? + var session: MXSession? { + willSet { + guard let currentSession = self.session else { return } + + if currentSession != newValue { + // Clear all stored coordinators on new session + coordinatorsForEventIdentifiers.removeAll() + } + } + } var coordinatorsForEventIdentifiers = [String: VoiceBroadcastPlaybackCoordinator]() - private init() { } + private override init() { } /// Create or retrieve the voiceBroadcast timeline coordinator for this event and return /// a view to be displayed in the timeline - func buildVoiceBroadcastPlaybackVCForEvent(_ event: MXEvent, senderDisplayName: String?) -> UIViewController? { + func buildVoiceBroadcastPlaybackVCForEvent(_ event: MXEvent, senderDisplayName: String?, voiceBroadcastState: String) -> UIViewController? { guard let session = session, let room = session.room(withRoomId: event.roomId) else { return nil } @@ -35,26 +44,10 @@ class VoiceBroadcastPlaybackProvider { return coordinator.toPresentable() } - let dispatchGroup = DispatchGroup() - dispatchGroup.enter() - var voiceBroadcastState = VoiceBroadcastInfo.State.stopped - - room.state { roomState in - if let stateEvent = roomState?.stateEvents(with: .custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType))?.last, - stateEvent.stateKey == event.stateKey, - let voiceBroadcastInfo = VoiceBroadcastInfo(fromJSON: stateEvent.content), - (stateEvent.eventId == event.eventId || voiceBroadcastInfo.eventId == event.eventId), - let state = VoiceBroadcastInfo.State(rawValue: voiceBroadcastInfo.state) { - voiceBroadcastState = state - } - - dispatchGroup.leave() - } - let parameters = VoiceBroadcastPlaybackCoordinatorParameters(session: session, room: room, voiceBroadcastStartEvent: event, - voiceBroadcastState: voiceBroadcastState, + voiceBroadcastState: VoiceBroadcastInfo.State(rawValue: voiceBroadcastState) ?? VoiceBroadcastInfo.State.stopped, senderDisplayName: senderDisplayName) guard let coordinator = try? VoiceBroadcastPlaybackCoordinator(parameters: parameters) else { return nil @@ -70,4 +63,11 @@ class VoiceBroadcastPlaybackProvider { func voiceBroadcastPlaybackCoordinatorForEventIdentifier(_ eventIdentifier: String) -> VoiceBroadcastPlaybackCoordinator? { coordinatorsForEventIdentifiers[eventIdentifier] } + + /// Pause current voice broadcast playback. + @objc public func pausePlaying() { + coordinatorsForEventIdentifiers.forEach { _, coordinator in + coordinator.pausePlaying() + } + } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift index c27da240e6..ff237a320c 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift @@ -26,14 +26,20 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic // MARK: - Properties // MARK: Private - private var voiceBroadcastAggregator: VoiceBroadcastAggregator private let mediaServiceProvider: VoiceMessageMediaServiceProvider private let cacheManager: VoiceMessageAttachmentCacheManager - private var audioPlayer: VoiceMessageAudioPlayer? + private var voiceBroadcastAggregator: VoiceBroadcastAggregator private var voiceBroadcastChunkQueue: [VoiceBroadcastChunk] = [] + private var voiceBroadcastAttachmentCacheManagerLoadResults: [VoiceMessageAttachmentCacheManagerLoadResult] = [] + + private var audioPlayer: VoiceMessageAudioPlayer? + private var displayLink: CADisplayLink! private var isLivePlayback = false + private var acceptProgressUpdates = true + + private var isActuallyPaused: Bool = false // MARK: Public @@ -50,9 +56,14 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic let viewState = VoiceBroadcastPlaybackViewState(details: details, broadcastState: VoiceBroadcastPlaybackViewModel.getBroadcastState(from: voiceBroadcastAggregator.voiceBroadcastState), playbackState: .stopped, - bindings: VoiceBroadcastPlaybackViewStateBindings()) + playingState: VoiceBroadcastPlayingState(duration: Float(voiceBroadcastAggregator.voiceBroadcast.duration)), + bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0)) super.init(initialViewState: viewState) + displayLink = CADisplayLink(target: WeakTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakTarget.triggerSelector) + displayLink.isPaused = true + displayLink.add(to: .current, forMode: .common) + self.voiceBroadcastAggregator.delegate = self } @@ -74,6 +85,8 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic playLive() case .pause: pause() + case .sliderChange(let didChange): + didSliderChanged(didChange) } } @@ -83,6 +96,8 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic /// Listen voice broadcast private func play() { isLivePlayback = false + displayLink.isPaused = false + isActuallyPaused = false if voiceBroadcastAggregator.isStarted == false { // Start the streaming by fetching broadcast chunks @@ -90,16 +105,16 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: Start streaming") state.playbackState = .buffering voiceBroadcastAggregator.start() - } - else if let audioPlayer = audioPlayer { + + updateDuration() + } else if let audioPlayer = audioPlayer { MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: resume") audioPlayer.play() - } - else { + } else { let chunks = voiceBroadcastAggregator.voiceBroadcast.chunks MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: restart from the beginning: \(chunks.count) chunks") - // Reinject all the chuncks we already have and play them + // Reinject all the chunks we already have and play them voiceBroadcastChunkQueue.append(contentsOf: chunks) processPendingVoiceBroadcastChunks() } @@ -112,6 +127,8 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic } isLivePlayback = true + displayLink.isPaused = false + isActuallyPaused = false // Flush the current audio player playlist audioPlayer?.removeAllPlayerItems() @@ -122,22 +139,25 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic MXLog.debug("[VoiceBroadcastPlaybackViewModel] playLive: Start streaming") state.playbackState = .buffering voiceBroadcastAggregator.start() - } - else { + + state.playingState.duration = Float(voiceBroadcastAggregator.voiceBroadcast.duration) + } else { let chunks = voiceBroadcastAggregator.voiceBroadcast.chunks MXLog.debug("[VoiceBroadcastPlaybackViewModel] playLive: restart from the last chunk: \(chunks.count) chunks") - // Reinject all the chuncks we already have and play the last one + // Reinject all the chunks we already have and play the last one voiceBroadcastChunkQueue.append(contentsOf: chunks) processPendingVoiceBroadcastChunksForLivePlayback() } } - /// Stop voice broadcast + /// Pause voice broadcast private func pause() { MXLog.debug("[VoiceBroadcastPlaybackViewModel] pause") isLivePlayback = false + displayLink.isPaused = true + isActuallyPaused = true if let audioPlayer = audioPlayer, audioPlayer.isPlaying { audioPlayer.pause() @@ -147,15 +167,22 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic private func stopIfVoiceBroadcastOver() { MXLog.debug("[VoiceBroadcastPlaybackViewModel] stopIfVoiceBroadcastOver") - // TODO: Check if the broadcast is over before stopping everything + // Check if the broadcast is over before stopping everything // If not, the player should not stopped. The view state must be move to buffering - stop() + // TODO: Define with more accuracy the threshold to detect the end of the playback + let remainingTime = state.playingState.duration - state.bindings.progress + if remainingTime < 500 { + stop() + } else { + state.playbackState = .buffering + } } private func stop() { MXLog.debug("[VoiceBroadcastPlaybackViewModel] stop") isLivePlayback = false + displayLink.isPaused = true // Objects will be released on audioPlayerDidStopPlaying audioPlayer?.stop() @@ -165,9 +192,9 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic // MARK: - Voice broadcast chunks playback /// Start the playback from the beginning or push more chunks to it - private func processPendingVoiceBroadcastChunks() { + private func processPendingVoiceBroadcastChunks(_ time: TimeInterval? = nil) { reorderPendingVoiceBroadcastChunks() - processNextVoiceBroadcastChunk() + processNextVoiceBroadcastChunk(time) } /// Start the playback from the last known chunk @@ -188,7 +215,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic chunks.sorted(by: {$0.sequence < $1.sequence}) } - private func processNextVoiceBroadcastChunk() { + private func processNextVoiceBroadcastChunk(_ time: TimeInterval? = nil) { MXLog.debug("[VoiceBroadcastPlaybackViewModel] processNextVoiceBroadcastChunk: \(voiceBroadcastChunkQueue.count) chunks remaining") guard voiceBroadcastChunkQueue.count > 0 else { @@ -196,6 +223,10 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic return } + if (isActuallyPaused == false && state.playbackState == .paused) || state.playbackState == .stopped { + state.playbackState = .buffering + } + // TODO: Control the download rate to avoid to download all chunk in mass // We could synchronise it with the number of chunks in the player playlist (audioPlayer.playerItems) @@ -210,45 +241,113 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic // TODO: Make sure there has no new incoming chunk that should be before this attachment // Be careful that this new chunk is not older than the chunk being played by the audio player. Else // we will get an unexecpted rewind. - + switch result { - case .success(let result): - guard result.eventIdentifier == chunk.attachment.eventId else { - return - } + case .success(let result): + guard result.eventIdentifier == chunk.attachment.eventId else { + return + } + + self.voiceBroadcastAttachmentCacheManagerLoadResults.append(result) + + if let audioPlayer = self.audioPlayer { + // Append the chunk to the current playlist + audioPlayer.addContentFromURL(result.url) - if let audioPlayer = self.audioPlayer { - // Append the chunk to the current playlist - audioPlayer.addContentFromURL(result.url) - - // Resume the player. Needed after a pause - if audioPlayer.isPlaying == false { - MXLog.debug("[VoiceBroadcastPlaybackViewModel] processNextVoiceBroadcastChunk: Resume the player") - audioPlayer.play() - } - } - else { - // Init and start the player on the first chunk - let audioPlayer = self.mediaServiceProvider.audioPlayerForIdentifier(result.eventIdentifier) - audioPlayer.registerDelegate(self) - - audioPlayer.loadContentFromURL(result.url, displayName: chunk.attachment.originalFileName) + // Resume the player. Needed after a buffering + if audioPlayer.isPlaying == false && self.state.playbackState == .buffering { + MXLog.debug("[VoiceBroadcastPlaybackViewModel] processNextVoiceBroadcastChunk: Resume the player") + self.displayLink.isPaused = false audioPlayer.play() - self.audioPlayer = audioPlayer + if let time = time { + audioPlayer.seekToTime(time) + } } - - case .failure (let error): - MXLog.error("[VoiceBroadcastPlaybackViewModel] processVoiceBroadcastChunkQueue: loadAttachment error", context: error) - if self.voiceBroadcastChunkQueue.count == 0 { - // No more chunk to try. Go to error - self.state.playbackState = .error + } else { + // Init and start the player on the first chunk + let audioPlayer = self.mediaServiceProvider.audioPlayerForIdentifier(result.eventIdentifier) + audioPlayer.registerDelegate(self) + + audioPlayer.loadContentFromURL(result.url, displayName: chunk.attachment.originalFileName) + self.displayLink.isPaused = false + audioPlayer.play() + if let time = time { + audioPlayer.seekToTime(time) } + self.audioPlayer = audioPlayer + } + + case .failure (let error): + MXLog.error("[VoiceBroadcastPlaybackViewModel] processVoiceBroadcastChunkQueue: loadAttachment error", context: error) + if self.voiceBroadcastChunkQueue.count == 0 { + // No more chunk to try. Go to error + self.state.playbackState = .error + } } self.processNextVoiceBroadcastChunk() } } + private func updateDuration() { + let duration = voiceBroadcastAggregator.voiceBroadcast.duration + let time = TimeInterval(duration / 1000) + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .abbreviated + + state.playingState.duration = Float(duration) + state.playingState.durationLabel = formatter.string(from: time) + } + + private func didSliderChanged(_ didChange: Bool) { + acceptProgressUpdates = !didChange + if didChange { + audioPlayer?.pause() + displayLink.isPaused = true + } else { + // Flush the current audio player playlist + audioPlayer?.removeAllPlayerItems() + + let chunks = reorderVoiceBroadcastChunks(chunks: Array(voiceBroadcastAggregator.voiceBroadcast.chunks)) + + // Reinject the chunks we need and play them + let remainingTime = state.playingState.duration - state.bindings.progress + var chunksDuration: UInt = 0 + for chunk in chunks.reversed() { + chunksDuration += chunk.duration + voiceBroadcastChunkQueue.append(chunk) + if Float(chunksDuration) >= remainingTime { + break + } + } + + MXLog.debug("[VoiceBroadcastPlaybackViewModel] didSliderChanged: restart to time: \(state.bindings.progress) milliseconds") + let time = state.bindings.progress - state.playingState.duration + Float(chunksDuration) + processPendingVoiceBroadcastChunks(TimeInterval(time / 1000)) + } + } + + @objc private func handleDisplayLinkTick() { + updateUI() + } + + private func updateUI() { + guard let playingEventId = voiceBroadcastAttachmentCacheManagerLoadResults.first(where: { result in + result.url == audioPlayer?.currentUrl + })?.eventIdentifier, + let playingSequence = voiceBroadcastAggregator.voiceBroadcast.chunks.first(where: { chunk in + chunk.attachment.eventId == playingEventId + })?.sequence else { + return + } + + let progress = Double(voiceBroadcastAggregator.voiceBroadcast.chunks.filter { chunk in + chunk.sequence < playingSequence + }.reduce(0) { $0 + $1.duration}) + (audioPlayer?.currentTime.rounded() ?? 0) * 1000 + + state.bindings.progress = Float(progress) + } + private static func getBroadcastState(from state: VoiceBroadcastInfo.State) -> VoiceBroadcastState { var broadcastState: VoiceBroadcastState switch state { @@ -288,11 +387,10 @@ extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate { func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) { if isLivePlayback && state.playbackState == .buffering { - // We started directly with a live playback but there was no known chuncks at that time + // We started directly with a live playback but there was no known chunks at that time // These are the first chunks we get. Start the playback on the latest one processPendingVoiceBroadcastChunksForLivePlayback() - } - else { + } else { processPendingVoiceBroadcastChunks() } } @@ -307,8 +405,7 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate { func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) { if isLivePlayback { state.playbackState = .playingLive - } - else { + } else { state.playbackState = .playing } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift index 04ade8a77d..fb2da1ddf4 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift @@ -45,33 +45,49 @@ struct VoiceBroadcastPlaybackView: View { var body: some View { let details = viewModel.viewState.details - VStack(alignment: .center, spacing: 16.0) { + VStack(alignment: .center) { - HStack { - Text(details.senderDisplayName ?? "") - //Text(VectorL10n.voiceBroadcastInTimelineTitle) - .font(theme.fonts.bodySB) - .foregroundColor(theme.colors.primaryContent) + HStack (alignment: .top) { + AvatarImage(avatarData: viewModel.viewState.details.avatarData, size: .xSmall) + + VStack(alignment: .leading, spacing: 0) { + Text(details.avatarData.displayName ?? details.avatarData.matrixItemId) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) + Label { + Text(details.senderDisplayName ?? details.avatarData.matrixItemId) + .foregroundColor(theme.colors.secondaryContent) + .font(theme.fonts.caption1) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastTileMic.image) + } + Label { + Text(VectorL10n.voiceBroadcastTile) + .foregroundColor(theme.colors.secondaryContent) + .font(theme.fonts.caption1) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastTileLive.image) + } + }.frame(maxWidth: .infinity, alignment: .leading) if viewModel.viewState.broadcastState == .live { Button { viewModel.send(viewAction: .playLive) } label: { - HStack { - Image(uiImage: Asset.Images.voiceBroadcastLive.image) - .renderingMode(.original) - Text("Live") - .font(theme.fonts.bodySB) + Label { + Text(VectorL10n.voiceBroadcastLive) + .font(theme.fonts.caption1SB) .foregroundColor(Color.white) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastLive.image) } - } - .padding(5.0) - .background(RoundedRectangle(cornerRadius: 4, style: .continuous) - .fill(backgroundColor)) + .padding(.horizontal, 5) + .background(RoundedRectangle(cornerRadius: 4, style: .continuous).fill(backgroundColor)) .accessibilityIdentifier("liveButton") } } - + .frame(maxWidth: .infinity, alignment: .leading) + if viewModel.viewState.playbackState == .error { VoiceBroadcastPlaybackErrorView() } else { @@ -101,13 +117,19 @@ struct VoiceBroadcastPlaybackView: View { } .activityIndicator(show: viewModel.viewState.playbackState == .buffering) } - + + Slider(value: $viewModel.progress, in: 0...viewModel.viewState.playingState.duration) { + Text("Slider") + } minimumValueLabel: { + Text("") + } maximumValueLabel: { + Text(viewModel.viewState.playingState.durationLabel ?? "").font(.body) + } onEditingChanged: { didChange in + viewModel.send(viewAction: .sliderChange(didChange: didChange)) + } } .padding([.horizontal, .top], 2.0) .padding([.bottom]) - .alert(item: $viewModel.alertInfo) { info in - info.alert - } } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift index 09a12b87d1..c9133f68eb 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift @@ -21,6 +21,7 @@ enum VoiceBroadcastPlaybackViewAction { case play case playLive case pause + case sliderChange(didChange: Bool) } enum VoiceBroadcastPlaybackState { @@ -34,6 +35,7 @@ enum VoiceBroadcastPlaybackState { struct VoiceBroadcastPlaybackDetails { let senderDisplayName: String? + let avatarData: AvatarInputProtocol } enum VoiceBroadcastState { @@ -43,20 +45,20 @@ enum VoiceBroadcastState { case paused } +struct VoiceBroadcastPlayingState { + var duration: Float + var durationLabel: String? +} + struct VoiceBroadcastPlaybackViewState: BindableState { var details: VoiceBroadcastPlaybackDetails var broadcastState: VoiceBroadcastState var playbackState: VoiceBroadcastPlaybackState + var playingState: VoiceBroadcastPlayingState var bindings: VoiceBroadcastPlaybackViewStateBindings } struct VoiceBroadcastPlaybackViewStateBindings { - // TODO: Neeeded? - var alertInfo: AlertInfo? -} - -enum VoiceBroadcastPlaybackAlertType { - // TODO: What is it? - case failedClosingVoiceBroadcast + var progress: Float } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift index 72a15185f3..4159d9aa7a 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift @@ -42,8 +42,8 @@ enum MockVoiceBroadcastPlaybackScreenState: MockScreenState, CaseIterable { /// Generate the view struct for the screen state. var screenView: ([Any], AnyView) { - let details = VoiceBroadcastPlaybackDetails(senderDisplayName: "Alice") - let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .live, playbackState: .stopped, bindings: VoiceBroadcastPlaybackViewStateBindings())) + let details = VoiceBroadcastPlaybackDetails(senderDisplayName: "Alice", avatarData: AvatarInput(mxContentUri: "", matrixItemId: "!fakeroomid:matrix.org", displayName: "The name of the room")) + let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .live, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: 10.0), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0))) return ( [false, viewModel], diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift index c13524e13c..e5e0afe3c6 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift @@ -45,7 +45,7 @@ final class VoiceBroadcastRecorderCoordinator: Coordinator, Presentable { voiceBroadcastRecorderService = VoiceBroadcastRecorderService(session: parameters.session, roomId: parameters.room.matrixItemId) - let details = VoiceBroadcastRecorderDetails(senderDisplayName: parameters.senderDisplayName) + let details = VoiceBroadcastRecorderDetails(senderDisplayName: parameters.senderDisplayName, avatarData: parameters.room.avatarData) let viewModel = VoiceBroadcastRecorderViewModel(details: details, recorderService: voiceBroadcastRecorderService) voiceBroadcastRecorderViewModel = viewModel @@ -56,7 +56,9 @@ final class VoiceBroadcastRecorderCoordinator: Coordinator, Presentable { func start() { } func toPresentable() -> UIViewController { - VectorHostingController(rootView: VoiceBroadcastRecorderView(viewModel: voiceBroadcastRecorderViewModel.context)) + let view = VoiceBroadcastRecorderView(viewModel: voiceBroadcastRecorderViewModel.context) + .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + return VectorHostingController(rootView: view) } func pauseRecording() { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift index c7bc2b1a06..3db5cad549 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift @@ -23,7 +23,16 @@ import Foundation // MARK: - Properties // MARK: Public - var session: MXSession? + var session: MXSession? { + willSet { + guard let currentSession = self.session else { return } + + if currentSession != newValue { + // Clear all stored coordinators on new session + coordinatorsForEventIdentifiers.removeAll() + } + } + } var coordinatorsForEventIdentifiers = [String: VoiceBroadcastRecorderCoordinator]() // MARK: Private diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift index d75f698306..f2e28e5da9 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift @@ -33,7 +33,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { private var chunkFile: AVAudioFile! = nil private var chunkFrames: AVAudioFrameCount = 0 - private var chunkFileNumber: Int = 1 + private var chunkFileNumber: Int = 0 // MARK: Public @@ -49,28 +49,38 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { // MARK: - VoiceBroadcastRecorderServiceProtocol func startRecordingVoiceBroadcast() { - let inputNode = audioEngine.inputNode + do { + try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default) + try AVAudioSession.sharedInstance().setActive(true) + + let inputNode = audioEngine.inputNode - let inputFormat = inputNode.inputFormat(forBus: audioNodeBus) - MXLog.debug("[VoiceBroadcastRecorderService] Start recording voice broadcast for bus name : \(String(describing: inputNode.name(forInputBus: audioNodeBus)))") + let inputFormat = inputNode.inputFormat(forBus: audioNodeBus) + MXLog.debug("[VoiceBroadcastRecorderService] Start recording voice broadcast for bus name : \(String(describing: inputNode.name(forInputBus: audioNodeBus)))") - inputNode.installTap(onBus: audioNodeBus, - bufferSize: 512, - format: inputFormat) { (buffer, time) -> Void in - DispatchQueue.main.async { - self.writeBuffer(buffer) + inputNode.installTap(onBus: audioNodeBus, + bufferSize: 512, + format: inputFormat) { (buffer, time) -> Void in + DispatchQueue.main.async { + self.writeBuffer(buffer) + } } - } - try? audioEngine.start() + try audioEngine.start() + + // Disable the sleep mode during the recording until we are able to handle it + UIApplication.shared.isIdleTimerDisabled = true + } catch { + MXLog.debug("[VoiceBroadcastRecorderService] startRecordingVoiceBroadcast error", context: error) + stopRecordingVoiceBroadcast() + } } func stopRecordingVoiceBroadcast() { MXLog.debug("[VoiceBroadcastRecorderService] Stop recording voice broadcast") audioEngine.stop() audioEngine.inputNode.removeTap(onBus: audioNodeBus) - - resetValues() + UIApplication.shared.isIdleTimerDisabled = false voiceBroadcastService?.stopVoiceBroadcast(success: { [weak self] _ in MXLog.debug("[VoiceBroadcastRecorderService] Stopped") @@ -82,25 +92,33 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { // Send current chunk if self.chunkFile != nil { - self.sendChunkFile(at: self.chunkFile.url, sequence: self.chunkFileNumber) + self.sendChunkFile(at: self.chunkFile.url, sequence: self.chunkFileNumber) { + self.tearDownVoiceBroadcastService() + } + } else { + self.tearDownVoiceBroadcastService() } - - self.session.tearDownVoiceBroadcastService() }, failure: { error in MXLog.error("[VoiceBroadcastRecorderService] Failed to stop voice broadcast", context: error) + // Discard the service on VoiceBroadcastService error. We keep the service in case of other error type + if error as? VoiceBroadcastServiceError != nil { + self.tearDownVoiceBroadcastService() + } }) } func pauseRecordingVoiceBroadcast() { audioEngine.pause() + UIApplication.shared.isIdleTimerDisabled = false voiceBroadcastService?.pauseVoiceBroadcast(success: { [weak self] _ in guard let self = self else { return } // Send current chunk - self.sendChunkFile(at: self.chunkFile.url, sequence: self.chunkFileNumber) - self.chunkFile = nil - + if self.chunkFile != nil { + self.sendChunkFile(at: self.chunkFile.url, sequence: self.chunkFileNumber) + self.chunkFile = nil + } }, failure: { error in MXLog.error("[VoiceBroadcastRecorderService] Failed to pause voice broadcast", context: error) }) @@ -113,7 +131,8 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { guard let self = self else { return } // Update state - self.serviceDelegate?.voiceBroadcastRecorderService(self, didUpdateState: .started) + self.serviceDelegate?.voiceBroadcastRecorderService(self, didUpdateState: .resumed) + UIApplication.shared.isIdleTimerDisabled = true }, failure: { error in MXLog.error("[VoiceBroadcastRecorderService] Failed to resume voice broadcast", context: error) }) @@ -123,7 +142,19 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { /// Reset chunk values. private func resetValues() { chunkFrames = 0 - chunkFileNumber = 1 + chunkFileNumber = 0 + } + + /// Release the service + private func tearDownVoiceBroadcastService() { + resetValues() + session.tearDownVoiceBroadcastService() + + do { + try AVAudioSession.sharedInstance().setActive(false) + } catch { + MXLog.error("[VoiceBroadcastRecorderService] tearDownVoiceBroadcastService error", context: error) + } } /// Write audio buffer to chunk file. @@ -150,6 +181,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { // FIXME: Manage error return } + chunkFileNumber += 1 let temporaryFileName = "VoiceBroadcastChunk-\(roomId)-\(chunkFileNumber)" let fileUrl = directory .appendingPathComponent(temporaryFileName) @@ -165,18 +197,20 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { chunkFile = try? AVAudioFile(forWriting: fileUrl, settings: settings) if chunkFile != nil { - chunkFileNumber += 1 chunkFrames = 0 } else { + chunkFileNumber -= 1 stopRecordingVoiceBroadcast() // FIXME: Manage error ? } } /// Send chunk file to the server. - private func sendChunkFile(at url: URL, sequence: Int) { - guard let voiceBroadcastService = voiceBroadcastService else { + private func sendChunkFile(at url: URL, sequence: Int, completion: (() -> Void)? = nil) { + guard voiceBroadcastService != nil else { // FIXME: Manage error + MXLog.debug("[VoiceBroadcastRecorderService] sendChunkFile: service is not available") + completion?() return } @@ -200,21 +234,29 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { } convertAACToM4A(at: url) { [weak self] convertedUrl in - guard let self = self else { return } + guard let self = self else { + completion?() + return + } + + // Delete the source file. + self.deleteRecording(at: url) if let convertedUrl = convertedUrl { dispatchGroup.notify(queue: .main) { self.voiceBroadcastService?.sendChunkOfVoiceBroadcast(audioFileLocalURL: convertedUrl, mimeType: "audio/mp4", duration: UInt(duration * 1000), - samples: nil, sequence: UInt(sequence)) { eventId in MXLog.debug("[VoiceBroadcastRecorderService] Send voice broadcast chunk with success.") - if eventId != nil { - self.deleteRecording(at: url) - } + self.deleteRecording(at: convertedUrl) + completion?() } failure: { error in MXLog.error("[VoiceBroadcastRecorderService] Failed to send voice broadcast chunk.", context: error) + // Do not delete the file to be sent if request failed, the retry flow will need it + // There's no manual mechanism to clean it up afterwards but the tmp folder + // they live in will eventually be deleted by the system + completion?() } } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift index 71fb41cc11..411ce0333b 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift @@ -23,6 +23,13 @@ struct VoiceBroadcastRecorderView: View { @Environment(\.theme) private var theme: ThemeSwiftUI + private var backgroundColor: Color { + if viewModel.viewState.recordingState != .paused { + return theme.colors.alert + } + return theme.colors.quarterlyContent + } + // MARK: Public @ObservedObject var viewModel: VoiceBroadcastRecorderViewModel.Context @@ -30,10 +37,35 @@ struct VoiceBroadcastRecorderView: View { var body: some View { let details = viewModel.viewState.details - VStack(alignment: .leading, spacing: 16.0) { - Text(details.senderDisplayName ?? "") - .font(theme.fonts.bodySB) - .foregroundColor(theme.colors.primaryContent) + VStack(alignment: .center) { + + HStack(alignment: .top) { + AvatarImage(avatarData: viewModel.viewState.details.avatarData, size: .xSmall) + + VStack(alignment: .leading, spacing: 0) { + Text(details.avatarData.displayName ?? details.avatarData.matrixItemId) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) + Label { + Text(VectorL10n.voiceBroadcastTile) + .foregroundColor(theme.colors.secondaryContent) + .font(theme.fonts.caption1) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastTileLive.image) + } + }.frame(maxWidth: .infinity, alignment: .leading) + + Label { + Text(VectorL10n.voiceBroadcastLive) + .font(theme.fonts.caption1SB) + .foregroundColor(Color.white) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastLive.image) + } + .padding(.horizontal, 5) + .background(RoundedRectangle(cornerRadius: 4, style: .continuous).fill(backgroundColor)) + .accessibilityIdentifier("liveButton") + } HStack(alignment: .top, spacing: 16.0) { Button { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift index b88021bfea..7a2566aad7 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift @@ -32,6 +32,7 @@ enum VoiceBroadcastRecorderState { struct VoiceBroadcastRecorderDetails { let senderDisplayName: String? + let avatarData: AvatarInputProtocol } struct VoiceBroadcastRecorderViewState: BindableState { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift index baa9488f42..bc915d36a3 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift @@ -31,7 +31,7 @@ enum MockVoiceBroadcastRecorderScreenState: MockScreenState, CaseIterable { } var screenView: ([Any], AnyView) { - let details = VoiceBroadcastRecorderDetails(senderDisplayName: "") + let details = VoiceBroadcastRecorderDetails(senderDisplayName: "", avatarData: AvatarInput(mxContentUri: "", matrixItemId: "!fakeroomid:matrix.org", displayName: "The name of the room")) let viewModel = MockVoiceBroadcastRecorderViewModel(initialViewState: VoiceBroadcastRecorderViewState(details: details, recordingState: .started, bindings: VoiceBroadcastRecorderViewStateBindings())) return ( diff --git a/RiotSwiftUI/Modules/UserSessions/Common/Service/MatrixSDK/RiotSettings+ UserSessionSettingsProtocol.swift b/RiotSwiftUI/Modules/UserSessions/Common/Service/MatrixSDK/RiotSettings+ UserSessionSettingsProtocol.swift new file mode 100644 index 0000000000..2df7826577 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/Common/Service/MatrixSDK/RiotSettings+ UserSessionSettingsProtocol.swift @@ -0,0 +1,28 @@ +// +// Copyright 2022 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 Combine + +extension RiotSettings: UserSessionSettingsProtocol { + var showIPAddressesInSessionsManagerPublisher: AnyPublisher { + NotificationCenter.default + .publisher(for: .userDefaultValueUpdated) + .compactMap { $0.object as? String } + .filter { $0 == RiotSettings.UserDefaultsKeys.showIPAddressesInSessionsManager } + .map { _ in RiotSettings.shared.showIPAddressesInSessionsManager } + .eraseToAnyPublisher() + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/Common/Service/Mock/MockUserSessionSettings.swift b/RiotSwiftUI/Modules/UserSessions/Common/Service/Mock/MockUserSessionSettings.swift new file mode 100644 index 0000000000..addec40a40 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/Common/Service/Mock/MockUserSessionSettings.swift @@ -0,0 +1,25 @@ +// +// Copyright 2022 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 Combine + +final class MockUserSessionSettings: UserSessionSettingsProtocol { + var showIPAddressesInSessionsManager: Bool = false + + var showIPAddressesInSessionsManagerPublisher: AnyPublisher { + Just(showIPAddressesInSessionsManager).eraseToAnyPublisher() + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/Common/Service/UserSessionSettingsProtocol.swift b/RiotSwiftUI/Modules/UserSessions/Common/Service/UserSessionSettingsProtocol.swift new file mode 100644 index 0000000000..9e9a4d9ea7 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/Common/Service/UserSessionSettingsProtocol.swift @@ -0,0 +1,23 @@ +// +// Copyright 2022 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 Combine + +protocol UserSessionSettingsProtocol: AnyObject { + var showIPAddressesInSessionsManager: Bool { get set } + var showIPAddressesInSessionsManagerPublisher: AnyPublisher { get } +} + diff --git a/RiotTests/UserAgentParserTests.swift b/RiotSwiftUI/Modules/UserSessions/Common/Test/Unit/UserAgentParserTests.swift similarity index 99% rename from RiotTests/UserAgentParserTests.swift rename to RiotSwiftUI/Modules/UserSessions/Common/Test/Unit/UserAgentParserTests.swift index e7704b3dea..7d47666037 100644 --- a/RiotTests/UserAgentParserTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/Common/Test/Unit/UserAgentParserTests.swift @@ -14,11 +14,10 @@ // limitations under the License. // +@testable import RiotSwiftUI import XCTest -@testable import Element class UserAgentParserTests: XCTestCase { - func testAndroidUserAgents() throws { let uaStrings = [ // New User Agent Implementation @@ -182,7 +181,7 @@ class UserAgentParserTests: XCTestCase { "Element/1.9.9; iOS", "Element/1.9.7 Android", "some random string", - "Element/1.9.9; iOS ", + "Element/1.9.9; iOS " ] let userAgents = uaStrings.map { UserAgentParser.parse($0) } @@ -200,5 +199,4 @@ class UserAgentParserTests: XCTestCase { XCTAssertEqual(userAgents, expected) } - } diff --git a/RiotSwiftUI/Modules/UserSessions/Common/View/DestructiveButton.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/DestructiveButton.swift new file mode 100644 index 0000000000..ccbfceab98 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/Common/View/DestructiveButton.swift @@ -0,0 +1,30 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct DestructiveButton