diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 24e8ab2e6..46a6fab45 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -43,10 +43,12 @@ jobs: # Common setup # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job + - name: Brew bundle + run: brew bundle - name: Bundle install run: | bundle config path vendor/bundle - bundle install --jobs 4 --retry 3 + bundle install --jobs 4 --retry 3 - name: Use right MatrixSDK versions run: bundle exec fastlane point_dependencies_to_related_branches diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 00d4fa2f9..fa3af8d1d 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -48,10 +48,12 @@ jobs: # Common setup # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job + - name: Brew bundle + run: brew bundle - name: Bundle install run: | bundle config path vendor/bundle - bundle install --jobs 4 --retry 3 + bundle install --jobs 4 --retry 3 - name: Use right MatrixSDK versions run: bundle exec fastlane point_dependencies_to_related_branches diff --git a/.github/workflows/ci-ui-tests.yml b/.github/workflows/ci-ui-tests.yml new file mode 100644 index 000000000..fb65d471d --- /dev/null +++ b/.github/workflows/ci-ui-tests.yml @@ -0,0 +1,59 @@ +name: UI Tests CI + +on: + # Triggers the workflow on any pull request + pull_request: + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +env: + # Make the git branch for a PR available to our Fastfile + MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + +jobs: + tests: + name: UI Tests + runs-on: macos-11 + + concurrency: + # Only allow a single run of this workflow on each branch, automatically cancelling older runs. + group: ui-tests-${{ github.head_ref }} + cancel-in-progress: true + + steps: + - uses: actions/checkout@v2 + + # Common cache + # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job + - uses: actions/cache@v2 + with: + path: Pods + key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-pods- + - uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems- + + # Make sure we use the latest version of MatrixSDK + - name: Reset MatrixSDK pod + run: rm -rf Pods/MatrixSDK + + # Common setup + # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job + - name: Brew bundle + run: brew bundle + - name: Bundle install + run: | + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + - name: Use right MatrixSDK versions + run: bundle exec fastlane point_dependencies_to_related_branches + + # Main step + - name: UI tests + run: bundle exec fastlane uitest diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml index c952f6a4a..d8222469f 100644 --- a/.github/workflows/release-alpha.yml +++ b/.github/workflows/release-alpha.yml @@ -25,7 +25,7 @@ jobs: if: "${{ env.P12_KEY != '' || env.P12_PASSWORD_KEY != '' }}" run: echo "::set-output name=defined::true" build: - # Run job if secrets are avilable (not avaiable for forks). + # Run job if secrets are available (not available for forks). needs: [check-secret] if: needs.check-secret.outputs.out-key == 'true' name: Release @@ -63,11 +63,12 @@ jobs: # Common setup # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job + - name: Brew bundle + run: brew bundle - name: Bundle install run: | bundle config path vendor/bundle bundle install --jobs 4 --retry 3 - - name: Use right MatrixSDK versions run: bundle exec fastlane point_dependencies_to_related_branches @@ -84,7 +85,7 @@ jobs: - name: Build Ad-hoc release and send it to Diawi run: bundle exec fastlane alpha env: - # Automaticaly bypass 2FA upgrade if possible on Apple account. + # Automatically bypass 2FA upgrade if possible on Apple account. SPACESHIP_SKIP_2FA_UPGRADE: true APPLE_ID: ${{ secrets.FASTLANE_USER }} FASTLANE_USER: ${{ secrets.FASTLANE_USER }} diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml index d1e110540..994a13941 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-move-labelled.yml @@ -11,7 +11,6 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'A-Maths') || contains(github.event.issue.labels.*.name, 'A-Message-Pinning') || - contains(github.event.issue.labels.*.name, 'A-Threads') || contains(github.event.issue.labels.*.name, 'A-Polls') || contains(github.event.issue.labels.*.name, 'A-Location-Sharing') || contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') || diff --git a/Brewfile b/Brewfile new file mode 100644 index 000000000..f3c727fda --- /dev/null +++ b/Brewfile @@ -0,0 +1,2 @@ +brew "xcodegen" +brew "mint" diff --git a/Brewfile.lock.json b/Brewfile.lock.json new file mode 100644 index 000000000..d4b09dac7 --- /dev/null +++ b/Brewfile.lock.json @@ -0,0 +1,91 @@ +{ + "entries": { + "brew": { + "xcodegen": { + "version": "2.28.0", + "bottle": { + "rebuild": 0, + "root_url": "https://ghcr.io/v2/homebrew/core", + "files": { + "arm64_monterey": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/xcodegen/blobs/sha256:fa493f26e65f0bb0c6be559a395efb84009d842d55035c8fcfd7bcc35096fd17", + "sha256": "fa493f26e65f0bb0c6be559a395efb84009d842d55035c8fcfd7bcc35096fd17" + }, + "arm64_big_sur": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/xcodegen/blobs/sha256:54ce7cba17293f6eabd644af8c8855ca5ac46f4e8475e5a1a961053d31061210", + "sha256": "54ce7cba17293f6eabd644af8c8855ca5ac46f4e8475e5a1a961053d31061210" + }, + "monterey": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/xcodegen/blobs/sha256:261df12ea22b281c6683113755a95887213aec51a345851d3671eec6a82dd169", + "sha256": "261df12ea22b281c6683113755a95887213aec51a345851d3671eec6a82dd169" + }, + "big_sur": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/xcodegen/blobs/sha256:24936b2f648842c026cc0da57ac4d2a04669a1bd459af06d20acfbfa3a1c33da", + "sha256": "24936b2f648842c026cc0da57ac4d2a04669a1bd459af06d20acfbfa3a1c33da" + }, + "catalina": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/xcodegen/blobs/sha256:d7b4298a5833f5c2abaa8f19cd60f1da2f13379b9534d67746ac5a37b754e1bd", + "sha256": "d7b4298a5833f5c2abaa8f19cd60f1da2f13379b9534d67746ac5a37b754e1bd" + } + } + } + }, + "mint": { + "version": "0.17.1", + "bottle": { + "rebuild": 0, + "root_url": "https://ghcr.io/v2/homebrew/core", + "files": { + "arm64_monterey": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/mint/blobs/sha256:1c0ec84137dd50cf949a68e1b8d3729956e2843e1cc48c6827d26e6d7dbc74fc", + "sha256": "1c0ec84137dd50cf949a68e1b8d3729956e2843e1cc48c6827d26e6d7dbc74fc" + }, + "arm64_big_sur": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/mint/blobs/sha256:c57aaea4b6eb863ef946bafe3a77f3d32ad4e10e05876b7c6b2df8f8b9656f4e", + "sha256": "c57aaea4b6eb863ef946bafe3a77f3d32ad4e10e05876b7c6b2df8f8b9656f4e" + }, + "monterey": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/mint/blobs/sha256:5faf98e60b6d18332bcac4ab076f6ba861ee7daea4c23a85f97e6c8fa3d1f463", + "sha256": "5faf98e60b6d18332bcac4ab076f6ba861ee7daea4c23a85f97e6c8fa3d1f463" + }, + "big_sur": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/mint/blobs/sha256:3ccf422821dd5fc82488f8e0ab2a11efb645901527b8cf9c42979cc152a9ce02", + "sha256": "3ccf422821dd5fc82488f8e0ab2a11efb645901527b8cf9c42979cc152a9ce02" + }, + "catalina": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/mint/blobs/sha256:d09ea36619994628564fb3d7e8e71b8c368c59f68e29174fb84b9b127bd9290e", + "sha256": "d09ea36619994628564fb3d7e8e71b8c368c59f68e29174fb84b9b127bd9290e" + }, + "x86_64_linux": { + "cellar": "/home/linuxbrew/.linuxbrew/Cellar", + "url": "https://ghcr.io/v2/homebrew/core/mint/blobs/sha256:1d73dd0102396a53abac4721557dc7d7c2897bdb0e95551e04869c48d11df764", + "sha256": "1d73dd0102396a53abac4721557dc7d7c2897bdb0e95551e04869c48d11df764" + } + } + } + } + } + }, + "system": { + "macos": { + "monterey": { + "HOMEBREW_VERSION": "3.4.8", + "HOMEBREW_PREFIX": "/opt/homebrew", + "Homebrew/homebrew-core": "c56787cde0b726d961c1949aca8548551133975e", + "CLT": "", + "Xcode": "13.3", + "macOS": "12.3.1" + } + } + } +} diff --git a/CHANGES.md b/CHANGES.md index 6cbe0c932..cf0cbbcc5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,37 @@ +## Changes in 1.8.14 (2022-05-05) + +🙌 Improvements + +- Upgrade MatrixSDK version ([v0.23.4](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.23.4)). +- Spaces: Bring leaving space experience in line with Web ([#4850](https://github.com/vector-im/element-ios/issues/4850)) +- Location sharing: Add cell for live location sharing in timeline ([#6029](https://github.com/vector-im/element-ios/issues/6029)) +- Location sharing: Add timer selector when start live location sharing ([#6071](https://github.com/vector-im/element-ios/issues/6071)) +- Location sharing: Connect SDK to location sharing timeline cell ([#6077](https://github.com/vector-im/element-ios/issues/6077)) + +🐛 Bugfixes + +- RoomNavigationParameters: Fix initializer by not defining convenience. ([#5883](https://github.com/vector-im/element-ios/issues/5883)) +- Fail to open a sub space ([#5965](https://github.com/vector-im/element-ios/issues/5965)) +- RecentsViewController: Fix disappearing filter on search cancellation & empty view on the first screen appearing. ([#6076](https://github.com/vector-im/element-ios/issues/6076)) +- RoomsViewController: Avoid crash by fixing section index to scroll. ([#6086](https://github.com/vector-im/element-ios/issues/6086)) +- Search: Prevent crash when searching ([#6115](https://github.com/vector-im/element-ios/issues/6115)) + +🗣 Translations + +- Localisations: Remove strings with bad formatting and add a run script to detect errors at compile time. ([#5990](https://github.com/vector-im/element-ios/issues/5990)) + +🧱 Build + +- UI Tests: Fix broken tests and add a check on PRs. ([#6050](https://github.com/vector-im/element-ios/issues/6050)) + +🚧 In development 🚧 + +- Authentication: Begin implementing authentication flow with a Service, Registration screen and Server Selection screen. ([#5648](https://github.com/vector-im/element-ios/issues/5648)) +- Location sharing: Add live location viewer screen. ([#5723](https://github.com/vector-im/element-ios/issues/5723)) +- Location sharing: Support live location event in the timeline. ([#6057](https://github.com/vector-im/element-ios/issues/6057)) +- Location sharing: Integrate live location viewer screen with room screen. ([#6081](https://github.com/vector-im/element-ios/issues/6081)) + + ## Changes in 1.8.13 (2022-04-20) ✨ Features diff --git a/Config/AppIdentifiers.xcconfig b/Config/AppIdentifiers.xcconfig index e3cb5d48b..a55dc348f 100644 --- a/Config/AppIdentifiers.xcconfig +++ b/Config/AppIdentifiers.xcconfig @@ -27,13 +27,13 @@ DEVELOPMENT_TEAM = 7J4U792NQT // Provisioning profiles RIOT_PROVISIONING_PROFILE_SPECIFIER = Vector App Store -RIOT_PROVISIONING_PROFILE = 4b43c1ca-3246-4984-828f-165838f5715a +RIOT_PROVISIONING_PROFILE = 7579fa6f-9887-415e-90fc-2c7acd8812e6 NSE_PROVISIONING_PROFILE_SPECIFIER = "Vector NSE: App Store" -NSE_PROVISIONING_PROFILE = de44ca91-4318-4c23-8611-b531793505c2 +NSE_PROVISIONING_PROFILE = e73107b2-1bfe-4615-be3e-39fd4dcb2af0 SHARE_EXTENSION_PROVISIONING_PROFILE_SPECIFIER = "Vector Share Extension: App Store" -SHARE_EXTENSION_PROVISIONING_PROFILE = 546090a2-77ca-4bc2-b904-da5bd97a2f37 +SHARE_EXTENSION_PROVISIONING_PROFILE = 8c797ca0-0440-49bd-be8d-11d761152995 SIRI_INTENTS_PROVISIONING_PROFILE_SPECIFIER = "Vector Siri Intents: App Store" -SIRI_INTENTS_PROVISIONING_PROFILE = 6951ad31-4850-445a-89c8-b64bca0a1c44 +SIRI_INTENTS_PROVISIONING_PROFILE = 1690e81a-5ad3-4d99-b578-02693579be71 diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index b5cb77e7c..74de12959 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.8.13 -CURRENT_PROJECT_VERSION = 1.8.13 +MARKETING_VERSION = 1.8.14 +CURRENT_PROJECT_VERSION = 1.8.14 diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index cccba278d..a08546787 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -385,6 +385,8 @@ final class BuildSettings: NSObject { // MARK: - Onboarding static let onboardingShowAccountPersonalization = false + static let onboardingEnableNewAuthenticationFlow = false + static let onboardingHostYourOwnServerLink = URL(string: "https://element.io/contact-sales")! // MARK: - Unified Search static let unifiedSearchScreenShowPublicDirectory = true diff --git a/Config/Project.xcconfig b/Config/Project.xcconfig index 331f7b44f..95b6295d6 100644 --- a/Config/Project.xcconfig +++ b/Config/Project.xcconfig @@ -25,10 +25,10 @@ KEYCHAIN_ACCESS_GROUP = $(AppIdentifierPrefix)$(BASE_BUNDLE_IDENTIFIER).keychain.shared // Build settings -IPHONEOS_DEPLOYMENT_TARGET = 12.1 +IPHONEOS_DEPLOYMENT_TARGET = 14.0 SDKROOT = iphoneos TARGETED_DEVICE_FAMILY = 1,2 -SWIFT_VERSION = 5.3.1 +SWIFT_VERSION = 5.6 ENABLE_BITCODE = NO LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @executable_path/../../Frameworks diff --git a/DesignKit/Source/ColorValues.swift b/DesignKit/Source/ColorValues.swift index 5694a5503..338d1cfe8 100644 --- a/DesignKit/Source/ColorValues.swift +++ b/DesignKit/Source/ColorValues.swift @@ -46,5 +46,7 @@ public struct ColorValues: Colors { public let background: UIColor + public let ems: UIColor + public let namesAndAvatars: [UIColor] } diff --git a/DesignKit/Source/Colors.swift b/DesignKit/Source/Colors.swift index d7c885e59..bf3e9abd3 100644 --- a/DesignKit/Source/Colors.swift +++ b/DesignKit/Source/Colors.swift @@ -55,7 +55,7 @@ public protocol Colors { /// Separating line var separator: ColorType { get } - // Cards, tiles + /// Cards, tiles var tile: ColorType { get } /// Top navigation background on iOS @@ -64,6 +64,9 @@ public protocol Colors { /// Background UI color var background: ColorType { get } + /// Global color: The EMS brand's purple colour. + var ems: ColorType { get } + /// - Names in chat timeline /// - Avatars default states that include first name letter var namesAndAvatars: [ColorType] { get } diff --git a/DesignKit/Source/ColorsSwiftUI.swift b/DesignKit/Source/ColorsSwiftUI.swift index 701aee537..b685ac0d7 100644 --- a/DesignKit/Source/ColorsSwiftUI.swift +++ b/DesignKit/Source/ColorsSwiftUI.swift @@ -47,6 +47,8 @@ public struct ColorSwiftUI: Colors { public let background: Color + public var ems: Color + public let namesAndAvatars: [Color] init(values: ColorValues) { @@ -62,6 +64,7 @@ public struct ColorSwiftUI: Colors { tile = Color(values.tile) navigation = Color(values.navigation) background = Color(values.background) + ems = Color(values.ems) namesAndAvatars = values.namesAndAvatars.map({ Color($0) }) } } diff --git a/DesignKit/Variants/Colors/Dark/DarkColors.swift b/DesignKit/Variants/Colors/Dark/DarkColors.swift index b6b0ba5ed..24678fcd0 100644 --- a/DesignKit/Variants/Colors/Dark/DarkColors.swift +++ b/DesignKit/Variants/Colors/Dark/DarkColors.swift @@ -33,6 +33,7 @@ public class DarkColors { tile: UIColor(rgb:0x394049), navigation: UIColor(rgb:0x21262C), background: UIColor(rgb:0x15191E), + ems: UIColor(rgb: 0x7E69FF), namesAndAvatars: [ UIColor(rgb:0x368BD6), UIColor(rgb:0xAC3BA8), diff --git a/DesignKit/Variants/Colors/Light/LightColors.swift b/DesignKit/Variants/Colors/Light/LightColors.swift index 2e7d8147a..332a24162 100644 --- a/DesignKit/Variants/Colors/Light/LightColors.swift +++ b/DesignKit/Variants/Colors/Light/LightColors.swift @@ -34,6 +34,7 @@ public class LightColors { tile: UIColor(rgb:0xF3F8FD), navigation: UIColor(rgb:0xF4F6FA), background: UIColor(rgb:0xFFFFFF), + ems: UIColor(rgb: 0x7E69FF), namesAndAvatars: [ UIColor(rgb:0x368BD6), UIColor(rgb:0xAC3BA8), diff --git a/INSTALL.md b/INSTALL.md index 667f308f4..792ae4fef 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -8,6 +8,7 @@ To build Element iOS project you need: - [Ruby](https://www.ruby-lang.org/), a dynamic programming language used by several build tools. - [CocoaPods](https://cocoapods.org), library dependencies manager for Xcode projects. - [XcodeGen](https://github.com/yonaskolb/XcodeGen), an Xcode project generator. +- [Mint](https://github.com/yonaskolb/Mint), a package manager that installs and runs executable Swift packages - [bundler](https://bundler.io/) (optional), is also a dependency manager used to manage build tools dependency (CocoaPods, Fastlane). ### Install Ruby @@ -26,12 +27,12 @@ $ gem install cocoapods In the last case please ensure that you are using the same version as indicated at the end of the `Podfile.lock` file. -### Install XcodeGen +### Install XcodeGen and Mint -You can directly install XcodeGen with [Homebrew](https://brew.sh) or RubyGems: +You can install XcodeGen and Mint using the included [Homebrew](https://brew.sh) Brewfile: ``` -$ brew install xcodegen +$ brew bundle ``` ### Install bundler (optional) diff --git a/Podfile b/Podfile index 8c6a75f18..03f7da41d 100644 --- a/Podfile +++ b/Podfile @@ -1,7 +1,7 @@ source 'https://cdn.cocoapods.org/' # Uncomment this line to define a global platform for your project -platform :ios, '12.1' +platform :ios, '14.0' # Use frameworks to allow usage of pods written in Swift use_frameworks! @@ -13,7 +13,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.23.3' +$matrixSDKVersion = '= 0.23.4' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } @@ -57,6 +57,7 @@ end def import_SwiftUI_pods pod 'Introspect', '~> 0.1' + pod 'DSBottomSheet', '~> 0.3' end abstract_target 'RiotPods' do @@ -151,4 +152,4 @@ post_install do |installer| config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)', '-Xcc', '-Wno-nullability-completeness'] end end -end \ No newline at end of file +end diff --git a/Podfile.lock b/Podfile.lock index 76519dbe5..1cc3bd2e1 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -20,6 +20,7 @@ PODS: - BlueRSA (1.0.200) - DGCollectionViewLeftAlignFlowLayout (1.0.4) - Down (0.11.0) + - DSBottomSheet (0.3.0) - DSWaveformImage (6.1.1) - DTCoreText (1.6.26): - DTCoreText/Core (= 1.6.26) @@ -105,6 +106,7 @@ DEPENDENCIES: - AnalyticsEvents (from `https://github.com/matrix-org/matrix-analytics-events.git`, branch `release/swift`) - DGCollectionViewLeftAlignFlowLayout (~> 1.0.4) - Down (~> 0.11.0) + - DSBottomSheet (~> 0.3) - DSWaveformImage (~> 6.1.1) - DTCoreText (~> 1.6.25) - ffmpeg-kit-ios-audio (= 4.5.1) @@ -139,6 +141,7 @@ SPEC REPOS: - BlueRSA - DGCollectionViewLeftAlignFlowLayout - Down + - DSBottomSheet - DSWaveformImage - DTCoreText - DTFoundation @@ -191,6 +194,7 @@ SPEC CHECKSUMS: BlueRSA: dfeef51db96bcc4edec654956c1581adbda4e6a3 DGCollectionViewLeftAlignFlowLayout: a0fa58797373ded039cafba8133e79373d048399 Down: b6ba1bc985c9d2f4e15e3b293d2207766fa12612 + DSBottomSheet: ca0ac37eb5af2dd54663f86b84382ed90a59be2a DSWaveformImage: 3c718a0cf99291887ee70d1d0c18d80101d3d9ce DTCoreText: ec749e013f2e1f76de5e7c7634642e600a7467ce DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536 @@ -225,6 +229,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 8055bae15b82bd29b1d12c64ff038fe6f86a8ca0 +PODFILE CHECKSUM: 39feedaf75b9a9287e4fe5309200e0493a9251a3 -COCOAPODS: 1.11.2 +COCOAPODS: 1.11.3 diff --git a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme index 84ecb908a..a9bea1d96 100644 --- a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme +++ b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme @@ -4,7 +4,8 @@ version = "1.3"> + buildImplicitDependencies = "YES" + runPostActionsOnFailure = "NO"> + + + + + + diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_server_selection_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Authentication/authentication_server_selection_icon.imageset/Contents.json new file mode 100644 index 000000000..fec295dec --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_server_selection_icon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "authentication_server_selection_icon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_server_selection_icon.imageset/authentication_server_selection_icon.svg b/Riot/Assets/Images.xcassets/Authentication/authentication_server_selection_icon.imageset/authentication_server_selection_icon.svg new file mode 100644 index 000000000..17b23458e --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_server_selection_icon.imageset/authentication_server_selection_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_apple.imageset/Contents.json b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_apple.imageset/Contents.json new file mode 100644 index 000000000..eddbc14dc --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_apple.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "authentication_sso_apple.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_apple.imageset/authentication_sso_apple.svg b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_apple.imageset/authentication_sso_apple.svg new file mode 100644 index 000000000..2d1d3b3d8 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_apple.imageset/authentication_sso_apple.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_facebook.imageset/Contents.json b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_facebook.imageset/Contents.json new file mode 100644 index 000000000..c087db709 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_facebook.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "authentication_sso_facebook.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_facebook.imageset/authentication_sso_facebook.svg b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_facebook.imageset/authentication_sso_facebook.svg new file mode 100644 index 000000000..21a4b1fe2 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_facebook.imageset/authentication_sso_facebook.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_github.imageset/Contents.json b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_github.imageset/Contents.json new file mode 100644 index 000000000..3fa20bfef --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_github.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "authentication_sso_github.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_github.imageset/authentication_sso_github.svg b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_github.imageset/authentication_sso_github.svg new file mode 100644 index 000000000..91ad466f1 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_github.imageset/authentication_sso_github.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_gitlab.imageset/Contents.json b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_gitlab.imageset/Contents.json new file mode 100644 index 000000000..80f0a4dc6 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_gitlab.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "authentication_sso_gitlab.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_gitlab.imageset/authentication_sso_gitlab.svg b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_gitlab.imageset/authentication_sso_gitlab.svg new file mode 100644 index 000000000..885be976d --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_gitlab.imageset/authentication_sso_gitlab.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_google.imageset/Contents.json b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_google.imageset/Contents.json new file mode 100644 index 000000000..7fb05cc39 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_google.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "authentication_sso_google.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_google.imageset/authentication_sso_google.svg b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_google.imageset/authentication_sso_google.svg new file mode 100644 index 000000000..e402ec590 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_google.imageset/authentication_sso_google.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_twitter.imageset/Contents.json b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_twitter.imageset/Contents.json new file mode 100644 index 000000000..3ebd5a43f --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_twitter.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "authentication_sso_twitter.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_twitter.imageset/authentication_sso_twitter.svg b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_twitter.imageset/authentication_sso_twitter.svg new file mode 100644 index 000000000..f1bf030ce --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/authentication_sso_icon_twitter.imageset/authentication_sso_twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/Images.xcassets/Common/share_action_button.imageset/Contents.json b/Riot/Assets/Images.xcassets/Common/share_action_button.imageset/Contents.json index a6bb96098..1f52d4d2c 100644 --- a/Riot/Assets/Images.xcassets/Common/share_action_button.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Common/share_action_button.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_icon.imageset/Contents.json new file mode 100644 index 000000000..1390be53e --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "location_live_cell_ended_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_live_cell_ended_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_live_cell_ended_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_icon.imageset/location_live_cell_ended_icon.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_icon.imageset/location_live_cell_ended_icon.png new file mode 100644 index 000000000..9266e542a Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_icon.imageset/location_live_cell_ended_icon.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_icon.imageset/location_live_cell_ended_icon@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_icon.imageset/location_live_cell_ended_icon@2x.png new file mode 100644 index 000000000..e6bd8d1da Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_icon.imageset/location_live_cell_ended_icon@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_icon.imageset/location_live_cell_ended_icon@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_icon.imageset/location_live_cell_ended_icon@3x.png new file mode 100644 index 000000000..2aad77a52 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_icon.imageset/location_live_cell_ended_icon@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Contents.json new file mode 100644 index 000000000..188a203cc --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "Subtract.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Subtract@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Subtract@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract.png new file mode 100644 index 000000000..16be3b51b Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract@2x.png new file mode 100644 index 000000000..4e20a1517 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract@3x.png new file mode 100644 index 000000000..61ea2a528 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_icon.imageset/Contents.json new file mode 100644 index 000000000..7ba4f36ba --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "location_live_cell_loading_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_live_cell_loading_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_live_cell_loading_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_icon.imageset/location_live_cell_loading_icon.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_icon.imageset/location_live_cell_loading_icon.png new file mode 100644 index 000000000..fd67d933e Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_icon.imageset/location_live_cell_loading_icon.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_icon.imageset/location_live_cell_loading_icon@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_icon.imageset/location_live_cell_loading_icon@2x.png new file mode 100644 index 000000000..da2778461 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_icon.imageset/location_live_cell_loading_icon@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_icon.imageset/location_live_cell_loading_icon@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_icon.imageset/location_live_cell_loading_icon@3x.png new file mode 100644 index 000000000..49441f8b0 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_icon.imageset/location_live_cell_loading_icon@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/Contents.json new file mode 100644 index 000000000..2753cacb9 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "location_placeholder_background_image.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_placeholder_background_image@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_placeholder_background_image@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/location_placeholder_background_image.png b/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/location_placeholder_background_image.png new file mode 100644 index 000000000..b161191b7 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/location_placeholder_background_image.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/location_placeholder_background_image@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/location_placeholder_background_image@2x.png new file mode 100644 index 000000000..304c25f56 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/location_placeholder_background_image@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/location_placeholder_background_image@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/location_placeholder_background_image@3x.png new file mode 100644 index 000000000..1bfa2e441 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/location_placeholder_background_image@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker.png b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker.png index eedadad5e..4f1a4b8fb 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker.png and b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@2x.png index fce4dcb04..a48c72b01 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@2x.png and b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@3x.png index ff8ce3aad..e5a28f6e6 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@3x.png and b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@3x.png differ diff --git a/Riot/Assets/ar.lproj/Localizable.strings b/Riot/Assets/ar.lproj/Localizable.strings index d0e49f4f9..c5e8f6fef 100644 --- a/Riot/Assets/ar.lproj/Localizable.strings +++ b/Riot/Assets/ar.lproj/Localizable.strings @@ -96,7 +96,6 @@ /** Image Messages **/ /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "لَقَد أرسَلَ %@ صُّورة %@"; /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "العُضو %@: * %@ %@"; diff --git a/Riot/Assets/bg.lproj/Localizable.strings b/Riot/Assets/bg.lproj/Localizable.strings index 0b966c728..26cf616f0 100644 --- a/Riot/Assets/bg.lproj/Localizable.strings +++ b/Riot/Assets/bg.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ изпрати снимка %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ публикува снимка %@ в %@"; /* A single unread message in a room */ diff --git a/Riot/Assets/bg.lproj/Vector.strings b/Riot/Assets/bg.lproj/Vector.strings index 62a612174..9c760b32a 100644 --- a/Riot/Assets/bg.lproj/Vector.strings +++ b/Riot/Assets/bg.lproj/Vector.strings @@ -445,7 +445,6 @@ "network_offline_prompt" = "Интернет връзката изглежда не работи."; "public_room_section_title" = "Публични стаи (в %@):"; "bug_report_prompt" = "Приложението спря да работи последния път. Искате ли да изпратите съобщение за грешка?"; -"rage_shake_prompt" = "Изглежда, че разклащате телефона, което ни кара да мислим, че не сте доволни от %@. Искате ли да изпратите съобщение за грешка?"; "do_not_ask_again" = "Не питай отново"; "camera_access_not_granted" = "%@ няма разрешение да използва камерата. Моля, проверете настройките за сигурност"; "large_badge_value_k_format" = "%.1fK"; @@ -469,7 +468,6 @@ "bug_report_send" = "Изпрати"; "no_voip" = "%@ Ви се обажда, но %@ все още не поддържа разговори.\nМожете да пренебрегнете това известие и да отговорите на това позвъняване от друго устройство или да го откажете."; // Crash report -"google_analytics_use_prompt" = "Искате ли да помогнете за подобрението на %@ като анонимно изпращате съобщения за грешки и данни за използване?"; // Crypto "e2e_enabling_on_app_update" = "%@ поддържа шифроване от край до край, но за да го включите трябва да влезете в профила си отново.\n\nМоже да го направите сега или по-късно от настройките на приложението."; "e2e_need_log_in_again" = "Трябва да влезете обратно в профила си, за да се създадат ключове за шифроване от-край-до-край за тази сесия и да се изпрати публичния ключ към Home сървъра.\nТова е еднократно. Извинете за неудобството."; @@ -560,7 +558,6 @@ "settings_key_backup_info_not_valid" = "Тази сесия не прави резервно копие на ключовете Ви, но имате съществуващо резервно копие, от което да възстановявате или допълвате в бъдеще."; "settings_key_backup_info_progress" = "Правене на резервно копие на %@ ключа…"; "settings_key_backup_info_progress_done" = "Има резервно копие на всички ключове"; -"settings_key_backup_info_not_trusted_from_verifiable_device_fix_action" = "За да използвате Възстановяване на Защитени Съобщения на това устройство, потвърдете %@ сега."; "settings_key_backup_info_not_trusted_fix_action" = "За да използвате Възстановяване на Защитени Съобщения на това устройство, въведете паролата си или ключа за възстановяване."; "settings_key_backup_info_trust_signature_unknown" = "Резервното копие има подпис от сесия с идентификатор: %@"; "settings_key_backup_info_trust_signature_valid" = "Резервното копие има валиден подпис от текущата сесия"; @@ -671,10 +668,6 @@ "room_event_action_reply" = "Отговори"; "room_event_action_edit" = "Редактирай"; "auth_login_single_sign_on" = "Вход"; -"room_event_action_reaction_agree" = "Съгласи се с %@"; -"room_event_action_reaction_disagree" = "Противоречи на %@"; -"room_event_action_reaction_like" = "Харесай %@"; -"room_event_action_reaction_dislike" = "Не харесай %@"; "room_action_reply" = "Отговори"; "settings_labs_message_reaction" = "Реагирай на съобщения с емоджи"; "settings_key_backup_button_connect" = "Свържи сесията към резервно копие на ключове"; @@ -820,7 +813,6 @@ "room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Не е конфигуриран сървър за самоличност, така че не можете да започнете чат с контакт посредством имейл адрес."; // Service terms "service_terms_modal_title" = "Условия за ползване"; -"service_terms_modal_message" = "За да продължите, трябва да приемете Условията за ползване на тази услуга (%@)."; "service_terms_modal_accept_button" = "Приеми"; "service_terms_modal_description_for_identity_server" = "Бъдете откриваеми от потребители"; "service_terms_modal_description_for_integration_manager" = "Използвайте ботове, връзки към други мрежи и стикери"; @@ -879,7 +871,6 @@ "settings_identity_server_no_is_description" = "В момента не използвате сървър за самоличност. Добавете такъв по-горе, за да откривате и бъдете откриваеми от съществуващи ваши контакти."; // Identity server settings "identity_server_settings_title" = "Сървър за самоличност"; -"identity_server_settings_description" = "В момента използвате %2 за да откривате и да бъдете открити от съществуващи ваши контакти."; "identity_server_settings_no_is_description" = "В момента не използвате сървър за самоличност. Добавете такъв по-горе, за да откривате и бъдете открити от съществуващи ваши контакти."; "identity_server_settings_place_holder" = "Въведете сървър за самоличност"; "identity_server_settings_add" = "Добави"; @@ -906,7 +897,6 @@ "service_terms_modal_description_for_identity_server_2" = "Бъдете откриваеми по телефон или имейл"; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "Откриване на контакти"; -"service_terms_modal_message_identity_server" = "Приемете условията на сървъра за самоличност (%@) за да откривате контакти."; // Generic errors "error_invite_3pid_with_no_identity_server" = "Добавете сървър за самоличност в настройки за да каните по имейл."; "error_not_supported_on_mobile" = "Не може да правите това от %@ мобилен телефон."; @@ -1348,7 +1338,6 @@ "event_formatter_call_you_missed" = "Пропуснахте това обаждане"; "event_formatter_call_you_declined" = "Отказахте разговора"; "event_formatter_call_you_currently_in" = "Активен разговор"; -"event_formatter_call_has_ended" = "Прекратяване на %@"; "event_formatter_call_ringing" = "Звънене…"; "event_formatter_call_connecting" = "Свързване…"; "event_formatter_call_video" = "Видео разговор"; @@ -1481,7 +1470,6 @@ "notice_room_aliases" = "Адресите на стаята са: %@"; "notice_room_related_groups" = "Групите, асоциирани с тази стая, са: %@"; "notice_encrypted_message" = "Шифровано съобщение"; -"notice_encryption_enabled" = "%@ включи шифроването от край до край (алгоритъм %@)"; "notice_image_attachment" = "прикачена снимка"; "notice_audio_attachment" = "прикачено аудио"; "notice_video_attachment" = "прикачено видео"; @@ -1503,7 +1491,6 @@ // room display name "room_displayname_empty_room" = "Празна стая"; "room_displayname_two_members" = "%@ и %@"; -"room_displayname_more_than_two_members" = "%@ и %u други"; // Settings "settings" = "Настройки"; "settings_enable_inapp_notifications" = "Включване на известия в приложението"; diff --git a/Riot/Assets/ca.lproj/InfoPlist.strings b/Riot/Assets/ca.lproj/InfoPlist.strings index 9d0a7a98e..912a5bbab 100644 --- a/Riot/Assets/ca.lproj/InfoPlist.strings +++ b/Riot/Assets/ca.lproj/InfoPlist.strings @@ -1,5 +1,8 @@ // Permissions usage explanations "NSCameraUsageDescription" = "La càmera s'utilitza per fer fotos i vídeos, fer vídeo conferència."; "NSPhotoLibraryUsageDescription" = "La fototeca s'utilitza per enviar fotos i vídeos."; -"NSMicrophoneUsageDescription" = "El micròfon s'utilitza per fer vídeos, fer trucades."; -"NSContactsUsageDescription" = "Per tal de mostrar-vos quins dels vostres contactes ja són usuaris de Element o Matrix, podem enviar les seves adreces i números de telèfon desades a la vostra agenda cap al vostre servidor d'identitats de Matrix. El nou Vector no emmagatzema aquestes dades ni les usa per a cap altra finalitat. Per a més informació, si us plau visiteu l'apartat de política de privacitat als paràmetres de l'aplicació."; +"NSMicrophoneUsageDescription" = "Element necessita accedir al vostre micròfon per a fer i rebre trucades, vídeos i gravar missatges de veu."; +"NSContactsUsageDescription" = "Element us mostrarà els vostres contactes per si els voleu convidar a xatejar."; +"NSLocationWhenInUseUsageDescription" = "Quan compartiu la vostra localització amb altres, Element en necessita accés per mostrar-lis un mapa."; +"NSFaceIDUsageDescription" = "Face ID es fa servir per accedir a la vostra app."; +"NSCalendarsUsageDescription" = "Consulteu la vostra agenda de reunions a l'app."; diff --git a/Riot/Assets/ca.lproj/Localizable.strings b/Riot/Assets/ca.lproj/Localizable.strings index cd3bbb08c..9cc1026d7 100644 --- a/Riot/Assets/ca.lproj/Localizable.strings +++ b/Riot/Assets/ca.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ ha enviat una foto %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ ha publicat una foto %@ a %@"; /* Multiple unread messages in a room */ diff --git a/Riot/Assets/ca.lproj/Vector.strings b/Riot/Assets/ca.lproj/Vector.strings index 401009009..ddceb0f20 100644 --- a/Riot/Assets/ca.lproj/Vector.strings +++ b/Riot/Assets/ca.lproj/Vector.strings @@ -260,7 +260,6 @@ "settings_report_bug" = "Informa d'un error"; "settings_config_home_server" = "El servidor hoste és %@"; "settings_config_identity_server" = "El servidor d'identitat és %@"; -"settings_config_user_id" = "Sessió iniciada com"; "settings_user_settings" = "Ajustos d'usuari"; "settings_notifications_settings" = "Ajustos de notificacions"; "settings_calls_settings" = "Trucades"; @@ -463,7 +462,6 @@ "no_voip_title" = "Trucada entrant"; "no_voip" = "%@ t'està trucant però %@ encara no suporta trucades.\nPots ignorar aquesta notificació i respondre la trucada des de un altre dispositiu o pots rebutjar-la."; // Crash report -"google_analytics_use_prompt" = "T'agradaria ajudar a millorar %@ enviant automàticament informes de bloquejos i ús de dades?"; // Crypto "e2e_enabling_on_app_update" = "%@ ara permet l'ús de xifrat punt a punt però has de tornar a connectar-te per tal d'activar-lo.\n\nPots fer-ho ara o més tard des de les preferències de l'aplicació."; "e2e_need_log_in_again" = "Has de tornar a iniciar sessió per tal de generar claus de xifrat punt a punt per a aquest dispositiu i enviar la clau pública al servidor.\nAixò només s'ha de fer un cop, disculpa les molèsties."; @@ -647,7 +645,6 @@ "notice_room_aliases" = "Els àlies de la sala són: %@"; "notice_room_related_groups" = "Els grups associats amb aquesta sala són: %@"; "notice_encrypted_message" = "Missatge xifrat"; -"notice_encryption_enabled" = "%@ ha activat el xifrat punt a punt (algoritme %@)"; "notice_image_attachment" = "adjunt d'imatge"; "notice_audio_attachment" = "adjunt d'àudio"; "notice_video_attachment" = "adjunt de vídeo"; @@ -669,7 +666,6 @@ // room display name "room_displayname_empty_room" = "Sala buida"; "room_displayname_two_members" = "%@ i %@"; -"room_displayname_more_than_two_members" = "%@ i %u més"; // Settings "settings" = "Configuració"; "settings_enable_inapp_notifications" = "Habilitar les notificacions de les App integrades"; diff --git a/Riot/Assets/cs.lproj/Vector.strings b/Riot/Assets/cs.lproj/Vector.strings index ddb31ffaa..4dd6c6076 100644 --- a/Riot/Assets/cs.lproj/Vector.strings +++ b/Riot/Assets/cs.lproj/Vector.strings @@ -139,7 +139,6 @@ "contacts_address_book_section" = "LOKÁLNÍ KONTAKTY"; "contacts_address_book_matrix_users_toggle" = "Pouze Matrix uživatelé"; "contacts_address_book_no_contact" = "Žádné lokální kontakty"; -"contacts_address_book_permission_denied" = "Nepovolil jste přístup aplikace Element k místním kontaktům"; "contacts_user_directory_section" = "UŽIVATELSKÝ ADRESÁŘ"; "contacts_user_directory_offline_section" = "UŽIVATELSKÝ ADRESÁŘ (offline)"; // Chat participants @@ -269,7 +268,6 @@ "room_participants_action_ban" = "Vyhodit z této místnosti"; "room_creation_error_invite_user_by_email_without_identity_server" = "Není nakonfigurován žádný server identity, takže nemůžete přidat účastníka pomocí e-mailu."; "auth_softlogout_recover_encryption_keys" = "Přihlaste se a obnovte šifrovací klíče uložené výhradně v tomto zařízení. Potřebujete je ke čtení všech zabezpečených zpráv na jakémkoli zařízení."; -"auth_softlogout_reason" = "Váš správce domovského serveru (%1$@) vás odhlásil z vašeho účtu %2 @ (%3$@)."; "auth_softlogout_signed_out" = "Jste odhlášeni"; "auth_autodiscover_invalid_response" = "Neplatná odpověď na objevení domovského serveru"; "auth_accept_policies" = "Přečtěte si a přijměte zásady tohoto domovského serveru:"; diff --git a/Riot/Assets/cy.lproj/Localizable.strings b/Riot/Assets/cy.lproj/Localizable.strings index 5a18dd675..c8aae8df9 100644 --- a/Riot/Assets/cy.lproj/Localizable.strings +++ b/Riot/Assets/cy.lproj/Localizable.strings @@ -13,7 +13,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "Anfonwyd %@ lun %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "Postiodd %@ lun %@ yn %@"; /* A single unread message in a room */ diff --git a/Riot/Assets/cy.lproj/Vector.strings b/Riot/Assets/cy.lproj/Vector.strings index b88ac420a..c2b70e9b9 100644 --- a/Riot/Assets/cy.lproj/Vector.strings +++ b/Riot/Assets/cy.lproj/Vector.strings @@ -623,7 +623,6 @@ "no_voip_title" = "Galwad sy'n dod i mewn"; "no_voip" = "Mae %@ yn eich ffonio ond nid yw %@ yn cefnogi galwadau eto.\nGallwch anwybyddu'r hysbysiad hwn ac ateb yr alwad o ddyfais arall neu gallwch ei wrthod."; // Crash report -"google_analytics_use_prompt" = "Hoffech chi helpu i wella %@ trwy gyrru adroddiadau pall a data defnydd dienw yn awtomatig?"; // Crypto "e2e_enabling_on_app_update" = "Mae %@ bellach yn cefnogi amgryptio o'r dechrau i'r diwedd ond mae angen i chi fewngofnodi eto i'w alluogi.\n\nGallwch ei wneud nawr neu'n hwyrach o'r gosodiadau."; "e2e_need_log_in_again" = "Mae angen i chi fewngofnodi i gynhyrchu allweddi amgryptio o'r dechrau i'r diwedd ar gyfer y sesiwn hon a chyflwyno'r allwedd gyhoeddus i'ch hafanweinydd\nDim ond unwaith fydd rhaid gwneud hyn; sori am yr anghyfleustra."; @@ -678,7 +677,6 @@ "gdpr_consent_not_given_alert_review_now_action" = "Adolygu rwan"; // Service terms "service_terms_modal_title" = "Telerau Gwasanaeth"; -"service_terms_modal_message" = "I barhau maen' rhaid i chi dderbyn telerau y gwasanaeth hwn (%@)."; "service_terms_modal_accept_button" = "Derbyn"; "service_terms_modal_decline_button" = "Gwrthod"; "service_terms_modal_description_for_identity_server_1" = "Dod o hyd i eraill dros y ffôn neu e-bost"; @@ -686,7 +684,6 @@ "service_terms_modal_description_for_integration_manager" = "Defnyddiwch Botiau, pontydd, teclynnau a phecynnau sticeri"; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "Darganfod Cysylltiadau"; -"service_terms_modal_message_identity_server" = "Derbyn telerau'r gweinydd adnabod (%@) i ddarganfod cysylltiadau."; "deactivate_account_title" = "Dad-actifadu Cyfrif"; "deactivate_account_informations_part1" = "Bydd hyn yn golygu na ellir defnyddio'ch cyfrif yn barhaol. Ni fyddwch yn gallu mewngofnodi, ac ni fydd unrhyw un yn gallu ailgofrestru'r un dynodwr defnyddiwr. Bydd hyn yn achosi i'ch cyfrif adael yr holl ystafelloedd y mae'n cymryd rhan ynddynt, a bydd yn tynnu manylion eich cyfrif o'ch gweinydd adnabod. "; "deactivate_account_informations_part2_emphasize" = "Ni ellir gwrthdroi'r weithred hon."; @@ -1098,7 +1095,6 @@ "notice_room_aliases" = "Arallenwau'r ystafell yw: %@"; "notice_room_related_groups" = "Y grwpiau sy'n gysylltiedig â'r ystafell hon yw: %@"; "notice_encrypted_message" = "Neges amgryptiedig"; -"notice_encryption_enabled" = "Trodd %@ ar amgryptio o'r dechrau i'r diwedd (algorithm %@)"; "notice_image_attachment" = "atodiad llun"; "notice_audio_attachment" = "atodiad sain"; "notice_video_attachment" = "atodiad fideo"; diff --git a/Riot/Assets/de.lproj/Localizable.strings b/Riot/Assets/de.lproj/Localizable.strings index e298ee166..cc3120d86 100644 --- a/Riot/Assets/de.lproj/Localizable.strings +++ b/Riot/Assets/de.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ hat ein Bild gesendet %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ sendet ein Bild %@ in %@"; /* Multiple unread messages in a room */ diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 4fedaa943..c23f88faa 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -224,7 +224,7 @@ "settings_add_email_address" = "E-Mail-Adresse hinzufügen"; "settings_remove_phone_prompt_msg" = "Bist du sicher, dass du die Telefon-Nummer %@ entfernen möchtest?"; "settings_night_mode" = "Nachtmodus"; -"settings_enable_push_notif" = "Benachrichtungen auf diesem Gerät"; +"settings_enable_push_notif" = "Benachrichtigungen auf diesem Gerät"; "settings_unignore_user" = "Alle Nachrichten von %@ anzeigen?"; "settings_contacts_phonebook_country" = "Land des Telefonbuches"; "settings_labs_e2e_encryption" = "Ende-zu-Ende-Verschlüsselung"; @@ -380,10 +380,9 @@ "call_incoming_voice_prompt" = "Eingehender Sprachanruf von %@"; "call_incoming_video_prompt" = "Eingehender Videoanruf von %@"; // No VoIP support -"no_voip_title" = "Eingehener Anruf"; +"no_voip_title" = "Eingehender Anruf"; "no_voip" = "%@ ruft dich an, aber %@ unterstützt derzeit keine Anrufe.\nDu kannst diese Benachrichtung ignorieren und den Anruf von einem anderen Gerät annehmen oder ihn abweisen."; // Crash report -"google_analytics_use_prompt" = "Möchtest du helfen %@ zu verbessern, indem du anonyme Absturzberichte und Daten über die Verwendung sendest?"; // Crypto "e2e_enabling_on_app_update" = "%@ unterstützt nun Ende-zu-Ende Verschlüsselung. Um sie zu aktivieren musst du dich erneut anmelden.\n\nDu kannst sie nun aktivieren oder später in den Einstellungen."; "e2e_need_log_in_again" = "Du musst dich erneut anmelden um Ende-zu-Ende Schlüssel für diese Sitzung zu erstellen und zum Server zu senden.\nDies ist nur einmal notwendig."; @@ -531,7 +530,7 @@ "event_formatter_rerequest_keys_part2" = " von deinen anderen Sitzungen anfragen."; // Re-request confirmation dialog "rerequest_keys_alert_title" = "Anfrage gesendet"; -"rerequest_keys_alert_message" = "Bitte %@ auf einem anderen Gerät öffenen, das die Nachricht entschlüsseln kann, damit es die Schlüssel an diese Sitzung senden kann."; +"rerequest_keys_alert_message" = "Bitte %@ auf einem anderen Gerät öffnen, das die Nachricht entschlüsseln kann, damit es die Schlüssel an diese Sitzung senden kann."; "room_message_reply_to_placeholder" = "Antwort senden (unverschlüsselt)…"; "encrypted_room_message_reply_to_placeholder" = "Sende eine verschlüsselte Antwort…"; "room_message_reply_to_short_placeholder" = "Sende eine Antwort…"; @@ -558,7 +557,6 @@ "settings_key_backup_info_none" = "Deine Schlüssel werden nicht von dieser Sitzung gesichert."; "settings_key_backup_info_signout_warning" = "Sichere deine Schlüssel damit du sie beim Abmelden nicht verlierst."; "settings_key_backup_info_version" = "Version der Schlüsselsicherung: %@"; -"settings_key_backup_info_algorithm" = "Algorithmus"; "settings_key_backup_info_valid" = "Diese Sitzung sichert deine Schlüssel."; "settings_key_backup_info_not_valid" = "Diese Sitzung sichert nicht dein Schlüssel, es ist jedoch eine Sicherungskopie vorhanden, von der du deine Schlüssel wiederherstellen und die du später hinzufügen kannst."; "settings_key_backup_info_progress" = "Sichere %@ Schlüssel…"; @@ -657,10 +655,6 @@ "room_message_unable_open_link_error_message" = "Konnte Link nicht öffnen."; "room_event_action_reply" = "Antworten"; "room_event_action_edit" = "Bearbeiten"; -"room_event_action_reaction_agree" = "%@ zustimmen"; -"room_event_action_reaction_disagree" = "%@ nicht zustimmen"; -"room_event_action_reaction_like" = "%@ zustimmen"; -"room_event_action_reaction_dislike" = "%@ nicht zustimmen"; "room_action_reply" = "Antworten"; "settings_labs_message_reaction" = "Mit einem Emoji reagieren"; "settings_key_backup_button_connect" = "Verbinde diese Sitzung mit der Schlüsselsicherung"; @@ -738,7 +732,7 @@ "auth_softlogout_recover_encryption_keys" = "Melde dich an, um ausschließlich auf diesem Gerät gespeicherte Verschlüsselungsschlüssel wiederherzustellen. Du benötigst sie, um deine verschlüsselten Nachrichten auf jedem Gerät zu lesen."; "auth_softlogout_clear_data" = "Persönliche Daten löschen"; "auth_softlogout_clear_data_message_1" = "Warnung: Deine persönlichen Daten (einschließlich Verschlüsselungsschlüssel) sind noch auf diesem Gerät gespeichert."; -"auth_softlogout_clear_data_message_2" = "Deaktiviere es, wenn du dieses Gerät nicht mehr verwendest oder du dich mit einem anderen Konto anmelden möchtst."; +"auth_softlogout_clear_data_message_2" = "Deaktiviere es, wenn du dieses Gerät nicht mehr verwendest oder du dich mit einem anderen Konto anmelden möchtest."; "auth_softlogout_clear_data_button" = "Lösche alle Daten"; "auth_softlogout_clear_data_sign_out_title" = "Bist du sicher?"; "auth_softlogout_clear_data_sign_out_msg" = "Möchtest du wirklich alle derzeit auf diesem Gerät gespeicherten Daten löschen? Melde dich erneut an, um auf deine Kontodaten und Nachrichten zuzugreifen."; @@ -806,7 +800,6 @@ "room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Es ist kein Identitätsserver konfiguriert, sodass du keinen Chat mit einem Kontakt über eine E-Mail starten kannst."; // Service terms "service_terms_modal_title" = "Nutzungsbedingungen"; -"service_terms_modal_message" = "Um fortzufahren, musst du die Nutzungsbedingungen akzeptieren (%@)."; "service_terms_modal_accept_button" = "Akzeptieren"; "service_terms_modal_description_for_identity_server" = "Für andere auffindbar sein"; "service_terms_modal_description_for_integration_manager" = "Bots, Brücken, Widgets und Aufkleberpakete verwenden"; @@ -884,7 +877,6 @@ "identity_server_settings_alert_error_invalid_identity_server" = "%@ ist kein gültiger Identitätsserver."; "call_no_stun_server_error_message_1" = "Bitte die Administration deines Heimservers %@, einen TURN-Server zu konfigurieren, damit Anrufe zuverlässig funktionieren."; "call_no_stun_server_error_message_2" = "Alternativ kannst du versuchen, den öffentlichen Server unter %@ zu verwenden. Dieser wird nicht so zuverlässig sein, und deine IP-Adresse wird mit ihm geteilt. Du kannst dies auch in den Einstellungen konfigurieren"; -"service_terms_modal_message_identity_server" = "Akzeptiere die Bedingungen des Identitätsservers (%@), um Kontakte zu finden."; "identity_server_settings_alert_disconnect_still_sharing_3pid" = "Du teilst noch deine persönlichen Daten mit dem Identitätsserver %@.\n\nWir empfehlen dir deine E-Mail-Adresse und Telefonnummer zu entfernen, bevor du die Verbindung zum Identitätsserver trennst."; "settings_add_3pid_password_title_email" = "E-Mail-Adresse hinzufügen"; "settings_add_3pid_password_title_msidsn" = "Telefonnummer hinzufügen"; @@ -1315,7 +1307,6 @@ "callbar_only_multiple_paused" = "%@ pausierte Anrufe"; "callbar_only_single_paused" = "Pausierter Anruf"; "callbar_active_and_multiple_paused" = "1 aktiver Anruf (%@) · %@ pausierte Anrufe"; -"callbar_active_and_single_paused" = "1 aktiver Anruf (%1$s) · 1 pausierter Anruf"; // Call Bar "callbar_only_single_active" = "Zurück zum Anruf (%@)"; @@ -1371,7 +1362,7 @@ "user_avatar_view_accessibility_label" = "Avatar"; "secrets_recovery_with_key_information_unlock_secure_backup_with_key" = "Zum Fortfahren gib deinen Sicherheitsschlüssel ein."; -"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "Zum Forfahren gib deine Passphrase ein."; +"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "Zum Fortfahren gib deine Passphrase ein."; // Success from secure backup "key_backup_setup_success_from_secure_backup_info" = "Deine Schlüssel werden gesichert."; @@ -1642,10 +1633,9 @@ "onboarding_use_case_work_messaging" = "Teams"; "onboarding_use_case_community_messaging" = "Gemeinschaften"; /* The placeholder string contains onboarding_use_case_skip_button as a tappable action */ -"onboarding_use_case_not_sure_yet" = "Noch nicht sicher? Du kannst"; "onboarding_use_case_skip_button" = "diese Frage überspringen"; -"onboarding_use_case_existing_server_message" = "Willst du einen existierenden Server beitreten?"; -"onboarding_use_case_existing_server_button" = "MIt Server verbinden"; +"onboarding_use_case_existing_server_message" = "Willst du einem existierenden Server beitreten?"; +"onboarding_use_case_existing_server_button" = "Mit Server verbinden"; "search_filter_placeholder" = "Filtern"; @@ -1781,7 +1771,6 @@ // room display name "room_displayname_empty_room" = "Leerer Raum"; "room_displayname_two_members" = "%@ und %@"; -"room_displayname_more_than_two_members" = "%@ und %u andere"; // Settings "settings" = "Einstellungen"; "settings_enable_inapp_notifications" = "Benachrichtigungen innerhalb der App aktivieren"; @@ -1855,8 +1844,8 @@ "room_please_select" = "Bitte wähle einen Raum"; "room_error_join_failed_title" = "Konnte Raum nicht betreten"; "room_error_join_failed_empty_room" = "Es ist aktuell nicht möglich einen leeren Raum zu betreten."; -"room_error_name_edition_not_authorized" = "Du bist nicht authorisiert den Raumnamen zu ändern"; -"room_error_topic_edition_not_authorized" = "Du bist nicht authorisiert das Raumthema zu ändern"; +"room_error_name_edition_not_authorized" = "Du bist nicht autorisiert den Raumnamen zu ändern"; +"room_error_topic_edition_not_authorized" = "Du bist nicht autorisiert das Raumthema zu ändern"; "room_error_cannot_load_timeline" = "Konnte Verlauf nicht laden"; "room_error_timeline_event_not_found_title" = "Konnte Position im Verlauf nicht laden"; "room_error_timeline_event_not_found" = "Konnte spezifischen Punkt im Verlauf dieses Raumes nicht finden"; @@ -1872,7 +1861,7 @@ "message_reply_to_message_to_reply_to_prefix" = "Als Antwort auf"; // Room members "room_member_ignore_prompt" = "Sicher, dass alle Nachrichten von diesem Benutzer versteckt werden sollen?"; -"room_member_power_level_prompt" = "Du kannst diese Änderung nicht rückgangig machen, weil du dem Benutzer die gleiche Berechtigungsstufe gibst, die du selbst hast.\nBist du sicher?"; +"room_member_power_level_prompt" = "Du kannst diese Änderung nicht rückgängig machen, weil du dem Benutzer die gleiche Berechtigungsstufe gibst, die du selbst hast.\nBist du sicher?"; // Attachment "attachment_size_prompt" = "Möchtest du senden als:"; "attachment_original" = "Originalgröße (%@)"; @@ -1904,7 +1893,7 @@ "e2e_passphrase_enter" = "Passphrase eingeben"; // E2E export "e2e_export_room_keys" = "Exportiere Raumschlüssel"; -"e2e_export_prompt" = "Dieser Prozeß erlaubt den Export von Schlüsseln, die du in verschlüsselten Räumen empfangen hast, in eine lokale Datei. Du kannst dann die Datei in einem anderen Matrixclient in Zukunft importieren, so dass dieser Client die Nachrichten auch entschlüsseln kann.\nDie exportierte Datei wird jedem der sie lesen kann erlauben, alle verschlüsselten Nachrichten sehen können, also verwahre die Datei sicher."; +"e2e_export_prompt" = "Dieser Prozess erlaubt den Export von Schlüsseln, die du in verschlüsselten Räumen empfangen hast, in eine lokale Datei. Du kannst dann die Datei in einem anderen Matrixclient in Zukunft importieren, so dass dieser Client die Nachrichten auch entschlüsseln kann.\nDie exportierte Datei wird jedem der sie lesen kann erlauben, alle verschlüsselten Nachrichten sehen können, also verwahre die Datei sicher."; "e2e_export" = "Exportiere"; "e2e_passphrase_confirm" = "Passphrase bestätigen"; "e2e_passphrase_empty" = "Die Passphrase darf nicht leer sein"; @@ -2086,7 +2075,7 @@ "notification_settings_receive_a_call" = "Benachrichtige, wenn ich einen Anruf erhalte"; "notification_settings_suppress_from_bots" = "Unterdrücke Benachrichtigungen von Bots"; "notification_settings_by_default" = "Als Standard..."; -"notification_settings_notify_all_other" = "Benachrichtige für alle andereren Nachrichten/Räume"; +"notification_settings_notify_all_other" = "Benachrichtige für alle anderen Nachrichten/Räume"; // gcm section // Settings keys @@ -2114,7 +2103,7 @@ "call_more_actions_change_audio_device" = "Audiogerät ändern"; "call_more_actions_unhold" = "Fortsetzen"; "call_more_actions_hold" = "Halten"; -"call_holded" = "Du hast den Anurf pausiert"; +"call_holded" = "Du hast den Anruf pausiert"; "call_remote_holded" = "%@ hat den Anruf pausiert"; "notice_declined_video_call_by_you" = "Du hast den Anruf abgelehnt"; "notice_declined_video_call" = "%@ hat den Anruf abgelehnt"; diff --git a/Riot/Assets/en.lproj/Untranslated.strings b/Riot/Assets/en.lproj/Untranslated.strings index d1710dc24..c1cde0786 100644 --- a/Riot/Assets/en.lproj/Untranslated.strings +++ b/Riot/Assets/en.lproj/Untranslated.strings @@ -20,4 +20,33 @@ "image_picker_action_files" = "Choose from files"; +// MARK: Onboarding Authentication WIP +"authentication_registration_title" = "Create your account"; +"authentication_registration_message" = "We’ll need some info to get you set up."; +"authentication_registration_server_title" = "Choose your server to store your data"; +"authentication_registration_matrix_description" = "Join millions for free on the largest public server"; +"authentication_registration_username" = "Username"; +"authentication_registration_password" = "Password"; +"authentication_registration_username_footer" = "You can’t change this later"; +"authentication_registration_password_footer" = "Must be 8 characters or more"; + +"authentication_server_selection_title" = "Choose your server"; +"authentication_server_selection_message" = "What is the address of your server? A server is like a home for all your data."; +"authentication_server_selection_server_url" = "Server URL"; +"authentication_server_selection_server_footer" = "You can only connect to a server that has already been set up"; +"authentication_server_selection_ems_title" = "Want to host your own server?"; +/* This string will be followed by authentication_server_selection_ems_link on the next line. */ +"authentication_server_selection_ems_message" = "Element Matrix Services (EMS) is a robust and reliable hosting service for fast, secure real time communication. Find out how on"; +"authentication_server_selection_ems_link" = "element.io/ems"; +"authentication_server_selection_ems_button" = "Get in touch"; +"authentication_server_selection_generic_error" = "Cannot find a server at this URL, please check it is correct."; + +// MARK: Spaces WIP "spaces_feature_not_available" = "This feature isn't available here. For now, you can do this with %@ on your computer."; + +"leave_space_action" = "Leave space"; +"leave_space_and_one_room" = "Leave space and 1 room"; +"leave_space_and_more_rooms" = "Leave space and %@ rooms"; +"leave_space_selection_title" = "SELECT ROOMS"; +"leave_space_selection_all_rooms" = "Select all rooms"; +"leave_space_selection_no_rooms" = "Select no rooms"; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 48f3859ee..5424d7666 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -76,6 +76,7 @@ "error" = "Error"; "suggest" = "Suggest"; "edit" = "Edit"; +"confirm" = "Confirm"; // Activities "loading" = "Loading"; @@ -2138,10 +2139,28 @@ Tap the + to start adding people."; "location_sharing_live_share_title" = "Share live location"; "live_location_sharing_banner_title" = "Live location enabled"; +"live_location_sharing_ended" = "Live location ended"; "live_location_sharing_banner_stop" = "Stop"; "location_sharing_static_share_title" = "Send my current location"; "location_sharing_pin_drop_share_title" = "Send this location"; "location_sharing_live_map_callout_title" = "Share location"; +"location_sharing_live_viewer_title" = "Location"; +"location_sharing_live_list_item_time_left" = "%@ left"; +"location_sharing_live_list_item_sharing_expired" = "Sharing expired"; +"location_sharing_live_list_item_last_update" = "Updated %@ ago"; +"location_sharing_live_list_item_last_update_invalid" = "Unknown last update"; +"location_sharing_live_list_item_current_user_display_name" = "You"; +"location_sharing_live_list_item_stop_sharing_action" = "Stop sharing"; +"location_sharing_live_timer_incoming" = "Live until %@"; +"location_sharing_live_loading" = "Loading Live location..."; +"location_sharing_live_error" = "Live location error"; +"location_sharing_live_timer_selector_title" = "Choose for how long others will see your accurate location."; +"location_sharing_live_timer_selector_short" = "for 15 minutes"; +"location_sharing_live_timer_selector_medium" = "for 1 hour"; +"location_sharing_live_timer_selector_long" = "for 8 hours"; +"location_sharing_live_no_user_locations_error_title" = "No user locations available"; +"location_sharing_live_stop_sharing_error" = "Fail to stop sharing location"; +"location_sharing_live_stop_sharing_progress" = "Stop location sharing"; // MARK: - MatrixKit diff --git a/Riot/Assets/eo.lproj/Vector.strings b/Riot/Assets/eo.lproj/Vector.strings index 826fb2892..b0ae5a061 100644 --- a/Riot/Assets/eo.lproj/Vector.strings +++ b/Riot/Assets/eo.lproj/Vector.strings @@ -312,8 +312,6 @@ "joined" = "Aliĝita"; "device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Uzi rehavajn pasfrazon aŭ ŝlosilon"; "device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Uzi rehavan ŝlosilon"; -"device_verification_self_verify_wait_additional_information" = "Ĉi tio funkcias por Element kaj aliaj klientoj kapablaj je delegaj subskriboj."; -"device_verification_self_verify_wait_information" = "Kontrolu ĉi tiun saluton per unu el viaj aliaj salutaĵoj, dononte al ĝi aliron al ĉifritaj mesaĝoj.\n\nUzu la plej freŝan version de Element per viaj aliaj aparatoj:"; "device_verification_self_verify_wait_new_sign_in_title" = "Kontrolu ĉi tiun saluton"; // MARK: Self verification wait @@ -347,7 +345,6 @@ "device_verification_incoming_description_2" = "Kontrolo de ĉi tiu salutaĵo markos ĝin fidata, kaj markos ankaŭ vian salutaĵon fidata por la kunulo."; "secure_key_backup_setup_intro_use_security_key_title" = "Uzi Sekurecan ŝlosilon"; "secure_key_backup_setup_intro_info" = "Malhelpu perdon de aliro al ĉifritaj mesaĝoj kaj datumoj per savkopiado de ŝlosiloj al via servilo."; -"rerequest_keys_alert_message" = "Bonvolu ruli Elementon per alia aparato, kiu povas malĉifri la mesaĝon, por ke ĝi povu resendi la ŝlosilojn al ĉi tiu salutaĵo."; // Re-request confirmation dialog "rerequest_keys_alert_title" = "Peto sendiĝis"; @@ -367,7 +364,6 @@ "deactivate_account_title" = "Malaktivigi konton"; "service_terms_modal_policy_checkbox_accessibility_hint" = "Kontrolu por akcepti %@"; -"service_terms_modal_message_identity_server" = "Akceptu la uzokondiĉojn de la identiga servilo (%@) por trovi kontaktojn."; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "Trovado de kontaktoj"; @@ -376,7 +372,6 @@ "service_terms_modal_description_for_identity_server_1" = "Trovi aliajn per telefono aŭ retpoŝtadreso"; "service_terms_modal_decline_button" = "Rifuzi"; "service_terms_modal_accept_button" = "Akcepti"; -"service_terms_modal_message" = "Por daŭrigi, vi devas akcepti la uzokondiĉojn de ĉi tiu servo (%@)."; // Service terms "service_terms_modal_title" = "Uzokondiĉoj"; @@ -452,10 +447,8 @@ "e2e_need_log_in_again" = "Vi bezonas resaluti por estigi tutvoje ĉifrajn ŝlosilojn porĉi tiu salutaĵo, kaj sendi la publikan ŝlosilon al via hejmservilo.\nTio necesas nur unufoje; pardonu la ĝenon."; // Crypto -"e2e_enabling_on_app_update" = "Element nun subtenas tutvojan ĉifradon, sed vi bezonas resaluti por ŝalti ĝin.\n\nVi povas fari tion nun, aŭ pli poste per agordoj de la aplikaĵo."; // Crash report -"google_analytics_use_prompt" = "Ĉu vi volus nin helpi pri plibonigoj al %@ per memaga kaj sennoma raportado de fiaskoj kaj datumoj pri uzado?"; "no_voip" = "%@ vin vokas, sed %@ ankoraŭ ne subtenas vokojn.\nVi povas malatenti ĉi tiun sciigon kaj respondi la vokon per alia aparato, aŭ vi povas ĝin rifuzi."; // No VoIP support @@ -656,7 +649,6 @@ // MARK: - Major update -"major_update_title" = "Riot nun estas Element"; "cross_signing_setup_banner_subtitle" = "Kontrolu aliajn viajn aparatojn pli facile"; // MARK: - Cross-signing @@ -975,7 +967,6 @@ "settings_integrations_allow_button" = "Administri kunigojn"; "settings_calls_stun_server_fallback_description" = "Permesi repaŝan servilon %@ asistan je vokoj, kiam la hejmservilo ne provizas servilon (via IP-adreso ne doniĝus dum voko)."; "settings_calls_stun_server_fallback_button" = "Permesi repaŝan servilon asistan je vokoj"; -"settings_callkit_info" = "Ricevi vokpetojn ĉe via ŝlosa ekrano. Vidi viajn vokojn de Element ĉe la sistema vokhistorio. Se iCloud estas ŝaltita, la vokhistorio doniĝas ankaŭ al Apple."; "settings_pin_rooms_with_unread" = "Alpingli ĉambrojn kun nelegitaj mesaĝoj"; "settings_pin_rooms_with_missed_notif" = "Alpinigli ĉambrojn kun nerimarkitaj sciigoj"; "settings_show_decrypted_content" = "Montri malĉifritajn enhavojn"; @@ -1145,7 +1136,6 @@ "identity_server_settings_alert_error_terms_not_accepted" = "Vi devas akcepti la uzokondiĉojn de %@, por uzi ĝin kiel identiga servilo."; "identity_server_settings_alert_disconnect" = "Ĉu malkonektu de la identiga servilo %@?"; "identity_server_settings_alert_disconnect_title" = "Malkonektu identigan servilon"; -"identity_server_settings_alert_change" = "Ĉu malkonektu de identiga servilo %1$@ kaj anstataŭe konektu al %@$A?"; "identity_server_settings_alert_change_title" = "Ŝanĝu identigan servilon"; "identity_server_settings_alert_no_terms" = "Via elektita identiga servilo ne havas uzokondiĉojn. Daŭrigu nur se vi fidas la posedanton de tiu servilo."; "identity_server_settings_alert_no_terms_title" = "Identiga servilo ne havas uzokondiĉojn"; @@ -1171,7 +1161,6 @@ // Manage session "manage_session_title" = "Administru salutaĵon"; "security_settings_user_password_description" = "Konfirmi vian identon per enigo de via pasvorto"; -"security_settings_coming_soon" = "Bedaŭron. Ĉi tiu funkcio ne jam estas subtenata por Element iOS. Bonvolu uzi alian Matrix klienton por agordi ĝin. Element iOS sekvos ĝin."; "security_settings_complete_security_alert_message" = "Vi kompletigu sekurigon je ĉi tiu salutaĵo unue."; "security_settings_complete_security_alert_title" = "Kompletigi sekurigon"; "security_settings_blacklist_unverified_devices_description" = "Kontroli ĉiujn salutaĵojn de uzanto, por marki ilin fidata kaj sendi mesaĝojn al ĝi."; @@ -1268,7 +1257,6 @@ "room_participants_filter_room_members_for_dm" = "Filtri ĉambranojn"; "room_participants_add_participant" = "Aldoni partoprenanton"; "contacts_user_directory_offline_section" = "KATALOGO DE UZANTOJ (nefunkcia)"; -"contacts_address_book_permission_denied" = "Vi ne permesis al Element aliri viajn lokajn kontaktojn"; "rooms_empty_view_information" = "Ĉambroj taŭgas por ajna grupbabilo, privata aŭ publika. Premu la + por trovi ekzistantaj ĉambroj, aŭ fari novajn."; "rooms_empty_view_title" = "Ĉambroj"; "people_empty_view_information" = "Sekure babili kun iu ajn.Premu la + por inviti personojn."; @@ -1416,7 +1404,6 @@ "event_formatter_call_back" = "Revoki"; "event_formatter_call_you_declined" = "Vi rifuzis ĉi tiun vokon"; "event_formatter_call_you_currently_in" = "Aktiva voko"; -"event_formatter_call_has_ended" = "Finiĝis %@"; "event_formatter_call_video" = "Vidvoko"; "event_formatter_call_voice" = "Voĉvoko"; diff --git a/Riot/Assets/es.lproj/Localizable.strings b/Riot/Assets/es.lproj/Localizable.strings index eadd4af14..5b921c2f2 100644 --- a/Riot/Assets/es.lproj/Localizable.strings +++ b/Riot/Assets/es.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ te envió una imagen %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ publicó una imagen %@ en %@"; /* A single unread message in a room */ diff --git a/Riot/Assets/es.lproj/Vector.strings b/Riot/Assets/es.lproj/Vector.strings index 269c6bce9..53e40f242 100644 --- a/Riot/Assets/es.lproj/Vector.strings +++ b/Riot/Assets/es.lproj/Vector.strings @@ -66,7 +66,6 @@ "deactivate_account_password_alert_message" = "Para continuar, introduce la contraseña de tu cuenta de Matrix"; "deactivate_account_forget_messages_information_part2_emphasize" = "Advertencia"; "deactivate_account_informations_part2_emphasize" = "Esta acción es irreversible."; -"rerequest_keys_alert_message" = "Por favor, abre Element en otro dispositivo que pueda descifrar el mensaje para que envíe las claves a esta sesión."; // Re-request confirmation dialog "rerequest_keys_alert_title" = "Solicitud Enviada"; "auth_password_dont_match" = "Las contraseñas no coinciden"; @@ -495,9 +494,7 @@ "no_voip_title" = "Llamada entrante"; "no_voip" = "%@ te está llamando pero %@ aún no admite llamadas.\nPuedes ignorar esta notificación y contestar la llamada desde otro dispositivo o puedes rechazarla."; // Crash report -"google_analytics_use_prompt" = "¿Te gustaría ayudar a mejorar %@ enviando automáticamente informes de fallas y datos de uso anónimos?"; // Crypto -"e2e_enabling_on_app_update" = "Element ahora admite cifrado de extremo a extremo, pero debes volver a iniciar sesión para activarlo.\n\nPuedes hacerlo ahora o más tarde desde los ajustes de la aplicación."; "e2e_need_log_in_again" = "Tienes que volver a iniciar sesión para generar claves de cifrado de extremo a extremo para este dispositivo y enviar la clave pública a tu servidor base.\nSolo hay que hacer esto una vez, disculpa las molestias."; // Bug report "bug_report_title" = "Informe de Error"; @@ -740,7 +737,6 @@ "notice_room_aliases" = "Los aliases de la sala son: %@"; "notice_room_related_groups" = "Los grupos asociados a esta sala son: %@"; "notice_encrypted_message" = "Mensaje cifrado"; -"notice_encryption_enabled" = "%@ activó el cifrado de extremo a extremo (algoritmo %@)"; "notice_image_attachment" = "imagen adjunta"; "notice_audio_attachment" = "audio adjunto"; "notice_video_attachment" = "vídeo adjunto"; @@ -764,7 +760,6 @@ // room display name "room_displayname_empty_room" = "Sala vacía"; "room_displayname_two_members" = "%@ y %@"; -"room_displayname_more_than_two_members" = "%@ y otros %u"; // Settings "settings" = "Ajustes"; "settings_enable_inapp_notifications" = "Habilitar notificaciones de la aplicación"; @@ -1876,7 +1871,7 @@ // MARK: - Favourites "favourites_empty_view_title" = "Salas y personas favoritas"; -"home_syncing" = "Sincronización"; +"home_syncing" = "Sincronizando"; "home_context_menu_leave" = "Salir"; "home_context_menu_normal_priority" = "Prioridad normal"; "home_context_menu_low_priority" = "Prioridad baja"; @@ -1911,19 +1906,19 @@ "room_info_list_one_member" = "1 miembro"; "create_room_placeholder_address" = "#saladepruebas:matrix.org"; -"create_room_section_header_address" = "Dirección de la sala"; +"create_room_section_header_address" = "DIRECCIÓN"; "create_room_show_in_directory" = "Incluir en la lista pública de salas"; "create_room_section_footer_type" = "La gente solo puede unirse a una sala privada si les invitas."; -"create_room_type_public" = "Sala pública"; -"create_room_type_private" = "Sala privada"; -"create_room_section_header_type" = "Tipo de sala"; +"create_room_type_public" = "Sala pública (cualquiera)"; +"create_room_type_private" = "Sala privada (por invitación)"; +"create_room_section_header_type" = "QUIÉN PUEDE ACCEDER"; "create_room_section_footer_encryption" = "Una vez la actives, no podrás desactivarla."; "create_room_enable_encryption" = "Activar cifrado"; -"create_room_section_header_encryption" = "Cifrado de la sala"; -"create_room_placeholder_topic" = "Asunto"; -"create_room_section_header_topic" = "Asunto de la sala (opcional)"; +"create_room_section_header_encryption" = "CIFRADO"; +"create_room_placeholder_topic" = "¿De qué va esta sala?"; +"create_room_section_header_topic" = "ASUNTO (OPCIONAL)"; "create_room_placeholder_name" = "Nombre"; -"create_room_section_header_name" = "Nombre de la sala"; +"create_room_section_header_name" = "NOMBRE"; // MARK: - Create Room @@ -2227,3 +2222,174 @@ "rooms_empty_view_information" = "Las salas son buena opción para grupos públicos o privados. Toca el + para encontrar salas existentes o crear nuevas."; "contacts_address_book_no_identity_server" = "No hay un servidor de identidad configurado"; "find_your_contacts_identity_service_error" = "No se ha podido conectar con el servidor de identidad."; +"stop" = "Parar"; +"room_displayname_more_than_two_members" = "%@ y %@ más"; +"ignore_user" = "Ignorar usuario"; +"location_sharing_live_list_item_stop_sharing_action" = "Dejar de compartir"; +"location_sharing_live_list_item_current_user_display_name" = "Tú"; +"location_sharing_live_list_item_last_update_invalid" = "Última actualización desconocida"; +"location_sharing_live_list_item_last_update" = "Actualizado hace %@"; +"location_sharing_live_list_item_sharing_expired" = "Caducado"; +"location_sharing_live_list_item_time_left" = "queda %@"; +"location_sharing_live_viewer_title" = "Ubicación"; +"location_sharing_live_map_callout_title" = "Compartir ubicación"; +"location_sharing_pin_drop_share_title" = "Enviar esta ubicación"; +"location_sharing_static_share_title" = "Enviar mi ubicación actual"; +"live_location_sharing_banner_stop" = "Detener"; +"live_location_sharing_banner_title" = "Compartiendo ubicación en tiempo real"; + +// MARK: Live location sharing + +"location_sharing_live_share_title" = "Compartir ubicación en tiempo real"; +"side_menu_coach_message" = "Desliza hacia la derecha o toca para ver todas las salas"; +"spaces_add_room_missing_permission_message" = "No tienes permiso para añadir salas a este espacio."; +"spaces_creation_in_one_space" = "en 1 espacio"; +"spaces_creation_in_many_spaces" = "en %@ espacios"; +"spaces_creation_in_spacename_plus_many" = "en %@ + %@ espacios"; +"spaces_creation_in_spacename_plus_one" = "en %@ + 1 espacio"; +"spaces_creation_in_spacename" = "en %@"; +"spaces_creation_post_process_inviting_users" = "Invitando a %@ personas"; +"spaces_creation_post_process_adding_rooms" = "Añadiendo %@ salas"; +"spaces_creation_post_process_creating_room" = "Creando %@"; +"spaces_creation_post_process_uploading_avatar" = "Enviando avatar"; +"spaces_creation_post_process_creating_space_task" = "Creando %@"; +"spaces_creation_post_process_creating_space" = "Creando espacio"; +"spaces_creation_invite_by_username_message" = "También puedes invitarles en otro momento."; +"spaces_creation_invite_by_username_title" = "Invita a tu equipo"; +"spaces_creation_invite_by_username" = "Invitar por nombre de usuario"; +"spaces_creation_add_rooms_message" = "Como este espacio es solo para ti, nadie más se enterará. Puedes añadir más en otro momenro."; +"spaces_creation_add_rooms_title" = "¿Qué quieres añadir?"; +"spaces_creation_sharing_type_me_and_teammates_detail" = "Un espacio privado para tu equipo y tú"; +"spaces_creation_sharing_type_me_and_teammates_title" = "Mi equipo y yo"; +"spaces_creation_sharing_type_just_me_detail" = "Un espacio privado para organizar tus salas"; +"spaces_creation_sharing_type_just_me_title" = "Solo yo"; +"spaces_creation_sharing_type_message" = "Asegúrate de que la gente apropiada puede acceder a %@. Puedes cambiarlo más adelante."; +"spaces_creation_sharing_type_title" = "¿Con quién vas a trabajar?"; +"spaces_creation_email_invites_email_title" = "Email"; +"spaces_creation_email_invites_message" = "También puedes invitarles más tarde."; +"spaces_creation_email_invites_title" = "Invita a tu equipo"; +"spaces_creation_new_rooms_support" = "Ayuda"; +"spaces_creation_new_rooms_random" = "Otros"; +"spaces_creation_new_rooms_general" = "General"; +"spaces_creation_new_rooms_room_name_title" = "Nombre de la sala"; +"spaces_creation_new_rooms_message" = "Crearemos una sala para cada tema."; +"spaces_creation_new_rooms_title" = "¿De qué se hablará?"; +"spaces_creation_cancel_message" = "Perderás el progreso."; +"spaces_creation_cancel_title" = "¿Parar de crear un espacio?"; +"spaces_creation_private_space_title" = "Tu espacio privado"; +"spaces_creation_public_space_title" = "Tu espacio público"; +"spaces_creation_address_already_exists" = "%@\nya existe"; +"spaces_creation_address_invalid_characters" = "%@\ntiene caracteres no válidos"; +"spaces_creation_address_default_message" = "Tu espacio se podrá ver desde\n%@"; +"spaces_creation_empty_room_name_error" = "El nombre es necesario"; +"spaces_creation_address" = "Dirección"; +"spaces_creation_settings_message" = "Añade algún detalle para ayudar a que destaque. Lo puedes cambiar en cualquier momento."; +"spaces_creation_footer" = "Puedes cambiarlo más adelante"; +"spaces_creation_visibility_message" = "Para unirte a un espacio que ya exista, pide que te inviten a él."; +"spaces_creation_visibility_title" = "¿Qué tipo de espacio quieres crear?"; + +// Mark: - Space Creation + +"spaces_creation_hint" = "Los espacios son una nueva manera de agrupar salas y personas."; +"space_settings_current_address_message" = "Tu espacio es accesible desde\n%@"; +"space_settings_update_failed_message" = "Fallo al actualizar los ajustes del espacio. ¿Quieres reintentarlo?"; +"space_settings_access_section" = "¿Quién puede acceder a este espacio?"; +"space_topic" = "Descripción"; +"space_public_join_rule_detail" = "Público, recomendado para comunidades"; +"spaces_add_space" = "Añadir espacio"; +"spaces_add_room" = "Añadir sala"; +"spaces_invite_people" = "Invitar genre"; +"space_private_join_rule_detail" = "Solo por invitación (recomendado si es para ti o para equipos)"; +"spaces_explore_rooms_one_room" = "1 sala"; +"spaces_explore_rooms_room_number" = "%@ salas"; +"spaces_create_space_title" = "Crear un espacio"; +"spaces_add_space_title" = "Crear espacio"; +"room_invite_not_enough_permission" = "No tienes permiso para invitar a esta sala"; +"space_invite_not_enough_permission" = "No tienes permiso para invitar a este espacio"; +"room_invite_to_room_option_detail" = "No serán parte de %@."; +"room_invite_to_room_option_title" = "Solo para esta sala"; +"room_invite_to_space_option_detail" = "Pueden explorar %@, pero no serán miembros de %@."; + +// Mark: - Room invite + +"room_invite_to_space_option_title" = "Para %@"; +"share_invite_link_space_text" = "Oye, únete a este espacio en %@"; +"share_invite_link_room_text" = "Oye, únete a esta sala en %@"; + +// MARK: - Share invite link + +"share_invite_link_action" = "Compartir enlace de invitación"; +"create_room_processing" = "Creando sala"; +"create_room_suggest_room" = "Sugerir a los miembros del espacio"; +"create_room_show_in_directory_footer" = "Esto ayudará a que la gente la encuentre y se una."; +"create_room_promotion_header" = "PROMOCIÓN"; +"create_room_section_footer_type_private" = "Solo las personas invitadas pueden encontrarla y unirse."; +"create_room_type_restricted" = "Miembros del espacio"; +"rerequest_keys_alert_message" = "Por favor, abre %@ en otro dispositivo para recibir las claves en esta sesión y poder descifrar los mensajes."; + +// Crypto +"e2e_enabling_on_app_update" = "Ahora, %@ ofrece cifrado de extremo a extremo. Para usarlo, tienes que volver a iniciar sesión y activarlo.\n\nPuedes hacerlo ahora, o más adelante desde los ajustes de la aplicación."; +"call_jitsi_unable_to_start" = "No ha sido posible empezar la llamada en grupo"; +"room_suggestion_settings_screen_message" = "Los miembros del espacio verán la sala como sugerida para unirse."; +"room_suggestion_settings_screen_title" = "Sugiere una sala dentro de un espacio"; + +// Room suggestion Settings +"room_suggestion_settings_screen_nav_title" = "Sugerir sala"; +"room_access_space_chooser_other_spaces_section" = "Otros espacios o salas"; +"room_access_space_chooser_known_spaces_section" = "Espacios que conoces e incluyen %@"; +"room_access_settings_screen_setting_room_access" = "Configurando acceso"; +"room_access_settings_screen_upgrade_alert_upgrading" = "Actualizando la sala"; +"room_access_settings_screen_upgrade_alert_upgrade_button" = "Actualizar"; +"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "Invitar miembros automáticamente a la nueva sala"; +"room_access_settings_screen_upgrade_alert_note" = "Al actualizar, se crea una nueva versión de la sala. Todos los mensajes actuales se quedarán archivados aquí."; +"room_access_settings_screen_upgrade_alert_message" = "Cualquiera en %@ podrá encontrar la sala y unirse a ella, sin que tengas que invitarle manualmente. Puedes cambiar esto cuando quieras en los ajustes de la sala."; +"room_access_settings_screen_upgrade_alert_title" = "Actualizar sala"; +"room_access_settings_screen_public_message" = "Cualquiera puede encontrarla y unirse."; +"room_access_settings_screen_edit_spaces" = "Editar espacios"; +"room_access_settings_screen_upgrade_required" = "Actualización necesaria"; +"room_access_settings_screen_restricted_message" = "Permitir que cualquiera en un ciertos espacios la encuentre y se una.\nTendrás que elegir qué espacios."; +"room_access_settings_screen_private_message" = "Solo las personas invitadas pueden encontrar y unirse."; +"room_access_settings_screen_message" = "Decide quién puede encontrar y unirse a %@."; +"room_access_settings_screen_title" = "¿Quién puede acceder a esta sala?"; + +// Room Access Settings +"room_access_settings_screen_nav_title" = "Acceso a la sala"; +"room_details_promote_room_suggest_title" = "Sugerir a los miembros del espacio"; +"room_details_access_row_title" = "Acceso"; +"settings_presence_offline_mode_description" = "Si lo activas, los demás siempre te verán como desconectado, incluso mientras usas la aplicación."; +"settings_presence_offline_mode" = "Modo desconectado"; +"settings_presence" = "Presencia"; +"settings_labs_enable_auto_report_decryption_errors" = "Informar automáticamente de errores al descifrar"; +"room_preview_decline_invitation_options" = "¿Quieres rechazar la invitación o ignorar a quien te ha invitado?"; +"threads_discourage_information_2" = "\n\n¿Activar los hilos de todas formas?"; +"threads_discourage_information_1" = "Tu servidor base todavía no es compatible con los hilos, por lo que pueden funcionar de forma inesperada. Algunos hilos estarán disponibles de forma estable. "; +"threads_beta_cancel" = "Ahora no"; +"threads_beta_enable" = "Probar"; +"threads_beta_information_link" = "Más información"; +"threads_beta_information" = "Mantén las conversaciones organizadas con hilos.\n\nLos hilos te ayudan a mantener centradas las conversaciones y a seguirlas fácilmente. "; +"threads_notice_title" = "Los hilos ya no son un experimento 🎉"; +"threads_beta_title" = "Hilos"; +"threads_notice_done" = "Vale"; +"onboarding_celebration_button" = "Continuar"; +"onboarding_celebration_message" = "Hemos guardado tus preferencias."; +"onboarding_celebration_title" = "¡Ya estás!"; +"onboarding_avatar_accessibility_label" = "Imagen de perfil"; +"onboarding_display_name_max_length" = "Tu nombre público debe tener menos de 256 caracteres"; +"onboarding_display_name_hint" = "Puedes cambiarlo más adelante"; +"onboarding_display_name_placeholder" = "Nombre público"; +"onboarding_display_name_message" = "Esto aparecerá junto a tus mensajes."; +"onboarding_display_name_title" = "Elige tu nombre público"; +"onboarding_personalization_skip" = "Saltar este paso"; +"onboarding_personalization_save" = "Guardar y continuar"; +"onboarding_congratulations_home_button" = "Ir al inicio"; +"onboarding_congratulations_personalize_button" = "Personalizar perfil"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "Has creado tu cuenta, %@."; +"onboarding_congratulations_title" = "¡Enhorabuena!"; +"saving" = "Guardando"; + +// Activities +"loading" = "Cargando"; +"edit" = "Editar"; +"add" = "Añadir"; +"new_word" = "Nuevo"; diff --git a/Riot/Assets/et.lproj/Localizable.strings b/Riot/Assets/et.lproj/Localizable.strings index 9b128fe11..9f1002800 100644 --- a/Riot/Assets/et.lproj/Localizable.strings +++ b/Riot/Assets/et.lproj/Localizable.strings @@ -7,7 +7,6 @@ /* New message from a specific person in a named room. Content included. */ "MSG_FROM_USER_IN_ROOM_WITH_CONTENT" = "%@ kirjutas jututoas %@: %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ saatis pildi %@"; /* A single unread message in a room */ "SINGLE_UNREAD_IN_ROOM" = "Sa said sõnumi jututoas %@"; /* A single unread message */ diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index ff6b4bcaa..affe29d9d 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -742,8 +742,6 @@ "directory_cell_title" = "Sirvi kataloogi"; "directory_cell_description" = "%tu jututuba"; "directory_search_results_title" = "Sirvi tulemusi kataloogist"; -"directory_search_results" = "%@ kohta leidsin %tu tulemust"; -"directory_search_results_more_than" = ">%@ kohta leidsin %tu tulemust"; "directory_searching_title" = "Otsin kataaloogist…"; "directory_search_fail" = "Andmete laadimine ei õnnestunud"; // Contacts @@ -777,7 +775,6 @@ "gdpr_consent_not_given_alert_review_now_action" = "Vaata üle"; // Service terms "service_terms_modal_title" = "Kasutustingimused"; -"service_terms_modal_message" = "Jätkamaks pead nõustuma selle teenuse kasutustingimustega(%@)."; "service_terms_modal_accept_button" = "Nõustu"; "service_terms_modal_decline_button" = "Keeldu"; "service_terms_modal_description_for_identity_server_1" = "Leia teisi kasutajaid telefoninumbri või e-posti aadressi alusel"; @@ -785,7 +782,6 @@ "service_terms_modal_description_for_integration_manager" = "Kasuta roboteid, võrgusildu, vidinaid või kleepsupakke"; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "Kasutajate leidmine"; -"service_terms_modal_message_identity_server" = "Väliste kasutajate leidmiseks pead nõustuma isikutuvastusserveri (%@) kasutustingimustega."; "service_terms_modal_policy_checkbox_accessibility_hint" = "Nõustumaks %@ tingimustega tee siia märge"; "key_backup_setup_skip_alert_message" = "Kui sa logid välja või kaotad seadme, siis sa ei saa enam lugeda oma krüptitud sõnumeid."; "key_backup_setup_intro_info" = "Sõnumid krüptitud jututubades kasutavad läbivat krüptimist. Ainult sinul ja saaja(te)l on võtmed selliste sõnumite lugemiseks.\n\nVältimaks krüptovõtmete kadumist, varunda nad turvaliselt."; @@ -988,7 +984,6 @@ "room_details_addresses_disable_main_address_prompt_msg" = "Sel jututoal ei saa olema põhiaadressi. Seetõttu valitakse vaikimisi põhiaadress juhuslikult"; "room_details_flair_section" = "Näita kogukondade rinnasilte"; // Crash report -"google_analytics_use_prompt" = "Kas sa soovid aitata parandada %@ rakendust saates arendajatele automaatseid ja anonüümsed veateateid ning kasutusteavet?"; // Crypto "e2e_enabling_on_app_update" = "%@ nüüd toetab läbivat krüptimist ning selle kasutusele võtmiseks peaksid sa uuesti sisse logima.\n\nSa võid teha seda kohe või määrata rakenduse seadistustes."; "e2e_need_log_in_again" = "Selle sessiooni läbiva krüptimise võtmete loomiseks ja avaliku võtme salvestamiseks koduserverisse peaksid sa uuesti sisse logima.\nVabandame ebamugavuse pärast, aga see õnneks on ühekordne toiming."; @@ -1146,19 +1141,19 @@ "searchable_directory_x_network" = "%@ võrk"; "searchable_directory_search_placeholder" = "Nimi või Matrix'i tunnus"; "create_room_title" = "Uus jututuba"; -"create_room_section_header_name" = "Jututoa nimi"; +"create_room_section_header_name" = "NIMI"; "create_room_placeholder_name" = "Nimi"; -"create_room_section_header_topic" = "Jututoa teema (kui soovid lisada)"; -"create_room_placeholder_topic" = "Jututoa teema"; -"create_room_section_header_encryption" = "Krüptimine jututoas"; +"create_room_section_header_topic" = "TEEMA (kui soovid lisada)"; +"create_room_placeholder_topic" = "Millest siin jututoas räägitakse?"; +"create_room_section_header_encryption" = "KRÜPTIMINE"; "create_room_enable_encryption" = "Võta krüptimine kasutusele"; "create_room_section_footer_encryption" = "Krüptimist ei saa hiljem välja lülitada."; -"create_room_section_header_type" = "Jututoa tüüp"; -"create_room_type_private" = "Omavaheline jututuba"; -"create_room_type_public" = "Avalik jututuba"; +"create_room_section_header_type" = "KES PÄÄSEB SIIA LIGI"; +"create_room_type_private" = "Privaatne jututuba (kutse alusel)"; +"create_room_type_public" = "Avalik jututuba (kõigile)"; "create_room_section_footer_type" = "Omavahelise jututoaga saab liituda vaid kutsega."; -"create_room_show_in_directory" = "Näita jututubade loendit"; -"create_room_section_header_address" = "Jututoa aadress"; +"create_room_show_in_directory" = "Näita jututubade loendis"; +"create_room_section_header_address" = "AADRESS"; "create_room_placeholder_address" = "#torenimi:domeen.ee"; "room_info_list_room_encrypted" = "See jututuba on läbivalt krüptitud"; "room_info_list_one_member" = "1 liige"; @@ -1306,12 +1301,10 @@ // Chat "room_slide_to_end_group_call" = "Viipa kõne lõpetamiseks kõigi jaoks"; "callbar_only_single_active_group" = "Rühmakõnega liitumiseks puuduta (%@)"; -"space_beta_announce_information" = "Kogukonnakeskused on uus viis inimeste ja jututubade sidumiseks. Neid veel ei saa iOS'is kasutada, kuid nad on olemas %@'i veebirakenduses ja töölauarakenduses."; "space_beta_announce_subtitle" = "Uus versioon senistest kogukondadest"; "space_beta_announce_title" = "Kogukonnakeskused saavad varsti olema ka siin"; "space_beta_announce_badge" = "BEETA"; "space_feature_unavailable_information" = "Kogukonnakeskused on uus viis inimeste ja jututubade sidumiseks.\n\nNad saavad varsti olema kasutusel ka siin keskkonnas. Kui praegu liitud mõne kogukonnakeskusega mõnes muus keskkonnas, siis pääsed ligi kõikidele seotud jututubadele ka siin."; -"space_feature_unavailable_subtitle" = "Kogukonnakeskuseid ei saa iOS'is kasutada, kuid nad on olemas %@'i veebirakenduses ja töölauarakenduses"; // Mark: - Spaces @@ -1380,9 +1373,7 @@ "version_check_modal_action_title_supported" = "Selge lugu"; "version_check_modal_subtitle_supported" = "Me oleme arendanud %@'i kiiremaks ja mugavamaks. Sinu praegune iOS'i versioon ei oska kõiki neid uuendusi kasutada ja tema tugi on lõppemas.\nKui soovid kasutada %@'i kõiki võimalusi, siis palun uuenda oma iOS'i versiooni."; "version_check_modal_title_supported" = "Rakenduse kasutamise võimalus iOS'i versioonis %@ on lõppemas"; -"version_check_banner_subtitle_deprecated" = "Me oleme lõpetanud selle rakenduse toe IOS'i versioonis %@. Kui soovid kasutada %@'i kõiki võimalusi, siis palun uuenda oma iOS'i versiooni."; "version_check_banner_title_deprecated" = "Rakenduse kasutamise võimalus iOS'i versioonis %@ on lõppenud"; -"version_check_banner_subtitle_supported" = "Me üsna varsti lõpetame selle rakenduse toe IOS'i versioonis %@. Kui soovid kasutada %@'i kõiki võimalusi, siis palun uuenda oma iOS'i versiooni."; // Mark: - Version check @@ -1419,7 +1410,7 @@ "space_public_join_rule" = "Avalik kogukond"; "space_private_join_rule" = "Privaatne kogukond"; "space_participants_action_ban" = "Sea selles kogukonnakeskus suhtluskeeld"; -"space_participants_action_remove" = "Eemalda sellest kogukonnakeskusest"; +"space_participants_action_remove" = "Eemalda sellest kogukonnast"; "spaces_coming_soon_detail" = "See funktsionaalsus pole siin rakenduses hetkel veel saadaval, aga üsna varsti saab olema. Seni saad sa seda toimingut teha %@'i töölauarakenduses."; "spaces_invites_coming_soon_title" = "Varsti lisandub kutsete saatmine"; "spaces_add_rooms_coming_soon_title" = "Varsti on jututubade lisamine võimalik"; @@ -2067,7 +2058,6 @@ "attachment_small_with_resolution" = "Väiksena %@ (~%@)"; "attachment_size_prompt_message" = "Seadistustest saad määrata, et see funktsionaalsus pole kasutusel."; "attachment_size_prompt_title" = "Saatmiseks kinnita meedia suurus"; -"room_displayname_all_other_participants_left" = "%@ (lahkus(id))"; "room_displayname_all_other_members_left" = "%@ (lahkus(id))"; "attachment_unsupported_preview_message" = "See failitüüp ei ole toetatud."; "attachment_unsupported_preview_title" = "Eelvaate kuvamine ei õnnestu"; @@ -2077,3 +2067,186 @@ "room_participants_leave_processing" = "Lahkumine"; "notice_error_unformattable_event" = "** Sõnumi töötlemine ei õnnestu. Palun anna meile sellest veast teada"; "settings_labs_use_only_latest_user_avatar_and_name" = "Sõnumite ajaloos leiduvate kasutajate puhul näita viimati kasutatud tunnuspilti ning nime"; +"ignore_user" = "Eira kasutajat"; +"location_sharing_pin_drop_share_title" = "Jaga seda asukohta"; +"location_sharing_static_share_title" = "Jaga minu praegust asukohta"; +"live_location_sharing_banner_stop" = "Lõpeta asukoha jagamine"; +"live_location_sharing_banner_title" = "Reaalajas asukoha jagamine on kasutusel"; + +// MARK: Live location sharing + +"location_sharing_live_share_title" = "Jaga asukohta reaalajas"; +"side_menu_coach_message" = "Kõikide jututubade nägemiseks viipa paremale või klõpsi"; +"spaces_add_room_missing_permission_message" = "Sul pole õigusi siia kogukonda lisada jututubasid."; +"spaces_creation_in_one_space" = "1's kogukonnas"; +"spaces_creation_in_many_spaces" = "%@ kogukonnas"; +"spaces_creation_in_spacename_plus_many" = "%@ ja veel %@'s kogukonnas"; +"spaces_creation_in_spacename_plus_one" = "%@ ja veel 1's kogukonnas"; +"spaces_creation_in_spacename" = "%@ kogukonnas"; +"spaces_creation_post_process_inviting_users" = "Kutsun kasutajaid %@ kogukonda"; +"spaces_creation_post_process_adding_rooms" = "%@ jututubade lisamine"; +"spaces_creation_post_process_creating_room" = "%@ loomine"; +"spaces_creation_post_process_uploading_avatar" = "Profiilipildi ehk avatari üleslaadimine"; +"spaces_creation_post_process_creating_space_task" = "%@ loomine"; +"spaces_creation_post_process_creating_space" = "Loon kogukonda"; +"spaces_creation_invite_by_username_message" = "Sa saad neile ka hiljem kutse saata."; +"spaces_creation_invite_by_username_title" = "Kutsu oma kaasteelisi"; +"spaces_creation_invite_by_username" = "Kutsu kasutajanime alusel"; +"spaces_creation_add_rooms_message" = "Kuna see kogukond on vaid sinu jaoks, siis keegi teine ei saa sellest hetkel teada. Küll aga saad hiljem huvilisi lisada."; +"spaces_creation_add_rooms_title" = "Mida sa soovid lisada?"; +"spaces_creation_sharing_type_me_and_teammates_detail" = "Privaatne kogukond sinu ja sinu kaasteeliste jaoks"; +"spaces_creation_sharing_type_me_and_teammates_title" = "Mina ja minu kaasteelised"; +"spaces_creation_sharing_type_just_me_detail" = "Privaatne kogukond jututubade koondamiseks"; +"spaces_creation_sharing_type_just_me_title" = "Vaid mina"; +"spaces_creation_sharing_type_message" = "Kontrolli, et vajalikel inimestel oleks ligipääs %@ kogukonda Sa võid seda hiljem muuta."; +"spaces_creation_sharing_type_title" = "Kellega sa koos töötad?"; +"spaces_creation_email_invites_email_title" = "E-posti aadress"; +"spaces_creation_email_invites_message" = "Sa saad neile ka hiljem kutse saata."; +"spaces_creation_email_invites_title" = "Kutsu oma kaasteelisi"; +"spaces_creation_new_rooms_support" = "Kasutajatugi"; +"spaces_creation_new_rooms_random" = "Juhuslik"; +"spaces_creation_new_rooms_general" = "Üldist"; +"spaces_creation_new_rooms_room_name_title" = "Jututoa nimi"; +"spaces_creation_new_rooms_message" = "Loome siis igaühe jaoks oma jututoa."; +"spaces_creation_new_rooms_title" = "Mis teemal te kavatsete suhelda?"; +"spaces_creation_cancel_message" = "Sinu senitehtud muudatused jäävad salvestamata."; +"spaces_creation_cancel_title" = "Kas jätame kogukonna loomise pooleli?"; +"spaces_creation_private_space_title" = "Sinu privaatne kogukond"; +"spaces_creation_public_space_title" = "Sinu avalik kogukond"; +"spaces_creation_address_already_exists" = "%@\nselline nimi on juba olemas"; +"spaces_creation_address_invalid_characters" = "%@\nsiin leidub valesid tähemärke"; +"spaces_creation_address_default_message" = "Sinu kogukond on nähtav siin\n%@"; +"spaces_creation_empty_room_name_error" = "Nimi on nõutav"; +"spaces_creation_address" = "Aadress"; +"spaces_creation_settings_message" = "Eristumiseks palun lisa natuke teavet. Sa saad seda hiljem muuta ja täiendada."; +"spaces_creation_footer" = "Sa võid seda hiljem muuta"; +"spaces_creation_visibility_message" = "Olemasoleva kogukonnaga liitumiseks vajad sa kutset."; +"spaces_creation_visibility_title" = "Missugust kogukonda sooviksid sa luua?"; + +// Mark: - Space Creation + +"spaces_creation_hint" = "Kogukonnad on uus viis jututubade ja inimeste ühendamiseks."; +"space_settings_current_address_message" = "Sinu kogukond on nähtav siin\n%@"; +"space_settings_update_failed_message" = "Kogukonna seadistuste muutmine ei õnnestunud. Kas proovime uuesti?"; +"space_settings_access_section" = "Kes pääsevad ligi siia kogukonda?"; +"space_topic" = "Kirjeldus"; +"space_public_join_rule_detail" = "Avaliku ligipääsuga kogukond"; +"spaces_add_space" = "Lisa kogukond"; +"spaces_add_room" = "Lisa jututuba"; +"spaces_invite_people" = "Kutsu teisi kasutajaid"; +"space_private_join_rule_detail" = "Liitumine vaid kutse alusel, sobib sulle ja sinu lähematele kaaslastele"; +"spaces_explore_rooms_one_room" = "1 jututuba"; +"spaces_explore_rooms_room_number" = "%@ jututuba"; +"spaces_create_space_title" = "Loo kogukond"; +"spaces_add_space_title" = "Loo kogukond"; +"space_invite_not_enough_permission" = "Sul pole õigusi siia kogukonda osalejate kutsumiseks"; +"room_invite_not_enough_permission" = "Sul pole õigusi siia jututuppa osalejate kutsumiseks"; +"room_invite_to_room_option_detail" = "Nad ei saa osalema %@ jututoas."; +"room_invite_to_room_option_title" = "Vaid selle jututoaga"; +"room_invite_to_space_option_detail" = "Huvilised saavad uurida %@ kogukonda, kuid ei saa %@ kogukonnaga liituda."; +"share_invite_link_space_text" = "Hei, liitu selle kogukonnaga - %@"; +"share_invite_link_room_text" = "Hei, liitu selle jututoaga - %@"; + +// MARK: - Share invite link + +"share_invite_link_action" = "Jaga kutse linki"; +"create_room_processing" = "Loon jututuba"; +"create_room_suggest_room_footer" = "Soovitatavaid jututube tutvustatakse kogukonnas kui sellised, millega liitumine oleks hea mõte."; +"create_room_suggest_room" = "Soovita kogukonna liikmetele"; +"create_room_show_in_directory_footer" = "See aitab kasutajatel jututuba leida ja sellega liituda."; +"create_room_promotion_header" = "REKLAAM"; +"create_room_section_footer_type_restricted" = "Kõik kogukonna liikmed saavad leida ja liituda."; +"create_room_section_footer_type_private" = "Jututuba saab leida ja sellega liituda vaid kutsete alusel."; +"create_room_type_restricted" = "Kogukonna liikmed"; +"call_jitsi_unable_to_start" = "Rühmakõne alustamine ei õnnestunud"; +"room_suggestion_settings_screen_message" = "Soovitatavaid jututube tutvustatakse kogukonnas kui sellised, millega liitumine oleks hea mõte."; +"room_suggestion_settings_screen_title" = "Lisa jututuba kogukonnas soovitatavate hulka"; + +// Room suggestion Settings +"room_suggestion_settings_screen_nav_title" = "Soovita jututuba"; +"room_access_space_chooser_other_spaces_section_info" = "Ilmselt on tegemist millegagi, kus %@ haldajad veel osalevad."; +"room_access_space_chooser_other_spaces_section" = "Muud kogukonnad või jututoad"; +"room_access_space_chooser_known_spaces_section" = "Sulle teadaolevad kogukonnad, milles leidub %@"; +"room_access_settings_screen_setting_room_access" = "Seadistame ligipääsu jututuppa"; +"room_access_settings_screen_upgrade_alert_upgrading" = "Uuendan jututoa versiooni"; +"room_access_settings_screen_upgrade_alert_upgrade_button" = "Uuenda"; +"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "Kutsu huvilisi automaatselt uude jututuppa"; +"room_access_settings_screen_upgrade_alert_title" = "Uuendan jututoa versiooni"; +"room_access_settings_screen_public_message" = "Kõik saavad jututuba leida ja sellega liituda."; +"room_access_settings_screen_edit_spaces" = "Muuda kogukondi"; +"room_access_settings_screen_upgrade_required" = "Vajalik on uuendus"; +"room_access_settings_screen_restricted_message" = "Kõik kogukonna liikmed saavad leida ja liituda.\nJärgmisena pead valima lubatavad kogukonnad."; +"room_access_settings_screen_private_message" = "Leidmine ja liitumine toimub vaid kutse alusel."; +"room_access_settings_screen_message" = "Vali kes saavad %@ jututuba leida ja võivad temaga liituda."; +"room_access_settings_screen_title" = "Kes pääsevad ligi siia jututuppa?"; + +// Room Access Settings +"room_access_settings_screen_nav_title" = "Ligipääs jututuppa"; +"room_details_promote_room_suggest_title" = "Soovita kogukonna liikmetele"; +"room_details_promote_room_title" = "Reklaami jututuba"; +"threads_notice_information" = "Kõik testperioodil loodud jutulõngad kuvatakse tavaliste vastustena.

Kuna jutulõngad on nüüd osa Matrix'i spetsifikatsioonist, siis see on ühekordne muudatus."; +"room_details_access_row_title" = "Ligipääs"; +"settings_labs_enable_auto_report_decryption_errors" = "Automaatselt teata dekrüptimise vigadest"; +"room_preview_decline_invitation_options" = "Kas sa soovid keelduda kutsest või eirata kasutajat?"; +"threads_beta_cancel" = "Mitte praegu"; +"threads_beta_enable" = "Proovi nüüd"; +"threads_beta_information_link" = "Lisateave"; +"threads_beta_information" = "Halda vestlusi jutulõngadena.\n\nJutulõngad aitavad hoida vestlused teemakohastena ning mugavalt loetavatena. "; +"threads_beta_title" = "Jutulõngad"; +"threads_notice_done" = "Selge lugu"; +"threads_notice_title" = "Jutulõngad ei ole enam katsetusjärgus! 🎉"; +"room_participants_invite_prompt_to_msg" = "Kas sa oled kindel, et soovid kutsuda %@ %@ jututuppa?"; +"onboarding_celebration_button" = "Alustame nüüd"; +"onboarding_celebration_message" = "Sinu eelistused on salvestatud."; +"onboarding_celebration_title" = "Kõik on valmis!"; +"onboarding_avatar_accessibility_label" = "Profiilipilt"; +"onboarding_avatar_message" = "Sa võid seda hiljem alati muuta."; +"onboarding_avatar_title" = "Lisa profiilipilt"; +"onboarding_display_name_max_length" = "Sinu kuvatav nimi peab olema lühem, kui 256 tähemärki"; +"onboarding_display_name_hint" = "Sa võid seda hiljem muuta"; +"onboarding_display_name_placeholder" = "Kuvatav nimi"; +"onboarding_display_name_message" = "Seda näidatakse sõnumite saatmisel."; +"onboarding_display_name_title" = "Vali kuvatav nimi"; +"onboarding_personalization_skip" = "Jäta see samm vahele"; +"onboarding_personalization_save" = "Salvesta ja jätka"; +"onboarding_congratulations_home_button" = "Mine avalehele"; +"onboarding_congratulations_personalize_button" = "Isikupärasta oma profiili"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "Sinu kasutajakonto %@ on nüüd olemas."; +"onboarding_congratulations_title" = "Õnnitlused!"; +"saving" = "Salvestame"; + +// Activities +"loading" = "Laadime"; +"edit" = "Muuda"; +"suggest" = "Soovita"; +"add" = "Lisa"; +"existing" = "Olemasolev"; +"new_word" = "Uus"; +"stop" = "Peata"; +"joining" = "Liitun"; + +// Mark: - Room invite + +"room_invite_to_space_option_title" = "Kasutajale %@"; +"location_sharing_live_list_item_last_update" = "Uuendamise välp: %@"; +"location_sharing_live_list_item_stop_sharing_action" = "Lõpeta asukoha jagamine"; +"location_sharing_live_list_item_current_user_display_name" = "Sina"; +"location_sharing_live_list_item_last_update_invalid" = "Viimase uuendamise aeg pole teada"; +"location_sharing_live_list_item_sharing_expired" = "Asukoha jagamine aegus"; +"location_sharing_live_list_item_time_left" = "kuvamisaega jäänud %@s"; +"location_sharing_live_viewer_title" = "Asukoht"; +"location_sharing_live_map_callout_title" = "Jaga asukohta"; +"create_room_section_footer_type_public" = "Jututuba saavad leida ja sellega liituda vaid need, kellel on kutse. See puudutab ka neid, kes pole kogukonna liikmed."; +"version_check_banner_subtitle_deprecated" = "Me oleme lõpetanud selle rakenduse (%@) toe IOS'i versioonis %@. Kui soovid kasutada%@'i kõiki võimalusi, siis palun uuenda oma iOS'i versiooni."; +"version_check_banner_subtitle_supported" = "Me üsna varsti lõpetame selle rakenduse (%@) toe IOS'i versioonis %@. Kui soovid kasutada %@'i kõiki võimalusi, siis palun uuenda oma iOS'i versiooni."; +"space_beta_announce_information" = "Kogukonnad on uus viis inimeste ja jututubade sidumiseks. Neid veel ei saa iOS'is kasutada, kuid nad on olemas Element'i veebirakenduses ja töölauarakenduses."; +"space_feature_unavailable_subtitle" = "Kogukondi ei saa iOS'is kasutada, kuid nad on olemas Element'i veebirakenduses ja töölauarakenduses"; +"room_access_settings_screen_upgrade_alert_note" = "Palun arvesta, et uuendusega tehakse jututoast uus variant. Kõik senised sõnumid jäävad sellesse jututuppa arhiveeritud olekus."; +"room_access_settings_screen_upgrade_alert_message_no_param" = "Kõik hõlmava kogukonna liikmed saavad antud jututuba leida ja sellega liituda - sa ei pea kedagi ükshaaval kutsuma. Neid jututoa seadistusi saad igal hetkel muuta."; +"room_access_settings_screen_upgrade_alert_message" = "Kõik %@ jututoa liikmed saavad antud jututuba leida ja sellega liituda - sa ei pea kedagi ükshaaval kutsuma. Neid jututoa seadistusi saad igal hetkel muuta."; +"settings_presence_offline_mode_description" = "Kui see valik on kasutusel, siis sa alati oled teiste jaoks võrgust väljas. Seda ka siis, kui kasutad rakendust."; +"settings_presence_offline_mode" = "Ei ole võrgus"; +"settings_presence" = "Olek võrgus"; +"threads_discourage_information_2" = "\n\nKas sa ikkagi soovid jutulõngad kasutusele võtta?"; +"threads_discourage_information_1" = "Sinu koduserver hetkel ei toeta jutulõngasid ning seega antud funktsionaalsus ei pruugi toimida korralikult. Kõik sõnumid jutulõngas ilmselt ei ole loetavad. "; diff --git a/Riot/Assets/eu.lproj/Localizable.strings b/Riot/Assets/eu.lproj/Localizable.strings index b39ed2681..8c35c6a26 100644 --- a/Riot/Assets/eu.lproj/Localizable.strings +++ b/Riot/Assets/eu.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ erabiltzaileak irudi bat bidali du %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ erabiltzaileak irudi bat %@ bidali du %@ gelara"; /* Multiple unread messages in a room */ diff --git a/Riot/Assets/eu.lproj/Vector.strings b/Riot/Assets/eu.lproj/Vector.strings index 285b965a2..2bb8082fc 100644 --- a/Riot/Assets/eu.lproj/Vector.strings +++ b/Riot/Assets/eu.lproj/Vector.strings @@ -280,7 +280,6 @@ // Chat "room_jump_to_first_unread" = "Jauzi irakurri gabeko lehen mezura"; "room_new_message_notification" = "mezu berri %d"; -"room_new_messages_notification" = "$d mezu berri"; "room_one_user_is_typing" = "%@ idazten ari da…"; "room_two_users_are_typing" = "%@ eta %@ idazten ari dira…"; "room_many_users_are_typing" = "%@, %@, eta beste batzuk idazten ari dira…"; @@ -404,7 +403,6 @@ "no_voip_title" = "Deia jasotzen"; "no_voip" = "%@ zu deitzen ari da baina %@(e)k ez ditu deiak onartzen oraindik.\nJakinarazpen hau ezikusi dezakezu eta deia beste gailu batetik hartu, edo deia baztertu."; // Crash report -"google_analytics_use_prompt" = "%@ hobetzen lagundu nahi duzu kraskatze-txosten era erabilera datu anonimoak automatikoki bidaliz?"; "e2e_need_log_in_again" = "Berriro hasi behar duzu saioa muturretik muturrerako zifratzerako saio honek gakoak sortzeko eta gako publikoa zure hasiera zerbitzarira bidali behar duzu.\nHau behin bakarrik egin behar duzu, barkatu eragozpenak."; "bug_crash_report_title" = "Kraskatze-txostena"; "bug_crash_report_description" = "Azaldu zer zeunden egiten programa kraskatu aurretik:"; @@ -660,10 +658,6 @@ "room_message_unable_open_link_error_message" = "Ezin izan da esteka ireki."; "room_event_action_reply" = "Erantzun"; "room_event_action_edit" = "Editatu"; -"room_event_action_reaction_agree" = "Ados %@"; -"room_event_action_reaction_disagree" = "Ez ados %@"; -"room_event_action_reaction_like" = "Gogokoa %@"; -"room_event_action_reaction_dislike" = "Ez gogokoa %@"; "room_action_reply" = "Erantzun"; "settings_key_backup_button_connect" = "Konektatu saio hau gakoen babes-kopiara"; "key_backup_setup_intro_setup_connect_action_with_existing_backup" = "Konektatu gailu hau gakoen babes-kopiara"; @@ -807,7 +801,6 @@ "photo_library_access_not_granted" = "%@(e)k ez du argazki liburutegia erabiltzeko baimenik, aldatu pribatutasun ezarpenak"; // Service terms "service_terms_modal_title" = "Erabilera baldintzak"; -"service_terms_modal_message" = "Jarraitzeko, zerbitzu honen erabilera baldintzak onartu behar dituzu (%@)."; "service_terms_modal_accept_button" = "Onartu"; "service_terms_modal_description_for_identity_server" = "Izan besteentzat aurkigarria"; "service_terms_modal_description_for_integration_manager" = "Erabili botak, zubiak, trepetak eta eranskailu multzoak"; @@ -891,7 +884,6 @@ "service_terms_modal_description_for_identity_server_2" = "Izan telefonoa edo e-maila erabiliz aurkigarria"; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "Kontaktuak aurkitzea"; -"service_terms_modal_message_identity_server" = "Onartu %@ identitate-zerbitzariaren baldintzak kontaktuak aurkitzeko."; // Generic errors "error_invite_3pid_with_no_identity_server" = "Gehitu identitate-zerbitzari bat zure ezarpenetan e-maila erabiliz gonbidatzeko."; "settings_add_3pid_password_title_email" = "Gehitu e-mail helbidea"; @@ -901,7 +893,6 @@ "error_not_supported_on_mobile" = "Ezin duzu hau %@ mugikorretik egin."; "settings_integrations" = "INTEGRAZIOAK"; "settings_integrations_allow_button" = "Kudeatu integrazioak"; -"settings_integrations_allow_description" = "Erabili integrazio kudeatzaileren bat botak, zubiak, trepetak eta eranskailu multzoak kudeatzeko.\n\nIntegrazio kudeatzaileek konfigurazio datuak jasotzen dituzte, eta trepetak aldatu ditzakete, gelarako gonbidapenak bidali, eta botere mailak zure izenean ezarri."; "widget_menu_refresh" = "Freskatu"; "widget_menu_open_outside" = "Ireki nabigatzailean"; "widget_menu_revoke_permission" = "Indargabetu sarbidea niretzat"; @@ -1033,7 +1024,6 @@ "security_settings_complete_security_alert_message" = "Aurretik segurtasuna osatu beharko zenuke oraingo saioan."; "security_settings_coming_soon" = "Sentitzen dugu. Ekintza hau ez dago iOS plataformarako %@ bezeroan eskuragarri oraindik. Erabili beste Matrix bezero bat ezartzeko. %@ iOS-ek erabili egingo du."; "device_verification_self_verify_wait_new_sign_in_title" = "Egiaztatu saio hau"; -"device_verification_self_verify_wait_additional_information" = "edo zeharkako sinadurarako gai den beste Matrix bezero bat"; // Scanning "key_verification_scan_confirmation_scanning_title" = "Ia bukatu duzu! Baieztapenaren zain…"; "key_verification_scan_confirmation_scanning_user_waiting_other" = "%@ itxaroten…"; @@ -1114,7 +1104,6 @@ // room display name "room_displayname_empty_room" = "Gela hutsa"; "room_displayname_two_members" = "%@ eta %@"; -"room_displayname_more_than_two_members" = "%@ eta beste %u"; // Settings "settings" = "Ezarpenak"; // button names @@ -1253,7 +1242,6 @@ "unignore" = "Berriro aintzat hartu"; "notice_room_name_removed" = "%@ erabiltzaileak gelaren izena kendu du"; "notice_room_topic_removed" = "%@ erabiltzaileak gelaren mintzagaia kendu du"; -"notice_event_redacted" = ""; "notice_event_redacted_by" = " nork: %@"; "notice_event_redacted_reason" = " [arrazoia: %@]"; "notice_room_created" = "%@ erabiltzaileak gela sortu du"; @@ -1322,7 +1310,6 @@ "notification_settings_people_join_leave_rooms" = "Jakinarazi niri jendea gelera elkartu edo gelatik ateratzean"; "notice_room_power_level_event_requirement" = "Gertaerekin lotutako gutxieneko botere maila:"; "notice_room_aliases" = "Gelaren ezizenak: %@"; -"notice_encryption_enabled" = "%@ erabiltzaileak muturretik muturrera zifratzea gaitu du (%@ algoritmoa)"; "notice_redaction" = "%@ erabiltzaileak gertaera bat kendu du (id: %@)"; "notice_error_unsupported_event" = "Onartu gabeko gertaera"; "notice_error_unexpected_event" = "Ustekabeko gertaera"; diff --git a/Riot/Assets/fa.lproj/Vector.strings b/Riot/Assets/fa.lproj/Vector.strings index 0a8ab450c..8ae59ee6b 100644 --- a/Riot/Assets/fa.lproj/Vector.strings +++ b/Riot/Assets/fa.lproj/Vector.strings @@ -217,7 +217,6 @@ // Room Preview "room_preview_invitation_format" = "شما توسط %@ برای پیوستن به این اتاق دعوت شده اید"; "room_title_one_member" = "۱ عضو"; -"room_title_members" = "٪@ اعضا"; "room_title_invite_members" = "دعوت از اعضا"; "room_title_one_active_member" = "%@/%@ عضو فعال"; "room_title_multiple_active_members" = "%@/%@ اعضای فعال"; @@ -228,7 +227,6 @@ "unknown_devices_answer_anyway" = "به هر حال پاسخ دهید"; "unknown_devices_call_anyway" = "به هر حال تماس بگیرید"; "unknown_devices_send_anyway" = "به هر حال ارسال کنید"; -"room_multiple_typing_notification" = "٪@ و دیگران"; "external_link_confirmation_message" = "پیوند %@ شما را به سایت دیگری انتقال می دهد: %@\n\nآیا مطمئن هستید که میخواهید ادامه دهید؟"; "external_link_confirmation_title" = "این لینک را دوبار بررسی کنید"; "media_type_accessibility_sticker" = "استیکر"; @@ -393,7 +391,6 @@ // Call "call_incoming_voice_prompt" = "تماس صوتی ورودی از %@"; -"room_does_not_exist" = "٪@ وجود ندارد"; "photo_library_access_not_granted" = "%@ اجازه دسترسی به عکس های دستگاه شما را ندارد، لطفاً تنظیمات حریم خصوصی را تغییر دهید"; "camera_unavailable" = "دوربین در دستگاه شما قابل دسترسی نیست"; "camera_access_not_granted" = "%@ اجازه استفاده از دوربین را ندارد، لطفاً تنظیمات حریم خصوصی را تغییر دهید"; @@ -410,11 +407,8 @@ "or" = "یا"; "event_formatter_jitsi_widget_removed_by_you" = "کنفرانس VoIP را حذف کردید"; "event_formatter_jitsi_widget_added_by_you" = "شما کنفرانس VoIP را اضافه کردید"; -"event_formatter_widget_removed_by_you" = "ویجت را حذف کردید:@%"; // Events formatter with you -"event_formatter_widget_added_by_you" = "ویجت را اضافه کردید:@ %"; -"event_formatter_group_call_incoming" = "@٪ در@ ٪"; "event_formatter_group_call_leave" = "ترک"; "event_formatter_group_call_join" = "پیوستن"; "event_formatter_group_call" = "تماس گروهی"; diff --git a/Riot/Assets/fi.lproj/Vector.strings b/Riot/Assets/fi.lproj/Vector.strings index 069d73fc2..05023828f 100644 --- a/Riot/Assets/fi.lproj/Vector.strings +++ b/Riot/Assets/fi.lproj/Vector.strings @@ -46,7 +46,6 @@ // Directory "directory_title" = "Luettelo"; "directory_server_picker_title" = "Valitse luettelo"; -"major_update_title" = "Riot on nyt Element"; "major_update_learn_more_action" = "Lue lisää"; "major_update_done_action" = "Selvä"; "room_member_power_level_short_moderator" = "Valvoja"; diff --git a/Riot/Assets/fr.lproj/Localizable.strings b/Riot/Assets/fr.lproj/Localizable.strings index 463cf9e1b..64f1ed513 100644 --- a/Riot/Assets/fr.lproj/Localizable.strings +++ b/Riot/Assets/fr.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@ : * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ a envoyé une image %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ a posté une image %@ dans %@"; /* A single unread message in a room */ diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index 27540c367..743ea3de4 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -420,7 +420,6 @@ "no_voip_title" = "Appel entrant"; "no_voip" = "%@ vous appelle mais %@ ne prend pas encore en charge les appels.\nVous pouvez ignorer cette notification et répondre à l’appel depuis un autre appareil, ou bien le rejeter."; // Crash report -"google_analytics_use_prompt" = "Souhaitez-vous aider à améliorer %@ en envoyant automatiquement des rapports d’anomalie et des statistiques d’utilisation ?"; // Crypto "e2e_enabling_on_app_update" = "%@ prend désormais en charge le chiffrement de bout en bout, mais vous devez vous reconnecter pour l’activer.\n\nVous pouvez le faire maintenant ou plus tard à partir des paramètres de l’application."; "e2e_need_log_in_again" = "Vous devez vous reconnecter pour générer les clés de chiffrement de bout en bout pour cette session et envoyer la clé publique vers votre serveur d’accueil.\nCeci ne se produira qu’une fois. Veuillez nous excuser pour ce désagrément."; @@ -561,7 +560,6 @@ "settings_key_backup_info_not_valid" = "Cette session ne sauvegarde pas vos clés, mais vous avez une sauvegarde existante que vous pouvez restaurer et joindre."; "settings_key_backup_info_progress" = "Sauvegarde de %@ clés…"; "settings_key_backup_info_progress_done" = "Toutes les clés ont été sauvegardées"; -"settings_key_backup_info_not_trusted_from_verifiable_device_fix_action" = "Pour utiliser la récupération de messages sécurisée sur cet appareil, vérifiez %@ maintenant."; "settings_key_backup_info_not_trusted_fix_action" = "Pour utiliser la récupération de messages sécurisée sur cet appareil, fournissez votre phrase de passe ou votre clé de récupération maintenant."; "settings_key_backup_info_trust_signature_unknown" = "La sauvegarde a une signature de la session ayant pour identifiant : %@"; "settings_key_backup_info_trust_signature_valid" = "La sauvegarde a une signature valide depuis cette session"; @@ -672,10 +670,6 @@ "auth_autodiscover_invalid_response" = "Réponse de découverte du serveur d’accueil non valide"; "room_event_action_reply" = "Répondre"; "room_event_action_edit" = "Modifier"; -"room_event_action_reaction_agree" = "D’accord %@"; -"room_event_action_reaction_disagree" = "Pas d’accord %@"; -"room_event_action_reaction_like" = "J’aime %@"; -"room_event_action_reaction_dislike" = "J’aime pas %@"; "room_action_reply" = "Répondre"; "settings_labs_message_reaction" = "Réagir aux messages avec des émojis"; "settings_key_backup_button_connect" = "Connecter cette session à la sauvegarde de clés"; @@ -821,7 +815,6 @@ "room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Aucun serveur d’identité n’est configuré donc vous ne pouvez pas commencer de discussion avec un contact en utilisant un e-mail."; // Service terms "service_terms_modal_title" = "Conditions de service"; -"service_terms_modal_message" = "Pour continuer vous devez accepter les conditions de ce service (%@)."; "service_terms_modal_accept_button" = "Accepter"; "service_terms_modal_description_for_identity_server" = "Se rendre découvrable pour les autres"; "service_terms_modal_description_for_integration_manager" = "Utiliser des robots, des passerelles, des widgets et des jeux d’autocollants"; @@ -905,7 +898,6 @@ "service_terms_modal_description_for_identity_server_2" = "Être trouvé par téléphone ou par e-mail"; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "Découverte des contacts"; -"service_terms_modal_message_identity_server" = "Acceptez les conditions du serveur d’identité (%@) pour découvrir des contacts."; "settings_add_3pid_password_title_email" = "Ajouter une adresse e-mail"; "settings_add_3pid_password_title_msidsn" = "Ajouter un numéro de téléphone"; "settings_add_3pid_password_message" = "Pour continuer, saisissez le mot de passe de votre compte Matrix"; @@ -921,7 +913,6 @@ "widget_integration_manager_disabled" = "Vous devez activer le gestionnaire d’intégrations dans les paramètres"; "widget_room_permission_title" = "Charger le widget"; "widget_room_permission_creator_info_title" = "Ce widget a été ajouté par :"; -"widget_room_permission_information" = "Son utilisation peut utiliser des cookies et partager des données avec %@ :\n\n• Votre nom affiché\n• L’URL de votre avatar\n• Votre identifiant d’utilisateur\n• Votre thème\n• L’identifiant du salon\n• L’identifiant du widget"; // Room widget permissions "room_widget_permission_title" = "Charger un widget"; "room_widget_permission_creator_info_title" = "Ce widget a été ajouté par :"; @@ -1640,7 +1631,6 @@ "poll_edit_form_update_failure_subtitle" = "Veuillez réessayer"; "poll_edit_form_update_failure_title" = "Échec lors de la mise à jour du sondage"; "poll_edit_form_poll_type" = "Type de sondage"; -"location_sharing_post_failure_subtitle" = "%# n’a pas pu envoyer votre localisation. Merci de réessayer plus tard."; "location_sharing_post_failure_title" = "Nous n’avons pas pu envoyer votre localisation"; "home_context_menu_leave" = "Partir"; "home_context_menu_unfavourite" = "Retirer des favoris"; @@ -1768,7 +1758,6 @@ "notice_room_aliases" = "Les alias du salon sont : %@"; "notice_room_related_groups" = "Les groupes associés à ce salon sont : %@"; "notice_encrypted_message" = "Message chiffré"; -"notice_encryption_enabled" = "%@ a activé le chiffrement de bout en bout (algorithme %@)"; "notice_image_attachment" = "image en pièce-jointe"; "notice_audio_attachment" = "audio en pièce-jointe"; "notice_video_attachment" = "vidéo en pièce-jointe"; @@ -1789,7 +1778,6 @@ // room display name "room_displayname_empty_room" = "Salon vide"; "room_displayname_two_members" = "%@ et %@"; -"room_displayname_more_than_two_members" = "%@ et %u autres"; // Settings "settings" = "Paramètres"; "settings_enable_inapp_notifications" = "Activer les notifications dans l’application"; diff --git a/Riot/Assets/he.lproj/Vector.strings b/Riot/Assets/he.lproj/Vector.strings index 9295a642c..ccace0635 100644 --- a/Riot/Assets/he.lproj/Vector.strings +++ b/Riot/Assets/he.lproj/Vector.strings @@ -307,7 +307,6 @@ "event_formatter_widget_removed" = "יישומון %@ הוסר ע\"י %@"; // Events formatter -"event_formatter_member_updates" = "% שינויי חברות"; "directory_server_placeholder" = "matrix.org"; "directory_server_type_homeserver" = "הקלד שרת בית על מנת להציג רשימת חדרים ממנו"; "directory_server_all_native_rooms" = "כל חדרי מטריקס המקוריים"; @@ -347,9 +346,7 @@ // Group participants "group_participants_add_participant" = "הוסף משתתף"; "group_invitation_format" = "%@ הזמין אותך להצטרף לקהילה זו"; -"group_home_multi_rooms_format" = "% חדרים"; "group_home_one_room_format" = "חדר 1"; -"group_home_multi_members_format" = "% חברים"; // Group Home "group_home_one_member_format" = "חבר 1"; @@ -778,8 +775,6 @@ "room_many_users_are_typing" = "%@, %@ וגם אחרים מקלידים…"; "room_two_users_are_typing" = "%@ & %@ מקלידים…"; "room_one_user_is_typing" = "%@ מקליד…"; -"room_new_messages_notification" = "%@ הודעות חדשות"; -"room_new_message_notification" = "%@ הודעה חדשה"; "room_accessiblity_scroll_to_bottom" = "גלול לתחתית"; "room_jump_to_first_unread" = "עבור ללא נקרא"; @@ -845,7 +840,6 @@ "room_participants_leave_prompt_msg" = "האם אתה בטוח שברצונך לעזוב את החדר?"; "room_participants_leave_prompt_title_for_dm" = "עזוב"; "room_participants_leave_prompt_title" = "עזוב חדר"; -"room_participants_multi_participants" = "%@ משתתפים"; "room_participants_one_participant" = "משתתף 1"; "room_participants_add_participant" = "הוסף משתתף"; @@ -869,10 +863,7 @@ // Contacts "contacts_address_book_section" = "אנשי קשר מקומיים"; "directory_searching_title" = "מחפש ספריות…"; -"directory_search_results_more_than" = ">% תוצאות נמצאו עבור %@"; -"directory_search_results" = "% תוצאות נמצאו עבור %@"; "directory_search_results_title" = "דפדף תוצאות ספרייה"; -"directory_cell_description" = "% חדרים"; // Directory "directory_cell_title" = "דפדף תיקייה"; @@ -927,7 +918,6 @@ "auth_softlogout_clear_data_message_2" = "מחק אותו שם סיימת להשתמש במכשיר זה, או אם אתה מעוניין להתחבר לחשבון אחר."; "auth_softlogout_clear_data_message_1" = "זהירות:המידע האישי שלך (כולל מפתחות הצפנה) עדיין שמור במכשיר זה."; "auth_softlogout_recover_encryption_keys" = "התחבר על מנת לשחזר את מפתחות ההצפנה השמור בצורה בלעדית במכשיר זה. תצטרך אותם על מנת לקרא את כל ההודעות המאובטחות שלך בכל מכשיר."; -"auth_softlogout_reason" = "מנהל שרת הבית שלך (% 1 $@) הוציא אותך החוצה מהחשבון שלך % 2 $@ (% 3 $@)."; "auth_autodiscover_invalid_response" = "תגובת חשיפה לא חוקית של שרת הבית"; "auth_accept_policies" = "אנא עבור ואשר את המדיניות בשרת הבית הזה:"; "auth_add_email_and_phone_warning" = "רישום באמצעות מייל ומספר טלפון יחד לא נתמך עדיין עד שה API קיים. רק מספר הטלפון יילקח בחשבון. תוכל להוסיף את כתובת המייל שלך בהגדרות הפרופיל שלך."; diff --git a/Riot/Assets/hu.lproj/Localizable.strings b/Riot/Assets/hu.lproj/Localizable.strings index 2285c32b4..82161237e 100644 --- a/Riot/Assets/hu.lproj/Localizable.strings +++ b/Riot/Assets/hu.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ képet küldött neked %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ képet küldött %@ ide: %@"; /* A single unread message in a room */ @@ -101,7 +100,6 @@ "AUDIO_FROM_USER" = "%@ hang fájlt küldött: %@"; /* New video message from a specific person, not referencing a room. */ -"VIDEO_FROM_USER" = "% videót küldött"; /** Media Messages **/ @@ -119,3 +117,6 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ megosztotta a földrajzi helyzetét"; + +/* New video message from a specific person, not referencing a room. */ +"VIDEO_FROM_USER" = "%@ videót küldött"; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 757189a94..da1687bd3 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -472,10 +472,8 @@ "camera_access_not_granted" = "%@ -nak/nek nincs joga a kamerát használni, kérlek változtasd meg az adatvédelmi beállításokat"; "large_badge_value_k_format" = "%.1fK"; // room display name -"room_displayname_invite_from" = "Meghívó innen: %@"; "room_displayname_room_invite" = "Szoba meghívó"; "room_displayname_two_members" = "%@ és %@"; -"room_displayname_more_than_two_members" = "%@ és %u mások"; "room_displayname_no_title" = "Üres szoba"; // Call "call_incoming_voice_prompt" = "Bejövő hanghívás innen: %@"; @@ -488,7 +486,6 @@ "no_voip_title" = "Bejövő hívás"; "no_voip" = "%@ hív téged de %@ egyenlőre nem támogatja a hívásokat.\nFigyelmen kívül hagyhatod ezt az értesítést és másik eszközről válaszolhatsz a hívásra vagy elutasíthatod azt."; // Crash report -"google_analytics_use_prompt" = "Szeretnél segíteni személytelen összeomlás és felhasználási adatok küldésével a(z) %@ fejlesztésében?"; // Crypto "e2e_enabling_on_app_update" = "%@ most már támogatja a végponttól végpontig titkosítást de újra be kell jelentkezned.\n\nMegteheted most vagy később az alkalmazás beállításainál."; "e2e_need_log_in_again" = "Vissza kell jelentkezned, hogy a munkamenetedhez a végponttól végpontig titkosítási kulcsokat létrehozzuk és a nyilvános kulcsokat elküldjük a Matrix szerverednek.\nEz egy egyszeri alkalom; elnézést a kellemetlenségért."; @@ -566,7 +563,6 @@ "settings_key_backup_info_not_valid" = "Ez a munkamenet nem menti el a kulcsaidat, de van létező mentésed ahonnan vissza tudsz állni és továbbléphetsz."; "settings_key_backup_info_progress" = "%@ kulcsok mentése…"; "settings_key_backup_info_progress_done" = "Minden kulcs elmentve"; -"settings_key_backup_info_not_trusted_from_verifiable_device_fix_action" = "A Biztonságos Üzenet Visszaállítás használatához ellenőrizd ezt: %@."; "settings_key_backup_info_not_trusted_fix_action" = "Ha a Biztonságos Üzenete Visszaállítást ezen az eszközön használni szeretnéd, akkor most add meg a jelmondatodat vagy a visszaállítási kulcsot."; "settings_key_backup_info_trust_signature_unknown" = "A mentés aláírással rendelkezik az alábbi munkamenet azonosítóval: %@"; "settings_key_backup_info_trust_signature_valid" = "A mentés érvényes aláírással rendelkezik ettől a munkamenetről"; @@ -677,10 +673,6 @@ "auth_autodiscover_invalid_response" = "Matrix szerver felderítésénél érvénytelen válasz érkezett"; "room_event_action_reply" = "Válasz"; "room_event_action_edit" = "Szerkeszt"; -"room_event_action_reaction_agree" = "Egyetért %@"; -"room_event_action_reaction_disagree" = "Ellentmond %@"; -"room_event_action_reaction_like" = "Kedveli %@"; -"room_event_action_reaction_dislike" = "Nem kedveli %@"; "room_action_reply" = "Válasz"; "settings_labs_message_reaction" = "Emoji reakció az üzenetre"; "settings_key_backup_button_connect" = "Munkamenet csatlakoztatása a Kulcs Mentéshez"; @@ -826,7 +818,6 @@ "photo_library_access_not_granted" = "A fénykép könyvár eléréséhez %@ nem rendelkezik engedéllyel, kérlek változtasd meg az adatvédelmi beállításokat"; // Service terms "service_terms_modal_title" = "Felhasználási feltételek"; -"service_terms_modal_message" = "A folytatáshoz el kell fogadnod a Felhasználási feltételeket (%@)."; "service_terms_modal_accept_button" = "Elfogad"; "service_terms_modal_description_for_identity_server" = "Látható mások számára"; "service_terms_modal_description_for_integration_manager" = "Használjon botokat, hidakat, kisalkalmazásokat és matrica csomagokat"; @@ -910,7 +901,6 @@ "service_terms_modal_description_for_identity_server_2" = "Legyél megtalálható telefonszámmal vagy e-mail címmel"; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "Ismerősök keresése"; -"service_terms_modal_message_identity_server" = "Az azonosítási szerver (%@) felhasználási feltételeit el kell fogadnod, hogy ismerősöket kereshess."; "settings_add_3pid_password_title_email" = "E-mail cím hozzáadása"; "settings_add_3pid_password_title_msidsn" = "Telefonszám hozzáadása"; "settings_add_3pid_password_message" = "A folytatáshoz add meg a Matrix fiók jelszavadat"; @@ -1226,19 +1216,19 @@ "searchable_directory_x_network" = "%@ hálózat"; "searchable_directory_search_placeholder" = "Név vagy azon."; "create_room_title" = "Új szoba"; -"create_room_section_header_name" = "Szoba neve"; +"create_room_section_header_name" = "NÉV"; "create_room_placeholder_name" = "Név"; -"create_room_section_header_topic" = "Szoba témája (nem kötelező)"; -"create_room_placeholder_topic" = "Téma"; -"create_room_section_header_encryption" = "Szoba titkosítása"; +"create_room_section_header_topic" = "TÉMA (NEM KÖTELEZŐ)"; +"create_room_placeholder_topic" = "Miről szól ez a szoba?"; +"create_room_section_header_encryption" = "TITKOSÍTÁS"; "create_room_enable_encryption" = "Titkosítás engedélyezése"; "create_room_section_footer_encryption" = "A titkosítást ezután nem lehet kikapcsolni."; -"create_room_section_header_type" = "Szoba típusa"; -"create_room_type_private" = "Privát szoba"; -"create_room_type_public" = "Nyilvános szoba"; +"create_room_section_header_type" = "KI ÉRHETI EL"; +"create_room_type_private" = "Privát szoba (csak meghívóval)"; +"create_room_type_public" = "Nyilvános szoba (bárki)"; "create_room_section_footer_type" = "Emberek csak meghívóval csatlakozhatnak a privát szobához."; -"create_room_show_in_directory" = "A szoba megjelenítése a listában"; -"create_room_section_header_address" = "Szoba címe"; +"create_room_show_in_directory" = "Megjelenítés a szoba listába"; +"create_room_section_header_address" = "CÍM"; "create_room_placeholder_address" = "#testroom:matrix.org"; "room_info_list_one_member" = "1 tag"; "room_info_list_several_members" = "%@ tag"; @@ -1755,7 +1745,6 @@ "notice_room_aliases" = "A szoba becenevei: %@"; "notice_room_related_groups" = "A szobához kapcsolódó csoportok: %@"; "notice_encrypted_message" = "Titkosított üzenet"; -"notice_encryption_enabled" = "%@ bekapcsolta a végponttól végpontig titkosítást (algoritmus: %@)"; "notice_image_attachment" = "képmelléklet"; "notice_audio_attachment" = "hangmelléklet"; "notice_video_attachment" = "videómelléklet"; @@ -2128,7 +2117,6 @@ "attachment_small_with_resolution" = "Kicsi %@ (~%@)"; "attachment_size_prompt_message" = "Ezt a beállításokban kikapcsolhatod."; "attachment_size_prompt_title" = "Méret megerősítése küldéshez"; -"room_displayname_all_other_participants_left" = "%@ (Bal)"; "room_displayname_all_other_members_left" = "%@ (Bal)"; "attachment_unsupported_preview_message" = "Ez a fájl típus nem támogatott."; "attachment_unsupported_preview_title" = "Az előnézetet nem lehet megjeleníteni"; @@ -2138,3 +2126,183 @@ "room_participants_leave_processing" = "Távozás"; "notice_error_unformattable_event" = "** Az üzenetet nem lehet megjeleníteni. Kérlek jelezd ezt a hibát"; "settings_labs_use_only_latest_user_avatar_and_name" = "A felhasználó jelenlegi profilképének és nevének megjelenítése a régi üzeneteknél is"; +"create_room_suggest_room" = "Javaslat a tér tagság számára"; +"create_room_show_in_directory_footer" = "Ez segít az embereknek megtalálni és csatlakozni."; +"create_room_promotion_header" = "JAVASOLT"; +"create_room_section_footer_type_public" = "Csak meghívottak találhatják meg léphetnek be és nem csak tér tagság."; +"create_room_section_footer_type_restricted" = "Bárki a téren megtalálhatja és beléphet."; +"create_room_section_footer_type_private" = "Csak a meghívott személyek találják meg és tudnak belépni."; +"create_room_type_restricted" = "Tér tagság"; +"call_jitsi_unable_to_start" = "Konferencia hívás nem indítható"; +"room_suggestion_settings_screen_message" = "A javasolt szobák a tér tagság számára csatlakozásra érdemesként lesznek feltüntetve."; +"room_suggestion_settings_screen_title" = "Szoba javasoltnak állítása a téren belül"; + +// Room suggestion Settings +"room_suggestion_settings_screen_nav_title" = "Szoba javaslat"; +"room_access_space_chooser_other_spaces_section" = "További terek és szobák"; +"room_access_space_chooser_known_spaces_section" = "Ismert terek amik tartalmazzák: %@"; +"room_access_settings_screen_setting_room_access" = "Szoba hozzáférés beállítások"; +"room_access_settings_screen_upgrade_alert_upgrading" = "Szoba fejlesztése"; +"room_access_settings_screen_upgrade_alert_upgrade_button" = "Fejlesztés"; +"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "Tagok automatikus meghívása az új szobába"; +"room_access_settings_screen_upgrade_alert_title" = "Szoba fejlesztése"; +"room_access_settings_screen_public_message" = "Bárki megtalálhatja és beléphet."; +"room_access_settings_screen_edit_spaces" = "Terek szerkesztése"; +"room_access_settings_screen_upgrade_required" = "Fejlesztés szükséges"; +"room_access_settings_screen_restricted_message" = "Bárki a téren megtalálhatja és beléphet.\nMeg kell adni mely terekre legyen igaz."; +"room_access_settings_screen_private_message" = "Csak meghívott emberek láthatják és léphetnek be."; +"room_access_settings_screen_message" = "Döntsd el ki találhatja meg és léphet be ide: %@."; +"room_access_settings_screen_title" = "Ki tud hozzáférni a szobához?"; + +// Room Access Settings +"room_access_settings_screen_nav_title" = "Szoba hozzáfárés"; +"room_details_promote_room_suggest_title" = "Javaslat a tér tagság számára"; +"room_details_promote_room_title" = "Szoba ajánlása"; +"room_details_access_row_title" = "Hozzáférés"; +"settings_labs_enable_auto_report_decryption_errors" = "Titkosítás visszafejtési hibák automatikus jelentése"; +"room_preview_decline_invitation_options" = "Elutasítod a meghívót vagy a felhasználót figyelmen kívül hagyod?"; +"threads_beta_cancel" = "Nem most"; +"threads_beta_enable" = "Próbáld ki"; +"threads_beta_information_link" = "Tudj meg többet"; +"threads_beta_title" = "Üzenetszálak"; +"threads_notice_done" = "Értem"; +"threads_notice_title" = "Az üzenetszálak többé már nem kísérleti funkció! 🎉"; +"room_participants_invite_prompt_to_msg" = "Biztos, hogy meg akarod hívni őt: %@ ide: %@?"; +"onboarding_celebration_button" = "Gyerünk"; +"onboarding_celebration_message" = "A beállítások elmentve."; +"onboarding_celebration_title" = "Minden kész!"; +"onboarding_avatar_accessibility_label" = "Profilkép"; +"onboarding_avatar_message" = "Bármikor megváltoztatható."; +"onboarding_avatar_title" = "Profilkép hozzáadása"; +"onboarding_display_name_max_length" = "A megjelenítendő név 256 karakternél rövidebb legyen"; +"onboarding_display_name_hint" = "Ezt később meg lehet változtatni"; +"onboarding_display_name_placeholder" = "Megjelenítendő név"; +"onboarding_display_name_message" = "Ez fog megjelenni amikor üzenetet küldesz."; +"onboarding_display_name_title" = "Válassz egy megjelenítési nevet"; +"onboarding_personalization_skip" = "Lépés kihagyása"; +"onboarding_personalization_save" = "Mentés és tovább"; +"onboarding_congratulations_home_button" = "Vigyél haza"; +"onboarding_congratulations_personalize_button" = "Profil személyre szabása"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "A fiókod elkészült: %@."; +"onboarding_congratulations_title" = "Gratulálunk!"; +"saving" = "Mentés"; + +// Activities +"loading" = "Betöltés"; +"edit" = "Szerkeszt"; +"suggest" = "Javasol"; +"add" = "Hozzáad"; +"existing" = "Létező"; +"new_word" = "Új"; +"stop" = "Állj"; +"joining" = "Belépés"; +"threads_beta_information" = "Tedd a megbeszéléseket átláthatóvá üzenetszálakkal.\n\nAz üzenetszálak segítenek a beszélgetések témánál tartásában és követésében. "; +"threads_notice_information" = "A kísérleti időszakban készült üzenetszálak ezentúl közönséges válaszokként jelennek meg

Ez egy egyszeri konverzió, mivel az üzenetszálak már a Matrix részét képezik."; +"ignore_user" = "Felhasználó figyelmen kívül hagyása"; +"location_sharing_pin_drop_share_title" = "Ennek a pozíciónak az elküldése"; +"location_sharing_static_share_title" = "Jelenlegi pozícióm elküldése"; +"live_location_sharing_banner_stop" = "Megállítás"; +"live_location_sharing_banner_title" = "Folyamatos pozíció megosztás engedélyezve"; + +// MARK: Live location sharing + +"location_sharing_live_share_title" = "Pozícióm folyamatos megosztása"; +"side_menu_coach_message" = "Húzd jobbra vagy koppints hosszan minden szoba megjelenítéséhez"; +"spaces_add_room_missing_permission_message" = "Nincs jogosultságod szobát hozzáadni ehhez a térhez."; +"spaces_creation_in_one_space" = "1 térben"; +"spaces_creation_in_many_spaces" = "%@ térben"; +"spaces_creation_in_spacename_plus_many" = "itt: %@ + %@ tér"; +"spaces_creation_in_spacename_plus_one" = "itt: %@ +1 tér"; +"spaces_creation_in_spacename" = "itt: %@"; +"spaces_creation_post_process_inviting_users" = "%@ felhasználó meghívása"; +"spaces_creation_post_process_adding_rooms" = "%@ szoba hozzáadása"; +"spaces_creation_post_process_creating_room" = "Készítés: %@"; +"spaces_creation_post_process_uploading_avatar" = "Profilkép feltöltése"; +"spaces_creation_post_process_creating_space_task" = "Készítés: %@"; +"spaces_creation_post_process_creating_space" = "Tér készítése"; +"spaces_creation_invite_by_username_message" = "Később is meg tudod hívni őket."; +"spaces_creation_invite_by_username_title" = "Csoporttársak meghívása"; +"spaces_creation_invite_by_username" = "Meghívás felhasználónévvel"; +"spaces_creation_add_rooms_message" = "Ez a tér csak a tied, senki nem lesz értesítve. Később is hozzáadhatsz többet."; +"spaces_creation_add_rooms_title" = "Mit szeretne hozzáadni?"; +"spaces_creation_sharing_type_me_and_teammates_detail" = "Privát tér neked és a csoporttársaidnak"; +"spaces_creation_sharing_type_me_and_teammates_title" = "Én és a csoporttársaim"; +"spaces_creation_sharing_type_just_me_detail" = "Privát tér a szobáid csoportosításához"; +"spaces_creation_sharing_type_just_me_title" = "Csak én"; +"spaces_creation_sharing_type_message" = "Ellenőrizd, hogy a megfelelő személyeknek van hozzáférése ide: %@. Később ezt megváltoztathatod."; +"spaces_creation_sharing_type_title" = "Kivel dolgozol együtt?"; +"spaces_creation_email_invites_email_title" = "E-mail"; +"spaces_creation_email_invites_message" = "Később is meg tudod hívni őket."; +"spaces_creation_email_invites_title" = "Csoporttársak meghívása"; +"spaces_creation_new_rooms_support" = "Támogatás"; +"spaces_creation_new_rooms_random" = "Véletlen"; +"spaces_creation_new_rooms_general" = "Általános"; +"spaces_creation_new_rooms_room_name_title" = "Szoba neve"; +"spaces_creation_new_rooms_message" = "Mindenhez készítünk egy szobát."; +"spaces_creation_new_rooms_title" = "Milyen beszélgetések lesznek?"; +"spaces_creation_cancel_message" = "Amit eddig beállítottál elveszik."; +"spaces_creation_cancel_title" = "Megszakítod a tér készítését?"; +"spaces_creation_private_space_title" = "Privát tér"; +"spaces_creation_public_space_title" = "Nyilvános tér"; +"spaces_creation_address_already_exists" = "%@\nmár létezik"; +"spaces_creation_address_invalid_characters" = "%@\nérvénytelen karaktert tartalmaz"; +"spaces_creation_address_default_message" = "A tered látható lesz itt:\n%@"; +"spaces_creation_empty_room_name_error" = "Név szükséges"; +"spaces_creation_address" = "Cím"; +"spaces_creation_settings_message" = "Adj hozzá pár információt, hogy tűnjön ki. Bármikor megváltoztathatod."; +"spaces_creation_footer" = "Ezt később meg lehet változtatni"; +"spaces_creation_visibility_message" = "Létező térbe való belépéshez meghívó szükséges."; +"spaces_creation_visibility_title" = "Milyen típusú teret szeretnél készíteni?"; + +// Mark: - Space Creation + +"spaces_creation_hint" = "Szobák és emberek csoportosításának új lehetősége a Terek használata."; +"space_settings_current_address_message" = "A tered látható itt:\n%@"; +"space_settings_update_failed_message" = "A tér beállításainak megváltoztatása nem sikerült. Megpróbálod újra?"; +"space_settings_access_section" = "Ki tud hozzáférni a térhez?"; +"space_topic" = "Leírás"; +"space_public_join_rule_detail" = "Nyílt tér mindenkinek, a legjobb a közösségeknek"; +"spaces_add_space" = "Tér hozzáadása"; +"spaces_add_room" = "Szoba hozzáadása"; +"spaces_invite_people" = "Személyek meghívása"; +"space_private_join_rule_detail" = "Csak meghívóval, saját célra és csoportoknak ideális"; +"spaces_explore_rooms_one_room" = "1 szoba"; +"spaces_explore_rooms_room_number" = "%@ szoba"; +"spaces_create_space_title" = "Tér készítése"; +"spaces_add_space_title" = "Tér készítése"; +"space_invite_not_enough_permission" = "Nincs jogosultságod embereket meghívni erre a térre"; +"room_invite_not_enough_permission" = "Nincs jogosultságod embereket meghívni ebbe a szobába"; +"room_invite_to_room_option_detail" = "Nem lesznek a részesei ennek: %@."; +"room_invite_to_room_option_title" = "Csak ehhez a szobához"; +"room_invite_to_space_option_detail" = "Felderíthetik ezt: %@, de nem lesznek a tagsága ennek: %@."; + +// Mark: - Room invite + +"room_invite_to_space_option_title" = "Ide: %@"; +"share_invite_link_space_text" = "Szia, lépj be ebbe a térbe itt: %@"; +"share_invite_link_room_text" = "Szia, lépj be a szobába itt: %@"; + +// MARK: - Share invite link + +"share_invite_link_action" = "Meghívási link megosztása"; +"create_room_processing" = "Szoba készítés"; +"create_room_suggest_room_footer" = "A javasolt szobák a tér tagság számára csatlakozásra érdemesként lesznek feltüntetve."; +"room_access_space_chooser_other_spaces_section_info" = "Ezek valószínűleg olyanok, amelyeknek más %@ adminok is tagjai."; +"room_displayname_more_than_two_members" = "%@ és %@ mások"; +"location_sharing_live_list_item_stop_sharing_action" = "Megosztás megállítása"; +"location_sharing_live_list_item_current_user_display_name" = "Te"; +"location_sharing_live_list_item_last_update_invalid" = "Frissítés ideje ismeretlen"; +"location_sharing_live_list_item_last_update" = "Frissítve ekkor: %@"; +"location_sharing_live_list_item_sharing_expired" = "A megosztás lejárt"; +"location_sharing_live_list_item_time_left" = "%@ elhagyta a szobát"; +"location_sharing_live_viewer_title" = "Földrajzi helyzet"; +"location_sharing_live_map_callout_title" = "Tartózkodási hely megosztása"; +"room_access_settings_screen_upgrade_alert_note" = "Vedd figyelembe, hogy a fejlesztés a szoba új verzióját hozza létre. Minden jelenlegi üzenet itt marad az archivált szobában."; +"room_access_settings_screen_upgrade_alert_message_no_param" = "A szülő térből bárki megtalálhatja és beléphet ebbe a szobába - nem kell meghívni egyenként senkit. Ezt a beállítást bármikor megváltoztathatod a szoba beállításokban."; +"room_access_settings_screen_upgrade_alert_message" = "Itt: %@ bárki megtalálhatja és beléphet ebbe a szobába - nem kell meghívni egyenként senkit. Ezt a beállítást bármikor megváltoztathatod a szoba beállításokban."; +"settings_presence_offline_mode_description" = "Ha engedélyezed úgy látszol majd mások számára, mintha nem kapcsolódnál a hálózathoz még akkor is amikor az alkalmazást használod."; +"settings_presence_offline_mode" = "Kapcsolat nélküli mód"; +"settings_presence" = "Állapot"; +"threads_discourage_information_2" = "\n\nEnnek ellenére be szeretné kapcsolni az üzenetszálakat?"; +"threads_discourage_information_1" = "A Matrix szervered jelenleg nem támogatja az üzenetszálakat így ez a funkció nem lesz megbízható. Bizonyos üzenetszálas üzenetek nem jelennek meg megbízhatóan. "; diff --git a/Riot/Assets/id.lproj/Localizable.strings b/Riot/Assets/id.lproj/Localizable.strings index 9961bfd63..4b2a6be15 100644 --- a/Riot/Assets/id.lproj/Localizable.strings +++ b/Riot/Assets/id.lproj/Localizable.strings @@ -65,13 +65,11 @@ "REACTION_FROM_USER" = "%@ mereaksi %@"; /* Look, stuff's happened, alright? Just open the app. */ -"MSGS_IN_TWO_PLUS_ROOMS" = "%@ pesan baru di %@, %@ dan %@ lainnya"; /* Multiple messages in two rooms */ "MSGS_IN_TWO_ROOMS" = "%@ pesan baru di %@ dan %@"; /* Multiple unread messages from two plus people (ie. for 4+ people: 'others' replaces the third person) */ -"MSGS_FROM_TWO_PLUS_USERS" = "%@ pesan baru dari %@, %@ dan %@ lainnya"; /* Multiple unread messages from three people */ "MSGS_FROM_THREE_USERS" = "%@ pesan baru dari %@, %@ dan %@"; @@ -168,3 +166,9 @@ /* New file message from a specific person, not referencing a room. */ "LOCATION_FROM_USER" = "%@ membagikan lokasinya"; + +/* Look, stuff's happened, alright? Just open the app. */ +"MSGS_IN_TWO_PLUS_ROOMS" = "%@ pesan baru dalam %@, %@ dan lainnya"; + +/* Multiple unread messages from two plus people (ie. for 4+ people: 'others' replaces the third person) */ +"MSGS_FROM_TWO_PLUS_USERS" = "%@ pesan baru dari %@, %@ dan lainnya"; diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 50b6eba23..79e0037f8 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -258,7 +258,7 @@ "call_transfer_title" = "Pindahkan"; "room_info_list_section_other" = "Lainnya"; "create_room_placeholder_address" = "#ruangantest:matrix.org"; -"create_room_placeholder_topic" = "Topik"; +"create_room_placeholder_topic" = "Tentang apa ruangan ini?"; "create_room_placeholder_name" = "Nama"; "biometrics_cant_unlocked_alert_message_retry" = "Coba lagi"; "pin_protection_reset_alert_action_reset" = "Atur Ulang"; @@ -667,7 +667,7 @@ // GDPR "gdpr_consent_not_given_alert_message" = "Untuk terus menggunakan homeserver %@, Anda harus menyetujui syarat dan ketentuannya."; "share_extension_failed_to_encrypt" = "Gagal untuk mengirim. Periksa di aplikasi utama pengaturan enkripsi untuk ruangan ini"; -"bug_report_logs_description" = "Untuk mendiagnosis masalah, catat dari klien ini akan dikirim dengan laporan kutu ini. Jika Anda lebih memilih untuk hanya mengirim teks di atas, harap hapus centang:"; +"bug_report_logs_description" = "Untuk mendiagnosis masalah, catatan dari klien ini akan dikirim dengan laporan kutu ini. Jika Anda lebih memilih untuk hanya mengirim teks di atas, harap hapus centang:"; "bug_report_description" = "Harap jelaskan bugnya. Apa yang Anda lakukan? Apa yang Anda harapkan terjadi? Apa yang sebenarnya terjadi?"; "e2e_key_backup_wrong_version" = "Cadangan kunci pesan aman baru telah terdeteksi.\n\nJika ini bukan Anda, atur Frasa Keamanan baru di Pengaturan."; @@ -675,7 +675,6 @@ "e2e_enabling_on_app_update" = "%@ sekarang mendukung enkripsi ujung-ke-ujung tetapi Anda harus masuk lagi untuk mengaktifkannya.\n\nAnda dapat melakukannya sekarang atau nanti di pengaturan aplikasi."; // Crash report -"google_analytics_use_prompt" = "Apakah Anda ingin membantu meningkatkan %@ dengan secara otomatis melaporkan laporan kerusakan dan data penggunaan secara anonim?"; "no_voip" = "%@ sedang memanggil Anda tetapi %@ belum mendukung panggilan.\nAnda dapat mengabaikan notifikasi ini dan jawab panggilannya di perangkat yang lain atau menolak panggilannya."; "call_no_stun_server_error_message_1" = "Mohon hubungi administrator homeserver %@ Anda untuk mengatur sebuah server TURN supaya panggilan dapat bekerja dengan handal."; "rage_shake_prompt" = "Anda tampaknya menggoyangkan telepon dengan frustrasi. Apakah Anda ingin mengirimkan laporan bug?"; @@ -837,7 +836,7 @@ "voice_message_release_to_send" = "Tahan untuk merekam, lepaskan untuk mengirim"; "spaces_empty_space_title" = "Space ini belum ada ruangan"; -"create_room_show_in_directory" = "Tampilkan ruangan di direktori"; +"create_room_show_in_directory" = "Tampilkan di direktori ruangan"; "secrets_reset_authentication_message" = "Masukkan kata sandi akun Matrix Anda untuk mengkonfirmasi"; "secrets_recovery_with_key_information_unlock_secure_backup_with_key" = "Masukkan Kunci Keamanan Anda untuk melanjutkan."; "secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "Masukkan Frasa Keamanan Anda untuk melanjutkan."; @@ -1065,7 +1064,7 @@ "room_intro_cell_information_room_without_topic_sentence2_part1" = "Tambahkan sebuah topik"; "room_avatar_view_accessibility_hint" = "Ubah avatar ruangan"; "call_transfer_error_message" = "Gagal memindahkan panggilan"; -"create_room_section_header_topic" = "Topik ruangan (opsional)"; +"create_room_section_header_topic" = "TOPIK (OPSIONAL)"; "searchable_directory_search_placeholder" = "Nama atau ID"; "biometrics_cant_unlocked_alert_message_login" = "Masuk kembali"; "biometrics_cant_unlocked_alert_title" = "Tidak dapat membuka kunci aplikasi"; @@ -1260,13 +1259,13 @@ // MARK: - Room Info "room_info_list_one_member" = "1 anggota"; -"create_room_section_header_address" = "Alamat ruangan"; -"create_room_type_public" = "Ruangan Publik"; -"create_room_type_private" = "Ruangan Privat"; -"create_room_section_header_type" = "Tipe ruangan"; +"create_room_section_header_address" = "ALAMAT"; +"create_room_type_public" = "Ruangan Publik (siapa saja)"; +"create_room_type_private" = "Ruangan Privat (undangan saja)"; +"create_room_section_header_type" = "SIAPA SAJA YANG DAPAT MENGAKSES"; "create_room_enable_encryption" = "Aktifkan Enkripsi"; -"create_room_section_header_encryption" = "Enkripsi ruangan"; -"create_room_section_header_name" = "Nama ruangan"; +"create_room_section_header_encryption" = "ENKRIPSI"; +"create_room_section_header_name" = "NAMA"; // MARK: - Create Room @@ -2070,7 +2069,6 @@ "notice_room_third_party_revoked_invite_by_you_for_dm" = "Anda menghilangkan undangannya %@"; "notice_room_third_party_registered_invite_by_you" = "Anda menerima undangan untuk %@"; "notice_room_third_party_invite_by_you_for_dm" = "Anda mengundang %@"; -"notice_room_third_party_invite_by_you" = "Anda mengirim sebuah undangan ke @% untuk bergabung ke ruangan ini"; "notice_room_invite_you" = "%@ mengundang Anda"; // Notice Events with "You" @@ -2335,3 +2333,182 @@ "room_participants_leave_processing" = "Meninggalkan"; "notice_error_unformattable_event" = "** Tidak dapat memuat pesan. Mohon laporkan sebuah kutu"; "settings_labs_use_only_latest_user_avatar_and_name" = "Tampilkan avatar dan nama terkini untuk pengguna di riwayat pesan"; +"ignore_user" = "Abaikan Pengguna"; +"location_sharing_pin_drop_share_title" = "Kirim lokasi ini"; +"location_sharing_static_share_title" = "Kirimkan lokasi saya saat ini"; +"live_location_sharing_banner_stop" = "Hentikan"; +"live_location_sharing_banner_title" = "Lokasi langsung diaktifkan"; + +// MARK: Live location sharing + +"location_sharing_live_share_title" = "Bagikan lokasi langsung"; +"side_menu_coach_message" = "Usap kanan atau ketuk untuk melihat semua ruangan"; +"spaces_add_room_missing_permission_message" = "Anda tidak memiliki izin untuk menambahkan ruangan ke space ini."; +"spaces_creation_in_one_space" = "dalam 1 space"; +"spaces_creation_in_many_spaces" = "dalam %@ space"; +"spaces_creation_in_spacename_plus_many" = "dalam %@ + %@ space"; +"spaces_creation_in_spacename_plus_one" = "dalam %@ + 1 space"; +"spaces_creation_in_spacename" = "dalam %@"; +"spaces_creation_post_process_inviting_users" = "Mengundang %@ pengguna"; +"spaces_creation_post_process_adding_rooms" = "Menambahkan %@ ruangan"; +"spaces_creation_post_process_creating_room" = "Membuat %@"; +"spaces_creation_post_process_uploading_avatar" = "Mengunggah avatar"; +"spaces_creation_post_process_creating_space_task" = "Membuat %@"; +"spaces_creation_post_process_creating_space" = "Membuat space"; +"spaces_creation_invite_by_username_message" = "Anda juga dapat mengundang mereka nanti."; +"spaces_creation_invite_by_username_title" = "Undang tim Anda"; +"spaces_creation_invite_by_username" = "Undang dengan nama pengguna"; +"spaces_creation_add_rooms_message" = "Space ini hanya untuk Anda, tidak akan diberitahukan kepada siapa saja. Anda dapat menambahkan lagi nanti."; +"spaces_creation_add_rooms_title" = "Apa yang Anda ingin tambahkan?"; +"spaces_creation_sharing_type_me_and_teammates_detail" = "Space privat untuk Anda & tim Anda"; +"spaces_creation_sharing_type_me_and_teammates_title" = "Saya dan tim saya"; +"spaces_creation_sharing_type_just_me_detail" = "Space yang privat untuk mengorganisir ruangan Anda"; +"spaces_creation_sharing_type_just_me_title" = "Hanya saya saja"; +"spaces_creation_sharing_type_message" = "Pastikan orang yang tepat memiliki akses ke %@. Anda dapat mengubahnya nanti."; +"spaces_creation_sharing_type_title" = "Dengan siapa Anda bekerja?"; +"spaces_creation_email_invites_email_title" = "Email"; +"spaces_creation_email_invites_message" = "Anda juga dapat mengundang mereka nanti."; +"spaces_creation_email_invites_title" = "Undang tim Anda"; +"spaces_creation_new_rooms_support" = "Dukungan"; +"spaces_creation_new_rooms_random" = "Acakan"; +"spaces_creation_new_rooms_general" = "Umum"; +"spaces_creation_new_rooms_room_name_title" = "Nama ruangan"; +"spaces_creation_new_rooms_message" = "Kami akan membuat ruangan untuk masing-masing."; +"spaces_creation_new_rooms_title" = "Apa saja diskusi yang akan Anda lakukan?"; +"spaces_creation_cancel_message" = "Kemajuan Anda akan hilang."; +"spaces_creation_cancel_title" = "Berhenti membuat sebuah space?"; +"spaces_creation_private_space_title" = "Space privat Anda"; +"spaces_creation_public_space_title" = "Space publik Anda"; +"spaces_creation_address_already_exists" = "%@\nsudah ada"; +"spaces_creation_address_invalid_characters" = "%@\nmemiliki karakter yang tidak valid"; +"spaces_creation_address_default_message" = "Space Anda dapat ditampilkan di\n%@"; +"spaces_creation_empty_room_name_error" = "Nama dibutuhkan"; +"spaces_creation_address" = "Alamat"; +"spaces_creation_settings_message" = "Tambahkan detail untuk membuatnya unik. Anda dapat mengubahnya di waktu yang mendatang."; +"spaces_creation_footer" = "Anda dapat mengubahnya nanti"; +"spaces_creation_visibility_message" = "Untuk bergabung ke space yang sudah ada, Anda harus memiliki sebuah undangan."; +"spaces_creation_visibility_title" = "Tipe space apa yang Anda ingin buat?"; + +// Mark: - Space Creation + +"spaces_creation_hint" = "Space adalah cara baru untuk mengelompokkan ruangan dan orang."; +"space_settings_current_address_message" = "Space Anda dapat ditampilkan di\n%@"; +"space_settings_update_failed_message" = "Gagal memperbarui pengaturan space. Apakah Anda ingin mencoba ulang?"; +"space_settings_access_section" = "Siapa saja yang dapat mengakses space ini?"; +"space_topic" = "Deskripsi"; +"space_public_join_rule_detail" = "Terbuka kepada siapa saja, baik untuk komunitas"; +"spaces_add_space" = "Tambahkan space"; +"spaces_add_room" = "Tambahkan ruangan"; +"spaces_invite_people" = "Undang orang"; +"space_private_join_rule_detail" = "Undangan saja, baik untuk Anda sendiri atau tim"; +"spaces_explore_rooms_one_room" = "1 ruangan"; +"spaces_explore_rooms_room_number" = "%@ ruangan"; +"spaces_create_space_title" = "Buat sebuah space"; +"spaces_add_space_title" = "Buat space"; +"space_invite_not_enough_permission" = "Anda tidak memiliki izin untuk mengundang orang-orang ke space ini"; +"room_invite_not_enough_permission" = "Anda tidak memiliki izin untuk mengundang orang-orang ke ruangan ini"; +"room_invite_to_room_option_detail" = "Mereka tidak akan menjadi bagian dari %@."; +"room_invite_to_room_option_title" = "Ke ruangan ini saja"; +"room_invite_to_space_option_detail" = "Mereka dapat menjelajahi %@, tetapi tidak akan sebagai anggota %@."; + +// Mark: - Room invite + +"room_invite_to_space_option_title" = "Ke %@"; +"share_invite_link_space_text" = "Hei, bergabung space ini di %@"; +"share_invite_link_room_text" = "Hei, bergabung ruangan ini di %@"; + +// MARK: - Share invite link + +"share_invite_link_action" = "Bagikan tautan undangan"; +"create_room_processing" = "Membuat ruangan"; +"create_room_suggest_room_footer" = "Ruangan yang disarankan dipromosikan kepada anggota space sebagai ruangan yang baik untuk bergabung."; +"create_room_suggest_room" = "Sarankan kepada anggota space"; +"create_room_show_in_directory_footer" = "Ini akan membantu orang-orang menemukan dan bergabung."; +"create_room_promotion_header" = "PROMOSI"; +"create_room_section_footer_type_public" = "Hanya orang yang diundang dapat mencari dan bergabung, tidak hanya orang-orang yang berada di nama Space."; +"create_room_section_footer_type_restricted" = "Siapa saja dalam nama Space dapat menemukan dan bergabung."; +"create_room_section_footer_type_private" = "Hanya orang yang diundang dapat menemukan dan bergabung."; +"create_room_type_restricted" = "Anggota space"; +"call_jitsi_unable_to_start" = "Tidak dapat memulai panggilan konferensi"; +"room_suggestion_settings_screen_message" = "Ruangan yang disarankan dipromosikan untuk anggota space sebagai ruangan yang baik untuk bergabung."; +"room_suggestion_settings_screen_title" = "Buat sebuah ruangan sebagai disarankan dalam sebuah space"; + +// Room suggestion Settings +"room_suggestion_settings_screen_nav_title" = "Sarankan ruangan"; +"room_access_space_chooser_other_spaces_section_info" = "Ini mungkin hal-hal yang menjadi bagian dari admin %@ lainnya."; +"room_access_space_chooser_other_spaces_section" = "Space atau ruangan lainnya"; +"room_access_space_chooser_known_spaces_section" = "Space yang Anda tahu berisi %@"; +"room_access_settings_screen_setting_room_access" = "Menetapkan akses ruangan"; +"room_access_settings_screen_upgrade_alert_upgrading" = "Meningkatkan ruangan"; +"room_access_settings_screen_upgrade_alert_upgrade_button" = "Tingkatkan"; +"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "Undang anggota secara otomatis ke ruangan baru"; +"room_access_settings_screen_upgrade_alert_title" = "Tingkatkan ruangan"; +"room_access_settings_screen_public_message" = "Siapa saja dapat menemukan dan bergabung."; +"room_access_settings_screen_edit_spaces" = "Edit space"; +"room_access_settings_screen_upgrade_required" = "Peningkatan diperlukan"; +"room_access_settings_screen_restricted_message" = "Biarkan siapa saja dalam space menemukan dan bergabung.\nAnda akan ditanyakan untuk mengkonfirmasi space apa saja."; +"room_access_settings_screen_private_message" = "Hanya orang yang diundang dapat menemukan dan bergabung."; +"room_access_settings_screen_message" = "Tentukan siapa yang dapat menemukan dan bergabung %@."; +"room_access_settings_screen_title" = "Siapa saja yang dapat mengakses ruangan ini?"; + +// Room Access Settings +"room_access_settings_screen_nav_title" = "Akses ruangan"; +"room_details_promote_room_suggest_title" = "Sarankan kepada anggota space"; +"room_details_promote_room_title" = "Promosikan ruangan"; +"room_details_access_row_title" = "Akses"; +"settings_labs_enable_auto_report_decryption_errors" = "Laporkan Kesalahan Pendekripsian Secara Otomatis"; +"room_preview_decline_invitation_options" = "Apakah Anda ingin menolak undangannya atau mengabaikan pengguna ini?"; +"threads_beta_cancel" = "Tidak sekarang"; +"threads_beta_enable" = "Coba"; +"threads_beta_information_link" = "Pelajari lebih lanjut"; +"threads_beta_information" = "Buat diskusi terorganisasi dengan utasan.\n\nUtasan membantu Anda membuat percakapan Anda sesuai topik dan mudah untuk dilacak. "; +"threads_beta_title" = "Utasan"; +"threads_notice_done" = "Mengerti"; +"threads_notice_information" = "Semua utasan yang dibuat selama periode eksperimental akan ditampilkan sebagai balasan biasa.

Ini adalah transisi sekali, utasan sekarang menjadi bagian dari spesifikasi Matrix."; +"threads_notice_title" = "Utasan tidak lagi eksperimental 🎉"; +"room_participants_invite_prompt_to_msg" = "Apakah Anda yakin ingin mengundang %@ ke %@?"; +"onboarding_celebration_button" = "Ayo"; +"onboarding_celebration_message" = "Preferensi Anda telah disimpan."; +"onboarding_celebration_title" = "Anda siap!"; +"onboarding_avatar_accessibility_label" = "Foto profil"; +"onboarding_avatar_message" = "Anda dapat mengubahnya kapan saja."; +"onboarding_avatar_title" = "Tambahkan foto profil"; +"onboarding_display_name_max_length" = "Nama tampilan Anda harus tidak lebih dari 256 karakter"; +"onboarding_display_name_hint" = "Anda dapat mengubahnya nanti"; +"onboarding_display_name_placeholder" = "Nama Tampilan"; +"onboarding_display_name_message" = "Ini akan ditampilkan ketika Anda mengirim pesan."; +"onboarding_display_name_title" = "Pilih sebuah nama tampilan"; +"onboarding_personalization_skip" = "Lewati"; +"onboarding_personalization_save" = "Simpan dan lanjutkan"; +"onboarding_congratulations_home_button" = "Kembalikan saya ke beranda"; +"onboarding_congratulations_personalize_button" = "Ubah profil"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "Akun %@ Anda telah dibuat."; +"onboarding_congratulations_title" = "Selamat!"; +"saving" = "Menyimpan"; + +// Activities +"loading" = "Memuat"; +"stop" = "Hentikan"; +"edit" = "Edit"; +"suggest" = "Sarankan"; +"add" = "Tambahkan"; +"existing" = "Sudah ada"; +"new_word" = "Baru"; +"joining" = "Bergabung"; +"location_sharing_live_list_item_stop_sharing_action" = "Berhenti membagikan"; +"location_sharing_live_list_item_current_user_display_name" = "Anda"; +"location_sharing_live_list_item_last_update_invalid" = "Pembaruan terakhir tidak diketahui"; +"location_sharing_live_list_item_last_update" = "Diperbarui %@ yang lalu"; +"location_sharing_live_list_item_sharing_expired" = "Pembagian kadaluwarsa"; +"location_sharing_live_list_item_time_left" = "%@ lagi"; +"location_sharing_live_viewer_title" = "Lokasi"; +"location_sharing_live_map_callout_title" = "Bagikan lokasi"; +"room_access_settings_screen_upgrade_alert_note" = "Dicatat bahwa meningkatkan akan membuat versi baru dari ruangannya. Semua pesan saat ini akan tetap di ruangan yang diarsip."; +"room_access_settings_screen_upgrade_alert_message_no_param" = "Siapa saja di induk ruangan dapat menemukan dan bergabung ke ruangan ini — tidak perlu mengundang semua secara manual. Anda dapat mengubahnya di pengaturan ruangan kapan saja."; +"room_access_settings_screen_upgrade_alert_message" = "Siapa saja di %@ dapat menemukan dan bergabung ke ruangan ini — tidak perlu mengundang semua secara manual. Anda dapat mengubahnya di pengaturan ruangan kapan saja."; +"settings_presence_offline_mode_description" = "Jika diaktifkan, Anda akan selalu terlihat luring kepada pengguna lain, bahkan ketika menggunakan aplikasi."; +"settings_presence_offline_mode" = "Mode Luring"; +"settings_presence" = "Presensi"; +"threads_discourage_information_2" = "\n\nApakah Anda ingin mengaktifkan utasan?"; +"threads_discourage_information_1" = "Homeserver Anda saat ini tidak mendukung utasan, jadi fitur ini mungkin tidak andal. Beberapa pesan yang diutas mungkin tidak tersedia. "; diff --git a/Riot/Assets/is.lproj/Localizable.strings b/Riot/Assets/is.lproj/Localizable.strings index 0786cfd97..03bb88796 100644 --- a/Riot/Assets/is.lproj/Localizable.strings +++ b/Riot/Assets/is.lproj/Localizable.strings @@ -70,7 +70,6 @@ "REACTION_FROM_USER" = "%@ brást við með %@"; /* Look, stuff's happened, alright? Just open the app. */ -"MSGS_IN_TWO_PLUS_ROOMS" = "@ ný skilaboð í %@, %@ og víðar"; /* Multiple messages in two rooms */ "MSGS_IN_TWO_ROOMS" = "%@ ný skilaboð í %@ og %@"; diff --git a/Riot/Assets/is.lproj/Vector.strings b/Riot/Assets/is.lproj/Vector.strings index 069a72d7e..753c6f52b 100644 --- a/Riot/Assets/is.lproj/Vector.strings +++ b/Riot/Assets/is.lproj/Vector.strings @@ -511,9 +511,9 @@ // MARK: - Home "home_empty_view_title" = "Velkomin í %@,\n%@"; -"create_room_type_private" = "Einkaspjallrás"; -"create_room_section_header_type" = "Tegund spjallrásar"; -"create_room_section_header_encryption" = "Dulritun spjallrásar"; +"create_room_type_private" = "Einkaspjallrás (einungis með boði)"; +"create_room_section_header_type" = "HVER HEFUR AÐGANG"; +"create_room_section_header_encryption" = "DULRITUN"; "searchable_directory_search_placeholder" = "Nafn eða auðkenni"; "biometrics_cant_unlocked_alert_message_login" = "Skráðu þig aftur inn"; "biometrics_mode_face_id" = "Face ID"; @@ -569,7 +569,6 @@ // unrecognized SSL certificate "ssl_trust" = "Treysta"; "call_transfer_to_user" = "Færa á %@"; -"call_video_with_user" = "Myndsímtal við %s@"; "call_voice_with_user" = "Raddsímtal við %@"; "call_more_actions_dialpad" = "Talnaborð"; "call_more_actions_transfer" = "Flutningur"; @@ -895,10 +894,10 @@ // MARK: - Room Info "room_info_list_one_member" = "1 meðlimur"; -"create_room_section_header_address" = "Vistfang spjallrásar"; -"create_room_type_public" = "Almenningsspjallrás"; +"create_room_section_header_address" = "VISTFANG"; +"create_room_type_public" = "Almenningsspjallrás (hver sem er)"; "create_room_enable_encryption" = "Þvinga dulritun"; -"create_room_placeholder_topic" = "Umfjöllunarefni"; +"create_room_placeholder_topic" = "Um hvað er rætt á þessari spjallrás?"; "key_verification_tile_request_incoming_approval_decline" = "Hafna"; "key_verification_tile_request_incoming_approval_accept" = "Samþykkja"; "key_verification_tile_request_status_cancelled" = "Hætt við %@"; @@ -1293,7 +1292,7 @@ "settings_call_invitations" = "Boð um símtöl"; "settings_room_invitations" = "Boð á spjallrás"; "settings_messages_containing_keywords" = "Stikkorð"; -"settings_messages_containing_at_room" = "@spjallrás"; +"settings_messages_containing_at_room" = "@room"; "settings_messages_containing_user_name" = "Notandanafnið mitt"; "settings_messages_containing_display_name" = "Birtingarnafn mitt"; "settings_encrypted_group_messages" = "Dulrituð hópskilaboð"; @@ -1381,9 +1380,9 @@ "room_recents_suggested_rooms_section" = "TILLÖGUR AÐ SPJALLRÁSUM"; "room_recents_server_notice_section" = "AÐVARANIR KERFIS"; "social_login_button_title_sign_up" = "Skrá inn með %@"; -"create_room_section_header_topic" = "Umfjöllunarefni spjallrásar (valkvætt)"; +"create_room_section_header_topic" = "UMFJÖLLUNAREFNI (VALKVÆTT)"; "create_room_placeholder_name" = "Heiti"; -"create_room_section_header_name" = "Heiti spjallrásar"; +"create_room_section_header_name" = "NAFN"; // MARK: - Create Room @@ -1535,7 +1534,7 @@ "invite_friends_action" = "Bjóða vinum á %@"; "call_transfer_error_message" = "Flutningur símtals mistókst"; -"create_room_show_in_directory" = "Birta spjallrás í spjallrásalistanum"; +"create_room_show_in_directory" = "Birta spjallrás í spjallrásalista"; "create_room_section_footer_encryption" = "Ekki er hægt að gera dulritun óvirka eftirá."; "biometrics_cant_unlocked_alert_title" = "Get ekki aflæst forriti"; "biometrics_usage_reason" = "Þú þarft að auðkenna þig til að fá aðgang að forritinu þínu"; diff --git a/Riot/Assets/it.lproj/Localizable.strings b/Riot/Assets/it.lproj/Localizable.strings index 824a5f1e9..bb8b3e707 100644 --- a/Riot/Assets/it.lproj/Localizable.strings +++ b/Riot/Assets/it.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ ha invitato un'immagine %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ ha inviato un'immagine %@ in %@"; /* A single unread message in a room */ diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 83bf336fc..b4ac7ff35 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -154,7 +154,6 @@ "contacts_address_book_section" = "CONTATTI LOCALI"; "contacts_address_book_matrix_users_toggle" = "Solo utenti Matrix"; "contacts_address_book_no_contact" = "Nessun contatto locale"; -"contacts_address_book_permission_required" = "%@ deve essere autorizzato per poter accedere alla Rubrica locale"; "contacts_address_book_permission_denied" = "Non hai autorizzato %@ ad accedere alla rubrica locale"; "contacts_user_directory_section" = "ELENCO UTENTI"; "contacts_user_directory_offline_section" = "ELENCO UTENTI (offline)"; @@ -520,7 +519,6 @@ "no_voip_title" = "Chiamata in arrivo"; "no_voip" = "%@ ti sta chiamando, ma %@ non supporta ancora le chiamate.\nPuoi ignorare questa notifica e rispondere alla chiamata da un altro dispositivo oppure puoi rifiutare la chiamata."; // Crash report -"google_analytics_use_prompt" = "Vuoi aiutare a migliorare %@ inviando automaticamente in modo anonimo i dati di utilizzo e le segnalazioni di crash?"; // Crypto "e2e_enabling_on_app_update" = "%@ ora supporta la crittografia da-utente-a-utente ma devi eseguire nuovamente l'accesso per abilitarla.\n\nPuoi farlo ora o più tardi dalle impostazioni dell'applicazione."; "e2e_need_log_in_again" = "È necessario eseguire nuovamente l'accesso per generare le chiavi di crittografia end-to-end per questa sessione ed inviare la chiave pubblica all'homeserver.\nVa fatto una sola volta; ci scusiamo per il disturbo."; @@ -534,7 +532,6 @@ "bug_report_description" = "Per favore descrivi l'errore. Cosa hai fatto? Cosa ti aspettavi dovesse accadere? Cosa è effettivamente successo?"; "bug_crash_report_title" = "Segnalazione del crash"; "bug_crash_report_description" = "Per favore descrivi cosa hai fatto prima del crash:"; -"bug_report_logs_description" = "Al fine di diagnosticare i problemi, i registri %@ di questo dispositivo saranno inviati con il rapporto dell'errore. Se preferisci inviare solo il testo soprastante, deseleziona:"; "bug_report_send_logs" = "Invia registri"; "bug_report_send_screenshot" = "Invia screenshot"; "bug_report_progress_zipping" = "Ottenimento registri"; @@ -690,7 +687,6 @@ "widget_integrations_server_failed_to_connect" = "La connessione al Server Integrazioni é fallita"; // Service terms "service_terms_modal_title" = "Termini del servizio"; -"service_terms_modal_message" = "Per continuare devi accettare i Termini di servizio (%@)."; "service_terms_modal_accept_button" = "Accetta"; "service_terms_modal_description_for_identity_server" = "Gli altri utenti possono trovarti"; "service_terms_modal_description_for_integration_manager" = "Usa bot, widget e stickers"; @@ -878,7 +874,6 @@ "service_terms_modal_description_for_identity_server_2" = "Fatti trovare attraverso il tuo numero di telefono e la tua email"; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "Trova contatti"; -"service_terms_modal_message_identity_server" = "Accetta i termini di servizio del server d'identità (%@) per trovare altri contatti."; // Generic errors "error_invite_3pid_with_no_identity_server" = "Aggiungi un server d'identità nelle impostazioni per poter invitare utenti tramite email."; "settings_add_3pid_password_title_email" = "Aggiungi indirizzo email"; @@ -1180,19 +1175,19 @@ "searchable_directory_x_network" = "Rete %@"; "searchable_directory_search_placeholder" = "Nome o ID"; "create_room_title" = "Nuova stanza"; -"create_room_section_header_name" = "Nome stanza"; +"create_room_section_header_name" = "NOME"; "create_room_placeholder_name" = "Nome"; -"create_room_section_header_topic" = "Argomento stanza (facoltativo)"; -"create_room_placeholder_topic" = "Argomento"; -"create_room_section_header_encryption" = "Crittografia stanza"; +"create_room_section_header_topic" = "ARGOMENTO (FACOLTATIVO)"; +"create_room_placeholder_topic" = "Di cosa parla questa stanza?"; +"create_room_section_header_encryption" = "CRITTOGRAFIA"; "create_room_enable_encryption" = "Attiva crittografia"; "create_room_section_footer_encryption" = "La crittografia non può essere disattivata in seguito."; -"create_room_section_header_type" = "Tipo stanza"; -"create_room_type_private" = "Stanza privata"; -"create_room_type_public" = "Stanza pubblica"; +"create_room_section_header_type" = "CHI PUÒ ACCEDERE"; +"create_room_type_private" = "Stanza privata (solo a invito)"; +"create_room_type_public" = "Stanza pubblica (chiunque)"; "create_room_section_footer_type" = "Le persone entrano in una stanza privata solo su invito."; -"create_room_show_in_directory" = "Mostra la stanza nell'elenco"; -"create_room_section_header_address" = "Indirizzo stanza"; +"create_room_show_in_directory" = "Mostra nell'elenco stanze"; +"create_room_section_header_address" = "INDIRIZZO"; "create_room_placeholder_address" = "#stanzatest:matrix.org"; "room_info_list_room_encrypted" = "I messaggi in questa stanza sono cifrati end-to-end"; "room_info_list_one_member" = "1 membro"; @@ -1735,7 +1730,6 @@ "notice_room_aliases" = "Gli alias di questo canale sono: %@"; "notice_room_related_groups" = "I gruppi associati a questo canale sono: %@"; "notice_encrypted_message" = "Messaggio criptato"; -"notice_encryption_enabled" = "%@ ha attivato la crittografia end-to-end (algoritmo %@)"; "notice_image_attachment" = "allegato immagine"; "notice_audio_attachment" = "allegato audio"; "notice_video_attachment" = "allegato video"; @@ -2111,3 +2105,184 @@ "room_participants_leave_processing" = "Uscita in corso"; "notice_error_unformattable_event" = "** Impossibile visualizzare il messaggio. Si prega di segnalare l'errore"; "settings_labs_use_only_latest_user_avatar_and_name" = "Mostra avatar e nome più recenti per gli utenti nella cronologia dei messaggi"; +"ignore_user" = "Ignora utente"; +"location_sharing_pin_drop_share_title" = "Invia questa posizione"; +"location_sharing_static_share_title" = "Invia la mia posizione attuale"; +"live_location_sharing_banner_stop" = "Ferma"; +"live_location_sharing_banner_title" = "Posizione in tempo reale attivata"; + +// MARK: Live location sharing + +"location_sharing_live_share_title" = "Condividi posizione in tempo reale"; +"side_menu_coach_message" = "Scorri a destra o tocca per vedere tutte le stanze"; +"spaces_add_room_missing_permission_message" = "Non hai i permessi per aggiungere stanze in questo spazio."; +"spaces_creation_in_one_space" = "in 1 spazio"; +"spaces_creation_in_many_spaces" = "in %@ spazi"; +"spaces_creation_in_spacename_plus_many" = "in %@ + %@ spazi"; +"spaces_creation_in_spacename_plus_one" = "in %@ + 1 spazio"; +"spaces_creation_in_spacename" = "in %@"; +"spaces_creation_post_process_inviting_users" = "Invito di %@ utenti"; +"spaces_creation_post_process_adding_rooms" = "Aggiunta di %@ stanze"; +"spaces_creation_post_process_creating_room" = "Creazione di %@"; +"spaces_creation_post_process_uploading_avatar" = "Invio dell'avatar"; +"spaces_creation_post_process_creating_space_task" = "Creazione di %@"; +"spaces_creation_post_process_creating_space" = "Creazione spazio"; +"spaces_creation_invite_by_username_message" = "Puoi invitarli anche più tardi."; +"spaces_creation_invite_by_username_title" = "Invita la tua squadra"; +"spaces_creation_invite_by_username" = "Invita per nome utente"; +"spaces_creation_add_rooms_message" = "Dato che questo spazio è solo per te, nessuno verrà informato. Puoi aggiungerne altri più tardi."; +"spaces_creation_add_rooms_title" = "Cosa vuoi aggiungere?"; +"spaces_creation_sharing_type_me_and_teammates_detail" = "Uno spazio privato per te e i tuoi compagni di squadra"; +"spaces_creation_sharing_type_me_and_teammates_title" = "Io e i compagni di squadra"; +"spaces_creation_sharing_type_just_me_detail" = "Uno spazio privato per organizzare le tue stanze"; +"spaces_creation_sharing_type_just_me_title" = "Solo io"; +"spaces_creation_sharing_type_message" = "Assicurati che le giuste persone abbiano accesso %@. Puoi cambiarlo in seguito."; +"spaces_creation_sharing_type_title" = "Con chi stai lavorando?"; +"spaces_creation_email_invites_email_title" = "Email"; +"spaces_creation_email_invites_message" = "Puoi invitarli anche più tardi."; +"spaces_creation_email_invites_title" = "Invita la tua squadra"; +"spaces_creation_new_rooms_support" = "Supporto"; +"spaces_creation_new_rooms_random" = "Casuale"; +"spaces_creation_new_rooms_general" = "Generale"; +"spaces_creation_new_rooms_room_name_title" = "Nome stanza"; +"spaces_creation_new_rooms_message" = "Creeremo una stanza per ognuna di esse."; +"spaces_creation_new_rooms_title" = "Quali sono alcune delle discussioni che porterai?"; +"spaces_creation_cancel_message" = "I progressi verranno persi."; +"spaces_creation_cancel_title" = "Interrompere la creazione dello spazio?"; +"spaces_creation_private_space_title" = "Il tuo spazio privato"; +"spaces_creation_public_space_title" = "Il tuo spazio pubblico"; +"spaces_creation_address_already_exists" = "%@\nesiste già"; +"spaces_creation_address_invalid_characters" = "%@\nha caratteri non validi"; +"spaces_creation_address_default_message" = "Il tuo spazio sarà visibile su\n%@"; +"spaces_creation_empty_room_name_error" = "Nome necessario"; +"spaces_creation_address" = "Indirizzo"; +"spaces_creation_settings_message" = "Aggiungi qualche dettaglio per farlo risaltare. Puoi cambiarli in qualsiasi momento."; +"spaces_creation_footer" = "Puoi cambiarlo in seguito"; +"spaces_creation_visibility_message" = "Per entrare in uno spazio esistente, ti serve un invito."; +"spaces_creation_visibility_title" = "Che tipo di spazio vuoi creare?"; + +// Mark: - Space Creation + +"spaces_creation_hint" = "Gli spazi sono un nuovo modo di raggruppare stanze e persone."; +"space_settings_current_address_message" = "Il tuo spazio è visibile su\n%@"; +"space_settings_update_failed_message" = "Aggiornamento impostazioni spazio fallito. Vuoi riprovare?"; +"space_settings_access_section" = "Chi può accedere a questo spazio?"; +"space_topic" = "Descrizione"; +"space_public_join_rule_detail" = "Aperto a chiunque, adatto per comunità"; +"spaces_add_space" = "Aggiungi spazio"; +"spaces_add_room" = "Aggiungi stanza"; +"spaces_invite_people" = "Invita persone"; +"space_private_join_rule_detail" = "Solo a invito, adatto per te stesso o squadre"; +"spaces_explore_rooms_one_room" = "1 stanza"; +"spaces_explore_rooms_room_number" = "%@ stanze"; +"spaces_create_space_title" = "Crea uno spazio"; +"spaces_add_space_title" = "Crea spazio"; +"space_invite_not_enough_permission" = "Non hai l'autorizzazione di invitare persone in questo spazio"; +"room_invite_not_enough_permission" = "Non hai l'autorizzazione di invitare persone in questa stanza"; +"room_invite_to_room_option_detail" = "Non faranno parte di %@."; +"room_invite_to_room_option_title" = "Solo in questa stanza"; +"room_invite_to_space_option_detail" = "Possono esplorare %@, ma non saranno membri di %@."; + +// Mark: - Room invite + +"room_invite_to_space_option_title" = "In %@"; +"share_invite_link_space_text" = "Ehi, unisciti a questo spazio su %@"; +"share_invite_link_room_text" = "Ehi, unisciti a questa stanza su %@"; + +// MARK: - Share invite link + +"share_invite_link_action" = "Condividi collegamento di invito"; +"create_room_processing" = "Creazione stanza"; +"create_room_suggest_room_footer" = "Le stanze consigliate vengono sponsorizzate ai membri dello spazio come buone."; +"create_room_suggest_room" = "Consiglia ai membri dello spazio"; +"create_room_show_in_directory_footer" = "Aiuterà le persone a trovarla ed entrare."; +"create_room_promotion_header" = "SPONSORIZZAZIONE"; +"room_suggestion_settings_screen_message" = "Le stanze consigliate vengono sponsorizzate ai membri dello spazio come buone."; +"create_room_section_footer_type_public" = "Solo le persone invitate possono trovarla ed entrare, non le persone nello spazio."; +"create_room_section_footer_type_restricted" = "Chiunque nello spazio può trovarla ed entrare."; +"create_room_section_footer_type_private" = "Solo le persone invitate possono trovarla ed entrare."; +"create_room_type_restricted" = "Membri dello spazio"; +"call_jitsi_unable_to_start" = "Impossibile iniziare la teleconferenza"; +"room_suggestion_settings_screen_title" = "Segna una stanza come consigliata in uno spazio"; + +// Room suggestion Settings +"room_suggestion_settings_screen_nav_title" = "Consiglia stanza"; +"room_access_space_chooser_other_spaces_section_info" = "Queste probabilmente sono cose di cui altri admin di %@ fanno parte."; +"room_access_space_chooser_other_spaces_section" = "Altri spazi o stanze"; +"room_access_space_chooser_known_spaces_section" = "Spazi conosciuti che contengono %@"; +"room_access_settings_screen_setting_room_access" = "Impostando accesso stanza"; +"room_access_settings_screen_upgrade_alert_upgrading" = "Aggiornamento stanza"; +"room_access_settings_screen_upgrade_alert_upgrade_button" = "Aggiorna"; +"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "Invita automaticamente i membri nella stanza nuova"; +"room_access_settings_screen_upgrade_alert_title" = "Aggiorna stanza"; +"room_access_settings_screen_public_message" = "Chiunque può trovare ed entrare."; +"room_access_settings_screen_edit_spaces" = "Modifica spazi"; +"room_access_settings_screen_upgrade_required" = "Aggiornamento necessario"; +"room_access_settings_screen_restricted_message" = "Permetti a chiunque in uno spazio di trovare ed entrare.\nTi verrà chiesto di confermare quali spazi."; +"room_access_settings_screen_private_message" = "Solo gli invitati possono trovare ed entrare."; +"room_access_settings_screen_message" = "Decidi chi può trovare ed entrare in %@."; +"room_access_settings_screen_title" = "Chi può accedere a questa stanza?"; + +// Room Access Settings +"room_access_settings_screen_nav_title" = "Accesso alla stanza"; +"room_details_promote_room_suggest_title" = "Consiglia ai membri dello spazio"; +"room_details_promote_room_title" = "Promuovi stanza"; +"room_details_access_row_title" = "Accesso"; +"settings_labs_enable_auto_report_decryption_errors" = "Auto-segnala errori di decifrazione"; +"room_preview_decline_invitation_options" = "Vuoi rifiutare l'invito o ignorare l'utente?"; +"threads_beta_cancel" = "Non ora"; +"threads_beta_enable" = "Provale"; +"threads_beta_information_link" = "Maggiori informazioni"; +"threads_beta_information" = "Tieni le discussioni organizzate in conversazioni.\n\nLe conversazioni aiutano a tenere le discussioni in tema e rintracciabili. "; +"threads_beta_title" = "Conversazioni"; +"threads_notice_done" = "Capito"; +"threads_notice_information" = "Tutte le conversazioni create durante il periodo sperimentale verranno visualizzate come normali risposte.

Sarà una transizione una-tantum, dato che le conversazioni ora fanno parte delle specifiche Matrix."; +"threads_notice_title" = "I messaggi in conversazioni non sono più sperimentali! 🎉"; +"room_participants_invite_prompt_to_msg" = "Vuoi davvero invitare %@ in %@?"; +"onboarding_celebration_button" = "Andiamo"; +"onboarding_celebration_message" = "Le tue preferenze sono state salvate."; +"onboarding_celebration_title" = "Tutto pronto!"; +"onboarding_avatar_accessibility_label" = "Immagine del profilo"; +"onboarding_avatar_message" = "Puoi cambiarla in qualsiasi momento."; +"onboarding_avatar_title" = "Aggiungi un'immagine del profilo"; +"onboarding_display_name_max_length" = "Il nome da mostrare deve avere meno di 256 caratteri"; +"onboarding_display_name_hint" = "Puoi cambiarlo in seguito"; +"onboarding_display_name_placeholder" = "Nome da mostrare"; +"onboarding_display_name_message" = "Verrà mostrato quando invii messaggi."; +"onboarding_display_name_title" = "Scegli un nome da mostrare"; +"onboarding_personalization_skip" = "Salta questo passo"; +"onboarding_personalization_save" = "Salva e continua"; +"onboarding_congratulations_home_button" = "Portami a casa"; +"onboarding_congratulations_personalize_button" = "Personalizza profilo"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "Il tuo account %@ è stato creato."; +"onboarding_congratulations_title" = "Congratulazioni!"; +"saving" = "Salvando"; + +// Activities +"loading" = "Caricamento"; +"edit" = "Modifica"; +"suggest" = "Consiglia"; +"add" = "Aggiungi"; +"existing" = "Esistente"; +"new_word" = "Nuovo"; +"stop" = "Ferma"; +"joining" = "Ingresso"; +"location_sharing_live_list_item_stop_sharing_action" = "Non condividere più"; +"location_sharing_live_list_item_current_user_display_name" = "Tu"; +"location_sharing_live_list_item_last_update_invalid" = "Ultimo aggiornamento sconosciuto"; +"location_sharing_live_list_item_last_update" = "Aggiornato %@ fa"; +"location_sharing_live_list_item_sharing_expired" = "Condivisione scaduta"; +"location_sharing_live_list_item_time_left" = "%@ è uscito"; +"location_sharing_live_viewer_title" = "Posizione"; +"location_sharing_live_map_callout_title" = "Condividi posizione"; +"bug_report_logs_description" = "Per diagnosticare i problemi, i registri di questo client verranno inviati con questo rapporto di errore. Se preferisci inviare solo il testo soprastante, deseleziona:"; +"room_access_settings_screen_upgrade_alert_note" = "Nota che aggiornare creerà una nuova versione della stanza. Tutti i messaggi attuali resteranno in questa stanza archiviata."; +"room_access_settings_screen_upgrade_alert_message_no_param" = "Chiunque in uno spazio superiore potrà trovare ed entrare in questa stanza - non serve invitare a mano tutti. Potrai cambiare questa cosa nelle impostazioni della stanza in qualsiasi momento."; +"room_access_settings_screen_upgrade_alert_message" = "Chiunque in %@ potrà trovare ed entrare in questa stanza - non serve invitare a mano tutti. Potrai cambiare questa cosa nelle impostazioni della stanza in qualsiasi momento."; +"settings_presence_offline_mode_description" = "Se attiva, apparirai sempre offline agli altri utenti, anche quando usi l'applicazione."; +"settings_presence_offline_mode" = "Modalità offline"; +"settings_presence" = "Presenza"; +"threads_discourage_information_2" = "\n\nVuoi comunque attivare le conversazioni?"; +"threads_discourage_information_1" = "Il tuo homeserver attualmente non supporta le conversazioni, perciò questa funzione sarà inaffidabile. Alcuni messaggi in conversazioni potrebbero non essere disponibili. "; +"contacts_address_book_permission_required" = "Autorizzazione necessaria per accedere alla rubrica locale"; diff --git a/Riot/Assets/ja.lproj/Localizable.strings b/Riot/Assets/ja.lproj/Localizable.strings index d28bb5e0c..9139c26eb 100644 --- a/Riot/Assets/ja.lproj/Localizable.strings +++ b/Riot/Assets/ja.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ さんが写真を送信 %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ さんが写真を投稿 %@ in %@"; /* Multiple unread messages in a room */ diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index 10c27e967..8b40a13d7 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -135,8 +135,6 @@ "directory_cell_title" = "ルーム一覧を見る"; "directory_cell_description" = "%tu ルーム"; "directory_search_results_title" = "ルーム一覧検索結果"; -"directory_search_results" = "%@の検索結果%tu件"; -"directory_search_results_more_than" = ">%@の検索結果%tu件"; "directory_searching_title" = "ルーム一覧を検索しています…"; "directory_search_fail" = "一覧を取得できませんでした"; // Contacts @@ -144,7 +142,6 @@ "contacts_address_book_matrix_users_toggle" = "Matrix利用者のみ"; "contacts_address_book_no_contact" = "端末内電話帳に連絡先がありません"; "contacts_address_book_permission_required" = "端末内電話帳へのアクセス権限が必要です"; -"contacts_address_book_permission_denied" = "端末の電話帳をElementアプリが読み取ることは許可されていません"; "contacts_user_directory_section" = "利用者一覧"; "contacts_user_directory_offline_section" = "利用者一覧(オフライン)"; // Chat participants @@ -159,7 +156,7 @@ "room_participants_remove_third_party_invite_msg" = "サードパーティの招待を削除することは、APIが存在するまでサポートされていません"; "room_participants_invite_prompt_title" = "確認"; "room_participants_invite_prompt_msg" = "%@をチャットに招待してよろしいですか?"; -"room_participants_filter_room_members" = "ルームメンバーを現在"; +"room_participants_filter_room_members" = "ルームメンバーを検索"; "room_participants_invite_another_user" = "ユーザーID、名前、電子メールで検索、招待"; "room_participants_invite_malformed_id_title" = "招待エラー"; "room_participants_invite_malformed_id" = "不正なIDです。メールアドレスを用いるか、'@localpart:domain'のようなMatrix IDを使用してください"; @@ -289,7 +286,6 @@ "settings_show_decrypted_content" = "復号化された文章を表示"; "settings_global_settings_info" = "あなたの%@ webクライアント上で、全体の通知設定が可能です"; "settings_pin_rooms_with_missed_notif" = "逃した通知があるルームを固定"; -"settings_callkit_info" = "画面がロックされているときに着信がありました。Elementの着信はシステムの通話履歴で確認できます。iCloudが有効になっている場合、この通話履歴はAppleと共有されます。"; "settings_ui_language" = "言語"; "settings_ui_theme" = "外観"; "settings_ui_theme_auto" = "自動"; @@ -414,10 +410,8 @@ "camera_access_not_granted" = "%@はカメラを使用する権限を持っていません。個人情報保護設定の変更をお願いします"; "large_badge_value_k_format" = "%.1fK"; // room display name -"room_displayname_invite_from" = "%@ から招待されました"; "room_displayname_room_invite" = "招待"; "room_displayname_two_members" = "%@ と %@"; -"room_displayname_more_than_two_members" = "%@ と %u 他"; "room_displayname_no_title" = "だれもいない部屋"; // Call "call_incoming_voice_prompt" = "%@ さんから通話の着信中"; @@ -430,9 +424,7 @@ "no_voip_title" = "通話着信中"; "no_voip" = "%@さんから通話の着信がありましたが、%@は通話をまだサポートしていません。\nこの通知を無視して、別の端末から着信に応答することも、拒否することもできます。"; // Crash report -"google_analytics_use_prompt" = "匿名の誤動作報告と使用状況データを自動的に報告して%@の改善に役立てますか?"; // Crypto -"e2e_enabling_on_app_update" = "Elementはエンドツーエンド暗号化をサポートするようになりましたが、有効にするには再びログインする必要があります。\n\nアプリの設定から再ログインできます。今すぐ、または後からでも構いません。"; "e2e_need_log_in_again" = "再度ログインして、このセッションのエンドツーエンド暗号鍵を生成し、公開鍵をホームサーバーに送信する必要があります。\nご迷惑をおかけしますが、ご了承ください。"; // Bug report "bug_report_title" = "バグレポート"; @@ -516,13 +508,13 @@ "group_participants_remove_prompt_msg" = "このグループから%@を削除してよろしいですか?"; "group_participants_invite_prompt_title" = "確認"; "group_participants_invite_prompt_msg" = "このグループに%@を招待してよろしいですか?"; -"group_participants_filter_members" = "コミュニティーのメンバーを絞り込む"; +"group_participants_filter_members" = "コミュニティーメンバーを検索"; "group_participants_invite_another_user" = "ユーザーIDまたは名前による検索/招待"; "group_participants_invite_malformed_id_title" = "招待エラー"; "group_participants_invite_malformed_id" = "不正なID。'@localpart:domain'のようなMatrix IDでなければなりません"; "group_participants_invited_section" = "招待中"; // Group rooms -"group_rooms_filter_rooms" = "コミュニティールームをフィルタリング"; +"group_rooms_filter_rooms" = "コミュニティールームを絞り込む"; "event_formatter_rerequest_keys_part1_link" = "暗号鍵を再要求"; "event_formatter_rerequest_keys_part2" = " あなたの他のセッションに。"; "homeserver_connection_lost" = "ホームサーバーに接続できませんでした。"; @@ -599,7 +591,7 @@ "room_intro_cell_add_participants_action" = "参加者を追加"; "room_participants_security_information_room_encrypted" = "このルームのメッセージはエンドツーエンド暗号化されています。\n\nメッセージは安全に保護されており、メッセージのロックを解除するための固有の鍵は、あなたと受信者だけが持っています。"; "room_participants_security_information_room_not_encrypted" = "このルームのメッセージはエンドツーエンド暗号化されていません。"; -"room_intro_cell_information_dm_sentence1_part3" = ". "; +"room_intro_cell_information_dm_sentence1_part3" = "とのダイレクトメッセージの始まりです。 "; "callbar_active_and_single_paused" = "1つのアクティブな通話(%@)· 1つの一時停止された通話"; // Call Bar @@ -746,7 +738,7 @@ "room_participants_action_security_status_verified" = "認証済"; "room_participants_action_section_security" = "セキュリティー"; "room_participants_start_new_chat_error_using_user_email_without_identity_server" = "IDサーバーが設定されていないため、メールアドレスを使って連絡先とチャットを開始することができません。"; -"room_participants_filter_room_members_for_dm" = "メンバーを絞り込む"; +"room_participants_filter_room_members_for_dm" = "メンバーを検索"; "room_participants_remove_third_party_invite_prompt_msg" = "招待を取り消してよろしいですか?"; "room_participants_leave_prompt_msg_for_dm" = "退出してよろしいですか?"; "room_participants_leave_prompt_title_for_dm" = "退出"; @@ -812,7 +804,6 @@ // Manage session "manage_session_title" = "セッションを管理"; "security_settings_user_password_description" = "アカウントのパスワードを入力して本人確認を行ってください"; -"security_settings_coming_soon" = "申し訳ありません。このアクションはElement iOSではまだ利用できません。他のMatrixクライアントを使って設定してください。将来的にはElement iOSでも実装される予定です。"; "security_settings_complete_security_alert_message" = "現在のセッションのセキュリティーを完了させる必要があります。"; "security_settings_blacklist_unverified_devices_description" = "全てのセッションを認証して、信頼できるものとしてマークしメッセージを送信します。"; "security_settings_blacklist_unverified_devices" = "信頼していないセッションにはメッセージを送信しない"; @@ -872,7 +863,6 @@ "settings_key_backup_info_signout_warning" = "鍵を失くさないよう、サインアウトする前にバックアップしてください。"; "settings_key_backup_info" = "暗号化されたメッセージは、エンドツーエンドの暗号化によって保護されています。これらの暗号化されたメッセージを読むための鍵を持っているのは、あなたと受信者だけです。"; "settings_labs_message_reaction" = "絵文字でメッセージに反応"; -"settings_calls_stun_server_fallback_description" = "ホームサーバーがフォールバックコールアシストサーバーを提供していない場合は%@を許可します(IPアドレスは通話中に共有されます)。"; "settings_security" = "セキュリティー"; "settings_three_pids_management_information_part3" = ""; "settings_three_pids_management_information_part2" = "ディスカバリー"; @@ -1239,7 +1229,6 @@ "notice_room_aliases" = "ルームエイリアス: %@"; "notice_room_related_groups" = "このルームに関連付けられたグループ: %@"; "notice_encrypted_message" = "暗号化されたメッセージ"; -"notice_encryption_enabled" = "%@はエンドツーエンド暗号化を有効にする (アルゴリズム %@)"; "notice_image_attachment" = "画像添付"; "notice_audio_attachment" = "音声添付"; "notice_video_attachment" = "動画添付"; @@ -1543,13 +1532,12 @@ "notice_profile_change_redacted_by_you" = "プロフィール %@を更新しました"; "notice_room_created_by_you" = "ルームを作成しました"; "notice_encryption_enabled_ok_by_you" = "あなたはエンドツーエンド暗号化をオンにしました。"; -"notice_encryption_enabled_unknown_algorithm_by_you" = "あなたはエンドツーエンド暗号化をオンにしました (不明なアルゴリズム %2$@)。"; "notice_redaction_by_you" = "イベントを編集しました (id: %@)"; "resume_call" = "再開"; "notice_room_history_visible_to_members_from_joined_point_for_dm" = "%@が今後のメッセージを「全員 (参加した時点以降)」閲覧可能に設定しました。"; "notice_room_history_visible_to_members_from_invited_point_for_dm" = "%@が今後のメッセージを「メンバーのみ (招待された時点以降)」閲覧可能に設定しました。"; "notice_room_history_visible_to_members_for_dm" = "%@が今後のメッセージを「メンバーのみ」閲覧可能に設定しました。"; -"room_intro_cell_information_room_without_topic_sentence2_part2" = " と、このルームの目的が分かりやすくなります。"; +"room_intro_cell_information_room_without_topic_sentence2_part2" = " すると、このルームの目的が分かりやすくなります。"; "room_intro_cell_information_room_without_topic_sentence2_part1" = "トピックを追加"; "security_settings_secure_backup_restore" = "バックアップから復元"; "settings_device_notifications" = "端末の通知"; @@ -1614,3 +1602,20 @@ "settings_confirm_media_size_description" = "この機能をオンにすると、画像や動画をどのサイズで送信するか確認する画面が表示されます。"; "settings_contacts_enable_sync_description" = "IDサーバーを使用して連絡先を探すと同時に、連絡先があなたを探せるようにします。"; "home_syncing" = "同期中"; +"search_filter_placeholder" = "絞り込む"; + +// MARK: - Share invite link + +"share_invite_link_action" = "招待リンクを共有"; +"room_intro_cell_information_room_with_topic_sentence2" = "トピック: %@"; +"room_intro_cell_information_room_sentence1_part3" = "の始まりです。 "; +"room_intro_cell_information_room_sentence1_part1" = "ここが "; +"room_intro_cell_information_dm_sentence1_part1" = "ここが "; +"settings_labs_enable_auto_report_decryption_errors" = "復号エラーを自動で報告"; +"spaces_create_space_title" = "スペースを作成"; +"spaces_add_space_title" = "スペースを作成"; +"spaces_creation_address" = "アドレス"; +"spaces_creation_visibility_message" = "既存のスペースに参加するには、招待が必要です。"; +"spaces_creation_footer" = "この設定は後から変更できます"; +"onboarding_display_name_hint" = "この設定は後から変更できます"; +"spaces_creation_visibility_title" = "作成するスペースの種類を選択してください"; diff --git a/Riot/Assets/kab.lproj/Localizable.strings b/Riot/Assets/kab.lproj/Localizable.strings index 6e07f4d0a..4100d9f4e 100644 --- a/Riot/Assets/kab.lproj/Localizable.strings +++ b/Riot/Assets/kab.lproj/Localizable.strings @@ -87,7 +87,6 @@ /** Image Messages **/ /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ yuzen tugna %@"; /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: *%@ %@"; diff --git a/Riot/Assets/kab.lproj/Vector.strings b/Riot/Assets/kab.lproj/Vector.strings index 57bba7107..5df673356 100644 --- a/Riot/Assets/kab.lproj/Vector.strings +++ b/Riot/Assets/kab.lproj/Vector.strings @@ -38,7 +38,6 @@ // Intro "secure_key_backup_setup_intro_title" = "Aḥraz aɣelsan"; -"rerequest_keys_alert_message" = "Ma ulac aɣilif err Element deg yibenk-nniḍen i izemren ad yekkes awgelhen i yiznan, akken ad yizmir ad yazen tisura ɣer tɣimit-a."; "deactivate_account_password_alert_title" = "Sens amiḍan"; "deactivate_account_validate_action" = "Sens amiḍan"; "deactivate_account_forget_messages_information_part2_emphasize" = "Asmigel"; @@ -829,7 +828,6 @@ // MARK: - Major update -"major_update_title" = "Riot tura d Element"; "secrets_reset_authentication_message" = "Sekcem awal uffir n umiḍan-ik·im i usentem"; "secrets_reset_warning_title" = "Ma yella twennzeḍ kullec"; @@ -915,7 +913,6 @@ "deactivate_account_forget_messages_information_part3" = ":aya ad yerr iseqdacen ara d-yernun ad walin taskant n yidiwenniyen ur nemmid ara)"; "deactivate_account_forget_messages_information_part1" = "Ttxil-k·m ttu akk iznan i yuzneɣ mi akken senseɣ amiḍan-iw ("; "deactivate_account_informations_part1" = "Aya ad yerr amiḍan-ik·im ad yuɣal ur yettwaseqdac ara i lebda. Ur tettizmireḍ ara ad tkecmeḍ, daɣen ulac win ara yizmiren ad yales asekles s usulay-a n useqdac. Ad yerr amiḍan-ik·im ad yettwakkes seg meṛṛa tixxamin ideg tettekkaḍ, rnu ad yekkes akk talqayt seg uqeddac-ik·im n timagit. "; -"service_terms_modal_message" = "I wakken ad tkemmleḍ tesriḍ ad tqebleḍ tiwtilin n umeẓlu-a (%@)."; // Service terms "service_terms_modal_title" = "Tiwtilin n umeẓlu"; @@ -1047,7 +1044,6 @@ "settings_key_backup_info_trust_signature_unknown" = "Aḥraz ɣer-s azmul seg tɣimit s usulay: %@"; "settings_key_backup_info_progress" = "Aḥraz n tsura %@…"; "settings_key_backup_info_valid" = "Tiɣimit-a tḥerrez tisura-inek·inem."; -"settings_key_backup_info_algorithm" = "Alguritm: ù@"; "settings_key_backup_info_version" = "Lqem n uḥraz n tsarut: %@"; "settings_crypto_export" = "Sifeḍ tisura"; "settings_crypto_device_key" = "\nTasarut n tɣimit:\n"; @@ -1132,8 +1128,6 @@ "room_conference_call_no_power" = "Tesriḍ tisirag i wakken ad tesferkeḍ asarag s usiwel deg texxamt-a"; "room_ongoing_conference_call_with_close" = "Asarag s usiwel iteddu. Rnu d %@ neɣ %@. %@ ."; "room_ongoing_conference_call" = "Asarag s usiwel iteddu. Rnu d %@ neɣ %@."; -"room_unsent_messages_unknown_devices_notification" = "Ur yettwazen ara yizen acku llant tɣimiyin ur nettwassen ara. %@ neɣ %@ tura?"; -"room_unsent_messages_notification" = "Iznan ur ttwaznen ara. %@ neɣ %@ tura?"; "room_message_unable_open_link_error_message" = "Yegguma ad yeldi useɣwen."; "room_one_user_is_typing" = "%@ yettaru…"; "room_accessiblity_scroll_to_bottom" = "Senkez d akessar"; @@ -1209,7 +1203,6 @@ "active_call_details" = "Asiwel urmid (%@)"; "active_call" = "Rmed asiwel"; "store_full_description" = "Element d anaw amaynut n usnas n tirawt d umɛiwen i:\n\n1. Ad ak·akem-yeǧǧ ad tsedduḍ aḍman n tudert-ik·im tabaḍnit\n2. Ad ak·akem-yeǧǧ ad teqqneḍ d yal win·tin yellan deg uzeṭṭa n Matrix, ula beṛṛa-ines s usleɣ n yisnasen am Slack\n3. Ad ak·akem-iḥrez seg udellel, seg yilɣa, seg tewwura yeffren d telmatin ifergen\n4. Ad tettuɣellseḍ s uwgelhen seg yixef ɣer yixef, s uzmul anmidag swayes ara tesneqdeḍ wiyaḍ\n\nElement yemgarad akk ɣef yisnasen-nniḍen n tirawt d umɛiwen yerna n uɣbalu yeldin.\n\nElement ad ak·akem-yeǧǧ ad tzedɣeḍ s wudem awurman - neɣ ad tferneḍ asenneftaɣ - i wakken ad tesɛuḍ tabaḍnit, ad tesɛuḍ ayla, ad tsedduḍ yisefka-inek·inem d yidewenniyen akken i tebɣiḍ. Ad ak·am-imudd anekcum ɣer uzeṭṭa yeldin; akken mačči kan d ameslay ara temmeslayeḍ d yiseqdacen-nniḍen n Element. Rnu d aɣellsan aṭas.\n\nElement yezmer ad yeg akk aya acku iteddu ɣef Matrix - alugen i teywalt yeldin.\n\nElement ad ak·am-imudd asenqed, ad ak·akem-yeǧǧ ad tferneḍ anwa ara yeɣren idiwenniyen-inek·inem. Seg usnas n Element, tzemreḍ ad tferneḍ iwumi ara tmuddeḍ tanezduɣt s yiberdan yemgaraden:\n\n1. Awi amiḍan baṭel ɣef uqeddac azayaz n matrix.org\n2. Sezdeɣ s timmad-ik·im amiḍan-inek·inem s uselkem n uqeddac ɣef yibenk-inek·inem\n3. Rnu amiḍan ɣef uqeddac udmawan s umulteɣ ɣer tɣerɣert n tnezduɣt n Element Matrix Services\n\nI wacu ara tferneḍ Element?\n\nILI-IK·IKEM D BAB N YISEFKA-INEK·INEM: D kečč·kemm ara ifernen adeg anida ara terreḍ isefka-inek·inem d yiznan-inek·inem. D ayla-inek·inem, ad t-tsedduḍ akken i tebɣiḍ. Mačči d kra n MEGACORP ara d-isuffɣen isefka-inek·inem neɣ ad imudd anekcum i wis kraḍ.\n\nTIRAWT YELDIN D UMƐIWEN: Tzemreḍ ad temmeslayeḍ d umdan-nniḍen akk n uzeṭṭa Matrix, isseqdac Element neɣ asnas-nniḍen n Matrix, ɣas ulamma isseqdac anagraw-nniḍen yemgarraden ɣef Slack, IRC neɣ XMPP.\n\nD AƔELLSAN ALMI DAYEN: d awgelhen n tidet seg yixef ɣer yixef (ala imttekkiyen deg udiwenni i izemren ad kksen awgelhen i yiznan) d uzmul anmidag i usenqed n yibenkan n yimttekkiyen deg udiwenni.\n\nTAYWALT YEMMDEN: Tirawt, isawalen s taɣect d tvidyut, beṭṭu n yifuyla, beṭṭu n ugdil d tegrumma yemmden n yimsidaf, n yibuten d yiwiǧiten. Rnu tixxamin, timɣiwnin, qqim mmeslay rnu seddu lecɣl.\n\nANIDA YEƔU TILIḌ: Ili-k·ikem d wid tettmeslayeḍ anida yebɣu tiliḍ s uzray n yiznan yemtawan akken iwata ɣef meṛṛa ibenkan-inek·inem akked ɣef web deg https://element.io/app."; -"settings_callkit_info" = "Nermes-d isawalen i d-ikecmen ɣef ugdil-inek·inem isekkṛen. Wali isawalen-inek·inem n Element deg uzray n yisawalen n unagraw. Ma yella iCloud yetturmed, azray-a n yisawalen ad yettwabḍu akked Apple."; "store_promotional_text" = "Nermes-d isawalen i d-ikecmen ɣef ugdil-inek·inem isekkṛen. Wali isawalen-inek·inem n Element deg uzray n yisawalen n unagraw. Ma yella iCloud yetturmed, azray-a n yisawalen ad yettwabḍu akked Apple."; "major_update_information" = "S tumert ara awen-d-nini nbeddel isem! Asnas-inek·inem yettwaleqqem, aql-ak·akem teqqneḍ ɣer umiḍan-inek·inem."; "bug_report_logs_description" = "I wakken ad nessiweḍ ad d-naf uguren, iɣmisen n umsaɣ-a ad ttwaznen s uneqqis-a n wabug. Ma yella tebɣiḍ ad tazneḍ kan aḍris yellan nnig, ttxil-k·m ṛcem tabewwaḍt:"; @@ -1237,7 +1230,6 @@ "pin_protection_not_allowed_pin" = "I ssebbat n tɣellist, tangalt-a PIN ulac-itt. Ttxil-k·m ɛreḍ tangalt-nniḍen n PIN"; "user_verification_sessions_list_information" = "Iznan akked useqdac-a deg texxamt-a ttwawgelhen seg yixef ɣer yixef, ur zmiren ara ad ttwaɣren sɣur wis kraḍ."; "key_verification_verified_other_session_information" = "Tzemreḍ tura ad teɣreḍ iznan iɣellsanen deg tɣimit-inek·inem-nniḍen, ula d iseqdacen-nniḍen ad ẓren belli zemren ad tteklen fell-as."; -"device_verification_self_verify_wait_information" = "Senqed tiɣimit-a seg yiwet gar tɣimiyin-inek·inem-nniḍen, serreḥ-as ad tekcem ɣer yiznan yettwawgelhen.\n\nSeqdec Element aneggaru ɣef yibenkan-inek·inem-nniḍen:"; "key_backup_setup_passphrase_info" = "Ad nekles anɣal yettwawgelhen n tsura-inek·inem ɣef uqeddac-nneɣ. Mmesten aḥraz-inek·inem s tefyirt tuffirt i wakken ad yeqqim d aɣellsan.\n\nI wugar n tɣellistt, ilaq ad yemgarad ɣef wawal uffir n umiḍan-ik·im."; "share_extension_failed_to_encrypt" = "Tuzna ur teddi ara. Senqed deg usnas agejdan iɣewwaren n uwgelhen n texxamt-a"; @@ -1248,10 +1240,8 @@ "e2e_key_backup_wrong_version" = "Aḥraz amaynut n tsarut n yiznan iɣellsanen yettwaf-d.\n\nWagi mačči d kečč·kemm, sbadu tafyirt tuffirt tamaynut deg yiɣewwaren."; // Crypto -"e2e_enabling_on_app_update" = "Element tura issefrak awgelhen seg yixef ɣer yixef maca tesriḍ ad tkecmeḍ i tikkelt-nniḍen i wakken ad t-tremdeḍ.\n\nTzemreḍ ad tgeḍ aya tura neɣ ticki deg yiɣewwaren n usnas."; // Crash report -"google_analytics_use_prompt" = "Tebɣiḍ ad talleḍ i usnerni n %@ s tuzna s wudem awurman n yineqqisen udrigen n truẓi d yisefka n useqdec?"; "room_details_addresses_disable_main_address_prompt_msg" = "Ulac ɣur-k·m tansa n yimayl tagejdant i yettwafernen. Tansa n yimayl tamezwert i texxamt-a ad tettwafran kan akka"; "identity_server_settings_alert_disconnect_still_sharing_3pid" = "Tebdiḍ tbeṭṭuḍ isefka-inek·inem udmawanen ɣef uqeddac n timagit %@.\n\nAd ak·akem-nweṣṣi ad tekkseḍ tansiwin-inek·inem n yimayl d wuṭṭunen n tiliɣri seg uqeddac n timagit send ad teffɣeḍ seg tuqqna."; "identity_server_settings_description" = "Aql-ak·akem akka tura tesseqdaceḍ %@ i wakken ad d-tafeḍ, dɣen ad tettwafeḍ sɣur inermisen i yella i tessneḍ."; @@ -1287,12 +1277,10 @@ "key_verification_bootstrap_not_setup_message" = "Tesriḍ ad tgeḍ tazwart s uzmul anmidag qbel."; "key_verification_verified_this_session_information" = "Tzemreḍ tura ad teɣreḍ iznan iɣellsanen ɣef yibenk-a, ula d iseqdacen-nniḍen ad ẓren belli zemren ad tteklen fell-as."; "key_verification_verified_new_session_information" = "Tzemreḍ tura ad teɣreḍ iznan iɣellsanen ɣef yibenk-inek·inem amaynut, ula d iseqdacen-nniḍen ad ẓren belli zemren ad tteklen fell-as."; -"device_verification_self_verify_wait_additional_information" = "Aya iteddu akked Element d yimsaɣen-nniḍen n Matrix yemṣadan d uzmul anmidag."; // Success from recovery key "key_backup_setup_success_from_recovery_key_info" = "Tisura-inek·inem ad ttwaḥerzent.\n\nEg tanɣalt n tsarut-a n tririt, rnu err-itt deg wadeg yettwaḍmanen."; "secure_key_backup_setup_existing_backup_error_info" = "Kkes-as asekkeṛ i wakken ad talseḍ aseqdec-ines deg uḥraz aɣellsan neɣ kkes-it i tmerna n uḥraz n yiznan imaynuten deg uḥraz aɣellsan."; -"service_terms_modal_message_identity_server" = "Qbel tiwital n uqeddac n timagit (%@) i usnirem n yinermisen."; "no_voip" = "%@ yessawal-ak·am-d maca %@ ur isefrak ara isawalen akka tura.\nTzemreḍ ad tzegleḍ alɣu-a, ad terreḍ ɣef usiwel seg yibenk-nniḍen neɣ tzemreḍ ad t-tagiḍ."; "photo_library_access_not_granted" = "%@ ur yesɛi ara tisirag ad ikcem ɣer temkarḍit n tewlafin, ttxil-k·m beddel-it deg yiɣewwaren n tbaḍnit"; "camera_access_not_granted" = "%@ ur yesɛi ara tisirag ad iseqdec takamiṛat, ttxil-k·m beddel-it deg yiɣewwaren n tbaḍnit"; @@ -1300,7 +1288,6 @@ // AuthenticatedSessionViewControllerFactory "authenticated_session_flow_not_supported" = "Asnas-a ur isefrak ara amtawi n usesteb ɣef uqeddac-inek·inem agejdan."; -"security_settings_coming_soon" = "Nesḥassef. Tigawt-a ulac-itt ɣef Elemnt iOS akka tura. Ttxil-k·m seqdec amsaɣ-nniḍen n Matrix i usbadu-ines. Element iOS ad t-iseqdec."; "security_settings_complete_security_alert_message" = "Ilaq deg tazwara ad temmed tɣellist deg tɣimit-agi-inek·inem tamirant."; "security_settings_blacklist_unverified_devices_description" = "Senqed meṛṛa tiɣimiyin n yiseqdac i wakken ad tcerḍeḍ fell-asent ttwattkalent, azen-asen daɣen iznan."; "security_settings_blacklist_unverified_devices" = "Ɣur-k·m ad tazneḍ akk iznan ɣer tɣimiyin ur nettwattkal ara"; @@ -1418,7 +1405,6 @@ "room_participants_invite_malformed_id_title" = "Tuccḍa deg uncd"; "room_participants_invite_another_user" = "Nadi / snubget s usulay n useqdac, Isem neɣ imayl"; "room_participants_remove_third_party_invite_prompt_msg" = "Tebɣiḍ s tidet ad tesfesxeḍ tinnubga-a?"; -"contacts_address_book_permission_denied" = "Ur teǧǧiḍ ara Element ad yekcem ɣer yinermisen-ik·im idiganen"; "directory_search_fail" = "Alqaḍ n yisefka yecceḍ"; "room_creation_invite_another_user" = "Nadi / snubget s usulay n useqdac, Isem neɣ imayl"; "auth_softlogout_clear_data_message_1" = "Ɣur-k·m: Isefka-inek·inem udmawanen (rnu ɣer-sen tisura n uwgelhen) mazal ttukelsent ɣef yibenk-a."; diff --git a/Riot/Assets/ko.lproj/Localizable.strings b/Riot/Assets/ko.lproj/Localizable.strings index adb741999..f76cb7db0 100644 --- a/Riot/Assets/ko.lproj/Localizable.strings +++ b/Riot/Assets/ko.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@님이 사진을 보냈습니다 %@"; /* A single unread message in a room */ "SINGLE_UNREAD_IN_ROOM" = "%@ 에서 메시지를 받았습니다"; /* A single unread message */ diff --git a/Riot/Assets/ko.lproj/Vector.strings b/Riot/Assets/ko.lproj/Vector.strings index 0c3bfd362..dc0ce0d07 100644 --- a/Riot/Assets/ko.lproj/Vector.strings +++ b/Riot/Assets/ko.lproj/Vector.strings @@ -24,7 +24,7 @@ "camera" = "카메라"; // String for App Store "store_short_description" = "안전한 분산 대화/VoIP"; -"store_full_description" = "여러분의 통제 하에 완전히 유연한 대화 앱. Riot은 여러분이 원하는 방식으로 대화할 수 있도록 합니다. 개방형 분산 커뮤니티의 표준 - [matrix]를 위해 제작됨. \n \n무료 matrix.org 계정을 만들고, https://modular.im에서 자신만의 서버, 혹은 다른 Matrix 서버를 얻으세요. \n \n왜 Riot.im을 선택해야 하나요? \n \n완전한 대화: 원하는 대로 팀이나 친구, 커뮤니티를 중심으로 방을 만드세요! 대화, 파일 공유, 위젯 추가와 음성 및 영상 통화 - 모두 무료입니다. \n \n강력한 통합: 여러분이 알고 사랑하는 도구와 함께 Riot.im을 사용하세요. Riot.im이라면 다른 대화 앱의 사용자와 그룹까지도 대화할 수 있습니다. \n \n개인 및 보안: 대화를 비밀로 유지하세요. 최첨단 종단간 암호화로 비밀 대화를 은밀하게 유지해줍니다. \n \n오픈 소스: Matrix로 만들어진 오픈 소스입니다. 자신의 데이터를 자신의 서버에 소유하거나, 신뢰하는 서버에 맡기세요. \n \n어디에 있든: 모든 기기나 https://riot.im에서 완전히 동기화된 메시지 기록으로 연락을 유지합니다."; +"store_full_description" = "Element는 다음과 같은 기능을 제공하는 새로운 유형의 메신저입니다.\n\n1. 프라이버시를 관리할 수 있는 권한을 제공합니다.\n2. Matrix 네트워크의 사용자와 통신할 수 있을 뿐만 아니라 Slack 등의 \n앱을 연계하여 다른 네트워크와도 통신할 수 있습니다.\n3. 광고, 데이터마이닝, 백도어와 클로즈 플랫폼으로부터의 사용자를 보호합니다.\n4. 교차 검증과 강력한 종단간 암호화을 통해 사용자를 보호합니다.\n\nElement는 탈중앙화이며 오픈 소스이기 때문에 다른 메신저 앱과는 완전히 다릅니다.\n\nElement에서는 데이터 및 대화에 대한 소유권 및 제어 권한을 가질 수 있도록 자체 호스팅을 통해 \n직접 메신저 서버를 운영할 수 있고, 다른 사용자가 제공하고 있는 서버를 선택할 수 있습니다. 또한 개방형 네트워크에 액세스할 수 있으므로 Element 이외의 사용자와도 이야기할 수 있습니다. 그리고 매우 안전합니다.\n\nElement는 개방-분산형 통신의 표준인 Matrix에서 동작하기 때문에 이 모든 것을 수행할 수 있습니다.\n\nElement는 어떤 서버를 사용할지 사용자가 직접 Element 앱에서 결정할 수 있습니다.\n\n1. matrix.org의 공식 서버에서 무료 계정을 생성한다.\n2. 자체 호스팅을 통해 직접 서버를 운영하고 계정을 관리한다.\n3. Element Matrix Services의 호스팅 플랫폼에 가입하여 사용자 커스텀 서버에서 계정을 만든다.\n\n왜 Element를 선택해야합니까?\n\n내 데이터를 내가 소유함: 데이터나 메시지를 보관할 곳을 스스로 정할 수 있습니다. 데이터를 수집하거나 제 3자에게 데이터를 제공하는 거대 IT 기업이 아닌, 사용자가 스스로 데이터를 소유하고 제어할 수 있습니다.\n\n개방적인 메시징 및 협업: Element 또는 다른 Matrix 앱을 사용하든, 심지어 Slack, IRC, XMPP와 같은 다른 메시징 시스템을 사용하던 상관없이 Matrix 네트워크 상의 다른 사용자와 채팅할 수 있습니다.\n\n매우 안전함: 강력한 종단간 암호화(대화에 참여하고 있는 사람만 메시지를 복호화할 수 있습니다) 및 대화 참가자의 장치를 확인하기 위한 교차 검증을 실시할 수 있습니다.\n\n완벽한 커뮤니케이션: 메시징, 음성 및 화상 통화, 파일 공유, 화면 공유 및 다양한 통합, 봇, 위젯 등을 제공합니다. 방이나 커뮤니티를 만들어 서로 연락하고 일을 원활하게 해낼 수 있습니다.\n\n언제 어디서나: 모든 장치 및 웹(https://element.io/app)에서 메시지가 완전히 동기화되므로 언제 어디서나 연락을 취할 수 있습니다."; "join" = "참가"; "decline" = "끊기"; "accept" = "수락"; @@ -92,7 +92,7 @@ "auth_reset_password_next_step_button" = "저는 제 이메일 주소를 확인했습니다"; "auth_reset_password_error_unauthorized" = "이메일 주소 확인에 실패함: 이메일에 있는 링크를 클릭했는 지 확인하세요"; "auth_reset_password_error_not_found" = "이메일 주소가 이 홈서버의 Matrix ID에 연결되어 있지 않습니다."; -"auth_reset_password_success_message" = "비밀번호가 다시 설정되었습니다.\n\n모든 기기에서 로그아웃되며 더 이상 푸시 알림을 받을 수 없습니다. 알림을 다시 켜려면 각 기기마다 다시 로그인하세요."; +"auth_reset_password_success_message" = "비밀번호가 재설정되었습니다.\n\n모든 기기에서 로그아웃되며 더 이상 푸시 알림을 받을 수 없습니다. 알림을 다시 켜려면 각 기기마다 다시 로그인하세요."; "auth_add_email_and_phone_warning" = "API가 있기 전까진 이메일과 전화번호를 동시에 등록하는 것을 지원하지 않습니다. 오직 계정에는 전화번호만 들어갑니다. 설정에서 프로필에 이메일을 추가할 수 있습니다."; "auth_accept_policies" = "이 홈서버의 정책을 검토하고 수락해주세요:"; "auth_autodiscover_invalid_response" = "잘못된 홈서버 검색 응답"; @@ -161,8 +161,6 @@ "directory_cell_title" = "목록 찾기"; "directory_cell_description" = "%tu개의 방"; "directory_search_results_title" = "목록 결과 찾기"; -"directory_search_results" = "%@(으)로 찾은 %tu개의 결과"; -"directory_search_results_more_than" = ">%@(으)로 찾은 %tu개의 결과"; "directory_searching_title" = "목록 검색 중…"; "directory_search_fail" = "데이터 가져오기에 실패함"; // Contacts @@ -233,8 +231,6 @@ "room_offline_notification" = "서버와의 연결이 끊겼습니다."; "room_unsent_messages_notification" = "메시지가 보내지지 않았습니다."; "room_unsent_messages_unknown_devices_notification" = "알 수 없는 기기가 있어 메시지 전송이 실패했습니다."; -"room_ongoing_conference_call" = "회의 전화가 진행 중입니다. %1$s 또는 %2$s로 참가하세요."; -"room_ongoing_conference_call_with_close" = "회의 전화가 진행 중입니다. %1$s 또는 %2$s로 참가하세요. %@."; "room_ongoing_conference_call_close" = "닫기"; "room_conference_call_no_power" = "이 방에서 회의 전화를 관리할 권한이 필요합니다"; "room_prompt_resend" = "모두 다시 보내기"; @@ -352,7 +348,6 @@ "settings_global_settings_info" = "당신의 %@ 웹 클라이언트에 전역 알림 설정이 켜졌습니다"; "settings_pin_rooms_with_missed_notif" = "알림을 놓친 방을 고정"; "settings_pin_rooms_with_unread" = "읽지 않은 메시지가 있는 방을 고정"; -"settings_on_denied_notification" = "%@에 의해 알림이 거부됬습니다, 기기 설정에서 %@을(를) 허용해주세요"; "settings_enable_callkit" = "통합 전화"; "settings_callkit_info" = "잠금 화면에서 수신 전화를 받습니다. 시스템의 통화 내역에서 %@ 통화를 확인하세요. iCloud가 켜져있다면, 이 통화 기록은 Apple과 공유됩니다."; "settings_ui_language" = "언어"; @@ -386,8 +381,8 @@ "settings_old_password" = "이전 비밀번호"; "settings_new_password" = "새 비밀번호"; "settings_confirm_password" = "비밀번호 확인"; -"settings_fail_to_update_password" = "비밀번호 업데이트에 실패함"; -"settings_password_updated" = "비밀번호가 업데이트되었습니다"; +"settings_fail_to_update_password" = "비밀번호 변경에 실패하였습니다"; +"settings_password_updated" = "비밀번호가 변경되었습니다"; "settings_crypto_device_name" = "세션 이름: "; "settings_crypto_device_id" = "\n세션 ID: "; "settings_crypto_device_key" = "\n세션 키:\n"; @@ -558,7 +553,6 @@ "no_voip_title" = "수신 전화"; "no_voip" = "%@님이 당신에게 전화를 걸고 있지만 %@은(는) 아직 전화를 지원하지 않습니다.\n이 알림을 무시하고 다른 기기에서 전화를 받거나 전화를 거절할 수 있습니다."; // Crash report -"google_analytics_use_prompt" = "%@이(가) 개선하도록 자동으로 익명의 충돌 보고서와 사용 데이터를 제공하겠습니까?"; // Crypto "e2e_enabling_on_app_update" = "%@는 이제 종단간 암호화를 지원하지만 암호화를 켜려면 다시 로그인해야 합니다.\n\n지금 다시 로그인하거나 나중에 애플리케이션 설정에서 할 수 있습니다."; "e2e_need_log_in_again" = "이 세션에 종단간 암호화 키를 생성하고 공개 키를 홈서버에 제출하려면 다시 로그인해야 함니다.\n한 번만 하면 됩니다. 불편을 드려 죄송합니다."; @@ -611,7 +605,6 @@ "gdpr_consent_not_given_alert_review_now_action" = "지금 검토"; // Service terms "service_terms_modal_title" = "서비스 약관"; -"service_terms_modal_message" = "계속하려면 이 서비스 약관 (%@)에 동의해야 합니다."; "service_terms_modal_accept_button" = "수락"; "service_terms_modal_description_for_identity_server" = "다른 사용자가 검색할 수 있음"; "service_terms_modal_description_for_integration_manager" = "봇, 브릿지, 위젯과 스티커팩을 사용"; @@ -640,8 +633,8 @@ "key_backup_setup_intro_setup_connect_action_with_existing_backup" = "이 기기를 키 백업에 연결"; "key_backup_setup_intro_manual_export_info" = "(고급)"; "key_backup_setup_intro_manual_export_action" = "수동으로 키 내보내기"; -"key_backup_setup_passphrase_title" = "백업을 암호로 보호하기"; -"key_backup_setup_passphrase_info" = "암호화된 키의 사본을 서버에 보관합니다. 암호로 된 백업을 보호하며 안전하게 유지해줍니다.\n\n보안을 최대화하려면, 암호는 계정 비밀번호와 달라야 합니다."; +"key_backup_setup_passphrase_title" = "백업을 보안 암호로 보호하기"; +"key_backup_setup_passphrase_info" = "암호화된 키의 사본을 서버에 보관합니다. 암호로 된 백업을 보호하며 안전하게 유지해줍니다.\n\n보안을 최대화하려면, 암호는 Matrix 계정의 비밀번호와 달라야 합니다."; "key_backup_setup_passphrase_passphrase_title" = "입력"; "key_backup_setup_passphrase_passphrase_placeholder" = "암호 입력"; "key_backup_setup_passphrase_passphrase_valid" = "좋아요!"; @@ -652,34 +645,34 @@ "key_backup_setup_passphrase_confirm_passphrase_invalid" = "암호가 틀렸습니다"; "key_backup_setup_passphrase_set_passphrase_action" = "암호 설정"; "key_backup_setup_passphrase_setup_recovery_key_info" = "또는, 안전한 곳에 저장해 둘 복구 키로 백업을 보호합니다."; -"key_backup_setup_passphrase_setup_recovery_key_action" = "(고급) 복구 키로 설정"; +"key_backup_setup_passphrase_setup_recovery_key_action" = "(고급) 보안 키로 설정"; "key_backup_setup_success_title" = "성공!"; // Success from passphrase -"key_backup_setup_success_from_passphrase_info" = "키가 백업되었습니다.\n\n복구 키는 안전망입니다 - 이것으로 암호를 잊어버려도 암호화된 메시지에 접근할 수 있습니다.\n\n복구키를 비밀번호 관리자 (혹은 금고)같은 안전한 장소에 두세요."; -"key_backup_setup_success_from_passphrase_save_recovery_key_action" = "복구 키 저장"; +"key_backup_setup_success_from_passphrase_info" = "키가 백업되었습니다.\n\n보안 키는 안전망입니다 - 암호를 잊어버려도 암호화된 메시지에 접근할 수 있습니다.\n\n보안 키를 비밀번호 관리자 (혹은 금고)같은 안전한 장소에 두세요."; +"key_backup_setup_success_from_passphrase_save_recovery_key_action" = "보안 키 저장"; "key_backup_setup_success_from_passphrase_done_action" = "끝"; // Success from recovery key -"key_backup_setup_success_from_recovery_key_info" = "키가 백업되었습니다.\n\n이 복구 키의 사본을 만들어 안전하게 보관하세요."; -"key_backup_setup_success_from_recovery_key_recovery_key_title" = "복구 키"; +"key_backup_setup_success_from_recovery_key_info" = "키가 백업되었습니다.\n\n보안 키의 사본을 만들어 안전하게 보관하세요."; +"key_backup_setup_success_from_recovery_key_recovery_key_title" = "보안 키"; "key_backup_setup_success_from_recovery_key_make_copy_action" = "사본 만들기"; "key_backup_setup_success_from_recovery_key_made_copy_action" = "사본을 만들었습니다"; "key_backup_recover_title" = "보안 메시지"; -"key_backup_recover_invalid_passphrase_title" = "맞지 않는 복구 암호"; -"key_backup_recover_invalid_passphrase" = "이 암호로 백업을 복호화할 수 없습니다: 올바른 복구 암호를 입력해서 확인해주세요."; -"key_backup_recover_invalid_recovery_key_title" = "복구 키가 맞지 않음"; -"key_backup_recover_invalid_recovery_key" = "이 키로 백업을 복호화할 수 없습니다: 올바른 복구 키를 입력해서 확인해주세요."; -"key_backup_recover_from_passphrase_info" = "복구 암호를 사용해 보안 메시지 기록을 푸세요"; +"key_backup_recover_invalid_passphrase_title" = "보안 키가 올바르지 않음"; +"key_backup_recover_invalid_passphrase" = "이 암호로 백업을 복호화할 수 없습니다: 올바른 보안 암호를 입력해서 확인해주세요."; +"key_backup_recover_invalid_recovery_key_title" = "보안 키가 맞지 않음"; +"key_backup_recover_invalid_recovery_key" = "이 키로 백업을 복호화할 수 없습니다: 올바른 보안 키를 입력해서 확인해주세요."; +"key_backup_recover_from_passphrase_info" = "보안 암호를 사용하여 보안 메시지 기록의 잠금을 해제하세요"; "key_backup_recover_from_passphrase_passphrase_title" = "입력"; "key_backup_recover_from_passphrase_passphrase_placeholder" = "암호 입력"; "key_backup_recover_from_passphrase_recover_action" = "기록 풀기"; -"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "복구 암호가 기억나지 않나요? "; -"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "복구 키를 사용하세요"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "보안 암호가 기억나지 않나요? "; +"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "보안 키를 사용하세요"; "key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "."; -"key_backup_recover_from_recovery_key_info" = "복구 키를 사용해 보안 메시지 기록을 풉니다"; +"key_backup_recover_from_recovery_key_info" = "보안 키를 사용해 보안 메시지 기록의 잠금을 해제합니다"; "key_backup_recover_from_recovery_key_recovery_key_title" = "입력"; -"key_backup_recover_from_recovery_key_recovery_key_placeholder" = "복구 키 입력"; +"key_backup_recover_from_recovery_key_recovery_key_placeholder" = "보안 키 입력"; "key_backup_recover_from_recovery_key_recover_action" = "기록 풀기"; -"key_backup_recover_from_recovery_key_lost_recovery_key_action" = "복구 키를 잃어버렸나요? 설정에서 새 것을 설정할 수 있어요."; +"key_backup_recover_from_recovery_key_lost_recovery_key_action" = "보안 키를 잃어버렸다면 설정에서 새로 설정할 수 있습니다."; "key_backup_recover_success_info" = "백업이 복구되었습니다!"; "key_backup_recover_done_action" = "끝"; "key_backup_setup_banner_title" = "절대 암호화된 메시지를 잃지 마세요"; @@ -711,7 +704,7 @@ // MARK: Start "device_verification_start_title" = "짧은 문장을 비교하여 확인"; "device_verification_start_wait_partner" = "상대방이 수락하기를 기다리는 중…"; -"device_verification_start_use_legacy" = "아무것도 안 나타나나요? 일부 클라이언트는 아직 상호작용 확인을 지원하지 않습니다. 예전 확인 방식을 사용하세요."; +"device_verification_start_use_legacy" = "아무것도 나타나지 않습니까? 일부 클라이언트는 아직 상호작용 확인을 지원하지 않습니다. 예전 확인 방식을 사용하세요."; "device_verification_start_verify_button" = "확인 시작"; "device_verification_start_use_legacy_action" = "예전 확인 방식 사용"; // MARK: Verify @@ -883,7 +876,6 @@ "service_terms_modal_description_for_identity_server_2" = "전화나 이메일로 다른 사람들이 찾을 수 있도록 하기"; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "연락처 탐색"; -"service_terms_modal_message_identity_server" = "연락처를 탐색하려면 ID 서버 (%@)의 약관에 동의해야 합니다."; "settings_add_3pid_password_title_email" = "이메일 주소 추가"; "settings_add_3pid_password_title_msidsn" = "전화번호 추가"; "settings_add_3pid_password_message" = "계속하려면, 비밀번호를 입력해주세요"; @@ -953,7 +945,7 @@ "security_settings_secure_backup_info_checking" = "확인 중…"; "security_settings_secure_backup_description" = "세션에 액세스할 수 없는 경우에 대비하여 계정 데이터로 암호화 키를 백업하십시오. 귀하의 키는 고유한 보안 키로 보호됩니다."; "security_settings_secure_backup" = "보안 백업"; -"security_settings_crypto_sessions_description_2" = "로그인을 인식 하지 못하는 경우 비밀번호를 변경하고 보안 백업을 재설정하세요."; +"security_settings_crypto_sessions_description_2" = "로그인을 인식하지 못할 경우 비밀번호를 변경하고 보안 백업을 재설정하세요."; "security_settings_crypto_sessions_loading" = "세션 로딩 중…"; "security_settings_crypto_sessions" = "내 세션"; @@ -1071,7 +1063,7 @@ // AuthenticatedSessionViewControllerFactory "authenticated_session_flow_not_supported" = "이 앱은 해당 홈서버의 인증 구조를 지원하지 않습니다."; -"security_settings_user_password_description" = "신원을 확인하기 위해 계정 비밀번호를 입력해주세요."; +"security_settings_user_password_description" = "신원을 확인하기 위해 계정 비밀번호를 입력해주세요"; "event_formatter_widget_removed_by_you" = "위젯을 제거함 : %@"; // Events formatter with you @@ -1181,8 +1173,8 @@ "answer_call" = "전화 받기"; "reject_call" = "전화 거부"; "end_call" = "전화 끝내기"; -"ignore" = "무시"; -"unignore" = "무시하지 않기"; +"ignore" = "차단"; +"unignore" = "차단 해제"; "notice_sticker" = "스티커"; // room display name "room_displayname_empty_room" = "빈 방"; @@ -1262,7 +1254,6 @@ "notice_room_aliases" = "방의 별칭: %@"; "notice_room_related_groups" = "이 방과 관련된 그룹: %@"; "notice_encrypted_message" = "암호화된 메시지"; -"notice_encryption_enabled" = "%@님이 종단간 암호화를 켰습니다 (알고리즘 %@)"; "notice_image_attachment" = "사진 첨부"; "notice_audio_attachment" = "소리 첨부"; "notice_video_attachment" = "영상 첨부"; @@ -1530,3 +1521,114 @@ // Onboarding "onboarding_splash_register_button_title" = "계정 만들기"; +"security_settings_complete_security_alert_title" = "보안 확인"; +"settings_enable_room_message_bubbles" = "메시지 버블"; +"settings_labs_use_only_latest_user_avatar_and_name" = "대화 기록에서 사용자의 최신 프로필 사진 및 이름 표시"; +"settings_labs_enable_auto_report_decryption_errors" = "복호화 오류 자동 보고"; +"settings_labs_enable_threads" = "스레드 메시징"; +"room_participants_invite_prompt_to_msg" = "%@을(를) %@에 초대하시겠습니까?"; +"rooms_empty_view_information" = "방은 비공개와 공개 모두 그룹채팅에 적합합니다. +를 눌러 기존에 개설된 방을 찾거나, 새로 개설할 수 있습니다."; +"onboarding_celebration_button" = "출발"; +"onboarding_celebration_message" = "설정이 저장되었습니다."; +"onboarding_celebration_title" = "모두 설정했어요!"; +"onboarding_avatar_accessibility_label" = "프로필 사진"; +"onboarding_avatar_message" = "언제든지 수정할 수 있습니다."; +"onboarding_avatar_title" = "프로필 사진 추가"; +"onboarding_display_name_max_length" = "프로필 이름은 256자 미만이어야 합니다"; +"onboarding_display_name_hint" = "나중에 수정할 수 있습니다"; +"onboarding_display_name_placeholder" = "프로필 이름"; +"onboarding_display_name_message" = "프로필 이름은 메시지를 보낼 때 표시됩니다."; +"onboarding_display_name_title" = "프로필 이름을 정해주세요"; +"onboarding_personalization_skip" = "이 과정 넘기기"; +"onboarding_personalization_save" = "저장 및 계속"; +"onboarding_congratulations_home_button" = "홈 화면으로 이동"; +"onboarding_congratulations_personalize_button" = "프로필 설정"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "계정 %@이(가) 새롭게 생성되었습니다."; +"onboarding_congratulations_title" = "축하합니다!"; +"onboarding_use_case_existing_server_message" = "기존 서버에 가입하려고 하시나요?"; +"onboarding_use_case_skip_button" = "질문 넘기기"; +/* The placeholder string contains onboarding_use_case_skip_button as a tappable action */ +"onboarding_use_case_not_sure_yet" = "아직 확실하지 않으신가요? %@"; +"onboarding_use_case_personal_messaging" = "친구와 가족"; +"onboarding_use_case_message" = "연결할 수 있도록 도와드릴게요."; +"onboarding_use_case_title" = "누구와 가장 많이 대화하나요?"; +"onboarding_splash_page_2_message" = "데이터 및 대화가 저장될 서버를 선택하여 내 정보에 대한 통제력과 독립성을 확보하세요. Matrix를 통해 연결됩니다."; +"onboarding_splash_page_1_title" = "대화 내용을 소유하세요."; +"onboarding_use_case_work_messaging" = "팀"; +"existing" = "기존의"; +"user_verification_session_details_additional_information_untrusted_current_user" = "이 세션에 로그인하고 있지 않은 경우, 계정이 도용되고 있을 가능성이 있습니다."; +"user_verification_session_details_additional_information_untrusted_other_user" = "이 세션을 검증하기 전까지 송수신되는 메시지에 경고 마크가 표시됩니다. 또는, 수동으로 검증할 수 있습니다."; +"user_verification_session_details_information_untrusted_current_user" = "이 세션을 검증한 것으로 표시하고 암호화된 메시지에 접근할 권한을 부여하려면:"; +"user_verification_session_details_information_trusted_current_user" = "이 세션은 검증되어 안전한 메시징이 적용되고 있습니다:"; +"user_verification_session_details_untrusted_title" = "검증되지 않음"; + +// Session details + +"user_verification_session_details_trusted_title" = "검증됨"; +"user_verification_sessions_list_session_untrusted" = "검증되지 않음"; +"user_verification_sessions_list_session_trusted" = "검증됨"; +"user_verification_sessions_list_table_title" = "세션"; +"user_verification_sessions_list_information" = "이 방의 모든 메시지는 종단간 암호화가 적용되어 제 3자가 읽을 수 없습니다."; +"user_verification_sessions_list_user_trust_level_unknown_title" = "알 수 없음"; +"user_verification_sessions_list_user_trust_level_warning_title" = "경고"; + +// Sessions list + +"user_verification_sessions_list_user_trust_level_trusted_title" = "검증됨"; +"user_verification_start_additional_information" = "안전을 위해 직접 만나거나 다른 방법을 사용하여 통신하십시오."; +"user_verification_start_waiting_partner" = "%@를 기다리는 중…"; +"user_verification_start_information_part2" = " 두 장치에서 일회용 코드를 사용하여 확인하십시오."; +"user_verification_start_information_part1" = "보안을 향상시키기 위해 "; + +// MARK: - User verification + +// Start + +"user_verification_start_verify_action" = "검증 시작하기"; +"key_verification_user_title" = "검증"; +"key_verification_tile_request_outgoing_title" = "검증 전송됨"; +"widget_picker_manage_integrations" = "통합 관리하기…"; +"room_widget_permission_room_id_permission" = "방 ID"; +"room_widget_permission_widget_id_permission" = "위젯 ID"; +"room_widget_permission_user_id_permission" = "아이디"; +"room_widget_permission_avatar_url_permission" = "프로필 사진 URL"; +"room_widget_permission_display_name_permission" = "닉네임"; +"room_widget_permission_information_title" = "%@와 데이터를 공유할 수 있습니다:\n"; +"room_widget_permission_webview_information_title" = "쿠키를 설정하고 데이터를 %@와 공유할 수 있습니다:\n"; +"room_widget_permission_creator_info_title" = "새로운 위젯이 추가되었습니다:"; + +// Room widget permissions +"room_widget_permission_title" = "위젯 불러오기"; +"notice_room_created_for_dm" = "%@님이 참가했습니다."; +"notice_room_name_removed_for_dm" = "%@님이 이름을 삭제했습니다"; +"suggest" = "제안"; +"add" = "추가"; +"stop" = "중단"; +"new_word" = "신규"; +"saving" = "저장 중"; + +// Activities +"loading" = "로딩 중"; +"edit" = "편집"; +"room_access_settings_screen_setting_room_access" = "방 접근 설정"; +"room_access_settings_screen_upgrade_alert_upgrading" = "방 업그레이드 중"; +"room_access_settings_screen_upgrade_alert_upgrade_button" = "업그레이드"; +"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "새로운 방에 자동으로 친구 초대"; +"room_access_settings_screen_upgrade_alert_title" = "방 업그레이드"; +"room_access_settings_screen_private_message" = "초대한 친구만 이 방을 찾고 들어올 수 있습니다."; +"room_access_settings_screen_restricted_message" = "스페이스에 있는 누구나 이 방을 찾고 들어올 수 있습니다.\n적용 시 어떤 스페이스인지 확인하는 메시지가 표시됩니다."; +"room_access_settings_screen_public_message" = "누구나 이 방을 찾고 들어올 수 있습니다."; +"room_access_settings_screen_edit_spaces" = "스페이스 수정"; +"room_access_settings_screen_upgrade_required" = "업그레이드 필요"; +"room_access_settings_screen_message" = "%@을(를) 찾고 들어올 수 있는 친구를 정할 수 있습니다."; +"room_access_settings_screen_title" = "누가 이 방에 접근할 수 있나요?"; + +// Room Access Settings +"room_access_settings_screen_nav_title" = "방 접근"; +"room_details_promote_room_suggest_title" = "스페이스 멤버에게 제안하기"; +"room_details_promote_room_title" = "방 홍보하기"; +"room_details_access_row_title" = "접근"; +"room_preview_decline_invitation_options" = "초대를 거부하거나 친구를 차단하시겠습니까?"; +"threads_beta_information_link" = "더 알아보기"; +"threads_beta_title" = "스레드"; diff --git a/Riot/Assets/lo.lproj/InfoPlist.strings b/Riot/Assets/lo.lproj/InfoPlist.strings new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Riot/Assets/lo.lproj/InfoPlist.strings @@ -0,0 +1 @@ + diff --git a/Riot/Assets/lo.lproj/Localizable.strings b/Riot/Assets/lo.lproj/Localizable.strings new file mode 100644 index 000000000..eaf312c61 --- /dev/null +++ b/Riot/Assets/lo.lproj/Localizable.strings @@ -0,0 +1,16 @@ + + + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ ຕອບກັບໃນ %@"; + +/* New message reply from a specific person, not referencing a room. */ +"REPLY_FROM_USER_TITLE" = "%@ ຕອບກັບ"; + +/** Titles **/ + +/* Message title for a specific person in a named room */ +"MSG_FROM_USER_IN_ROOM_TITLE" = "%@ ໃນ %@"; +/** General **/ + +"Notification" = "ແຈ້ງເຕືອນ"; diff --git a/Riot/Assets/lo.lproj/Vector.strings b/Riot/Assets/lo.lproj/Vector.strings new file mode 100644 index 000000000..13139beb7 --- /dev/null +++ b/Riot/Assets/lo.lproj/Vector.strings @@ -0,0 +1,34 @@ + + +"video" = "ວີດີໂອ"; +"voice" = "ສຽງ"; +"camera" = "ກ້ອງ"; +"preview" = "ເບິ່ງຕົວຢ່າງ"; +"accept" = "ຍອມຮັບ"; +"decline" = "ປະຕິດເສດ"; +"join" = "ເຂົ້າຮ່ວມ"; +"save" = "ບັນທຶກ"; +"cancel" = "ຍົກເລີກ"; +"enable" = "ເປີດໃຊ້"; +"off" = "ປິດ"; +"on" = "ເປີດ"; +"retry" = "ລອງໃໝ່"; +"invite" = "ເຊີນ"; +"remove" = "ເອົາອອກ"; +"leave" = "ອອກ"; +"start" = "ເລີ່ມຕົ້ນ"; +"create" = "ສ້າງ"; +"continue" = "ສືບຕໍ່"; +"back" = "ກັບຄືນ"; +"next" = "ຕໍ່ໄປ"; + +// Actions +"view" = "ເບິ່ງ"; +"warning" = "ຄຳເຕືອນ"; +"title_groups" = "ຊຸມຊົນ"; +"title_rooms" = "ຫ້ອງ"; +"title_people" = "ໝູ່ເພື່ອນ"; +"title_favourites" = "ໃຊ້ປະຈຳ"; + +// Titles +"title_home" = "ໜ້າຫຼັກ"; diff --git a/Riot/Assets/lv.lproj/Localizable.strings b/Riot/Assets/lv.lproj/Localizable.strings index 6046b88c7..d1f7880f6 100644 --- a/Riot/Assets/lv.lproj/Localizable.strings +++ b/Riot/Assets/lv.lproj/Localizable.strings @@ -11,9 +11,7 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ nosūtīja Tev attēlu"; /* New action message from a specific person in a named room. */ -"IMAGE_FROM_USER_IN_ROOM" = "%@ publicēja attēlu %@"; /* Multiple unread messages in a room */ "UNREAD_IN_ROOM" = "%@ jaunas ziņas %@"; /* Multiple unread messages from a specific person, not referencing a room */ diff --git a/Riot/Assets/nb-NO.lproj/Localizable.strings b/Riot/Assets/nb-NO.lproj/Localizable.strings index 5f6a77573..5ac730ae3 100644 --- a/Riot/Assets/nb-NO.lproj/Localizable.strings +++ b/Riot/Assets/nb-NO.lproj/Localizable.strings @@ -86,7 +86,6 @@ /** Image Messages **/ /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ sendte et bilde %@"; /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; diff --git a/Riot/Assets/nb-NO.lproj/Vector.strings b/Riot/Assets/nb-NO.lproj/Vector.strings index 460458427..65935f44a 100644 --- a/Riot/Assets/nb-NO.lproj/Vector.strings +++ b/Riot/Assets/nb-NO.lproj/Vector.strings @@ -811,7 +811,6 @@ "room_member_power_level_custom_in" = "Tilpasset (%@) av %@"; "pin_protection_confirm_pin_to_disable" = "Bekreft PIN-kode for å deaktivere PIN-kode"; "pin_protection_choose_pin" = "Opprett en PIN-kode for sikkerhet"; -"device_verification_self_verify_alert_message" = "Verifiser den nye påloggingen som vil ha tilgang til kontoen din:% @"; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "Finne kontakter"; @@ -845,7 +844,6 @@ // MARK: Sign out warning "sign_out_existing_key_backup_alert_title" = "Er du sikker på at du vil logge ut?"; -"service_terms_modal_message_identity_server" = "Godta brukervilkårene til identitetsserveren (%@) for å finne kontakter."; "event_formatter_call_back" = "Ring tilbake"; "room_details_advanced_e2e_encryption_disabled" = "Kryptering er ikke aktivert i dette rommet."; "room_details_flair_section" = "Vis assosiasjon for samfunn"; @@ -866,7 +864,6 @@ "settings_discovery_no_identity_server" = "Du bruker for øyeblikket ikke en identitetsserver. Legg til en for at dine eksisterende kontakter skal kunne finne deg."; "settings_unignore_user" = "Vis alle meldinger fra %@?"; "device_verification_cancelled" = "Den andre parten avbrøt verifiseringen."; -"settings_callkit_info" = "Motta innkommende samtale på låst skjerm. Se dine anrop i systemets anropshistorikk. Hvis iCloud er aktivert, vil anropshistorikken bli delt med Apple."; //"settings_enable_all_notif" = "Enable all notifications"; //"settings_messages_my_display_name" = "Msg containing my display name"; //"settings_messages_my_user_name" = "Msg containing my user name"; @@ -1055,7 +1052,6 @@ "e2e_enabling_on_app_update" = "%@ støtter nå ende-til-ende kryptering, men du må logge inn igjen for å aktivere det.\n\nDu kan gjøre det nå eller senere fra applikasjonsinnstillingene."; // Crash report -"google_analytics_use_prompt" = "Vil du hjelpe til med å forbedre %@ ved automatisk og anonymt sende inn krasj-rapporter og brukerdata?"; "no_voip" = "%@ ringer deg men %@ støtter ikke anrop enda.\nDu kan overse denne varselen og svare anropet fra en annen enhet eller du kan avvise det."; "call_actions_unhold" = "Gjenoppta"; "bug_report_description" = "Vennligst beskriv feilen. Hva gjorde du? Hva forventet du skulle skje? Hva skjedde faktisk?"; @@ -1107,7 +1103,6 @@ // GDPR "gdpr_consent_not_given_alert_message" = "For å fortsette å bruke %@ hjemmeserveren må du se gjennom og godta brukervilkårene."; "e2e_room_key_request_ignore_request" = "Ignorer forespørsel"; -"service_terms_modal_message" = "For å fortsette må du godta brukervilkårene for denne tjenesten (%@)."; "gdpr_consent_not_given_alert_review_now_action" = "Se gjennom nå"; "service_terms_modal_description_for_identity_server_1" = "Finn andre vha. telefonnummer og e-post"; "service_terms_modal_description_for_identity_server_2" = "Bli funnet vha. telefonnummer eller e-post"; @@ -1116,7 +1111,6 @@ "service_terms_modal_description_for_integration_manager" = "Bruk Bots, bridges, widgets og sticker packs"; "deactivate_account_informations_part5" = "Hvis du ønsker at vi skal glemme dine meldinger, kryss av i boksen under.\n\nMeldingssynlighet er sammenlignbar med e-post. Når vi glemmer dine meldinger betyr det at meldinger du har sendt ikke vil bli delt med nye eller uregistrerte brukere, men registrerte brukere som allerede har tilgang til disse meldingene vil fremdeles har tilgang på sin kopi."; "deactivate_account_informations_part4_emphasize" = "vil som standard ikke få oss til å glemme meldinger du har sendt. "; -"rerequest_keys_alert_message" = "Start appen på en annen enhet som kan dekryptere meldingen, slik at den kan sende nøklene til denne økten."; "deactivate_account_password_alert_message" = "For å fortsette, vennligst skriv inn passordet ditt"; "deactivate_account_forget_messages_information_part3" = ": dette vil føre til at fremtidige brukere ser en ufullstendig visning av samtaler)"; "deactivate_account_forget_messages_information_part1" = "Vennligst glem alle meldingene jeg har sendt når kontoen min er deaktivert ("; @@ -1221,7 +1215,6 @@ // Unverified sessions "key_verification_self_verify_unverified_sessions_alert_title" = "Gjennomgå dine innlogginger"; -"device_verification_self_verify_wait_information" = "Bekreft denne økten fra en av de andre øktene dine, og gi den tilgang til krypterte meldinger.\n\nBruk siste versjon av appen på de andre enhetene dine:"; "key_verification_self_verify_unverified_sessions_alert_message" = "Bekreft alle øktene dine for å sikre at kontoen og meldingene dine er trygge."; "device_verification_self_verify_wait_additional_information" = "Dette fungerer med %@ og andre klienter som støtter kryssignering."; "key_verification_verify_sas_additional_information" = "For ytterligere sikkerhet, bruk en annen pålitelig kommunikasjonskanal eller snakk direkte med personen."; @@ -1795,7 +1788,6 @@ // Permissions "camera_access_not_granted_for_call" = "Videosamtaler krever tilgang til kameraet, men %@ har ikke tillatelse til å bruke det"; -"ssl_homeserver_url" = "Hjemmeserver-URL:% @"; /* -*- Automatic localization for en diff --git a/Riot/Assets/nb.lproj/Localizable.strings b/Riot/Assets/nb.lproj/Localizable.strings index a881afa33..b74076feb 100644 --- a/Riot/Assets/nb.lproj/Localizable.strings +++ b/Riot/Assets/nb.lproj/Localizable.strings @@ -43,7 +43,6 @@ /** Image Messages **/ /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ sendte et bilde %@"; /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; diff --git a/Riot/Assets/nl.lproj/Localizable.strings b/Riot/Assets/nl.lproj/Localizable.strings index 7450d6c9b..0176086d1 100644 --- a/Riot/Assets/nl.lproj/Localizable.strings +++ b/Riot/Assets/nl.lproj/Localizable.strings @@ -33,7 +33,6 @@ /** Image Messages **/ /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ heeft een afbeelding gestuurd %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ heeft een afbeelding %@ in %@ geplaatst"; /** Coalesced messages **/ diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index 85999444d..a458119a2 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -53,17 +53,17 @@ "auth_skip" = "Overslaan"; "auth_send_reset_email" = "Herstel-e-mail versturen"; "auth_return_to_login" = "Terug naar het aanmeldingsscherm"; -"auth_user_id_placeholder" = "E-mailadres of gebruikersnaam"; +"auth_user_id_placeholder" = "E-mailadres of inlognaam"; "auth_password_placeholder" = "Wachtwoord"; "auth_new_password_placeholder" = "Nieuw wachtwoord"; -"auth_user_name_placeholder" = "Gebruikersnaam"; +"auth_user_name_placeholder" = "Inlognaam"; "auth_optional_email_placeholder" = "E-mailadres (optioneel)"; "auth_email_placeholder" = "E-mailadres"; "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_invalid_login_param" = "Onjuiste gebruikersnaam en/of 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)"; "auth_invalid_email" = "Dit ziet er niet uit als een geldig e-mailadres"; @@ -135,7 +135,7 @@ "search_people" = "Personen"; "search_files" = "Bestanden"; "search_default_placeholder" = "Zoeken"; -"search_people_placeholder" = "Zoeken op gebruikers-ID, naam of e-mailadres"; +"search_people_placeholder" = "Zoeken op persoon-ID, naam of e-mailadres"; "search_no_result" = "Geen resultaten"; // Directory "directory_cell_title" = "Bladeren door de catalogus"; @@ -147,7 +147,7 @@ "directory_search_fail" = "Ophalen van gegevens is mislukt"; // Contacts "contacts_address_book_section" = "LOKALE CONTACTEN"; -"contacts_address_book_matrix_users_toggle" = "Alleen Matrix-gebruikers"; +"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"; @@ -164,9 +164,9 @@ "room_participants_invite_prompt_title" = "Bevestiging"; "room_participants_invite_prompt_msg" = "Weet u zeker dat u %@ in dit gesprek wilt uitnodigen?"; "room_participants_filter_room_members" = "Kamerleden filteren"; -"room_participants_invite_another_user" = "Zoeken/uitnodigen met gebruikers-ID, naam of e-mailadres"; +"room_participants_invite_another_user" = "Zoeken/uitnodigen met persoon-ID, naam of e-mailadres"; "room_participants_invite_malformed_id_title" = "Uitnodigingsfout"; -"room_participants_invite_malformed_id" = "Ongeldige ID. Dit zou een e-mailadres of een Matrix-ID zoals ‘@gebruikersnaam:domein’ moeten zijn"; +"room_participants_invite_malformed_id" = "Ongeldige ID. Dit zou een e-mailadres of een Matrix-ID zoals ‘@persoon:domein’ moeten zijn"; "room_participants_invited_section" = "UITGENODIGD"; "room_participants_online" = "Online"; "room_participants_offline" = "Offline"; @@ -183,9 +183,9 @@ "room_participants_action_remove" = "Uit deze kamer verwijderen"; "room_participants_action_ban" = "Verbannen uit deze kamer"; "room_participants_action_unban" = "Ontbannen"; -"room_participants_action_ignore" = "Alle berichten van deze gebruiker verbergen"; -"room_participants_action_unignore" = "Alle berichten van deze gebruiker tonen"; -"room_participants_action_set_default_power_level" = "Terugzetten naar normale gebruiker"; +"room_participants_action_ignore" = "Alle berichten van deze persoon verbergen"; +"room_participants_action_unignore" = "Alle berichten van deze persoon tonen"; +"room_participants_action_set_default_power_level" = "Terugzetten naar normale persoon"; "room_participants_action_set_moderator" = "Benoemen tot moderator"; "room_participants_action_set_admin" = "Benoemen tot beheerder"; "room_participants_action_start_new_chat" = "Nieuw gesprek beginnen"; @@ -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 gebruiker verbergen?"; +"room_event_action_report_prompt_ignore_user" = "Wilt u alle berichten van deze persoon verbergen?"; "room_event_action_save" = "Opslaan"; "room_event_action_resend" = "Opnieuw versturen"; "room_event_action_delete" = "Verwijderen"; @@ -258,9 +258,9 @@ "settings_config_home_server" = "Server is %@"; "settings_config_identity_server" = "Identiteitsserver is %@"; "settings_config_user_id" = "Aangemeld als %@"; -"settings_user_settings" = "GEBRUIKERSINSTELLINGEN"; +"settings_user_settings" = "PERSOON INSTELLINGEN"; "settings_notifications_settings" = "MELDINGSINSTELLINGEN"; -"settings_ignored_users" = "GENEGEERDE GEBRUIKERS"; +"settings_ignored_users" = "GENEGEERDE PERSONEN"; "settings_contacts" = "APPARAAT CONTACTEN"; "settings_advanced" = "GEAVANCEERD"; "settings_other" = "OVERIG"; @@ -351,7 +351,7 @@ "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_banned_users_section" = "Verbannen gebruikers"; +"room_details_banned_users_section" = "Verbannen personen"; "room_details_advanced_section" = "Geavanceerd"; "room_details_advanced_room_id" = "Kamer-ID:"; "room_details_advanced_enable_e2e_encryption" = "Versleuteling inschakelen (let op: dit kan niet meer worden uitgeschakeld!)"; @@ -404,7 +404,6 @@ "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."; // Crash report -"google_analytics_use_prompt" = "Wilt u helpen met het verbeteren van %@ door anonieme crashrapporten en gebruiksstatistieken te versturen?"; // 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."; @@ -423,8 +422,8 @@ "auth_phone_in_use" = "Dit telefoonnummer is al in gebruik"; "auth_untrusted_id_server" = "De identiteitsserver is niet vertrouwd"; "auth_email_not_found" = "E-mail versturen mislukt: dit e-mailadres werd niet gevonden"; -"contacts_user_directory_section" = "GEBRUIKERSCATALOGUS"; -"contacts_user_directory_offline_section" = "GEBRUIKERSCATALOGUS (offline)"; +"contacts_user_directory_section" = "PERSONENGIDS"; +"contacts_user_directory_offline_section" = "PERSONENGIDS (offline)"; "settings_user_interface" = "GEBRUIKERSINTERFACE"; "settings_ui_language" = "Taal"; // Read Receipts @@ -448,7 +447,7 @@ "sending" = "Wordt verstuurd"; "search_in_progress" = "Aan het zoeken…"; "room_event_action_cancel_send" = "Verzending annuleren"; -"room_event_failed_to_send" = "Verzenden is mislukt"; +"room_event_failed_to_send" = "Versturen is mislukt"; "settings_calls_settings" = "OPROEPEN"; "settings_show_decrypted_content" = "Ontsleutelde inhoud tonen"; "settings_enable_callkit" = "Geïntegreerde oproepen"; @@ -471,7 +470,7 @@ "call_incoming_voice" = "Inkomende oproep…"; "call_incoming_video" = "Inkomende video-oproep…"; // Widget Integration Manager -"widget_integration_need_to_be_able_to_invite" = "U moet gebruikers kunnen uitnodigen om dat te kunnen doen."; +"widget_integration_need_to_be_able_to_invite" = "U 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."; @@ -521,15 +520,15 @@ "group_participants_invite_prompt_title" = "Bevestiging"; "group_participants_invite_prompt_msg" = "Weet u zeker dat u %@ in deze groep wilt uitnodigen?"; "group_participants_filter_members" = "Gemeenschapsleden filteren"; -"group_participants_invite_another_user" = "Zoeken/uitnodigen met gebruikers-ID of naam"; +"group_participants_invite_another_user" = "Zoeken/uitnodigen met persoon-ID of naam"; "group_participants_invite_malformed_id_title" = "Uitnodigingsfout"; -"group_participants_invite_malformed_id" = "Misvormde ID. Dit moet een Matrix-ID zijn, zoals ‘@gebruikersnaam:domein’"; +"group_participants_invite_malformed_id" = "Misvormde ID. Dit moet een Matrix-ID zijn, zoals ‘@persoon:domein’"; "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."; -"room_event_action_kick_prompt_reason" = "Reden voor het verwijderen van deze gebruiker"; -"room_event_action_ban_prompt_reason" = "Reden voor het verbannen van deze gebruiker"; +"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_review_now_action" = "Nu lezen"; @@ -550,9 +549,9 @@ "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 gebruikers die al toegang hebben tot deze berichten zullen alsnog toegang hebben tot hun eigen kopie ervan."; +"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_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 gebruikers een onvolledig beeld krijgen van gesprekken)"; +"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"; @@ -562,7 +561,7 @@ "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-gebruikers. 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. 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."; "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"; @@ -576,8 +575,8 @@ "room_resource_limit_exceeded_message_contact_2_link" = "contact op te nemen met uw 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 gebruikers overschreden, dus "; -"room_resource_usage_limit_reached_message_2" = "sommige gebruikers zullen zich niet kunnen aanmelden."; +"room_resource_usage_limit_reached_message_1_monthly_active_user" = "Deze server heeft zijn limiet voor maandelijks actieve personen overschreden, dus "; +"room_resource_usage_limit_reached_message_2" = "sommige personen zullen zich niet kunnen aanmelden."; "room_resource_usage_limit_reached_message_contact_3" = " om deze limiet te verhogen."; "settings_key_backup" = "SLEUTELBACK-UP"; "settings_labs_room_members_lazy_loading" = "Gespreksleden lui laden"; @@ -678,10 +677,6 @@ "sign_out_key_backup_in_progress_alert_cancel_action" = "Ik wacht wel"; "room_event_action_reply" = "Beantwoorden"; "room_event_action_edit" = "Bewerken"; -"room_event_action_reaction_agree" = "Akkoord met %@"; -"room_event_action_reaction_disagree" = "Niet akkoord met %@"; -"room_event_action_reaction_like" = "Duim omhoog voor %@"; -"room_event_action_reaction_dislike" = "Duim omlaag voor %@"; "room_action_reply" = "Beantwoorden"; "settings_labs_message_reaction" = "Beantwoord berichten met emoticons"; "settings_key_backup_button_connect" = "Dit apparaat verbinden met sleutelback-up"; @@ -914,7 +909,7 @@ "call_transfer_contacts_all" = "Alles"; "call_transfer_contacts_recent" = "Recent"; "call_transfer_dialpad" = "Kiestoetsen"; -"call_transfer_users" = "Gebruikers"; +"call_transfer_users" = "Personen"; // MARK: - Call Transfer "call_transfer_title" = "Doorschakelen"; @@ -928,19 +923,19 @@ "room_info_list_one_member" = "1 persoon"; "room_info_list_several_members" = "%@ personen"; "create_room_placeholder_address" = "#testkamer:matrix.org"; -"create_room_section_header_address" = "Kameradres"; -"create_room_show_in_directory" = "Kamer weergeven in de gids"; +"create_room_section_header_address" = "ADRES"; +"create_room_show_in_directory" = "Kamer weergeven in gids"; "create_room_section_footer_type" = "Personen kunnen alleen met een uitnodiging deelnemen aan een privékamer."; -"create_room_type_public" = "Publieke Kamer"; -"create_room_type_private" = "Privékamer"; -"create_room_section_header_type" = "Type kamer"; +"create_room_type_public" = "Publieke ruimte (iedereen)"; +"create_room_type_private" = "Privékamer (alleen op uitnodiging)"; +"create_room_section_header_type" = "WIE HEEFT TOEGANG"; "create_room_section_footer_encryption" = "Versleuteling kan achteraf niet worden uitgeschakeld."; "create_room_enable_encryption" = "Versleuteling inschakelen"; -"create_room_section_header_encryption" = "Kamerversleuteling"; -"create_room_placeholder_topic" = "Onderwerp"; -"create_room_section_header_topic" = "Kameronderwerp (optioneel)"; +"create_room_section_header_encryption" = "VERSLEUTELING"; +"create_room_placeholder_topic" = "Waar gaat deze kamer over?"; +"create_room_section_header_topic" = "ONDERWERP (OPTIONEEL)"; "create_room_placeholder_name" = "Naam"; -"create_room_section_header_name" = "Kamernaam"; +"create_room_section_header_name" = "NAAM"; // MARK: - Create Room @@ -1009,7 +1004,7 @@ "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 gebruikers."; +"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."; @@ -1104,7 +1099,7 @@ "key_verification_scan_confirmation_scanning_title" = "Bijna klaar! Wachten op bevestiging…"; "key_verification_verify_qr_code_scan_other_code_success_message" = "De QR-code is gevalideerd."; "key_verification_verify_qr_code_scan_other_code_success_title" = "Code gevalideerd!"; -"key_verification_verify_qr_code_other_scan_my_code_title" = "Heeft de andere gebruiker de QR-code gescand?"; +"key_verification_verify_qr_code_other_scan_my_code_title" = "Heeft de andere personen de QR-code gescand?"; "key_verification_verify_qr_code_start_emoji_action" = "Verifiëren met emoji"; "key_verification_verify_qr_code_cannot_scan_action" = "Scannen niet mogelijk?"; "key_verification_verify_qr_code_scan_code_action" = "Scan hun code"; @@ -1147,17 +1142,17 @@ // User -"key_verification_verified_user_information" = "Berichten met deze gebruikers 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 gebruikers weten dat zij u kunnen vertrouwen."; -"key_verification_verified_new_session_information" = "U kunt nu veilig uw berichten op uw andere apparaat lezen. Andere gebruikers weten dat zij u kunnen vertrouwen."; -"key_verification_verified_other_session_information" = "U kunt nu veilig uw berichten in uw andere sessie lezen. Andere gebruikers weten dat zij u kunnen vertrouwen."; +"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_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 Gebruikersinstellingen van deze sessie overeenkomt met de sleutel hieronder:"; +"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:"; // MARK: Manually Verify Device @@ -1238,7 +1233,6 @@ "secure_key_backup_setup_intro_title" = "Veilige Back-up"; "service_terms_modal_policy_checkbox_accessibility_hint" = "Aanvinken om te aanvaarden %@"; -"service_terms_modal_message_identity_server" = "Aanvaard de gebruikersvoorwaarden van de identiteitsserver (%@), om contacten te vinden."; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "Contacten ontdekken"; @@ -1248,7 +1242,7 @@ "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 gebruikers-ID"; +"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_information_title" = "Dit gebruiken kan gegevens delen met %@:\n"; @@ -1328,7 +1322,7 @@ "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_title" = "Beveiliging afronden"; -"security_settings_blacklist_unverified_devices_description" = "Verifieer alle sessies van een gebruiker om deze als vertrouwd te markeren en berichten naar te zenden."; +"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"; "security_settings_advanced" = "GEAVANCEERD"; "security_settings_export_keys_manually" = "Exporteer sleutels handmatig"; @@ -1367,7 +1361,7 @@ "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" = "Gebruikersinstellingen"; +"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_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."; @@ -1376,7 +1370,6 @@ "settings_add_3pid_password_message" = "Geef uw 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_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_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_button" = "Terugvalserver voor oproepen toestaan"; @@ -1416,7 +1409,7 @@ "room_participants_leave_prompt_msg_for_dm" = "Weet u zeker dat u het gesprek wilt 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 verkennen of maak een nieuwe aan."; +"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."; "rooms_empty_view_title" = "Kamers"; "people_empty_view_information" = "Veilig communiceren met iedereen. Druk op + om personen toe te voegen."; "people_empty_view_title" = "Personen"; @@ -1503,7 +1496,7 @@ "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"; "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 gebruikers die u wilt uitnodigen en probeer het opnieuw."; +"room_creation_dm_error" = "Uw direct gesprek kon niet aangemaakt worden. Controleer de personen die u wilt 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"; @@ -1522,7 +1515,7 @@ // Mark: - Voice Messages -"voice_message_release_to_send" = "Vasthouden om op te nemen, loslaten om te verzenden"; +"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_title" = "Meldingen uitgeschakeld"; @@ -1724,8 +1717,8 @@ "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 verzenden. Probeer het later opnieuw."; -"location_sharing_post_failure_title" = "We konden uw locatie niet verzenden"; +"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"; "home_context_menu_leave" = "Verlaten"; "home_context_menu_normal_priority" = "Normale prioriteit"; "home_context_menu_low_priority" = "Lage prioriteit"; @@ -1785,27 +1778,27 @@ // Login Screen "login_create_account" = "Account aanmaken:"; "login_server_url_placeholder" = "URL (bv. https://matrix.org)"; -"login_home_server_title" = "Thuisserver-URL:"; -"login_home_server_info" = "Uw thuisserver slaat al uw gespreks- en accountgegevens op"; +"login_home_server_title" = "Server-URL:"; +"login_home_server_info" = "Uw server slaat al uw 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 gebruikers 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 u eenvoudiger op Matrix vinden, verder geeft het u een manier om uw wachtwoord in de toekomst te wijzigen."; "login_email_placeholder" = "E-mailadres"; "login_prompt_email_token" = "Voer uw e-mailadres-validatiebewijs in:"; "login_error_title" = "Aanmelden Mislukt"; -"login_error_no_login_flow" = "Ophalen van authenticatie-informatie van deze thuisserver is mislukt"; -"login_error_do_not_support_login_flows" = "Momenteel bieden we geen ondersteuning voor sommige of alle aanmeldingsmethoden van deze thuisserver"; +"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"; "login_error_registration_is_not_supported" = "Registratie wordt momenteel niet ondersteund"; -"login_error_forbidden" = "Ongeldige gebruikersnaam/wachtwoord"; +"login_error_forbidden" = "Ongeldige inlognaam/wachtwoord"; "login_error_unknown_token" = "Het gespecificeerde toegangsbewijs is niet herkend"; "login_error_bad_json" = "Ongeldige JSON"; "login_error_not_json" = "Bevat geen geldige JSON"; "login_error_limit_exceeded" = "Er zijn te veel verzoeken verzonden"; -"login_error_user_in_use" = "Deze gebruikersnaam is al in gebruik"; +"login_error_user_in_use" = "Deze inlognaam is al in gebruik"; "login_error_login_email_not_yet" = "De koppeling in de e-mail is nog niet geopend"; "login_use_fallback" = "Terugvalpagina gebruiken"; "login_leave_fallback" = "Annuleren"; @@ -1832,7 +1825,7 @@ "select_account" = "Selecteer een account"; "attach_media" = "Media van de bibliotheek bijvoegen"; "capture_media" = "Foto/video maken"; -"invite_user" = "Matrix-gebruiker uitnodigen"; +"invite_user" = "Matrix-persoon uitnodigen"; "reset_to_default" = "Standaardwaarden herstellen"; "resend_message" = "Bericht opnieuw versturen"; "select_all" = "Alles selecteren"; @@ -1855,11 +1848,10 @@ "notice_room_created" = "%@ heeft de kamer aangemaakt en ingesteld."; "notice_room_join_rule" = "De toetredingsregel is: %@"; "notice_room_power_level_intro" = "De machtsniveaus van de gespreksleden zijn:"; -"notice_room_power_level_acting_requirement" = "De minimale machtsniveaus waarover een gebruiker moet beschikken vooraleer deze kan handelen zijn:"; +"notice_room_power_level_acting_requirement" = "De minimale machtsniveaus waarover een persoon moet beschikken vooraleer deze kan handelen zijn:"; "notice_room_power_level_event_requirement" = "De minimale machtsniveaus gerelateerd aan gebeurtenissen zijn:"; "notice_room_aliases" = "De gespreksbijnamen zijn: %@"; "notice_encrypted_message" = "Versleuteld bericht"; -"notice_encryption_enabled" = "%@ heeft eind-tot-eind-versleuteling aangezet (%@-algoritme)"; "notice_image_attachment" = "afbeeldingsbijlage"; "notice_audio_attachment" = "audiobijlage"; "notice_video_attachment" = "videobijlage"; @@ -1900,7 +1892,7 @@ // Encryption information "room_event_encryption_info_title" = "Informatie over eind-tot-eind-versleuteling\n\n"; "room_event_encryption_info_event" = "Gebeurtenisinformatie\n"; -"room_event_encryption_info_event_user_id" = "Gebruikers-ID\n"; +"room_event_encryption_info_event_user_id" = "Persoon-ID\n"; "room_event_encryption_info_event_identity_key" = "Curve25519-identiteitssleutel\n"; "room_event_encryption_info_event_fingerprint_key" = "Geclaimde Ed25519-vingerafdrukssleutel\n"; "room_event_encryption_info_event_algorithm" = "Algoritme\n"; @@ -1922,7 +1914,7 @@ "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 gebruikersinstellingen 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, 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_ok" = "Verifiëren"; // Account "account_save_changes" = "Wijzigingen opslaan"; @@ -1948,7 +1940,7 @@ "room_creation_alias_placeholder" = "(bv. #foo:voorbeeld.org)"; "room_creation_alias_placeholder_with_homeserver" = "(bv. #foo%@)"; "room_creation_participants_title" = "Deelnemers:"; -"room_creation_participants_placeholder" = "(bv. @jan:thuisserver1; @joep:thuisserver2…)"; +"room_creation_participants_placeholder" = "(bv. @jan:server1; @joep:server2…)"; // Room "room_please_select" = "Selecteer een gesprek"; "room_error_join_failed_title" = "Toetreden tot het gesprek is mislukt"; @@ -1962,8 +1954,8 @@ "room_no_power_to_create_conference_call" = "U heeft 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 gebruiker wilt verbergen?"; -"room_member_power_level_prompt" = "U kunt deze veranderingen niet ongedaan maken aangezien u de gebruiker tot hetzelfde niveau als uzelf promoveert.\nWeet u het zeker?"; +"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?"; // Attachment "attachment_size_prompt" = "Wilt u het versturen als:"; "attachment_original" = "Werkelijke grootte (%@)"; @@ -1977,7 +1969,7 @@ "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_import" = "Bezig met importeren…"; // Contacts -"contact_mx_users" = "Matrix-gebruikers"; +"contact_mx_users" = "Matrix personen"; "contact_local_contacts" = "Lokale contacten"; // Search "search_no_results" = "Geen resultaten"; @@ -1999,7 +1991,7 @@ "e2e_passphrase_empty" = "Wachtwoord mag niet leeg zijn"; "e2e_passphrase_not_match" = "Wachtwoorden moeten overeenkomen"; // Others -"user_id_title" = "Gebruikers-ID:"; +"user_id_title" = "Persoon-ID:"; "offline" = "offline"; "unsent" = "Niet verstuurd"; "error" = "Fout"; @@ -2009,13 +2001,13 @@ "public" = "Publiek"; "power_level" = "Machtsniveau"; "network_error_not_reachable" = "Controleer uw netwerkverbinding"; -"user_id_placeholder" = "bv: @jan:thuisserver"; -"ssl_homeserver_url" = "Thuisserver-URL: %@"; +"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" = "Gebruikers zoeken op basis van uw lokale contacten vereist toegang tot die contacten, maar %@ heeft hier geen toestemming voor"; -"local_contacts_access_discovery_warning_title" = "Gebruikers zoeken"; +"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_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."; // Country picker "country_picker_title" = "Kies een land"; @@ -2084,8 +2076,8 @@ "membership_invite" = "Uitgenodigd"; "membership_leave" = "Verlaten"; "membership_ban" = "Verbannen"; -"num_members_one" = "%@ gebruiker"; -"num_members_other" = "%@ gebruikers"; +"num_members_one" = "%@ persoon"; +"num_members_other" = "%@ personen"; "kick" = "Er uit zetten"; "ban" = "Verbannen"; "unban" = "Ontbannen"; @@ -2125,10 +2117,10 @@ "notification_settings_custom_sound" = "Aangepast geluid"; "notification_settings_per_room_notifications" = "Per-gespreks-meldingen"; "notification_settings_per_sender_notifications" = "Per-afzender-meldingen"; -"notification_settings_sender_hint" = "@gebruiker:domein.com"; +"notification_settings_sender_hint" = "@persoon:domein.nl"; "notification_settings_select_room" = "Selecteer een gesprek"; "notification_settings_other_alerts" = "Andere meldingen"; -"notification_settings_contain_my_user_name" = "Meld mij met geluid over berichten die mijn gebruikersnaam bevatten"; +"notification_settings_contain_my_user_name" = "Meld mij met geluid over berichten die mijn inlognaam bevatten"; "notification_settings_contain_my_display_name" = "Meld mij met geluid over berichten die mijn weergavenaam bevatten"; "notification_settings_just_sent_to_me" = "Meld mij met geluid over berichten die alleen naar mij gestuurd zijn"; "notification_settings_invite_to_a_new_room" = "Meld mij wanneer ik in een nieuw gesprek uitgenodigd word"; @@ -2172,8 +2164,8 @@ "notice_in_reply_to" = "In antwoord op"; "error_common_message" = "Er is een fout opgetreden. Probeer het later opnieuw."; "login_error_resource_limit_exceeded_title" = "Bronlimiet Overschreden"; -"login_error_resource_limit_exceeded_message_default" = "Deze thuisserver heeft één of meerdere van zijn bronlimieten overschreden."; -"login_error_resource_limit_exceeded_message_monthly_active_user" = "Deze thuisserver heeft zijn limiet voor maandelijks actieve gebruikers bereikt."; +"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_contact_button" = "Beheerder contacteren"; // Reply to message @@ -2278,8 +2270,204 @@ "attachment_small_with_resolution" = "Klein %@ (~%@)"; "attachment_size_prompt_message" = "U kunt dit uitzetten in uw instellingen."; "attachment_size_prompt_title" = "Bevestig de afmeting om te versturen"; -"room_displayname_all_other_participants_left" = "%@ (vertrok)"; "attachment_unsupported_preview_message" = "Dit bestandstype wordt niet ondersteund."; "attachment_unsupported_preview_title" = "Kan geen voorbeeld geven"; "room_displayname_all_other_members_left" = "%@ (Vertrok)"; "message_reply_to_sender_sent_their_location" = "deelde hun locatie."; +"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_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?"; +/* The placeholder string contains onboarding_use_case_skip_button as a tappable action */ +"onboarding_use_case_not_sure_yet" = "Nog niet zeker? U kunt %@"; +"notice_error_unformattable_event" = "** Kan bericht niet weergeven. Meld een bug a.u.b."; +"location_sharing_pin_drop_share_title" = "Stuur deze locatie"; +"location_sharing_static_share_title" = "Stuur mijn huidige locatie"; +"live_location_sharing_banner_stop" = "Stop"; +"live_location_sharing_banner_title" = "Live locatie ingeschakeld"; + +// MARK: Live location sharing + +"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_creation_in_one_space" = "in 1 space"; +"spaces_creation_in_many_spaces" = "in %@ spaces"; +"spaces_creation_in_spacename_plus_many" = "in %@ + %@ spaces"; +"spaces_creation_in_spacename_plus_one" = "in %@ + 1 space"; +"spaces_creation_in_spacename" = "in %@"; +"spaces_creation_post_process_adding_rooms" = "%@ kamers toevoegen"; +"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" = "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_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_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_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_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_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_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."; + +// 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_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"; +"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"; +"room_invite_to_room_option_detail" = "Ze zullen geen deel uitmaken van %@."; +"room_invite_to_room_option_title" = "Alleen naar deze kamer"; + +// Mark: - Room invite + +"room_invite_to_space_option_title" = "Tot %@"; +"share_invite_link_space_text" = "Hoi, word lid van deze ruimte op %@"; +"share_invite_link_room_text" = "Hoi, word lid van deze kamer op %@"; + +// MARK: - Share invite link + +"share_invite_link_action" = "Uitnodigingslink delen"; +"home_syncing" = "Synchroniseren"; +"create_room_suggest_room_footer" = "Voorgestelde kamers worden gepromoveerd tot leden van de space die goed lid kunnen worden."; +"create_room_suggest_room" = "Suggestie voor leden van de space"; +"create_room_show_in_directory_footer" = "Dit zal mensen helpen om te vinden en lid te worden."; +"create_room_promotion_header" = "PROMOTIE"; +"create_room_section_footer_type_public" = "Alleen mensen die zijn uitgenodigd, kunnen vinden en deelnemen, niet alleen mensen in de naam van de Space."; +"create_room_section_footer_type_restricted" = "Iedereen in Space naam kan vinden en deelnemen."; +"create_room_section_footer_type_private" = "Alleen uitgenodigde personen kunnen vinden en deelnemen."; +"create_room_type_restricted" = "Space deelnemers"; +"call_jitsi_unable_to_start" = "Kan gesprek niet starten"; +"room_suggestion_settings_screen_message" = "Voorgestelde kamers worden gepromoveerd tot space leden als geschikte leden om lid te worden."; +"room_suggestion_settings_screen_title" = "Maak een kamer voorgesteld in een space"; + +// Room suggestion Settings +"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_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"; +"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "Leden automatisch uitnodigen voor nieuwe chatruimte"; +"room_access_settings_screen_upgrade_alert_title" = "Upgrade kamer"; +"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_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?"; + +// Room Access Settings +"room_access_settings_screen_nav_title" = "Kamer toegang"; +"room_details_promote_room_suggest_title" = "Suggestie voor leden van de ruimte"; +"room_details_promote_room_title" = "Kamer promoten"; +"room_details_access_row_title" = "Toegang"; +"settings_labs_enable_auto_report_decryption_errors" = "Decoderingsfouten automatisch rapporteren"; +"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_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 %@?"; +"room_participants_leave_success" = "Kamer verlaten"; +"room_participants_leave_processing" = "Verlaten"; +"search_filter_placeholder" = "Filter"; +"onboarding_celebration_button" = "Laten we beginnen"; +"onboarding_celebration_message" = "Uw voorkeuren zijn opgeslagen."; +"onboarding_celebration_title" = "U bent gereed!"; +"onboarding_avatar_accessibility_label" = "Profielfoto"; +"onboarding_avatar_message" = "U kunt dit op elk moment wijzigen."; +"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_placeholder" = "Weergavenaam"; +"onboarding_display_name_message" = "Dit wordt weergegeven wanneer u berichten verzendt."; +"onboarding_display_name_title" = "Kies een weergavenaam"; +"onboarding_personalization_skip" = "Sla deze stap over"; +"onboarding_personalization_save" = "Opslaan en doorgaan"; +"onboarding_congratulations_home_button" = "Breng me het begin"; +"onboarding_congratulations_personalize_button" = "Personaliseer profiel"; +/* 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_community_messaging" = "Gemeenschappen"; +"onboarding_use_case_work_messaging" = "Teams"; +"onboarding_use_case_personal_messaging" = "Vrienden en familie"; +"onboarding_use_case_message" = "We helpen je om contact te maken."; +"onboarding_use_case_title" = "Met wie ga je het meest chatten?"; +"saving" = "Opslaan"; + +// Activities +"loading" = "Laden"; +"edit" = "Bewerk"; +"suggest" = "Suggestie"; +"add" = "Toevoegen"; +"existing" = "Bestaande"; +"new_word" = "Nieuw"; +"stop" = "Stop"; +"joining" = "Meedoen"; +"location_sharing_live_list_item_stop_sharing_action" = "Stop delen"; +"location_sharing_live_list_item_current_user_display_name" = "U"; +"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"; +"location_sharing_live_list_item_time_left" = "%@ over"; +"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 applicatie 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. "; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "Uw account %@ is aangemaakt."; +"onboarding_use_case_skip_button" = "sla deze vraag over"; diff --git a/Riot/Assets/pl.lproj/Localizable.strings b/Riot/Assets/pl.lproj/Localizable.strings index fffa61284..cdb5b0688 100644 --- a/Riot/Assets/pl.lproj/Localizable.strings +++ b/Riot/Assets/pl.lproj/Localizable.strings @@ -23,7 +23,6 @@ /* Look, stuff's happened, alright? Just open the app. */ "MSGS_IN_TWO_PLUS_ROOMS" = "%@ nowych wiadomości w %@, %@ i innych"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ wysłał(-a) zdjęcie %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ wysłał(-a) zdjęcie %@ w %@"; /* Multiple unread messages from a specific person, not referencing a room */ diff --git a/Riot/Assets/pl.lproj/Vector.strings b/Riot/Assets/pl.lproj/Vector.strings index 79e5f4750..8d3322a8d 100644 --- a/Riot/Assets/pl.lproj/Vector.strings +++ b/Riot/Assets/pl.lproj/Vector.strings @@ -774,7 +774,6 @@ "call_jitsi_error" = "Nie udało się dołączyć do rozmowy konferencyjnej."; "no_voip" = "%@ dzwoni do Ciebie ale %@ nie obsługuje jeszcze połączeń\nMożesz zignorować to powiadomienie i odebrać rozmowę na innym urządzeniu, bądź odrzucić je."; // Crash report -"google_analytics_use_prompt" = "Chcesz pomóc nam w ulepszaniu %@ przez automatyczne wysyłanie anonimowych raportów o błędach i danych użytkowania?"; // Key backup wrong version "e2e_key_backup_wrong_version_title" = "Nowy Klucz Kopii Zapasowej"; "room_participants_remove_third_party_invite_prompt_msg" = "Czy jesteś pewien, że chcesz odrzucić to zaproszenie?"; @@ -791,7 +790,6 @@ "share_extension_failed_to_encrypt" = "Nie udało się wysłać. Sprawdź w głównej aplikacji ustawienia szyfrowania dla tego pokoju"; // Service terms "service_terms_modal_title" = "Warunki usługi"; -"service_terms_modal_message" = "Aby kontynuować musisz zaakceptować warunki usługi (%@)."; "service_terms_modal_accept_button" = "Akceptuj"; "service_terms_modal_description_for_identity_server" = "Być wykrywalnym dla innych"; "service_terms_modal_description_for_integration_manager" = "Używaj Botów, mostków, widżetów i naklejek"; @@ -1159,7 +1157,6 @@ "key_verification_manually_verify_device_key_title" = "Klucz sesji"; "key_verification_manually_verify_device_id_title" = "Identyfikator sesji"; "key_verification_manually_verify_device_name_title" = "Nazwa sesji"; -"key_verification_manually_verify_device_instruction" = "Potwierdź zgodność poniższych informacji porównując następujące %@y z ustawieniami użytkownika w drugiej sesji:"; // MARK: Manually Verify Device @@ -1243,7 +1240,6 @@ "secure_key_backup_setup_intro_title" = "Bezpieczna kopia zapasowa"; "service_terms_modal_policy_checkbox_accessibility_hint" = "Zaznacz, aby zaakceptować %@"; -"service_terms_modal_message_identity_server" = "Zaakceptuj warunki serwera tożsamości (%@), aby móc wyszukiwać kontakty."; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "Odkrywanie kontaktów"; @@ -1503,10 +1499,8 @@ "room_recents_unknown_room_error_message" = "Nie mogę znaleźć tego pokoju. Upewnij się, że on istnieje"; "room_creation_dm_error" = "Nie mogliśmy utworzyć pokoju. Sprawdź użytkowników, których chcesz zaprosić, i spróbuj ponownie."; "version_check_modal_action_title_deprecated" = "Dowiedz się jak"; -"version_check_modal_subtitle_deprecated" = "Pracowaliśmy nad ulepszeniem %@u, aby polepszyć korzystanie z aplikacji. Niestety Twoja aktualna wersja systemu iOS nie jest zgodna z niektórymi z tych poprawek i nie jest już obsługiwana.\nRadzimy uaktualnić system operacyjny, aby w pełni wykorzystać jego potencjał."; "version_check_modal_title_deprecated" = "Nie obsługujemy już iOS %@"; "version_check_modal_action_title_supported" = "Rozumiem"; -"version_check_modal_subtitle_supported" = "Pracowaliśmy nad ulepszeniem %@u, aby polepszyć korzystanie z aplikacji. Niestety Twoja obecna wersja systemu iOS nie jest zgodna z niektórymi z tych poprawek i nie będzie już obsługiwana.\nRadzimy uaktualnić system operacyjny, aby w pełni wykorzystać jego potencjał."; "version_check_modal_title_supported" = "Kończymy wsparcie dla iOS %@"; "version_check_banner_subtitle_deprecated" = "Nie obsługujemy już %@u na iOS %@. Aby nadal korzystać z pełnego potencjału %@u, radzimy uaktualnić swoją wersję systemu iOS."; "version_check_banner_title_deprecated" = "Nie obsługujemy już iOS %@"; @@ -1563,7 +1557,6 @@ /* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ "analytics_prompt_terms_new_user" = "Tutaj możesz przeczytać nasze warunki użytkowania %@."; "analytics_prompt_message_upgrade" = "Wcześniej dostaliśmy Twoją zgodę na używanie zanonimizowanych danych na temat użytkowania. Teraz, by zrozumieć jak użytkownicy korzystają z wielu urządzeń, wygenerujemy identyfikator używany przez nie wszystkie."; -"analytics_prompt_message_new_user" = "Pomóż nam zidentyfikować problemy i poprawić @% udostępniając zanonimizowane dane o użytkowaniu. W celu zrozumienia jak użytkownicy korzystają z wielu urządzeń wygenerujemy identyfikator używany przez nie wszystkie."; // Analytics "analytics_prompt_title" = "Pomóż ulepszyć %@"; @@ -1646,7 +1639,6 @@ "notice_event_redacted_reason" = " [powód: %@]"; "notice_profile_change_redacted" = "%@ zaktualizował(-a) swój profil %@"; "notice_encrypted_message" = "Wiadomość zaszyfrowana"; -"notice_encryption_enabled" = "%@ włączył(a) szyfrowanie end-to-end (algorytm %@)"; "ssl_only_accept" = "Akceptuj certyfikat TYLKO wtedy gdy administrator opublikował odcisk palca pasujący do tego powyżej."; "ssl_unexpected_existing_expl" = "Certyfikat zmienił stan z zaufanego na niezaufany. Jest to NIEZWYKLE RZADKIE. Zalecane jest NIE AKCEPTOWANIE nowego certyfikatu."; "ssl_cert_not_trust" = "Może to oznaczać że ktoś zakłóca twoje połączenie, lub Twój telefon nie ufa certyfikatowi dostarczonemu przez zdalny serwer."; diff --git a/Riot/Assets/pr.lproj/Localizable.strings b/Riot/Assets/pr.lproj/Localizable.strings index ce12dfa8b..99c008846 100755 --- a/Riot/Assets/pr.lproj/Localizable.strings +++ b/Riot/Assets/pr.lproj/Localizable.strings @@ -39,7 +39,6 @@ /** Image Messages **/ /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ برای شما یک تصویر ارسال کرد %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ یک تصویر پست کرد %@ in %@"; diff --git a/Riot/Assets/pr.lproj/Vector.strings b/Riot/Assets/pr.lproj/Vector.strings index edf98a855..40dcce762 100755 --- a/Riot/Assets/pr.lproj/Vector.strings +++ b/Riot/Assets/pr.lproj/Vector.strings @@ -1,640 +1,632 @@ -/* - Copyright 2019 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. - */ - -// Titles -"title_home" = "خانه"; -"title_favourites" = "علاقه مندی ها"; -"title_people" = "افراد"; -"title_rooms" ="گروه"; -"title_groups" = "ارتباطات"; -"warning" = "خطا"; - -// Actions -"view" = "نمایش"; -"next" = "بعدی"; -"back" = "قبلی"; -"continue" = "ادامه"; -"create" = "ساختن"; -"start" = "شروع"; -"leave" = " ترک کردن"; -"remove" = "حذف"; -"invite" = "دعوت"; -"retry" = "مجدد"; -"on" = "روشن"; -"off" = "خاموش"; -"cancel" = "انصراف"; -"save" = "ذخیره"; -"join" = "پیوستن"; -"decline" = "کاهش"; -"accept" = "قبول"; -"preview" = "نمایش"; -"camera" = "دوربین"; -"voice" = "صدا"; -"video" = "فیلم"; -"active_call" = "تماس فعال"; -"active_call_details" = "تماس فعال (%@)"; -"later" = "بعدا"; -"rename" = "تغییر نام"; -"collapse" = "بستن"; -"send_to" = "ارسال به %@"; -"sending" = "ارسال... "; - -// Authentication -"auth_login" = "ورود"; -"auth_register" = "ثبت نام"; -"auth_submit" = "تایید"; -"auth_skip" = "رد شدن"; -"auth_send_reset_email" = "ارسال ایمیل بازیابی"; -"auth_return_to_login" = "بازگشت به صفحه ی ورود"; -"auth_user_id_placeholder" = "ایمیل یا نام کاربری"; -"auth_password_placeholder" = "گذرواژه"; -"auth_new_password_placeholder" = "گذرواژه جدید"; -"auth_user_name_placeholder" = "نام کاربری "; -"auth_optional_email_placeholder" = "ادرس ایمیل(اختیاری) "; -"auth_email_placeholder" = "آدرس ایمیل"; -"auth_optional_phone_placeholder" = "شماره تلفن(اختیاری) "; -"auth_phone_placeholder" = "شماره تلفن"; -"auth_repeat_password_placeholder" = "تکرار گذرواژه"; -"auth_repeat_new_password_placeholder" = "تایید گذرواژه"; -"auth_home_server_placeholder" = "URL (e.g. https://matrix.org)"; -"auth_identity_server_placeholder" = "URL (e.g. https://matrix.org)"; -"auth_invalid_login_param" = "گذرواژه و نام کاربری اشتباه است"; -"auth_invalid_user_name" = "نام کاربری وارد شده فرمت صحیح ندارد"; -"auth_invalid_password" = "گذرواژه عبور کوتاه است(حداقل 6 کارکتر) "; -"auth_invalid_email" = "ایمیل معتبر نیست"; -"auth_invalid_phone" = "شماره تلفن معتبر نیست "; -"auth_missing_password" = "گذرواژه را وارد کنید"; -"auth_add_email_message" = "وارد کردن ایمیل باعث بازگردانی گذرواژه می شود."; -"auth_add_phone_message" = "اضافه کردن شماره تلفن باعث پیدا شدن شما توسط کاربران می شود."; -"auth_add_email_phone_message" = "وارد کردن ایمیل باعث بازگردانی گذرواژه می شود."; -"auth_add_email_and_phone_message" = "اضافه کردن شماره تلفن باعث پیدا شدن شما توسط کاربران می شود."; -"auth_missing_email" = "ایمیل وارد نشده است"; -"auth_missing_phone" = "شماره تلفن وارد نشده است"; -"auth_missing_email_or_phone" = "شماره تلفن/ایمیل وارد نشده است"; -"auth_email_in_use" = "ایمیل قبلا ثبت نام شده است"; -"auth_phone_in_use" = "شماره تلفن قبلا ثبت نام شده است"; -"auth_untrusted_id_server" = "شماره شناسایی سرور قابل اعتماد نیست"; -"auth_password_dont_match" = "گذرواژهها یکی نیست"; -"auth_username_in_use" = "نام کاربری قبلا اشغال شده است"; -"auth_forgot_password" = "فراموشی گذرواژه؟ "; -"auth_email_not_found" = "ارسال ایمیل ناموفق بود، ایمیل معتبر نیست."; -"auth_use_server_options" = "استفاده از سرور دلخواه"; -"auth_email_validation_message" = "لطفا ایمیل را برای ثبت نام بررسی کنید"; -"auth_msisdn_validation_title" = "منتظر تایید... "; -"auth_msisdn_validation_message" = "کد پیامکی ارسال شد، لطفا کد را وارد کنید."; -"auth_msisdn_validation_error" = "تایید شماره تلفن ناموفق بود"; -"auth_recaptcha_message" = "لطفا ربات نبودن را تایید کنید."; -"auth_reset_password_message" = "To reset your password, enter the email address linked to your account:"; -"auth_reset_password_missing_email" = "The email address linked to your account must be entered."; -"auth_reset_password_missing_password" = "A new password must be entered."; -"auth_reset_password_email_validation_message" = "An email has been sent to %@. Once you've followed the link it contains, click below."; -"auth_reset_password_next_step_button" = "I have verified my email address"; -"auth_reset_password_error_unauthorized" = "Failed to verify email address: make sure you clicked the link in the email"; -"auth_reset_password_error_not_found" = "Your email address does not appear to be associated with a Matrix ID on this homeserver."; -"auth_reset_password_success_message" = "Your password has been reset.\n\nYou have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, re-log in on each device."; -"auth_add_email_and_phone_warning" = "Registration with email and phone number at once is not supported yet until the api exists. Only the phone number will be taken into account. You may add your email to your profile in settings."; -"auth_accept_policies" = "Please review and accept the policies of this homeserver:"; - -// Chat creation -"room_creation_title" = "گفتگو جدید"; -"room_creation_account" = "حساب"; -"room_creation_appearance" = "ظاهر"; -"room_creation_appearance_name" = "نام"; -"room_creation_appearance_picture" = "تصویر گفتگو(اختیاری) "; -"room_creation_privacy" = "حریم خصوصی"; -"room_creation_private_room" = "این گفتگو خصوصی است"; -"room_creation_public_room" = "این گفتگو عمومی است"; -"room_creation_make_public" = "عمومی کردن"; -"room_creation_make_public_prompt_title" = "آیا می خواهید این گفتگو را عمومی کنید؟"; -"room_creation_make_public_prompt_msg" = "از عمومی کردن گفتگو مطمئن هستید؟ همه می توانند گفتگوها را ببینند. "; -"room_creation_keep_private" = "خصوصی نگه داشتن"; -"room_creation_make_private" = "خصوصی کردن"; -"room_creation_wait_for_creation" = "یک گفتگ ایجاد شد لطفا صبر کنید."; -"room_creation_invite_another_user" = "جستجو یا دعوت بر اساس نام کاربری، ایمیل یا تلفن. "; - -// Room recents -"room_recents_directory_section" = "فهرست گفتگو"; -"room_recents_directory_section_network" = "شبکه"; -"room_recents_favourites_section" = "علاقه مندی ها"; -"room_recents_people_section" = "افراد"; -"room_recents_conversations_section" = "گفتگوها"; -"room_recents_no_conversation" = "بدون گفتگو"; -"room_recents_low_priority_section" = "اولویت پایین"; -"room_recents_server_notice_section" = "هشدارهای سیستم"; -"room_recents_invites_section" = "دعوت ها"; -"room_recents_start_chat_with" = "شروع گفتگو"; -"room_recents_create_empty_room" = "ساخت گفتگو"; -"room_recents_join_room" = "عضو شدن در گفتگو"; -"room_recents_join_room_title" = "عضو شدن در گفتگو"; -"room_recents_join_room_prompt" = "شناسه یا نام گفتگو را وارد کنید"; - -// People tab -"people_invites_section" = "دعوت ها"; -"people_conversation_section" = "گفتگوها"; -"people_no_conversation" = "بدون گفتگو"; - -// Rooms tab -"room_directory_no_public_room" = "گفتگوی عمومی موجود نیست"; - -// Groups tab -"group_invite_section" = "دعوت ها"; -"group_section" = "انجمن ها"; - -// Search -"search_rooms" = "گفتگوها"; -"search_messages" = "پیام ها"; -"search_people" = "افراد"; -"search_files" = "فایل ها"; -"search_default_placeholder" = "جستجو"; -"search_people_placeholder" = "جستجو با شناسه یا نام کاربری"; -"search_no_result" = "بدون نتیجه"; -"search_in_progress" = "در حال جستجو... "; - -// Directory -"directory_cell_title" = "باز کردن مسیر"; -"directory_cell_description" = "%tu گفتگو"; -"directory_search_results_title" = "باز کردن نتایج مسیر"; -"directory_search_results" = "%tu نتایج برای %@"; -"directory_search_results_more_than" = ">%tu نتایج پیدا شده برای %@"; -"directory_searching_title" = "جستجوی مسیر... "; -"directory_search_fail" = "گرفتن داده ها ناموفق بود. "; - -// Contacts -"contacts_address_book_section" = "مخاطبین محلی"; -"contacts_address_book_matrix_users_toggle" = "کاربران ماتریسی"; -"contacts_address_book_no_contact" = "بدون مخاطب محلی"; -"contacts_address_book_permission_required" = "برای دسترسی به مخاطبین محلی مجوز نیاز است."; -"contacts_address_book_permission_denied" = "شما دسترسی به برنامه را نداده اید."; -"contacts_user_directory_section" = "مسیر کاربر"; -"contacts_user_directory_offline_section" = "مسیر کاربر(آفلاین) "; - -// Chat participants -"room_participants_title" = "شرکت کنندگان"; -"room_participants_add_participant" = "اضافه کردن شرکت کننده"; -"room_participants_one_participant" = "1 شرکت کننده"; -"room_participants_multi_participants" = "%d شرکت کننده"; -"room_participants_leave_prompt_title" = "ترک گفتگو"; -"room_participants_leave_prompt_msg" = "آیا می خواهید از گفتگو خارج شوید؟"; -"room_participants_remove_prompt_title" = "تاییده"; -"room_participants_remove_prompt_msg" = "آیا می خواهید %@ را از این گفتگو حذف کنید؟"; -"room_participants_remove_third_party_invite_msg" = "گزینه ی حذف برای این api فعال نیست."; -"room_participants_invite_prompt_title" = "تاییده"; -"room_participants_invite_prompt_msg" = "آیا می خواهید %@ به این چت اضافه کنید"; -"room_participants_filter_room_members" = "فیلتر کردن اعضای گفتگو"; -"room_participants_invite_another_user" = "جستجو یا دعوت با شناسه یا ایمیل"; -"room_participants_invite_malformed_id_title" = "خطای دعوت"; -"room_participants_invite_malformed_id" = "باید ایمیل یا شناسه ماتریس وارد کنید."; -"room_participants_invited_section" = "دعوت شده"; - -"room_participants_online" = "آنلاین"; -"room_participants_offline" = "آفلاین"; -"room_participants_unknown" = "ناشناخته"; -"room_participants_idle" = "بی کار"; -"room_participants_now" = "حال"; -"room_participants_ago" = "قبل"; - -"room_participants_action_section_admin_tools" = "ابزارهای مدیر"; -"room_participants_action_section_direct_chats" = "گفتگوهای مستقیم"; -"room_participants_action_section_devices" = "دستگاه ها"; -"room_participants_action_section_other" = "دیگر"; - -"room_participants_action_invite" = "دعوت"; -"room_participants_action_leave" = "خروج از گفتگو"; -"room_participants_action_remove" = "حذف از این گفتگو"; -"room_participants_action_ban" = "ممنوع کردن"; -"room_participants_action_unban" = "رفع ممنوعیت"; -"room_participants_action_ignore" = "مخفی کردن همه ی پیام های این کاربر"; -"room_participants_action_unignore" = "نمایش همه ی پیام های این کاربر"; -"room_participants_action_set_default_power_level" = "بازیابی به کاربر عادی"; -"room_participants_action_set_moderator" = "مدیر کردن"; -"room_participants_action_set_admin" = "مدیر کردن"; -"room_participants_action_start_new_chat" = "شروع گفتگو جدید"; -"room_participants_action_start_voice_call" = "شروع تماس جدید"; -"room_participants_action_start_video_call" = "شروع تماس تصویری جدید"; -"room_participants_action_mention" = "نام بردن"; - -// Chat -"room_jump_to_first_unread" = "رفتن به اولین پیام خوانده نشده"; -"room_new_message_notification" = "%d پیام جدید"; -"room_new_messages_notification" = "%d پیام جدید"; -"room_one_user_is_typing" = "%@ در حال نوشتن... "; -"room_two_users_are_typing" = "%@ & %@ در حال نوشتن... "; -"room_many_users_are_typing" = "%@, %@ و بقیه در حال نوشتن"; -"room_message_placeholder" = "ارسال پیام (بدون رمزنگاری) "; -"room_message_reply_to_placeholder" = "ارسال پاسخ(بدون رمزنگاری) "; -"room_do_not_have_permission_to_post" = "شما مجاز به پست کردن در این گفتگو نیستید"; -"encrypted_room_message_placeholder" = "ارسال یک پیام رمزنگاری شده... "; -"encrypted_room_message_reply_to_placeholder" = "ارسال یک پاسخ رمزنگاری شده... "; -"room_message_short_placeholder" = "ارسال یک پیام... "; -"room_message_reply_to_short_placeholder" = "ارسال یک پاسخ... "; -"room_offline_notification" = "اتصال به سرور از دست رفت."; -"room_unsent_messages_notification" = "پیام ارسال نشد %@ یا %@ هم اکنون"; -"room_unsent_messages_unknown_devices_notification" = "پیام ارسال نشد %@ یا %@ هم اکنون"; -"room_ongoing_conference_call" = "رفتن به تماس کنفرانسی به عنوان %@ یا %@."; -"room_ongoing_conference_call_with_close" = "رفتن به تماس کنفرانسی %@ یا %@. %@ آن."; -"room_ongoing_conference_call_close" = "بستن"; -"room_conference_call_no_power" = "برای مدیریت تماس کنفرانسی نیاز به مجوز دارید."; -"room_prompt_resend" = "ارسال مجدد همه "; -"room_prompt_cancel" = "لغو همه "; -"room_resend_unsent_messages" = "ارسال مجدد پیام های ارسال نشده"; -"room_delete_unsent_messages" = "حذف پیام های ارسال نشده"; -"room_event_action_copy" = "کپی"; -"room_event_action_quote" = "نقل قول"; -"room_event_action_redact" = "Redact"; -"room_event_action_more" = "بیشتر"; -"room_event_action_share" = "اشتراک"; -"room_event_action_permalink" = "پیوند"; -"room_event_action_view_source" = "منابع"; -"room_event_action_view_decrypted_source" = "منبع رمز شده"; -"room_event_action_report" = "گزارش محتوا"; -"room_event_action_report_prompt_reason" = "دلیل گزارش این محتوا"; -"room_event_action_kick_prompt_reason" = "دلیل ممنوع کردن این کاربر"; -"room_event_action_ban_prompt_reason" = "دلیل ممنوع کردن این کاربر"; -"room_event_action_report_prompt_ignore_user" = "آیا می خواهید همه ی پیام های این کاربر را مخفی کنید؟"; -"room_event_action_save" = "ذخیره"; -"room_event_action_resend" = "ارسال مجدد"; -"room_event_action_delete" = "حذف"; -"room_event_action_cancel_send" = "لغو ارسال"; -"room_event_action_cancel_download" = "لغو دانلود"; -"room_event_action_view_encryption" = "اطلاعات رمزنگاری"; -"room_warning_about_encryption" = "رمزنگاری دوطرفه موجود نیست\n\n برای امن کردن داده باید اطمینان کنید\n\nدستگاه نمی تواند رمزنگاری کند.\n\n"; -"room_event_failed_to_send" = "خطا در ارسال"; -"room_action_send_photo_or_video" = "ارسال عکس یا فیلم"; -"room_action_send_sticker" = "ارسال استیکر"; -"room_replacement_information" = "این گفتگو جایگزین شده و دیگر موجود نیست"; -"room_replacement_link" = "گفتگو اینجا ادامه پیدا میکند"; -"room_predecessor_information" = "این گفتگو ادامه ی یک گفتگوی دیگر است"; -"room_predecessor_link" = "برای دیدن پیام های قدیمی کلیک کنید"; -"room_resource_limit_exceeded_message_contact_1" = " لطفا "; -"room_resource_limit_exceeded_message_contact_2_link" = "با مدیر خود تماس بگیرید"; -"room_resource_limit_exceeded_message_contact_3" = "برای ادامه ی استفاده از این سرویس"; -"room_resource_usage_limit_reached_message_1_default" = "یکی از منابع این سرور محدود است"; -"room_resource_usage_limit_reached_message_1_monthly_active_user" = "این سرور دارای محدود ماهیانه است "; -"room_resource_usage_limit_reached_message_2" = "بعضی از کاربران قادر به ورود نیستند"; -"room_resource_usage_limit_reached_message_contact_3" = " برای افزایش این محدودیت"; - -// Unknown devices -"unknown_devices_alert_title" = "گفتگو شامل دستگاه های ناشناخته است"; -"unknown_devices_alert" = "این گفتگو شامل دستگاه های ناشناخته است که فعال نشده اند"; -"unknown_devices_send_anyway" = "ارسال"; -"unknown_devices_call_anyway" = "تماس"; -"unknown_devices_answer_anyway" = "پاسخ"; -"unknown_devices_verify" = "تایید... "; -"unknown_devices_title" = "دستگاه های ناشناخته"; - -// Room Title -"room_title_new_room" = "گفتگو جدید"; -"room_title_multiple_active_members" = "%@/%@ کاربر فعال"; -"room_title_one_active_member" = "%@/%@ کاربر فعال"; -"room_title_invite_members" = "دعوت کاربر"; -"room_title_members" = "%@ کاربر"; -"room_title_one_member" = "یک کاربر"; - -// Room Preview -"room_preview_invitation_format" = "شما به این گفتگو دعوت شدید توسط: %@"; -"room_preview_subtitle" = "این یک نمایش از گفتگو است"; -"room_preview_unlinked_email_warning" = "این دعوت به %@, ارسال شده است که کاربری آن موجود نیست."; -"room_preview_try_join_an_unknown_room" = "شما قصد دسترسی به %@را دارید. آیا مطمئن هستید؟"; -"room_preview_try_join_an_unknown_room_default" = "یک گفتگو"; - -// Settings -"settings_title" = "تنظیمات"; -"account_logout_all" = "خارج کردن همه ی کاربران"; - -"settings_config_no_build_info" = "بدون شماره ساخت"; -"settings_mark_all_as_read" = "همه پیام ها خوانده شود"; -"settings_report_bug" = "گزارش خطا"; -"settings_clear_cache" = "پاک کردن کش"; -"settings_config_home_server" = "سرور : %@"; -"settings_config_identity_server" = "سرور شناسایی: %@"; -"settings_config_user_id" = "وارد شده به عنوان: %@"; - -"settings_user_settings" = "تنظیمات کاربری"; -"settings_notifications_settings" = "اعلان ها"; -"settings_calls_settings" = "تماس ها"; -"settings_user_interface" = "رابط کاربری"; -"settings_ignored_users" = "کاربران آگاه"; -"settings_contacts" = "کاربران محلی"; -"settings_advanced" = "پیشرفته"; -"settings_other" = "دیگر"; -"settings_labs" = "آزمایشگاه ها"; -"settings_flair" = "نمایش مجاز است"; -"settings_devices" = "دستگاه ها"; -"settings_cryptography" = "رمزنگاری"; -"settings_deactivate_account" = "غیر فعال کردن کاربری"; - -"settings_sign_out" = "خروج"; -"settings_sign_out_confirmation" = "آیا مطمئن هستید"; -"settings_sign_out_e2e_warn" = "شما کلید رمزنگاری را از دست می دهید و دیگر به پیام های قدیمی رمزنگاری شده دسترسی ندارید"; -"settings_profile_picture" = "عکس پروفایل"; -"settings_display_name" = "نام نمایش داده شده"; -"settings_first_name" = "نام"; -"settings_surname" = "نام خانوادگی"; -"settings_remove_prompt_title" = "تاییده"; -"settings_remove_email_prompt_msg" = "آیاا می خواهید ایمیل را حذف کنید %@?"; -"settings_remove_phone_prompt_msg" = "آیا می خواهید این شماره تلفن را حذف کنید؟ %@?"; -"settings_email_address" = "ایمیل"; -"settings_email_address_placeholder" = "ایمیل را وارد کنید"; -"settings_add_email_address" = "اضافه کردن ایمیل"; -"settings_phone_number" = "تلفن"; -"settings_add_phone_number" = "اضافه کردن تلفن"; -"settings_change_password" = "تغییر گذر واژه"; -"settings_night_mode" = "حالت شب"; -"settings_fail_to_update_profile" = "بروزرسانی ناموفق! "; - -"settings_enable_push_notif" = "اعلان ها بر روی دستگاه"; -"settings_show_decrypted_content" = "نمایش محتوای رمزگشایی شده"; -"settings_global_settings_info" = "تنظیمات اعلان های عمومی در %@ کلاینت وب شما موجود است"; -"settings_pin_rooms_with_missed_notif" = "پین کردن گفتگوها با اعلان از دست رفته"; -"settings_pin_rooms_with_unread" = "پین کردن گفتگوها با پیام های خوانده نشده"; -"settings_on_denied_notification" = "اعلان ها برای %@ محدود شده است, لطفا در تنظیمات تلفن ان ها را مجاز کنید. "; -//"settings_enable_all_notif" = "فعال سازی همه ی اعلان ها"; -//"settings_messages_my_display_name" = "پیام شامل نام نمایشی من است"; -//"settings_messages_my_user_name" = "پیام شاما نام کاربری من است. "; -//"settings_messages_sent_to_me" = "پیام های ارسال شده به من "; -//"settings_invited_to_room" = "زمانی که به یک گفتگو دعوت شدم"; -//"settings_join_leave_rooms" = "زمانی که افراد به گفتگو اضافه یا کم شدند"; -//"settings_call_invitations" = "صدا زدن دعوت ها"; - -"settings_enable_callkit" = "تماس گیری مجتمع"; -"settings_callkit_info" = "دریافت تماس ها در صفحه ی قفل، اگر فضای ابری فعال باشد تاریخچه با اپل به اشتراک گذاشته می شود."; -"settings_ui_language" = "زبان"; -"settings_ui_theme" = "تم"; -"settings_ui_theme_auto" = "خودکار"; -"settings_ui_theme_light" = "قالب روشن"; -"settings_ui_theme_dark" = "قالب تیره"; -"settings_ui_theme_black" = "قالب سیاه"; -"settings_ui_theme_picker_title" = "تم"; -"settings_ui_theme_picker_message" = "\"خودکار\" از تنظیمات \"Invert Colours\" تلفن شما استفاده میکند"; - -"settings_unignore_user" = "نمایش همه ی پیام های %@?"; - -"settings_contacts_discover_matrix_users" = "استفاده از ایمیل و تلفن برای جستجو افراد"; -"settings_contacts_phonebook_country" = "دفترچه تلفن"; - -"settings_labs_e2e_encryption" = "End-to-End Encryption"; -"settings_labs_e2e_encryption_prompt_message" = "برای اتمام رمزنگاری یکبار دیگر باید وارد شوید."; -"settings_labs_room_members_lazy_loading" = "اعضای گفتگوی lazy loading"; -"settings_labs_room_members_lazy_loading_error_message" = "سرور شما lazy loading را پشتیبانی نمی کند"; -"settings_labs_create_conference_with_jitsi" = "ایجاد تماس کنفرانسی با jitsi"; - -"settings_version" = "نسخه %@"; -"settings_olm_version" = "نسخه olm %@"; -"settings_copyright" = "کپی رایت"; -"settings_copyright_url" = "https://riot.im/copyright"; -"settings_term_conditions" = "شرایط استفاده"; -"settings_term_conditions_url" = "https://riot.im/tac_apple"; -"settings_privacy_policy" = "سیاست های حریم خصوصی"; -"settings_privacy_policy_url" = "https://riot.im/privacy"; -"settings_third_party_notices" = "Third-party اطلاعیه های"; -"settings_send_crash_report" = "ارسال اطلاعات توقف و یا داده ها"; -"settings_enable_rageshake" = "تکان دادن برای ارسال گزارش"; - -"settings_old_password" = "گذرواژه قدیمی"; -"settings_new_password" = "گذرواژه جدید"; -"settings_confirm_password" = "تایید گذرواژه"; -"settings_fail_to_update_password" = "بروزرسانی گذرواژه موفق نبود"; -"settings_password_updated" = "گذرواژه شما بروز شد"; - -"settings_crypto_device_name" = "نام دستگاه: "; -"settings_crypto_device_id" = "شناسه دستگاه"; -"settings_crypto_device_key" = "کلید دستگاه: "; -"settings_crypto_export" = "ذخیره ی کلیدهای گفتو"; -"settings_crypto_blacklist_unverified_devices" = "رمزنگاری برای تایید دستگاه ها"; - -"settings_deactivate_my_account" = "غیرفعال کردن حساب من"; - -// Room Details -"room_details_title" = "اطلاعات گفتگو"; -"room_details_people" = "افراد"; -"room_details_files" = "فایل ها"; -"room_details_settings" = "تنظیمات"; -"room_details_photo" = "تصویر گفتگو"; -"room_details_room_name" = "نام گفتگو"; -"room_details_topic" = "موضوع"; -"room_details_favourite_tag" = "علاقه مندی ها"; -"room_details_low_priority_tag" = "اولویت پایین"; -"room_details_mute_notifs" = "بی صدا کردن اعلان"; -"room_details_direct_chat" = "گفتگو مستقیم"; -"room_details_access_section"="چه کسی به این گفتگو دسترسی داشته باشد؟"; -"room_details_access_section_invited_only"="فقط افرادی که دعوت شده اند."; -"room_details_access_section_anyone_apart_from_guest"="هرکسی که لینک گفتگو را دارد به جز مهمان ها"; -"room_details_access_section_anyone"="هرکسی که لینک گفتگو را دارد شامل مهمان ها"; -"room_details_access_section_no_address_warning" = "گفتگو برای ساخت لینک نیاز به آدرس دارد"; -"room_details_access_section_directory_toggle"="لیست کردن گفتگو در دایرکتوری گفتگو"; -"room_details_history_section"="چه کسی بتواند تاریخچه را بخواند"; -"room_details_history_section_anyone"="هرکسی"; -"room_details_history_section_members_only"="کاربران(تا زمانی که این گزینه انتخاب شده است) "; -"room_details_history_section_members_only_since_invited"="کاربران(تا زمانی که دعوت شده باشند) "; -"room_details_history_section_members_only_since_joined"="کاربران(تا زمانی که عضو شده باشند) "; -"room_details_history_section_prompt_title" = "هشدار حریم خصوصی"; -"room_details_history_section_prompt_msg" = "تغییر مشاهده ی تاریخچه از زمان انتخاب این گزینه انجام می شود. "; -"room_details_addresses_section"="آدرس ها"; -"room_details_no_local_addresses" = "این گفتگو آدرس محلی ندارد"; -"room_details_new_address" = "اضافه کردن آدرس جدید"; -"room_details_new_address_placeholder" = "اضافه کردن آدرس جدید (مانند %@)"; -"room_details_addresses_invalid_address_prompt_title" = "فرمت نام مستعار معتبر نیست"; -"room_details_addresses_invalid_address_prompt_msg" = "%@ فرمت نام مستعار معتبر نیست"; -"room_details_addresses_disable_main_address_prompt_title" = "هشدار آدرس جدید"; -"room_details_addresses_disable_main_address_prompt_msg"="شما آدرس ندارید. آدرس این گفتگو به صورت تصادفی انتخاب می شود. "; -"room_details_flair_section"="نمایش برای انجمن"; -"room_details_new_flair_placeholder" = "اضافه کردن آدرس جدید انجمن (مانند %@)"; -"room_details_flair_invalid_id_prompt_title" = "فرمت غیرمعتبر"; -"room_details_flair_invalid_id_prompt_msg" = "%@ برای شناسه ی انجمن معتبر نیست"; -"room_details_banned_users_section"="کاربران مسدود شده"; -"room_details_advanced_section"="پیشرفته"; -"room_details_advanced_room_id"="شناسه ی گفتگو: "; -"room_details_advanced_enable_e2e_encryption"="فعال سازی رمزنگاری(هشدار دیگر نمایش داده نمی شود) "; -"room_details_advanced_e2e_encryption_enabled"="رمزنگاری در این گفتگو فعال است."; -"room_details_advanced_e2e_encryption_disabled"="رمزنگاری در این گفتگو فعال نیست. "; -"room_details_advanced_e2e_encryption_blacklist_unverified_devices"="رمزنگاری برای تایید دستگاه ها"; -"room_details_advanced_e2e_encryption_prompt_message"="رمزنگاری قابل اعتماد نیست. "; -"room_details_fail_to_update_avatar" = "خطا در بروزرسانی تصویر گفتگو"; -"room_details_fail_to_update_room_name" = "خطا در بروزرسانی نام گفتگو"; -"room_details_fail_to_update_topic" = "خطا در بروزرسانی تاپیک"; -"room_details_fail_to_update_room_guest_access" = "خطا در بروزرسانی دسترسی مهمان"; -"room_details_fail_to_update_room_join_rule" = "خطا در بروزرسانی قانون "; -"room_details_fail_to_update_room_directory_visibility" = "خطا در بروزرسانی دایرکتوری گفتگو"; -"room_details_fail_to_update_history_visibility" = "خطا در بروزرسانی تاریخچه گفتگو"; -"room_details_fail_to_add_room_aliases" = "خظا در بروزرسانی آدرس جدید"; -"room_details_fail_to_remove_room_aliases" = "خطا در حذف آدرس گفتگو"; -"room_details_fail_to_update_room_canonical_alias" = "خطا در بروزرسانی آدرس جدید"; -"room_details_fail_to_update_room_communities" = "خطا در بروزرسانی انجمن ها"; -"room_details_fail_to_update_room_direct" = "خطا در بروزرسانی پرچم این گفتگو"; -"room_details_fail_to_enable_encryption" = "خطا در فعال سازی رمزنگاری این گفتگو"; -"room_details_save_changes_prompt" = "آیا می خواهید تغییرات را ذخیره کنید؟"; -"room_details_set_main_address" = "تنظیم به عنوان آدرس اصلی"; -"room_details_unset_main_address" = "تنظیم نشده به عنوان آدرس اصلی"; -"room_details_copy_room_id" = "کپی کردن شناسه گفتگو"; -"room_details_copy_room_address" = "کپی کردن آدرس گفتگو"; -"room_details_copy_room_url" = "کپی کردن URL گفتگو"; - -// Group Details -"group_details_title" = "جزئیات انجمن"; -"group_details_home" = "خانه"; -"group_details_people" = "افراد"; -"group_details_rooms" = "گفتگوها"; - -// Group Home -"group_home_one_member_format" = "یک عضو"; -"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" = "اضافه کردن شرکت کننده"; -"group_participants_leave_prompt_title" = "ترک گروه"; -"group_participants_leave_prompt_msg" = "آیا می خواهید گروه را ترک کنید."; -"group_participants_remove_prompt_title" = "تاییده؟"; -"group_participants_remove_prompt_msg" = "آیا می خواهید %@ را از این گروه حذف کنید؟"; -"group_participants_invite_prompt_title" = "تاییده"; -"group_participants_invite_prompt_msg" = "آیا می خواهید %@ را به این گروه دعوت کنید"; -"group_participants_filter_members" = "فیلتر کردن اعضا انجمن"; -"group_participants_invite_another_user" = "جستجو یا دعوت با شناسه"; -"group_participants_invite_malformed_id_title" = "خطای دعوت"; -"group_participants_invite_malformed_id" = "شناسه ناقص ، شناسه نمونه: '@localpart:domain'"; -"group_participants_invited_section" = "دعوت شده"; - -// Group rooms -"group_rooms_filter_rooms" = "فیلتر کردن انجمن"; - -// Read Receipts -"read_receipts_list" = "خواندن لیست رسید ها"; -"receipt_status_read" = "خواندن: "; - -// Media picker -"media_picker_library" = "کتابخانه"; -"media_picker_select" = "انتخاب"; - -// Directory -"directory_title" = "دایرکتوری"; -"directory_server_picker_title" = "انتخاب دایرکتوری"; -"directory_server_all_rooms" = "تمام گفتگوها %@ سرور"; -"directory_server_all_native_rooms" = "تمام گفتگوهای ماتریسی"; -"directory_server_type_homeserver" = "نوشتن نام برای لیست کردن گفتگو"; -"directory_server_placeholder" = "matrix.org"; - -// Events formatter -"event_formatter_member_updates" = "%tu عضویت تغییر کرد"; -"event_formatter_widget_added" = "%@ ویجت عضو شده توسط : %@"; -"event_formatter_widget_removed" = "%@ ویجت حذف شده توسط: %@"; -"event_formatter_jitsi_widget_added" = "VoIP کنفرانس اضافه شده توسط : %@"; -"event_formatter_jitsi_widget_removed" = "VoIP کنفرانس حذف شده توسط: %@"; -"event_formatter_rerequest_keys_part1_link" = "درخواست مجدد کلیدهای رمزنگاری"; -"event_formatter_rerequest_keys_part2" = " از دستگاه های دیگر"; - -// Others -"or" = "یا"; -"you" = "شما"; -"today" = "امروز"; -"yesterday" = "دیروز"; -"network_offline_prompt" = "اتصال اینترنت غیر فعال می باشد"; -"homeserver_connection_lost" = "نمی تواند به سرور متصل شود. "; -"public_room_section_title" = "گفتگوهای عمومی (در %@):"; -"bug_report_prompt" = "برنامه در اجرای قبلی متوقف شده است. آیا می خواهید گزارش دهید؟"; -"rage_shake_prompt" = "تکران دادن خطا داشته است. آیا می خواهید گزارش دهید؟"; -"do_not_ask_again" = "دوباره سوال نشود."; -"camera_access_not_granted" = "%@ مجوز دسترسی به دوربین را نداردُ اطلاعات حریم خصوصی را تنظیم کنید."; -"large_badge_value_k_format" = "%.1fK"; - -// Call -"call_incoming_voice_prompt" = "تماس ورودی از: %@"; -"call_incoming_video_prompt" = "تماس تصویری از: %@"; -"call_incoming_voice" = "تماس ورودی... "; -"call_incoming_video" = "تماس تصویری ورودی... "; -"call_already_displayed" = "در حال حاضر یک تماس فعال است. "; -"call_jitsi_error" = "خطا در عضویت تماس کنفرانسی"; - -// No VoIP support -"no_voip_title" = "تماس ورودی"; -"no_voip" = "%@ در حال تماس باشماست اما %@ تماس ها را پشتیبانی نمی کند. \nشما می توانید تماس را در دستگاه دیر جواب دهید یا رد کنید. "; - -// Crash report -"google_analytics_use_prompt" = "آیا می خواهید بهبود دهید %@ با استفاده از ارسال گزارش توقف و داده ها"; - -// Crypto -"e2e_enabling_on_app_update" = "نرم افزار رمزنگاری را پشتبانی میکند. \n\nYشما می تواند آن را فعال کنید "; -"e2e_need_log_in_again" = "شما باید کلید را برای این دستگاه بسازید و برای سرور ارسال کنید"; - -// Bug report -"bug_report_title" = "گزارش خطا"; -"bug_report_description" = "لطفا خطا را تشریح کنید. چه کاری انجام می داید؟ انتظار چه کاری را داشتید و چه اتفاقی افتاد؟ "; -"bug_crash_report_title" = "گزارش توقف"; -"bug_crash_report_description" = "لطفا بگوید که چه کاری قبل از توقف انجتم دادید. "; -"bug_report_logs_description" = "برای تشخیص مشکل ، داده ها همراه با گزارش ارسال می شود، اگر می خواهید ارسال نشود تیک را بردارید"; -"bug_report_send_logs" = "ارسال لاگ ها"; -"bug_report_send_screenshot" = "ارسال اسکرین شات"; -"bug_report_progress_zipping" = "جمع اوری لاگ ها"; -"bug_report_progress_uploading" = "بروزرسانی لاگ ها"; -"bug_report_send" = "ارسال"; - -// Widget -"widget_no_power_to_manage" = "شما برای مدیریت ویجت ها به مجوز نیاز دارید."; -"widget_creation_failure" = "ایجاد ویجت با خطا روبه رو شد"; -"widget_sticker_picker_no_stickerpacks_alert" = "شما بسته ی استیکری فعالی ندارید."; -"widget_sticker_picker_no_stickerpacks_alert_add_now" = "آیا می خواهید اضافه کنید؟"; - -// Widget Integration Manager -"widget_integration_need_to_be_able_to_invite" = "شما باید کاربران را برای انجام این کار اضافه کنید."; -"widget_integration_unable_to_create" = "ساخت ویجت موفق نبود"; -"widget_integration_failed_to_send_request" = "خطا در ارسال درخواست"; -"widget_integration_room_not_recognised" = "این گفتگو شناسایی نشد"; -"widget_integration_positive_power_level" = "میزان پاور باید مثبت باشد"; -"widget_integration_must_be_in_room" = "شما در این گفتگو نیستید"; -"widget_integration_no_permission_in_room" = "شما مجوز برای این کار را ندارید."; -"widget_integration_missing_room_id" = "شناسه گفتگو در درخواست نیست"; -"widget_integration_missing_user_id" = "شناسه کاربر در درخواست نیست"; -"widget_integration_room_not_visible" = "گفنگو %@ قابل رویت نیست."; - -// Share extension -"share_extension_auth_prompt" = "در برنامه اصلی وارد شوید تا محتوا را به اشتراک بگذارید."; -"share_extension_failed_to_encrypt" = "خطا در ارسال تنظیمات رمزنگاری را بررسی کنید. "; - -// Room key request dialog -"e2e_room_key_request_title" = "نیاز به کلید رمزنگاری"; -"e2e_room_key_request_message_new_device" = "شما به یک دستگاه جدید نیاز دارید '%@', که نیاز به کلید رمزنگاری دارد"; -"e2e_room_key_request_message" = "دستگاه تایید نشده ی شما '%@' درخواست کلید رمزنگاری را دارد. "; -"e2e_room_key_request_start_verification" = "سروع تاییده.... "; -"e2e_room_key_request_share_without_verifying" = "اشتراک بدون تاییده"; -"e2e_room_key_request_ignore_request" = "قبول درخواست"; - -// GDPR -"gdpr_consent_not_given_alert_message" = "برای ادامه دادن %@ سرور شما باید شرایط و قوانین را مرور و تایید قبول کنید. "; -"gdpr_consent_not_given_alert_review_now_action" = "مرور "; - -// Deactivate account - -"deactivate_account_title" = "غیرفعال کردن حساب"; - -"deactivate_account_informations_part1" = "این باعث حذف حساب شما، شناسه ی شما و ترک تمام گروه ها و گفتوها می شود. "; -"deactivate_account_informations_part2_emphasize" = "این عمل بازگشت ناپذیر است. "; -"deactivate_account_informations_part3" = "\n\nغیرفعال کردن حساب شما "; -"deactivate_account_informations_part4_emphasize" = "باعث فراموش شدن پیام های شما نمی شود "; -"deactivate_account_informations_part5" = "اگر می خواهید پیام های شما فراموش شود تیک را بزنید."; - -"deactivate_account_forget_messages_information_part1" = "لطفا تمام پیام های من را فراموش کن ("; -"deactivate_account_forget_messages_information_part2_emphasize" = "هشار"; -"deactivate_account_forget_messages_information_part3" = ": این باعث می شود که کاربران گفتگوها ها را ناقص ببینند)"; - -"deactivate_account_validate_action" = "غیرفعال سازی حساب"; - -"deactivate_account_password_alert_title" = "غیرفعال سازی حساب"; -"deactivate_account_password_alert_message" = "لطفا گذرواژه را وارد کنید. "; - -// Re-request confirmation dialog -"rerequest_keys_alert_title" = "ارسال درخواست "; -"rerequest_keys_alert_message" = "لطفا برنامه را در دستگاهی دیگر که رمزنگاری دارد فعال کنید. "; +/* + Copyright 2019 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. + */ + +// Titles +"title_home" = "خانه"; +"title_favourites" = "علاقه مندی ها"; +"title_people" = "افراد"; +"title_rooms" ="گروه"; +"title_groups" = "ارتباطات"; +"warning" = "خطا"; + +// Actions +"view" = "نمایش"; +"next" = "بعدی"; +"back" = "قبلی"; +"continue" = "ادامه"; +"create" = "ساختن"; +"start" = "شروع"; +"leave" = " ترک کردن"; +"remove" = "حذف"; +"invite" = "دعوت"; +"retry" = "مجدد"; +"on" = "روشن"; +"off" = "خاموش"; +"cancel" = "انصراف"; +"save" = "ذخیره"; +"join" = "پیوستن"; +"decline" = "کاهش"; +"accept" = "قبول"; +"preview" = "نمایش"; +"camera" = "دوربین"; +"voice" = "صدا"; +"video" = "فیلم"; +"active_call" = "تماس فعال"; +"active_call_details" = "تماس فعال (%@)"; +"later" = "بعدا"; +"rename" = "تغییر نام"; +"collapse" = "بستن"; +"send_to" = "ارسال به %@"; +"sending" = "ارسال... "; + +// Authentication +"auth_login" = "ورود"; +"auth_register" = "ثبت نام"; +"auth_submit" = "تایید"; +"auth_skip" = "رد شدن"; +"auth_send_reset_email" = "ارسال ایمیل بازیابی"; +"auth_return_to_login" = "بازگشت به صفحه ی ورود"; +"auth_user_id_placeholder" = "ایمیل یا نام کاربری"; +"auth_password_placeholder" = "گذرواژه"; +"auth_new_password_placeholder" = "گذرواژه جدید"; +"auth_user_name_placeholder" = "نام کاربری "; +"auth_optional_email_placeholder" = "ادرس ایمیل(اختیاری) "; +"auth_email_placeholder" = "آدرس ایمیل"; +"auth_optional_phone_placeholder" = "شماره تلفن(اختیاری) "; +"auth_phone_placeholder" = "شماره تلفن"; +"auth_repeat_password_placeholder" = "تکرار گذرواژه"; +"auth_repeat_new_password_placeholder" = "تایید گذرواژه"; +"auth_home_server_placeholder" = "URL (e.g. https://matrix.org)"; +"auth_identity_server_placeholder" = "URL (e.g. https://matrix.org)"; +"auth_invalid_login_param" = "گذرواژه و نام کاربری اشتباه است"; +"auth_invalid_user_name" = "نام کاربری وارد شده فرمت صحیح ندارد"; +"auth_invalid_password" = "گذرواژه عبور کوتاه است(حداقل 6 کارکتر) "; +"auth_invalid_email" = "ایمیل معتبر نیست"; +"auth_invalid_phone" = "شماره تلفن معتبر نیست "; +"auth_missing_password" = "گذرواژه را وارد کنید"; +"auth_add_email_message" = "وارد کردن ایمیل باعث بازگردانی گذرواژه می شود."; +"auth_add_phone_message" = "اضافه کردن شماره تلفن باعث پیدا شدن شما توسط کاربران می شود."; +"auth_add_email_phone_message" = "وارد کردن ایمیل باعث بازگردانی گذرواژه می شود."; +"auth_add_email_and_phone_message" = "اضافه کردن شماره تلفن باعث پیدا شدن شما توسط کاربران می شود."; +"auth_missing_email" = "ایمیل وارد نشده است"; +"auth_missing_phone" = "شماره تلفن وارد نشده است"; +"auth_missing_email_or_phone" = "شماره تلفن/ایمیل وارد نشده است"; +"auth_email_in_use" = "ایمیل قبلا ثبت نام شده است"; +"auth_phone_in_use" = "شماره تلفن قبلا ثبت نام شده است"; +"auth_untrusted_id_server" = "شماره شناسایی سرور قابل اعتماد نیست"; +"auth_password_dont_match" = "گذرواژهها یکی نیست"; +"auth_username_in_use" = "نام کاربری قبلا اشغال شده است"; +"auth_forgot_password" = "فراموشی گذرواژه؟ "; +"auth_email_not_found" = "ارسال ایمیل ناموفق بود، ایمیل معتبر نیست."; +"auth_use_server_options" = "استفاده از سرور دلخواه"; +"auth_email_validation_message" = "لطفا ایمیل را برای ثبت نام بررسی کنید"; +"auth_msisdn_validation_title" = "منتظر تایید... "; +"auth_msisdn_validation_message" = "کد پیامکی ارسال شد، لطفا کد را وارد کنید."; +"auth_msisdn_validation_error" = "تایید شماره تلفن ناموفق بود"; +"auth_recaptcha_message" = "لطفا ربات نبودن را تایید کنید."; +"auth_reset_password_message" = "To reset your password, enter the email address linked to your account:"; +"auth_reset_password_missing_email" = "The email address linked to your account must be entered."; +"auth_reset_password_missing_password" = "A new password must be entered."; +"auth_reset_password_email_validation_message" = "An email has been sent to %@. Once you've followed the link it contains, click below."; +"auth_reset_password_next_step_button" = "I have verified my email address"; +"auth_reset_password_error_unauthorized" = "Failed to verify email address: make sure you clicked the link in the email"; +"auth_reset_password_error_not_found" = "Your email address does not appear to be associated with a Matrix ID on this homeserver."; +"auth_reset_password_success_message" = "Your password has been reset.\n\nYou have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, re-log in on each device."; +"auth_add_email_and_phone_warning" = "Registration with email and phone number at once is not supported yet until the api exists. Only the phone number will be taken into account. You may add your email to your profile in settings."; +"auth_accept_policies" = "Please review and accept the policies of this homeserver:"; + +// Chat creation +"room_creation_title" = "گفتگو جدید"; +"room_creation_account" = "حساب"; +"room_creation_appearance" = "ظاهر"; +"room_creation_appearance_name" = "نام"; +"room_creation_appearance_picture" = "تصویر گفتگو(اختیاری) "; +"room_creation_privacy" = "حریم خصوصی"; +"room_creation_private_room" = "این گفتگو خصوصی است"; +"room_creation_public_room" = "این گفتگو عمومی است"; +"room_creation_make_public" = "عمومی کردن"; +"room_creation_make_public_prompt_title" = "آیا می خواهید این گفتگو را عمومی کنید؟"; +"room_creation_make_public_prompt_msg" = "از عمومی کردن گفتگو مطمئن هستید؟ همه می توانند گفتگوها را ببینند. "; +"room_creation_keep_private" = "خصوصی نگه داشتن"; +"room_creation_make_private" = "خصوصی کردن"; +"room_creation_wait_for_creation" = "یک گفتگ ایجاد شد لطفا صبر کنید."; +"room_creation_invite_another_user" = "جستجو یا دعوت بر اساس نام کاربری، ایمیل یا تلفن. "; + +// Room recents +"room_recents_directory_section" = "فهرست گفتگو"; +"room_recents_directory_section_network" = "شبکه"; +"room_recents_favourites_section" = "علاقه مندی ها"; +"room_recents_people_section" = "افراد"; +"room_recents_conversations_section" = "گفتگوها"; +"room_recents_no_conversation" = "بدون گفتگو"; +"room_recents_low_priority_section" = "اولویت پایین"; +"room_recents_server_notice_section" = "هشدارهای سیستم"; +"room_recents_invites_section" = "دعوت ها"; +"room_recents_start_chat_with" = "شروع گفتگو"; +"room_recents_create_empty_room" = "ساخت گفتگو"; +"room_recents_join_room" = "عضو شدن در گفتگو"; +"room_recents_join_room_title" = "عضو شدن در گفتگو"; +"room_recents_join_room_prompt" = "شناسه یا نام گفتگو را وارد کنید"; + +// People tab +"people_invites_section" = "دعوت ها"; +"people_conversation_section" = "گفتگوها"; +"people_no_conversation" = "بدون گفتگو"; + +// Rooms tab +"room_directory_no_public_room" = "گفتگوی عمومی موجود نیست"; + +// Groups tab +"group_invite_section" = "دعوت ها"; +"group_section" = "انجمن ها"; + +// Search +"search_rooms" = "گفتگوها"; +"search_messages" = "پیام ها"; +"search_people" = "افراد"; +"search_files" = "فایل ها"; +"search_default_placeholder" = "جستجو"; +"search_people_placeholder" = "جستجو با شناسه یا نام کاربری"; +"search_no_result" = "بدون نتیجه"; +"search_in_progress" = "در حال جستجو... "; + +// Directory +"directory_cell_title" = "باز کردن مسیر"; +"directory_cell_description" = "%tu گفتگو"; +"directory_search_results_title" = "باز کردن نتایج مسیر"; +"directory_search_results" = "%tu نتایج برای %@"; +"directory_search_results_more_than" = ">%tu نتایج پیدا شده برای %@"; +"directory_searching_title" = "جستجوی مسیر... "; +"directory_search_fail" = "گرفتن داده ها ناموفق بود. "; + +// Contacts +"contacts_address_book_section" = "مخاطبین محلی"; +"contacts_address_book_matrix_users_toggle" = "کاربران ماتریسی"; +"contacts_address_book_no_contact" = "بدون مخاطب محلی"; +"contacts_address_book_permission_required" = "برای دسترسی به مخاطبین محلی مجوز نیاز است."; +"contacts_user_directory_section" = "مسیر کاربر"; +"contacts_user_directory_offline_section" = "مسیر کاربر(آفلاین) "; + +// Chat participants +"room_participants_title" = "شرکت کنندگان"; +"room_participants_add_participant" = "اضافه کردن شرکت کننده"; +"room_participants_one_participant" = "1 شرکت کننده"; +"room_participants_multi_participants" = "%d شرکت کننده"; +"room_participants_leave_prompt_title" = "ترک گفتگو"; +"room_participants_leave_prompt_msg" = "آیا می خواهید از گفتگو خارج شوید؟"; +"room_participants_remove_prompt_title" = "تاییده"; +"room_participants_remove_prompt_msg" = "آیا می خواهید %@ را از این گفتگو حذف کنید؟"; +"room_participants_remove_third_party_invite_msg" = "گزینه ی حذف برای این api فعال نیست."; +"room_participants_invite_prompt_title" = "تاییده"; +"room_participants_invite_prompt_msg" = "آیا می خواهید %@ به این چت اضافه کنید"; +"room_participants_filter_room_members" = "فیلتر کردن اعضای گفتگو"; +"room_participants_invite_another_user" = "جستجو یا دعوت با شناسه یا ایمیل"; +"room_participants_invite_malformed_id_title" = "خطای دعوت"; +"room_participants_invite_malformed_id" = "باید ایمیل یا شناسه ماتریس وارد کنید."; +"room_participants_invited_section" = "دعوت شده"; + +"room_participants_online" = "آنلاین"; +"room_participants_offline" = "آفلاین"; +"room_participants_unknown" = "ناشناخته"; +"room_participants_idle" = "بی کار"; +"room_participants_now" = "حال"; +"room_participants_ago" = "قبل"; + +"room_participants_action_section_admin_tools" = "ابزارهای مدیر"; +"room_participants_action_section_direct_chats" = "گفتگوهای مستقیم"; +"room_participants_action_section_devices" = "دستگاه ها"; +"room_participants_action_section_other" = "دیگر"; + +"room_participants_action_invite" = "دعوت"; +"room_participants_action_leave" = "خروج از گفتگو"; +"room_participants_action_remove" = "حذف از این گفتگو"; +"room_participants_action_ban" = "ممنوع کردن"; +"room_participants_action_unban" = "رفع ممنوعیت"; +"room_participants_action_ignore" = "مخفی کردن همه ی پیام های این کاربر"; +"room_participants_action_unignore" = "نمایش همه ی پیام های این کاربر"; +"room_participants_action_set_default_power_level" = "بازیابی به کاربر عادی"; +"room_participants_action_set_moderator" = "مدیر کردن"; +"room_participants_action_set_admin" = "مدیر کردن"; +"room_participants_action_start_new_chat" = "شروع گفتگو جدید"; +"room_participants_action_start_voice_call" = "شروع تماس جدید"; +"room_participants_action_start_video_call" = "شروع تماس تصویری جدید"; +"room_participants_action_mention" = "نام بردن"; + +// Chat +"room_jump_to_first_unread" = "رفتن به اولین پیام خوانده نشده"; +"room_new_message_notification" = "%d پیام جدید"; +"room_new_messages_notification" = "%d پیام جدید"; +"room_one_user_is_typing" = "%@ در حال نوشتن... "; +"room_two_users_are_typing" = "%@ & %@ در حال نوشتن... "; +"room_many_users_are_typing" = "%@, %@ و بقیه در حال نوشتن"; +"room_message_placeholder" = "ارسال پیام (بدون رمزنگاری) "; +"room_message_reply_to_placeholder" = "ارسال پاسخ(بدون رمزنگاری) "; +"room_do_not_have_permission_to_post" = "شما مجاز به پست کردن در این گفتگو نیستید"; +"encrypted_room_message_placeholder" = "ارسال یک پیام رمزنگاری شده... "; +"encrypted_room_message_reply_to_placeholder" = "ارسال یک پاسخ رمزنگاری شده... "; +"room_message_short_placeholder" = "ارسال یک پیام... "; +"room_message_reply_to_short_placeholder" = "ارسال یک پاسخ... "; +"room_offline_notification" = "اتصال به سرور از دست رفت."; +"room_ongoing_conference_call" = "رفتن به تماس کنفرانسی به عنوان %@ یا %@."; +"room_ongoing_conference_call_with_close" = "رفتن به تماس کنفرانسی %@ یا %@. %@ آن."; +"room_ongoing_conference_call_close" = "بستن"; +"room_conference_call_no_power" = "برای مدیریت تماس کنفرانسی نیاز به مجوز دارید."; +"room_prompt_resend" = "ارسال مجدد همه "; +"room_prompt_cancel" = "لغو همه "; +"room_resend_unsent_messages" = "ارسال مجدد پیام های ارسال نشده"; +"room_delete_unsent_messages" = "حذف پیام های ارسال نشده"; +"room_event_action_copy" = "کپی"; +"room_event_action_quote" = "نقل قول"; +"room_event_action_redact" = "Redact"; +"room_event_action_more" = "بیشتر"; +"room_event_action_share" = "اشتراک"; +"room_event_action_permalink" = "پیوند"; +"room_event_action_view_source" = "منابع"; +"room_event_action_view_decrypted_source" = "منبع رمز شده"; +"room_event_action_report" = "گزارش محتوا"; +"room_event_action_report_prompt_reason" = "دلیل گزارش این محتوا"; +"room_event_action_kick_prompt_reason" = "دلیل ممنوع کردن این کاربر"; +"room_event_action_ban_prompt_reason" = "دلیل ممنوع کردن این کاربر"; +"room_event_action_report_prompt_ignore_user" = "آیا می خواهید همه ی پیام های این کاربر را مخفی کنید؟"; +"room_event_action_save" = "ذخیره"; +"room_event_action_resend" = "ارسال مجدد"; +"room_event_action_delete" = "حذف"; +"room_event_action_cancel_send" = "لغو ارسال"; +"room_event_action_cancel_download" = "لغو دانلود"; +"room_event_action_view_encryption" = "اطلاعات رمزنگاری"; +"room_warning_about_encryption" = "رمزنگاری دوطرفه موجود نیست\n\n برای امن کردن داده باید اطمینان کنید\n\nدستگاه نمی تواند رمزنگاری کند.\n\n"; +"room_event_failed_to_send" = "خطا در ارسال"; +"room_action_send_photo_or_video" = "ارسال عکس یا فیلم"; +"room_action_send_sticker" = "ارسال استیکر"; +"room_replacement_information" = "این گفتگو جایگزین شده و دیگر موجود نیست"; +"room_replacement_link" = "گفتگو اینجا ادامه پیدا میکند"; +"room_predecessor_information" = "این گفتگو ادامه ی یک گفتگوی دیگر است"; +"room_predecessor_link" = "برای دیدن پیام های قدیمی کلیک کنید"; +"room_resource_limit_exceeded_message_contact_1" = " لطفا "; +"room_resource_limit_exceeded_message_contact_2_link" = "با مدیر خود تماس بگیرید"; +"room_resource_limit_exceeded_message_contact_3" = "برای ادامه ی استفاده از این سرویس"; +"room_resource_usage_limit_reached_message_1_default" = "یکی از منابع این سرور محدود است"; +"room_resource_usage_limit_reached_message_1_monthly_active_user" = "این سرور دارای محدود ماهیانه است "; +"room_resource_usage_limit_reached_message_2" = "بعضی از کاربران قادر به ورود نیستند"; +"room_resource_usage_limit_reached_message_contact_3" = " برای افزایش این محدودیت"; + +// Unknown devices +"unknown_devices_alert_title" = "گفتگو شامل دستگاه های ناشناخته است"; +"unknown_devices_alert" = "این گفتگو شامل دستگاه های ناشناخته است که فعال نشده اند"; +"unknown_devices_send_anyway" = "ارسال"; +"unknown_devices_call_anyway" = "تماس"; +"unknown_devices_answer_anyway" = "پاسخ"; +"unknown_devices_verify" = "تایید... "; +"unknown_devices_title" = "دستگاه های ناشناخته"; + +// Room Title +"room_title_new_room" = "گفتگو جدید"; +"room_title_multiple_active_members" = "%@/%@ کاربر فعال"; +"room_title_one_active_member" = "%@/%@ کاربر فعال"; +"room_title_invite_members" = "دعوت کاربر"; +"room_title_members" = "%@ کاربر"; +"room_title_one_member" = "یک کاربر"; + +// Room Preview +"room_preview_invitation_format" = "شما به این گفتگو دعوت شدید توسط: %@"; +"room_preview_subtitle" = "این یک نمایش از گفتگو است"; +"room_preview_unlinked_email_warning" = "این دعوت به %@, ارسال شده است که کاربری آن موجود نیست."; +"room_preview_try_join_an_unknown_room" = "شما قصد دسترسی به %@را دارید. آیا مطمئن هستید؟"; +"room_preview_try_join_an_unknown_room_default" = "یک گفتگو"; + +// Settings +"settings_title" = "تنظیمات"; +"account_logout_all" = "خارج کردن همه ی کاربران"; + +"settings_config_no_build_info" = "بدون شماره ساخت"; +"settings_mark_all_as_read" = "همه پیام ها خوانده شود"; +"settings_report_bug" = "گزارش خطا"; +"settings_clear_cache" = "پاک کردن کش"; +"settings_config_home_server" = "سرور : %@"; +"settings_config_identity_server" = "سرور شناسایی: %@"; +"settings_config_user_id" = "وارد شده به عنوان: %@"; + +"settings_user_settings" = "تنظیمات کاربری"; +"settings_notifications_settings" = "اعلان ها"; +"settings_calls_settings" = "تماس ها"; +"settings_user_interface" = "رابط کاربری"; +"settings_ignored_users" = "کاربران آگاه"; +"settings_contacts" = "کاربران محلی"; +"settings_advanced" = "پیشرفته"; +"settings_other" = "دیگر"; +"settings_labs" = "آزمایشگاه ها"; +"settings_flair" = "نمایش مجاز است"; +"settings_devices" = "دستگاه ها"; +"settings_cryptography" = "رمزنگاری"; +"settings_deactivate_account" = "غیر فعال کردن کاربری"; + +"settings_sign_out" = "خروج"; +"settings_sign_out_confirmation" = "آیا مطمئن هستید"; +"settings_sign_out_e2e_warn" = "شما کلید رمزنگاری را از دست می دهید و دیگر به پیام های قدیمی رمزنگاری شده دسترسی ندارید"; +"settings_profile_picture" = "عکس پروفایل"; +"settings_display_name" = "نام نمایش داده شده"; +"settings_first_name" = "نام"; +"settings_surname" = "نام خانوادگی"; +"settings_remove_prompt_title" = "تاییده"; +"settings_remove_email_prompt_msg" = "آیاا می خواهید ایمیل را حذف کنید %@?"; +"settings_remove_phone_prompt_msg" = "آیا می خواهید این شماره تلفن را حذف کنید؟ %@?"; +"settings_email_address" = "ایمیل"; +"settings_email_address_placeholder" = "ایمیل را وارد کنید"; +"settings_add_email_address" = "اضافه کردن ایمیل"; +"settings_phone_number" = "تلفن"; +"settings_add_phone_number" = "اضافه کردن تلفن"; +"settings_change_password" = "تغییر گذر واژه"; +"settings_night_mode" = "حالت شب"; +"settings_fail_to_update_profile" = "بروزرسانی ناموفق! "; + +"settings_enable_push_notif" = "اعلان ها بر روی دستگاه"; +"settings_show_decrypted_content" = "نمایش محتوای رمزگشایی شده"; +"settings_global_settings_info" = "تنظیمات اعلان های عمومی در %@ کلاینت وب شما موجود است"; +"settings_pin_rooms_with_missed_notif" = "پین کردن گفتگوها با اعلان از دست رفته"; +"settings_pin_rooms_with_unread" = "پین کردن گفتگوها با پیام های خوانده نشده"; +//"settings_enable_all_notif" = "فعال سازی همه ی اعلان ها"; +//"settings_messages_my_display_name" = "پیام شامل نام نمایشی من است"; +//"settings_messages_my_user_name" = "پیام شاما نام کاربری من است. "; +//"settings_messages_sent_to_me" = "پیام های ارسال شده به من "; +//"settings_invited_to_room" = "زمانی که به یک گفتگو دعوت شدم"; +//"settings_join_leave_rooms" = "زمانی که افراد به گفتگو اضافه یا کم شدند"; +//"settings_call_invitations" = "صدا زدن دعوت ها"; + +"settings_enable_callkit" = "تماس گیری مجتمع"; +"settings_ui_language" = "زبان"; +"settings_ui_theme" = "تم"; +"settings_ui_theme_auto" = "خودکار"; +"settings_ui_theme_light" = "قالب روشن"; +"settings_ui_theme_dark" = "قالب تیره"; +"settings_ui_theme_black" = "قالب سیاه"; +"settings_ui_theme_picker_title" = "تم"; +"settings_ui_theme_picker_message" = "\"خودکار\" از تنظیمات \"Invert Colours\" تلفن شما استفاده میکند"; + +"settings_unignore_user" = "نمایش همه ی پیام های %@?"; + +"settings_contacts_discover_matrix_users" = "استفاده از ایمیل و تلفن برای جستجو افراد"; +"settings_contacts_phonebook_country" = "دفترچه تلفن"; + +"settings_labs_e2e_encryption" = "End-to-End Encryption"; +"settings_labs_e2e_encryption_prompt_message" = "برای اتمام رمزنگاری یکبار دیگر باید وارد شوید."; +"settings_labs_room_members_lazy_loading" = "اعضای گفتگوی lazy loading"; +"settings_labs_room_members_lazy_loading_error_message" = "سرور شما lazy loading را پشتیبانی نمی کند"; +"settings_labs_create_conference_with_jitsi" = "ایجاد تماس کنفرانسی با jitsi"; + +"settings_version" = "نسخه %@"; +"settings_olm_version" = "نسخه olm %@"; +"settings_copyright" = "کپی رایت"; +"settings_copyright_url" = "https://riot.im/copyright"; +"settings_term_conditions" = "شرایط استفاده"; +"settings_term_conditions_url" = "https://riot.im/tac_apple"; +"settings_privacy_policy" = "سیاست های حریم خصوصی"; +"settings_privacy_policy_url" = "https://riot.im/privacy"; +"settings_third_party_notices" = "Third-party اطلاعیه های"; +"settings_send_crash_report" = "ارسال اطلاعات توقف و یا داده ها"; +"settings_enable_rageshake" = "تکان دادن برای ارسال گزارش"; + +"settings_old_password" = "گذرواژه قدیمی"; +"settings_new_password" = "گذرواژه جدید"; +"settings_confirm_password" = "تایید گذرواژه"; +"settings_fail_to_update_password" = "بروزرسانی گذرواژه موفق نبود"; +"settings_password_updated" = "گذرواژه شما بروز شد"; + +"settings_crypto_device_name" = "نام دستگاه: "; +"settings_crypto_device_id" = "شناسه دستگاه"; +"settings_crypto_device_key" = "کلید دستگاه: "; +"settings_crypto_export" = "ذخیره ی کلیدهای گفتو"; +"settings_crypto_blacklist_unverified_devices" = "رمزنگاری برای تایید دستگاه ها"; + +"settings_deactivate_my_account" = "غیرفعال کردن حساب من"; + +// Room Details +"room_details_title" = "اطلاعات گفتگو"; +"room_details_people" = "افراد"; +"room_details_files" = "فایل ها"; +"room_details_settings" = "تنظیمات"; +"room_details_photo" = "تصویر گفتگو"; +"room_details_room_name" = "نام گفتگو"; +"room_details_topic" = "موضوع"; +"room_details_favourite_tag" = "علاقه مندی ها"; +"room_details_low_priority_tag" = "اولویت پایین"; +"room_details_mute_notifs" = "بی صدا کردن اعلان"; +"room_details_direct_chat" = "گفتگو مستقیم"; +"room_details_access_section"="چه کسی به این گفتگو دسترسی داشته باشد؟"; +"room_details_access_section_invited_only"="فقط افرادی که دعوت شده اند."; +"room_details_access_section_anyone_apart_from_guest"="هرکسی که لینک گفتگو را دارد به جز مهمان ها"; +"room_details_access_section_anyone"="هرکسی که لینک گفتگو را دارد شامل مهمان ها"; +"room_details_access_section_no_address_warning" = "گفتگو برای ساخت لینک نیاز به آدرس دارد"; +"room_details_access_section_directory_toggle"="لیست کردن گفتگو در دایرکتوری گفتگو"; +"room_details_history_section"="چه کسی بتواند تاریخچه را بخواند"; +"room_details_history_section_anyone"="هرکسی"; +"room_details_history_section_members_only"="کاربران(تا زمانی که این گزینه انتخاب شده است) "; +"room_details_history_section_members_only_since_invited"="کاربران(تا زمانی که دعوت شده باشند) "; +"room_details_history_section_members_only_since_joined"="کاربران(تا زمانی که عضو شده باشند) "; +"room_details_history_section_prompt_title" = "هشدار حریم خصوصی"; +"room_details_history_section_prompt_msg" = "تغییر مشاهده ی تاریخچه از زمان انتخاب این گزینه انجام می شود. "; +"room_details_addresses_section"="آدرس ها"; +"room_details_no_local_addresses" = "این گفتگو آدرس محلی ندارد"; +"room_details_new_address" = "اضافه کردن آدرس جدید"; +"room_details_new_address_placeholder" = "اضافه کردن آدرس جدید (مانند %@)"; +"room_details_addresses_invalid_address_prompt_title" = "فرمت نام مستعار معتبر نیست"; +"room_details_addresses_invalid_address_prompt_msg" = "%@ فرمت نام مستعار معتبر نیست"; +"room_details_addresses_disable_main_address_prompt_title" = "هشدار آدرس جدید"; +"room_details_addresses_disable_main_address_prompt_msg"="شما آدرس ندارید. آدرس این گفتگو به صورت تصادفی انتخاب می شود. "; +"room_details_flair_section"="نمایش برای انجمن"; +"room_details_new_flair_placeholder" = "اضافه کردن آدرس جدید انجمن (مانند %@)"; +"room_details_flair_invalid_id_prompt_title" = "فرمت غیرمعتبر"; +"room_details_flair_invalid_id_prompt_msg" = "%@ برای شناسه ی انجمن معتبر نیست"; +"room_details_banned_users_section"="کاربران مسدود شده"; +"room_details_advanced_section"="پیشرفته"; +"room_details_advanced_room_id"="شناسه ی گفتگو: "; +"room_details_advanced_enable_e2e_encryption"="فعال سازی رمزنگاری(هشدار دیگر نمایش داده نمی شود) "; +"room_details_advanced_e2e_encryption_enabled"="رمزنگاری در این گفتگو فعال است."; +"room_details_advanced_e2e_encryption_disabled"="رمزنگاری در این گفتگو فعال نیست. "; +"room_details_advanced_e2e_encryption_blacklist_unverified_devices"="رمزنگاری برای تایید دستگاه ها"; +"room_details_advanced_e2e_encryption_prompt_message"="رمزنگاری قابل اعتماد نیست. "; +"room_details_fail_to_update_avatar" = "خطا در بروزرسانی تصویر گفتگو"; +"room_details_fail_to_update_room_name" = "خطا در بروزرسانی نام گفتگو"; +"room_details_fail_to_update_topic" = "خطا در بروزرسانی تاپیک"; +"room_details_fail_to_update_room_guest_access" = "خطا در بروزرسانی دسترسی مهمان"; +"room_details_fail_to_update_room_join_rule" = "خطا در بروزرسانی قانون "; +"room_details_fail_to_update_room_directory_visibility" = "خطا در بروزرسانی دایرکتوری گفتگو"; +"room_details_fail_to_update_history_visibility" = "خطا در بروزرسانی تاریخچه گفتگو"; +"room_details_fail_to_add_room_aliases" = "خظا در بروزرسانی آدرس جدید"; +"room_details_fail_to_remove_room_aliases" = "خطا در حذف آدرس گفتگو"; +"room_details_fail_to_update_room_canonical_alias" = "خطا در بروزرسانی آدرس جدید"; +"room_details_fail_to_update_room_communities" = "خطا در بروزرسانی انجمن ها"; +"room_details_fail_to_update_room_direct" = "خطا در بروزرسانی پرچم این گفتگو"; +"room_details_fail_to_enable_encryption" = "خطا در فعال سازی رمزنگاری این گفتگو"; +"room_details_save_changes_prompt" = "آیا می خواهید تغییرات را ذخیره کنید؟"; +"room_details_set_main_address" = "تنظیم به عنوان آدرس اصلی"; +"room_details_unset_main_address" = "تنظیم نشده به عنوان آدرس اصلی"; +"room_details_copy_room_id" = "کپی کردن شناسه گفتگو"; +"room_details_copy_room_address" = "کپی کردن آدرس گفتگو"; +"room_details_copy_room_url" = "کپی کردن URL گفتگو"; + +// Group Details +"group_details_title" = "جزئیات انجمن"; +"group_details_home" = "خانه"; +"group_details_people" = "افراد"; +"group_details_rooms" = "گفتگوها"; + +// Group Home +"group_home_one_member_format" = "یک عضو"; +"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" = "اضافه کردن شرکت کننده"; +"group_participants_leave_prompt_title" = "ترک گروه"; +"group_participants_leave_prompt_msg" = "آیا می خواهید گروه را ترک کنید."; +"group_participants_remove_prompt_title" = "تاییده؟"; +"group_participants_remove_prompt_msg" = "آیا می خواهید %@ را از این گروه حذف کنید؟"; +"group_participants_invite_prompt_title" = "تاییده"; +"group_participants_invite_prompt_msg" = "آیا می خواهید %@ را به این گروه دعوت کنید"; +"group_participants_filter_members" = "فیلتر کردن اعضا انجمن"; +"group_participants_invite_another_user" = "جستجو یا دعوت با شناسه"; +"group_participants_invite_malformed_id_title" = "خطای دعوت"; +"group_participants_invite_malformed_id" = "شناسه ناقص ، شناسه نمونه: '@localpart:domain'"; +"group_participants_invited_section" = "دعوت شده"; + +// Group rooms +"group_rooms_filter_rooms" = "فیلتر کردن انجمن"; + +// Read Receipts +"read_receipts_list" = "خواندن لیست رسید ها"; +"receipt_status_read" = "خواندن: "; + +// Media picker +"media_picker_library" = "کتابخانه"; +"media_picker_select" = "انتخاب"; + +// Directory +"directory_title" = "دایرکتوری"; +"directory_server_picker_title" = "انتخاب دایرکتوری"; +"directory_server_all_rooms" = "تمام گفتگوها %@ سرور"; +"directory_server_all_native_rooms" = "تمام گفتگوهای ماتریسی"; +"directory_server_type_homeserver" = "نوشتن نام برای لیست کردن گفتگو"; +"directory_server_placeholder" = "matrix.org"; + +// Events formatter +"event_formatter_member_updates" = "%tu عضویت تغییر کرد"; +"event_formatter_widget_added" = "%@ ویجت عضو شده توسط : %@"; +"event_formatter_widget_removed" = "%@ ویجت حذف شده توسط: %@"; +"event_formatter_jitsi_widget_added" = "VoIP کنفرانس اضافه شده توسط : %@"; +"event_formatter_jitsi_widget_removed" = "VoIP کنفرانس حذف شده توسط: %@"; +"event_formatter_rerequest_keys_part1_link" = "درخواست مجدد کلیدهای رمزنگاری"; +"event_formatter_rerequest_keys_part2" = " از دستگاه های دیگر"; + +// Others +"or" = "یا"; +"you" = "شما"; +"today" = "امروز"; +"yesterday" = "دیروز"; +"network_offline_prompt" = "اتصال اینترنت غیر فعال می باشد"; +"homeserver_connection_lost" = "نمی تواند به سرور متصل شود. "; +"public_room_section_title" = "گفتگوهای عمومی (در %@):"; +"bug_report_prompt" = "برنامه در اجرای قبلی متوقف شده است. آیا می خواهید گزارش دهید؟"; +"rage_shake_prompt" = "تکران دادن خطا داشته است. آیا می خواهید گزارش دهید؟"; +"do_not_ask_again" = "دوباره سوال نشود."; +"camera_access_not_granted" = "%@ مجوز دسترسی به دوربین را نداردُ اطلاعات حریم خصوصی را تنظیم کنید."; +"large_badge_value_k_format" = "%.1fK"; + +// Call +"call_incoming_voice_prompt" = "تماس ورودی از: %@"; +"call_incoming_video_prompt" = "تماس تصویری از: %@"; +"call_incoming_voice" = "تماس ورودی... "; +"call_incoming_video" = "تماس تصویری ورودی... "; +"call_already_displayed" = "در حال حاضر یک تماس فعال است. "; +"call_jitsi_error" = "خطا در عضویت تماس کنفرانسی"; + +// No VoIP support +"no_voip_title" = "تماس ورودی"; +"no_voip" = "%@ در حال تماس باشماست اما %@ تماس ها را پشتیبانی نمی کند. \nشما می توانید تماس را در دستگاه دیر جواب دهید یا رد کنید. "; + +// Crash report + +// Crypto +"e2e_need_log_in_again" = "شما باید کلید را برای این دستگاه بسازید و برای سرور ارسال کنید"; + +// Bug report +"bug_report_title" = "گزارش خطا"; +"bug_report_description" = "لطفا خطا را تشریح کنید. چه کاری انجام می داید؟ انتظار چه کاری را داشتید و چه اتفاقی افتاد؟ "; +"bug_crash_report_title" = "گزارش توقف"; +"bug_crash_report_description" = "لطفا بگوید که چه کاری قبل از توقف انجتم دادید. "; +"bug_report_logs_description" = "برای تشخیص مشکل ، داده ها همراه با گزارش ارسال می شود، اگر می خواهید ارسال نشود تیک را بردارید"; +"bug_report_send_logs" = "ارسال لاگ ها"; +"bug_report_send_screenshot" = "ارسال اسکرین شات"; +"bug_report_progress_zipping" = "جمع اوری لاگ ها"; +"bug_report_progress_uploading" = "بروزرسانی لاگ ها"; +"bug_report_send" = "ارسال"; + +// Widget +"widget_no_power_to_manage" = "شما برای مدیریت ویجت ها به مجوز نیاز دارید."; +"widget_creation_failure" = "ایجاد ویجت با خطا روبه رو شد"; +"widget_sticker_picker_no_stickerpacks_alert" = "شما بسته ی استیکری فعالی ندارید."; +"widget_sticker_picker_no_stickerpacks_alert_add_now" = "آیا می خواهید اضافه کنید؟"; + +// Widget Integration Manager +"widget_integration_need_to_be_able_to_invite" = "شما باید کاربران را برای انجام این کار اضافه کنید."; +"widget_integration_unable_to_create" = "ساخت ویجت موفق نبود"; +"widget_integration_failed_to_send_request" = "خطا در ارسال درخواست"; +"widget_integration_room_not_recognised" = "این گفتگو شناسایی نشد"; +"widget_integration_positive_power_level" = "میزان پاور باید مثبت باشد"; +"widget_integration_must_be_in_room" = "شما در این گفتگو نیستید"; +"widget_integration_no_permission_in_room" = "شما مجوز برای این کار را ندارید."; +"widget_integration_missing_room_id" = "شناسه گفتگو در درخواست نیست"; +"widget_integration_missing_user_id" = "شناسه کاربر در درخواست نیست"; +"widget_integration_room_not_visible" = "گفنگو %@ قابل رویت نیست."; + +// Share extension +"share_extension_auth_prompt" = "در برنامه اصلی وارد شوید تا محتوا را به اشتراک بگذارید."; +"share_extension_failed_to_encrypt" = "خطا در ارسال تنظیمات رمزنگاری را بررسی کنید. "; + +// Room key request dialog +"e2e_room_key_request_title" = "نیاز به کلید رمزنگاری"; +"e2e_room_key_request_message_new_device" = "شما به یک دستگاه جدید نیاز دارید '%@', که نیاز به کلید رمزنگاری دارد"; +"e2e_room_key_request_message" = "دستگاه تایید نشده ی شما '%@' درخواست کلید رمزنگاری را دارد. "; +"e2e_room_key_request_start_verification" = "سروع تاییده.... "; +"e2e_room_key_request_share_without_verifying" = "اشتراک بدون تاییده"; +"e2e_room_key_request_ignore_request" = "قبول درخواست"; + +// GDPR +"gdpr_consent_not_given_alert_message" = "برای ادامه دادن %@ سرور شما باید شرایط و قوانین را مرور و تایید قبول کنید. "; +"gdpr_consent_not_given_alert_review_now_action" = "مرور "; + +// Deactivate account + +"deactivate_account_title" = "غیرفعال کردن حساب"; + +"deactivate_account_informations_part1" = "این باعث حذف حساب شما، شناسه ی شما و ترک تمام گروه ها و گفتوها می شود. "; +"deactivate_account_informations_part2_emphasize" = "این عمل بازگشت ناپذیر است. "; +"deactivate_account_informations_part3" = "\n\nغیرفعال کردن حساب شما "; +"deactivate_account_informations_part4_emphasize" = "باعث فراموش شدن پیام های شما نمی شود "; +"deactivate_account_informations_part5" = "اگر می خواهید پیام های شما فراموش شود تیک را بزنید."; + +"deactivate_account_forget_messages_information_part1" = "لطفا تمام پیام های من را فراموش کن ("; +"deactivate_account_forget_messages_information_part2_emphasize" = "هشار"; +"deactivate_account_forget_messages_information_part3" = ": این باعث می شود که کاربران گفتگوها ها را ناقص ببینند)"; + +"deactivate_account_validate_action" = "غیرفعال سازی حساب"; + +"deactivate_account_password_alert_title" = "غیرفعال سازی حساب"; +"deactivate_account_password_alert_message" = "لطفا گذرواژه را وارد کنید. "; + +// Re-request confirmation dialog +"rerequest_keys_alert_title" = "ارسال درخواست "; diff --git a/Riot/Assets/pt.lproj/InfoPlist.strings b/Riot/Assets/pt.lproj/InfoPlist.strings index 9cf939453..f67ec78e2 100644 --- a/Riot/Assets/pt.lproj/InfoPlist.strings +++ b/Riot/Assets/pt.lproj/InfoPlist.strings @@ -5,3 +5,6 @@ "NSPhotoLibraryUsageDescription" = "A biblioteca de fotos é usada para enviar fotos e vídeos."; // Permissions usage explanations "NSCameraUsageDescription" = "A câmara é usada para tirar fotos e vídeos e fazer videochamadas."; +"NSLocationWhenInUseUsageDescription" = "Quando partilha a sua localização com outros, o Element necessita de acesso para lhes mostrar um mapa."; +"NSFaceIDUsageDescription" = "O Face ID é usado para aceder à sua app."; +"NSCalendarsUsageDescription" = "Veja as suas reuniões na app."; diff --git a/Riot/Assets/pt.lproj/Vector.strings b/Riot/Assets/pt.lproj/Vector.strings index e3dbb85af..9300274dc 100644 --- a/Riot/Assets/pt.lproj/Vector.strings +++ b/Riot/Assets/pt.lproj/Vector.strings @@ -5,3 +5,93 @@ + +"onboarding_splash_page_2_title" = "Está no controlo."; +"onboarding_splash_page_1_message" = "Comunicação segura e independente que lhe confere o mesmo nível de privacidade que uma conversa cara-a-cara na sua própria casa."; +"onboarding_splash_page_1_title" = "Seja dono das suas conversas."; +"onboarding_splash_login_button_title" = "Já tenho uma conta"; + +// Onboarding +"onboarding_splash_register_button_title" = "Criar conta"; +"accessibility_button_label" = "botão"; + +// Accessibility +"accessibility_checkbox_label" = "caixa de seleção"; +"callbar_only_single_active_group" = "Toque para aderir à chamada de grupo (%@)"; +"callbar_return" = "Regressar"; +"callbar_only_multiple_paused" = "%@ chamadas em pausa"; +"callbar_only_single_paused" = "Chamada em pausa"; +"callbar_active_and_multiple_paused" = "1 chamada ativa (%@) · %@ chamadas em pausa"; +"callbar_active_and_single_paused" = "1 chamada ativa (%@) · 1 chamada em pausa"; + +// Call Bar +"callbar_only_single_active" = "Toque para voltar à chamada (%@)"; +"saving" = "A guardar"; +"sending" = "A enviar"; + +// Activities +"loading" = "A carregar"; +"edit" = "Editar"; +"suggest" = "Sugerir"; +"error" = "Erro"; +"ok" = "OK"; +"add" = "Adicionar"; +"existing" = "Existente"; +"new_word" = "Novo"; +"stop" = "Parar"; +"public" = "Público"; +"private" = "Privado"; +"done" = "Concluido"; +"open" = "Abrir"; +"less" = "Menos"; +"more" = "Mais"; +"switch" = "Alterar"; +"store_promotional_text" = "Aplicação de mensagens e colaboração que preserva a sua privacidade. Descentralizada para o pôr em controlo. Sem exploração de dados, backdoors e sem acesso de terceiros."; +"settings_callkit_info" = "Receba chamadas diretamente no seu ecrã de bloqueio. Veja as suas chamadas do %@ no histórico de chamadas do seu sistema. Se o iCloud estiver ativado, o seu histórico de chamadas será partilhado com a Apple."; +"submit_code" = "Submeter código"; +"submit" = "Submeter"; +"store_full_description" = "O Element é um novo tipo de aplicação de mensagens e colaboração que:\n\n1. Põe-no em controlo para preservar a sua privacidade\n2. Permite-lhe comunicar com qualquer pessoa na rede Matrix e vai mais além através da integração com apps como o Slack\n3. Protege-o de anúncios, exploração de dados, backdoors e jardins murados\n4. Mantém-no seguro através de encriptação ponta-a-ponta, com assinaturas cruzadas para verificar os outros.\n\nO Element é completamente diferente das outras aplicações de mensagens e colaboração porque é descentralizado e de código aberto.\n\nO Element permita que o aloje por si próprio - ou escolha um anfitrião - para que tenha privacidade, posse e controlo dos seus dados e das suas conversas. Confere-lhe acesso a uma rede aberta; para que não fique preso apenas aos outros utilizadores do Element, para além de ser muito seguro.\n\nO Element é capaz de fazer tudo isto porque opera no Matrix - o padrão para comunicação aberta e descentralizada.\n\nO Element põe-no no controlo, permitindo-lhe escolher quem aloja as suas conversas. Através da aplicação do Element, pode escolher o alojamento de diferentes formas:\n\n1. Registe uma conta gratuita no servidor público matrix.org\n2. Aloje você mesmo a sua própria conta, correndo um servidor no seu próprio hardware\n3. Registe uma conta num servidor personalizado, bastando-lhe apenas subscrever a plataforma de alojamento Element Matrix Services\n\nPorquê escolher o Element?\n\nSEJA DONO DOS SEUS DADOS: Você decide onde quer manter os seus dados e as suas mensagens. Você é o dono e é quem está no controlo, por oposição a uma MEGACORPORAÇÃO que explora ou permite que terceiros tenham acesso aos seus dados.\n\nMENSAGENS E COLABORAÇÃO ABERTAS: Pode conversar com qualquer pessoa na rede Matrix, quer estejam a usar o Element ou outra app Matrix, e mesmo se estiverem a usar outro sistema de mensagens como o Slack, IRC ou XMPP.\n\nSUPERSEGURO: Encriptação ponta-a-ponta real (apenas os envolvidos na conversa conseguem desencriptar mensagens) e encriptação cruzada para verificar os dispositivos dos participantes na conversa.\n\nCOMUNICAÇÃO COMPLETA: Mensagens, chamadas de voz e vídeo, partilha de ficheiros, partilha de ecrã e mais um data de integrações, bots e widgets. Crie salas e comunidades, mantenha o contacto e realize as suas tarefas.\n\nONDE QUER QUE ESTEJA: Mantenha o contacto onde quer que esteja com a sincronização do histórico de mensagens através de todos os seus dispositivos e na web em https://element.io/app."; +// String for App Store +"store_short_description" = "Chat/VoIP descentralizado seguro"; +"joined" = "Aderido"; +"join" = "Aderir"; +"joining" = "Aderindo"; +"skip" = "Saltar"; +"close" = "Fechar"; +"send_to" = "Enviar para %@"; +"collapse" = "colapsar"; +"rename" = "Mudar o nome"; +"later" = "Mais tarde"; +"active_call_details" = "Chamada Ativa (%@)"; +"active_call" = "Chamada Ativa"; +"video" = "Vídeo"; +"voice" = "Voz"; +"camera" = "Câmera"; +"preview" = "Pré-visualização"; +"accept" = "Aceitar"; +"decline" = "Declinar"; +"save" = "Guardar"; +"cancel" = "Cancelar"; +"enable" = "Ativar"; +"off" = "Desligado"; +"on" = "Ligado"; +"retry" = "Tentar novamente"; +"invite" = "Convidar"; +"remove" = "Remover"; +"leave" = "Sair"; +"start" = "Iniciar"; +"create" = "Criar"; +"continue" = "Continuar"; +"back" = "Anterior"; +"next" = "Próximo"; + +// Actions +"view" = "Ver"; +"warning" = "Aviso"; +"title_groups" = "Comunidades"; +"title_rooms" = "Salas"; +"title_people" = "Pessoas"; +"title_favourites" = "Favoritos"; + +// Titles +"title_home" = "Página inicial"; diff --git a/Riot/Assets/pt_BR.lproj/Localizable.strings b/Riot/Assets/pt_BR.lproj/Localizable.strings index c923ccf92..49f919d5d 100644 --- a/Riot/Assets/pt_BR.lproj/Localizable.strings +++ b/Riot/Assets/pt_BR.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ enviou uma imagem %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ postou uma imagem %@ em %@"; /* A single unread message in a room */ diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index 7ca1ea2d9..67a8561a5 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -495,7 +495,6 @@ "no_voip_title" = "Chamada entrante"; "no_voip" = "%@ está chamando você mas %@ não suporta chamadas ainda.\nVocê pode ignorar esta notificação e atender a chamada de um outro dispositivo ou você pode rejeitá-la."; // Crash report -"google_analytics_use_prompt" = "Você gostaria de ajudar a melhorar %@ ao reportar automaticamente reportes de crash e dados de uso anônimos?"; // Crypto "e2e_enabling_on_app_update" = "%@ agora suporta encriptação ponta-a-ponta mas você precisa fazer login de novo para habilitá-la.\n\nVocê pode fazê-lo agora ou mais tarde desde os ajustes do aplicativo."; "e2e_need_log_in_again" = "Você precisa fazer login de volta para gerar chaves de encriptação ponta-a-ponta para esta sessão e submeter a chave pública a seu servidorcasa.\nIsto é só desta vez; desculpe pela inconveniência."; @@ -553,7 +552,7 @@ "deactivate_account_password_alert_message" = "Para continuar, por favor entre a senha de sua conta Matrix"; // Re-request confirmation dialog "rerequest_keys_alert_title" = "Requisição Enviada"; -"rerequest_keys_alert_message" = "Por favor lance %@ num outro dispositivo que possa decriptar a mensagem para que ele possa enviar as chaves para esta sessão."; +"rerequest_keys_alert_message" = "Por favor lance %@ em um outro dispositivo que possa decriptar a mensagem para que ele possa enviar as chaves para esta sessão."; // String for App Store "store_short_description" = "Chat/VoIP descentralizado e seguro"; "store_full_description" = "Element is um novo tipo de mensageiro e app de colaboração que:\n\n1. Põe você em controle para preservar sua privacidade\n2. Permite você se comunicar com qualquer pessoa na rede Matrix, e até além ao integrar-se com apps como Slack\n3. Protege você de publicidade, datamining, backdoors e jardins murados\n4. Assegura você através de encriptação ponta-a-pontam com assinatura cruzada para verificar ouras(os)\n\nElement é completamente diferente de outros apps de mensageria e colaboração porque ele é descentralizado e open source.\n\nElement permite você auto-hospedar - ou escolher um host - para que você tenha privacidade, propriedade e controle de seus dados e conversas. Ele dá a você acesso a uma rede aberta; então você não está simplesmente preso falando com outras(os) usuárias(os) Element somente. E ele é muito seguro.\n\nElement é capaz de fazer tudo isso porque ele opera em Matrix - o padrão para comunicação aberta e descentralizada.\n\nElement põe você em controle ao permitir você escolher quem hospeda suas conversas. De seu app Element, você pode escolher hospedar de diferentes maneiras:\n\n1. Pegar uma conta grátis no servidor público matrix.org\n2. Auto-hospedar sua conta ao rodar um servidor em seu próprio hardware\n3. Registrar-se para uma conta num servidor personalizado ao simplesmente assinar a plataforma de hospedagem Element Matrix Services\n\nPorquê escolher Element?\n\nTENHA POSSE DE SEUS DADOS: Você decide onde manter seus dados e mensagens. Você os possui e controla, não alguma MEGACORP que mina seus dados ou dá acesso a terceiros.\n\nMENSAGERIA E COLABORAÇÃO ABERTOS: Você pode conversar com qualquer outra pessoa na rede Matrix, caso ela esteja usando Element ou um outro app Matrix, e mesmo se ela estiver usando um sistema de mensageria diferente, do tipo de Slack, IRC ou XMPP.\n\nSUPER-SEGURO: Encriptação ponta-a-ponta real (somente aquelas/es na conversa podem decriptar mensagens), e assinatura cruzada para verificar os dispositivos de participantes de conversa.\n\nCOMUNICAÇÃO COMPLETA: Mensageria, chamadas de voz e vídeo, compartilhamento de arquivo, compartilhamento de tela e um monte de integrações, bots e widgets. Construa salas, comunidades, fique em contato e tenha as coisas feitas.\n\nEM TODO LUGAR ONDE VOCÊ ESTEJA: Fique em contato onde quer que você esteja com histórico de mensagem sincronizado por todos os dispositivos e na web em https://element.io/app."; @@ -930,12 +929,10 @@ "room_widget_permission_room_id_permission" = "ID de sala"; // Service terms "service_terms_modal_title" = "Termos De Serviço"; -"service_terms_modal_message" = "Para continuar você precisa aceitar os termos deste serviço (%@)."; "service_terms_modal_accept_button" = "Aceitar"; "service_terms_modal_decline_button" = "Declinar"; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "Descoberta de contatos"; -"service_terms_modal_message_identity_server" = "Aceite os termos do servidor de identidade (%@) para descobrir contatos."; "service_terms_modal_policy_checkbox_accessibility_hint" = "Marque para aceitar %@"; "secure_key_backup_setup_intro_title" = "Backup Seguro"; "secure_key_backup_setup_intro_info" = "Salvaguardar-se contra perda de acesso a mensagens & dados encriptados ao fazer backup de chaves de encriptação em seu servidor."; @@ -1128,7 +1125,7 @@ "cross_signing_setup_banner_title" = "Configurar encriptação"; "cross_signing_setup_banner_subtitle" = "Verifique seus outros dispositivos mais fácil"; "major_update_title" = "Riot agora é %@"; -"major_update_learn_more_action" = "Saiba mais"; +"major_update_learn_more_action" = "Saber mais"; "major_update_done_action" = "Entendido"; "pin_protection_choose_pin" = "Crie um PIN por segurança"; "pin_protection_confirm_pin" = "Confirme seu PIN"; @@ -1178,19 +1175,19 @@ "room_info_list_one_member" = "1 membro"; "create_room_placeholder_address" = "#salateste:matrix.org"; -"create_room_section_header_address" = "Endereço de sala"; -"create_room_show_in_directory" = "Mostrar a sala no diretório"; +"create_room_section_header_address" = "ENDEREÇO"; +"create_room_show_in_directory" = "Mostrar em diretório de sala"; "create_room_section_footer_type" = "Pessoas se juntam a uma sala privada somente com o convite da sala."; -"create_room_type_public" = "Sala Pública"; -"create_room_type_private" = "Sala Privada"; -"create_room_section_header_type" = "Tipo de sala"; +"create_room_type_public" = "Sala Pública (qualquer pessoa)"; +"create_room_type_private" = "Sala Privada (convite somente)"; +"create_room_section_header_type" = "QUEM PODE ACESSAR"; "create_room_section_footer_encryption" = "Encriptação não pode ser desabilitada em seguida."; "create_room_enable_encryption" = "Habilitar Encriptação"; -"create_room_section_header_encryption" = "Encriptação de sala"; -"create_room_placeholder_topic" = "Tópico"; -"create_room_section_header_topic" = "Tópico de sala (opcional)"; +"create_room_section_header_encryption" = "ENCRIPTAÇÃO"; +"create_room_placeholder_topic" = "Sobre o quê é esta sala?"; +"create_room_section_header_topic" = "TÓPICO (OPCIONAL)"; "create_room_placeholder_name" = "Nome"; -"create_room_section_header_name" = "Nome de sala"; +"create_room_section_header_name" = "NOME"; // MARK: - Create Room @@ -1576,7 +1573,7 @@ "ok" = "OK"; "onboarding_splash_page_1_message" = "Comunicação segura e independente que lhe dá o mesmo nível de privacidade que uma conversa face-a-face em sua própria casa."; "settings_enable_room_message_bubbles" = "Bolhas de mensagem"; -"onboarding_splash_page_4_message" = "Element também é ótimo para o lugar de trabalho. É confiado pelas organizações mais seguras do mundo."; +"onboarding_splash_page_4_message" = "Element também é ótimo para o lugar de trabalho. Ele é confiado pelas organizações mais seguras do mundo."; "onboarding_splash_page_4_title_no_pun" = "Mensageria para seu time."; "onboarding_splash_page_3_message" = "Encriptado ponta-a-ponta e nenhum número de telefone requerido. Sem publicidade ou datamining."; "onboarding_splash_page_3_title" = "Mensageria segura."; @@ -1731,7 +1728,6 @@ "notice_encrypted_message" = "Mensagem encriptada"; "set_power_level" = "Definir Nível de Poder"; "power_level" = "Nível de Poder"; -"notice_encryption_enabled" = "%@ ativou a criptografia de ponta a ponta (algorithm %@)"; "notice_image_attachment" = "anexo de imagem"; "notice_audio_attachment" = "anexo de áudio"; "notice_video_attachment" = "anexo de vídeo"; @@ -2099,13 +2095,191 @@ "attachment_small_with_resolution" = "Pequeno %@ (~%@)"; "attachment_size_prompt_message" = "Você pode desligar isto em ajustes."; "attachment_size_prompt_title" = "Confirmar tamanho para enviar"; -"room_displayname_all_other_participants_left" = "%@ (Saiu)"; "room_displayname_all_other_members_left" = "%@ (Saiu)"; "attachment_unsupported_preview_message" = "Este tipo de arquivo não é suportado."; "attachment_unsupported_preview_title" = "Incapaz de previsualizar"; "message_reply_to_sender_sent_their_location" = "tem compartilhado a localização dela(e)."; -"home_syncing" = "Sincando"; +"home_syncing" = "Sincronizando"; "room_participants_leave_success" = "Saiu de sala"; "room_participants_leave_processing" = "Saindo"; "notice_error_unformattable_event" = "** Incapaz de render mensagem. Por favor reporte um bug"; "settings_labs_use_only_latest_user_avatar_and_name" = "Mostrar avatar e nomes mais recentes para usuárias(os) em histórico de mensagem"; +"ignore_user" = "Ignorar Usuária(o)"; +"location_sharing_pin_drop_share_title" = "Enviar esta localização"; +"location_sharing_static_share_title" = "Enviar minha localização atual"; +"live_location_sharing_banner_stop" = "Parar"; +"live_location_sharing_banner_title" = "Localização ao vivo habilitada"; + +// MARK: Live location sharing + +"location_sharing_live_share_title" = "Compartilhar localização ao vivo"; +"side_menu_coach_message" = "Deslize para a direita ou toque para ver todas as salas"; +"spaces_add_room_missing_permission_message" = "Você não tem permissões para adicionar salas a este espaço."; +"spaces_creation_in_one_space" = "em 1 espaço"; +"spaces_creation_in_many_spaces" = "em %@ espaços"; +"spaces_creation_in_spacename_plus_many" = "em %@ + %@ espaços"; +"spaces_creation_in_spacename_plus_one" = "em %@ + 1 espaço"; +"spaces_creation_in_spacename" = "em %@"; +"spaces_creation_post_process_inviting_users" = "Convidando %@ usuárias(os)"; +"spaces_creation_post_process_adding_rooms" = "Adicionando %@ salas"; +"spaces_creation_post_process_creating_room" = "Criando %@"; +"spaces_creation_post_process_uploading_avatar" = "Fazendo upload de avatar"; +"spaces_creation_post_process_creating_space_task" = "Criando %@"; +"spaces_creation_post_process_creating_space" = "Criando espaço"; +"spaces_creation_invite_by_username_message" = "Você pode convidá-las(os) mais tarde também."; +"spaces_creation_invite_by_username_title" = "Convide seu time"; +"spaces_creation_invite_by_username" = "Convide por nome de usuária(o)"; +"spaces_creation_add_rooms_message" = "Como este espaço é apenas para você, ninguém vai ser informado. Você pode adicionar outras mais tarde."; +"spaces_creation_add_rooms_title" = "O que você quer adicionar?"; +"spaces_creation_sharing_type_me_and_teammates_detail" = "Um espaço privado para você e suas/seus colegas de time"; +"spaces_creation_sharing_type_me_and_teammates_title" = "Eu e minhas/meus colegas de time"; +"spaces_creation_sharing_type_just_me_detail" = "Um espaço privado para organizar suas salas"; +"spaces_creation_sharing_type_just_me_title" = "Apenas eu"; +"spaces_creation_sharing_type_message" = "Assegure-se que as pessoas certas têm acesso %@. Você pode mudar isto mais tarde."; +"spaces_creation_sharing_type_title" = "Com quem você está trabalhando?"; +"spaces_creation_email_invites_email_title" = "Email"; +"spaces_creation_email_invites_message" = "Você pode convidá-las(os) mais tarde também."; +"spaces_creation_email_invites_title" = "Convide seu time"; +"spaces_creation_new_rooms_support" = "Suporte"; +"spaces_creation_new_rooms_random" = "Aleatório"; +"spaces_creation_new_rooms_general" = "Geral"; +"spaces_creation_new_rooms_room_name_title" = "Nome de sala"; +"spaces_creation_new_rooms_message" = "Nós vamos criar uma sala para cada uma."; +"spaces_creation_new_rooms_title" = "Quais são algumas discussões que você vai ter?"; +"spaces_creation_cancel_message" = "Seu progresso vai ser perdido."; +"spaces_creation_cancel_title" = "Parar de criar um espaço?"; +"spaces_creation_private_space_title" = "Seu espaço privado"; +"spaces_creation_public_space_title" = "Seu espaço público"; +"spaces_creation_address_already_exists" = "%@\njá existe"; +"spaces_creation_address_invalid_characters" = "%@\ntem caracteres inválidos"; +"spaces_creation_address_default_message" = "Seu espaço vai ser visualizável em\n%@"; +"spaces_creation_empty_room_name_error" = "Nome requerido"; +"spaces_creation_address" = "Endereço"; +"spaces_creation_settings_message" = "Adicione alguns detalhes para ajudá-lo a se destacar. Você pode mudar estes a qualquer ponto."; +"spaces_creation_footer" = "Você pode mudar isto mais tarde"; +"spaces_creation_visibility_message" = "Para se juntar a um espaço existente, você precisa de um convite."; +"spaces_creation_visibility_title" = "Que tipo de espaço você quer criar?"; + +// Mark: - Space Creation + +"spaces_creation_hint" = "Espaços são uma nova maneira de agrupar salas e pessoas."; +"space_settings_current_address_message" = "Seu espaço está visível em\n%@"; +"space_settings_update_failed_message" = "Falha para atualizar definições de espaço. Você quer retentar?"; +"space_settings_access_section" = "Quem pode acessar este espaço?"; +"space_topic" = "Descrição"; +"space_public_join_rule_detail" = "Aberto para qualquer pessoa, melhor para comunidades"; +"spaces_add_space" = "Adicionar espaço"; +"spaces_add_room" = "Adicionar sala"; +"spaces_invite_people" = "Convidar pessoas"; +"space_private_join_rule_detail" = "Convite somente, melhor para você mesma(o) ou times"; +"spaces_explore_rooms_one_room" = "1 sala"; +"spaces_explore_rooms_room_number" = "%@ salas"; +"spaces_create_space_title" = "Criar um espaço"; +"spaces_add_space_title" = "Criar espaço"; +"space_invite_not_enough_permission" = "Você não tem permissão para convidar pessoas para este espaço"; +"room_invite_not_enough_permission" = "Você não tem permissão para convidar pessoas para esta sala"; +"room_invite_to_room_option_detail" = "Ela/ele não vai ser uma parte de %@."; +"room_invite_to_room_option_title" = "Para apenas esta sala"; +"room_invite_to_space_option_detail" = "Ela/ele pode explorar %@, mas não vai ser um membro de %@."; + +// Mark: - Room invite + +"room_invite_to_space_option_title" = "Para %@"; +"share_invite_link_space_text" = "Hey, junte-se a este espaço em %@"; +"share_invite_link_room_text" = "Hey, junte-se a esta sala em %@"; + +// MARK: - Share invite link + +"share_invite_link_action" = "Compartilhar link de convite"; +"create_room_processing" = "Criando sala"; +"create_room_suggest_room_footer" = "Salas sugeridas são promovidas a membros de espaço como boas para se juntarem."; +"create_room_suggest_room" = "Sugerir para membros de espaço"; +"create_room_show_in_directory_footer" = "Isto vai ajudar pessoas achar e se juntar."; +"create_room_promotion_header" = "PROMOÇÃO"; +"create_room_section_footer_type_public" = "Somente pessoas convidadas podem achar e se juntar, não apenas pessoas em Nome de espaço."; +"create_room_section_footer_type_restricted" = "Qualquer pessoa em Nome de espaço pode achar e se juntar."; +"create_room_section_footer_type_private" = "Somente pessoas convidadas podem achar e se juntar."; +"create_room_type_restricted" = "Membros de espaço"; +"call_jitsi_unable_to_start" = "Incapaz de começar chamada de conferência"; +"room_suggestion_settings_screen_message" = "Salas sugeridas são promovidas para membros de espaço como boas para se juntarem."; +"room_suggestion_settings_screen_title" = "Fazer uma sala sugerida em um espaço"; + +// Room suggestion Settings +"room_suggestion_settings_screen_nav_title" = "Sugerir sala"; +"room_access_space_chooser_other_spaces_section_info" = "Estas são provavelmente coisas das quais outras(os) admins de %@ são uma parte."; +"room_access_space_chooser_other_spaces_section" = "Outros espaços ou salas"; +"room_access_space_chooser_known_spaces_section" = "Espaços que você conhece contendo %@"; +"room_access_settings_screen_setting_room_access" = "Definindo acesso de sala"; +"room_access_settings_screen_upgrade_alert_upgrading" = "Fazer upgrade de sala"; +"room_access_settings_screen_upgrade_alert_upgrade_button" = "Fazer upgrade"; +"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "Convidar automaticamente membros para nova sala"; +"room_access_settings_screen_public_message" = "Qualquer pessoa pode encontrar e se juntar."; +"room_access_settings_screen_private_message" = "Somente pessoas convidadas podem achar e se juntar."; +"room_access_settings_screen_message" = "Decida quem pode achar e se juntar a %@."; +"room_access_settings_screen_restricted_message" = "Deixar qualquer pessoa em um espaço achar e se juntar.\nVocê vai ser pedida(o) para confirmar quais espaços."; +"room_access_settings_screen_upgrade_alert_title" = "Fazer upgrade de sala"; +"room_access_settings_screen_edit_spaces" = "Editar espaços"; +"room_access_settings_screen_upgrade_required" = "Upgrade requerido"; +"room_access_settings_screen_title" = "Quem pode acessar esta sala?"; + +// Room Access Settings +"room_access_settings_screen_nav_title" = "Acesso de sala"; +"room_details_promote_room_suggest_title" = "Sugerir para membros de espaço"; +"room_details_promote_room_title" = "Promover sala"; +"room_details_access_row_title" = "Acesso"; +"settings_labs_enable_auto_report_decryption_errors" = "Auto Reportar Erros de Decriptação"; +"room_preview_decline_invitation_options" = "Você quer declinar o convite ou ignorar esta(e) usuária(o)?"; +"threads_beta_cancel" = "Não agora"; +"threads_beta_enable" = "Teste aí"; +"threads_beta_information_link" = "Saber mais"; +"threads_beta_information" = "Mantenha discussões organizadas com threads.\n\nThreads ajudam manter suas conversas em-tópico e fáceis de rastrear. "; +"threads_beta_title" = "Threads"; +"threads_notice_done" = "Entendido"; +"threads_notice_information" = "Todas as threads criadas durante o período experimental não vão ser rendidas como respostas regulares.

Isto vai ser uma transição única, visto que threads são agora parte da especificação Matrix."; +"threads_notice_title" = "Threads não mais experimentais 🎉"; +"room_participants_invite_prompt_to_msg" = "Você tem certeza que você quer convidar %@ para %@?"; +"onboarding_celebration_button" = "Vamo lá"; +"onboarding_celebration_message" = "Suas preferências têm sido salvas."; +"onboarding_celebration_title" = "Você está pronta(o)!"; +"onboarding_avatar_accessibility_label" = "Imagem de perfil"; +"onboarding_avatar_message" = "Você pode mudar isto a qualquer hora."; +"onboarding_avatar_title" = "Adicione uma imagem de perfil"; +"onboarding_display_name_max_length" = "Seu nome de exibição deve ser menos que 256 caracteres"; +"onboarding_display_name_hint" = "Você pode mudar isto mais tarde"; +"onboarding_display_name_placeholder" = "Nome de Exibição"; +"onboarding_display_name_message" = "Isto vai ser mostrado quando você enviar mensagens."; +"onboarding_display_name_title" = "Escolha um nome de exibição"; +"onboarding_personalization_skip" = "Pular este passo"; +"onboarding_personalization_save" = "Salvar e continuar"; +"onboarding_congratulations_home_button" = "Me leve para casa"; +"onboarding_congratulations_personalize_button" = "Personalizar perfil"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "Sua conta %@ tem sido criada."; +"onboarding_congratulations_title" = "Parabéns!"; +"saving" = "Salvando"; + +// Activities +"loading" = "Carregando"; +"edit" = "Editar"; +"suggest" = "Sugerir"; +"new_word" = "Nova(o)"; +"add" = "Adicionar"; +"existing" = "Existente"; +"stop" = "Parar"; +"joining" = "Juntando-Se"; +"location_sharing_live_list_item_stop_sharing_action" = "Parar de compartilhar"; +"location_sharing_live_list_item_current_user_display_name" = "Você"; +"location_sharing_live_list_item_last_update_invalid" = "Última atualização desconhecida"; +"location_sharing_live_list_item_last_update" = "Atualizado %@ atrás"; +"location_sharing_live_list_item_sharing_expired" = "Compartilhamento expirado"; +"location_sharing_live_list_item_time_left" = "%@ saiu"; +"location_sharing_live_viewer_title" = "Localização"; +"location_sharing_live_map_callout_title" = "Compartilhar localização"; +"room_access_settings_screen_upgrade_alert_note" = "Por favor note que fazer upgrade vai fazer uma nova versão da sala. Todas as mensagens atuais vão ficar nesta sala arquivada."; +"room_access_settings_screen_upgrade_alert_message_no_param" = "Qualquer pessoa em um espaço pai vai ser capaz de achar e se juntar a esta sala - não precisa manualmente convidar todo mundo. Você vai ser capaz de mudar isto em configurações de sala a qualquer hora."; +"room_access_settings_screen_upgrade_alert_message" = "Qualquer pessoa em %@ vai ser capaz de achar e se juntar a esta sala - não precisa manualmente convidar todo mundo. Você vai ser capaz de mudar isto em configurações de sala a qualquer hora."; +"settings_presence_offline_mode_description" = "Se habilitado, você sempre vai aparecer offline para outras(os) usuários, mesmo quando usando o aplicativo."; +"settings_presence_offline_mode" = "Modo Offline"; +"settings_presence" = "Presença"; +"threads_discourage_information_2" = "\n\nVocê quer habilitar threads mesmo assim?"; +"threads_discourage_information_1" = "Seu servidorcasa não atualmente suporta threads, então esta funcionalidade pode ser inconfiável. Algumas mensagens de thread podem não estar confiavelmente disponíveis. "; diff --git a/Riot/Assets/ru.lproj/Localizable.strings b/Riot/Assets/ru.lproj/Localizable.strings index 6a50f8b90..0ba02d8a1 100644 --- a/Riot/Assets/ru.lproj/Localizable.strings +++ b/Riot/Assets/ru.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ отправил(а) фото %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ отправил(а) фото %@ в %@"; /* Multiple unread messages in a room */ diff --git a/Riot/Assets/ru.lproj/Vector.strings b/Riot/Assets/ru.lproj/Vector.strings index 5897a73b7..e49ff4ca3 100644 --- a/Riot/Assets/ru.lproj/Vector.strings +++ b/Riot/Assets/ru.lproj/Vector.strings @@ -369,7 +369,6 @@ // No VoIP support "no_voip_title" = "Входящий вызов"; // Crash report -"google_analytics_use_prompt" = "Вы хотите помочь улучшить %@, автоматически отправляя анонимные отчеты о сбоях и данные об использовании?"; // Crypto "e2e_enabling_on_app_update" = "%@ теперь поддерживает сквозное шифрование, но вам нужно снова войти в систему, чтобы активировать его.\n\nВы можете сделать это сейчас или позже из настроек приложения."; "e2e_need_log_in_again" = "Вам нужно войти в систему, чтобы сгенерировать ключи шифрования для этого устройства и отправлять публичный ключ вашему серверу.\nЭто необходимо только один раз; просим прощения за неудобства."; @@ -384,8 +383,8 @@ "bug_report_progress_zipping" = "Сбор журналов"; "bug_report_progress_uploading" = "Отправка отчета"; "bug_report_send" = "Отправить"; -"on" = "Включить"; -"off" = "Выключить"; +"on" = "Вкл"; +"off" = "Выкл"; "preview" = "Просмотр"; "room_creation_account" = "Учетная запись"; "directory_searching_title" = "Поиск в каталоге…"; @@ -562,7 +561,6 @@ "settings_key_backup_info_not_valid" = "Этот сеанс не делает резервное копирование ваших ключей, но у вас есть существующая резервная копия, которую вы можете восстановить и добавить в дальнейшем."; "settings_key_backup_info_progress" = "Резервное копирование %@ ключей…"; "settings_key_backup_info_progress_done" = "Все ключи сохранены"; -"settings_key_backup_info_not_trusted_from_verifiable_device_fix_action" = "Для Безопасного Восстановления Сообщений на этом устройстве, проверьте %@."; "settings_key_backup_info_not_trusted_fix_action" = "Для Безопасного Восстановления Сообщений на этом устройстве, требуется пароль или ключ восстановления."; "settings_key_backup_info_trust_signature_unknown" = "Резервное копирование имеет подпись от сеанса со следующим ID: %@"; "settings_key_backup_info_trust_signature_valid" = "Резервная копия имеет действующую подпись этой сессии"; @@ -673,10 +671,6 @@ "auth_autodiscover_invalid_response" = "Неверный ответ обнаружения сервера"; "room_event_action_reply" = "Ответ"; "room_event_action_edit" = "Редактировать"; -"room_event_action_reaction_agree" = "%@ согласен"; -"room_event_action_reaction_disagree" = "%@ несогласен"; -"room_event_action_reaction_like" = "%@ согласен"; -"room_event_action_reaction_dislike" = "%@ не согласны"; "room_action_reply" = "Ответ"; "settings_labs_message_reaction" = "Реагировать на сообщения с Emoji"; "settings_key_backup_button_connect" = "Подключите этот сеанс к резервному копированию ключей"; @@ -703,7 +697,6 @@ "widget_integrations_server_failed_to_connect" = "Не удалось подключиться к серверу интеграции"; // Service terms "service_terms_modal_title" = "Условия пользования"; -"service_terms_modal_message" = "Для продолжения Вам необходимо принять условия данной услуги (%@)."; "service_terms_modal_accept_button" = "Принять"; "service_terms_modal_description_for_identity_server" = "Быть открытыми для других"; "service_terms_modal_description_for_integration_manager" = "Используйте боты, мосты, виджеты и стикеры"; @@ -988,7 +981,6 @@ "room_widget_permission_theme_permission" = "Ваша тема"; "room_widget_permission_widget_id_permission" = "ID виджета"; "room_widget_permission_room_id_permission" = "ID комнаты"; -"service_terms_modal_message_identity_server" = "Примите условия сервера идентификации (%@) для обнаружения контактов."; "service_terms_modal_policy_checkbox_accessibility_hint" = "Отметьте, чтобы принять %@"; "secure_key_backup_setup_intro_title" = "Безопасное резервное копирование"; "secure_key_backup_setup_intro_info" = "Защитите себя от потери доступа к зашифрованным сообщениям и данным, создав резервную копию ключей шифрования на своём сервере."; @@ -1465,7 +1457,6 @@ "space_public_join_rule" = "Публичное пространство"; "space_private_join_rule" = "Приватное пространство"; "space_participants_action_remove" = "Удалить из этого пространства"; -"spaces_coming_soon_detail" = "Эта функция еще не реализована здесь, но она уже на подходе. Пока вы можете сделать это с помощью Element на вашем компьютере."; "spaces_invites_coming_soon_title" = "Приглашения скоро будут"; "spaces_add_rooms_coming_soon_title" = "Добавление комнат скоро будет"; "spaces_coming_soon_title" = "Скоро будет"; @@ -1588,7 +1579,6 @@ "notice_room_power_level_acting_requirement" = "Минимальные уровни доступа пользователя для совершения действия:"; "notice_room_power_level_event_requirement" = "Минимальные уровни доступа, связанные с событиями:"; "notice_encrypted_message" = "Зашифрованное сообщение"; -"notice_encryption_enabled" = "%@ включил(а) сквозное шифрование (алгоритм %@)"; "notice_image_attachment" = "прикрепленное изображение"; "notice_audio_attachment" = "прикрепленное аудио"; "notice_video_attachment" = "прикрепленное видео"; @@ -1609,7 +1599,6 @@ // room display name "room_displayname_empty_room" = "Пустая комната"; "room_displayname_two_members" = "%@ и %@"; -"room_displayname_more_than_two_members" = "%@ и %u другие"; // Settings "settings" = "Настройки"; "settings_enable_inapp_notifications" = "Включить уведомления в приложении"; @@ -1890,7 +1879,6 @@ "notice_room_invite_by_you" = "Вы пригласили %@"; "notice_room_invite_you" = "%@ пригласил Вас"; "notice_room_third_party_invite_by_you" = "Вы отправили приглашение %@ вступить в комнату"; -"notice_room_third_party_registered_invite_by_you" = "Вы приняли приглашение для @%"; "notice_room_third_party_revoked_invite_by_you" = "Вы отозвали приглашение для %@ вступить в комнату"; "notice_room_join_by_you" = "Вы вошли"; "notice_room_leave_by_you" = "Вы вышли"; @@ -1979,3 +1967,19 @@ "attachment_unsupported_preview_message" = "Этот тип файла не поддерживается."; "attachment_unsupported_preview_title" = "Не удалось показать предварительный просмотр"; "message_reply_to_sender_sent_their_location" = "поделились своим местоположением."; + +// Onboarding +"onboarding_splash_register_button_title" = "Создать учетную запись"; +"accessibility_button_label" = "кнопка"; +"saving" = "Сохранение"; + +// Activities +"loading" = "Загрузка"; +"edit" = "Редактировать"; +"suggest" = "Предложить"; +"add" = "Добавить"; +"existing" = "Существующее"; +"new_word" = "Новое"; +"stop" = "Остановить"; +"joining" = "Соединение"; +"enable" = "Включить"; diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index 1c27c3981..2a1df4534 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -9,7 +9,6 @@ "collapse" = "zbaliť"; "rename" = "Premenovať"; "later" = "Neskôr"; -"active_call_details" = "Prebiehajúci hovor (%s)"; "active_call" = "Aktívny hovor"; "video" = "Video"; "voice" = "Hlas"; @@ -109,7 +108,6 @@ "people_invites_section" = "POZVANIA"; "auth_reset_password_error_unauthorized" = "Nepodarilo sa overiť emailovú adresu: Uistite sa, že ste správne klikli na odkaz v emailovej správe"; "auth_reset_password_next_step_button" = "Overil som svoju emailovú adresu"; -"auth_reset_password_email_validation_message" = "Na adresu %s bola odoslaná správa. Po kliknutí na odkaz, ktorý obsahuje, kliknite nižšie."; "auth_reset_password_missing_password" = "Musíte zadať nové heslo."; "auth_reset_password_missing_email" = "Musíte zadať emailovú adresu prepojenú s vašim účtom."; "auth_reset_password_message" = "Ak chcete obnoviť vaše heslo k účtu Matrix, zadajte emailovú adresu prepojenú s vašim účtom:"; @@ -178,7 +176,6 @@ "settings_old_password" = "staré heslo"; "settings_third_party_notices" = "Poznámky tretích strán"; "settings_privacy_policy" = "Zásady ochrany súkromia"; -"settings_version" = "Verzia %s"; "settings_labs_e2e_encryption" = "End-to-End šifrovanie"; "settings_integrations_allow_button" = "Spravovať integrácie"; "settings_room_upgrades" = "Aktualizácia miestnosti"; @@ -267,7 +264,7 @@ "call_transfer_users" = "Používatelia"; "room_info_list_section_other" = "Ostatné"; "create_room_placeholder_address" = "#testroom:matrix.org"; -"create_room_placeholder_topic" = "Téma"; +"create_room_placeholder_topic" = "O čom je táto miestnosť?"; "create_room_placeholder_name" = "Názov"; "biometrics_cant_unlocked_alert_message_retry" = "Skúsiť znovu"; "pin_protection_settings_section_header" = "PIN"; @@ -321,7 +318,7 @@ "device_verification_emoji_key" = "Kľúč"; "device_verification_emoji_lock" = "Zámka"; "device_verification_emoji_scissors" = "Nožnice"; -"device_verification_emoji_paperclip" = "Kancelárska spinka"; +"device_verification_emoji_paperclip" = "Kancelárska sponka"; "device_verification_emoji_pencil" = "Ceruzka"; "device_verification_emoji_book" = "Kniha"; "device_verification_emoji_gift" = "Darček"; @@ -562,9 +559,7 @@ "room_participants_action_section_admin_tools" = "Nástroje správcu"; "room_participants_filter_room_members_for_dm" = "Filtrovať členov"; "room_participants_filter_room_members" = "Filtrovať členov v miestnosti"; -"room_participants_invite_prompt_msg" = "Ste si istí, že chcete pozvať používateľa %s do tejto konverzácie?"; "room_participants_remove_third_party_invite_prompt_msg" = "Ste si istí, že chcete odvolať toto pozvanie?"; -"room_participants_remove_prompt_msg" = "Ste si istí, že chcete používateľa %s odstrániť z tejto konverzácie?"; "room_participants_leave_prompt_msg_for_dm" = "Ste si istí, že chcete odísť?"; "room_participants_leave_prompt_msg" = "Ste si istí, že chcete opustiť miestnosť?"; "room_participants_leave_prompt_title" = "Opustiť miestnosť"; @@ -833,8 +828,6 @@ "settings_config_user_id" = "Prihlásený ako %@"; "settings_mark_all_as_read" = "Označiť všetky správy ako prečítané"; "settings_config_no_build_info" = "Žiadne informácie o zostavení"; -"room_preview_try_join_an_unknown_room" = "Pokúšate sa zobraziť %s. Chcete vstúpiť a pridať sa k diskusii?"; -"room_preview_unlinked_email_warning" = "Toto pozvanie bolo odoslané na emailovú adresu %s, ktorá nie je priradená k tomuto účtu. Môžete sa prihlásiť k inému účtu, alebo pridať túto emailovú adresu do vášho účtu."; "room_preview_subtitle" = "Toto je náhľad do miestnosti. Všetky akcie pre túto miestnosť sú zakázané."; // Room Preview @@ -933,7 +926,6 @@ "widget_menu_open_outside" = "Otvoriť v prehliadači"; "bug_report_background_mode" = "Pokračovať v pozadí"; "e2e_key_backup_wrong_version_button_wasme" = "Bol(a) som to ja"; -"call_no_stun_server_error_use_fallback_button" = "Skúste použiť %s"; "call_incoming_video" = "Prichádzajúci video hovor…"; "event_formatter_group_call_incoming" = "%@ v %@"; "event_formatter_call_active_video" = "Aktívny video hovor"; @@ -990,7 +982,6 @@ "room_unsent_messages_cancel_title" = "Vymazať neodoslané správy"; "room_message_reply_to_short_placeholder" = "Odoslať odpoveď…"; "room_message_short_placeholder" = "Odoslať správu…"; -"room_one_user_is_typing" = "%s píše…"; "room_new_messages_notification" = "%d nových správ"; "room_new_message_notification" = "%d nová správa"; "room_jump_to_first_unread" = "Preskočiť na neprečítanú"; @@ -1013,8 +1004,6 @@ "poll_edit_form_create_poll" = "Vytvoriť anketu"; "version_check_modal_action_title_supported" = "Rozumiem"; "voice_message_lock_screen_placeholder" = "Hlasová správa"; -"voice_message_remaining_recording_time" = "%1$s ostáva"; -"side_menu_app_version" = "Verzia %s"; "side_menu_action_invite_friends" = "Pozvať priateľov"; // Mark: - Side menu @@ -1039,13 +1028,13 @@ // MARK: - Room Info "room_info_list_one_member" = "1 člen"; -"create_room_section_header_address" = "Adresa miestnosti"; -"create_room_type_public" = "Verejná miestnosť"; -"create_room_type_private" = "Súkromná miestnosť"; -"create_room_section_header_type" = "Typ miestnosti"; +"create_room_section_header_address" = "ADRESA"; +"create_room_type_public" = "Verejná miestnosť (ktokoľvek)"; +"create_room_type_private" = "Súkromná miestnosť (len pre pozvaných)"; +"create_room_section_header_type" = "KTO MÁ PRÍSTUP"; "create_room_enable_encryption" = "Povoliť šifrovanie"; -"create_room_section_header_encryption" = "Šifrovanie miestnosti"; -"create_room_section_header_name" = "Názov miestnosti"; +"create_room_section_header_encryption" = "ŠIFROVANIE"; +"create_room_section_header_name" = "NÁZOV"; // MARK: - Create Room @@ -1308,8 +1297,8 @@ // MARK: - Favourites "favourites_empty_view_title" = "Obľúbené miestnosti a ľudia"; -"create_room_show_in_directory" = "Zobraziť miestnosť v adresári"; -"create_room_section_header_topic" = "Téma miestnosti (voliteľné)"; +"create_room_show_in_directory" = "Zobraziť v adresári miestností"; +"create_room_section_header_topic" = "TÉMA (VOLITEĽNÉ)"; "searchable_directory_search_placeholder" = "Meno alebo ID"; // MARK: - Searchable Directory View Controller @@ -1365,7 +1354,7 @@ "file_upload_error_unsupported_file_type_message" = "Nepodporovaný typ súboru."; "key_verification_self_verify_unverified_sessions_alert_message" = "Overte všetky vaše relácie, aby ste si boli istý, že sú vaše správy a účet bezpečné."; "sign_out_non_existing_key_backup_alert_title" = "Ak sa teraz odhlásite, prídete o zašifrované správy"; -"device_verification_emoji_thumbs up" = "Palec hore"; +"device_verification_emoji_thumbs up" = "Palec nahor"; // Device @@ -1475,7 +1464,6 @@ "room_widget_permission_webview_information_title" = "Používaním prijímate cookies od a zdieľate údaje %@:\n"; "room_widget_permission_creator_info_title" = "Tento widget pridal:"; "widget_integration_manager_disabled" = "V nastaveniach je potrebné povoliť správcu integrácie"; -"widget_integration_room_not_visible" = "Miestnosť %s nie je viditeľná."; "widget_integration_missing_user_id" = "V požiadavke chýba user_id."; "widget_integration_missing_room_id" = "V požiadavke chýba room_id."; "widget_integration_no_permission_in_room" = "V tejto miestnosti na to nemáte povolenie."; @@ -1519,7 +1507,6 @@ "event_formatter_widget_added" = "%@ widget pridal %@"; "directory_server_type_homeserver" = "Zadajte domovský server, z ktorého chcete zobraziť zoznam verejných miestností"; "directory_server_all_native_rooms" = "Všetky natívne miestnosti Matrix"; -"directory_server_all_rooms" = "Všetky miestnosti na serveri %s"; "directory_server_picker_title" = "Vybrať adresár"; // Media picker @@ -1861,7 +1848,6 @@ "login_user_id_placeholder" = "Matrix ID (napr. @fero:matrix.org alebo fero)"; "login_identity_server_info" = "Matrix poskytuje servery totožnosti na sledovanie, ktoré e-maily atď. patria k jednotlivým Matrix ID. V súčasnosti existuje iba stránka https://matrix.org."; "login_home_server_info" = "Váš domovský server ukladá všetky vaše konverzácie a údaje o účte"; -"ssl_fingerprint_hash" = "Odtlačok (%s):"; "call_more_actions_dialpad" = "Číselník"; "call_ended" = "Hovor ukončený"; @@ -1893,7 +1879,7 @@ "notice_room_join" = "%@ sa pripojil/a"; "language_picker_default_language" = "Predvolené (%@)"; "user_id_placeholder" = "napr.: @fero:domovskyserver"; -"power_level" = "Úroveň právomoci"; +"power_level" = "Úroveň oprávnenia"; // Others "user_id_title" = "ID používateľa:"; @@ -1936,8 +1922,8 @@ "resend" = "Odoslať znovu"; "copy_button_name" = "Kopírovať"; "send" = "Odoslať"; -"private" = "Súkromné"; -"public" = "Verejné"; +"private" = "Súkromný"; +"public" = "Verejný"; "default" = "predvolené"; "error" = "Chyba"; "unsent" = "Neodoslané"; @@ -2062,7 +2048,6 @@ "notice_conference_call_started" = "Začala sa VoIP konferencia"; "notice_conference_call_request" = "%@ požiadal/a o VoIP konferenciu"; "notice_declined_video_call" = "%@ odmietol hovor"; -"notice_ended_video_call" = "%s ukončil/a hovor"; "notice_room_name_changed_for_dm" = "%@ zmenil/a svoje meno na %@."; "notice_room_name_changed" = "%@ zmenil názov miestnosti na %@."; "notice_topic_changed" = "%@ zmenil tému na \"%@\"."; @@ -2134,7 +2119,7 @@ // Attachment "attachment_size_prompt" = "Chcete odoslať ako:"; -"room_member_power_level_prompt" = "Túto zmenu nebudete môcť vrátiť späť, pretože tomuto používateľovi udeľujete rovnakú úroveň moci, akú máte vy.\nSte si istí?"; +"room_member_power_level_prompt" = "Túto zmenu nebudete môcť vrátiť späť, pretože tomuto používateľovi udeľujete rovnakú úroveň oprávnenia, akú máte vy.\nSte si istí?"; // Room members "room_member_ignore_prompt" = "Ste si istí, že chcete skryť všetky správy od tohto používateľa?"; @@ -2330,3 +2315,197 @@ "room_participants_leave_processing" = "Opustenie"; "joined" = "Sa pripojil/a"; "callbar_return" = "Späť"; +"room_access_space_chooser_other_spaces_section_info" = "To sú pravdepodobne veci, ktorých súčasťou sú aj iní administrátori %@."; +"space_invite_not_enough_permission" = "Nemáte povolenie pozývať ľudí do tohto priestoru"; +"onboarding_congratulations_home_button" = "Zober ma domov"; +"ignore_user" = "Ignorovať používateľa"; +"location_sharing_pin_drop_share_title" = "Poslať túto polohu"; +"location_sharing_static_share_title" = "Poslať moju aktuálnu polohu"; +"live_location_sharing_banner_stop" = "Zastaviť"; +"live_location_sharing_banner_title" = "Poloha v reálnom čase zapnutá"; + +// MARK: Live location sharing + +"location_sharing_live_share_title" = "Zdieľať polohu v reálnom čase"; +"side_menu_coach_message" = "Potiahnutím doprava alebo ťuknutím zobrazíte všetky miestnosti"; +"spaces_add_room_missing_permission_message" = "Nemáte oprávnenie pridávať miestnosti do tohto priestoru."; +"spaces_creation_in_one_space" = "v 1 priestore"; +"spaces_creation_in_many_spaces" = "v %@ priestoroch"; +"spaces_creation_in_spacename_plus_many" = "v %@ + %@ priestory"; +"spaces_creation_in_spacename_plus_one" = "v %@ + 1 priestor"; +"spaces_creation_in_spacename" = "v %@"; +"spaces_creation_post_process_inviting_users" = "Pozývanie %@ používateľov"; +"spaces_creation_post_process_adding_rooms" = "Pridávanie %@ miestností"; +"spaces_creation_post_process_creating_room" = "Vytváranie %@"; +"spaces_creation_post_process_uploading_avatar" = "Nahrávanie obrázka"; +"spaces_creation_post_process_creating_space_task" = "Vytváranie %@"; +"spaces_creation_post_process_creating_space" = "Vytváranie priestoru"; +"spaces_creation_invite_by_username_message" = "Môžete ich pozvať aj neskôr."; +"spaces_creation_invite_by_username_title" = "Pozvite svoj tím"; +"spaces_creation_invite_by_username" = "Pozvať podľa používateľského mena"; +"spaces_creation_add_rooms_message" = "Keďže tento priestor je určený len pre vás, nikto nebude informovaný. Neskôr môžete pridať ďalšie."; +"spaces_creation_add_rooms_title" = "Čo chcete pridať?"; +"spaces_creation_sharing_type_me_and_teammates_detail" = "Súkromný priestor pre vás a vašich spolupracovníkov"; +"spaces_creation_sharing_type_me_and_teammates_title" = "Ja a moji kolegovia z tímu"; +"spaces_creation_sharing_type_just_me_detail" = "Súkromný priestor na usporiadanie vašich miestností"; +"spaces_creation_sharing_type_just_me_title" = "Iba ja"; +"spaces_creation_sharing_type_message" = "Uistite sa, že k %@ majú prístup správni ľudia. Neskôr to môžete zmeniť."; +"spaces_creation_sharing_type_title" = "S kým spolupracujete?"; +"spaces_creation_email_invites_email_title" = "Email"; +"spaces_creation_email_invites_message" = "Môžete ich pozvať aj neskôr."; +"spaces_creation_email_invites_title" = "Pozvite svoj tím"; +"spaces_creation_new_rooms_support" = "Podpora"; +"spaces_creation_new_rooms_random" = "Náhodné"; +"spaces_creation_new_rooms_general" = "Všeobecné"; +"spaces_creation_new_rooms_room_name_title" = "Názov miestnosti"; +"spaces_creation_new_rooms_message" = "Pre každú z nich vytvoríme miestnosti."; +"spaces_creation_new_rooms_title" = "Aké diskusie budete viesť?"; +"spaces_creation_cancel_message" = "Váš doterajší postup sa stratí."; +"spaces_creation_cancel_title" = "Zastaviť vytváranie priestoru?"; +"spaces_creation_private_space_title" = "Váš súkromný priestor"; +"spaces_creation_public_space_title" = "Váš verejný priestor"; +"spaces_creation_address_already_exists" = "%@\nuž existuje"; +"spaces_creation_address_invalid_characters" = "%@\nmá neplatné znaky"; +"spaces_creation_address_default_message" = "Váš priestor si môžete pozrieť na adrese\n%@"; +"spaces_creation_empty_room_name_error" = "Vyžaduje sa názov"; +"spaces_creation_address" = "Adresa"; +"spaces_creation_settings_message" = "Pridajte niekoľko detailov, ktoré pomôžu vyniknúť. Tie môžete kedykoľvek zmeniť."; +"spaces_creation_footer" = "Neskôr to môžete zmeniť"; +"spaces_creation_visibility_message" = "Ak sa chcete pripojiť k existujúcemu priestoru, potrebujete pozvánku."; +"spaces_creation_visibility_title" = "Aký typ priestoru chcete vytvoriť?"; + +// Mark: - Space Creation + +"spaces_creation_hint" = "Priestory sú novým spôsobom spájania miestností a ľudí."; +"space_settings_current_address_message" = "Váš priestor si môžete pozrieť na adrese\n%@"; +"space_settings_update_failed_message" = "Nepodarilo sa aktualizovať nastavenia priestoru. Chcete to skúsiť znova?"; +"space_settings_access_section" = "Kto má prístup do tohto priestoru?"; +"space_topic" = "Popis"; +"space_public_join_rule_detail" = "Otvorený priestor pre každého, najlepšie pre komunity"; +"spaces_add_space" = "Pridať priestor"; +"spaces_add_room" = "Pridať miestnosť"; +"spaces_invite_people" = "Pozvať ľudí"; +"space_private_join_rule_detail" = "Len pre pozvaných, najlepšie pre seba alebo tímy"; +"spaces_explore_rooms_one_room" = "1 miestnosť"; +"spaces_explore_rooms_room_number" = "%@ miestnosti"; +"spaces_create_space_title" = "Vytvoriť priestor"; +"spaces_add_space_title" = "Vytvoriť priestor"; +"room_invite_not_enough_permission" = "Nemáte povolenie pozývať ľudí do tejto miestnosti"; +"room_invite_to_room_option_detail" = "Nebudú súčasťou %@."; +"room_invite_to_room_option_title" = "Iba do tejto miestnosti"; +"room_invite_to_space_option_detail" = "Môžu preskúmať %@, ale nebudú členmi %@."; + +// Mark: - Room invite + +"room_invite_to_space_option_title" = "Do %@"; +"share_invite_link_space_text" = "Ahoj, pridaj sa do tohto priestoru na %@"; +"share_invite_link_room_text" = "Ahoj, pridaj sa do tejto miestnosti na %@"; + +// MARK: - Share invite link + +"share_invite_link_action" = "Zdieľať odkaz na pozvánku"; +"create_room_processing" = "Vytváranie miestnosti"; +"create_room_suggest_room_footer" = "Navrhované miestnosti sú propagované členom priestoru ako vhodné na pripojenie."; +"create_room_suggest_room" = "Navrhnúť členom priestoru"; +"create_room_show_in_directory_footer" = "Toto pomôže ľuďom nájsť a pripojiť sa."; +"create_room_promotion_header" = "PROPAGÁCIA"; +"create_room_section_footer_type_public" = "Nájsť a pripojiť sa môžu len pozvaní ľudia, nie len ľudia s názvom priestoru."; +"create_room_section_footer_type_restricted" = "Ktokoľvek v priestore môže nájsť a pripojiť sa."; +"create_room_section_footer_type_private" = "Nájsť a pripojiť sa môžu len pozvaní ľudia."; +"create_room_type_restricted" = "Členovia priestoru"; +"call_jitsi_unable_to_start" = "Nie je možné spustiť konferenčný hovor"; +"room_suggestion_settings_screen_message" = "Navrhované miestnosti sú propagované členom priestoru ako vhodné na pripojenie."; +"room_suggestion_settings_screen_title" = "Vytvorte miestnosť navrhnutú v priestore"; + +// Room suggestion Settings +"room_suggestion_settings_screen_nav_title" = "Navrhnúť miestnosť"; +"room_access_space_chooser_other_spaces_section" = "Iné priestory alebo miestnosti"; +"room_access_space_chooser_known_spaces_section" = "Priestory, ktoré poznáte obsahujúce %@"; +"room_access_settings_screen_setting_room_access" = "Nastavovanie prístupu do miestnosti"; +"room_access_settings_screen_upgrade_alert_upgrading" = "Aktualizácia miestnosti"; +"room_access_settings_screen_upgrade_alert_upgrade_button" = "Aktualizovať"; +"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "Automaticky pozvať členov do novej miestnosti"; +"room_access_settings_screen_upgrade_alert_title" = "Aktualizovať miestnosť"; +"room_access_settings_screen_public_message" = "Ktokoľvek môže nájsť a pripojiť sa."; +"room_access_settings_screen_edit_spaces" = "Upraviť priestory"; +"room_access_settings_screen_upgrade_required" = "Vyžaduje sa aktualizácia"; +"room_access_settings_screen_restricted_message" = "Umožnite komukoľvek v priestore nájsť a pripojiť sa.\nBudete vyzvaní, aby ste potvrdili, do ktorých priestorov."; +"room_access_settings_screen_private_message" = "Nájsť a pripojiť sa môžu len pozvaní ľudia."; +"room_access_settings_screen_message" = "Rozhodnite, kto môže nájsť a pripojiť sa do %@."; +"room_access_settings_screen_title" = "Kto má prístup do tejto miestnosti?"; + +// Room Access Settings +"room_access_settings_screen_nav_title" = "Prístup k miestnosti"; +"room_details_promote_room_suggest_title" = "Navrhnúť členom priestoru"; +"room_details_promote_room_title" = "Propagovať miestnosť"; +"room_details_access_row_title" = "Prístup"; +"settings_labs_enable_auto_report_decryption_errors" = "Automatické hlásenie chýb dešifrovania"; +"room_preview_decline_invitation_options" = "Chcete odmietnuť pozvanie alebo ignorovať tohto používateľa?"; +"threads_beta_cancel" = "Teraz nie"; +"threads_beta_enable" = "Vyskúšajte si to"; +"threads_beta_information_link" = "Zistiť viac"; +"threads_beta_information" = "Udržujte diskusie organizované pomocou vlákien.\n\nVlákna pomáhajú udržiavať diskusie na téme a uľahčujú ich sledovanie. "; +"threads_beta_title" = "Vlákna"; +"threads_notice_done" = "Rozumiem"; +"threads_notice_information" = "Všetky vlákna vytvorené počas experimentálneho obdobia budú teraz odosielané ako bežné odpovede.

Bude to jednorazový prechod, keďže vlákna sú teraz súčasťou špecifikácie Matrixu."; +"threads_notice_title" = "Vlákna už nie sú experimentálne 🎉"; +"room_participants_invite_prompt_to_msg" = "Ste si istí, že chcete pozvať používateľa %@ do %@?"; +"onboarding_celebration_button" = "Poďme na to"; +"onboarding_celebration_message" = "Vaše nastavenia boli uložené."; +"onboarding_celebration_title" = "Všetko je pripravené!"; +"onboarding_avatar_accessibility_label" = "Profilový obrázok"; +"onboarding_avatar_message" = "Toto môžete kedykoľvek zmeniť."; +"onboarding_avatar_title" = "Pridať profilový obrázok"; +"onboarding_display_name_max_length" = "Vaše zobrazované meno musí mať menej ako 256 znakov"; +"onboarding_display_name_hint" = "Neskôr to môžete zmeniť"; +"onboarding_display_name_placeholder" = "Zobrazované meno"; +"onboarding_display_name_message" = "Toto sa zobrazí pri odosielaní správ."; +"onboarding_display_name_title" = "Vyberte si zobrazované meno"; +"onboarding_personalization_skip" = "Vynechať tento krok"; +"onboarding_personalization_save" = "Uložiť a pokračovať"; +"onboarding_congratulations_personalize_button" = "Prispôsobiť profil"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "Váš účet %@ bol vytvorený."; +"onboarding_congratulations_title" = "Gratulujeme!"; +"saving" = "Ukladanie"; + +// Activities +"loading" = "Načítavane"; +"edit" = "Upraviť"; +"suggest" = "Navrhnúť"; +"add" = "Pridať"; +"existing" = "Existujúce"; +"new_word" = "Nové"; +"stop" = "Zastaviť"; +"joining" = "Pripájanie sa"; +"room_access_settings_screen_upgrade_alert_message_no_param" = "Každý, kto sa nachádza v nadradenom priestore, bude môcť túto miestnosť nájsť a pripojiť sa k nej - nie je potrebné každého ručne pozývať. V nastaveniach miestnosti to budete môcť kedykoľvek zmeniť."; +"ssl_fingerprint_hash" = "Odtlačok (%@):"; +"notice_ended_video_call" = "%@ ukončil/a hovor"; +"location_sharing_live_list_item_stop_sharing_action" = "Zastaviť zdieľanie"; +"location_sharing_live_list_item_current_user_display_name" = "Vy"; +"location_sharing_live_list_item_last_update_invalid" = "Posledná aktualizácia neznáma"; +"location_sharing_live_list_item_last_update" = "Aktualizované pred %@"; +"location_sharing_live_list_item_sharing_expired" = "Zdieľanie vypršalo"; +"location_sharing_live_list_item_time_left" = "%@ ostáva"; +"location_sharing_live_viewer_title" = "Poloha"; +"location_sharing_live_map_callout_title" = "Zdieľať polohu"; +"voice_message_remaining_recording_time" = "%@s odišiel/a"; +"side_menu_app_version" = "Verzia %@"; +"widget_integration_room_not_visible" = "Miestnosť %@ nie je viditeľná."; +"call_no_stun_server_error_use_fallback_button" = "Skúste použiť %@"; +"directory_server_all_rooms" = "Všetky miestnosti na serveri %@"; +"room_access_settings_screen_upgrade_alert_note" = "Vezmite prosím na vedomie, že aktualizácia vytvorí novú verziu miestnosti. Všetky aktuálne správy zostanú v tejto archivovanej miestnosti."; +"room_access_settings_screen_upgrade_alert_message" = "Každý v %@ bude môcť nájsť túto miestnosť a pripojiť sa k nej - nie je potrebné každého ručne pozývať. V nastaveniach miestnosti to budete môcť kedykoľvek zmeniť."; +"settings_presence_offline_mode_description" = "Ak je táto funkcia zapnutá, budete sa ostatným používateľom vždy javiť ako offline, a to aj pri používaní aplikácie."; +"settings_presence_offline_mode" = "Režim offline"; +"settings_presence" = "Prítomnosť"; +"settings_version" = "Verzia %@"; +"room_preview_try_join_an_unknown_room" = "Pokúšate sa zobraziť %@. Chcete vstúpiť a pridať sa k diskusii?"; +"room_preview_unlinked_email_warning" = "Toto pozvanie bolo odoslané na emailovú adresu %@, ktorá nie je priradená k tomuto účtu. Môžete sa prihlásiť k inému účtu, alebo pridať túto emailovú adresu do práve prihláseného účtu."; +"threads_discourage_information_2" = "\n\nChcete aj napriek tomu povoliť vlákna?"; +"threads_discourage_information_1" = "Váš domovský server v súčasnosti nepodporuje vlákna, takže táto funkcia môže byť nespoľahlivá. Niektoré správy vo vláknach nemusia byť spoľahlivo dostupné. "; +"room_one_user_is_typing" = "%@ píše…"; +"room_participants_invite_prompt_msg" = "Ste si istí, že chcete pozvať používateľa %@ do tejto konverzácie?"; +"room_participants_remove_prompt_msg" = "Ste si istí, že chcete používateľa %@ odstrániť z tejto konverzácie?"; +"auth_reset_password_email_validation_message" = "Na adresu %@ bola odoslaná správa. Potom, čo kliknete na odkaz z tejto správy, pokračujte kliknutím nižšie."; +"active_call_details" = "Prebiehajúci hovor (%@)"; diff --git a/Riot/Assets/sq.lproj/Localizable.strings b/Riot/Assets/sq.lproj/Localizable.strings index 636ef3ec8..a49dd9660 100644 --- a/Riot/Assets/sq.lproj/Localizable.strings +++ b/Riot/Assets/sq.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ dërgoi një foto %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ postoi një foto %@ në %@"; /* A single unread message in a room */ diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index 509287f56..3feb86e4f 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -464,7 +464,7 @@ "deactivate_account_forget_messages_information_part2_emphasize" = "Kujdes"; "deactivate_account_validate_action" = "Çaktivizoje llogarinë"; "deactivate_account_password_alert_title" = "Çaktivizoni Llogarinë"; -"deactivate_account_password_alert_message" = "Që të vazhdohet, ju lutemi, jepni fjalëkalimin e llogarisë tuaj Matrix."; +"deactivate_account_password_alert_message" = "Që të vazhdohet, ju lutemi, jepni fjalëkalimin e llogarisë tuaj Matrix"; // Re-request confirmation dialog "rerequest_keys_alert_title" = "Kërkesa u Dërgua"; "auth_reset_password_success_message" = "Fjalëkalimi i llogarisë tuaj Matrix u ricaktua.\n\nËshtë bërë dalja juaj nga llogaria në krejt sesionet dhe s’do të merrni më njoftime push. Për riaktivizim të njoftimeve, ribëni hyrjen në çdo pajisje."; @@ -518,7 +518,6 @@ "large_badge_value_k_format" = "%.1fK"; "no_voip" = "%@ po ju thërret, por %@ nuk mbulon ende thirrje.\nMund ta shpërfillni këtë njoftim dhe t’i përgjigjeni thirrjes prej një tjetër pajisjeje, ose mund të mos e pranoni."; // Crash report -"google_analytics_use_prompt" = "Do të donit të ndihmoni në përmirësimin e %@-it duke parashtruar automatikisht dhe në mënyrë anonime raporte vithisjesh dhe të dhëna përdorimi?"; // Crypto "e2e_enabling_on_app_update" = "%@ tani mbulon fshehtëzim skaj-më-skaj, por lypset të ribëni hyrjen që ta aktivizoni.\n\nMund ta bëni tani ose më vonë, që prej rregullimeve të aplikacionit."; "bug_report_description" = "Ju lutemi, përshkruajeni të metën. Ç’po bënit? Ç’prisnit të ndodhte? Ç’ndodhi në fakt?"; @@ -557,7 +556,6 @@ "settings_key_backup_info_not_valid" = "Ky sesion nuk po bën kopjeruajtje të kyçeve tuaja, por keni një kopjeruajtje ekzistuese që mund ta përdorni për rimarrje dhe ta shtoni më tej."; "settings_key_backup_info_progress" = "Po kopjeruhen kyçet për %@…"; "settings_key_backup_info_progress_done" = "U kopjeruajtën krejt kyçet"; -"settings_key_backup_info_not_trusted_from_verifiable_device_fix_action" = "Për të përdorur Rikthim Mesazhesh të Sigurt në këtë pajisje, verifikoni tani %@."; "settings_key_backup_info_not_trusted_fix_action" = "Për të përdorur Rikthim Mesazhesh të Sigurt në këtë pajisje, jepni tani një frazëkalim ose një kyç rikthimesh."; "settings_key_backup_info_trust_signature_unknown" = "Kopjeruajtja ka një nënshkrim nga një sesion me ID: %@"; "settings_key_backup_info_trust_signature_valid" = "Kopjeruajtja ka një nënshkrim të vlefshëm prej këtij sesioni"; @@ -669,10 +667,6 @@ "room_details_fail_to_update_room_direct" = "S’arrihet të përditësohet shenja si e drejtpërdrejtë e kësaj dhome"; "room_event_action_reply" = "Përgjigjuni"; "room_event_action_edit" = "Përpunoni"; -"room_event_action_reaction_agree" = "Pajtohem me %@"; -"room_event_action_reaction_disagree" = "S’pajtohem me %@"; -"room_event_action_reaction_like" = "Pëlqejeni %@"; -"room_event_action_reaction_dislike" = "Shpëlqejeni %@"; "room_action_reply" = "Përgjigjuni"; "settings_labs_message_reaction" = "Reagoni ndaj mesazhesh me emoji"; "settings_key_backup_button_connect" = "Lidhe këtë sesion me Kopjeruajtje Kyçesh"; @@ -817,7 +811,6 @@ "room_participants_start_new_chat_error_using_user_email_without_identity_server" = "S’ka shërbyes identitetesh të formësuar, ndaj s’mund të nisni një fjalosje me një kontakt duke përdorur një email."; // Service terms "service_terms_modal_title" = "Kushte Shërbimi"; -"service_terms_modal_message" = "Që të vazhdohet, lypset të pranoni kushtet e këtij shërbimi (%@)."; "service_terms_modal_accept_button" = "Pranoji"; "service_terms_modal_description_for_identity_server" = "Jini i zbulueshëm nga të tjerët"; "service_terms_modal_description_for_integration_manager" = "Përdorni Robotë, ura, widget-e dhe paketa ngjitësish"; @@ -850,7 +843,7 @@ "settings_three_pids_management_information_part3" = "."; "settings_add_3pid_password_title_email" = "Shtoni adresë email"; "settings_add_3pid_password_title_msidsn" = "Shtoni numër telefoni"; -"settings_add_3pid_password_message" = "Që të vazhdohet, ju lutemi, jepni fjalëkalimin e llogarisë tuaj Matrix."; +"settings_add_3pid_password_message" = "Që të vazhdohet, ju lutemi, jepni fjalëkalimin e llogarisë tuaj Matrix"; "settings_add_3pid_invalid_password_message" = "Kredenciale të pavlefshme"; "settings_devices_description" = "Emri publik i një sesioni është i dukshëm për persona me të cilët komunikoni"; "settings_discovery_no_identity_server" = "S’po përdorni ndonjë shërbyes identitetesh. Që të jeni i zbulueshëm nga kontakte ekzistuese që njihni, shtoni një të tillë."; @@ -901,7 +894,6 @@ "service_terms_modal_description_for_identity_server_2" = "Bëhuni i gjetshëm përmes telefoni ose email-i"; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "Zbulim kontaktesh"; -"service_terms_modal_message_identity_server" = "Që të zbuloni kontakte, pranoni kushtet e shërbyesit të identiteteve (%@)."; // Generic errors "error_invite_3pid_with_no_identity_server" = "Që të ftoni me email, shtoni një shërbyes identitetesh, që nga rregullimet tuaja."; "error_not_supported_on_mobile" = "Këtë s’mund ta bëni nga %@ për celular."; @@ -1192,19 +1184,19 @@ "searchable_directory_x_network" = "Rrjet %@"; "searchable_directory_search_placeholder" = "Emër ose ID"; "create_room_title" = "Dhomë e Re"; -"create_room_section_header_name" = "Emër dhome"; +"create_room_section_header_name" = "EMËR"; "create_room_placeholder_name" = "Emër"; -"create_room_section_header_topic" = "Temë dhome (në daçi)"; -"create_room_placeholder_topic" = "Temë"; -"create_room_section_header_encryption" = "Fshehtëzim dhome"; +"create_room_section_header_topic" = "TEMË (NË DAÇI)"; +"create_room_placeholder_topic" = "Përse është kjo dhomë?"; +"create_room_section_header_encryption" = "FSHEHTËZIM"; "create_room_enable_encryption" = "Aktivizoni Fshehtëzim"; "create_room_section_footer_encryption" = "Fshehtëzimi s’mund të çaktivizohet më pas."; -"create_room_section_header_type" = "Lloj dhome"; -"create_room_type_private" = "Dhomë Private"; -"create_room_type_public" = "Dhomë Publike"; +"create_room_section_header_type" = "KUSH MUND TË HYJË"; +"create_room_type_private" = "Dhomë Private (vetëm me ftesa)"; +"create_room_type_public" = "Dhomë Publike (kushdo)"; "create_room_section_footer_type" = "Njerëzit marrin pjesë në një dhomë vetëm me ftesën për te dhoma."; -"create_room_show_in_directory" = "Shfaqe dhomën te lista"; -"create_room_section_header_address" = "Adresë dhome"; +"create_room_show_in_directory" = "Shfaqe te listë dhomash"; +"create_room_section_header_address" = "ADRESË"; "create_room_placeholder_address" = "#provëdhome:matrix.org"; "room_info_list_room_encrypted" = "Mesazhet në këtë dhomë janë të fshehtëzuar skaj më skaj"; "room_info_list_one_member" = "1 anëtar"; @@ -1540,7 +1532,7 @@ "poll_edit_form_add_option" = "Shtoni mundësi"; "poll_edit_form_option_number" = "Mundësia %lu"; "poll_edit_form_create_options" = "Krijo mundësi"; -"poll_edit_form_input_placeholder" = "Shkruani diçka!"; +"poll_edit_form_input_placeholder" = "Shkruani diçka"; "poll_edit_form_question_or_topic" = "Pyetje ose temë"; "poll_edit_form_poll_question_or_topic" = "Pyetje ose temë pyetësori"; @@ -1733,7 +1725,6 @@ "notice_room_aliases" = "Aliaset e dhomës janë: %@"; "notice_room_related_groups" = "Grupet përshoqëruar kësaj dhome janë: %@"; "notice_encrypted_message" = "Mesazhi i fshehtëzuar"; -"notice_encryption_enabled" = "%@ aktivizoi fshehtëzimin skaj-më-skaj (algoritëm %@)"; "notice_image_attachment" = "bashkëngjitje figurash"; "notice_audio_attachment" = "bashkëngjitje audio"; "notice_video_attachment" = "bashkëngjitje videosh"; @@ -2128,3 +2119,166 @@ "notice_error_unformattable_event" = "** S’arrihet të riprodhohet mesazhi. Ju lutemi, njoftoni një të metë"; "settings_labs_use_only_latest_user_avatar_and_name" = "Shfaq në historik mesazhesh avatarin dhe emrin më të ri të përdoruesve"; "room_participants_leave_success" = "Doli nga dhoma"; +"space_invite_not_enough_permission" = "S’keni leje të ftoni njerëz në këtë hapësirë"; +"room_access_settings_screen_public_message" = "Kushdo mund ta gjejë dhe hyjë në të."; +"ignore_user" = "Shpërfille Përdoruesin"; +"location_sharing_pin_drop_share_title" = "Dërgoje këtë vendndodhje"; +"location_sharing_static_share_title" = "Dërgo vendndodhjen time të tanishme"; +"live_location_sharing_banner_stop" = "Ndale"; +"live_location_sharing_banner_title" = "Vendndodhje Live e aktivizuar"; + +// MARK: Live location sharing + +"location_sharing_live_share_title" = "Jepe vendndodhjen “live”"; +"side_menu_coach_message" = "Fërkojeni për djathtas, ose prekeni që të shihni krejt dhomat"; +"spaces_add_room_missing_permission_message" = "S’keni leje të shtoni dhoma në këtë hapësirë."; +"spaces_creation_in_one_space" = "në 1 hapësirë"; +"spaces_creation_in_many_spaces" = "në %@ hapësira"; +"spaces_creation_in_spacename_plus_many" = "te %@ + %@ hapësira"; +"spaces_creation_in_spacename_plus_one" = "te %@ + 1 hapësirë"; +"spaces_creation_in_spacename" = "në %@"; +"spaces_creation_post_process_inviting_users" = "Po ftohen përdorues %@"; +"spaces_creation_post_process_adding_rooms" = "Po shtohen dhoma %@"; +"spaces_creation_post_process_creating_room" = "Po krijohet %@"; +"spaces_creation_post_process_uploading_avatar" = "Po ngarkohet avatari"; +"spaces_creation_post_process_creating_space_task" = "Po krijohet %@"; +"spaces_creation_post_process_creating_space" = "Po krijohet hapësirë"; +"spaces_creation_invite_by_username_message" = "Mund t’i ftoni edhe ata më vonë."; +"spaces_creation_invite_by_username_title" = "Ftoni ekipin tuaj"; +"spaces_creation_invite_by_username" = "Ftoni përmes emri përdoruesi"; +"spaces_creation_add_rooms_message" = "Ngaqë kjo hapësirë është vetëm për ju, askush s’do të njoftohet. Më vonë mund të shtoni më tepër."; +"spaces_creation_add_rooms_title" = "Ç’doni të shtoni?"; +"spaces_creation_sharing_type_me_and_teammates_detail" = "Një hapësirë private për ju & anëtarët e ekipit tuaj"; +"spaces_creation_sharing_type_me_and_teammates_title" = "Unë dhe anëtarët e ekipit tim"; +"spaces_creation_sharing_type_just_me_detail" = "Një hapësirë private për të sistemuar dhomat tuaja"; +"spaces_creation_sharing_type_just_me_title" = "Vetëm unë"; +"spaces_creation_sharing_type_message" = "Siguroni që te %@ të kenë hyrje personat e duhur. Këtë mund të ndryshoni më vonë."; +"spaces_creation_sharing_type_title" = "Me cilët po punoni?"; +"spaces_creation_email_invites_email_title" = "Email"; +"spaces_creation_email_invites_message" = "Mund t’i ftoni edhe ata më vonë."; +"spaces_creation_email_invites_title" = "Ftoni ekipin tuaj"; +"spaces_creation_new_rooms_support" = "Asistencë"; +"spaces_creation_new_rooms_random" = "Kuturu"; +"spaces_creation_new_rooms_general" = "Të Përgjithshme"; +"spaces_creation_new_rooms_room_name_title" = "Emër dhome"; +"spaces_creation_new_rooms_message" = "Do të krijojmë një dhomë për secilin."; +"spaces_creation_new_rooms_title" = "Cilat janë disa nga gjërat që doni të diskutoni?"; +"spaces_creation_cancel_message" = "Ecuria juaj do të humbë."; +"spaces_creation_cancel_title" = "Të ndalet krijimi i një hapësire?"; +"spaces_creation_private_space_title" = "Hapësira juaj private"; +"spaces_creation_public_space_title" = "Hapësira juaj publike"; +"spaces_creation_address_already_exists" = "%@\nekziston tashmë"; +"spaces_creation_address_invalid_characters" = "%@\nka shenja të pavlefshme"; +"spaces_creation_address_default_message" = "Hapësira juaj do të jetë e dukshme te\n%@"; +"spaces_creation_empty_room_name_error" = "Emri është i domosdoshëm"; +"spaces_creation_address" = "Adresë"; +"spaces_creation_settings_message" = "Shtoni ndonjë hollësi për të ndihmuar të dalë në pah. Këto mund t’i ndryshoni kur të doni."; +"spaces_creation_footer" = "Këtë mund ta ndryshoni më vonë"; +"spaces_creation_visibility_message" = "Që të hyni në një hapësirë ekzistuese, ju duhet një ftesë."; +"spaces_creation_visibility_title" = "Ç’lloj hapësire doni të krijoni?"; + +// Mark: - Space Creation + +"spaces_creation_hint" = "Hapësirat janë një rrugë e re për të grupuar dhoma dhe persona."; +"space_settings_current_address_message" = "Hapësira juaj do të jetë e dukshme te\n%@"; +"space_settings_update_failed_message" = "S’u arrit të përditësohen rregullimet e hapësirës. Doni të riprovohet?"; +"space_settings_access_section" = "Kush mund të hyjë në këtë hapësirë?"; +"space_topic" = "Përshkrim"; +"space_public_join_rule_detail" = "E hapur për këdo, më e mira për bashkësi"; +"spaces_add_space" = "Shtoni hapësirë"; +"spaces_add_room" = "Shtoni dhomë"; +"spaces_invite_people" = "Ftoni njerëz"; +"space_private_join_rule_detail" = "Vetëm me ftesa, më e mira për ju, ose ekipe"; +"spaces_explore_rooms_one_room" = "1 dhomë"; +"spaces_explore_rooms_room_number" = "%@ dhoma"; +"spaces_create_space_title" = "Krijoni një hapësirë"; +"spaces_add_space_title" = "Krijo hapësirën"; +"room_invite_not_enough_permission" = "S’keni leje të ftoni njerëz në këtë dhomë"; +"room_invite_to_room_option_detail" = "S’do të jenë pjesë e %@."; +"room_invite_to_room_option_title" = "Thjesht te kjo dhomë"; + +// Mark: - Room invite + +"room_invite_to_space_option_title" = "Te %@"; +"share_invite_link_space_text" = "Hej, ejani te kjo hapësirë në %@"; +"share_invite_link_room_text" = "Hej, ejani te kjo dhomë në %@"; + +// MARK: - Share invite link + +"share_invite_link_action" = "Jepuni lidhje ftese"; +"home_syncing" = "Njëkohësim"; +"create_room_processing" = "Po krijohet dhomë"; +"create_room_suggest_room_footer" = "Dhomat e sugjeruara promovohen te anëtarë të hapësirës si të mira për të hyrë."; +"create_room_suggest_room" = "Sugjerojuani anëtarëve të hapësirës"; +"create_room_show_in_directory_footer" = "Kjo do t’i ndihmojë personat ta gjejnë dhe hyjnë në të."; +"create_room_promotion_header" = "PROMOCION"; +"create_room_section_footer_type_public" = "Vetëm personat e ftuar mund ta gjejnë dhe hyjnë në të, jo thjesht persona në Emër hapësire."; +"create_room_section_footer_type_restricted" = "Cilido në Emër hapësire mund ta gjejë dhe hyjë në të."; +"create_room_section_footer_type_private" = "Vetëm personat e ftuar mund ta gjejnë dhe hyjnë në të."; +"create_room_type_restricted" = "Anëtarë hapësire"; +"call_jitsi_unable_to_start" = "S’arrihet të niset thirrje konferencë"; +"room_suggestion_settings_screen_message" = "Dhomat e sugjeruara u promovohen anëtarëve të hapësirës si të mira për të hyrë."; +"room_suggestion_settings_screen_title" = "Krijoni një dhomë të sugjeruar në një hapësirë"; + +// Room suggestion Settings +"room_suggestion_settings_screen_nav_title" = "Sugjeroni dhomë"; +"room_access_space_chooser_other_spaces_section_info" = "Këto ka shumë mundësi të jenë gjëra ku përgjegjës të tjerë të %@ janë pjesë."; +"room_access_space_chooser_other_spaces_section" = "Hapësira ose dhoma të tjera"; +"room_access_space_chooser_known_spaces_section" = "Hapësirat që njihni, përmbajnë %@"; +"room_access_settings_screen_setting_room_access" = "Rregullim hyrjeje në dhomë"; +"room_access_settings_screen_upgrade_alert_upgrading" = "Përmirësim dhome"; +"room_access_settings_screen_upgrade_alert_upgrade_button" = "Përmirësoje"; +"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "Fto vetvetiu anëtarë te dhomë e re"; +"room_access_settings_screen_upgrade_alert_title" = "Përmirësoni dhomën"; +"room_access_settings_screen_edit_spaces" = "Përpunoni hapësira"; +"room_access_settings_screen_upgrade_required" = "Lypset domosdo përmirësim"; +"room_access_settings_screen_restricted_message" = "Lejoni këdo në një hapësirë të gjejë dhe hyjë.\nDo t’ju kërkohet të ripohoni cilat hapësira."; +"room_access_settings_screen_private_message" = "Vetëm personat e ftuar mund ta gjejnë dhe hyjnë në të."; +"room_access_settings_screen_message" = "Vendosni se cilët mund të gjejnë dhe hyjnë te %@."; +"room_access_settings_screen_title" = "Kush mund të hyjë në këtë dhomë?"; + +// Room Access Settings +"room_access_settings_screen_nav_title" = "Leje mbi dhomën"; +"room_details_promote_room_suggest_title" = "Sugjerojuani anëtarëve të hapësirës"; +"room_details_promote_room_title" = "Promovoni dhomën"; +"room_details_access_row_title" = "Hyrje"; +"settings_labs_enable_auto_report_decryption_errors" = "Raporto Vetvetiu Gabime Shfshehtëzimi"; +"room_preview_decline_invitation_options" = "Doni të hidhni poshtë ftesën, apo të shpërfillni këtë përdorues?"; +"threads_beta_cancel" = "Jo tani"; +"threads_beta_enable" = "Provojeni"; +"threads_beta_information_link" = "Mësoni më tepër"; +"threads_beta_information" = "Mbajini diskutimet të sistemuara me rrjedha.\n\nRrjedhat ndihmojnë të mbahen bisedat tuaja brenda temës dhe që të ndiqen kollaj. "; +"threads_beta_title" = "Rrjedha"; +"threads_notice_done" = "E kuptova"; +"threads_notice_information" = "Krejt rrjedhat e krijuara gjatë periudhës eksperimentale tani do të shfaqen si përgjigje të rregullta.

Ky do të jetë një tranzicion vetëm për një herë, ngaqë tanimë rrjedhat janë pjesë e protokollit Matrix."; +"threads_notice_title" = "Rrjedhat s’janë më eksperimentale 🎉"; +"room_participants_invite_prompt_to_msg" = "Jeni i sigurt se doni të ftohet %@ te %@?"; +"onboarding_celebration_button" = "Shkojmë"; +"onboarding_celebration_message" = "Parapëlqimet tuaja u ruajtën."; +"onboarding_celebration_title" = "Kaq qe!"; +"onboarding_avatar_accessibility_label" = "Foto profili"; +"onboarding_avatar_message" = "Këtë mund ta ndryshoni në çfarëdo kohe.."; +"onboarding_avatar_title" = "Shtoni një foto profili"; +"onboarding_display_name_max_length" = "Emri juaj për në ekran duhet të jetë më pak se 256 shenja"; +"onboarding_display_name_hint" = "Këtë mund ta ndryshoni më vonë"; +"onboarding_display_name_placeholder" = "Emër Në Ekran"; +"onboarding_display_name_message" = "Kjo do të shfaqet kur dërgoni mesazhe."; +"onboarding_display_name_title" = "Zgjidhni një emër për në ekran"; +"onboarding_personalization_skip" = "Anashkalojeni këtë hap"; +"onboarding_personalization_save" = "Ruajeni dhe vazhdoni"; +"onboarding_congratulations_home_button" = "Shpjemëni në shtëpi"; +"onboarding_congratulations_personalize_button" = "Personalizoni profilin"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "Llogaria juaj %@ u krijua."; +"onboarding_congratulations_title" = "Përgëzime!"; +"saving" = "Ruajtje"; + +// Activities +"loading" = "Po ngarkohet"; +"edit" = "Përpunojeni"; +"suggest" = "Sugjeroni"; +"add" = "Shtoni"; +"existing" = "Ekzistues"; +"new_word" = "E re"; +"stop" = "Ndale"; +"joining" = "Po hyhet"; diff --git a/Riot/Assets/sv.lproj/Localizable.strings b/Riot/Assets/sv.lproj/Localizable.strings index ebadca622..5b3e918dd 100644 --- a/Riot/Assets/sv.lproj/Localizable.strings +++ b/Riot/Assets/sv.lproj/Localizable.strings @@ -13,7 +13,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ skickade en bild %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ skickade en bild %@ i %@"; /* A single unread message in a room */ diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings index bfb2bf910..cfb178a1f 100644 --- a/Riot/Assets/sv.lproj/Vector.strings +++ b/Riot/Assets/sv.lproj/Vector.strings @@ -142,10 +142,8 @@ "room_participants_leave_prompt_title" = "Lämna rum"; "room_participants_leave_prompt_msg" = "Är du säker på att du vill lämna rummet?"; "room_participants_remove_prompt_title" = "Bekräftelse"; -"room_participants_remove_prompt_msg" = "Är du säker på att du vill ta bort %s från den här chatten?"; "room_participants_remove_third_party_invite_prompt_msg" = "Är du säker på att du vill återkalla den här inbjudan?"; "room_participants_invite_prompt_title" = "Bekräftelse"; -"room_participants_invite_prompt_msg" = "Är du säker på att du vill bjuda in %s till den här chatten?"; "room_participants_invite_another_user" = "Sök / bjud in efter användar-ID, namn eller e-postadress"; "room_participants_invite_malformed_id" = "Felformaterat ID. Ska vara en e-postadress eller ett Matrix-ID som '@lokaldel:domän'"; "room_participants_online" = "Online"; @@ -184,7 +182,6 @@ // Chat "room_jump_to_first_unread" = "Hoppa till oläst"; "room_new_message_notification" = "%d nytt meddelande"; -"room_new_messages_notification" = "%u nya meddelanden"; "room_one_user_is_typing" = "%@ skriver…"; "room_two_users_are_typing" = "%@ och %@ skriver…"; "room_many_users_are_typing" = "%@, %@ och andra skriver…"; @@ -229,7 +226,7 @@ "room_action_reply" = "Svara"; "room_replacement_information" = "Det här rummet har ersatts och är inte längre aktivt."; "room_replacement_link" = "Konversationen fortsätter här."; -"room_predecessor_information" = "Det här rumet är en fortsättning på en annan konversation."; +"room_predecessor_information" = "Det här rummet är en fortsättning på en annan konversation."; "room_predecessor_link" = "Tryck här för att se äldre meddelanden."; "room_message_edits_history_title" = "Meddelanderedigeringar"; "room_accessibility_search" = "Sök"; @@ -438,7 +435,6 @@ "room_accessibility_hangup" = "Lägg på"; "media_type_accessibility_image" = "Bild"; // Room Preview -"room_preview_invitation_format" = "Du har blivit inbjuden till det här rummet av %s"; "settings_global_settings_info" = "Globala aviseringsinställningar är tillgängliga i webbklienten för %@"; "settings_third_party_notices" = "Tredjepartslicenser"; "settings_crypto_device_name" = "Sessionsnamn: "; @@ -510,9 +506,7 @@ "group_participants_leave_prompt_title" = "Lämna grupp"; "group_participants_leave_prompt_msg" = "Är du säker på att du vill lämna gruppen?"; "group_participants_remove_prompt_title" = "Bekräftelse"; -"group_participants_remove_prompt_msg" = "Är du säker på att du vill ta bort %s från den här gruppen?"; "group_participants_invite_prompt_title" = "Bekräftelse"; -"group_participants_invite_prompt_msg" = "Är du säker på att du vill bjuda in %s till den här gruppen?"; "group_participants_filter_members" = "Filtrera gemenskapsmedlemmar"; "group_participants_invite_another_user" = "Sök / bjud in efter användar-ID eller namn"; "group_participants_invite_malformed_id_title" = "Fel vid inbjudan"; @@ -619,7 +613,6 @@ "identity_server_settings_alert_disconnect" = "Koppla från identitetsservern %@?"; "identity_server_settings_alert_error_terms_not_accepted" = "Du måste acceptera villkoren hos %@ för att kunna ställa in den som identitetsserver."; "identity_server_settings_alert_error_invalid_identity_server" = "%@ är inte en giltig identitetsserver."; -"service_terms_modal_message_identity_server" = "Acceptera villkoren hos identitetsservern (%@) för att upptäcka kontakter."; "device_verification_self_verify_wait_additional_information" = "Detta funkar med %@ och andra Matrix-klienter som stöder korssignering."; // Generic errors "error_invite_3pid_with_no_identity_server" = "Lägg till en identitetsserver i dina inställningar för att bjuda in via e-post."; @@ -737,7 +730,6 @@ "e2e_room_key_request_share_without_verifying" = "Dela utan att verifiera"; // GDPR "gdpr_consent_not_given_alert_message" = "För att fortsätta använda hemservern %@ behöver du ta del av och godkänna villkoren."; -"service_terms_modal_message" = "För att fortsätta behöver du godkänna villkoren för den här tjänsten (%@)."; "service_terms_modal_description_for_identity_server_2" = "Hitta mig via telefon eller e-post"; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "Upptäckt av kontakter"; @@ -822,10 +814,8 @@ "call_jitsi_error" = "Misslyckades att gå med i gruppsamtalet."; "call_no_stun_server_error_title" = "Samtalet misslyckades p.g.a. felkonfigurerad server"; "call_no_stun_server_error_message_1" = "Vänligen be administratören för hemservern %@ att konfigurera en TURN-server för att samtal ska funka tillförlitligt."; -"call_no_stun_server_error_message_2" = "Alternativt kan du försöka använda den offentliga servern på% @, men det här är inte lika tillförlitligt och det delar din IP-adress med den servern. Du kan också hantera detta i inställningarna"; "no_voip" = "%@ ringer dig men %@ stöder inte samtal än.\nDu kan ignorera den här aviseringen och svara på samtalet med en annan enhet eller avvisa det."; // Crash report -"google_analytics_use_prompt" = "Vill du hjälpa till att förbättra %@ genom att automatiskt skicka anonyma kraschrapporter och användningsdata?"; "e2e_need_log_in_again" = "Du behöver logga in igen för att generera nycklar för totalsträckskryptering för den här sessionen och skicka den publika nyckeln till din hemserver.\nDet behövs bara en gång; beklagar olägenheten."; "e2e_key_backup_wrong_version" = "En ny säkerhetskopia för säkra meddelandenycklar har upptäckts.\n\nOm det inte var du, ställ in en ny säkerhetsfras i inställningarna."; "bug_report_description" = "Vänligen beskriv buggen. Vad gjorde du? Vad förväntade du dig att hända? Vad hände egentligen?"; @@ -1163,19 +1153,19 @@ "searchable_directory_x_network" = "%@-nätverk"; "searchable_directory_search_placeholder" = "Namn eller ID"; "create_room_title" = "Nytt rum"; -"create_room_section_header_name" = "Rumsnamn"; +"create_room_section_header_name" = "NAMN"; "create_room_placeholder_name" = "Namn"; -"create_room_section_header_topic" = "Rumsämne (valfritt)"; -"create_room_placeholder_topic" = "Ämne"; -"create_room_section_header_encryption" = "Rumskryptering"; +"create_room_section_header_topic" = "ÄMNE (VALFRITT)"; +"create_room_placeholder_topic" = "Vad handlar rummet om?"; +"create_room_section_header_encryption" = "KRYPTERING"; "create_room_enable_encryption" = "Aktivera kryptering"; "create_room_section_footer_encryption" = "Kryptering kan inte inaktiveras igen."; -"create_room_section_header_type" = "Rumstyp"; -"create_room_type_private" = "Privat rum"; -"create_room_type_public" = "Offentligt rum"; +"create_room_section_header_type" = "ÅTKOMST"; +"create_room_type_private" = "Privat rum (enbart via inbjudan)"; +"create_room_type_public" = "Offentligt rum (vem som helst)"; "create_room_section_footer_type" = "Personer kan endast gå med i ett privat rum om de bjuds in."; -"create_room_show_in_directory" = "Visa rummet i katalogen"; -"create_room_section_header_address" = "Rumsadress"; +"create_room_show_in_directory" = "Visa i rumskatalogen"; +"create_room_section_header_address" = "ADRESS"; "create_room_placeholder_address" = "#testrum:matrix.org"; "room_info_list_one_member" = "1 medlem"; "room_info_list_several_members" = "%@ medlemmar"; @@ -1558,9 +1548,9 @@ "settings_about" = "OM"; "message_from_a_thread" = "Från en tråd"; "threads_empty_show_all_threads" = "Visa alla trådar"; -"threads_empty_tip" = "Tips: Tryck på ett meddelande och använd “Tråd” för att starta en."; -"threads_empty_info_my" = "Svara på en pågående tråd eller tryck på ett meddelande och använd “Tråd” för att starta en ny."; -"threads_empty_info_all" = "Trådar hjälper till att hålla dina konversationer till ämnet och lätta att följa."; +"threads_empty_tip" = "Tips: Tryck på ett meddelande och använd “Tråd” för att starta en tråd."; +"threads_empty_info_my" = "Svara i en pågående tråd eller tryck på ett meddelande och välj “Tråd” för att starta en ny tråd."; +"threads_empty_info_all" = "Trådar hjälper till att hålla dina konversationer till ämnet och gör dem lättare att följa."; "threads_empty_title" = "Håll diskussioner organiserade med trådar"; "threads_action_my_threads" = "Mina trådar"; "threads_action_all_threads" = "Alla trådar"; @@ -1725,7 +1715,6 @@ "settings" = "Inställningar"; "settings_enable_inapp_notifications" = "Aktivera aviseringar i appen"; "settings_enable_push_notifications" = "Aktivera pushnotiser"; -"settings_enter_validation_token_for" = "Ange valideringstoken för &@:"; "notification_settings_room_rule_title" = "Rum: '%@'"; // Devices "device_details_title" = "Sessionsinformation\n"; @@ -2063,7 +2052,6 @@ "attachment_small_with_resolution" = "Liten %@ (~%@)"; "attachment_size_prompt_message" = "Du kan stänga av detta i inställningarna."; "attachment_size_prompt_title" = "Bekräfta storlek att skicka"; -"room_displayname_all_other_participants_left" = "%@ (Kvar)"; "attachment_unsupported_preview_message" = "Den här filtypen stöds inte."; "attachment_unsupported_preview_title" = "Kunde inte förhandsgranska"; "room_displayname_all_other_members_left" = "%@ (Kvar)"; @@ -2073,3 +2061,192 @@ "settings_labs_use_only_latest_user_avatar_and_name" = "Visa senaste avatar och namn för användare i meddelandehistoriken"; "room_participants_leave_success" = "Lämnade rummet"; "room_participants_leave_processing" = "Lämnar"; +"spaces_creation_new_rooms_support" = "Support"; +"create_room_promotion_header" = "BEFORDRAN"; +"onboarding_celebration_button" = "Nu kör vi"; +"ignore_user" = "Ignorera användare"; +"location_sharing_pin_drop_share_title" = "Skicka denna position"; +"location_sharing_static_share_title" = "Skicka min nuvarande position"; +"live_location_sharing_banner_stop" = "Avsluta"; +"live_location_sharing_banner_title" = "Positionering i realtid är aktivt"; + +// MARK: Live location sharing + +"location_sharing_live_share_title" = "Dela position i realtid"; +"side_menu_coach_message" = "Svep åt höger eller tryck för att se alla rum"; +"spaces_add_room_missing_permission_message" = "Du är inte behörig att lägga till rum i detta utrymme."; +"spaces_creation_in_one_space" = "i 1 utrymme"; +"spaces_creation_in_many_spaces" = "i %@ utrymmen"; +"spaces_creation_in_spacename_plus_many" = "i %@ + %@ utrymmen"; +"spaces_creation_in_spacename_plus_one" = "i %@ + 1 utrymme"; +"spaces_creation_in_spacename" = "i %@"; +"spaces_creation_post_process_inviting_users" = "Bjuder in %@ användare"; +"spaces_creation_post_process_adding_rooms" = "Lägger till %@ rum"; +"spaces_creation_post_process_creating_room" = "Skapar %@"; +"spaces_creation_post_process_uploading_avatar" = "Laddar upp avatar"; +"spaces_creation_post_process_creating_space_task" = "Skapar %@"; +"spaces_creation_post_process_creating_space" = "Skapar utrymme"; +"spaces_creation_invite_by_username_message" = "Du kan bjuda in dem senare också."; +"spaces_creation_invite_by_username_title" = "Bjud in ditt team"; +"spaces_creation_invite_by_username" = "Bjud in genom användarnamn"; +"spaces_creation_add_rooms_message" = "Eftersom det här utrymmet endast är för dig så kommer ingen annan informeras. Du kan lägga till fler senare."; +"spaces_creation_add_rooms_title" = "Vad skulle du vilja lägga till?"; +"spaces_creation_sharing_type_me_and_teammates_detail" = "Ett privat utrymme för dig & dina teamkamrater"; +"spaces_creation_sharing_type_me_and_teammates_title" = "Jag och andra i mitt team"; +"spaces_creation_sharing_type_just_me_detail" = "Ett privat utrymme för att organisera dina rum"; +"spaces_creation_sharing_type_just_me_title" = "Endast jag"; +"spaces_creation_sharing_type_message" = "Se till att rätt personer har åtkomst till %@. Du kan ändra detta senare."; +"spaces_creation_sharing_type_title" = "Vilka arbetar du med?"; +"spaces_creation_email_invites_email_title" = "E-post"; +"spaces_creation_email_invites_message" = "Du kan bjuda in dem senare också."; +"spaces_creation_email_invites_title" = "Bjud in ditt team"; +"spaces_creation_new_rooms_random" = "Slumpmässigt"; +"spaces_creation_new_rooms_general" = "Allmänt"; +"spaces_creation_new_rooms_room_name_title" = "Namn på rummet"; +"spaces_creation_new_rooms_message" = "Vi kommer skapa ett rum för varje."; +"spaces_creation_new_rooms_title" = "Vad kommer några av samtalsämnena vara?"; +"spaces_creation_cancel_message" = "Dina val kommer inte sparas."; +"spaces_creation_cancel_title" = "Vill du avbryta att skapa ett utrymme?"; +"spaces_creation_private_space_title" = "Ditt privata utrymme"; +"spaces_creation_public_space_title" = "Ditt offentliga utrymme"; +"spaces_creation_address_already_exists" = "%@\nfinns redan"; +"spaces_creation_address_invalid_characters" = "%@\nhar ogiltiga tecken"; +"spaces_creation_address_default_message" = "Ditt utrymme kommer kunna ses på\n%@"; +"spaces_creation_empty_room_name_error" = "Namn krävs"; +"spaces_creation_address" = "Adress"; +"spaces_creation_settings_message" = "Lägg till lite mer info för att synas. Du kan ändra detta när som helst."; +"spaces_creation_footer" = "Du kan ändra detta senare"; +"spaces_creation_visibility_message" = "För att gå med i ett existerande utrymme så behöver du en inbjudan."; +"spaces_creation_visibility_title" = "Vilken typ av utrymme vill du skapa?"; + +// Mark: - Space Creation + +"spaces_creation_hint" = "Utrymmen är ett nytt sätt att gruppera rum och personer."; +"space_settings_current_address_message" = "Ditt utrymme kan ses på\n%@"; +"space_settings_update_failed_message" = "Misslyckades att uppdatera inställningar för utrymmet. Vill du försöka igen?"; +"space_settings_access_section" = "Vem har åtkomst till detta utrymmet?"; +"space_topic" = "Beskrivning"; +"space_public_join_rule_detail" = "Öppet för alla, passar bäst för öppna grupper"; +"spaces_add_space" = "Lägg till utrymme"; +"spaces_add_room" = "Lägg till rum"; +"spaces_invite_people" = "Bjud in andra"; +"space_private_join_rule_detail" = "Endast inbjudna, passar bäst för individer eller team"; +"spaces_explore_rooms_one_room" = "1 rum"; +"spaces_explore_rooms_room_number" = "%@ rum"; +"spaces_create_space_title" = "Skapa ett utrymme"; +"spaces_add_space_title" = "Skapa utrymme"; +"space_invite_not_enough_permission" = "Du är inte behörig att bjuda in andra till detta utrymme"; +"room_invite_not_enough_permission" = "Du är inte behörig att bjuda in andra till detta rum"; +"room_invite_to_room_option_detail" = "De kommer inte bli del av %@."; +"room_invite_to_room_option_title" = "Endast till detta rum"; +"room_invite_to_space_option_detail" = "De kommer kunna utforska %@, men kommer inte bli medlemmar i %@."; + +// Mark: - Room invite + +"room_invite_to_space_option_title" = "Till %@"; +"share_invite_link_space_text" = "Hej! Gå med i utrymmet här: %@"; +"share_invite_link_room_text" = "Hej! Gå med i rummet här: %@"; + +// MARK: - Share invite link + +"share_invite_link_action" = "Dela inbjudningslänk"; +"create_room_processing" = "Skapar rum"; +"create_room_suggest_room_footer" = "Rekommenderade rum visas för medlemmar i utrymmet som förslag på bra ställen att gå med."; +"create_room_suggest_room" = "Föreslå för utrymmesmedlemmar"; +"create_room_show_in_directory_footer" = "Detta kommer underlätta för andra att hitta och gå med."; +"create_room_section_footer_type_public" = "Endast inbjudna kan hitta och gå med, inte bara personer i utrymmet."; +"create_room_section_footer_type_restricted" = "Vem som helst i utrymmet kan hitta och gå med."; +"create_room_section_footer_type_private" = "Endast inbjudna personer kan hitta och gå med."; +"create_room_type_restricted" = "Utrymmesmedlemmar"; +"call_jitsi_unable_to_start" = "Kunde inte starta gruppsamtal"; +"room_suggestion_settings_screen_message" = "Rekommenderade rum visas för medlemmar i utrymmet, som bra förslag på bra ställen att gå med i."; +"room_suggestion_settings_screen_title" = "Gör så att ett rum blir rekommenderat i ett utrymme"; + +// Room suggestion Settings +"room_suggestion_settings_screen_nav_title" = "Föreslå rum"; +"room_access_space_chooser_other_spaces_section_info" = "Administratörer i %@ är troligen även med i dessa."; +"room_access_space_chooser_other_spaces_section" = "Andra utrymmen eller rum"; +"room_access_space_chooser_known_spaces_section" = "Utrymmen du känner till som innehåller %@"; +"room_access_settings_screen_setting_room_access" = "Ställer in rumsåtkomst"; +"room_access_settings_screen_upgrade_alert_upgrading" = "Uppgraderar rum"; +"room_access_settings_screen_upgrade_alert_upgrade_button" = "Uppgradera"; +"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "Bjud automatiskt in medlemmar till nya rum"; +"room_access_settings_screen_upgrade_alert_title" = "Uppgradera rum"; +"room_access_settings_screen_public_message" = "Vem som helst kan hitta och gå med."; +"room_access_settings_screen_edit_spaces" = "Redigera utrymmen"; +"room_access_settings_screen_upgrade_required" = "Uppgradering krävs"; +"room_access_settings_screen_restricted_message" = "Tillåt vem som helst i ett utrymme att hitta och gå med.\nDu kommer behöva bekräfta vilka utrymmen det gäller."; +"room_access_settings_screen_private_message" = "Endast inbjudna kan hitta och gå med."; +"room_access_settings_screen_message" = "Välj vilka som kan hitta och gå med i %@."; +"room_access_settings_screen_title" = "Vika kan komma åt detta rum?"; + +// Room Access Settings +"room_access_settings_screen_nav_title" = "Rumsåtkomst"; +"room_details_promote_room_suggest_title" = "Föreslå utrymme för medlemmar"; +"room_details_promote_room_title" = "Befordra rum"; +"room_details_access_row_title" = "Åtkomst"; +"settings_labs_enable_auto_report_decryption_errors" = "Rapportera avkrypteringsfel automatiskt"; +"room_preview_decline_invitation_options" = "Vill du avböja inbjudan eller ignorera denna användare?"; +"threads_beta_cancel" = "Inte nu"; +"threads_beta_enable" = "Prova"; +"threads_beta_information_link" = "Läs mer"; +"threads_beta_information" = "Håll diskussioner organiserade med trådar.\n\nTrådar hjälper dig hålla konversationer till ämnet och gör dem lättare att följa. "; +"threads_beta_title" = "Trådar"; +"threads_notice_done" = "Jag förstår"; +"threads_notice_information" = "Alla trådar som skapades under experimentperioden kommer nu att skrivas ut som vanliga svar.

Detta kommer vara en engångshändelse under övergången, eftersom trådar nu är en del av Matrix-specen."; +"threads_notice_title" = "Trådar är inte längre experimentella 🎉"; +"room_participants_invite_prompt_to_msg" = "Är du säker på att du vill bjuda in %@ till %@?"; +"onboarding_celebration_message" = "Dina val har sparats."; +"onboarding_celebration_title" = "Nu är du klar!"; +"onboarding_avatar_accessibility_label" = "Profilbild"; +"onboarding_avatar_message" = "Du kan ändra detta när som helst."; +"onboarding_avatar_title" = "Lägg till en profilbild"; +"onboarding_display_name_max_length" = "Ditt visningsnamn måste vara kortare än 256 tecken"; +"onboarding_display_name_hint" = "Du kan ändra detta senare"; +"onboarding_display_name_placeholder" = "Visningsnamn"; +"onboarding_display_name_message" = "Detta kommer visas när du skickar meddelanden."; +"onboarding_display_name_title" = "Välj visningsnamn"; +"onboarding_personalization_skip" = "Hoppa över steget"; +"onboarding_personalization_save" = "Spara och fortsätt"; +"onboarding_congratulations_home_button" = "Ta mig hem"; +"onboarding_congratulations_personalize_button" = "Personifiera profil"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "Ditt konto %@ har skapats."; +"onboarding_congratulations_title" = "Grattis!"; +"new_word" = "Ny"; +"joining" = "Går med"; +"saving" = "Sparar"; + +// Activities +"loading" = "Laddar"; +"edit" = "Redigera"; +"suggest" = "Föreslå"; +"add" = "Lägg till"; +"existing" = "Existerande"; +"stop" = "Stopp"; +"settings_enter_validation_token_for" = "Ange valideringstoken för %@:"; +"location_sharing_live_list_item_stop_sharing_action" = "Sluta dela"; +"location_sharing_live_list_item_current_user_display_name" = "Du"; +"location_sharing_live_list_item_last_update_invalid" = "Okänd senast uppdatering"; +"location_sharing_live_list_item_last_update" = "Uppdaterades för %@ sen"; +"location_sharing_live_list_item_sharing_expired" = "Delning löpte ut"; +"location_sharing_live_list_item_time_left" = "%@ kvar"; +"location_sharing_live_viewer_title" = "Plats"; +"location_sharing_live_map_callout_title" = "Dela plats"; +"call_no_stun_server_error_message_2" = "Alternativt kan du testa den offentliga servern på %@, men det här är inte lika tillförlitligt och det delar din IP-adress med den servern. Du kan också hantera detta i inställningarna"; +"group_participants_invite_prompt_msg" = "Är du säker på att du vill bjuda in %@ till den här gruppen?"; +"group_participants_remove_prompt_msg" = "Är du säker på att du vill ta bort %@ från den här gruppen?"; +"room_access_settings_screen_upgrade_alert_note" = "Observera att uppgradering kommer att skapa en ny version av rummet. Alla nuvarande meddelanden kommer att bli kvar i det här arkiverade rummet."; +"room_access_settings_screen_upgrade_alert_message_no_param" = "Vem som helst i överutrymmet kommer kunna hitta och gå med i rummet - du behöver inte bjuda in någon manuellt. Du kan ändra detta i inställningarna när som helst."; +"room_access_settings_screen_upgrade_alert_message" = "Vem som helst i %@ kommer kunna hitta och gå med i rummet - du behöver inte bjuda in någon manuellt. Du kan ändra detta i inställningarna när som helst."; +"settings_presence_offline_mode_description" = "Om det är aktiverat så kommer du alltid visas som offline för andra användare, även när du använder applikationen."; +"settings_presence_offline_mode" = "Offlineläge"; +"settings_presence" = "Närvaro"; + +// Room Preview +"room_preview_invitation_format" = "Du har blivit inbjuden till det här rummet av %@"; +"threads_discourage_information_2" = "\n\nVill du aktivera trådar ändå?"; +"threads_discourage_information_1" = "Din hemserver stöder för tillfället inte trådar, så den här funktionen kan vara opålitlig. Vissa trådade meddelanden kanske inte alltid är tillgängliga. "; +"room_new_messages_notification" = "%d nya meddelanden"; +"room_participants_invite_prompt_msg" = "Är du säker på att du vill bjuda in %@ till den här chatten?"; +"room_participants_remove_prompt_msg" = "Är du säker på att du vill ta bort %@ från den här chatten?"; diff --git a/Riot/Assets/th.lproj/Localizable.strings b/Riot/Assets/th.lproj/Localizable.strings index 46994a59a..a6272922c 100644 --- a/Riot/Assets/th.lproj/Localizable.strings +++ b/Riot/Assets/th.lproj/Localizable.strings @@ -21,7 +21,6 @@ /** Image Messages **/ /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ ได้ส่งรูปภาพ %@"; /* Incoming named video conference invite from a specific person */ "VIDEO_CONF_NAMED_FROM_USER" = "การโทรกลุ่มวิดีโอจาก %@: '%@'"; diff --git a/Riot/Assets/third_party_licenses.html b/Riot/Assets/third_party_licenses.html index 8d973a9a6..bfbc72922 100644 --- a/Riot/Assets/third_party_licenses.html +++ b/Riot/Assets/third_party_licenses.html @@ -1950,6 +1950,31 @@ Library. CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+
  • + DSBottomSheet (https://github.com/danielsaidi/BottomSheet) +

    + MIT License +

    + Copyright (c) 2021 Daniel Saidi +

    + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: +

    + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. +

    + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +

    +
  • diff --git a/Riot/Assets/tzm.lproj/Localizable.strings b/Riot/Assets/tzm.lproj/Localizable.strings index 3c80b883e..6d11b29c2 100644 --- a/Riot/Assets/tzm.lproj/Localizable.strings +++ b/Riot/Assets/tzm.lproj/Localizable.strings @@ -17,7 +17,6 @@ /** Image Messages **/ /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "yuzen %@ yat twelaft %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "ifser%@ yat twelaft %@ g %@"; diff --git a/Riot/Assets/uk.lproj/Localizable.strings b/Riot/Assets/uk.lproj/Localizable.strings index cbf5f4781..276df4c3e 100644 --- a/Riot/Assets/uk.lproj/Localizable.strings +++ b/Riot/Assets/uk.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ надсилає зображення %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ оприлюднює зображення %@ в %@"; /* A single unread message in a room */ diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index c3b7f33a4..eecaf5ebc 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -305,10 +305,10 @@ "bug_report_send" = "Надіслати"; "room_details_topic" = "Тема"; "room_details_room_name_for_dm" = "Назва"; -"create_room_placeholder_topic" = "Тема"; -"create_room_section_header_topic" = "Тема кімнати (необов'язково)"; +"create_room_placeholder_topic" = "Про що ця кімната?"; +"create_room_section_header_topic" = "ТЕМА (НЕОБОВ'ЯЗКОВО)"; "create_room_placeholder_name" = "Назва"; -"create_room_section_header_name" = "Назва кімнати"; +"create_room_section_header_name" = "НАЗВА"; // MARK: - Create Room @@ -549,7 +549,7 @@ "room_participants_action_unban" = "Розблокувати"; "room_participants_action_ban" = "Заблокувати у цій кімнаті"; "room_intro_cell_information_dm_sentence1_part1" = "Це початок вашого особистого спілкування з "; -"create_room_show_in_directory" = "Показати кімнату в каталозі"; +"create_room_show_in_directory" = "Показати в каталозі кімнат"; "directory_server_placeholder" = "matrix.org"; "directory_server_all_rooms" = "Усі кімнати на сервері %@"; "directory_server_picker_title" = "Вибрати каталог"; @@ -610,13 +610,13 @@ "home_empty_view_title" = "Вітаємо у %@,\n%@"; "call_transfer_users" = "Користувачі"; "create_room_placeholder_address" = "#testroom:matrix.org"; -"create_room_section_header_address" = "Адреса кімнати"; -"create_room_type_public" = "Загальнодоступна кімната"; -"create_room_type_private" = "Приватна кімната"; -"create_room_section_header_type" = "Тип кімнати"; +"create_room_section_header_address" = "АДРЕСА"; +"create_room_type_public" = "Загальнодоступна кімната (будь-хто)"; +"create_room_type_private" = "Приватна кімната (лише за запрошенням)"; +"create_room_section_header_type" = "ХТО МАЄ ДОСТУП"; "create_room_section_footer_encryption" = "Шифрування не може бути вимкнено."; "create_room_enable_encryption" = "Увімкнути шифрування"; -"create_room_section_header_encryption" = "Шифрування кімнати"; +"create_room_section_header_encryption" = "ШИФРУВАННЯ"; "biometrics_desetup_disable_button_title_x" = "Вимкнути %@"; "biometrics_desetup_title_x" = "Вимкнути %@"; "biometrics_setup_subtitle" = "Заощадьте час"; @@ -631,7 +631,6 @@ "pin_protection_settings_change_pin" = "Змінити PIN-код"; "pin_protection_settings_enable_pin" = "Увімкнути PIN-код"; "pin_protection_settings_enabled_forced" = "PIN-код увімкнено"; -"pin_protection_settings_section_header_x" = "PIN-код і %@"; "pin_protection_mismatch_error_message" = "Повторіть спробу"; "pin_protection_mismatch_error_title" = "PIN-коди відрізняються"; "pin_protection_reset_alert_action_reset" = "Скинути"; @@ -1062,7 +1061,6 @@ "settings_config_user_id" = "Ви ввійшли як %@"; "unknown_devices_alert" = "Кімната містить сеанси, які досі не пройшли звірку.\nТобто нема гарантії, що ці сеанси належать користувачам, від імені яких вони створені.\nРадимо звірити кожен сеанс, перш ніж продовжити; але за потреби можете повторити надсилання повідомлення без звірки."; "room_action_camera" = "Зробити світлину або відео"; -"room_ongoing_conference_call_with_close" = "Відбувається конференц-виклик. Приєднатись як %1$s чи %2$s. %@ його."; "room_member_power_level_short_custom" = "Інше"; "room_member_power_level_custom_in" = "Інше (%@) у %@"; "room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Поки жоден сервер ідентифікації не налаштований, ви не можете почати бесіду з кимось за адресою е-пошти."; @@ -1160,7 +1158,7 @@ "settings_third_party_notices" = "Примітки третіх сторін"; "settings_labs_enable_ringing_for_group_calls" = "Дзвінок групових викликів"; "settings_labs_enabled_polls" = "Опитування"; -"settings_labs_create_conference_with_jitsi" = "Створити конференц-виклик за допомогою jitsi"; +"settings_labs_create_conference_with_jitsi" = "Створити груповий виклик за допомогою jitsi"; "settings_labs_e2e_encryption_prompt_message" = "Щоб завершити налаштування шифрування вам потрібно повторно увійти."; "settings_labs_e2e_encryption" = "Наскрізне шифрування"; "settings_unignore_user" = "Показати всі повідомлення від %@?"; @@ -1195,8 +1193,7 @@ "room_message_edits_history_title" = "Редагування повідомлення"; "room_resource_usage_limit_reached_message_1_monthly_active_user" = "Цей домашній сервер досяг свого місячного обмеження активних користувачів, тож "; "room_resource_usage_limit_reached_message_1_default" = "Цей домашній сервер досягнув одного зі своїх лімітів ресурсів, тож "; -"room_conference_call_no_power" = "Для керування конференц-викликами у цій кімнаті потрібен дозвіл"; -"room_ongoing_conference_call" = "Відбувається конференц-виклик. Приєднатись як %1$s чи %2$s."; +"room_conference_call_no_power" = "Для керування груповими викликами у цій кімнаті потрібен дозвіл"; "room_participants_security_information_room_encrypted_for_dm" = "Повідомлення тут захищені наскрізним шифруванням.\n\nВаші повідомлення захищені замками, тож лише ви та отримувач маєте унікальні ключі для їхнього відмикання."; "room_participants_security_information_room_encrypted" = "Повідомлення тут захищені наскрізним шифруванням.\n\nВаші повідомлення захищені замками, тож лише ви та отримувачі мають унікальні ключі для їхнього відмикання."; "room_participants_action_security_status_complete_security" = "Завершити налаштування безпеки"; @@ -1211,7 +1208,6 @@ "room_preview_subtitle" = "Це попередній перегляд кімнати. Ви в режимі лише читання."; // Room Preview -"room_preview_invitation_format" = "%s запрошує вас приєднатися до цієї кімнати"; // Unknown devices "unknown_devices_alert_title" = "Кімната містить невідомі сеанси"; @@ -1468,7 +1464,7 @@ "analytics_prompt_point_3" = "Можете вимкнути це коли завгодно в налаштуваннях"; "analytics_prompt_not_now" = "Відкласти"; "analytics_prompt_yes" = "Так, усе гаразд"; -"analytics_prompt_stop" = "Більше не надсилати"; +"analytics_prompt_stop" = "Припинити надсилання"; "analytics_prompt_terms_link_upgrade" = "тут"; /* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ "analytics_prompt_terms_upgrade" = "Прочитайте всі наші умови %@. Згодні з ними?"; @@ -1509,7 +1505,7 @@ "call_no_stun_server_error_message_2" = "Також ви можете спробувати публічний сервер %@, але це буде менш надійно й сервер бачитиме вашу IP-адресу. Ви можете керувати цим у налаштуваннях"; "call_no_stun_server_error_message_1" = "Запропонуйте адміністратору вашого домашнього сервера %@ налаштувати сервер TURN для надійної роботи викликів."; "call_no_stun_server_error_title" = "Не вдалося зателефонувати через неправильно налаштований сервер"; -"call_jitsi_error" = "Не вдалося приєднатися до конференції."; +"call_jitsi_error" = "Не вдалося приєднатися до групового виклику."; "call_already_displayed" = "Виклик уже триває."; "photo_library_access_not_granted" = "%@ не має доступу до медіатеки, просимо змінити налаштування приватності"; "camera_unavailable" = "Камера недоступна на вашому пристрої"; @@ -1517,12 +1513,12 @@ "rage_shake_prompt" = "Схоже, ви розчаровано струсили телефон. Бажаєте надіслати звіт про ваду?"; "bug_report_prompt" = "При останньому запуску застосунок закрився з помилкою. Бажаєте надіслати звіт про помилку?"; "homeserver_connection_lost" = "Не вдалося з'єднатися з домашнім сервером."; -"event_formatter_jitsi_widget_removed_by_you" = "Ви вилучаєте голосову конференцію"; -"event_formatter_jitsi_widget_added_by_you" = "Ви додаєте голосову конференцію"; +"event_formatter_jitsi_widget_removed_by_you" = "Ви вилучили груповий голосовий виклик"; +"event_formatter_jitsi_widget_added_by_you" = "Ви додали груповий голосовий виклик"; "event_formatter_rerequest_keys_part2" = " ваших інших сеансів."; "event_formatter_rerequest_keys_part1_link" = "Повторити запит ключів шифрування"; -"event_formatter_jitsi_widget_removed" = "%@ вилучає голосову конференцію"; -"event_formatter_jitsi_widget_added" = "%@ додає голосову конференцію"; +"event_formatter_jitsi_widget_removed" = "%@ вилучає голосовий груповий виклик"; +"event_formatter_jitsi_widget_added" = "%@ додає груповий голосовий виклик"; "event_formatter_widget_removed" = "%@ віджет видалено %@"; "event_formatter_widget_added" = "%@ віджет додано %@"; @@ -2188,7 +2184,6 @@ "settings_enter_validation_token_for" = "Введіть токен підтвердження для %@:"; "settings_enable_push_notifications" = "Увімкнути push-сповіщення"; "settings_enable_inapp_notifications" = "Увімкнути сповіщення в застосунку"; -"room_displayname_all_other_participants_left" = "%@ (виходить)"; "notice_crypto_error_unknown_inbound_session_id" = "Сеанс відправника не надіслав нам ключі для цього повідомлення."; "notice_room_power_level_event_requirement" = "Найнижчий рівень повноважень пов'язаний з подією:"; "notification_settings_global_info" = "Налаштування сповіщень зберігаються у вашому обліковому записі й спільні для всіх клієнтів, які їх підтримують (включно зі сповіщеннями стільниці).\n\nПравила застосовуються по черзі; спочатку надсилається повідомлення першого збігу з правилом.\nОтже: сповіщення для кожного слова важливіші за сповіщення для кожної кімнати, які важливіші за сповіщення від кожного відправника.\nДля кількох правил одного виду важливіше перше у списку."; @@ -2259,7 +2254,7 @@ "notice_room_topic_removed_by_you" = "Ви вилучили тему"; "notice_room_name_removed_by_you_for_dm" = "Ви вилучили назву"; "notice_room_name_removed_by_you" = "Ви вилучили назву кімнати"; -"notice_conference_call_request_by_you" = "Ви запитали VoIP-конференцію"; +"notice_conference_call_request_by_you" = "Ви запросили на голосовий груповий виклик"; "notice_answered_video_call_by_you" = "Ви відповіли на виклик"; "notice_placed_video_call_by_you" = "Ви розпочали відеовиклик"; "notice_placed_voice_call_by_you" = "Ви розпочали голосовий виклик"; @@ -2269,9 +2264,9 @@ "notice_display_name_removed_by_you" = "Ви вилучили показуване ім'я"; "notice_display_name_changed_from_by_you" = "Ви змінили показуване ім'я з %@ на %@"; "notice_display_name_set_by_you" = "Ви вказали показуваним іменем %@"; -"notice_conference_call_finished" = "VoIP-конференція завершилася"; -"notice_conference_call_started" = "VoIP-конференція розпочалася"; -"notice_conference_call_request" = "%@ запитує VoIP-конференцію"; +"notice_conference_call_finished" = "Голосовий груповий виклик завершено"; +"notice_conference_call_started" = "Груповий голосовий виклик розпочато"; +"notice_conference_call_request" = "%@ запрошує до групового голосового виклику"; "notice_declined_video_call" = "%@ відхиляє виклик"; "notice_ended_video_call" = "%@ завершує виклик"; "notice_answered_video_call" = "%@ відповідає на виклик"; @@ -2280,7 +2275,7 @@ // Room members "room_member_ignore_prompt" = "Ви впевнені, що хочете сховати всі повідомлення від цього користувача?"; -"room_no_conference_call_in_encrypted_rooms" = "Кімнати з шифруванням не підтримують конференцвиклики"; +"room_no_conference_call_in_encrypted_rooms" = "Кімнати з шифруванням не підтримують групові виклики"; "room_error_topic_edition_not_authorized" = "Ви не маєте повноважень змінювати тему цієї кімнати"; "room_error_name_edition_not_authorized" = "Ви не маєте повноважень змінювати назву цієї кімнати"; "room_error_join_failed_empty_room" = "На цю мить неможливо приєднатися до порожньої кімнати."; @@ -2298,7 +2293,7 @@ "attachment_small_with_resolution" = "Малий %@ (~%@)"; "attachment_size_prompt_message" = "Ви можете вимкнути це у налаштуваннях."; "attachment_size_prompt_title" = "Підтвердити розмір, щоб надіслати"; -"room_no_power_to_create_conference_call" = "Вам потрібен дозвіл, щоб надсилати запрошення, щоб розпочати конференцію в цій кімнаті"; +"room_no_power_to_create_conference_call" = "Вам потрібен дозвіл на надсилання запрошення, щоб розпочати груповий виклик у цій кімнаті"; "room_event_encryption_verify_message" = "Щоб переконатися, що цьому сеансу можна довіряти, зв’яжіться з його власником іншим способом (наприклад, особисто чи телефоном) і запитайте його, чи збігається ключ, який вони бачать у налаштуваннях користувача для цього сеансу, з ключем нижче:\n\nНазва сеансу: %@\nID сеансу: %@\nКлюч сеансу: %@\n\nЯкщо він збігається, натисніть кнопку підтвердження внизу. Якщо ні, значить хтось інший перехоплює цей сеанс, і ви, ймовірно, хочете натиснути кнопку чорного списку.\n\nУ майбутньому цей процес перевірки буде ускладнено."; "call_more_actions_hold" = "Утримувати"; "call_holded" = "Ви утримуєте виклик"; @@ -2332,3 +2327,187 @@ "room_participants_leave_processing" = "Вихід триває"; "notice_error_unformattable_event" = "** Неможливо показати повідомлення. Надішліть звіт про помилку"; "settings_labs_use_only_latest_user_avatar_and_name" = "Показувати останній аватар та ім'я для користувачів у історії повідомлень"; +"side_menu_coach_message" = "Проведіть праворуч або торкніть, щоб переглянути всі кімнати"; +"ignore_user" = "Нехтувати користувачем"; +"location_sharing_pin_drop_share_title" = "Надіслати ці координати"; +"location_sharing_static_share_title" = "Надіслати моє поточне місцеперебування"; +"live_location_sharing_banner_stop" = "Припинити"; +"live_location_sharing_banner_title" = "Місцеперебування поширюється наживо"; + +// MARK: Live location sharing + +"location_sharing_live_share_title" = "Поширити місцеперебування наживо"; +"spaces_add_room_missing_permission_message" = "Ви не маєте дозволу додавати кімнати в цей простір."; +"spaces_creation_in_one_space" = "в 1 простір"; +"spaces_creation_in_many_spaces" = "у %@ простори"; +"spaces_creation_in_spacename_plus_many" = "у %@ + %@ простори"; +"spaces_creation_in_spacename_plus_one" = "у %@ + 1 простір"; +"spaces_creation_in_spacename" = "у %@"; +"spaces_creation_post_process_inviting_users" = "Запрошення %@ користувачів"; +"spaces_creation_post_process_adding_rooms" = "Додання %@ кімнат"; +"spaces_creation_post_process_creating_room" = "Створення %@"; +"spaces_creation_post_process_uploading_avatar" = "Завантаження аватару"; +"spaces_creation_post_process_creating_space_task" = "Створення %@"; +"spaces_creation_post_process_creating_space" = "Створення простору"; +"spaces_creation_invite_by_username_message" = "Можете запросити їх і пізніше."; +"spaces_creation_invite_by_username_title" = "Запросіть свою команду"; +"spaces_creation_invite_by_username" = "Запросити за користувацьким іменем"; +"spaces_creation_add_rooms_message" = "Оскільки це лише ваш простір, ніхто не отримає сповіщень. Згодом можете додати більше."; +"spaces_creation_add_rooms_title" = "Що варто сюди додати?"; +"spaces_creation_sharing_type_me_and_teammates_detail" = "Приватний простір для всієї вашої команди"; +"spaces_creation_sharing_type_me_and_teammates_title" = "Спільно з командою"; +"spaces_creation_sharing_type_just_me_detail" = "Приватний простір для впорядкування власних кімнат"; +"spaces_creation_sharing_type_just_me_title" = "Ні з ким"; +"spaces_creation_sharing_type_message" = "Надайте доступ до %@ всім, кому потрібно. Це можна змінити пізніше."; +"spaces_creation_sharing_type_title" = "Із ким ви працюєте разом?"; +"spaces_creation_email_invites_email_title" = "Е-пошта"; +"spaces_creation_email_invites_message" = "Можете запросити їх і пізніше."; +"spaces_creation_email_invites_title" = "Запросіть свою команду"; +"spaces_creation_new_rooms_support" = "Підтримка"; +"spaces_creation_new_rooms_random" = "Різне"; +"spaces_creation_new_rooms_general" = "Загальне"; +"spaces_creation_new_rooms_room_name_title" = "Назва кімнати"; +"spaces_creation_new_rooms_message" = "Ми створимо по кімнаті для кожної."; +"spaces_creation_new_rooms_title" = "Які теми ви плануєте обговорити?"; +"spaces_creation_cancel_message" = "Введені дані буде втрачено."; +"spaces_creation_cancel_title" = "Припинити створення простору?"; +"spaces_creation_private_space_title" = "Ваш приватний простір"; +"spaces_creation_public_space_title" = "Ваш загальнодоступний простір"; +"spaces_creation_address_already_exists" = "%@\nвже існує"; +"spaces_creation_address_invalid_characters" = "%@\nмістить хибні символи"; +"spaces_creation_address_default_message" = "Ваш простір буде можна переглянути як\n%@"; +"spaces_creation_empty_room_name_error" = "Назва обов'язкова"; +"spaces_creation_address" = "Адреса"; +"spaces_creation_settings_message" = "Уточніть, що вирізняє вашу кімнату. Ви зможете будь-коли це відредагувати."; +"spaces_creation_footer" = "Можете змінити це згодом"; +"spaces_creation_visibility_message" = "Щоб приєднатися до наявного простору, вам потрібне запрошення."; +"spaces_creation_visibility_title" = "Простір якого типу ви хочете створити?"; + +// Mark: - Space Creation + +"spaces_creation_hint" = "Простори — це новий спосіб згуртувати кімнати та людей."; +"space_settings_current_address_message" = "Ваш простір можна переглянути як\n%@"; +"space_settings_update_failed_message" = "Не вдалося оновити налаштування простору. Повторити спробу?"; +"space_settings_access_section" = "Хто має доступ до цього простору?"; +"space_topic" = "Опис"; +"space_public_join_rule_detail" = "Відкрито для всіх — найкраще для спільнот"; +"spaces_add_space" = "Додати простір"; +"spaces_add_room" = "Додати кімнату"; +"spaces_invite_people" = "Запросити людей"; +"space_private_join_rule_detail" = "Лише за запрошенням — найкраще для особистого чи командного використання"; +"spaces_explore_rooms_one_room" = "1 кімната"; +"spaces_explore_rooms_room_number" = "%@ кімнат"; +"spaces_create_space_title" = "Створити простір"; +"spaces_add_space_title" = "Створити простір"; +"space_invite_not_enough_permission" = "Ви не маєте дозволу запрошувати людей до цього простору"; +"room_invite_not_enough_permission" = "Ви не маєте дозволу запрошувати людей до цієї кімнати"; +"room_invite_to_room_option_detail" = "Їх не буде додано до %@."; +"room_invite_to_room_option_title" = "Лише до цієї кімнати"; +"room_invite_to_space_option_detail" = "Вони зможуть досліджувати %@, але не стануть учасником %@."; + +// Mark: - Room invite + +"room_invite_to_space_option_title" = "До %@"; +"share_invite_link_space_text" = "Раджу цей простір в %@"; +"share_invite_link_room_text" = "Раджу цю кімнату в %@"; + +// MARK: - Share invite link + +"share_invite_link_action" = "Поширити запрошувальне посилання"; +"create_room_processing" = "Створення кімнати"; +"create_room_suggest_room_footer" = "Пропоновані кімнати рекомендуються учасникам простору як варті приєднання."; +"create_room_suggest_room" = "Пропонувати учасникам простору"; +"create_room_show_in_directory_footer" = "Це допомагатиме людям знаходити й приєднуватися."; +"create_room_promotion_header" = "РЕКОМЕНДАЦІЯ"; +"create_room_section_footer_type_public" = "Знайти й приєднатись можуть лише запрошені, а не всі у Space name."; +"create_room_section_footer_type_restricted" = "Будь-хто в Space name може знайти й приєднатися."; +"create_room_section_footer_type_private" = "Лише запрошені можуть знайти й приєднатися."; +"create_room_type_restricted" = "Учасники простору"; +"call_jitsi_unable_to_start" = "Не вдалося розпочати груповий виклик"; +"room_suggestion_settings_screen_message" = "Пропоновані кімнати рекомендуються учасникам простору як варті приєднання."; +"room_suggestion_settings_screen_title" = "Зробіть кімнату пропонованою для простору"; + +// Room suggestion Settings +"room_suggestion_settings_screen_nav_title" = "Запропонувати кімнату"; +"room_access_space_chooser_other_spaces_section_info" = "Ймовірно, в переліченому бере участь решта адміністрації %@."; +"room_access_space_chooser_other_spaces_section" = "Інші простори чи кімнати"; +"room_access_space_chooser_known_spaces_section" = "Відомі вам простори, що містять %@"; +"room_access_settings_screen_setting_room_access" = "Налаштування доступу до кімнати"; +"room_access_settings_screen_upgrade_alert_upgrading" = "Поліпшення кімнати"; +"room_access_settings_screen_upgrade_alert_upgrade_button" = "Поліпшити"; +"room_access_settings_screen_upgrade_alert_auto_invite_switch" = "Автоматично запросити учасників до нової кімнати"; +"room_access_settings_screen_upgrade_alert_title" = "Покращити кімнату"; +"room_access_settings_screen_public_message" = "Будь-хто може знайти й приєднатися."; +"room_access_settings_screen_edit_spaces" = "Редагувати простори"; +"room_access_settings_screen_upgrade_required" = "Покращення обов'язкове"; +"room_access_settings_screen_restricted_message" = "Дозвольте всім у просторі знаходити й приєднуватися.\nВас буде запитано підтвердити з яких саме просторів."; +"room_access_settings_screen_private_message" = "Лише запрошені люди можуть знайти й приєднатися."; +"room_access_settings_screen_message" = "Визначте, хто може знайти й приєднатися до %@."; +"room_access_settings_screen_title" = "Хто має доступ до кімнати?"; + +// Room Access Settings +"room_access_settings_screen_nav_title" = "Доступ до кімнати"; +"room_details_promote_room_suggest_title" = "Порадити учасникам простору"; +"room_details_promote_room_title" = "Рекламувати кімнату"; +"room_details_access_row_title" = "Доступ"; +"settings_labs_enable_auto_report_decryption_errors" = "Автозвіт про помилки розшифрування"; +"room_preview_decline_invitation_options" = "Бажаєте відкласти запрошення або знехтувати користувача?"; +"threads_beta_cancel" = "Не зараз"; +"threads_beta_enable" = "Спробувати"; +"threads_beta_information_link" = "Докладніше"; +"threads_beta_information" = "Спілкуйтеся у тредах.\n\nТреди допомагають розмежовувати свої розмови за темами та легко стежити за ними. "; +"threads_beta_title" = "Треди"; +"threads_notice_done" = "Зрозуміло"; +"threads_notice_information" = "Усі треди, створені в експериментальному режимі, буде показано звичайними відповідями.

    Це буде одноразовим переходом, оскільки треди зараз є частиною специфікації Matrix."; +"threads_notice_title" = "Треди більше не експериментальна функція 🎉"; +"room_participants_invite_prompt_to_msg" = "Ви впевнені, що хочете запросити %@ до %@?"; +"onboarding_celebration_button" = "Поїхали"; +"onboarding_celebration_message" = "Ваші налаштування збережено."; +"onboarding_celebration_title" = "Усе налаштовано!"; +"onboarding_avatar_accessibility_label" = "Зображення профілю"; +"onboarding_avatar_message" = "Ви можете змінити його будь-коли."; +"onboarding_avatar_title" = "Додати зображення профілю"; +"onboarding_display_name_max_length" = "Ваше показуване ім'я повинно складатися з менш ніж 256 символів"; +"onboarding_display_name_hint" = "Ви можете змінити його пізніше"; +"onboarding_display_name_placeholder" = "Показуване ім'я"; +"onboarding_display_name_message" = "Його буде показано у надісланих повідомленнях."; +"onboarding_display_name_title" = "Виберіть показуване ім'я"; +"onboarding_personalization_skip" = "Пропустити цей крок"; +"onboarding_personalization_save" = "Зберегти й продовжити"; +"onboarding_congratulations_home_button" = "На головну"; +"onboarding_congratulations_personalize_button" = "Персоналізувати профіль"; +/* The placeholder string contains the user's matrix ID */ +"onboarding_congratulations_message" = "Ваш обліковий запис %@ створено."; +"onboarding_congratulations_title" = "Вітаємо!"; +"saving" = "Збереження"; + +// Activities +"loading" = "Завантаження"; +"edit" = "Змінити"; +"suggest" = "Порадити"; +"add" = "Додати"; +"existing" = "Наявне"; +"new_word" = "Нове"; +"stop" = "Зупинити"; +"joining" = "Приєднання"; +"location_sharing_live_list_item_stop_sharing_action" = "Припинити надсилання"; +"location_sharing_live_list_item_current_user_display_name" = "Ви"; +"location_sharing_live_list_item_last_update_invalid" = "Час останнього оновлення невідомий"; +"location_sharing_live_list_item_last_update" = "Оновлено %@ тому"; +"location_sharing_live_list_item_sharing_expired" = "Надсилання завершено"; +"location_sharing_live_list_item_time_left" = "%@ виходить"; +"location_sharing_live_viewer_title" = "Місцеперебування"; +"location_sharing_live_map_callout_title" = "Поділитися місцеперебуванням"; +"room_access_settings_screen_upgrade_alert_note" = "Зауважте, що оновлення створить нову версію кімнати. Усі поточні повідомлення залишаться в цій архівованій кімнаті."; +"room_access_settings_screen_upgrade_alert_message_no_param" = "Будь-хто з батьківського простору зможе знайти цю кімнату та приєднатися до неї — не потрібно вручну запрошувати всіх. Ви зможете будь-коли змінити це в налаштуваннях кімнати."; +"room_access_settings_screen_upgrade_alert_message" = "Будь-хто в %@ зможе знайти та приєднатися до цієї кімнати — не потрібно вручну запрошувати всіх. Ви зможете змінити це в налаштуваннях кімнати у будь-який час."; +"settings_presence_offline_mode_description" = "Якщо ввімкнено, ваш статус завжди будете офлайн для інших користувачів, навіть коли вони користуються іншим застосунком."; +"settings_presence_offline_mode" = "Офлайн-режим"; +"settings_presence" = "Присутність"; + +// Room Preview +"room_preview_invitation_format" = "%@ запрошує вас приєднатися до цієї кімнати"; +"threads_discourage_information_2" = "\n\nУсе одно увімкнути треди?"; +"threads_discourage_information_1" = "Наразі ваш домашній сервер не підтримує треди, тому ця функція може працювати нестабільно. Деякі повідомлення у тредах можуть бути недоступними. "; +"room_ongoing_conference_call_with_close" = "Поточний груповий виклик. Приєднатися як %@ або %@. %@."; +"room_ongoing_conference_call" = "Поточний груповий виклик. Приєднатися як %@ або %@."; diff --git a/Riot/Assets/vi.lproj/Localizable.strings b/Riot/Assets/vi.lproj/Localizable.strings index 0e6d08a3c..bbad48963 100644 --- a/Riot/Assets/vi.lproj/Localizable.strings +++ b/Riot/Assets/vi.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ đã gửi bạn một hình ảnh %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ đã đăng một ảnh %@ trong %@"; /* Multiple unread messages in a room */ diff --git a/Riot/Assets/vi.lproj/Vector.strings b/Riot/Assets/vi.lproj/Vector.strings index b9b636103..f46d98bcc 100644 --- a/Riot/Assets/vi.lproj/Vector.strings +++ b/Riot/Assets/vi.lproj/Vector.strings @@ -144,7 +144,6 @@ "contacts_address_book_matrix_users_toggle" = "Chỉ người dùng của matrix"; "contacts_address_book_no_contact" = "Không có danh bạ nội bộ"; "contacts_address_book_permission_required" = "Quyền được yêu cầu để truy cập danh bạ nội bộ"; -"contacts_address_book_permission_denied" = "Bạn chưa cho phép Element truy cập danh bạ nội bộ của bạn"; "contacts_user_directory_section" = "DANH MỤC NGƯỜI DÙNG"; "contacts_user_directory_offline_section" = "DANH MỤC NGƯỜI DÙNG (ngoại tuyến)"; // Chat participants @@ -291,7 +290,6 @@ "settings_pin_rooms_with_missed_notif" = "Ghim các phòng có thông báo bị bỏ lỡ"; "settings_pin_rooms_with_unread" = "Ghim các phòng có các tin nhắn chưa đọc"; "settings_enable_callkit" = "Cuộc gọi tích hợp"; -"settings_callkit_info" = "Nhận cuộc gọi tới trên màn hình khóa. Xem lịch sử cuộc gọi trong lịch sử cuộc gọi của hệ thống. Nếu iCloud được kích hoạt, lịch sử cuộc gọi sẽ được chia sẻ với Apple."; "settings_ui_language" = "Ngôn ngữ"; "settings_ui_theme" = "Chủ đề"; "settings_ui_theme_auto" = "Tự động"; @@ -426,9 +424,7 @@ "no_voip_title" = "Cuộc gọi tới"; "no_voip" = "%@ đang gọi nhưng %@ chưa hỗ trợ gọi thoại.\nBạn có thể bỏ qua thông báo này và trả lời cuộc gọi từ thiết bị khác hoặc từ chối nó."; // Crash report -"google_analytics_use_prompt" = "Bạn có muốn giúp cải thiện %@ bằng cách tự động báo cáo sự cố vô danh và dữ liệu sử dụng?"; // Crypto -"e2e_enabling_on_app_update" = "Element đã hỗ trợ mã hóa end-to-end nhưng bạn cần phải đăng nhập lại để kích hoạt.\n\nBạn có thể thực hiện ngay bây giờ hoặc sau đó từ cài đặt của ứng dụng."; "e2e_need_log_in_again" = "Bạn cần phải đăng nhập lại để tạo các khoá mã hóa end-to-end cho thiết bị này và gửi khoá chung tới máy chủ nhà của bạn.\nĐây là một lần duy nhất; xin lỗi vì sự bất tiện."; // Bug report "bug_report_title" = "Báo cáo lỗi"; @@ -502,7 +498,6 @@ "space_private_join_rule" = "Space riêng tư"; "space_participants_action_ban" = "Cấm từ space này"; "space_participants_action_remove" = "Loại bỏ khỏi space này"; -"spaces_coming_soon_detail" = "Tính năng này chưa được triển khai ở đây, nhưng nó đang được tiến hành. Bây giờ, bạn có thể làm với Element trên máy tính của bạn."; "spaces_invites_coming_soon_title" = "Mời sắp có"; "spaces_add_rooms_coming_soon_title" = "Thêm phòng sắp có"; "spaces_coming_soon_title" = "Sắp có"; @@ -1711,9 +1706,7 @@ "notice_room_power_level_intro" = "Công suất của thành viên trong phòng:"; "notice_room_power_level_acting_requirement" = "Mức công suất tối thiểu mà người dùng phải có trước khi hoạt động là:"; "notice_room_power_level_event_requirement" = "Mức công suất tối thiểu liên quan đến sự kiện là:"; -"notice_room_aliases" = "Các bí danh phòng là:% @"; "notice_encrypted_message" = "Tin nhắn đã được mã hoá"; -"notice_encryption_enabled" = "%@ đã bật mã hoá end-to-end (thuật toán %@)"; "notice_image_attachment" = "hình ảnh đính kèm"; "notice_audio_attachment" = "âm thanh đính kèm"; "notice_video_attachment" = "video đính kèm"; @@ -1735,7 +1728,6 @@ // room display name "room_displayname_empty_room" = "Phòng trống"; "room_displayname_two_members" = "%@ và %@"; -"room_displayname_more_than_two_members" = "%@ và %u người khác"; // Settings "settings" = "Cài đặt"; "settings_enable_inapp_notifications" = "Bật thông báo bên trong ứng dụng"; diff --git a/Riot/Assets/vls.lproj/Localizable.strings b/Riot/Assets/vls.lproj/Localizable.strings index 39c3056a6..7b198c22d 100644 --- a/Riot/Assets/vls.lproj/Localizable.strings +++ b/Riot/Assets/vls.lproj/Localizable.strings @@ -13,7 +13,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@: * %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ èt e fotootje %@ gesteurd"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ èt e fotootje %@ in %@ geplatst"; /* A single unread message in a room */ diff --git a/Riot/Assets/vls.lproj/Vector.strings b/Riot/Assets/vls.lproj/Vector.strings index 284b7009f..066b8db13 100644 --- a/Riot/Assets/vls.lproj/Vector.strings +++ b/Riot/Assets/vls.lproj/Vector.strings @@ -187,7 +187,6 @@ "notice_room_aliases" = "De gespreksbynoamn zyn: %@"; "notice_room_related_groups" = "De groepn da geassocieerd zyn me da gesprek hier zyn: %@"; "notice_encrypted_message" = "Versleuterd bericht"; -"notice_encryption_enabled" = "%@ èt end-tout-end-versleuterienge angezet (%@-algoritme)"; "notice_image_attachment" = "fotobylage"; "notice_audio_attachment" = "geluudsbylage"; "notice_video_attachment" = "videobylage"; diff --git a/Riot/Assets/zh_Hans.lproj/Localizable.strings b/Riot/Assets/zh_Hans.lproj/Localizable.strings index ba1ba8c11..20458950e 100644 --- a/Riot/Assets/zh_Hans.lproj/Localizable.strings +++ b/Riot/Assets/zh_Hans.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@:* %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ 发送了一张图片:%@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ 发送了一张图片:%@(来自 %@)"; /* A single unread message in a room */ diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index 110c7814b..43483b2b1 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -125,7 +125,6 @@ "directory_cell_title" = "浏览目录"; "directory_cell_description" = "%tu 个聊天室"; "directory_search_results_title" = "浏览目录结果"; -"directory_search_results" = "搜索%@,找到了%tu个结果"; "directory_search_results_more_than" = ">找到了%tu个 %@的搜索结果"; "directory_searching_title" = "正在搜索目录…"; "directory_search_fail" = "获取数据失败"; @@ -183,7 +182,6 @@ "room_new_messages_notification" = "%d 条未读消息"; "room_one_user_is_typing" = "%@ 正在输入…"; "room_two_users_are_typing" = "%@ 和 %@ 正在输入…"; -"room_many_users_are_typing" = "%@、%@ 和 %@ 正在输入…"; "room_message_placeholder" = "发送消息(未加密)…"; "encrypted_room_message_placeholder" = "发送加密消息…"; "room_message_short_placeholder" = "发送消息…"; @@ -385,7 +383,6 @@ "no_voip_title" = "来电"; "no_voip" = "%@ 正在请求与您通话然而 %@ 尚不支持通话。\n您可以忽略此通知并通过其他设备接听,或者您可以拒绝接听。"; // Crash report -"google_analytics_use_prompt" = "您打算通过自动报告匿名的崩溃报告和使用数据来帮助提升 %@ 吗?"; // Crypto "e2e_enabling_on_app_update" = "%@ 现在支持端到端加密,但是您需要重新登录以启用它。\n\n您可以现在重新登录,也可以之后从应用程序设置中选择开启。"; "e2e_need_log_in_again" = "您需要登录回来以便为此会话生成端对端加密密钥并提交公钥到您的主服务器。\n这只需要做一次;很抱歉造成打扰。"; @@ -822,7 +819,6 @@ "room_widget_permission_room_id_permission" = "聊天室ID"; // Service terms "service_terms_modal_title" = "服务条款"; -"service_terms_modal_message" = "你需要接受此服务(%@)的条款以继续。"; "service_terms_modal_accept_button" = "接受"; "service_terms_modal_decline_button" = "拒绝"; "service_terms_modal_description_for_identity_server_1" = "用电话或邮箱找到别人"; @@ -830,7 +826,6 @@ "service_terms_modal_description_for_integration_manager" = "使用机器人,桥接,小插件和贴图包"; // Service terms - Variant for identity server when displayed out of a context "service_terms_modal_title_identity_server" = "通讯录发现"; -"service_terms_modal_message_identity_server" = "接受此身份认证服务器(%@)的条款以发现联系人。"; "service_terms_modal_policy_checkbox_accessibility_hint" = "查看以接受%@"; "key_backup_setup_intro_setup_connect_action_with_existing_backup" = "关联此设备到“密钥备份”"; "key_backup_recover_connent_banner_subtitle" = "关联此会话到“密钥备份”"; @@ -1001,7 +996,6 @@ "security_settings_cryptography" = "加密"; "security_settings_complete_security_alert_title" = "绝对安全"; "security_settings_complete_security_alert_message" = "您应该先完成当前会话的安全防护。"; -"security_settings_coming_soon" = "对不起。这个操作在 %@ iOS 版本上还不可用。请使用另一个 Matrix 客户端来设置它。Elment iOS 会沿用其他客户端的设置。"; // Recover from private key "key_backup_recover_from_private_key_info" = "备份恢复中…"; // MARK: - Device Verification @@ -1499,7 +1493,6 @@ "space_private_join_rule" = "私密空间"; "space_participants_action_ban" = "禁止进入这个空间"; "space_participants_action_remove" = "从这个空间删除"; -"spaces_coming_soon_detail" = "此特性尚未实现,还在开发。现在,您可以在计算机上使用 Element 来体验。"; "spaces_invites_coming_soon_title" = "邀请即将到来"; "spaces_coming_soon_title" = "即将到来"; "spaces_no_member_found_detail" = "寻找不在 %@ 中的某人?现在,你可以通过网页或桌面端邀请他们。"; @@ -1598,7 +1591,6 @@ /* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ "analytics_prompt_terms_new_user" = "你可以阅读我们所有的条款 %@。"; "analytics_prompt_message_upgrade" = "您之前同意与我们共享匿名使用数据。 现在,为了帮助了解人们如何使用多个设备,我们将生成一个随机标识符,由您的设备共享。"; -"analytics_prompt_message_new_user" = "通过共享匿名使用数据帮助我们发现问题并改进 Element。 为了了解人们如何使用多个设备,我们将生成一个随机标识符,由您的设备共享。"; // Analytics "analytics_prompt_title" = "帮助改进 %@"; @@ -1690,7 +1682,6 @@ "notice_room_power_level_event_requirement" = "事件所需的最小权限级别:"; "notice_room_aliases" = "此聊天室的别名是:%@"; "notice_encrypted_message" = "已加密消息"; -"notice_encryption_enabled" = "%@ 打开了端对端加密(算法 %@)"; "notice_image_attachment" = "图片附件"; "notice_audio_attachment" = "音频附件"; "notice_video_attachment" = "视频附件"; @@ -1708,7 +1699,6 @@ "notice_error_unknown_event_type" = "未知的事件类型"; "notice_room_history_visible_to_anyone" = "%@ 将未来的聊天室消息历史设为对所有人可见。"; "notice_room_history_visible_to_members" = "%@ 将未来的聊天室消息历史设为对所有聊天室成员可见。"; -"notice_room_history_visible_to_members_from_invited_point" = "你将未来的聊天室消息历史设为对所有聊天室成员可见,从他们被邀请时开始。"; "notice_room_history_visible_to_members_from_joined_point" = "%@ 将未来的聊天室消息历史设为对所有聊天室成员可见,从他们加入时开始。"; "notice_crypto_unable_to_decrypt" = "** 无法解密:%@ **"; "notice_crypto_error_unknown_inbound_session_id" = "发送者的会话没有向我们发送此消息的密钥。"; @@ -2073,7 +2063,6 @@ "attachment_small_with_resolution" = "小 %@ (~%@)"; "attachment_size_prompt_message" = "你可以在设置中关闭这个。"; "attachment_size_prompt_title" = "确认要发送的大小"; -"room_displayname_all_other_participants_left" = "%@ (离开)"; "room_displayname_all_other_members_left" = "%@ (离开)"; "attachment_unsupported_preview_message" = "文件类型不受支持。"; "attachment_unsupported_preview_title" = "无法预览"; diff --git a/Riot/Assets/zh_Hant.lproj/InfoPlist.strings b/Riot/Assets/zh_Hant.lproj/InfoPlist.strings index 6c0c0cae5..26c3b7ea0 100644 --- a/Riot/Assets/zh_Hant.lproj/InfoPlist.strings +++ b/Riot/Assets/zh_Hant.lproj/InfoPlist.strings @@ -1,5 +1,5 @@ // Permissions usage explanations "NSCameraUsageDescription" = "相機權限會用來拍攝照片與影片,以及進行視訊通話。"; "NSPhotoLibraryUsageDescription" = "照片圖庫的權限會用來傳送照片與影片。"; -"NSMicrophoneUsageDescription" = "麥克風的權限會用來拍攝影片與進行通話。"; +"NSMicrophoneUsageDescription" = "Element需要麥克風的權限來拍攝影片、照片以及進行通話。"; "NSContactsUsageDescription" = "為了要顯示您的聯絡人中哪些人已在使用 Element 或 Matrix,我們將會傳送聯絡資訊內的電子郵件位址與電話給您的 Matrix 身份伺服器。New Vector 不會儲存這些資訊,也不會將這些資訊用於其他目的。請檢視應用程式設定的隱私權政策頁面來取得更多資訊。"; diff --git a/Riot/Assets/zh_Hant.lproj/Localizable.strings b/Riot/Assets/zh_Hant.lproj/Localizable.strings index f6a941e2b..6f3abfe1c 100644 --- a/Riot/Assets/zh_Hant.lproj/Localizable.strings +++ b/Riot/Assets/zh_Hant.lproj/Localizable.strings @@ -11,7 +11,6 @@ /* New action message from a specific person in a named room. */ "ACTION_FROM_USER_IN_ROOM" = "%@:* %@ %@"; /* New action message from a specific person, not referencing a room. */ -"IMAGE_FROM_USER" = "%@ 傳送給您一張圖片 %@"; /* New action message from a specific person in a named room. */ "IMAGE_FROM_USER_IN_ROOM" = "%@ 貼出一張圖片 %@ 在 %@"; /* Multiple unread messages in a room */ diff --git a/Riot/Assets/zh_Hant.lproj/Vector.strings b/Riot/Assets/zh_Hant.lproj/Vector.strings index e0784d095..8e87b3d07 100644 --- a/Riot/Assets/zh_Hant.lproj/Vector.strings +++ b/Riot/Assets/zh_Hant.lproj/Vector.strings @@ -214,8 +214,6 @@ "directory_cell_title" = "瀏覽目錄"; "directory_cell_description" = "%tu 個聊天室"; "directory_search_results_title" = "聊天室目錄搜尋結果"; -"directory_search_results" = "搜尋 %@ 有 %tu 個結果"; -"directory_search_results_more_than" = "搜尋 %@ 有超過 %tu 個結果"; "directory_searching_title" = "搜尋聊天室目錄中……"; "directory_search_fail" = "無法取得資料"; // Contacts @@ -263,7 +261,6 @@ "room_new_messages_notification" = "%d 條未讀訊息"; "room_one_user_is_typing" = "%@ 正在輸入…"; "room_two_users_are_typing" = "%@ 和 %@ 正在輸入…"; -"room_many_users_are_typing" = "%@、%@ 和 %@ 正在輸入…"; "room_message_short_placeholder" = "傳送訊息…"; "room_unsent_messages_notification" = "訊息未被傳送。"; "room_unsent_messages_unknown_devices_notification" = "由於存在未知的工作階段導致訊息未被傳送。"; @@ -345,7 +342,6 @@ "settings_pin_rooms_with_missed_notif" = "釘選含有錯過的通知的聊天室"; "settings_pin_rooms_with_unread" = "釘選含有未讀訊息的聊天室"; "settings_enable_callkit" = "整合式通話"; -"settings_callkit_info" = "在鎖定畫面接聽 %@ 來電、在通話紀錄中顯示 %@ 通話。 如果您已啟用 iCloud ,則這些通話紀錄會與蘋果公司共享。"; "settings_ui_language" = "語言"; "settings_ui_theme" = "主題"; "settings_ui_theme_auto" = "自動"; @@ -486,7 +482,6 @@ "no_voip_title" = "來電"; "no_voip" = "%@ 正在撥打給您,但 %@ 尚不支援通話。\n您可以忽略此通知透過其他裝置接聽或拒絕接聽。"; // Crash report -"google_analytics_use_prompt" = "您願意透過自動送出匿名的崩潰報告和使用資料來幫住%@進步嗎?"; // Crypto "e2e_enabling_on_app_update" = "%@ 目前支援點對點加密,但您需要重新登入來啟用它。\n\n您可以現在重新登入或稍後在應用程式設定中進行。"; // Bug report @@ -513,7 +508,6 @@ "e2e_room_key_request_message_new_device" = "您新增的工作階段 '%@', 正在請求加密密鑰。"; "e2e_room_key_request_message" = "您未驗證的工作階段 '%@' 正在請求加密密鑰。"; // GDPR -"gdpr_consent_not_given_alert_message" = "如要繼續使用此家伺服器,您必須同意該合約條款。"; "gdpr_consent_not_given_alert_review_now_action" = "現在重新檢視"; "deactivate_account_title" = "註銷帳號"; "deactivate_account_informations_part1" = "這會使您的帳號永久無法使用。 您將不能以此帳號登入且任何人都將無法以此帳號的ID重新進行註冊。這會使您的帳號立即離開所有參加的聊天室,並從身份伺服器上將您帳號的詳細資料移除。 "; @@ -560,7 +554,6 @@ // Intro "secure_key_backup_setup_intro_title" = "安全備份"; -"service_terms_modal_message_identity_server" = "接受身份伺服器(%@)的條款以尋找聯繫人。"; // MARK: Key backup setup @@ -1123,7 +1116,6 @@ // room display name "room_displayname_empty_room" = "空的聊天室"; "room_displayname_two_members" = "%@ 和 %@"; -"room_displayname_more_than_two_members" = "%@ 和 %u 個其他人"; // Settings "settings" = "設定"; "settings_enable_push_notifications" = "啟用推播通知"; @@ -1140,7 +1132,6 @@ "notice_room_power_level_event_requirement" = "事件相關的最小權限級別是:"; "notice_room_aliases" = "此聊天室別名是:%@"; "notice_room_related_groups" = "此聊天室關聯的群組是:%@"; -"notice_encryption_enabled" = "%@ 開啓了端對端加密 (演算法 %@)"; "notice_feedback" = "回報事件 (id:%@):%@"; "notice_redaction" = "%@ 取消了一个事件 (id: %@)"; "notice_error_unsupported_event" = "不支援的事件"; diff --git a/Riot/Categories/MXBeaconInfoSummary.swift b/Riot/Categories/MXBeaconInfoSummary.swift new file mode 100644 index 000000000..3d47a466c --- /dev/null +++ b/Riot/Categories/MXBeaconInfoSummary.swift @@ -0,0 +1,26 @@ +// +// 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 +import MatrixSDK + +extension MXBeaconInfoSummaryProtocol { + + /// Indicate true if a beacon info summary can be displayed on a map + var isDisplayable: Bool { + return self.isActive && self.lastBeacon != nil + } +} diff --git a/Riot/Categories/MXHTTPClient+Async.swift b/Riot/Categories/MXHTTPClient+Async.swift new file mode 100644 index 000000000..ab5c87a52 --- /dev/null +++ b/Riot/Categories/MXHTTPClient+Async.swift @@ -0,0 +1,78 @@ +// +// 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 + +@available(iOS 13.0, *) +extension MXHTTPClient { + /// Errors thrown by the async extensions to `MXHTTPClient.` + enum ClientError: Error { + /// An unexpected response was received. + case invalidResponse + /// The error that occurred was missing from the closure. + case unknownError + } + + /// Validates a third party ID code at the given URL. + func validateThreePIDCode(submitURL: String, validationBody: ThreePIDValidationCodeBody) async throws -> Bool { + let data = try validationBody.jsonData() + let responseDictionary = try await request(withMethod: "POST", path: submitURL, parameters: nil, data: data) + + // Response is a json dictionary with a single success parameter + guard let success = responseDictionary["success"] as? Bool else { + throw ClientError.invalidResponse + } + + return success + } + + /// An async version of `request(withMethod:path:parameters:success:failure:)`. + func request(withMethod method: String, + path: String, + parameters: [AnyHashable: Any]?, + needsAuthentication: Bool? = nil, + data: Data? = nil, + headers: [AnyHashable: Any]? = nil, + timeout: TimeInterval = -1) async throws -> [AnyHashable: Any] { + try await getResponse { success, failure in + request(withMethod: method, + path: path, + parameters: parameters, + needsAuthentication: needsAuthentication ?? isAuthenticatedClient, + data: data, + headers: headers, + timeout: timeout, + uploadProgress: nil, + success: success, + failure: failure) + } + } + + private func getResponse(_ callback: (@escaping (T?) -> Void, @escaping (Error?) -> Void) -> MXHTTPOperation) async throws -> T { + try await withCheckedThrowingContinuation { continuation in + _ = callback { response in + guard let response = response else { + continuation.resume(with: .failure(ClientError.invalidResponse)) + return + } + + continuation.resume(with: .success(response)) + } _: { error in + continuation.resume(with: .failure(error ?? ClientError.unknownError)) + } + } + } +} diff --git a/Riot/Categories/MXLocationService.swift b/Riot/Categories/MXLocationService.swift new file mode 100644 index 000000000..0c3dc7379 --- /dev/null +++ b/Riot/Categories/MXLocationService.swift @@ -0,0 +1,35 @@ +// +// 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 +import MatrixSDK + +extension MXLocationService { + + public func isSomeoneSharingDisplayableLocation(inRoomWithId roomId: String) -> Bool { + return self.getDisplayableBeaconInfoSummaries(inRoomWithId: roomId).isEmpty == false + } + + /// Get beacon info summaries that can be shown on a map + func getDisplayableBeaconInfoSummaries(inRoomWithId roomId: String) -> [MXBeaconInfoSummaryProtocol] { + + let liveBeaconInfoSummaries = self.getLiveBeaconInfoSummaries(inRoomWithId: roomId) + + return liveBeaconInfoSummaries.filter { beaconInfoSummary in + return beaconInfoSummary.isDisplayable + } + } +} diff --git a/Riot/Categories/MXRestClient+Async.swift b/Riot/Categories/MXRestClient+Async.swift new file mode 100644 index 000000000..c99289002 --- /dev/null +++ b/Riot/Categories/MXRestClient+Async.swift @@ -0,0 +1,143 @@ +// +// 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 + +@available(iOS 13.0, *) +extension MXRestClient { + /// Errors thrown by the async extensions to `MXRestClient.` + enum ClientError: Error { + /// An unexpected response was received. + case invalidResponse + /// The error that occurred was missing from the closure. + case unknownError + /// An error occurred whilst decoding the received JSON. + case decodingError + } + + /// An async version of `wellKnow(_:failure:)`. + func wellKnown() async throws -> MXWellKnown { + try await getResponse { success, failure in + wellKnow(success, failure: failure) + } + } + + /// An async version of `getRegisterSession(completion:)`. + func getRegisterSession() async throws -> MXAuthenticationSession { + try await getResponse(getRegisterSession) + } + + /// An async version of `getLoginSession(completion:)`. + func getLoginSession() async throws -> MXAuthenticationSession { + try await getResponse(getLoginSession) + } + + /// An async version of `isUsernameAvailable(_:completion:)`. + func isUsernameAvailable(_ username: String) async throws -> Bool { + let availability = try await getResponse { completion in + isUsernameAvailable(username, completion: completion) + } + return availability.available + } + + /// An async version of `register(parameters:completion:)`, that takes a `RegistrationParameters` value instead of a dictionary. + func register(parameters: RegistrationParameters) async throws -> MXLoginResponse { + let dictionary = try parameters.dictionary() + return try await register(parameters: dictionary) + } + + /// An async version of `register(parameters:completion:)`. + func register(parameters: [String: Any]) async throws -> MXLoginResponse { + let jsonDictionary = try await getResponse { completion in + register(parameters: parameters, completion: completion) + } + guard let loginResponse = MXLoginResponse(fromJSON: jsonDictionary) else { throw ClientError.decodingError } + return loginResponse + } + + /// An async version of both `requestToken(forEmail:isDuringRegistration:clientSecret:sendAttempt:nextLink:success:failure:)` and + /// `requestToken(forPhoneNumber:isDuringRegistration:countryCode:clientSecret:sendAttempt:nextLink:success:failure:)` depending + /// on the kind of third party ID is supplied to the `threePID` parameter. + func requestTokenDuringRegistration(for threePID: RegisterThreePID, clientSecret: String, sendAttempt: UInt) async throws -> RegistrationThreePIDTokenResponse { + switch threePID { + case .email(let email): + let sessionID: String = try await getResponse { success, failure in + requestToken(forEmail: email, + isDuringRegistration: true, + clientSecret: clientSecret, + sendAttempt: sendAttempt, + nextLink: nil, + success: success, + failure: failure) + } + + return RegistrationThreePIDTokenResponse(sessionID: sessionID) + case .msisdn(let msisdn, let countryCode): + let (sessionID, msisdn, submitURL): (String?, String?, String?) = try await getResponse { success, failure in + requestToken(forPhoneNumber: msisdn, + isDuringRegistration: true, + countryCode: countryCode, + clientSecret: clientSecret, + sendAttempt: sendAttempt, + nextLink: nil, + success: success, + failure: failure) + } + guard let sessionID = sessionID else { throw ClientError.invalidResponse } + return RegistrationThreePIDTokenResponse(sessionID: sessionID, submitURL: submitURL, msisdn: msisdn) + } + } + + // MARK: Private + + private func getResponse(_ callback: (@escaping (MXResponse) -> Void) -> MXHTTPOperation) async throws -> T { + try await withCheckedThrowingContinuation { continuation in + _ = callback { response in + guard let value = response.value else { + continuation.resume(with: .failure(response.error ?? ClientError.unknownError)) + return + } + + continuation.resume(with: .success(value)) + } + } + } + + private func getResponse(_ callback: (@escaping (T?) -> Void, @escaping (Error?) -> Void) -> MXHTTPOperation) async throws -> T { + try await withCheckedThrowingContinuation { continuation in + _ = callback { response in + guard let response = response else { + continuation.resume(with: .failure(ClientError.invalidResponse)) + return + } + + continuation.resume(with: .success(response)) + } _: { error in + continuation.resume(with: .failure(error ?? ClientError.unknownError)) + } + } + } + + private func getResponse(_ callback: (@escaping (T?, U?, V?) -> Void, @escaping (Error?) -> Void) -> MXHTTPOperation) async throws -> (T?, U?, V?) { + try await withCheckedThrowingContinuation { continuation in + _ = callback { arg1, arg2, arg3 in + continuation.resume(with: .success((arg1, arg2, arg3))) + } _: { error in + continuation.resume(with: .failure(error ?? ClientError.unknownError)) + } + } + } +} diff --git a/Riot/Categories/MXSession.swift b/Riot/Categories/MXSession.swift new file mode 100644 index 000000000..10df98141 --- /dev/null +++ b/Riot/Categories/MXSession.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 Foundation + +extension MXSession { + + func avatarInput(for userId: String) -> AvatarInput { + let user = self.user(withUserId: userId) + + return AvatarInput(mxContentUri: user?.avatarUrl, + matrixItemId: userId, + displayName: user?.displayname) + } +} diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index d9c9dadb8..83e645684 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -30,6 +30,14 @@ internal class Asset: NSObject { internal static let socialLoginButtonGitlab = ImageAsset(name: "social_login_button_gitlab") internal static let socialLoginButtonGoogle = ImageAsset(name: "social_login_button_google") internal static let socialLoginButtonTwitter = ImageAsset(name: "social_login_button_twitter") + internal static let authenticationServerSelectionEmsLogo = ImageAsset(name: "authentication_server_selection_ems_logo") + internal static let authenticationServerSelectionIcon = ImageAsset(name: "authentication_server_selection_icon") + internal static let authenticationSsoIconApple = ImageAsset(name: "authentication_sso_icon_apple") + internal static let authenticationSsoIconFacebook = ImageAsset(name: "authentication_sso_icon_facebook") + internal static let authenticationSsoIconGithub = ImageAsset(name: "authentication_sso_icon_github") + internal static let authenticationSsoIconGitlab = ImageAsset(name: "authentication_sso_icon_gitlab") + internal static let authenticationSsoIconGoogle = ImageAsset(name: "authentication_sso_icon_google") + internal static let authenticationSsoIconTwitter = ImageAsset(name: "authentication_sso_icon_twitter") internal static let callAudioMuteOffIcon = ImageAsset(name: "call_audio_mute_off_icon") internal static let callAudioMuteOnIcon = ImageAsset(name: "call_audio_mute_on_icon") internal static let callAudioRouteBuiltin = ImageAsset(name: "call_audio_route_builtin") @@ -174,9 +182,13 @@ internal class Asset: NSObject { internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon") internal static let liveLocationIcon = ImageAsset(name: "live_location_icon") internal static let locationCenterMapIcon = ImageAsset(name: "location_center_map_icon") + internal static let locationLiveCellEndedIcon = ImageAsset(name: "location_live_cell_ended_icon") + internal static let locationLiveCellIcon = ImageAsset(name: "location_live_cell_icon") + internal static let locationLiveCellLoadingIcon = ImageAsset(name: "location_live_cell_loading_icon") internal static let locationLiveIcon = ImageAsset(name: "location_live_icon") internal static let locationMarkerIcon = ImageAsset(name: "location_marker_icon") internal static let locationPinIcon = ImageAsset(name: "location_pin_icon") + internal static let locationPlaceholderBackgroundImage = ImageAsset(name: "location_placeholder_background_image") internal static let locationShareIcon = ImageAsset(name: "location_share_icon") internal static let locationUserMarker = ImageAsset(name: "location_user_marker") internal static let pollCheckboxDefault = ImageAsset(name: "poll_checkbox_default") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 012642ad4..b30de7848 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -847,6 +847,10 @@ public class VectorL10n: NSObject { public static var collapse: String { return VectorL10n.tr("Vector", "collapse") } + /// Confirm + public static var confirm: String { + return VectorL10n.tr("Vector", "confirm") + } /// Local Contacts public static var contactLocalContacts: String { return VectorL10n.tr("Vector", "contact_local_contacts") @@ -2727,6 +2731,10 @@ public class VectorL10n: NSObject { public static var liveLocationSharingBannerTitle: String { return VectorL10n.tr("Vector", "live_location_sharing_banner_title") } + /// Live location ended + public static var liveLocationSharingEnded: String { + return VectorL10n.tr("Vector", "live_location_sharing_ended") + } /// Loading public static var loading: String { return VectorL10n.tr("Vector", "loading") @@ -2759,14 +2767,82 @@ public class VectorL10n: NSObject { public static var locationSharingInvalidAuthorizationSettings: String { return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_settings") } + /// Live location error + public static var locationSharingLiveError: String { + return VectorL10n.tr("Vector", "location_sharing_live_error") + } + /// You + public static var locationSharingLiveListItemCurrentUserDisplayName: String { + return VectorL10n.tr("Vector", "location_sharing_live_list_item_current_user_display_name") + } + /// Updated %@ ago + public static func locationSharingLiveListItemLastUpdate(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_live_list_item_last_update", p1) + } + /// Unknown last update + public static var locationSharingLiveListItemLastUpdateInvalid: String { + return VectorL10n.tr("Vector", "location_sharing_live_list_item_last_update_invalid") + } + /// Sharing expired + public static var locationSharingLiveListItemSharingExpired: String { + return VectorL10n.tr("Vector", "location_sharing_live_list_item_sharing_expired") + } + /// Stop sharing + public static var locationSharingLiveListItemStopSharingAction: String { + return VectorL10n.tr("Vector", "location_sharing_live_list_item_stop_sharing_action") + } + /// %@ left + public static func locationSharingLiveListItemTimeLeft(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_live_list_item_time_left", p1) + } + /// Loading Live location... + public static var locationSharingLiveLoading: String { + return VectorL10n.tr("Vector", "location_sharing_live_loading") + } /// Share location public static var locationSharingLiveMapCalloutTitle: String { return VectorL10n.tr("Vector", "location_sharing_live_map_callout_title") } + /// No user locations available + public static var locationSharingLiveNoUserLocationsErrorTitle: String { + return VectorL10n.tr("Vector", "location_sharing_live_no_user_locations_error_title") + } /// Share live location public static var locationSharingLiveShareTitle: String { return VectorL10n.tr("Vector", "location_sharing_live_share_title") } + /// Fail to stop sharing location + public static var locationSharingLiveStopSharingError: String { + return VectorL10n.tr("Vector", "location_sharing_live_stop_sharing_error") + } + /// Stop location sharing + public static var locationSharingLiveStopSharingProgress: String { + return VectorL10n.tr("Vector", "location_sharing_live_stop_sharing_progress") + } + /// Live until %@ + public static func locationSharingLiveTimerIncoming(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_live_timer_incoming", p1) + } + /// for 8 hours + public static var locationSharingLiveTimerSelectorLong: String { + return VectorL10n.tr("Vector", "location_sharing_live_timer_selector_long") + } + /// for 1 hour + public static var locationSharingLiveTimerSelectorMedium: String { + return VectorL10n.tr("Vector", "location_sharing_live_timer_selector_medium") + } + /// for 15 minutes + public static var locationSharingLiveTimerSelectorShort: String { + return VectorL10n.tr("Vector", "location_sharing_live_timer_selector_short") + } + /// Choose for how long others will see your accurate location. + public static var locationSharingLiveTimerSelectorTitle: String { + return VectorL10n.tr("Vector", "location_sharing_live_timer_selector_title") + } + /// Location + public static var locationSharingLiveViewerTitle: String { + return VectorL10n.tr("Vector", "location_sharing_live_viewer_title") + } /// %@ could not load the map. Please try again later. public static func locationSharingLoadingMapErrorTitle(_ p1: String) -> String { return VectorL10n.tr("Vector", "location_sharing_loading_map_error_title", p1) @@ -8106,15 +8182,18 @@ public class VectorL10n: NSObject { extension VectorL10n { static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { - let format = NSLocalizedString(key, tableName: table, bundle: Bundle.app, comment: "") - let locale: Locale - if let providedLocale = LocaleProvider.locale { - locale = providedLocale - } else { - locale = Locale.current - } - - return String(format: format, locale: locale, arguments: args) + let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "") + let locale = LocaleProvider.locale ?? Locale.current + return String(format: format, locale: locale, arguments: args) + } + /// The bundle to load strings from. This will be the app's bundle unless running + /// the UI tests target, in which case the strings are contained in the tests bundle. + static let bundle: Bundle = { + if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil { + // The tests bundle is embedded inside a runner. Find the bundle for VectorL10n. + return Bundle(for: VectorL10n.self) } + return Bundle.app + }() } diff --git a/Riot/Generated/UntranslatedStrings.swift b/Riot/Generated/UntranslatedStrings.swift index b44654fd3..c681cd91d 100644 --- a/Riot/Generated/UntranslatedStrings.swift +++ b/Riot/Generated/UntranslatedStrings.swift @@ -10,10 +10,102 @@ import Foundation // swiftlint:disable function_parameter_count identifier_name line_length type_body_length public extension VectorL10n { + /// Join millions for free on the largest public server + static var authenticationRegistrationMatrixDescription: String { + return VectorL10n.tr("Untranslated", "authentication_registration_matrix_description") + } + /// We’ll need some info to get you set up. + static var authenticationRegistrationMessage: String { + return VectorL10n.tr("Untranslated", "authentication_registration_message") + } + /// Password + static var authenticationRegistrationPassword: String { + return VectorL10n.tr("Untranslated", "authentication_registration_password") + } + /// Must be 8 characters or more + static var authenticationRegistrationPasswordFooter: String { + return VectorL10n.tr("Untranslated", "authentication_registration_password_footer") + } + /// Choose your server to store your data + static var authenticationRegistrationServerTitle: String { + return VectorL10n.tr("Untranslated", "authentication_registration_server_title") + } + /// Create your account + static var authenticationRegistrationTitle: String { + return VectorL10n.tr("Untranslated", "authentication_registration_title") + } + /// Username + static var authenticationRegistrationUsername: String { + return VectorL10n.tr("Untranslated", "authentication_registration_username") + } + /// You can’t change this later + static var authenticationRegistrationUsernameFooter: String { + return VectorL10n.tr("Untranslated", "authentication_registration_username_footer") + } + /// Get in touch + static var authenticationServerSelectionEmsButton: String { + return VectorL10n.tr("Untranslated", "authentication_server_selection_ems_button") + } + /// element.io/ems + static var authenticationServerSelectionEmsLink: String { + return VectorL10n.tr("Untranslated", "authentication_server_selection_ems_link") + } + /// Element Matrix Services (EMS) is a robust and reliable hosting service for fast, secure real time communication. Find out how on + static var authenticationServerSelectionEmsMessage: String { + return VectorL10n.tr("Untranslated", "authentication_server_selection_ems_message") + } + /// Want to host your own server? + static var authenticationServerSelectionEmsTitle: String { + return VectorL10n.tr("Untranslated", "authentication_server_selection_ems_title") + } + /// Cannot find a server at this URL, please check it is correct. + static var authenticationServerSelectionGenericError: String { + return VectorL10n.tr("Untranslated", "authentication_server_selection_generic_error") + } + /// What is the address of your server? A server is like a home for all your data. + static var authenticationServerSelectionMessage: String { + return VectorL10n.tr("Untranslated", "authentication_server_selection_message") + } + /// You can only connect to a server that has already been set up + static var authenticationServerSelectionServerFooter: String { + return VectorL10n.tr("Untranslated", "authentication_server_selection_server_footer") + } + /// Server URL + static var authenticationServerSelectionServerUrl: String { + return VectorL10n.tr("Untranslated", "authentication_server_selection_server_url") + } + /// Choose your server + static var authenticationServerSelectionTitle: String { + return VectorL10n.tr("Untranslated", "authentication_server_selection_title") + } /// Choose from files static var imagePickerActionFiles: String { return VectorL10n.tr("Untranslated", "image_picker_action_files") } + /// Leave space + static var leaveSpaceAction: String { + return VectorL10n.tr("Untranslated", "leave_space_action") + } + /// Leave space and %@ rooms + static func leaveSpaceAndMoreRooms(_ p1: String) -> String { + return VectorL10n.tr("Untranslated", "leave_space_and_more_rooms", p1) + } + /// Leave space and 1 room + static var leaveSpaceAndOneRoom: String { + return VectorL10n.tr("Untranslated", "leave_space_and_one_room") + } + /// Select all rooms + static var leaveSpaceSelectionAllRooms: String { + return VectorL10n.tr("Untranslated", "leave_space_selection_all_rooms") + } + /// Select no rooms + static var leaveSpaceSelectionNoRooms: String { + return VectorL10n.tr("Untranslated", "leave_space_selection_no_rooms") + } + /// SELECT ROOMS + static var leaveSpaceSelectionTitle: String { + return VectorL10n.tr("Untranslated", "leave_space_selection_title") + } /// This feature isn't available here. For now, you can do this with %@ on your computer. static func spacesFeatureNotAvailable(_ p1: String) -> String { return VectorL10n.tr("Untranslated", "spaces_feature_not_available", p1) diff --git a/Riot/Managers/Theme/Theme.swift b/Riot/Managers/Theme/Theme.swift index 93d9cb2a9..fe8d54425 100644 --- a/Riot/Managers/Theme/Theme.swift +++ b/Riot/Managers/Theme/Theme.swift @@ -104,6 +104,12 @@ import DesignKit var roomCellOutgoingBubbleBackgroundColor: UIColor { get } + // Localisation Cells + + var roomCellLocalisationIconStartedColor: UIColor { get } + + var roomCellLocalisationErrorColor: UIColor { get } + // MARK: - Customisation methods diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift index d61ed3954..84b0239ce 100644 --- a/Riot/Managers/Theme/Themes/DarkTheme.swift +++ b/Riot/Managers/Theme/Themes/DarkTheme.swift @@ -98,6 +98,10 @@ class DarkTheme: NSObject, Theme { } var roomCellOutgoingBubbleBackgroundColor: UIColor = UIColor(rgb: 0x133A34) + + var roomCellLocalisationIconStartedColor: UIColor = UIColor(rgb: 0x5C56F5) + + var roomCellLocalisationErrorColor: UIColor = UIColor(rgb: 0xFF5B55) func applyStyle(onTabBar tabBar: UITabBar) { tabBar.unselectedItemTintColor = self.tabBarUnselectedItemTintColor diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift index e2afd9339..a07de1e8f 100644 --- a/Riot/Managers/Theme/Themes/DefaultTheme.swift +++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift @@ -105,6 +105,10 @@ class DefaultTheme: NSObject, Theme { var roomCellOutgoingBubbleBackgroundColor: UIColor = UIColor(rgb: 0xE7F8F3) + var roomCellLocalisationIconStartedColor: UIColor = UIColor(rgb: 0x5C56F5) + + var roomCellLocalisationErrorColor: UIColor = UIColor(rgb: 0xFF5B55) + func applyStyle(onTabBar tabBar: UITabBar) { tabBar.unselectedItemTintColor = self.tabBarUnselectedItemTintColor tabBar.tintColor = self.tintColor diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index 74880e69f..39a4ffb54 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -261,12 +261,10 @@ extension Analytics { /// Track an E2EE error that occurred /// - Parameters: /// - reason: The error that occurred. - /// - count: The number of times that error occurred. - func trackE2EEError(_ reason: DecryptionFailureReason, count: Int) { - for _ in 0.. Bool { // Prevent Key Verification from using swipe to dismiss return false diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index bfea2a272..dbe84405f 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -349,6 +349,12 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro // the selected room (if any) is highlighted. [self refreshCurrentSelectedCell:YES]; } + + if (self.recentsDataSource) + { + [self refreshRecentsTable]; + [self showEmptyViewIfNeeded]; + } } - (void)viewDidLayoutSubviews @@ -1082,8 +1088,12 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro } else { - MXLogDebug(@"[RecentsViewController]: Reloading table view section %ld", indexPath.section); - [self.recentsTableView reloadSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationNone]; + // Ideally we would call tableView.reloadSections, but this can lead to crashes if multiple sections need such an update and they + // vertically depend on each other. It is unclear whether this is due to further issues in the data model (e.g. data race) + // or some undocumented table view behavior. To avoid this we reload the entire table view, even if this means reloading + // multiple times for several section updates. + MXLogDebug(@"[RecentsViewController]: Reloading the entire table view due to updates in section %ld", indexPath.section); + [self refreshRecentsTable]; } } else if (!changes) @@ -2184,11 +2194,15 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro cellData = [self.dataSource cellDataAtIndexPath:nextIndexPath]; } - if (!cellData && [self.recentsTableView numberOfRowsInSection:section] > 0) + if (!cellData && section < self.recentsTableView.numberOfSections && [self.recentsTableView numberOfRowsInSection:section] > 0) { // Scroll back to the top. [self.recentsTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section] atScrollPosition:UITableViewScrollPositionTop animated:YES]; } + else if (section >= self.recentsTableView.numberOfSections) + { + MXLogFailure(@"[RecentsViewController] Section %ld is invalid in a table view with only %ld sections", section, self.recentsTableView.numberOfSections); + } } } @@ -2253,6 +2267,17 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro state:UIControlStateNormal]; } +- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar +{ + [self.recentsSearchBar resignFirstResponder]; + [self hideSearchBar:YES]; + self.recentsTableView.contentOffset = CGPointMake(0, self.recentsSearchBar.frame.size.height); + self.recentsTableView.tableHeaderView = nil; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self refreshRecentsTable]; + }); +} + #pragma mark - CreateRoomCoordinatorBridgePresenterDelegate - (void)createRoomCoordinatorBridgePresenterDelegate:(CreateRoomCoordinatorBridgePresenter *)coordinatorBridgePresenter didCreateNewRoom:(MXRoom *)room diff --git a/Riot/Modules/Common/Recents/Service/Mock/MockRoomSummary.swift b/Riot/Modules/Common/Recents/Service/Mock/MockRoomSummary.swift index 6344cb1b0..3503282ab 100644 --- a/Riot/Modules/Common/Recents/Service/Mock/MockRoomSummary.swift +++ b/Riot/Modules/Common/Recents/Service/Mock/MockRoomSummary.swift @@ -36,6 +36,8 @@ public class MockRoomSummary: NSObject, MXRoomSummaryProtocol { public var joinRule: String? = kMXRoomJoinRuleInvite + public var historyVisibility: String? + public var membership: MXMembership = .join public var membershipTransitionState: MXMembershipTransitionState = .joined diff --git a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.h b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.h index e2c56d63d..785972fa5 100644 --- a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.h +++ b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.h @@ -175,7 +175,7 @@ typedef BOOL (^MXKAccountOnCertificateChange)(MXKAccount *mxAccount, NSData *cer @param credentials user's credentials */ -- (instancetype)initWithCredentials:(MXCredentials*)credentials; +- (nonnull instancetype)initWithCredentials:(MXCredentials*)credentials; /** Create a matrix session based on the provided store. diff --git a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m index 4de8b53a6..9c35500d3 100644 --- a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m +++ b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m @@ -135,7 +135,7 @@ static NSArray *initialSyncSilentErrorsHTTPStatusCodes; } } -- (instancetype)initWithCredentials:(MXCredentials*)credentials +- (nonnull instancetype)initWithCredentials:(MXCredentials*)credentials { if (self = [super init]) { diff --git a/Riot/Modules/MatrixKit/Models/MXKAppSettings.m b/Riot/Modules/MatrixKit/Models/MXKAppSettings.m index b31fc0658..8d8e44b12 100644 --- a/Riot/Modules/MatrixKit/Models/MXKAppSettings.m +++ b/Riot/Modules/MatrixKit/Models/MXKAppSettings.m @@ -147,7 +147,9 @@ static NSString *const kMXAppGroupID = @"group.org.matrix"; kMXEventTypeStringKeyVerificationCancel, kMXEventTypeStringKeyVerificationDone, kMXEventTypeStringPollStart, - kMXEventTypeStringPollStartMSC3381 + kMXEventTypeStringPollStartMSC3381, + kMXEventTypeStringBeaconInfo, + kMXEventTypeStringBeaconInfoMSC3672 ].mutableCopy; @@ -179,7 +181,9 @@ static NSString *const kMXAppGroupID = @"group.org.matrix"; kMXEventTypeStringKeyVerificationCancel, kMXEventTypeStringKeyVerificationDone, kMXEventTypeStringPollStart, - kMXEventTypeStringPollStartMSC3381 + kMXEventTypeStringPollStartMSC3381, + kMXEventTypeStringBeaconInfo, + kMXEventTypeStringBeaconInfoMSC3672 ].mutableCopy; lastMessageEventTypesAllowList = @[ diff --git a/Riot/Modules/Onboarding/AuthenticationCoordinator.swift b/Riot/Modules/Onboarding/AuthenticationCoordinator.swift new file mode 100644 index 000000000..bd2409670 --- /dev/null +++ b/Riot/Modules/Onboarding/AuthenticationCoordinator.swift @@ -0,0 +1,354 @@ +// File created from ScreenTemplate +// $ createScreen.sh Onboarding Authentication +/* + Copyright 2021 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit + +@available(iOS 14.0, *) +struct AuthenticationCoordinatorParameters { + let navigationRouter: NavigationRouterType + /// The screen that should be shown when starting the flow. + let initialScreen: AuthenticationCoordinator.EntryPoint + /// Whether or not the coordinator should show the loading spinner, key verification etc. + let canPresentAdditionalScreens: Bool +} + +/// A coordinator that handles authentication, verification and setting a PIN. +@available(iOS 14.0, *) +final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtocol { + + enum EntryPoint { + case registration + case selectServerForRegistration + case login + } + + // MARK: - Properties + + // MARK: Private + + private let navigationRouter: NavigationRouterType + private let authenticationService = AuthenticationService.shared + + private let initialScreen: EntryPoint + private var canPresentAdditionalScreens: Bool + private var isWaitingToPresentCompleteSecurity = false + + private var verificationListener: SessionVerificationListener? + + /// The password entered, for use when setting up cross-signing. + private var password: String? + /// The session created when successfully authenticated. + private var session: MXSession? + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: ((AuthenticationCoordinatorResult) -> Void)? + + // MARK: - Setup + + init(parameters: AuthenticationCoordinatorParameters) { + self.navigationRouter = parameters.navigationRouter + self.initialScreen = parameters.initialScreen + self.canPresentAdditionalScreens = parameters.canPresentAdditionalScreens + + super.init() + } + + // MARK: - Public + + func start() { + Task { + do { + let flow: AuthenticationFlow = initialScreen == .login ? .login : .registration + try await authenticationService.startFlow(flow, for: authenticationService.state.homeserver.address) + } catch { + MXLog.error("[AuthenticationCoordinator] start: Failed to start") + await MainActor.run { displayError(error) } + return + } + + await MainActor.run { + switch initialScreen { + case .registration: + showRegistrationScreen() + case .selectServerForRegistration: + showServerSelectionScreen() + case .login: + showLoginScreen() + } + } + } + } + + func toPresentable() -> UIViewController { + navigationRouter.toPresentable() + } + + func presentPendingScreensIfNecessary() { + canPresentAdditionalScreens = true + + showLoadingAnimation() + + if isWaitingToPresentCompleteSecurity { + isWaitingToPresentCompleteSecurity = false + presentCompleteSecurity() + } + } + + // MARK: - Private + + /// Presents an alert on top of the navigation router, using the supplied error's `localizedDescription`. + @MainActor func displayError(_ error: Error) { + let alert = UIAlertController(title: VectorL10n.error, + message: error.localizedDescription, + preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: VectorL10n.ok, style: .default)) + + toPresentable().present(alert, animated: true) + } + + // MARK: - Registration + + /// Pushes the server selection screen into the flow (other screens may also present it modally later). + @MainActor private func showServerSelectionScreen() { + MXLog.debug("[AuthenticationCoordinator] showServerSelectionScreen") + let parameters = AuthenticationServerSelectionCoordinatorParameters(authenticationService: authenticationService, + hasModalPresentation: false) + let coordinator = AuthenticationServerSelectionCoordinator(parameters: parameters) + coordinator.completion = { [weak self, weak coordinator] result in + guard let self = self, let coordinator = coordinator else { return } + self.serverSelectionCoordinator(coordinator, didCompleteWith: result) + } + + coordinator.start() + add(childCoordinator: coordinator) + + if navigationRouter.modules.isEmpty { + navigationRouter.setRootModule(coordinator, popCompletion: nil) + } else { + navigationRouter.push(coordinator, animated: true) { [weak self] in + self?.remove(childCoordinator: coordinator) + } + } + } + + @available(iOS 14.0, *) + /// Shows the next screen in the flow after the server selection screen. + @MainActor private func serverSelectionCoordinator(_ coordinator: AuthenticationServerSelectionCoordinator, + didCompleteWith result: AuthenticationServerSelectionCoordinatorResult) { + switch result { + case .updated: + showRegistrationScreen() + case .dismiss: + MXLog.failure("[AuthenticationCoordinator] AuthenticationServerSelectionScreen is requesting dismiss when part of a stack.") + } + } + + /// Shows the registration screen. + @MainActor private func showRegistrationScreen() { + MXLog.debug("[AuthenticationCoordinator] showRegistrationScreen") + let homeserver = authenticationService.state.homeserver + let parameters = AuthenticationRegistrationCoordinatorParameters(navigationRouter: navigationRouter, + authenticationService: authenticationService, + registrationFlow: homeserver.registrationFlow, + loginMode: homeserver.preferredLoginMode) + let coordinator = AuthenticationRegistrationCoordinator(parameters: parameters) + coordinator.completion = { [weak self, weak coordinator] result in + guard let self = self, let coordinator = coordinator else { return } + self.registrationCoordinator(coordinator, didCompleteWith: result) + } + + coordinator.start() + add(childCoordinator: coordinator) + + if navigationRouter.modules.isEmpty { + navigationRouter.setRootModule(coordinator, popCompletion: nil) + } else { + navigationRouter.push(coordinator, animated: true) { [weak self] in + self?.remove(childCoordinator: coordinator) + } + } + } + + /// Displays the next view in the flow after the registration screen. + @available(iOS 14.0, *) + @MainActor private func registrationCoordinator(_ coordinator: AuthenticationRegistrationCoordinator, + didCompleteWith result: AuthenticationRegistrationCoordinatorResult) { + switch result { + case .selectServer: + showServerSelectionScreen() + case .completed(let result): + handleRegistrationResult(result) + } + } + + /// Shows the login screen. + @MainActor private func showLoginScreen() { + MXLog.debug("[AuthenticationCoordinator] showLoginScreen") + + } + + // MARK: - Registration Handlers + /// Determines the next screen to show from the flow result and pushes it. + func handleRegistrationResult(_ result: RegistrationResult) { + switch result { + case .success(let mxSession): + onSessionCreated(session: mxSession, isAccountCreated: true) + case .flowResponse(let flowResult): + // TODO + break + } + } + + /// Handles the creation of a new session following on from a successful authentication. + func onSessionCreated(session: MXSession, isAccountCreated: Bool) { + self.session = session + // self.password = password + + if canPresentAdditionalScreens { + showLoadingAnimation() + } + + let verificationListener = SessionVerificationListener(session: session, password: password) + + verificationListener.completion = { [weak self] result in + guard let self = self else { return } + switch result { + case .needsVerification: + guard self.canPresentAdditionalScreens else { + MXLog.debug("[AuthenticationCoordinator] Delaying presentCompleteSecurity during onboarding.") + self.isWaitingToPresentCompleteSecurity = true + return + } + + MXLog.debug("[AuthenticationCoordinator] Complete security") + self.presentCompleteSecurity() + case .authenticationIsComplete: + self.authenticationDidComplete() + } + } + + verificationListener.start() + self.verificationListener = verificationListener + + completion?(.didLogin(session: session, authenticationType: isAccountCreated ? .register : .login)) + } + + // MARK: - Additional Screens + + /// Replace the contents of the navigation router with a loading animation. + private func showLoadingAnimation() { + let loadingViewController = LaunchLoadingViewController() + loadingViewController.modalPresentationStyle = .fullScreen + + // Replace the navigation stack with the loading animation + // as there is nothing to navigate back to. + navigationRouter.setRootModule(loadingViewController) + } + + /// Present the key verification screen modally. + private func presentCompleteSecurity() { + guard let session = session else { + MXLog.error("[LegacyAuthenticationCoordinator] presentCompleteSecurity: Unable to present security due to missing session.") + authenticationDidComplete() + return + } + + let isNewSignIn = true + let cancellable = !session.vc_homeserverConfiguration().encryption.isSecureBackupRequired + let keyVerificationCoordinator = KeyVerificationCoordinator(session: session, flow: .completeSecurity(isNewSignIn), cancellable: cancellable) + + keyVerificationCoordinator.delegate = self + let presentable = keyVerificationCoordinator.toPresentable() + presentable.presentationController?.delegate = self + navigationRouter.present(presentable, animated: true) + keyVerificationCoordinator.start() + add(childCoordinator: keyVerificationCoordinator) + } + + /// Complete the authentication flow. + private func authenticationDidComplete() { + completion?(.didComplete) + } +} + +// MARK: - KeyVerificationCoordinatorDelegate +@available(iOS 14.0, *) +extension AuthenticationCoordinator: KeyVerificationCoordinatorDelegate { + func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) { + if let crypto = session?.crypto, + !crypto.backup.hasPrivateKeyInCryptoStore || !crypto.backup.enabled { + MXLog.debug("[LegacyAuthenticationCoordinator][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys") + crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) + } + + navigationRouter.dismissModule(animated: true) { [weak self] in + self?.authenticationDidComplete() + } + } + + func keyVerificationCoordinatorDidCancel(_ coordinator: KeyVerificationCoordinatorType) { + navigationRouter.dismissModule(animated: true) { [weak self] in + self?.authenticationDidComplete() + } + } +} + +// MARK: - UIAdaptivePresentationControllerDelegate +@available(iOS 14.0, *) +extension AuthenticationCoordinator: UIAdaptivePresentationControllerDelegate { + func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { + // Prevent Key Verification from using swipe to dismiss + return false + } +} + + + +// MARK: - Unused conformances +@available(iOS 14.0, *) +extension AuthenticationCoordinator { + var customServerFieldsVisible: Bool { + get { false } + set { /* no-op */ } + } + + func update(authenticationType: MXKAuthenticationType) { + // unused + } + + func update(externalRegistrationParameters: [AnyHashable: Any]) { + // unused + } + + func update(softLogoutCredentials: MXCredentials) { + // unused + } + + func updateHomeserver(_ homeserver: String?, andIdentityServer identityServer: String?) { + // unused + } + + func continueSSOLogin(withToken loginToken: String, transactionID: String) -> Bool { + #warning("To be implemented elsewhere") + return false + } +} diff --git a/Riot/Modules/Onboarding/OnboardingCoordinator.swift b/Riot/Modules/Onboarding/OnboardingCoordinator.swift index 2b5753816..0d3d68906 100644 --- a/Riot/Modules/Onboarding/OnboardingCoordinator.swift +++ b/Riot/Modules/Onboarding/OnboardingCoordinator.swift @@ -56,8 +56,9 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { } // Keep a strong ref as we need to init authVC early to preload its view private let authenticationCoordinator: AuthenticationCoordinatorProtocol + #warning("This might be removable when SSO comes through the AuthenticationService?") /// A boolean to prevent authentication being shown when already in progress. - private var isShowingAuthentication = false + private var isShowingLegacyAuthentication = false // MARK: Screen results private var splashScreenResult: OnboardingSplashScreenViewModelResult? @@ -87,8 +88,8 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { self.parameters = parameters // Preload the authVC (it is *really* slow to load in realtime) - let authenticationParameters = AuthenticationCoordinatorParameters(navigationRouter: parameters.router, canPresentAdditionalScreens: false) - authenticationCoordinator = AuthenticationCoordinator(parameters: authenticationParameters) + let authenticationParameters = LegacyAuthenticationCoordinatorParameters(navigationRouter: parameters.router, canPresentAdditionalScreens: false) + authenticationCoordinator = LegacyAuthenticationCoordinator(parameters: authenticationParameters) super.init() } @@ -100,7 +101,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { if #available(iOS 14.0, *), parameters.softLogoutCredentials == nil, BuildSettings.authScreenShowRegister { showSplashScreen() } else { - showAuthenticationScreen() + showLegacyAuthenticationScreen() } } @@ -124,7 +125,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { /// When SSO login succeeded, when SFSafariViewController is used, continue login with success parameters. func continueSSOLogin(withToken loginToken: String, transactionID: String) -> Bool { - guard isShowingAuthentication else { return false } + guard isShowingLegacyAuthentication else { return false } return authenticationCoordinator.continueSSOLogin(withToken: loginToken, transactionID: transactionID) } @@ -159,7 +160,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { case .register: showUseCaseSelectionScreen() case .login: - showAuthenticationScreen() + showLegacyAuthenticationScreen() } } @@ -190,16 +191,50 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { @available(iOS 14.0, *) private func useCaseSelectionCoordinator(_ coordinator: OnboardingUseCaseSelectionCoordinator, didCompleteWith result: OnboardingUseCaseViewModelResult) { useCaseResult = result - showAuthenticationScreen() + + guard BuildSettings.onboardingEnableNewAuthenticationFlow else { + showLegacyAuthenticationScreen() + return + } + + if result == .customServer { + beginAuthentication(with: .selectServerForRegistration) + } else { + beginAuthentication(with: .registration) + } } // MARK: - Authentication - /// Show the authentication screen. Any parameters that have been set in previous screens are be applied. - private func showAuthenticationScreen() { - guard !isShowingAuthentication else { return } + /// Show the authentication flow, starting at the specified initial screen. + @available(iOS 14.0, *) + private func beginAuthentication(with initialScreen: AuthenticationCoordinator.EntryPoint) { + MXLog.debug("[OnboardingCoordinator] beginAuthentication") - MXLog.debug("[OnboardingCoordinator] showAuthenticationScreen") + let parameters = AuthenticationCoordinatorParameters(navigationRouter: navigationRouter, + initialScreen: initialScreen, + canPresentAdditionalScreens: false) + let coordinator = AuthenticationCoordinator(parameters: parameters) + coordinator.completion = { [weak self, weak coordinator] result in + guard let self = self, let coordinator = coordinator else { return } + + switch result { + case .didLogin(let session, let authenticationType): + self.authenticationCoordinator(coordinator, didLoginWith: session, and: authenticationType) + case .didComplete: + self.authenticationCoordinatorDidComplete(coordinator) + } + } + + add(childCoordinator: coordinator) + coordinator.start() + } + + /// Show the legacy authentication screen. Any parameters that have been set in previous screens are be applied. + private func showLegacyAuthenticationScreen() { + guard !isShowingLegacyAuthentication else { return } + + MXLog.debug("[OnboardingCoordinator] showLegacyAuthenticationScreen") let coordinator = authenticationCoordinator coordinator.completion = { [weak self, weak coordinator] result in @@ -239,13 +274,13 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { } else { navigationRouter.push(coordinator, animated: true) { [weak self] in self?.remove(childCoordinator: coordinator) - self?.isShowingAuthentication = false + self?.isShowingLegacyAuthentication = false } } - isShowingAuthentication = true + isShowingLegacyAuthentication = true } - /// Displays the next view in the flow after the authentication screen, + /// Displays the next view in the flow after the authentication screens, /// whilst crypto and the rest of the app is launching in the background. private func authenticationCoordinator(_ coordinator: AuthenticationCoordinatorProtocol, didLoginWith session: MXSession, @@ -295,9 +330,9 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { } } - /// Displays the next view in the flow after the authentication screen. + /// Completes the onboarding flow if possible, otherwise waits for any remaining screens. private func authenticationCoordinatorDidComplete(_ coordinator: AuthenticationCoordinatorProtocol) { - isShowingAuthentication = false + isShowingLegacyAuthentication = false // Handle the chosen use case where applicable if authenticationType == .register, @@ -519,7 +554,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol { } guard authenticationFinished else { - MXLog.debug("[OnboardingCoordinator] Allowing AuthenticationCoordinator to display any remaining screens.") + MXLog.debug("[OnboardingCoordinator] Allowing LegacyAuthenticationCoordinator to display any remaining screens.") authenticationCoordinator.presentPendingScreensIfNecessary() return } diff --git a/Riot/Modules/Onboarding/OnboardingCoordinatorBridgePresenter.swift b/Riot/Modules/Onboarding/OnboardingCoordinatorBridgePresenter.swift index 32f433936..2286bc046 100644 --- a/Riot/Modules/Onboarding/OnboardingCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Onboarding/OnboardingCoordinatorBridgePresenter.swift @@ -61,15 +61,7 @@ final class OnboardingCoordinatorBridgePresenter: NSObject { // MARK: - Public func present(from viewController: UIViewController, animated: Bool) { - let onboardingCoordinatorParameters = OnboardingCoordinatorParameters(softLogoutCredentials: parameters.softLogoutCredentials) - - let onboardingCoordinator = OnboardingCoordinator(parameters: onboardingCoordinatorParameters) - onboardingCoordinator.completion = { [weak self] in - self?.completion?() - } - if let externalRegistrationParameters = parameters.externalRegistrationParameters { - onboardingCoordinator.update(externalRegistrationParameters: externalRegistrationParameters) - } + let onboardingCoordinator = makeOnboardingCoordinator() let presentable = onboardingCoordinator.toPresentable() presentable.modalPresentationStyle = .fullScreen @@ -86,16 +78,7 @@ final class OnboardingCoordinatorBridgePresenter: NSObject { let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) - let onboardingCoordinatorParameters = OnboardingCoordinatorParameters(router: navigationRouter, - softLogoutCredentials: parameters.softLogoutCredentials) - - let onboardingCoordinator = OnboardingCoordinator(parameters: onboardingCoordinatorParameters) - onboardingCoordinator.completion = { [weak self] in - self?.completion?() - } - if let externalRegistrationParameters = parameters.externalRegistrationParameters { - onboardingCoordinator.update(externalRegistrationParameters: externalRegistrationParameters) - } + let onboardingCoordinator = makeOnboardingCoordinator(navigationRouter: navigationRouter) onboardingCoordinator.start() // Will trigger the view controller push @@ -148,4 +131,22 @@ final class OnboardingCoordinatorBridgePresenter: NSObject { } } } + + // MARK: - Private + + /// Makes an `OnboardingCoordinator` using the supplied navigation router, or creating one if needed. + private func makeOnboardingCoordinator(navigationRouter: NavigationRouterType? = nil) -> OnboardingCoordinator { + let onboardingCoordinatorParameters = OnboardingCoordinatorParameters(router: navigationRouter, + softLogoutCredentials: parameters.softLogoutCredentials) + + let onboardingCoordinator = OnboardingCoordinator(parameters: onboardingCoordinatorParameters) + onboardingCoordinator.completion = { [weak self] in + self?.completion?() + } + if let externalRegistrationParameters = parameters.externalRegistrationParameters { + onboardingCoordinator.update(externalRegistrationParameters: externalRegistrationParameters) + } + + return onboardingCoordinator + } } diff --git a/Riot/Modules/Onboarding/SessionVerificationListener.swift b/Riot/Modules/Onboarding/SessionVerificationListener.swift new file mode 100644 index 000000000..f8973d45a --- /dev/null +++ b/Riot/Modules/Onboarding/SessionVerificationListener.swift @@ -0,0 +1,140 @@ +// +// 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 + +/// An object that will listen for the cross-signing state of a new session +/// determining whether or not verification needs to be performed. +class SessionVerificationListener { + enum Result { + case needsVerification + case authenticationIsComplete + } + + // MARK: - Properties + + /// The completion handler called once the cross-signing state has been determined. + var completion: ((Result) -> Void)? + /// The session being used + private let session: MXSession + /// The session's password (if used), for boot-strapping the cross-signing. + private let password: String? + /// The cross-signing service. + private let crossSigningService = CrossSigningService() + + // MARK: - Setup + + /// Creates a new listener object. + /// - Parameter session: The session to listen to. + /// - Parameter password: The password used for the session (optional). + init(session: MXSession, password: String?) { + self.session = session + self.password = password + } + + // MARK: - Public + + /// Start listening for the cross-signing state of the supplied session. + func start() { + registerSessionStateChangeNotification(for: session) + } + + // MARK: - Private + + private func registerSessionStateChangeNotification(for session: MXSession) { + NotificationCenter.default.addObserver(self, selector: #selector(sessionStateDidChange), name: .mxSessionStateDidChange, object: session) + } + + private func unregisterSessionStateChangeNotification() { + NotificationCenter.default.removeObserver(self, name: .mxSessionStateDidChange, object: nil) + } + + @objc private func sessionStateDidChange(_ notification: Notification) { + guard let session = notification.object as? MXSession else { + MXLog.error("[SessionVerificationListener] sessionStateDidChange: Missing session in the notification") + return + } + + if session.state == .storeDataReady { + if let crypto = session.crypto, crypto.crossSigning != nil { + // Do not make key share requests while the "Complete security" is not complete. + // If the device is self-verified, the SDK will restore the existing key backup. + // Then, it will re-enable outgoing key share requests + crypto.setOutgoingKeyRequestsEnabled(false, onComplete: nil) + } + } else if session.state == .running { + unregisterSessionStateChangeNotification() + + if let crypto = session.crypto, let crossSigning = crypto.crossSigning { + crossSigning.refreshState { [weak self] stateUpdated in + guard let self = self else { return } + + MXLog.debug("[SessionVerificationListener] sessionStateDidChange: crossSigning.state: \(crossSigning.state)") + + switch crossSigning.state { + case .notBootstrapped: + // TODO: This is still not sure we want to disable the automatic cross-signing bootstrap + // if the admin disabled e2e by default. + // Do like riot-web for the moment + if session.vc_homeserverConfiguration().encryption.isE2EEByDefaultEnabled { + // Bootstrap cross-signing on user's account + // We do it for both registration and new login as long as cross-signing does not exist yet + if let password = self.password, !password.isEmpty { + MXLog.debug("[SessionVerificationListener] sessionStateDidChange: Bootstrap with password") + + crossSigning.setup(withPassword: password) { + MXLog.debug("[SessionVerificationListener] sessionStateDidChange: Bootstrap succeeded") + self.completion?(.authenticationIsComplete) + } failure: { error in + MXLog.error("[SessionVerificationListener] sessionStateDidChange: Bootstrap failed. Error: \(error)") + crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) + self.completion?(.authenticationIsComplete) + } + } else { + // Try to setup cross-signing without authentication parameters in case if a grace period is enabled + self.crossSigningService.setupCrossSigningWithoutAuthentication(for: session) { + MXLog.debug("[SessionVerificationListener] sessionStateDidChange: Bootstrap succeeded without credentials") + self.completion?(.authenticationIsComplete) + } failure: { error in + MXLog.error("[SessionVerificationListener] sessionStateDidChange: Do not know how to bootstrap cross-signing. Skip it.") + crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) + self.completion?(.authenticationIsComplete) + } + } + } else { + crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) + self.completion?(.authenticationIsComplete) + } + case .crossSigningExists: + MXLog.debug("[SessionVerificationListener] sessionStateDidChange: Needs verification") + self.completion?(.needsVerification) + default: + MXLog.debug("[SessionVerificationListener] sessionStateDidChange: Nothing to do") + + crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) + self.completion?(.authenticationIsComplete) + } + } failure: { [weak self] error in + MXLog.error("[SessionVerificationListener] sessionStateDidChange: Fail to refresh crypto state with error: \(error)") + crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil) + self?.completion?(.authenticationIsComplete) + } + } else { + completion?(.authenticationIsComplete) + } + } + } +} diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index 706776efb..aa9c661b1 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -16,6 +16,8 @@ #import "MatrixKit.h" +@protocol MXBeaconInfoSummaryProtocol; + extern NSString *const URLPreviewDidUpdateNotification; // Custom tags for MXKRoomBubbleCellDataStoring.tag @@ -33,7 +35,8 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) RoomBubbleCellDataTagGroupCall, RoomBubbleCellDataTagRoomCreationIntro, RoomBubbleCellDataTagPoll, - RoomBubbleCellDataTagLocation + RoomBubbleCellDataTagLocation, + RoomBubbleCellDataTagLiveLocation }; /** @@ -92,6 +95,8 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) */ @property(nonatomic) BOOL isKeyVerificationOperationPending; +@property(nonatomic, strong, readonly) id beaconInfoSummary; + /** Index of the component which needs a sent tick displayed. -1 if none. */ diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 70ad44a48..c6149ecf1 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -38,6 +38,8 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat // Flags to "Show All" reactions for an event @property(nonatomic) NSMutableSet *eventsToShowAllReactions; +@property(nonatomic, strong, readwrite) id beaconInfoSummary; + @end @implementation RoomBubbleCellData @@ -159,6 +161,15 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat break; } + case MXEventTypeBeaconInfo: + { + self.tag = RoomBubbleCellDataTagLiveLocation; + self.collapsable = NO; + self.collapsed = NO; + + [self updateBeaconInfoSummaryWithEventId:event.eventId]; + break; + } case MXEventTypeCustom: { if ([event.type isEqualToString:kWidgetMatrixEventTypeString] @@ -184,6 +195,8 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat self.collapsable = NO; self.collapsed = NO; } + + break; } default: break; @@ -210,6 +223,11 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat // Update any URL preview data as necessary. [self refreshURLPreviewForEventId:event.eventId]; + + if (self.tag == RoomBubbleCellDataTagLiveLocation) + { + [self updateBeaconInfoSummaryWithEventId:eventId]; + } return retVal; } @@ -279,6 +297,17 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat return NO; } + if (self.tag == RoomBubbleCellDataTagLiveLocation) + { + // If the summary does not exist don't show the cell + if (!self.beaconInfoSummary) + { + return YES; + } + + return NO; + } + return [super hasNoDisplay]; } @@ -983,6 +1012,9 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat case RoomBubbleCellDataTagLocation: shouldAddEvent = NO; break; + case RoomBubbleCellDataTagLiveLocation: + shouldAddEvent = NO; + break; default: break; } @@ -1294,5 +1326,11 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat }]; } +- (void)updateBeaconInfoSummaryWithEventId:(NSString *)eventId +{ + id beaconInfoSummary = [self.mxSession.aggregations.beaconAggregations beaconInfoSummaryFor:eventId inRoomWithId:self.roomId]; + + self.beaconInfoSummary = beaconInfoSummary; +} @end diff --git a/Riot/Modules/Room/Location/LocationMarkerView.xib b/Riot/Modules/Room/Location/LocationMarkerView.xib index e4ec30008..fb09fd4be 100644 --- a/Riot/Modules/Room/Location/LocationMarkerView.xib +++ b/Riot/Modules/Room/Location/LocationMarkerView.xib @@ -1,6 +1,8 @@ + + @@ -8,56 +10,53 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + - - - - + + + + + + + + + + + + + @@ -71,6 +70,6 @@ - + diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift index cb2fb9146..e467a8242 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -17,6 +17,59 @@ import UIKit import Reusable import Mapbox +import SwiftUI + +protocol RoomTimelineLocationViewDelegate: AnyObject { + func roomTimelineLocationViewDidTapStopButton(_ roomTimelineLocationView: RoomTimelineLocationView) + func roomTimelineLocationViewDidTapRetryButton(_ roomTimelineLocationView: RoomTimelineLocationView) +} + +struct RoomTimelineLocationViewData { + let location: CLLocationCoordinate2D? + let userAvatarData: AvatarViewData? + let mapStyleURL: URL +} + +struct LiveLocationBannerViewData { + let placeholderIcon: UIImage? + let iconTint: UIColor + let title: String + let titleColor: UIColor + let timeLeftString: String? + let rightButtonTitle: String? + let rightButtonTag: RightButtonTag + let coordinate: CLLocationCoordinate2D? + + var showTimer: Bool { + return timeLeftString != nil + } + + var showRightButton: Bool { + return rightButtonTitle != nil + } + + var showPlaceholderImage: Bool { + return placeholderIcon != nil + } +} + +enum TimelineLiveLocationViewState { + case incoming(_ status: LiveLocationSharingStatus) // live location started by other users + case outgoing(_ status: LiveLocationSharingStatus) // live location started from current user +} + + +enum LiveLocationSharingStatus { + case starting + case started(_ coordinate: CLLocationCoordinate2D, _ timeleft: TimeInterval) + case failure + case stopped +} + +enum RightButtonTag: Int { + case stopSharing = 0 + case retrySharing +} class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegate { @@ -37,9 +90,37 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat @IBOutlet private var descriptionIcon: UIImageView! @IBOutlet private var attributionLabel: UILabel! + // MARK: - Live Location + @IBOutlet private var placeholderBackground: UIImageView! + @IBOutlet private var placeholderIcon: UIImageView! + @IBOutlet private var liveLocationContainerView: UIView! + @IBOutlet private var liveLocationImageView: UIImageView! + @IBOutlet private var liveLocationStatusLabel: UILabel! + @IBOutlet private var liveLocationTimerLabel: UILabel! + @IBOutlet private var rightButton: UIButton! + + + private var mapView: MGLMapView! private var annotationView: LocationMarkerView? private static var usernameColorGenerator = UserNameColorGenerator() + private var theme: Theme! + + private lazy var incomingTimerFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm" + return dateFormatter + }() + + private lazy var outgoingTimerFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.zeroFormattingBehavior = .dropAll + formatter.allowedUnits = [.hour, .minute, .second] + formatter.unitsStyle = .brief + return formatter + }() + + weak var delegate: RoomTimelineLocationViewDelegate? // MARK: Public @@ -52,7 +133,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat descriptionContainerView.isHidden = (newValue?.count ?? 0 == 0) } } - + override func awakeFromNib() { super.awakeFromNib() @@ -70,34 +151,174 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat clipsToBounds = true layer.borderWidth = Constants.cellBorderRadius layer.cornerRadius = Constants.cellCornerRadius + + theme = ThemeService.shared().theme + } + + // MARK: - Private + + private func displayLocation(_ location: CLLocationCoordinate2D?, + userAvatarData: AvatarViewData? = nil, + mapStyleURL: URL, + bannerViewData: LiveLocationBannerViewData? = nil) { + + if let location = location { + mapView.styleURL = mapStyleURL + + annotationView = LocationMarkerView.loadFromNib() + + if let userAvatarData = userAvatarData { + let avatarBackgroundColor = Self.usernameColorGenerator.color(from: userAvatarData.matrixItemId) + annotationView?.setAvatarData(userAvatarData, avatarBackgroundColor: avatarBackgroundColor) + } + + if let annotations = mapView.annotations { + mapView.removeAnnotations(annotations) + } + + mapView.setCenter(location, zoomLevel: Constants.mapZoomLevel, animated: false) + + let pointAnnotation = MGLPointAnnotation() + pointAnnotation.coordinate = location + mapView.addAnnotation(pointAnnotation) + } else { + mapView.isHidden = true + } + + // Configure live location banner + guard let bannerViewData = bannerViewData else { + liveLocationContainerView.isHidden = true + return + } + + liveLocationContainerView.isHidden = false + liveLocationContainerView.backgroundColor = theme.colors.background.withAlphaComponent(0.85) + + liveLocationImageView.image = Asset.Images.locationLiveCellIcon.image + liveLocationImageView.tintColor = bannerViewData.iconTint + + liveLocationStatusLabel.text = bannerViewData.title + liveLocationStatusLabel.textColor = bannerViewData.titleColor + + liveLocationTimerLabel.text = bannerViewData.timeLeftString + liveLocationTimerLabel.textColor = theme.colors.tertiaryContent + liveLocationTimerLabel.isHidden = !bannerViewData.showTimer + + rightButton.setTitle(bannerViewData.rightButtonTitle, for: .normal) + rightButton.isHidden = !bannerViewData.showRightButton + rightButton.tag = bannerViewData.rightButtonTag.rawValue + + placeholderBackground.isHidden = !bannerViewData.showPlaceholderImage + placeholderIcon.image = bannerViewData.placeholderIcon + placeholderIcon.isHidden = !bannerViewData.showPlaceholderImage + placeholderBackground.isHidden = !bannerViewData.showPlaceholderImage + mapView.isHidden = bannerViewData.showPlaceholderImage + } + + private func liveLocationBannerViewData(from viewState: TimelineLiveLocationViewState) -> LiveLocationBannerViewData { + + let iconTint: UIColor + let title: String + var titleColor: UIColor = theme.colors.primaryContent + var placeholderIcon: UIImage? + var timeLeftString: String? + var rightButtonTitle: String? + var rightButtonTag: RightButtonTag = .stopSharing + var liveCoordinate: CLLocationCoordinate2D? + + switch viewState { + case .incoming(let liveLocationSharingStatus): + switch liveLocationSharingStatus { + case .starting: + iconTint = theme.colors.tertiaryContent + title = VectorL10n.locationSharingLiveLoading + titleColor = theme.colors.tertiaryContent + placeholderIcon = Asset.Images.locationLiveCellLoadingIcon.image + case .started(let coordinate, let timeLeft): + iconTint = theme.roomCellLocalisationIconStartedColor + title = VectorL10n.liveLocationSharingBannerTitle + timeLeftString = generateTimerString(for: timeLeft, isIncomingLocation: true) + liveCoordinate = coordinate + case .failure: + iconTint = theme.roomCellLocalisationErrorColor + title = VectorL10n.locationSharingLiveError + rightButtonTitle = VectorL10n.retry + rightButtonTag = .retrySharing + case .stopped: + iconTint = theme.colors.tertiaryContent + title = VectorL10n.liveLocationSharingEnded + titleColor = theme.colors.tertiaryContent + placeholderIcon = Asset.Images.locationLiveCellEndedIcon.image + } + case .outgoing(let liveLocationSharingStatus): + switch liveLocationSharingStatus { + case .starting: + iconTint = theme.colors.tertiaryContent + title = VectorL10n.locationSharingLiveLoading + titleColor = theme.colors.tertiaryContent + placeholderIcon = Asset.Images.locationLiveCellLoadingIcon.image + case .started(let coordinate, let timeLeft): + iconTint = theme.roomCellLocalisationIconStartedColor + title = VectorL10n.liveLocationSharingBannerTitle + timeLeftString = generateTimerString(for: timeLeft, isIncomingLocation: false) + rightButtonTitle = VectorL10n.stop + liveCoordinate = coordinate + case .failure: + iconTint = theme.roomCellLocalisationErrorColor + title = VectorL10n.locationSharingLiveError + rightButtonTitle = VectorL10n.retry + rightButtonTag = .retrySharing + case .stopped: + iconTint = theme.colors.tertiaryContent + title = VectorL10n.liveLocationSharingEnded + titleColor = theme.colors.tertiaryContent + placeholderIcon = Asset.Images.locationLiveCellEndedIcon.image + } + } + + return LiveLocationBannerViewData(placeholderIcon: placeholderIcon, + iconTint: iconTint, + title: title, + titleColor: titleColor, + timeLeftString: timeLeftString, + rightButtonTitle: rightButtonTitle, + rightButtonTag: rightButtonTag, + coordinate: liveCoordinate) + } + + private func generateTimerString(for timestamp: Double, + isIncomingLocation: Bool) -> String? { + let timerInSec = timestamp + let timerString: String? + if isIncomingLocation { + timerString = VectorL10n.locationSharingLiveTimerIncoming(incomingTimerFormatter.string(from: Date(timeIntervalSince1970: timerInSec))) + } else if let outgoingTimer = outgoingTimerFormatter.string(from: Date(timeIntervalSince1970: timerInSec).timeIntervalSinceNow) { + timerString = VectorL10n.locationSharingLiveListItemTimeLeft(outgoingTimer) + } else { + timerString = nil + } + return timerString } // MARK: - Public - public func displayLocation(_ location: CLLocationCoordinate2D, - userAvatarData: AvatarViewData? = nil, - mapStyleURL: URL) { - - mapView.styleURL = mapStyleURL - - annotationView = LocationMarkerView.loadFromNib() - - if let userAvatarData = userAvatarData { - let avatarBackgroundColor = Self.usernameColorGenerator.color(from: userAvatarData.matrixItemId) - annotationView?.setAvatarData(userAvatarData, avatarBackgroundColor: avatarBackgroundColor) - } - - if let annotations = mapView.annotations { - mapView.removeAnnotations(annotations) - } - - mapView.setCenter(location, zoomLevel: Constants.mapZoomLevel, animated: false) - - let pointAnnotation = MGLPointAnnotation() - pointAnnotation.coordinate = location - mapView.addAnnotation(pointAnnotation) + public func displayStaticLocation(with viewData: RoomTimelineLocationViewData) { + displayLocation(viewData.location, + userAvatarData: viewData.userAvatarData, + mapStyleURL: viewData.mapStyleURL, + bannerViewData: nil) } + public func displayLiveLocation(with viewData: RoomTimelineLocationViewData, liveLocationViewState: TimelineLiveLocationViewState) { + let bannerViewData = liveLocationBannerViewData(from: liveLocationViewState) + displayLocation(bannerViewData.coordinate, + userAvatarData: viewData.userAvatarData, + mapStyleURL: viewData.mapStyleURL, + bannerViewData: bannerViewData) + + } + + // MARK: - Themable func update(theme: Theme) { @@ -107,6 +328,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat descriptionIcon.tintColor = theme.colors.accent attributionLabel.textColor = theme.colors.accent layer.borderColor = theme.colors.quinaryContent.cgColor + self.theme = theme } // MARK: - MGLMapViewDelegate @@ -114,4 +336,14 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { return annotationView } + + // MARK: - Action + + @IBAction private func didTapTightButton(_ sender: Any) { + if rightButton.tag == RightButtonTag.stopSharing.rawValue { + delegate?.roomTimelineLocationViewDidTapStopButton(self) + } else if rightButton.tag == RightButtonTag.retrySharing.rawValue { + delegate?.roomTimelineLocationViewDidTapRetryButton(self) + } + } } diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib index fb2e22a29..79e8d7097 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib @@ -4,7 +4,6 @@ - @@ -15,15 +14,26 @@ - - + + + + + + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + @@ -84,12 +168,21 @@ + + + + + + + - + + + diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index bdb355b23..b5203ad1f 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -32,6 +32,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { private let userIndicatorStore: UserIndicatorStore private var selectedEventId: String? private var loadingCancel: UserIndicatorCancel? + private var locationSharingIndicatorCancel: UserIndicatorCancel? // Used for location sharing advertizements private var roomDataSourceManager: MXKRoomDataSourceManager { return MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.parameters.session) @@ -248,6 +249,87 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { completion?() } + private func showLiveLocationViewer() { + guard let roomId = self.roomId else { + return + } + + self.showLiveLocationViewer(for: roomId) + } + + private func showLiveLocationViewer(for roomId: String) { + + guard let mxSession = self.mxSession, let navigationRouter = self.navigationRouter else { + return + } + + guard mxSession.locationService.isSomeoneSharingDisplayableLocation(inRoomWithId: roomId) else { + return + } + + let parameters = LiveLocationSharingViewerCoordinatorParameters(session: mxSession, roomId: roomId, navigationRouter: nil) + + let coordinator = LiveLocationSharingViewerCoordinator(parameters: parameters) + + coordinator.completion = { [weak self, weak coordinator] in + guard let self = self, let coordinator = coordinator else { + return + } + + self.navigationRouter?.dismissModule(animated: true, completion: nil) + self.remove(childCoordinator: coordinator) + } + + add(childCoordinator: coordinator) + + navigationRouter.present(coordinator, animated: true) + coordinator.start() + } + + private func stopLiveLocationSharing(forBeaconInfoEventId beaconInfoEventId: String? = nil, inRoomWithId roomId: String) { + guard let session = self.mxSession else { + return + } + + let errorHandler: (Error) -> Void = { error in + + let viewController = self.roomViewController + + viewController.errorPresenter.presentError(from: viewController, title: VectorL10n.error, message: VectorL10n.locationSharingLiveStopSharingError, animated: true) { + } + } + + // TODO: Handle loading state on the banner by replacing stop button with a spinner + self.showLocationSharingIndicator(withMessage: VectorL10n.locationSharingLiveStopSharingProgress) + + if let beaconInfoEventId = beaconInfoEventId { + session.locationService.stopUserLocationSharing(withBeaconInfoEventId: beaconInfoEventId, roomId: roomId) { + [weak self] response in + + self?.hideLocationSharingIndicator() + + switch response { + case .success: + break + case .failure(let error): + errorHandler(error) + } + } + } else { + session.locationService.stopUserLocationSharing(inRoomWithId: roomId) { [weak self] response in + + self?.hideLocationSharingIndicator() + + switch response { + case .success: + break + case .failure(let error): + errorHandler(error) + } + } + } + } + private func showLocationCoordinatorWithEvent(_ event: MXEvent, bubbleData: MXKRoomBubbleCellDataStoring) { guard #available(iOS 14.0, *) else { return @@ -371,6 +453,19 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { loadingCancel?() loadingCancel = nil } + + private func showLocationSharingIndicator(withMessage message: String) { + guard locationSharingIndicatorCancel == nil else { + return + } + + locationSharingIndicatorCancel = userIndicatorStore.present(type: .loading(label: message, isInteractionBlocking: false)) + } + + private func hideLocationSharingIndicator() { + locationSharingIndicatorCancel?() + locationSharingIndicatorCancel = nil + } } // MARK: - RoomIdentifiable @@ -449,6 +544,15 @@ extension RoomCoordinator: RoomViewControllerDelegate { showLocationCoordinatorWithEvent(event, bubbleData: bubbleData) } + func roomViewController(_ roomViewController: RoomViewController, didRequestLiveLocationPresentationForBubbleData bubbleData: MXKRoomBubbleCellDataStoring) { + + guard let roomId = bubbleData.roomId else { + return + } + + showLiveLocationViewer(for: roomId) + } + func roomViewController(_ roomViewController: RoomViewController, locationShareActivityViewControllerFor event: MXEvent) -> UIActivityViewController? { guard let location = event.location else { return nil @@ -494,11 +598,17 @@ extension RoomCoordinator: RoomViewControllerDelegate { } func roomViewControllerDidTapLiveLocationSharingBanner(_ roomViewController: RoomViewController) { - // TODO: + + showLiveLocationViewer() } - func roomViewControllerDidStopLiveLocationSharing(_ roomViewController: RoomViewController) { - // TODO: + func roomViewControllerDidStopLiveLocationSharing(_ roomViewController: RoomViewController, beaconInfoEventId: String?) { + + guard let roomId = self.roomId else { + return + } + + self.stopLiveLocationSharing(forBeaconInfoEventId: beaconInfoEventId, inRoomWithId: roomId) } func threadsCoordinator(for roomViewController: RoomViewController, threadId: String?) -> ThreadsCoordinatorBridgePresenter? { diff --git a/Riot/Modules/Room/RoomViewController+LocationSharing.swift b/Riot/Modules/Room/RoomViewController+LocationSharing.swift index d0a69b2c4..ffc9bf57d 100644 --- a/Riot/Modules/Room/RoomViewController+LocationSharing.swift +++ b/Riot/Modules/Room/RoomViewController+LocationSharing.swift @@ -53,7 +53,7 @@ import Foundation guard let self = self else { return } - self.delegate?.roomViewControllerDidStopLiveLocationSharing(self) + self.delegate?.roomViewControllerDidStopLiveLocationSharing(self, beaconInfoEventId: nil) } self.topBannersStackView?.addArrangedSubview(bannerView) diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 6a76b76fc..6e6107899 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -70,6 +70,9 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification; // Remove Jitsi widget container @property (weak, nonatomic, nullable) IBOutlet UIView *removeJitsiWidgetContainer; +// Error presenter +@property (nonatomic, strong, readonly) MXKErrorAlertPresentation *errorPresenter; + /** Preview data for a room invitation received by email, or a link to a room. */ @@ -249,6 +252,10 @@ handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters; didRequestLocationPresentationForEvent:(MXEvent *)event bubbleData:(id)bubbleData; +/// Ask the coordinator to present the live location sharing viewer. +- (void)roomViewController:(RoomViewController *)roomViewController +didRequestLiveLocationPresentationForBubbleData:(id)bubbleData; + - (nullable UIActivityViewController *)roomViewController:(RoomViewController *)roomViewController locationShareActivityViewControllerForEvent:(MXEvent *)event; @@ -281,7 +288,7 @@ didRequestEditForPollWithStartEvent:(MXEvent *)startEvent; - (void)roomViewControllerDidStopLoading:(RoomViewController *)roomViewController; /// User tap live location sharing stop action -- (void)roomViewControllerDidStopLiveLocationSharing:(RoomViewController *)roomViewController; +- (void)roomViewControllerDidStopLiveLocationSharing:(RoomViewController *)roomViewController beaconInfoEventId:(nullable NSString*)beaconInfoEventId; /// User tap live location sharing banner - (void)roomViewControllerDidTapLiveLocationSharingBanner:(RoomViewController *)roomViewController; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index e12eaae98..4a6caca8c 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -2905,7 +2905,7 @@ static CGSize kThreadListBarButtonItemImageSize; } } } - else if (bubbleData.tag == RoomBubbleCellDataTagLocation) + else if (bubbleData.tag == RoomBubbleCellDataTagLocation || bubbleData.tag == RoomBubbleCellDataTagLiveLocation) { if (bubbleData.isIncoming) { @@ -3156,6 +3156,26 @@ static CGSize kThreadListBarButtonItemImageSize; [self mention:roomMember]; } } + else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellStopShareButtonPressed]) + { + NSString *beaconInfoEventId; + + if ([bubbleData isKindOfClass:[RoomBubbleCellData class]]) + { + RoomBubbleCellData *roomBubbleCellData = (RoomBubbleCellData*)bubbleData; + beaconInfoEventId = roomBubbleCellData.beaconInfoSummary.id; + } + + [self.delegate roomViewControllerDidStopLiveLocationSharing:self beaconInfoEventId:beaconInfoEventId]; + } + else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellRetryShareButtonPressed]) + { + MXEvent *selectedEvent = userInfo[kMXKRoomBubbleCellEventKey]; + if (selectedEvent) + { + // TODO: - Implement retry live location action + } + } else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnMessageTextView] || [actionIdentifier isEqualToString:kMXKRoomBubbleCellTapOnContentView]) { // Retrieve the tapped event @@ -3166,6 +3186,10 @@ static CGSize kThreadListBarButtonItemImageSize; { [self cancelEventSelection]; } + else if (bubbleData.tag == RoomBubbleCellDataTagLiveLocation) + { + [self.delegate roomViewController:self didRequestLiveLocationPresentationForBubbleData:bubbleData]; + } else if (tappedEvent) { if (tappedEvent.eventType == MXEventTypeRoomCreate) diff --git a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.h b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.h index 8e247835f..15e18f89b 100644 --- a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.h +++ b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.h @@ -83,6 +83,20 @@ extern NSString *const kMXKRoomBubbleCellTapOnContentView; */ extern NSString *const kMXKRoomBubbleCellUnsentButtonPressed; +/** + Action identifier used when the user pressed stop share button displayed in live location cell. + + The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the live location event to stop. + */ +extern NSString *const kMXKRoomBubbleCellStopShareButtonPressed; + +/** + Action identifier used when the user pressed retry share button displayed in live location cell. + + The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the live location event to retry. + */ +extern NSString *const kMXKRoomBubbleCellRetryShareButtonPressed; + /** Action identifier used when the user long pressed on a displayed event. diff --git a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m index c4367d598..f5fcdc4ff 100644 --- a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m +++ b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m @@ -41,7 +41,10 @@ NSString *const kMXKRoomBubbleCellTapOnAttachmentView = @"kMXKRoomBubbleCellTapO NSString *const kMXKRoomBubbleCellTapOnOverlayContainer = @"kMXKRoomBubbleCellTapOnOverlayContainer"; NSString *const kMXKRoomBubbleCellTapOnContentView = @"kMXKRoomBubbleCellTapOnContentView"; + NSString *const kMXKRoomBubbleCellUnsentButtonPressed = @"kMXKRoomBubbleCellUnsentButtonPressed"; +NSString *const kMXKRoomBubbleCellStopShareButtonPressed = @"kMXKRoomBubbleCellStopShareButtonPressed"; +NSString *const kMXKRoomBubbleCellRetryShareButtonPressed = @"kMXKRoomBubbleCellRetryShareButtonPressed"; NSString *const kMXKRoomBubbleCellLongPressOnEvent = @"kMXKRoomBubbleCellLongPressOnEvent"; NSString *const kMXKRoomBubbleCellLongPressOnProgressView = @"kMXKRoomBubbleCellLongPressOnProgressView"; diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift index 4b86e4209..40f59eb50 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift @@ -15,41 +15,99 @@ // import Foundation +import MatrixSDK class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCellReadMarkerDisplayable { private var locationView: RoomTimelineLocationView! + private var event: MXEvent? override func render(_ cellData: MXKCellData!) { super.render(cellData) guard #available(iOS 14.0, *), - let bubbleData = cellData as? RoomBubbleCellData, - let event = bubbleData.events.last, - event.eventType == __MXEventType.roomMessage, - let locationContent = event.location + let bubbleData = cellData as? RoomBubbleCellData else { return } locationView.update(theme: ThemeService.shared().theme) + + if bubbleData.cellDataTag == .location, + let event = bubbleData.events.last { + self.event = event + renderStaticLocation(event) + } else if bubbleData.cellDataTag == .liveLocation, + let beaconInfoSummary = bubbleData.beaconInfoSummary { + renderLiveLocation(beaconInfoSummary) + } + } + + private func renderStaticLocation(_ event: MXEvent) { + guard let locationContent = event.location else { + return + } + locationView.locationDescription = locationContent.locationDescription let location = CLLocationCoordinate2D(latitude: locationContent.latitude, longitude: locationContent.longitude) let mapStyleURL = bubbleData.mxSession.vc_homeserverConfiguration().tileServer.mapStyleURL + let avatarViewData: AvatarViewData? + if locationContent.assetType == .user { - let avatarViewData = AvatarViewData(matrixItemId: bubbleData.senderId, - displayName: bubbleData.senderDisplayName, - avatarUrl: bubbleData.senderAvatarUrl, - mediaManager: bubbleData.mxSession.mediaManager, - fallbackImage: .matrixItem(bubbleData.senderId, bubbleData.senderDisplayName)) - - locationView.displayLocation(location, userAvatarData: avatarViewData, mapStyleURL: mapStyleURL) + avatarViewData = AvatarViewData(matrixItemId: bubbleData.senderId, + displayName: bubbleData.senderDisplayName, + avatarUrl: bubbleData.senderAvatarUrl, + mediaManager: bubbleData.mxSession.mediaManager, + fallbackImage: .matrixItem(bubbleData.senderId, bubbleData.senderDisplayName)) } else { - locationView.displayLocation(location, mapStyleURL: mapStyleURL) + avatarViewData = nil } + + locationView.displayStaticLocation(with: RoomTimelineLocationViewData(location: location, userAvatarData: avatarViewData, mapStyleURL: mapStyleURL)) + } + + private func renderLiveLocation(_ beaconInfoSummary: MXBeaconInfoSummaryProtocol) { + let liveLocationState: TimelineLiveLocationViewState = locationSharingViewState(from: beaconInfoSummary) + let avatarViewData = AvatarViewData(matrixItemId: bubbleData.senderId, + displayName: bubbleData.senderDisplayName, + avatarUrl: bubbleData.senderAvatarUrl, + mediaManager: bubbleData.mxSession.mediaManager, + fallbackImage: .matrixItem(bubbleData.senderId, bubbleData.senderDisplayName)) + let mapStyleURL = bubbleData.mxSession.vc_homeserverConfiguration().tileServer.mapStyleURL + + locationView.displayLiveLocation(with: RoomTimelineLocationViewData(location: nil, userAvatarData: avatarViewData, mapStyleURL: mapStyleURL), + liveLocationViewState: liveLocationState) + } + + private func locationSharingViewState(from beaconInfoSummary: MXBeaconInfoSummaryProtocol) -> TimelineLiveLocationViewState { + + let viewState: TimelineLiveLocationViewState + + let liveLocationStatus: LiveLocationSharingStatus + + if beaconInfoSummary.hasStopped || beaconInfoSummary.hasExpired { + liveLocationStatus = .stopped + } else if let lastBeacon = beaconInfoSummary.lastBeacon { + + let expiryTimeinterval = TimeInterval(beaconInfoSummary.expiryTimestamp/1000) // Timestamp is in millisecond in the SDK + + let coordinate = CLLocationCoordinate2D(latitude: lastBeacon.location.latitude, longitude: lastBeacon.location.longitude) + + liveLocationStatus = .started(coordinate, expiryTimeinterval) + } else { + liveLocationStatus = .starting + } + + if beaconInfoSummary.userId == bubbleData.mxSession.myUserId { + viewState = .outgoing(liveLocationStatus) + } else { + viewState = .incoming(liveLocationStatus) + } + + return viewState } override func setupViews() { @@ -61,11 +119,29 @@ class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, Room guard #available(iOS 14.0, *), let contentView = roomCellContentView?.innerContentView else { - return - } + return + } locationView = RoomTimelineLocationView.loadFromNib() contentView.vc_addSubViewMatchingParent(locationView) } } + +extension LocationPlainCell: RoomTimelineLocationViewDelegate { + func roomTimelineLocationViewDidTapStopButton(_ roomTimelineLocationView: RoomTimelineLocationView) { + guard let event = self.event else { + return + } + + delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellStopShareButtonPressed, userInfo: [kMXKRoomBubbleCellEventKey: event]) + } + + func roomTimelineLocationViewDidTapRetryButton(_ roomTimelineLocationView: RoomTimelineLocationView) { + guard let event = self.event else { + return + } + + delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellRetryShareButtonPressed, userInfo: [kMXKRoomBubbleCellEventKey: event]) + } +} diff --git a/Riot/Modules/Rooms/RoomsViewController.m b/Riot/Modules/Rooms/RoomsViewController.m index 51b203c44..e6a595a69 100644 --- a/Riot/Modules/Rooms/RoomsViewController.m +++ b/Riot/Modules/Rooms/RoomsViewController.m @@ -139,7 +139,7 @@ // Check whether the recents data source is correctly configured. if (recentsDataSource.recentsDataSourceMode == RecentsDataSourceModeRooms) { - [self scrollToTheTopTheNextRoomWithMissedNotificationsInSection:[recentsDataSource.sections sectionTypeForSectionIndex:RecentsDataSourceSectionTypeConversation]]; + [self scrollToTheTopTheNextRoomWithMissedNotificationsInSection:[recentsDataSource.sections sectionIndexForSectionType:RecentsDataSourceSectionTypeConversation]]; } } diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift index c194d0b90..599b50c38 100644 --- a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift +++ b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift @@ -116,18 +116,19 @@ final class SpaceListViewModel: SpaceListViewModelType { } func select(spaceWithId spaceId: String) { - for (sectionIndex, section) in self.sections.enumerated() { - switch section { - case .home: break - case .addSpace: break - case .spaces(let viewDataList): - for (row, itemViewData) in viewDataList.enumerated() where itemViewData.spaceId == spaceId { - let indexPath = IndexPath(row: row, section: sectionIndex) - self.selectSpace(with: spaceId) - self.selectedIndexPath = indexPath - self.update(viewState: .selectionChanged(indexPath)) - } - } + var foundIndexPath: IndexPath? + + if let spaceService = self.userSessionsService.mainUserSession?.matrixSession.spaceService, + let firstRootAncestor = spaceService.firstRootAncestorForRoom(withId: spaceId) { + foundIndexPath = indexPathOf(spaceWithId: firstRootAncestor.spaceId) + } else { + foundIndexPath = indexPathOf(spaceWithId: spaceId) + } + + if let indexPath = foundIndexPath { + self.selectSpace(with: spaceId) + self.selectedIndexPath = indexPath + self.update(viewState: .selectionChanged(indexPath)) } } @@ -303,4 +304,20 @@ final class SpaceListViewModel: SpaceListViewModelType { self.update(viewState: .loaded([])) } + + private func indexPathOf(spaceWithId spaceId: String) -> IndexPath? { + for (sectionIndex, section) in self.sections.enumerated() { + switch section { + case .home: break + case .addSpace: break + case .spaces(let viewDataList): + for (row, itemViewData) in viewDataList.enumerated() where itemViewData.spaceId == spaceId { + return IndexPath(row: row, section: sectionIndex) + } + } + } + + return nil + } + } diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift index dce516a80..ef3290ec7 100644 --- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift +++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift @@ -142,7 +142,11 @@ final class SpaceMemberListViewController: RoomParticipantsViewController { private func renderLoaded(space: MXSpace) { self.activityPresenter.removeCurrentActivityIndicator(animated: true) self.mxRoom = space.room - self.titleView.subtitleLabel.text = space.summary?.displayname + if let spaceName = space.summary?.displayname { + self.titleView.breadcrumbView.breadcrumbs = [spaceName] + } else { + self.titleView.breadcrumbView.breadcrumbs = [] + } } private func render(error: Error) { diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift index 8954f504d..6cf4d886e 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuListItemViewData.swift @@ -25,6 +25,7 @@ enum SpaceMenuListItemAction { case addSpace case settings case leaveSpace + case leaveSpaceAndChooseRooms case invite } diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift index 2cc8d9dbd..8093578d5 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuPresenter.swift @@ -45,7 +45,9 @@ class SpaceMenuPresenter: NSObject { private weak var selectedSpace: MXSpace? private var session: MXSession! private var spaceId: String! - + private weak var spaceMenuViewController: SpaceMenuViewController? + private var leaveSpaceCoordinator: Coordinator? + // MARK: - Public func present(forSpaceWithId spaceId: String, @@ -74,6 +76,7 @@ class SpaceMenuPresenter: NSObject { private func showMenu(for spaceId: String, session: MXSession) { let menuViewController = SpaceMenuViewController.instantiate(forSpaceWithId: spaceId, matrixSession: session, viewModel: self.viewModel) self.present(menuViewController, animated: true) + self.spaceMenuViewController = menuViewController } private func present(_ viewController: SpaceMenuViewController, animated: Bool) { @@ -96,6 +99,29 @@ class SpaceMenuPresenter: NSObject { self.presentingViewController?.present(viewController, animated: animated, completion: nil) } } + + @available(iOS 14.0, *) + private func showLeaveSpace() { + let name = session.spaceService.getSpace(withId: spaceId)?.summary?.displayname ?? VectorL10n.spaceTag + + let selectionHeader = MatrixItemChooserSelectionHeader(title: VectorL10n.leaveSpaceSelectionTitle, + selectAllTitle: VectorL10n.leaveSpaceSelectionAllRooms, + selectNoneTitle: VectorL10n.leaveSpaceSelectionNoRooms) + let paramaters = MatrixItemChooserCoordinatorParameters(session: session, + title: VectorL10n.leaveSpaceTitle(name), + detail: VectorL10n.leaveSpaceMessage(name), + selectionHeader: selectionHeader, + viewProvider: LeaveSpaceViewProvider(navTitle: nil), + itemsProcessor: LeaveSpaceItemsProcessor(spaceId: spaceId, session: session)) + let coordinator = MatrixItemChooserCoordinator(parameters: paramaters) + coordinator.start() + self.leaveSpaceCoordinator = coordinator + coordinator.completion = { [weak self] result in + self?.spaceMenuViewController?.dismiss(animated: true) + self?.leaveSpaceCoordinator = nil + } + self.spaceMenuViewController?.present(coordinator.toPresentable(), animated: true) + } } // MARK: - SpaceMenuModelViewModelCoordinatorDelegate @@ -120,6 +146,10 @@ extension SpaceMenuPresenter: SpaceMenuModelViewModelCoordinatorDelegate { self.delegate?.spaceMenuPresenter(self, didCompleteWith: .settings, forSpaceWithId: self.spaceId, with: self.session) case .invite: self.delegate?.spaceMenuPresenter(self, didCompleteWith: .invite, forSpaceWithId: self.spaceId, with: self.session) + case .leaveSpaceAndChooseRooms: + if #available(iOS 14.0, *) { + self.showLeaveSpace() + } default: MXLog.error("[SpaceMenuPresenter] spaceListViewModel didSelectItem: invalid action \(action)") } diff --git a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift index 51409d1ca..fb0fbbd9b 100644 --- a/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift +++ b/Riot/Modules/Spaces/SpaceMenu/SpaceMenuViewModel.swift @@ -85,18 +85,24 @@ class SpaceMenuViewModel: SpaceMenuViewModelType { } private func leaveSpace() { - guard let room = self.session.room(withRoomId: self.spaceId), let displayName = room.summary?.displayname else { + guard #available(iOS 14, *) else { + guard let room = self.session.room(withRoomId: self.spaceId), let displayName = room.summary?.displayname else { + return + } + + var isAdmin = false + if let roomState = room.dangerousSyncState, let powerLevels = roomState.powerLevels { + let powerLevel = powerLevels.powerLevelOfUser(withUserID: self.session.myUserId) + let roomPowerLevel = RoomPowerLevelHelper.roomPowerLevel(from: powerLevel) + isAdmin = roomPowerLevel == .admin + } + + self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .leaveOptions(displayName, isAdmin)) return } - - var isAdmin = false - if let roomState = room.dangerousSyncState, let powerLevels = roomState.powerLevels { - let powerLevel = powerLevels.powerLevelOfUser(withUserID: self.session.myUserId) - let roomPowerLevel = RoomPowerLevelHelper.roomPowerLevel(from: powerLevel) - isAdmin = roomPowerLevel == .admin - } - - self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .leaveOptions(displayName, isAdmin)) + + self.viewDelegate?.spaceMenuViewModel(self, didUpdateViewState: .deselect) + self.coordinatorDelegate?.spaceMenuViewModel(self, didSelectItemWith: .leaveSpaceAndChooseRooms) } private func leaveSpaceAndKeepRooms() { diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift index 1fe801a1d..cccdd1860 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift @@ -190,7 +190,7 @@ final class SpaceExploreRoomViewController: UIViewController { case .loading: self.renderLoading() case .spaceNameFound(let spaceName): - self.titleView.subtitleLabel.text = spaceName + self.titleView.breadcrumbView.breadcrumbs = [spaceName] case .loaded(let children, let hasMore): self.hasMore = hasMore self.renderLoaded(children: children) diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift index 1ccf521fb..8e2a72808 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoomCoordinator.swift @@ -422,6 +422,10 @@ extension ExploreRoomCoordinator: RoomViewControllerDelegate { // TODO: } + func roomViewController(_ roomViewController: RoomViewController, didRequestLiveLocationPresentationForBubbleData bubbleData: MXKRoomBubbleCellDataStoring) { + // TODO: + } + func roomViewController(_ roomViewController: RoomViewController, didRequestLocationPresentationFor event: MXEvent, bubbleData: MXKRoomBubbleCellDataStoring) { // TODO: } @@ -474,7 +478,7 @@ extension ExploreRoomCoordinator: RoomViewControllerDelegate { // TODO: } - func roomViewControllerDidStopLiveLocationSharing(_ roomViewController: RoomViewController) { + func roomViewControllerDidStopLiveLocationSharing(_ roomViewController: RoomViewController, beaconInfoEventId: String?) { // TODO: } diff --git a/Riot/Modules/TabBar/BreadcrumbView.swift b/Riot/Modules/TabBar/BreadcrumbView.swift new file mode 100644 index 000000000..bf0471e6d --- /dev/null +++ b/Riot/Modules/TabBar/BreadcrumbView.swift @@ -0,0 +1,128 @@ +// +// 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 +import UIKit + +/// `BreadcrumbView` can be used to display a path into a single line of text and manages ellipsis. +@objcMembers +class BreadcrumbView: UIView, Themable { + + // MARK: - Constants + + private enum Constants { + static let separator: String = "/" + } + + // MARK: - Properties + + public var breadcrumbs: [String] = [] { + didSet { + populateLabels() + } + } + + // MARK: - Private + + private var labels: [UILabel] = [] + + // MARK: - Lifecycle + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + // MARK: - Themable + + func update(theme: Theme) { + for label in labels { + update(theme: theme, for: label) + } + } + + // MARK: - Private + + private func populateLabels() { + for label in labels { + label.removeFromSuperview() + } + + labels.removeAll() + + for (index, breadcrumb) in breadcrumbs.enumerated() { + if index > 0 { + createLabel(with: Constants.separator, at: index) + } + createLabel(with: breadcrumb, at: index) + } + self.layoutIfNeeded() + } + + private func createLabel(with text: String?, at index: Int) { + guard let text = text, !text.isEmpty else { + return + } + + let label = UILabel(frame: .zero) + label.backgroundColor = .clear + label.text = text + let priority: UILayoutPriority + if index < breadcrumbs.count - 1 { + // We put a higher priority to the first element then decrease the priority linearly for the next elements. + priority = UILayoutPriority(UILayoutPriority.defaultLow.rawValue + Float(breadcrumbs.count * 2 - labels.count)) + } else { + // The last element has the highest priority + priority = .defaultHigh + } + label.setContentCompressionResistancePriority(priority, for: .horizontal) + + update(theme: ThemeService.shared().theme, for: label) + + self.addSubview(label) + self.labels.append(label) + + label.translatesAutoresizingMaskIntoConstraints = false + label.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor).isActive = true + label.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor).isActive = true + + if let prevSibling = prevSibling(of: label) { + label.leadingAnchor.constraint(equalTo: prevSibling.trailingAnchor).isActive = true + } else { + label.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor).isActive = true + } + + if index == breadcrumbs.count - 1 && label.text != Constants.separator { + label.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor).isActive = true + } + } + + private func prevSibling(of label: UILabel) -> UILabel? { + guard let index = labels.firstIndex(of: label), index - 1 >= 0 else { + return nil + } + + return labels[index-1] + } + + private func update(theme: Theme, for label: UILabel) { + label.textColor = theme.colors.tertiaryContent + label.font = theme.fonts.footnote + } +} diff --git a/Riot/Modules/TabBar/MainTitleView.swift b/Riot/Modules/TabBar/MainTitleView.swift index 701dc4d90..2194f542e 100644 --- a/Riot/Modules/TabBar/MainTitleView.swift +++ b/Riot/Modules/TabBar/MainTitleView.swift @@ -22,7 +22,7 @@ class MainTitleView: UIStackView, Themable { // MARK: - Properties public private(set) var titleLabel: UILabel! - public private(set) var subtitleLabel: UILabel! + public private(set) var breadcrumbView: BreadcrumbView! // MARK: - Lifecycle @@ -42,8 +42,7 @@ class MainTitleView: UIStackView, Themable { self.titleLabel.textColor = theme.colors.primaryContent self.titleLabel.font = theme.fonts.calloutSB - self.subtitleLabel.textColor = theme.colors.tertiaryContent - self.subtitleLabel.font = theme.fonts.footnote + self.breadcrumbView.update(theme: theme) } // MARK: - Private @@ -52,11 +51,10 @@ class MainTitleView: UIStackView, Themable { self.titleLabel = UILabel(frame: .zero) self.titleLabel.backgroundColor = .clear - self.subtitleLabel = UILabel(frame: .zero) - self.subtitleLabel.backgroundColor = .clear + self.breadcrumbView = BreadcrumbView(frame: .zero) self.addArrangedSubview(titleLabel) - self.addArrangedSubview(subtitleLabel) + self.addArrangedSubview(breadcrumbView) self.distribution = .equalCentering self.axis = .vertical self.alignment = .center diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index 435f7ebcb..91dab11c1 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -723,8 +723,24 @@ - (void)filterRoomsWithParentId:(NSString*)roomParentId inMatrixSession:(MXSession*)mxSession { - titleView.subtitleLabel.text = roomParentId ? [mxSession roomSummaryWithRoomId:roomParentId].displayname : nil; + if (roomParentId) { + NSString *parentName = [mxSession roomSummaryWithRoomId:roomParentId].displayname; + NSMutableArray *breadcrumbs = [[NSMutableArray alloc] initWithObjects:parentName, nil]; + MXSpace *firstRootAncestor = roomParentId ? [mxSession.spaceService firstRootAncestorForRoomWithId:roomParentId] : nil; + NSString *rootName = nil; + if (firstRootAncestor) + { + rootName = [mxSession roomSummaryWithRoomId:firstRootAncestor.spaceId].displayname; + [breadcrumbs insertObject:rootName atIndex:0]; + } + titleView.breadcrumbView.breadcrumbs = breadcrumbs; + } + else + { + titleView.breadcrumbView.breadcrumbs = @[]; + } + recentsDataSource.currentSpace = [mxSession.spaceService getSpaceWithId:roomParentId]; [self updateSideMenuNotifcationIcon]; } diff --git a/Riot/target.yml b/Riot/target.yml index cee9c6613..17686d9e0 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -51,6 +51,13 @@ targets: runOnlyWhenInstalling: false shell: /bin/sh script: "\"${PODS_ROOT}/SwiftGen/bin/swiftgen\" config run --config \"Tools/SwiftGen/swiftgen-config.yml\"\n" + - name: 📖 locheck + runOnlyWhenInstalling: false + shell: /bin/sh + script: | + # homebrew uses a non-standard directory on M1 + if [[ $(arch) = arm64 ]]; then export PATH="$PATH:/opt/homebrew/bin"; fi + xcrun --sdk macosx mint run Asana/locheck@0.9.6 discoverlproj --treat-warnings-as-errors --ignore-missing --ignore lproj_file_missing_from_translation $PROJECT_DIR/Riot/Assets sources: - path: ../RiotSwiftUI/Modules diff --git a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift new file mode 100644 index 000000000..75fcac428 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift @@ -0,0 +1,77 @@ +// +// 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 + +/// A value that represents the type of authentication flow being used. +enum AuthenticationFlow { + case login + case registration +} + +/// Errors that can be thrown from `AuthenticationService`. +enum AuthenticationError: String, LocalizedError { + /// A failure to convert a struct into a dictionary. + case dictionaryError + case invalidHomeserver + case loginFlowNotCalled + case missingMXRestClient + + var errorDescription: String? { + switch self { + case .invalidHomeserver: + return VectorL10n.authenticationServerSelectionGenericError + default: + return VectorL10n.errorCommonMessage + } + } +} + +/// Errors that can be thrown from `RegistrationWizard` +enum RegistrationError: String, LocalizedError { + case registrationDisabled + case createAccountNotCalled + case missingThreePIDData + case missingThreePIDURL + case threePIDValidationFailure + case threePIDClientFailure + + var errorDescription: String? { + switch self { + case .registrationDisabled: + return VectorL10n.loginErrorRegistrationIsNotSupported + default: + return VectorL10n.errorCommonMessage + } + } +} + +/// Errors that can be thrown from `LoginWizard` +enum LoginError: String, Error { + case unimplemented +} + +/// Represents an SSO Identity Provider as provided in a login flow. +struct SSOIdentityProvider: Identifiable { + /// The identifier field (id field in JSON) is the Identity Provider identifier used for the SSO Web page redirection `/login/sso/redirect/{idp_id}`. + let id: String + /// The name field is a human readable string intended to be printed by the client. + let name: String + /// The brand field is optional. It allows the client to style the login button to suit a particular brand. + let brand: String? + /// The icon field is an optional field that points to an icon representing the identity provider. If present then it must be an HTTPS URL to an image resource. + let iconURL: String? +} diff --git a/RiotSwiftUI/Modules/Authentication/Common/HomeserverAddress.swift b/RiotSwiftUI/Modules/Authentication/Common/HomeserverAddress.swift new file mode 100644 index 000000000..d0bca43e0 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Common/HomeserverAddress.swift @@ -0,0 +1,31 @@ +// +// 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 + +struct HomeserverAddress { + /// Ensures the address contains a scheme, otherwise makes it `https`. + static func sanitized(_ address: String) -> String { + !address.contains("://") ? "https://\(address.lowercased())" : address.lowercased() + } + + /// Strips the `https://` away from the address (but leaves `http://`) for display in labels. + /// + /// `http://` is left in the string to make it clear when a chosen server doesn't use SSL. + static func displayable(_ address: String) -> String { + address.replacingOccurrences(of: "https://", with: "") + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationPendingData.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationPendingData.swift new file mode 100644 index 000000000..2aa4bc499 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationPendingData.swift @@ -0,0 +1,43 @@ +// +// 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 + +/// This class holds all pending data when creating a session, either by login or by register +class AuthenticationPendingData { + let homeserverAddress: String + + // MARK: - Common + + var clientSecret = UUID().uuidString + var sendAttempt: UInt = 0 + + // MARK: - For login + + // var resetPasswordData: ResetPasswordData? + + // MARK: - For registration + + var currentSession: String? + var isRegistrationStarted = false + var currentThreePIDData: ThreePIDData? + + // MARK: - Setup + + init(homeserverAddress: String) { + self.homeserverAddress = homeserverAddress + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift new file mode 100644 index 000000000..38021f43e --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift @@ -0,0 +1,247 @@ +// +// 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 + +@available(iOS 14.0, *) +protocol AuthenticationServiceDelegate: AnyObject { + func authenticationServiceDidUpdateRegistrationParameters(_ authenticationService: AuthenticationService) +} + +@available(iOS 14.0, *) +class AuthenticationService: NSObject { + + /// The shared service object. + static let shared = AuthenticationService() + + // MARK: - Properties + + // MARK: Private + + /// The rest client used to make authentication requests. + private var client: MXRestClient + /// The object used to create a new `MXSession` when authentication has completed. + private var sessionCreator = SessionCreator() + + // MARK: Public + + /// The current state of the authentication flow. + private(set) var state: AuthenticationState + /// The current login wizard or `nil` if `startFlow` hasn't been called. + private(set) var loginWizard: LoginWizard? + /// The current registration wizard or `nil` if `startFlow` hasn't been called for `.registration`. + private(set) var registrationWizard: RegistrationWizard? + + // MARK: - Setup + + override init() { + guard let homeserverURL = URL(string: BuildSettings.serverConfigDefaultHomeserverUrlString) else { + MXLog.failure("[AuthenticationService]: Failed to create URL from default homeserver URL string.") + fatalError("Invalid default homeserver URL string.") + } + + state = AuthenticationState(flow: .login, homeserverAddress: BuildSettings.serverConfigDefaultHomeserverUrlString) + client = MXRestClient(homeServer: homeserverURL, unrecognizedCertificateHandler: nil) + + super.init() + } + + // MARK: - Public + + /// Whether authentication is needed by checking for any accounts. + /// - Returns: `true` there are no accounts or if there is an inactive account that has had a soft logout. + var needsAuthentication: Bool { + MXKAccountManager.shared().accounts.isEmpty || softLogoutCredentials != nil + } + + /// Credentials to be used when authenticating after soft logout, otherwise `nil`. + var softLogoutCredentials: MXCredentials? { + guard MXKAccountManager.shared().activeAccounts.isEmpty else { return nil } + for account in MXKAccountManager.shared().accounts { + if account.isSoftLogout { + return account.mxCredentials + } + } + + return nil + } + + /// Get the last authenticated [Session], if there is an active session. + /// - Returns: The last active session if any, or `nil` + var lastAuthenticatedSession: MXSession? { + MXKAccountManager.shared().activeAccounts?.first?.mxSession + } + + func startFlow(_ flow: AuthenticationFlow, for homeserverAddress: String) async throws { + reset() + + let loginFlows = try await loginFlow(for: homeserverAddress) + + state.homeserver = .init(address: loginFlows.homeserverAddress, + addressFromUser: homeserverAddress, + preferredLoginMode: loginFlows.loginMode, + loginModeSupportedTypes: loginFlows.supportedLoginTypes) + + let loginWizard = LoginWizard() + self.loginWizard = loginWizard + + if flow == .registration { + do { + let registrationWizard = RegistrationWizard(client: client) + state.homeserver.registrationFlow = try await registrationWizard.registrationFlow() + self.registrationWizard = registrationWizard + } catch { + guard state.homeserver.preferredLoginMode.hasSSO, error as? RegistrationError == .registrationDisabled else { + throw error + } + // Continue without throwing when registration is disabled but SSO is available. + } + } + + state.flow = flow + } + + /// Get a SSO url + func getSSOURL(redirectUrl: String, deviceId: String?, providerId: String?) -> String? { + fatalError("Not implemented.") + } + + /// Get the sign in or sign up fallback URL + func fallbackURL(for flow: AuthenticationFlow) -> URL { + switch flow { + case .login: + return client.loginFallbackURL + case .registration: + return client.registerFallbackURL + } + } + + /// True when login and password has been sent with success to the homeserver + var isRegistrationStarted: Bool { + registrationWizard?.isRegistrationStarted ?? false + } + + /// Reset the service to a fresh state. + func reset() { + loginWizard = nil + registrationWizard = nil + + // The previously used homeserver is re-used as `startFlow` will be called again a replace it anyway. + self.state = AuthenticationState(flow: .login, homeserverAddress: state.homeserver.address) + } + + /// Create a session after a SSO successful login + func makeSessionFromSSO(credentials: MXCredentials) -> MXSession { + sessionCreator.createSession(credentials: credentials, client: client) + } + +// /// Perform a well-known request, using the domain from the matrixId +// func getWellKnownData(matrixId: String, +// homeServerConnectionConfig: HomeServerConnectionConfig?) async -> WellknownResult { +// +// } +// +// /// Authenticate with a matrixId and a password +// /// Usually call this after a successful call to getWellKnownData() +// /// - Parameter homeServerConnectionConfig the information about the homeserver and other configuration +// /// - Parameter matrixId the matrixId of the user +// /// - Parameter password the password of the account +// /// - Parameter initialDeviceName the initial device name +// /// - Parameter deviceId the device id, optional. If not provided or null, the server will generate one. +// func directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, +// matrixId: String, +// password: String, +// initialDeviceName: String, +// deviceId: String? = nil) async -> MXSession { +// +// } + + // MARK: - Private + + /// Request the supported login flows for this homeserver. + /// This is the first method to call to be able to get a wizard to login or to create an account + /// - Parameter homeserverAddress: The homeserver string entered by the user. + private func loginFlow(for homeserverAddress: String) async throws -> LoginFlowResult { + let homeserverAddress = HomeserverAddress.sanitized(homeserverAddress) + + guard var homeserverURL = URL(string: homeserverAddress) else { + MXLog.error("[AuthenticationService] Unable to create a URL from the supplied homeserver address when calling loginFlow.") + throw AuthenticationError.invalidHomeserver + } + + let state = AuthenticationState(flow: .login, homeserverAddress: homeserverAddress) + + if let wellKnown = try? await wellKnown(for: homeserverURL), + let baseURL = URL(string: wellKnown.homeServer.baseUrl) { + homeserverURL = baseURL + } + + #warning("Add an unrecognized certificate handler.") + let client = MXRestClient(homeServer: homeserverURL, unrecognizedCertificateHandler: nil) + + let loginFlow = try await getLoginFlowResult(client: client) + + self.client = client + self.state = state + + return loginFlow + } + + /// Request the supported login flows for the corresponding session. + /// This method is used to get the flows for a server after a soft-logout. + /// - Parameter session: The MXSession where a soft-logout has occurred. + private func loginFlow(for session: MXSession) async throws -> LoginFlowResult { + guard let client = session.matrixRestClient else { + MXLog.error("[AuthenticationService] loginFlow called on a session that doesn't have a matrixRestClient.") + throw AuthenticationError.missingMXRestClient + } + let state = AuthenticationState(flow: .login, homeserverAddress: client.homeserver) + + let loginFlow = try await getLoginFlowResult(client: session.matrixRestClient) + + self.client = client + self.state = state + + return loginFlow + } + + private func getLoginFlowResult(client: MXRestClient) async throws -> LoginFlowResult { + // Get the login flow + let loginFlowResponse = try await client.getLoginSession() + + let identityProviders = loginFlowResponse.flows?.compactMap { $0 as? MXLoginSSOFlow }.first?.identityProviders ?? [] + return LoginFlowResult(supportedLoginTypes: loginFlowResponse.flows?.compactMap { $0 } ?? [], + ssoIdentityProviders: identityProviders.sorted { $0.name < $1.name }.map { $0.ssoIdentityProvider }, + homeserverAddress: client.homeserver) + } + + /// Perform a well-known request on the specified homeserver URL. + private func wellKnown(for homeserverURL: URL) async throws -> MXWellKnown { + let wellKnownClient = MXRestClient(homeServer: homeserverURL, unrecognizedCertificateHandler: nil) + + // The .well-known/matrix/client API is often just a static file returned with no content type. + // Make our HTTP client compatible with this behaviour + wellKnownClient.acceptableContentTypes = nil + + return try await wellKnownClient.wellKnown() + } +} + +extension MXLoginSSOIdentityProvider { + var ssoIdentityProvider: SSOIdentityProvider { + SSOIdentityProvider(id: identifier, name: name, brand: brand, iconURL: icon) + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationState.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationState.swift new file mode 100644 index 000000000..05f8dd735 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationState.swift @@ -0,0 +1,48 @@ +// +// 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 +import MatrixSDK + +@available(iOS 14.0, *) +struct AuthenticationState { + // var serverType: ServerType = .unknown + var flow: AuthenticationFlow + + /// Information about the currently selected homeserver. + var homeserver: Homeserver + var isForceLoginFallbackEnabled = false + + init(flow: AuthenticationFlow, homeserverAddress: String) { + self.flow = flow + self.homeserver = Homeserver(address: homeserverAddress) + } + + struct Homeserver { + /// The homeserver address as returned by the server. + var address: String + /// The homeserver address as input by the user (it can differ to the well-known request). + var addressFromUser: String? + + /// The preferred login mode for the server + var preferredLoginMode: LoginMode = .unknown + /// Supported types for the login. + var loginModeSupportedTypes = [MXLoginFlow]() + + /// The response returned when querying the homeserver for registration flows. + var registrationFlow: RegistrationResult? + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginModels.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginModels.swift new file mode 100644 index 000000000..7e1e4b900 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginModels.swift @@ -0,0 +1,71 @@ +// +// 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 + +struct LoginFlowResult { + let supportedLoginTypes: [MXLoginFlow] + let ssoIdentityProviders: [SSOIdentityProvider] + let homeserverAddress: String + + var loginMode: LoginMode { + if supportedLoginTypes.contains(where: { $0.type == kMXLoginFlowTypeSSO }), + supportedLoginTypes.contains(where: { $0.type == kMXLoginFlowTypePassword }) { + return .ssoAndPassword(ssoIdentityProviders: ssoIdentityProviders) + } else if supportedLoginTypes.contains(where: { $0.type == kMXLoginFlowTypeSSO }) { + return .sso(ssoIdentityProviders: ssoIdentityProviders) + } else if supportedLoginTypes.contains(where: { $0.type == kMXLoginFlowTypePassword }) { + return .password + } else { + return .unsupported + } + } +} + +enum LoginMode { + case unknown + case password + case sso(ssoIdentityProviders: [SSOIdentityProvider]) + case ssoAndPassword(ssoIdentityProviders: [SSOIdentityProvider]) + case unsupported + + var ssoIdentityProviders: [SSOIdentityProvider]? { + switch self { + case .sso(let ssoIdentityProviders), .ssoAndPassword(let ssoIdentityProviders): + return ssoIdentityProviders + default: + return nil + } + } + + var hasSSO: Bool { + switch self { + case .sso, .ssoAndPassword: + return true + default: + return false + } + } + + var supportsSignModeScreen: Bool { + switch self { + case .password, .ssoAndPassword: + return true + case .unknown, .unsupported, .sso: + return false + } + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginWizard.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginWizard.swift new file mode 100644 index 000000000..48bb7e99f --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginWizard.swift @@ -0,0 +1,31 @@ +// +// 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 LoginWizard { + struct State { + /// For SSO session recovery + var deviceId: String? + var resetPasswordEmail: String? + // var resetPasswordData: ResetPasswordData? + + var clientSecret = UUID().uuidString + var sendAttempt: UInt = 0 + } + + // TODO +} diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationModels.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationModels.swift new file mode 100644 index 000000000..d3ca60c0b --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationModels.swift @@ -0,0 +1,198 @@ +// +// 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 + +/// The parameters used for registration requests. +struct RegistrationParameters: Codable { + /// Authentication parameters + var auth: AuthenticationParameters? + + /// The account username + var username: String? + + /// The account password + var password: String? + + /// Device name + var initialDeviceDisplayName: String? + + /// Temporary flag to notify the server that we support MSISDN flow. Used to prevent old app + /// versions to end up in fallback because the HS returns the MSISDN flow which they don't support + var xShowMSISDN: Bool? + + enum CodingKeys: String, CodingKey { + case auth + case username + case password + case initialDeviceDisplayName = "initial_device_display_name" + case xShowMSISDN = "x_show_msisdn" + } + + /// The parameters as a JSON dictionary for use in MXRestClient. + func dictionary() throws -> [String: Any] { + let jsonData = try JSONEncoder().encode(self) + let object = try JSONSerialization.jsonObject(with: jsonData) + guard let dictionary = object as? [String: Any] else { + MXLog.error("[RegistrationParameters] dictionary: Unexpected type decoded \(type(of: object)). Expected a Dictionary.") + throw AuthenticationError.dictionaryError + } + + return dictionary + } +} + +/// The data passed to the `auth` parameter in authentication requests. +struct AuthenticationParameters: Codable { + /// The type of authentication taking place. The identifier from `MXLoginFlowType`. + let type: String + + /// Note: session can be null for reset password request + var session: String? + + /// parameter for "m.login.recaptcha" type + var captchaResponse: String? + + /// parameter for "m.login.email.identity" type + var threePIDCredentials: ThreePIDCredentials? + + enum CodingKeys: String, CodingKey { + case type + case session + case captchaResponse = "response" + case threePIDCredentials = "threepid_creds" + } + + /// Creates the authentication parameters for a captcha step. + static func captchaParameters(session: String, captchaResponse: String) -> AuthenticationParameters { + AuthenticationParameters(type: kMXLoginFlowTypeRecaptcha, session: session, captchaResponse: captchaResponse) + } + + /// Creates the authentication parameters for a third party ID step using an email address. + static func emailIdentityParameters(session: String, threePIDCredentials: ThreePIDCredentials) -> AuthenticationParameters { + AuthenticationParameters(type: kMXLoginFlowTypeEmailIdentity, session: session, threePIDCredentials: threePIDCredentials) + } + + // Note that there is a bug in Synapse (needs investigation), but if we pass .msisdn, + // the homeserver answer with the login flow with MatrixError fields and not with a simple MatrixError 401. + /// Creates the authentication parameters for a third party ID step using a phone number. + static func msisdnIdentityParameters(session: String, threePIDCredentials: ThreePIDCredentials) -> AuthenticationParameters { + AuthenticationParameters(type: kMXLoginFlowTypeMSISDN, session: session, threePIDCredentials: threePIDCredentials) + } + + /// Creates the authentication parameters for a password reset step. + static func resetPasswordParameters(clientSecret: String, sessionID: String) -> AuthenticationParameters { + AuthenticationParameters(type: kMXLoginFlowTypeEmailIdentity, + session: nil, + threePIDCredentials: ThreePIDCredentials(clientSecret: clientSecret, sessionID: sessionID)) + } +} + +/// The result from a response of a registration flow step. +enum RegistrationResult { + /// Registration has completed, creating an `MXSession` for the account. + case success(MXSession) + /// The request was successful but there are pending steps to complete. + case flowResponse(FlowResult) +} + +/// The state of an authentication flow after a step has been completed. +struct FlowResult { + /// The stages in the flow that are yet to be completed. + let missingStages: [Stage] + /// The stages in the flow that have been completed. + let completedStages: [Stage] + + /// A stage in the authentication flow. + enum Stage { + /// The stage with the type `m.login.recaptcha`. + case reCaptcha(mandatory: Bool, publicKey: String) + + /// The stage with the type `m.login.email.identity`. + case email(mandatory: Bool) + + /// The stage with the type `m.login.msisdn`. + case msisdn(mandatory: Bool) + + /// The stage with the type `m.login.dummy`. + /// + /// This stage can be mandatory if there is no other stages. In this case the account cannot + /// be created by just sending a username and a password, the dummy stage has to be completed. + case dummy(mandatory: Bool) + + /// The stage with the type `m.login.terms`. + case terms(mandatory: Bool, policies: [String: String]) + + /// A stage of an unknown type. + case other(mandatory: Bool, type: String, params: [AnyHashable: Any]) + + /// Whether the stage is a dummy stage that is also mandatory. + var isDummyAndMandatory: Bool { + guard case let .dummy(isMandatory) = self else { return false } + return isMandatory + } + } +} + +extension MXAuthenticationSession { + /// The flows from the session mapped as a `FlowResult` value. + var flowResult: FlowResult { + let allFlowTypes = Set(flows.flatMap { $0.stages ?? [] }) + var missingStages = [FlowResult.Stage]() + var completedStages = [FlowResult.Stage]() + + allFlowTypes.forEach { flow in + let isMandatory = flows.allSatisfy { $0.stages.contains(flow) } + + let stage: FlowResult.Stage + switch flow { + case kMXLoginFlowTypeRecaptcha: + let parameters = params[flow] as? [AnyHashable: Any] + let publicKey = parameters?["public_key"] as? String + stage = .reCaptcha(mandatory: isMandatory, publicKey: publicKey ?? "") + case kMXLoginFlowTypeDummy: + stage = .dummy(mandatory: isMandatory) + case kMXLoginFlowTypeTerms: + let parameters = params[flow] as? [String: String] + stage = .terms(mandatory: isMandatory, policies: parameters ?? [:]) + case kMXLoginFlowTypeMSISDN: + stage = .msisdn(mandatory: isMandatory) + case kMXLoginFlowTypeEmailIdentity: + stage = .email(mandatory: isMandatory) + default: + let parameters = params[flow] as? [AnyHashable: Any] + stage = .other(mandatory: isMandatory, type: flow, params: parameters ?? [:]) + } + + if let completed = completed, completed.contains(flow) { + completedStages.append(stage) + } else { + missingStages.append(stage) + } + } + + return FlowResult(missingStages: missingStages, completedStages: completedStages) + } + + /// Determines the next stage to be completed in the flow. + func nextUncompletedStage(flowIndex: Int = 0) -> String? { + guard flows.count < flowIndex else { return nil } + return flows[flowIndex].stages.first { + guard let completed = completed else { return false } + return !completed.contains($0) + } + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationWizard.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationWizard.swift new file mode 100644 index 000000000..1315f2101 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationWizard.swift @@ -0,0 +1,275 @@ +// +// 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 + +@available(iOS 14.0, *) +/// Set of methods to be able to create an account on a homeserver. +/// +/// Common scenario to register an account successfully: +/// - Call `registrationFlow` to check that you application supports all the mandatory registration stages +/// - Call `createAccount` to start the account creation +/// - Fulfil all mandatory stages using the methods `performReCaptcha` `acceptTerms` `dummy`, etc. +/// +/// More documentation can be found in the file https://github.com/vector-im/element-android/blob/main/docs/signup.md +/// and https://matrix.org/docs/spec/client_server/latest#account-registration-and-management +class RegistrationWizard { + struct State { + var currentSession: String? + var isRegistrationStarted = false + var currentThreePIDData: ThreePIDData? + + var clientSecret = UUID().uuidString + var sendAttempt: UInt = 0 + } + + let client: MXRestClient + let sessionCreator: SessionCreator + + private(set) var state: State + + /// This is the current ThreePID, waiting for validation. The SDK will store it in database, so it can be + /// restored even if the app has been killed during the registration + var currentThreePID: String? { + guard let threePid = state.currentThreePIDData?.threePID else { return nil } + + switch threePid { + case .email(let string): + return string + case .msisdn(let msisdn, _): + return state.currentThreePIDData?.registrationResponse.formattedMSISDN ?? msisdn + } + } + + /// True when login and password have been sent with success to the homeserver, + /// i.e. `createAccount` has been called successfully. + var isRegistrationStarted: Bool { + state.isRegistrationStarted + } + + init(client: MXRestClient, sessionCreator: SessionCreator = SessionCreator()) { + self.client = client + self.sessionCreator = sessionCreator + + self.state = State() + } + + /// Call this method to get the possible registration flow of the current homeserver. + /// It can be useful to ensure that your application implementation supports all the stages + /// required to create an account. If it is not the case, you will have to use the web fallback + /// to let the user create an account with your application. + /// See `AuthenticationService.getFallbackUrl` + func registrationFlow() async throws -> RegistrationResult { + let parameters = RegistrationParameters() + + do { + let result = try await performRegistrationRequest(parameters: parameters) + return result + } catch { + // Map M_FORBIDDEN into a registration error. + guard let mxError = MXError(nsError: error), mxError.errcode == kMXErrCodeStringForbidden else { throw error } + MXLog.warning("[RegistrationWizard] Registration is disabled for the selected server.") + throw RegistrationError.registrationDisabled + } + } + + /// Can be call to check is the desired username is available for registration on the current homeserver. + /// It may also fails if the desired username is not correctly formatted or does not follow any restriction on + /// the homeserver. Ex: username with only digits may be rejected. + /// - Parameter username the desired username. Ex: "alice" + func registrationAvailable(username: String) async throws -> Bool { + try await client.isUsernameAvailable(username) + } + + /// This is the first method to call in order to create an account and start the registration process. + /// + /// - Parameter username the desired username. Ex: "alice" + /// - Parameter password the desired password + /// - Parameter initialDeviceDisplayName the device display name + func createAccount(username: String?, + password: String?, + initialDeviceDisplayName: String?) async throws -> RegistrationResult { + let parameters = RegistrationParameters(username: username, password: password, initialDeviceDisplayName: initialDeviceDisplayName) + let result = try await performRegistrationRequest(parameters: parameters, isCreatingAccount: true) + state.isRegistrationStarted = true + return result + } + + /// Perform the "m.login.recaptcha" stage. + /// + /// - Parameter response: The response from ReCaptcha + func performReCaptcha(response: String) async throws -> RegistrationResult { + guard let session = state.currentSession else { + MXLog.error("[RegistrationWizard] performReCaptcha: Missing authentication session, createAccount hasn't been called.") + throw RegistrationError.createAccountNotCalled + } + + let parameters = RegistrationParameters(auth: AuthenticationParameters.captchaParameters(session: session, captchaResponse: response)) + return try await performRegistrationRequest(parameters: parameters) + } + + /// Perform the "m.login.terms" stage. + func acceptTerms() async throws -> RegistrationResult { + guard let session = state.currentSession else { + MXLog.error("[RegistrationWizard] acceptTerms: Missing authentication session, createAccount hasn't been called.") + throw RegistrationError.createAccountNotCalled + } + + let parameters = RegistrationParameters(auth: AuthenticationParameters(type: kMXLoginFlowTypeTerms, session: session)) + return try await performRegistrationRequest(parameters: parameters) + } + + /// Perform the "m.login.dummy" stage. + func dummy() async throws -> RegistrationResult { + guard let session = state.currentSession else { + MXLog.error("[RegistrationWizard] dummy: Missing authentication session, createAccount hasn't been called.") + throw RegistrationError.createAccountNotCalled + } + + let parameters = RegistrationParameters(auth: AuthenticationParameters(type: kMXLoginFlowTypeDummy, session: session)) + return try await performRegistrationRequest(parameters: parameters) + } + + /// Perform the "m.login.email.identity" or "m.login.msisdn" stage. + /// + /// - Parameter threePID the threePID to add to the account. If this is an email, the homeserver will send an email + /// to validate it. For a msisdn a SMS will be sent. + func addThreePID(threePID: RegisterThreePID) async throws -> RegistrationResult { + state.currentThreePIDData = nil + return try await sendThreePID(threePID: threePID) + } + + /// Ask the homeserver to send again the current threePID (email or msisdn). + func sendAgainThreePID() async throws -> RegistrationResult { + guard let threePID = state.currentThreePIDData?.threePID else { + MXLog.error("[RegistrationWizard] sendAgainThreePID: Missing authentication session, createAccount hasn't been called.") + throw RegistrationError.createAccountNotCalled + } + return try await sendThreePID(threePID: threePID) + } + + /// Send the code received by SMS to validate a msisdn. + /// If the code is correct, the registration request will be executed to validate the msisdn. + func handleValidateThreePID(code: String) async throws -> RegistrationResult { + return try await validateThreePid(code: code) + } + + /// Useful to poll the homeserver when waiting for the email to be validated by the user. + /// Once the email is validated, this method will return successfully. + /// - Parameter delay How long to wait before sending the request. + func checkIfEmailHasBeenValidated(delay: TimeInterval) async throws -> RegistrationResult { + MXLog.failure("The delay on this method is no longer available. Move this to the object handling the polling.") + guard let parameters = state.currentThreePIDData?.registrationParameters else { + MXLog.error("[RegistrationWizard] checkIfEmailHasBeenValidated: The current 3pid data hasn't been stored in the state.") + throw RegistrationError.missingThreePIDData + } + + return try await performRegistrationRequest(parameters: parameters) + } + + // MARK: - Private + + private func validateThreePid(code: String) async throws -> RegistrationResult { + guard let threePIDData = state.currentThreePIDData else { + MXLog.error("[RegistrationWizard] validateThreePid: There is no third party ID data stored in the state.") + throw RegistrationError.missingThreePIDData + } + + guard let submitURL = threePIDData.registrationResponse.submitURL else { + MXLog.error("[RegistrationWizard] validateThreePid: The third party ID data doesn't contain a submitURL.") + throw RegistrationError.missingThreePIDURL + } + + + let validationBody = ThreePIDValidationCodeBody(clientSecret: state.clientSecret, + sessionID: threePIDData.registrationResponse.sessionID, + code: code) + + #warning("Seems odd to pass a nil baseURL and then the url as the path, yet this is how MXK3PID works") + guard let httpClient = MXHTTPClient(baseURL: nil, andOnUnrecognizedCertificateBlock: nil) else { + MXLog.error("[RegistrationWizard] validateThreePid: Failed to create an MXHTTPClient.") + throw RegistrationError.threePIDClientFailure + } + guard try await httpClient.validateThreePIDCode(submitURL: submitURL, validationBody: validationBody) else { + MXLog.error("[RegistrationWizard] validateThreePid: Third party ID validation failed.") + throw RegistrationError.threePIDValidationFailure + } + + let parameters = threePIDData.registrationParameters + MXLog.failure("This method used to add a 3-second delay to the request. This should be moved to the caller of `handleValidateThreePID`.") + return try await performRegistrationRequest(parameters: parameters) + } + + private func sendThreePID(threePID: RegisterThreePID) async throws -> RegistrationResult { + guard let session = state.currentSession else { + MXLog.error("[RegistrationWizard] sendThreePID: Missing authentication session, createAccount hasn't been called.") + throw RegistrationError.createAccountNotCalled + } + + let response = try await client.requestTokenDuringRegistration(for: threePID, + clientSecret: state.clientSecret, + sendAttempt: state.sendAttempt) + + state.sendAttempt += 1 + + let threePIDCredentials = ThreePIDCredentials(clientSecret: state.clientSecret, sessionID: response.sessionID) + let authenticationParameters: AuthenticationParameters + switch threePID { + case .email: + authenticationParameters = AuthenticationParameters.emailIdentityParameters(session: session, threePIDCredentials: threePIDCredentials) + case .msisdn: + authenticationParameters = AuthenticationParameters.msisdnIdentityParameters(session: session, threePIDCredentials: threePIDCredentials) + } + + let parameters = RegistrationParameters(auth: authenticationParameters) + + state.currentThreePIDData = ThreePIDData(threePID: threePID, registrationResponse: response, registrationParameters: parameters) + + // Send the session id for the first time + return try await performRegistrationRequest(parameters: parameters) + } + + private func performRegistrationRequest(parameters: RegistrationParameters, isCreatingAccount: Bool = false) async throws -> RegistrationResult { + do { + let response = try await client.register(parameters: parameters) + let credentials = MXCredentials(loginResponse: response, andDefaultCredentials: client.credentials) + return .success(sessionCreator.createSession(credentials: credentials, client: client)) + } catch { + let nsError = error as NSError + + guard + let jsonResponse = nsError.userInfo[MXHTTPClientErrorResponseDataKey] as? [String: Any], + let authenticationSession = MXAuthenticationSession(fromJSON: jsonResponse) + else { throw error } + + state.currentSession = authenticationSession.session + let flowResult = authenticationSession.flowResult + + if isCreatingAccount || isRegistrationStarted { + return try await handleMandatoryDummyStage(flowResult: flowResult) + } + + return .flowResponse(flowResult) + } + } + + /// Checks for a mandatory dummy stage and handles it automatically when possible. + private func handleMandatoryDummyStage(flowResult: FlowResult) async throws -> RegistrationResult { + // If the dummy stage is mandatory, do the dummy stage now + guard flowResult.missingStages.contains(where: { $0.isDummyAndMandatory }) else { return .flowResponse(flowResult) } + return try await dummy() + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/SessionCreator.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/SessionCreator.swift new file mode 100644 index 000000000..daa468c60 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/SessionCreator.swift @@ -0,0 +1,38 @@ +// +// 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 + +/// A WIP class that has common functionality to create a new session. +class SessionCreator { + /// Creates an `MXSession` using the supplied credentials and REST client. + func createSession(credentials: MXCredentials, client: MXRestClient) -> MXSession { + // Report the new account in account manager + if credentials.identityServer == nil { + #warning("Check that the client is actually updated with this info?") + credentials.identityServer = client.identityServer + } + + let account = MXKAccount(credentials: credentials) + + if let identityServer = credentials.identityServer { + account.identityServerURL = identityServer + } + + MXKAccountManager.shared().addAccount(account, andOpenSession: true) + return account.mxSession + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/ThreePIDModels.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/ThreePIDModels.swift new file mode 100644 index 000000000..57682a88f --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/ThreePIDModels.swift @@ -0,0 +1,89 @@ +// +// 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 + +enum RegisterThreePID { + case email(String) + case msisdn(msisdn: String, countryCode: String) +} + +struct ThreePIDCredentials: Codable { + var clientSecret: String? + + var identityServer: String? + + var sessionID: String? + + enum CodingKeys: String, CodingKey { + case clientSecret = "client_secret" + case identityServer = "id_server" + case sessionID = "sid" + } +} + +struct ThreePIDData { + let threePID: RegisterThreePID + let registrationResponse: RegistrationThreePIDTokenResponse + let registrationParameters: RegistrationParameters +} + +// TODO: This could potentially become an MXJSONModel? +struct RegistrationThreePIDTokenResponse { + /// Required. The session ID. Session IDs are opaque strings that must consist entirely of the characters [0-9a-zA-Z.=_-]. + /// Their length must not exceed 255 characters and they must not be empty. + let sessionID: String + + /// An optional field containing a URL where the client must submit the validation token to, with identical parameters to the Identity + /// Service API's POST /validate/email/submitToken endpoint. The homeserver must send this token to the user (if applicable), + /// who should then be prompted to provide it to the client. + /// + /// If this field is not present, the client can assume that verification will happen without the client's involvement provided + /// the homeserver advertises this specification version in the /versions response (ie: r0.5.0). + var submitURL: String? = nil + + // MARK: - Additional data that may be needed + + var msisdn: String? = nil + var formattedMSISDN: String? = nil + var success: Bool? = nil + + enum CodingKeys: String, CodingKey { + case sessionID = "sid" + case submitURL = "submit_url" + case msisdn + case formattedMSISDN = "intl_fmt" + case success + } +} + +struct ThreePIDValidationCodeBody: Codable { + let clientSecret: String + + let sessionID: String + + let code: String + + enum CodingKeys: String, CodingKey { + case clientSecret = "client_secret" + case sessionID = "sid" + case code = "token" + } + + func jsonData() throws -> Data { + try JSONEncoder().encode(self) + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationModels.swift b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationModels.swift new file mode 100644 index 000000000..da72ac4eb --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationModels.swift @@ -0,0 +1,123 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// MARK: View model + +enum AuthenticationRegistrationViewModelResult { + /// The user would like to select another server. + case selectServer + /// Validate the supplied username with the homeserver. + case validateUsername(String) + /// Create an account using the supplied credentials. + case createAccount(username: String, password: String) +} + +// MARK: View + +struct AuthenticationRegistrationViewState: BindableState { + /// The address of the homeserver. + var homeserverAddress: String + /// Whether or not to show the username and password text fields with the next button + var showRegistrationForm: Bool + /// An array containing the available SSO options for login. + var ssoIdentityProviders: [SSOIdentityProvider] + /// View state that can be bound to from SwiftUI. + var bindings: AuthenticationRegistrationBindings + /// Whether or not the username field has been edited yet. + /// + /// This is used to delay showing an error state until the user has tried 1 username. + var hasEditedUsername = false + /// Whether or not the password field has been edited yet. + /// + /// This is used to delay showing an error state until the user has tried 1 password. + var hasEditedPassword = false + + /// An error message to be shown in the username text field footer. + var usernameErrorMessage: String? + + /// The message to show in the username text field footer. + var usernameFooterMessage: String { + usernameErrorMessage ?? VectorL10n.authenticationRegistrationUsernameFooter + } + + /// A description that can be shown for the currently selected homeserver. + var serverDescription: String? { + guard homeserverAddress == "matrix.org" else { return nil } + return VectorL10n.authenticationRegistrationMatrixDescription + } + + /// Whether to show any SSO buttons. + var showSSOButtons: Bool { + !ssoIdentityProviders.isEmpty + } + + /// Whether the current `username` is valid. + var isUsernameValid: Bool { + !bindings.username.isEmpty && usernameErrorMessage == nil + } + + /// Whether the current `password` is valid. + var isPasswordValid: Bool { + bindings.password.count >= 8 + } + + /// `true` if it is possible to continue, otherwise `false`. + var hasValidCredentials: Bool { + isUsernameValid && isPasswordValid + } +} + +struct AuthenticationRegistrationBindings { + /// The username input by the user. + var username = "" + /// The password input by the user. + var password = "" + /// Information describing the currently displayed alert. + var alertInfo: AlertInfo? +} + +enum AuthenticationRegistrationViewAction { + /// The user would like to select another server. + case selectServer + /// Validate the supplied username with the homeserver. + case validateUsername + /// Allows password validation to take place (sent after editing the password for the first time). + case enablePasswordValidation + /// Clear any errors being shown in the username text field footer. + case clearUsernameError + /// Continue using the input username and password. + case next + /// Login using the supplied SSO provider ID. + case continueWithSSO(id: String) +} + +enum AuthenticationRegistrationErrorType: Hashable { + /// An error to be shown in the username text field footer. + case usernameUnavailable(String) + + /// An error response from the homeserver. + case mxError(String) + /// The current homeserver address isn't valid. + case invalidHomeserver + /// The response from the homeserver was unexpected. + case invalidResponse + /// The homeserver doesn't support registration. + case registrationDisabled + /// An unknown error occurred. + case unknown +} diff --git a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModel.swift b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModel.swift new file mode 100644 index 000000000..9f30db8a0 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModel.swift @@ -0,0 +1,98 @@ +// +// 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 +import Combine + +@available(iOS 14, *) +typealias AuthenticationRegistrationViewModelType = StateStoreViewModel + + +@available(iOS 14, *) +class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelType, AuthenticationRegistrationViewModelProtocol { + + // MARK: - Properties + + // MARK: Public + + @MainActor var completion: ((AuthenticationRegistrationViewModelResult) -> Void)? + + // MARK: - Setup + + init(homeserverAddress: String, showRegistrationForm: Bool = true, ssoIdentityProviders: [SSOIdentityProvider]) { + let bindings = AuthenticationRegistrationBindings() + let viewState = AuthenticationRegistrationViewState(homeserverAddress: HomeserverAddress.displayable(homeserverAddress), + showRegistrationForm: showRegistrationForm, + ssoIdentityProviders: ssoIdentityProviders, + bindings: bindings) + + super.init(initialViewState: viewState) + } + + // MARK: - Public + + override func process(viewAction: AuthenticationRegistrationViewAction) { + Task { + await MainActor.run { + switch viewAction { + case .selectServer: + completion?(.selectServer) + case .validateUsername: + state.hasEditedUsername = true + completion?(.validateUsername(state.bindings.username)) + case .enablePasswordValidation: + state.hasEditedPassword = true + case .clearUsernameError: + guard state.usernameErrorMessage != nil else { return } + state.usernameErrorMessage = nil + case .next: + completion?(.createAccount(username: state.bindings.username, password: state.bindings.password)) + case .continueWithSSO(let id): + break + } + } + } + } + + @MainActor func update(homeserverAddress: String, showRegistrationForm: Bool, ssoIdentityProviders: [SSOIdentityProvider]) { + state.homeserverAddress = HomeserverAddress.displayable(homeserverAddress) + state.showRegistrationForm = showRegistrationForm + state.ssoIdentityProviders = ssoIdentityProviders + } + + @MainActor func displayError(_ type: AuthenticationRegistrationErrorType) { + switch type { + case .usernameUnavailable(let message): + state.usernameErrorMessage = message + case .mxError(let message): + state.bindings.alertInfo = AlertInfo(id: type, + title: VectorL10n.error, + message: message) + case .invalidHomeserver, .invalidResponse: + state.bindings.alertInfo = AlertInfo(id: type, + title: VectorL10n.error, + message: VectorL10n.authenticationServerSelectionGenericError) + case .registrationDisabled: + state.bindings.alertInfo = AlertInfo(id: type, + title: VectorL10n.error, + message: VectorL10n.loginErrorRegistrationIsNotSupported) + case .unknown: + state.bindings.alertInfo = AlertInfo(id: type) + } + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModelProtocol.swift new file mode 100644 index 000000000..e63a5c601 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModelProtocol.swift @@ -0,0 +1,34 @@ +// +// 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 AuthenticationRegistrationViewModelProtocol { + + @MainActor var completion: ((AuthenticationRegistrationViewModelResult) -> Void)? { get set } + @available(iOS 14, *) + var context: AuthenticationRegistrationViewModelType.Context { get } + + /// Update the view with new homeserver information. + /// - Parameters: + /// - homeserverAddress: The homeserver string to be shown to the user. + /// - showRegistrationForm: Whether or not to display the username and password text fields. + /// - ssoIdentityProviders: The supported SSO login options. + @MainActor func update(homeserverAddress: String, showRegistrationForm: Bool, ssoIdentityProviders: [SSOIdentityProvider]) + + /// Display an error to the user. + @MainActor func displayError(_ type: AuthenticationRegistrationErrorType) +} diff --git a/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift b/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift new file mode 100644 index 000000000..79dc03de5 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift @@ -0,0 +1,246 @@ +// +// 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 +import CommonKit +import MatrixSDK + +@available(iOS 14.0, *) +struct AuthenticationRegistrationCoordinatorParameters { + let navigationRouter: NavigationRouterType + let authenticationService: AuthenticationService + /// The registration flow that is available for the chosen server. + let registrationFlow: RegistrationResult? + /// The login mode to allow SSO buttons to be shown when available. + let loginMode: LoginMode +} + +enum AuthenticationRegistrationCoordinatorResult { + /// The user would like to select another server. + case selectServer + /// The screen completed with the associated registration result. + case completed(RegistrationResult) +} + +@available(iOS 14.0, *) +final class AuthenticationRegistrationCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: AuthenticationRegistrationCoordinatorParameters + private let authenticationRegistrationHostingController: VectorHostingController + private var authenticationRegistrationViewModel: AuthenticationRegistrationViewModelProtocol + + private var currentTask: Task? { + willSet { + currentTask?.cancel() + } + } + + private var navigationRouter: NavigationRouterType { parameters.navigationRouter } + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var waitingIndicator: UserIndicator? + + /// The authentication service used for the registration. + var authenticationService: AuthenticationService { parameters.authenticationService } + /// The wizard used to handle the registration flow. May be `nil` when only SSO is supported. + var registrationWizard: RegistrationWizard? + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + @MainActor var completion: ((AuthenticationRegistrationCoordinatorResult) -> Void)? + + // MARK: - Setup + + @MainActor init(parameters: AuthenticationRegistrationCoordinatorParameters) { + self.parameters = parameters + self.registrationWizard = parameters.authenticationService.registrationWizard + + let homeserver = parameters.authenticationService.state.homeserver + let viewModel = AuthenticationRegistrationViewModel(homeserverAddress: homeserver.addressFromUser ?? homeserver.address, + showRegistrationForm: homeserver.registrationFlow != nil, + ssoIdentityProviders: parameters.loginMode.ssoIdentityProviders ?? []) + authenticationRegistrationViewModel = viewModel + + let view = AuthenticationRegistrationScreen(viewModel: viewModel.context) + authenticationRegistrationHostingController = VectorHostingController(rootView: view) + authenticationRegistrationHostingController.vc_removeBackTitle() + authenticationRegistrationHostingController.enableNavigationBarScrollEdgeAppearance = true + + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: authenticationRegistrationHostingController) + } + + // MARK: - Public + func start() { + Task { + await MainActor.run { + MXLog.debug("[AuthenticationRegistrationCoordinator] did start.") + authenticationRegistrationViewModel.completion = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[AuthenticationRegistrationCoordinator] AuthenticationRegistrationViewModel did complete with result: \(result).") + switch result { + case .selectServer: + self.presentServerSelectionScreen() + case.validateUsername(let username): + self.validateUsername(username) + case .createAccount(let username, let password): + self.createAccount(username: username, password: password) + } + } + } + } + } + + func toPresentable() -> UIViewController { + return self.authenticationRegistrationHostingController + } + + // MARK: - Private + + /// Show a blocking activity indicator whilst saving. + @MainActor private func startLoading(label: String? = nil) { + waitingIndicator = indicatorPresenter.present(.loading(label: label ?? VectorL10n.loading, isInteractionBlocking: true)) + } + + /// Hide the currently displayed activity indicator. + @MainActor private func stopLoading() { + waitingIndicator = nil + } + + /// Asks the homeserver to check the supplied username's format and availability. + @MainActor private func validateUsername(_ username: String) { + guard let registrationWizard = registrationWizard else { + MXLog.failure("[AuthenticationRegistrationCoordinator] The registration wizard was requested before getting the login flow.") + return + } + + currentTask = Task { + do { + _ = try await registrationWizard.registrationAvailable(username: username) + } catch { + guard !Task.isCancelled, let mxError = MXError(nsError: error as NSError) else { return } + if mxError.errcode == kMXErrCodeStringUserInUse + || mxError.errcode == kMXErrCodeStringInvalidUsername + || mxError.errcode == kMXErrCodeStringExclusiveResource { + authenticationRegistrationViewModel.displayError(.usernameUnavailable(mxError.error)) + } + } + } + } + + /// Creates an account on the homeserver with the supplied username and password. + @MainActor private func createAccount(username: String, password: String) { + guard let registrationWizard = registrationWizard else { + MXLog.failure("[AuthenticationRegistrationCoordinator] createAccount: The registration wizard is nil.") + return + } + + // reAuthHelper.data = state.password + let deviceDisplayName = UIDevice.current.isPhone ? VectorL10n.loginMobileDevice : VectorL10n.loginTabletDevice + + startLoading() + + currentTask = Task { [weak self] in + do { + let result = try await registrationWizard.createAccount(username: username, password: password, initialDeviceDisplayName: deviceDisplayName) + + guard !Task.isCancelled else { return } + completion?(.completed(result)) + + self?.stopLoading() + } catch { + self?.stopLoading() + self?.handleError(error) + } + } + } + + /// Processes an error to either update the flow or display it to the user. + @MainActor private func handleError(_ error: Error) { + if let mxError = MXError(nsError: error as NSError) { + authenticationRegistrationViewModel.displayError(.mxError(mxError.error)) + return + } + + if let authenticationError = error as? AuthenticationError { + switch authenticationError { + case .invalidHomeserver: + authenticationRegistrationViewModel.displayError(.invalidHomeserver) + case .dictionaryError: + authenticationRegistrationViewModel.displayError(.unknown) + case .loginFlowNotCalled: + #warning("Reset the flow") + case .missingMXRestClient: + #warning("Forget the soft logout session") + } + return + } + + if let registrationError = error as? RegistrationError { + switch registrationError { + case .registrationDisabled: + authenticationRegistrationViewModel.displayError(.registrationDisabled) + case .createAccountNotCalled, .missingThreePIDData, .missingThreePIDURL, .threePIDClientFailure, .threePIDValidationFailure: + // Shouldn't happen at this stage + authenticationRegistrationViewModel.displayError(.unknown) + } + return + } + + authenticationRegistrationViewModel.displayError(.unknown) + } + + /// Presents the server selection screen as a modal. + @MainActor private func presentServerSelectionScreen() { + MXLog.debug("[AuthenticationCoordinator] showServerSelectionScreen") + let parameters = AuthenticationServerSelectionCoordinatorParameters(authenticationService: authenticationService, + hasModalPresentation: true) + let coordinator = AuthenticationServerSelectionCoordinator(parameters: parameters) + coordinator.completion = { [weak self, weak coordinator] result in + guard let self = self, let coordinator = coordinator else { return } + self.serverSelectionCoordinator(coordinator, didCompleteWith: result) + } + + coordinator.start() + add(childCoordinator: coordinator) + + let modalRouter = NavigationRouter() + modalRouter.setRootModule(coordinator) + + navigationRouter.present(modalRouter, animated: true) + } + + /// Handles the result from the server selection modal, dismissing it after updating the view. + @MainActor private func serverSelectionCoordinator(_ coordinator: AuthenticationServerSelectionCoordinator, + didCompleteWith result: AuthenticationServerSelectionCoordinatorResult) { + if result == .updated { + let homeserver = authenticationService.state.homeserver + authenticationRegistrationViewModel.update(homeserverAddress: homeserver.addressFromUser ?? homeserver.address, + showRegistrationForm: homeserver.registrationFlow != nil, + ssoIdentityProviders: homeserver.preferredLoginMode.ssoIdentityProviders ?? []) + + self.registrationWizard = authenticationService.registrationWizard + } + + navigationRouter.dismissModule(animated: true) { [weak self] in + self?.remove(childCoordinator: coordinator) + } + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Registration/MockAuthenticationRegistrationScreenState.swift b/RiotSwiftUI/Modules/Authentication/Registration/MockAuthenticationRegistrationScreenState.swift new file mode 100644 index 000000000..cc420c7b0 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Registration/MockAuthenticationRegistrationScreenState.swift @@ -0,0 +1,77 @@ +// +// 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. +@available(iOS 14.0, *) +enum MockAuthenticationRegistrationScreenState: MockScreenState, CaseIterable { + // A case for each state you want to represent + // with specific, minimal associated data that will allow you + // mock that screen. + case matrixDotOrg + case passwordOnly + case passwordWithCredentials + case passwordWithUsernameError + case ssoOnly + + /// The associated screen + var screenType: Any.Type { + AuthenticationRegistrationScreen.self + } + + /// Generate the view struct for the screen state. + var screenView: ([Any], AnyView) { + let viewModel: AuthenticationRegistrationViewModel + switch self { + case .matrixDotOrg: + viewModel = AuthenticationRegistrationViewModel(homeserverAddress: "https://matrix.org", ssoIdentityProviders: [ + SSOIdentityProvider(id: "1", name: "Apple", brand: "apple", iconURL: nil), + SSOIdentityProvider(id: "2", name: "Facebook", brand: "facebook", iconURL: nil), + SSOIdentityProvider(id: "3", name: "GitHub", brand: "github", iconURL: nil), + SSOIdentityProvider(id: "4", name: "GitLab", brand: "gitlab", iconURL: nil), + SSOIdentityProvider(id: "5", name: "Google", brand: "google", iconURL: nil) + ]) + case .passwordOnly: + viewModel = AuthenticationRegistrationViewModel(homeserverAddress: "https://example.com", ssoIdentityProviders: []) + case .passwordWithCredentials: + viewModel = AuthenticationRegistrationViewModel(homeserverAddress: "https://example.com", ssoIdentityProviders: []) + viewModel.context.username = "alice" + viewModel.context.password = "password" + case .passwordWithUsernameError: + viewModel = AuthenticationRegistrationViewModel(homeserverAddress: "https://example.com", ssoIdentityProviders: []) + viewModel.state.hasEditedUsername = true + Task { + await MainActor.run { + viewModel.displayError(.usernameUnavailable(VectorL10n.authInvalidUserName)) + } + } + case .ssoOnly: + viewModel = AuthenticationRegistrationViewModel(homeserverAddress: "https://company.com", + showRegistrationForm: false, + ssoIdentityProviders: [SSOIdentityProvider(id: "test", name: "SAML", brand: nil, iconURL: nil)]) + } + + + // can simulate service and viewModel actions here if needs be. + + return ( + [viewModel], AnyView(AuthenticationRegistrationScreen(viewModel: viewModel.context)) + ) + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Registration/Test/UI/AuthenticationRegistrationUITests.swift b/RiotSwiftUI/Modules/Authentication/Registration/Test/UI/AuthenticationRegistrationUITests.swift new file mode 100644 index 000000000..ce6177ef9 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Registration/Test/UI/AuthenticationRegistrationUITests.swift @@ -0,0 +1,144 @@ +// +// 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 XCTest +import RiotSwiftUI + +@available(iOS 14.0, *) +class AuthenticationRegistrationUITests: MockScreenTest { + + override class var screenType: MockScreenState.Type { + return MockAuthenticationRegistrationScreenState.self + } + + override class func createTest() -> MockScreenTest { + return AuthenticationRegistrationUITests(selector: #selector(verifyAuthenticationRegistrationScreen)) + } + + func verifyAuthenticationRegistrationScreen() throws { + guard let screenState = screenState as? MockAuthenticationRegistrationScreenState else { fatalError("no screen") } + switch screenState { + case .matrixDotOrg: + let state = "matrix.org" + validateRegistrationFormIsVisible(for: state) + validateSSOButtonsAreShown(for: state) + + validateNoErrorsAreShown(for: state) + case .passwordOnly: + let state = "a password only server" + validateRegistrationFormIsVisible(for: state) + validateSSOButtonsAreHidden(for: state) + + validateNextButtonIsDisabled(for: state) + + validateNoErrorsAreShown(for: state) + case .passwordWithCredentials: + let state = "a password only server with credentials entered" + validateRegistrationFormIsVisible(for: state) + validateSSOButtonsAreHidden(for: state) + + validateNextButtonIsEnabled(for: state) + + validateNoErrorsAreShown(for: state) + case .passwordWithUsernameError: + let state = "a password only server with an invalid username" + validateRegistrationFormIsVisible(for: state) + validateSSOButtonsAreHidden(for: state) + + validateNextButtonIsDisabled(for: state) + + validateUsernameError(for: state) + case .ssoOnly: + let state = "an SSO only server" + validateRegistrationFormIsHidden(for: state) + validateSSOButtonsAreShown(for: state) + } + } + + /// Checks that the username and password text fields are shown along with the next button. + func validateRegistrationFormIsVisible(for state: String) { + let usernameTextField = app.textFields.element + let passwordTextField = app.secureTextFields.element + let nextButton = app.buttons["nextButton"] + + XCTAssertTrue(usernameTextField.exists, "Username input should be shown for \(state).") + XCTAssertTrue(passwordTextField.exists, "Password input should be shown for \(state).") + XCTAssertTrue(nextButton.exists, "The next button should be shown for \(state).") + } + + /// Checks that the username and password text fields are hidden along with the next button. + func validateRegistrationFormIsHidden(for state: String) { + let usernameTextField = app.textFields.element + let passwordTextField = app.secureTextFields.element + let nextButton = app.buttons["nextButton"] + + XCTAssertFalse(usernameTextField.exists, "Username input should not be shown for \(state).") + XCTAssertFalse(passwordTextField.exists, "Password input should not be shown for \(state).") + XCTAssertFalse(nextButton.exists, "The next button should not be shown for \(state).") + } + + /// Checks that there is at least one SSO button shown on the screen. + func validateSSOButtonsAreShown(for state: String) { + let ssoButtons = app.buttons.matching(identifier: "ssoButton") + XCTAssertGreaterThan(ssoButtons.count, 0, "There should be at least 1 SSO button shown for \(state).") + } + + /// Checks that no SSO buttons shown on the screen. + func validateSSOButtonsAreHidden(for state: String) { + let ssoButtons = app.buttons.matching(identifier: "ssoButton") + XCTAssertEqual(ssoButtons.count, 0, "There should not be any SSO buttons shown for \(state).") + } + + /// Checks that the next button is shown but is disabled. + func validateNextButtonIsDisabled(for state: String) { + let nextButton = app.buttons["nextButton"] + XCTAssertTrue(nextButton.exists, "The next button should be shown.") + XCTAssertFalse(nextButton.isEnabled, "The next button should be disabled for \(state).") + } + + /// Checks that the next button is shown and is enabled. + func validateNextButtonIsEnabled(for state: String) { + let nextButton = app.buttons["nextButton"] + XCTAssertTrue(nextButton.exists, "The next button should be shown.") + XCTAssertTrue(nextButton.isEnabled, "The next button should be enabled for \(state).") + } + + /// Checks that the username text field footer is showing an error. + func validateUsernameError(for state: String) { + let usernameFooter = textFieldFooter(for: "usernameTextField") + XCTAssertTrue(usernameFooter.exists, "The username footer should be shown for \(state).") + XCTAssertEqual(usernameFooter.label, VectorL10n.authInvalidUserName, "The username footer should be showing an error for \(state).") + } + + /// Checks that neither the username or password text field footers are showing an error. + func validateNoErrorsAreShown(for state: String) { + let usernameFooter = textFieldFooter(for: "usernameTextField") + let passwordFooter = textFieldFooter(for: "passwordTextField") + + XCTAssertTrue(usernameFooter.exists, "The username footer should be shown for \(state).") + XCTAssertTrue(passwordFooter.exists, "The password footer should be shown for \(state).") + XCTAssertEqual(usernameFooter.label, VectorL10n.authenticationRegistrationUsernameFooter, + "The username footer should be showing the default message for \(state).") + XCTAssertEqual(passwordFooter.label, VectorL10n.authenticationRegistrationPasswordFooter, + "The password footer should be showing the default message for \(state).") + } + + /// Gets the text field footer for the supplied identifier. + func textFieldFooter(for identifier: String) -> XCUIElement { + let matches = app.staticTexts.matching(identifier: identifier) + return matches.element(boundBy: matches.count - 1) + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Registration/Test/Unit/AuthenticationRegistrationViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/Registration/Test/Unit/AuthenticationRegistrationViewModelTests.swift new file mode 100644 index 000000000..affe61c8b --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Registration/Test/Unit/AuthenticationRegistrationViewModelTests.swift @@ -0,0 +1,218 @@ +// +// 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 XCTest +import Combine + +@testable import RiotSwiftUI + +@available(iOS 14.0, *) +@MainActor class AuthenticationRegistrationViewModelTests: XCTestCase { + var viewModel: AuthenticationRegistrationViewModelProtocol! + var context: AuthenticationRegistrationViewModelType.Context! + + @MainActor override func setUp() async throws { + viewModel = AuthenticationRegistrationViewModel(homeserverAddress: "", ssoIdentityProviders: []) + context = viewModel.context + } + + func testMatrixDotOrg() { + // Given matrix.org with some SSO providers. + let address = "https://matrix.org" + let ssoProviders = [ + SSOIdentityProvider(id: "apple", name: "Apple", brand: "Apple", iconURL: nil), + SSOIdentityProvider(id: "google", name: "Google", brand: "Google", iconURL: nil), + SSOIdentityProvider(id: "github", name: "Github", brand: "Github", iconURL: nil) + ] + + // When updating the view model with the server. + viewModel.update(homeserverAddress: address, showRegistrationForm: true, ssoIdentityProviders: ssoProviders) + + // Then the form should show the server description along with the username and password fields and the SSO buttons. + XCTAssertEqual(context.viewState.homeserverAddress, "matrix.org", "The homeserver address should have the https scheme stripped away.") + XCTAssertEqual(context.viewState.serverDescription, VectorL10n.authenticationRegistrationMatrixDescription, "A description should be shown for matrix.org.") + XCTAssertTrue(context.viewState.showRegistrationForm, "The username and password section should be shown.") + XCTAssertTrue(context.viewState.showSSOButtons, "The SSO buttons should be shown.") + } + + func testBasicServer() { + // Given a basic server example.com that only supports password registration. + let address = "https://example.com" + + // When updating the view model with the server. + viewModel.update(homeserverAddress: address, showRegistrationForm: true, ssoIdentityProviders: []) + + // Then the form should only show the username and password section. + XCTAssertEqual(context.viewState.homeserverAddress, "example.com", "The homeserver address should have the https scheme stripped away.") + XCTAssertNil(context.viewState.serverDescription, "A description should not be shown when the server isn't matrix.org.") + XCTAssertTrue(context.viewState.showRegistrationForm, "The username and password section should be shown.") + XCTAssertFalse(context.viewState.showSSOButtons, "The SSO buttons should not be shown.") + } + + func testUnsecureServer() { + // Given a server that uses http for communication. + let address = "http://testserver.local" + + // When updating the view model with the server. + viewModel.update(homeserverAddress: address, showRegistrationForm: true, ssoIdentityProviders: []) + + // Then the form should only show the username and password section. + XCTAssertEqual(context.viewState.homeserverAddress, address, "The homeserver address should show the http scheme.") + XCTAssertNil(context.viewState.serverDescription, "A description should not be shown when the server isn't matrix.org.") + } + + func testSSOOnlyServer() { + // Given matrix.org with some SSO providers. + let address = "https://example.com" + let ssoProviders = [ + SSOIdentityProvider(id: "apple", name: "Apple", brand: "Apple", iconURL: nil), + SSOIdentityProvider(id: "google", name: "Google", brand: "Google", iconURL: nil), + SSOIdentityProvider(id: "github", name: "Github", brand: "Github", iconURL: nil) + ] + + // When updating the view model with the server. + viewModel.update(homeserverAddress: address, showRegistrationForm: false, ssoIdentityProviders: ssoProviders) + + // Then the form should show the server description along with the username and password fields and the SSO buttons. + XCTAssertEqual(context.viewState.homeserverAddress, "example.com", "The homeserver address should have the https scheme stripped away.") + XCTAssertNil(context.viewState.serverDescription, "A description should not be shown when the server isn't matrix.org.") + XCTAssertFalse(context.viewState.showRegistrationForm, "The username and password section should not be shown.") + XCTAssertTrue(context.viewState.showSSOButtons, "The SSO buttons should be shown.") + } + + func testUsernameError() async { + // Given a form with a valid username. + context.username = "bob" + XCTAssertNil(context.viewState.usernameErrorMessage, "The shouldn't be a username error when the view model is created.") + XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown.") + XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid if there is no error.") + + // When displaying the error as a username error. + let errorMessage = "Username unavailable" + viewModel.displayError(.usernameUnavailable(errorMessage)) + + // Then the error should be shown in the footer. + XCTAssertEqual(context.viewState.usernameErrorMessage, errorMessage, "The error message should be stored.") + XCTAssertEqual(context.viewState.usernameFooterMessage, errorMessage, "The error message should replace the standard footer message.") + XCTAssertFalse(context.viewState.isUsernameValid, "The username should be invalid when an error is shown.") + + + // When clearing the error. + context.send(viewAction: .clearUsernameError) + + // Wait for the action to spawn a Task on the main actor as the Context protocol doesn't support actors. + let task = Task { try await Task.sleep(nanoseconds: 100_000_000) } + _ = await task.result + + // Then the error should be hidden again. + XCTAssertNil(context.viewState.usernameErrorMessage, "The shouldn't be a username error anymore.") + XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooter, "The standard footer message should be shown again.") + XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid when an error is cleared.") + } + + func testEmptyUsernameWithShortPassword() { + // Given a form with an empty username and password. + XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") + XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") + XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.") + XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.") + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + + // When entering a password of 7 characters without a username. + context.username = "" + context.password = "1234567" + + // Then the credentials should remain invalid. + XCTAssertFalse(context.viewState.isPasswordValid, "A 7-character password should be invalid.") + XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.") + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + } + + func testEmptyUsernameWithValidPassword() { + // Given a form with an empty username and password. + XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") + XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") + XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.") + XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.") + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + + // When entering a password of 8 characters without a username. + context.username = "" + context.password = "12345678" + + // Then the password should be valid but the credentials should still be invalid. + XCTAssertTrue(context.viewState.isPasswordValid, "An 8-character password should be valid.") + XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.") + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + } + + func testValidUsernameWithEmptyPassword() { + // Given a form with an empty username and password. + XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") + XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") + XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.") + XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.") + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + + // When entering a username without a password. + context.username = "bob" + context.password = "" + + // Then the username should be valid but the credentials should still be invalid. + XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.") + XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid when there is no error.") + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + } + + func testUsernameErrorWithValidPassword() { + // Given a form with an empty username and password. + XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") + XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") + XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.") + XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.") + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + + // When entering a username and password and encountering a username error + context.username = "bob" + context.password = "12345678" + + let errorMessage = "Username unavailable" + viewModel.displayError(.usernameUnavailable(errorMessage)) + + // Then the password should be valid but the credentials should still be invalid. + XCTAssertTrue(context.viewState.isPasswordValid, "An 8-character password should be valid.") + XCTAssertFalse(context.viewState.isUsernameValid, "The username should be invalid when an error is shown.") + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + } + + func testValidCredentials() { + // Given a form with an empty username and password. + XCTAssertTrue(context.password.isEmpty, "The initial value for the password should be empty.") + XCTAssertTrue(context.username.isEmpty, "The initial value for the username should be empty.") + XCTAssertFalse(context.viewState.isPasswordValid, "An empty password should be invalid.") + XCTAssertFalse(context.viewState.isUsernameValid, "An empty username should be invalid.") + XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") + + // When entering a username and an 8-character password. + context.username = "bob" + context.password = "12345678" + + // Then the credentials should be considered valid. + XCTAssertTrue(context.viewState.isPasswordValid, "An 8-character password should be valid.") + XCTAssertTrue(context.viewState.isUsernameValid, "The username should be valid when there is no error.") + XCTAssertTrue(context.viewState.hasValidCredentials, "The credentials should be valid when the username and password are valid.") + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Registration/View/AuthenticationRegistrationScreen.swift b/RiotSwiftUI/Modules/Authentication/Registration/View/AuthenticationRegistrationScreen.swift new file mode 100644 index 000000000..a9f8efb99 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Registration/View/AuthenticationRegistrationScreen.swift @@ -0,0 +1,198 @@ +// +// 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 + +@available(iOS 14.0, *) +struct AuthenticationRegistrationScreen: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + @ObservedObject var viewModel: AuthenticationRegistrationViewModel.Context + + var body: some View { + ScrollView { + VStack(spacing: 0) { + header + .padding(.top, OnboardingMetrics.topPaddingToNavigationBar) + .padding(.bottom, 36) + + serverInfo + .padding(.leading, 12) + + Divider() + .padding(.vertical, 21) + + if viewModel.viewState.showRegistrationForm { + registrationForm + } + + if viewModel.viewState.showRegistrationForm && viewModel.viewState.showSSOButtons { + Text(VectorL10n.or) + .foregroundColor(theme.colors.secondaryContent) + .padding(.top, 16) + } + + if viewModel.viewState.showSSOButtons { + ssoButtons + .padding(.top, 16) + } + + } + .frame(maxWidth: OnboardingMetrics.maxContentWidth) + .frame(maxWidth: .infinity) + .padding(.horizontal, 16) + .padding(.bottom, 16) + } + .accentColor(theme.colors.accent) + .background(theme.colors.background.ignoresSafeArea()) + .alert(item: $viewModel.alertInfo) { $0.alert } + } + + /// The header containing the icon, title and message. + var header: some View { + VStack(spacing: 8) { + Image(Asset.Images.onboardingCongratulationsIcon.name) + .resizable() + .renderingMode(.template) + .foregroundColor(theme.colors.accent) + .frame(width: 90, height: 90) + .background(Circle().foregroundColor(.white).padding(2)) + .padding(.bottom, 8) + .accessibilityHidden(true) + + Text(VectorL10n.authenticationRegistrationTitle) + .font(theme.fonts.title2B) + .multilineTextAlignment(.center) + .foregroundColor(theme.colors.primaryContent) + + Text(VectorL10n.authenticationRegistrationMessage) + .font(theme.fonts.body) + .multilineTextAlignment(.center) + .foregroundColor(theme.colors.secondaryContent) + } + } + + /// The sever information section that includes a button to select a different server. + var serverInfo: some View { + VStack(alignment: .leading, spacing: 4) { + Text(VectorL10n.authenticationRegistrationServerTitle) + .font(theme.fonts.subheadline) + .foregroundColor(theme.colors.secondaryContent) + + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(viewModel.viewState.homeserverAddress) + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + + if let serverDescription = viewModel.viewState.serverDescription { + Text(serverDescription) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.tertiaryContent) + .accessibilityIdentifier("serverDescriptionText") + } + } + + Spacer() + + Button { viewModel.send(viewAction: .selectServer) } label: { + Text(VectorL10n.edit) + .font(theme.fonts.body) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .overlay(RoundedRectangle(cornerRadius: 8).stroke(theme.colors.accent)) + } + } + } + } + + /// The form with text fields for username and password, along with a submit button. + var registrationForm: some View { + VStack(spacing: 21) { + RoundedBorderTextField(title: nil, + placeHolder: VectorL10n.authenticationRegistrationUsername, + text: $viewModel.username, + footerText: viewModel.viewState.usernameFooterMessage, + isError: viewModel.viewState.hasEditedUsername && !viewModel.viewState.isUsernameValid, + isFirstResponder: false, + configuration: UIKitTextInputConfiguration(returnKeyType: .default, + autocapitalizationType: .none, + autocorrectionType: .no), + onEditingChanged: { validateUsername(isEditing: $0) }) + .onChange(of: viewModel.username) { _ in viewModel.send(viewAction: .clearUsernameError) } + .accessibilityIdentifier("usernameTextField") + + RoundedBorderTextField(title: nil, + placeHolder: VectorL10n.authPasswordPlaceholder, + text: $viewModel.password, + footerText: VectorL10n.authenticationRegistrationPasswordFooter, + isError: viewModel.viewState.hasEditedPassword && !viewModel.viewState.isPasswordValid, + isFirstResponder: false, + configuration: UIKitTextInputConfiguration(isSecureTextEntry: true), + onEditingChanged: { validatePassword(isEditing: $0) }) + .accessibilityIdentifier("passwordTextField") + + Button { viewModel.send(viewAction: .next) } label: { + Text(VectorL10n.next) + } + .buttonStyle(PrimaryActionButtonStyle()) + .disabled(!viewModel.viewState.hasValidCredentials) + .accessibilityIdentifier("nextButton") + } + } + + /// A list of SSO buttons that can be used for login. + var ssoButtons: some View { + VStack(spacing: 16) { + ForEach(viewModel.viewState.ssoIdentityProviders) { provider in + AuthenticationSSOButton(provider: provider) { + viewModel.send(viewAction: .continueWithSSO(id: provider.id)) + } + .accessibilityIdentifier("ssoButton") + } + } + } + + /// Validates the username when the text field ends editing. + func validateUsername(isEditing: Bool) { + guard !isEditing, !viewModel.username.isEmpty else { return } + viewModel.send(viewAction: .validateUsername) + } + + /// Enables password validation the first time the user finishes editing the password text field. + func validatePassword(isEditing: Bool) { + guard !viewModel.viewState.hasEditedPassword, !isEditing else { return } + viewModel.send(viewAction: .enablePasswordValidation) + } +} + +// MARK: - Previews + +@available(iOS 15.0, *) +struct AuthenticationRegistration_Previews: PreviewProvider { + static let stateRenderer = MockAuthenticationRegistrationScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup(addNavigation: true) + .navigationViewStyle(.stack) + } +} diff --git a/RiotSwiftUI/Modules/Authentication/Registration/View/AuthenticationSSOButton.swift b/RiotSwiftUI/Modules/Authentication/Registration/View/AuthenticationSSOButton.swift new file mode 100644 index 000000000..743685900 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/Registration/View/AuthenticationSSOButton.swift @@ -0,0 +1,84 @@ +// +// 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 + +@available(iOS 14.0, *) +/// An button that displays the icon and name of an SSO provider. +struct AuthenticationSSOButton: View { + + // MARK: - Constants + + enum Brand: String { + case apple, facebook, github, gitlab, google, twitter + } + + // MARK: - Private + + @Environment(\.theme) private var theme + + // MARK: - Public + + let provider: SSOIdentityProvider + let action: () -> Void + + // MARK: - Views + + var body: some View { + Button(action: action) { + HStack { + icon + .frame(maxWidth: .infinity, alignment: .leading) + + Text(VectorL10n.socialLoginButtonTitleContinue(provider.name)) + .foregroundColor(theme.colors.primaryContent) + .multilineTextAlignment(.center) + .layoutPriority(1) + + icon + .frame(minWidth: 0, maxWidth: .infinity, alignment: .trailing) + .opacity(0) + } + .frame(maxWidth: .infinity) + .contentShape(RoundedRectangle(cornerRadius: 8)) + } + .buttonStyle(SecondaryActionButtonStyle(customColor: theme.colors.quinaryContent)) + } + + @ViewBuilder + var icon: some View { + switch provider.brand { + case Brand.apple.rawValue: + Image(Asset.Images.authenticationSsoIconApple.name) + .renderingMode(.template) + .foregroundColor(theme.colors.primaryContent) + case Brand.facebook.rawValue: + Image(Asset.Images.authenticationSsoIconFacebook.name) + case Brand.github.rawValue: + Image(Asset.Images.authenticationSsoIconGithub.name) + .renderingMode(.template) + .foregroundColor(theme.colors.primaryContent) + case Brand.gitlab.rawValue: + Image(Asset.Images.authenticationSsoIconGitlab.name) + case Brand.google.rawValue: + Image(Asset.Images.authenticationSsoIconGoogle.name) + case Brand.twitter.rawValue: + Image(Asset.Images.authenticationSsoIconTwitter.name) + default: + EmptyView() + } + } +} diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionModels.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionModels.swift new file mode 100644 index 000000000..f163f4b5d --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionModels.swift @@ -0,0 +1,82 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// MARK: View model + +enum AuthenticationServerSelectionViewModelResult { + /// The user would like to use the homeserver at the given address. + case confirm(homeserverAddress: String) + /// Dismiss the view without using the entered address. + case dismiss +} + +// MARK: View + +struct AuthenticationServerSelectionViewState: BindableState { + /// View state that can be bound to from SwiftUI. + var bindings: AuthenticationServerSelectionBindings + /// An error message to be shown in the text field footer. + var footerErrorMessage: String? + /// Whether the screen is presented modally or within a navigation stack. + var hasModalPresentation: Bool + + /// The message to show in the text field footer. + var footerMessage: String { + footerErrorMessage ?? VectorL10n.authenticationServerSelectionServerFooter + } + + /// The title shown on the confirm button. + var buttonTitle: String { + hasModalPresentation ? VectorL10n.confirm : VectorL10n.next + } + + /// The text field is showing an error. + var isShowingFooterError: Bool { + footerErrorMessage != nil + } + + /// Whether it is possible to continue when tapping the confirmation button. + var hasValidationError: Bool { + bindings.homeserverAddress.isEmpty || isShowingFooterError + } +} + +struct AuthenticationServerSelectionBindings { + /// The homeserver address input by the user. + var homeserverAddress: String + /// Information describing the currently displayed alert. + var alertInfo: AlertInfo? +} + +enum AuthenticationServerSelectionViewAction { + /// The user would like to use the homeserver at the input address. + case confirm + /// Dismiss the view without using the entered address. + case dismiss + /// Open the EMS link. + case getInTouch + /// Clear any errors shown in the text field footer. + case clearFooterError +} + +enum AuthenticationServerSelectionErrorType: Hashable { + /// An error message to be shown in the text field footer. + case footerMessage(String) + /// An error occurred when trying to open the EMS link + case openURLAlert +} diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModel.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModel.swift new file mode 100644 index 000000000..14fd5d5c9 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModel.swift @@ -0,0 +1,84 @@ +// +// 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 + +@available(iOS 14, *) +typealias AuthenticationServerSelectionViewModelType = StateStoreViewModel +@available(iOS 14, *) +class AuthenticationServerSelectionViewModel: AuthenticationServerSelectionViewModelType, AuthenticationServerSelectionViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var completion: ((AuthenticationServerSelectionViewModelResult) -> Void)? + + // MARK: - Setup + + init(homeserverAddress: String, hasModalPresentation: Bool) { + let bindings = AuthenticationServerSelectionBindings(homeserverAddress: HomeserverAddress.displayable(homeserverAddress)) + super.init(initialViewState: AuthenticationServerSelectionViewState(bindings: bindings, + hasModalPresentation: hasModalPresentation)) + } + + // MARK: - Public + + override func process(viewAction: AuthenticationServerSelectionViewAction) { + Task { + await MainActor.run { + switch viewAction { + case .confirm: + completion?(.confirm(homeserverAddress: state.bindings.homeserverAddress)) + case .dismiss: + completion?(.dismiss) + case .getInTouch: + getInTouch() + case .clearFooterError: + guard state.footerErrorMessage != nil else { return } + withAnimation { state.footerErrorMessage = nil } + } + } + } + } + + @MainActor func displayError(_ type: AuthenticationServerSelectionErrorType) { + switch type { + case .footerMessage(let message): + withAnimation { + state.footerErrorMessage = message + } + case .openURLAlert: + state.bindings.alertInfo = AlertInfo(id: .openURLAlert, title: VectorL10n.roomMessageUnableOpenLinkErrorMessage) + } + } + + // MARK: - Private + + /// Opens the EMS link in the user's browser. + @MainActor private func getInTouch() { + let url = BuildSettings.onboardingHostYourOwnServerLink + + UIApplication.shared.open(url) { [weak self] success in + guard !success, let self = self else { return } + self.displayError(.openURLAlert) + } + } +} diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModelProtocol.swift new file mode 100644 index 000000000..4ff73e1c4 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModelProtocol.swift @@ -0,0 +1,27 @@ +// +// 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 AuthenticationServerSelectionViewModelProtocol { + + @MainActor var completion: ((AuthenticationServerSelectionViewModelResult) -> Void)? { get set } + @available(iOS 14, *) + var context: AuthenticationServerSelectionViewModelType.Context { get } + + /// Displays an error to the user. + @MainActor func displayError(_ type: AuthenticationServerSelectionErrorType) +} diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift new file mode 100644 index 000000000..6b9401cc5 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift @@ -0,0 +1,139 @@ +// +// 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 +import CommonKit + +@available(iOS 14.0, *) +struct AuthenticationServerSelectionCoordinatorParameters { + let authenticationService: AuthenticationService + /// Whether the screen is presented modally or within a navigation stack. + let hasModalPresentation: Bool +} + +enum AuthenticationServerSelectionCoordinatorResult { + case updated + case dismiss +} + +@available(iOS 14.0, *) +final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: AuthenticationServerSelectionCoordinatorParameters + private let authenticationServerSelectionHostingController: VectorHostingController + private var authenticationServerSelectionViewModel: AuthenticationServerSelectionViewModelProtocol + + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var loadingIndicator: UserIndicator? + + /// The authentication service that will be updated with the new selection. + var authenticationService: AuthenticationService { parameters.authenticationService } + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + @MainActor var completion: ((AuthenticationServerSelectionCoordinatorResult) -> Void)? + + // MARK: - Setup + + @MainActor init(parameters: AuthenticationServerSelectionCoordinatorParameters) { + self.parameters = parameters + + let homeserver = parameters.authenticationService.state.homeserver + let viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: homeserver.addressFromUser ?? homeserver.address, + hasModalPresentation: parameters.hasModalPresentation) + let view = AuthenticationServerSelectionScreen(viewModel: viewModel.context) + authenticationServerSelectionViewModel = viewModel + authenticationServerSelectionHostingController = VectorHostingController(rootView: view) + authenticationServerSelectionHostingController.vc_removeBackTitle() + authenticationServerSelectionHostingController.enableNavigationBarScrollEdgeAppearance = true + + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: authenticationServerSelectionHostingController) + } + + // MARK: - Public + + func start() { + Task { + await MainActor.run { + MXLog.debug("[AuthenticationServerSelectionCoordinator] did start.") + authenticationServerSelectionViewModel.completion = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[AuthenticationServerSelectionCoordinator] AuthenticationServerSelectionViewModel did complete with result: \(result).") + + switch result { + case .confirm(let homeserverAddress): + self.useHomeserver(homeserverAddress) + case .dismiss: + self.completion?(.dismiss) + } + } + } + } + } + + func toPresentable() -> UIViewController { + return self.authenticationServerSelectionHostingController + } + + // MARK: - Private + + /// Show an activity indicator whilst loading. + /// - Parameters: + /// - label: The label to show on the indicator. + /// - isInteractionBlocking: Whether the indicator should block any user interaction. + @MainActor private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) { + loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) + } + + /// Hide the currently displayed activity indicator. + @MainActor private func stopLoading() { + loadingIndicator = nil + } + + /// Updates the login flow using the supplied homeserver address, or shows an error when this isn't possible. + @MainActor private func useHomeserver(_ homeserverAddress: String) { + startLoading() + authenticationService.reset() + + let homeserverAddress = HomeserverAddress.sanitized(homeserverAddress) + + Task { + do { + #warning("The screen should be configuration for .login too.") + try await authenticationService.startFlow(.registration, for: homeserverAddress) + stopLoading() + + completion?(.updated) + } catch { + stopLoading() + + if let error = error as? RegistrationError { + authenticationServerSelectionViewModel.displayError(.footerMessage(error.localizedDescription)) + } else { + // Show the MXError message if possible otherwise use a generic server error + let message = MXError(nsError: error)?.error ?? VectorL10n.authenticationServerSelectionGenericError + authenticationServerSelectionViewModel.displayError(.footerMessage(message)) + } + } + } + } +} diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/MockAuthenticationServerSelectionScreenState.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/MockAuthenticationServerSelectionScreenState.swift new file mode 100644 index 000000000..8cc3c425b --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/MockAuthenticationServerSelectionScreenState.swift @@ -0,0 +1,66 @@ +// +// 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. +@available(iOS 14.0, *) +enum MockAuthenticationServerSelectionScreenState: MockScreenState, CaseIterable { + // A case for each state you want to represent + // with specific, minimal associated data that will allow you + // mock that screen. + case matrix + case emptyAddress + case invalidAddress + case nonModal + + /// The associated screen + var screenType: Any.Type { + AuthenticationServerSelectionScreen.self + } + + /// Generate the view struct for the screen state. + var screenView: ([Any], AnyView) { + let viewModel: AuthenticationServerSelectionViewModel + switch self { + case .matrix: + viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "https://matrix.org", + hasModalPresentation: true) + case .emptyAddress: + viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "", + hasModalPresentation: true) + case .invalidAddress: + viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "thisisbad", + hasModalPresentation: true) + Task { + await MainActor.run { + viewModel.displayError(.footerMessage(VectorL10n.errorCommonMessage)) + } + } + case .nonModal: + viewModel = AuthenticationServerSelectionViewModel(homeserverAddress: "https://matrix.org", + hasModalPresentation: false) + } + + // can simulate service and viewModel actions here if needs be. + + return ( + [viewModel], AnyView(AuthenticationServerSelectionScreen(viewModel: viewModel.context)) + ) + } +} diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift new file mode 100644 index 000000000..f3dfb1e09 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.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 XCTest +import RiotSwiftUI + +@available(iOS 14.0, *) +class AuthenticationServerSelectionUITests: MockScreenTest { + + override class var screenType: MockScreenState.Type { + return MockAuthenticationServerSelectionScreenState.self + } + + override class func createTest() -> MockScreenTest { + return AuthenticationServerSelectionUITests(selector: #selector(verifyAuthenticationServerSelectionScreen)) + } + + func verifyAuthenticationServerSelectionScreen() throws { + guard let screenState = screenState as? MockAuthenticationServerSelectionScreenState else { fatalError("no screen") } + switch screenState { + case .matrix: + verifyNormalState() + case .emptyAddress: + verifyEmptyAddress() + case .invalidAddress: + verifyInvalidAddress() + case .nonModal: + verifyNonModalPresentation() + } + } + + func verifyNormalState() { + let serverTextField = app.textFields.element + XCTAssertEqual(serverTextField.value as? String, "matrix.org", "The server shown should be matrix.org with the https scheme hidden.") + + let confirmButton = app.buttons["confirmButton"] + XCTAssertEqual(confirmButton.label, VectorL10n.confirm, "The confirm button should say Confirm when in modal presentation.") + XCTAssertTrue(confirmButton.exists, "The confirm button should always be shown.") + XCTAssertTrue(confirmButton.isEnabled, "The confirm button should be enabled when there is an address.") + + let textFieldFooter = app.staticTexts["addressTextField"] + XCTAssertTrue(textFieldFooter.exists) + XCTAssertEqual(textFieldFooter.label, VectorL10n.authenticationServerSelectionServerFooter) + + let dismissButton = app.buttons["dismissButton"] + XCTAssertTrue(dismissButton.exists, "The dismiss button should be shown during modal presentation.") + } + + func verifyEmptyAddress() { + let serverTextField = app.textFields.element + XCTAssertEqual(serverTextField.value as? String, "", "The text field should be empty in this state.") + + let confirmButton = app.buttons["confirmButton"] + XCTAssertTrue(confirmButton.exists, "The confirm button should always be shown.") + XCTAssertFalse(confirmButton.isEnabled, "The confirm button should be disabled when the address is empty.") + } + + func verifyInvalidAddress() { + let serverTextField = app.textFields.element + XCTAssertEqual(serverTextField.value as? String, "thisisbad", "The text field should show the entered server.") + + let confirmButton = app.buttons["confirmButton"] + XCTAssertTrue(confirmButton.exists, "The confirm button should always be shown.") + XCTAssertFalse(confirmButton.isEnabled, "The confirm button should be disabled when there is an error.") + + let textFieldFooter = app.staticTexts["addressTextField"] + XCTAssertTrue(textFieldFooter.exists) + XCTAssertEqual(textFieldFooter.label, VectorL10n.errorCommonMessage) + } + + func verifyNonModalPresentation() { + let dismissButton = app.buttons["dismissButton"] + XCTAssertFalse(dismissButton.exists, "The dismiss button should be hidden when not in modal presentation.") + + let confirmButton = app.buttons["confirmButton"] + XCTAssertEqual(confirmButton.label, VectorL10n.next, "The confirm button should say Next when not in modal presentation.") + } +} diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/Unit/AuthenticationServerSelectionViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/Unit/AuthenticationServerSelectionViewModelTests.swift new file mode 100644 index 000000000..ccc3b3095 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/Unit/AuthenticationServerSelectionViewModelTests.swift @@ -0,0 +1,59 @@ +// +// 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 XCTest + +@testable import RiotSwiftUI + +@available(iOS 14.0, *) +class AuthenticationServerSelectionViewModelTests: XCTestCase { + private enum Constants { + static let counterInitialValue = 0 + } + + var viewModel: AuthenticationServerSelectionViewModelProtocol! + var context: AuthenticationServerSelectionViewModelType.Context! + + override func setUp() async throws { + viewModel = await AuthenticationServerSelectionViewModel(homeserverAddress: "", hasModalPresentation: true) + context = await viewModel.context + } + + @MainActor func testErrorMessage() async { + // Given a new instance of the view model. + XCTAssertNil(context.viewState.footerErrorMessage, "There should not be an error message for a new view model.") + XCTAssertEqual(context.viewState.footerMessage, VectorL10n.authenticationServerSelectionServerFooter, "The standard footer message should be shown.") + + // When an error occurs. + let message = "Unable to contact server." + viewModel.displayError(.footerMessage(message)) + + // Then the footer should now be showing an error. + XCTAssertEqual(context.viewState.footerErrorMessage, message, "The error message should be stored.") + XCTAssertEqual(context.viewState.footerMessage, message, "The error message should be shown.") + + // And when clearing the error. + context.send(viewAction: .clearFooterError) + + // Wait for the action to spawn a Task on the main actor as the Context protocol doesn't support actors. + let task = Task { try await Task.sleep(nanoseconds: 100_000_000) } + _ = await task.result + + // Then the error message should now be removed. + XCTAssertNil(context.viewState.footerErrorMessage, "The error message should have been cleared.") + XCTAssertEqual(context.viewState.footerMessage, VectorL10n.authenticationServerSelectionServerFooter, "The standard footer message should be shown again.") + } +} diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift new file mode 100644 index 000000000..8974e71e0 --- /dev/null +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift @@ -0,0 +1,190 @@ +// +// 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 + +@available(iOS 14.0, *) +struct AuthenticationServerSelectionScreen: View { + + enum Constants { + static let textFieldID = "textFieldID" + } + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme + + /// The scroll view proxy can be stored here for use in other methods. + @State private var scrollView: ScrollViewProxy? + + // MARK: Public + + @ObservedObject var viewModel: AuthenticationServerSelectionViewModel.Context + + // MARK: Views + + var body: some View { + GeometryReader { geometry in + ScrollView { + ScrollViewReader { reader in + VStack(spacing: 0) { + header + .padding(.top, OnboardingMetrics.topPaddingToNavigationBar) + .padding(.bottom, 36) + + serverForm + + Spacer() + + emsBanner + .padding(.vertical, 16) + } + .frame(maxWidth: OnboardingMetrics.maxContentWidth, minHeight: geometry.size.height) + .frame(maxWidth: .infinity) + .padding(.horizontal, 16) + .onAppear { scrollView = reader } + } + } + } + .ignoresSafeArea(.keyboard) + .background(theme.colors.background.ignoresSafeArea()) + .toolbar { toolbar } + .alert(item: $viewModel.alertInfo) { $0.alert } + .accentColor(theme.colors.accent) + } + + /// The title, message and icon at the top of the screen. + var header: some View { + VStack(spacing: 8) { + Image(Asset.Images.authenticationServerSelectionIcon.name) + .resizable() + .renderingMode(.template) + .foregroundColor(theme.colors.accent) + .frame(width: 90, height: 90) + .background(Circle().foregroundColor(.white).padding(4)) + .padding(.bottom, 8) + .accessibilityHidden(true) + + Text(VectorL10n.authenticationServerSelectionTitle) + .font(theme.fonts.title2B) + .multilineTextAlignment(.center) + .foregroundColor(theme.colors.primaryContent) + + Text(VectorL10n.authenticationServerSelectionMessage) + .font(theme.fonts.body) + .multilineTextAlignment(.center) + .foregroundColor(theme.colors.secondaryContent) + } + } + + /// The text field and confirm button where the user enters a server URL. + var serverForm: some View { + VStack(alignment: .leading, spacing: 12) { + RoundedBorderTextField(title: nil, + placeHolder: VectorL10n.authenticationServerSelectionServerUrl, + text: $viewModel.homeserverAddress, + footerText: viewModel.viewState.footerMessage, + isError: viewModel.viewState.isShowingFooterError, + isFirstResponder: false, + configuration: UIKitTextInputConfiguration(keyboardType: .URL, + returnKeyType: .default, + autocapitalizationType: .none, + autocorrectionType: .no), + onTextChanged: nil, + onEditingChanged: textFieldEditingChangeHandler) + .onChange(of: viewModel.homeserverAddress) { _ in viewModel.send(viewAction: .clearFooterError) } + .id(Constants.textFieldID) + .accessibilityIdentifier("addressTextField") + + Button { viewModel.send(viewAction: .confirm) } label: { + Text(viewModel.viewState.buttonTitle) + } + .buttonStyle(PrimaryActionButtonStyle()) + .disabled(viewModel.viewState.hasValidationError) + .accessibilityIdentifier("confirmButton") + } + } + + /// A banner shown beneath the server form with information about hosting your own server. + var emsBanner: some View { + VStack(spacing: 12) { + Image(Asset.Images.authenticationServerSelectionEmsLogo.name) + .padding(.top, 8) + .accessibilityHidden(true) + + Text(VectorL10n.authenticationServerSelectionEmsTitle) + .font(theme.fonts.title3SB) + .multilineTextAlignment(.center) + .foregroundColor(theme.colors.primaryContent) + + VStack(spacing: 2) { + Text(VectorL10n.authenticationServerSelectionEmsMessage) + .font(theme.fonts.callout) + .multilineTextAlignment(.center) + .foregroundColor(theme.colors.secondaryContent) + Text(VectorL10n.authenticationServerSelectionEmsLink) + .font(theme.fonts.callout) + .multilineTextAlignment(.center) + .foregroundColor(theme.colors.primaryContent) + } + .padding(.bottom, 4) + .accessibilityElement(children: .combine) + + Button { viewModel.send(viewAction: .getInTouch) } label: { + Text(VectorL10n.authenticationServerSelectionEmsButton) + .font(theme.fonts.body) + } + .buttonStyle(PrimaryActionButtonStyle(customColor: theme.colors.ems)) + } + .padding(16) + .background(RoundedRectangle(cornerRadius: 9).foregroundColor(theme.colors.system)) + } + + @ToolbarContentBuilder + var toolbar: some ToolbarContent { + ToolbarItem(placement: .cancellationAction) { + if viewModel.viewState.hasModalPresentation { + Button { viewModel.send(viewAction: .dismiss) } label: { + Text(VectorL10n.cancel) + } + .accessibilityLabel(VectorL10n.cancel) + .accessibilityIdentifier("dismissButton") + } + } + } + + /// Ensures the textfield is on screen when editing starts. + /// + /// This is required due to the `.ignoresSafeArea(.keyboard)` modifier which preserves + /// the spacing between the Next button and the EMS banner when the keyboard appears. + func textFieldEditingChangeHandler(isEditing: Bool) { + guard isEditing else { return } + withAnimation { scrollView?.scrollTo(Constants.textFieldID) } + } +} + +// MARK: - Previews + +@available(iOS 15.0, *) +struct AuthenticationServerSelection_Previews: PreviewProvider { + static let stateRenderer = MockAuthenticationServerSelectionScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup(addNavigation: true) + .navigationViewStyle(.stack) + } +} diff --git a/RiotSwiftUI/Modules/Common/ErrorHandling/AlertInfo.swift b/RiotSwiftUI/Modules/Common/ErrorHandling/AlertInfo.swift index 509eb81d0..6c442be48 100644 --- a/RiotSwiftUI/Modules/Common/ErrorHandling/AlertInfo.swift +++ b/RiotSwiftUI/Modules/Common/ErrorHandling/AlertInfo.swift @@ -36,12 +36,20 @@ struct AlertInfo: Identifiable { var secondaryButton: (title: String, action: (() -> Void)?)? = nil } -extension AlertInfo where T == Int { +extension AlertInfo { /// Initialises the type with the title and message from an `NSError` along with the default Ok button. - init?(error: NSError? = nil) { + init?(error: NSError? = nil) where T == Int { + self.init(id: error?.code ?? -1, error: error) + } + + /// Initialises the type with the title and message from an `NSError` along with the default Ok button. + /// - Parameters: + /// - id: An ID that identifies the error. + /// - error: The Error that occurred. + init?(id: T, error: NSError? = nil) { guard error?.domain != NSURLErrorDomain && error?.code != NSURLErrorCancelled else { return nil } - id = error?.code ?? -1 + self.id = id title = error?.userInfo[NSLocalizedFailureReasonErrorKey] as? String ?? VectorL10n.error message = error?.userInfo[NSLocalizedDescriptionKey] as? String ?? VectorL10n.errorCommonMessage } diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index cae73c224..ca153fdbc 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -20,17 +20,24 @@ import Foundation @available(iOS 14.0, *) enum MockAppScreens { static let appScreens: [MockScreenState.Type] = [ + MockLiveLocationSharingViewerScreenState.self, + MockAuthenticationRegistrationScreenState.self, + MockAuthenticationServerSelectionScreenState.self, MockOnboardingCelebrationScreenState.self, MockOnboardingAvatarScreenState.self, MockOnboardingDisplayNameScreenState.self, MockOnboardingCongratulationsScreenState.self, MockOnboardingUseCaseSelectionScreenState.self, MockOnboardingSplashScreenScreenState.self, + MockStaticLocationViewingScreenState.self, MockLocationSharingScreenState.self, MockAnalyticsPromptScreenState.self, MockUserSuggestionScreenState.self, MockPollEditFormScreenState.self, MockSpaceCreationEmailInvitesScreenState.self, + MockSpaceSettingsScreenState.self, + MockRoomAccessTypeChooserScreenState.self, + MockRoomUpgradeScreenState.self, MockMatrixItemChooserScreenState.self, MockSpaceCreationMenuScreenState.self, MockSpaceCreationRoomsScreenState.self, diff --git a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift index 5e29d179b..faf39e0e4 100644 --- a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift +++ b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift @@ -30,12 +30,19 @@ struct ScreenList: View { var body: some View { NavigationView { List { - ForEach(0.. Void + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + var body: some View { + Button(action: action, label: { + HStack { + Image(systemName: selected ? "largecircle.fill.circle" : "circle") + .renderingMode(.template) + .resizable().frame(width: 20, height: 20) + .foregroundColor(selected ? theme.colors.accent : theme.colors.tertiaryContent) + Text(title) + .font(theme.fonts.callout) + .foregroundColor(theme.colors.primaryContent) + Spacer() + } + .padding(EdgeInsets(top: 3, leading: 3, bottom: 3, trailing: 3)) + .background(Color.clear) + }) + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct RadioButton_Previews: PreviewProvider { + static var previews: some View { + Group { + buttonGroup.theme(.light) + buttonGroup.theme(.dark).preferredColorScheme(.dark) + } + .padding() + } + + static var buttonGroup: some View { + VStack { + RadioButton(title: "A title", selected: false, action: {}) + RadioButton(title: "A title", selected: true, action: {}) + } + } +} diff --git a/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift b/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift index 84c75089a..c9f3d73c3 100644 --- a/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift +++ b/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift @@ -58,6 +58,7 @@ struct RoundedBorderTextField: View { .font(theme.fonts.callout) .foregroundColor(theme.colors.tertiaryContent) .lineLimit(1) + .accessibilityHidden(true) } if isEnabled { ThemableTextField(placeholder: "", text: $text, configuration: configuration, onEditingChanged: { edit in @@ -66,22 +67,24 @@ struct RoundedBorderTextField: View { }) .makeFirstResponder(isFirstResponder) .showClearButton(text: $text) - .onChange(of: text, perform: { newText in + .onChange(of: text) { newText in onTextChanged?(newText) - }) + } .frame(height: 30) + .accessibilityLabel(text.isEmpty ? placeHolder : "") } else { ThemableTextField(placeholder: "", text: $text, configuration: configuration, onEditingChanged: { edit in self.editing = edit onEditingChanged?(edit) }) .makeFirstResponder(isFirstResponder) - .onChange(of: text, perform: { newText in + .onChange(of: text) { newText in onTextChanged?(newText) - }) + } .frame(height: 30) .allowsHitTesting(false) .opacity(0.5) + .accessibilityLabel(text.isEmpty ? placeHolder : "") } } .padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: text.isEmpty ? 8 : 0)) diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift new file mode 100644 index 000000000..40f079692 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift @@ -0,0 +1,96 @@ +// +// 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 + +struct LiveLocationSharingViewerCoordinatorParameters { + let session: MXSession + let roomId: String + let navigationRouter: NavigationRouterType? +} + +final class LiveLocationSharingViewerCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: LiveLocationSharingViewerCoordinatorParameters + private let navigationRouter: NavigationRouterType + private let liveLocationSharingViewerHostingController: UIViewController + private var liveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelProtocol + + private let shareLocationActivityControllerBuilder = ShareLocationActivityControllerBuilder() + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + var completion: (() -> Void)? + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: LiveLocationSharingViewerCoordinatorParameters) { + self.parameters = parameters + + let service = LiveLocationSharingViewerService(session: parameters.session, roomId: parameters.roomId) + + let viewModel = LiveLocationSharingViewerViewModel(mapStyleURL: BuildSettings.tileServerMapStyleURL, service: service) + let view = LiveLocationSharingViewer(viewModel: viewModel.context) + .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + liveLocationSharingViewerViewModel = viewModel + liveLocationSharingViewerHostingController = VectorHostingController(rootView: view) + + navigationRouter = parameters.navigationRouter ?? NavigationRouter() + } + + // MARK: - Public + func start() { + MXLog.debug("[LiveLocationSharingViewerCoordinator] did start.") + liveLocationSharingViewerViewModel.completion = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[LiveLocationSharingViewerCoordinator] LiveLocationSharingViewerViewModel did complete with result: \(result).") + switch result { + case .done: + self.completion?() + case .share(let coordinate): + self.presentLocationActivityController(with: coordinate) + } + } + + let viewController: UIViewController = self.liveLocationSharingViewerHostingController + + if navigationRouter.modules.count > 1 { + navigationRouter.push(viewController, animated: true, popCompletion: nil) + } else { + navigationRouter.setRootModule(viewController) + } + } + + func toPresentable() -> UIViewController { + return navigationRouter.toPresentable() + .vc_setModalFullScreen(true) // Set fullscreen as DSBottomSheet is not working with modal pan gesture recognizer + } + + func presentLocationActivityController(with coordinate: CLLocationCoordinate2D) { + + let shareActivityController = shareLocationActivityControllerBuilder.build(with: coordinate) + + self.liveLocationSharingViewerHostingController.present(shareActivityController, animated: true) + } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift new file mode 100644 index 000000000..5ea241749 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift @@ -0,0 +1,67 @@ +// +// 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 Combine +import CoreLocation + +// MARK: - Coordinator + +// MARK: View model + +enum LiveLocationSharingViewerViewModelResult { + case done + case share(_ coordinate: CLLocationCoordinate2D) +} + +// MARK: View + +@available(iOS 14, *) +struct LiveLocationSharingViewerViewState: BindableState { + + /// Map style URL + let mapStyleURL: URL + + /// Map annotations to display on map + var annotations: [UserLocationAnnotation] + + /// Map annotation to focus on + var highlightedAnnotation: UserLocationAnnotation? + + /// Live location list items + var listItemsViewData: [LiveLocationListItemViewData] + + var showLoadingIndicator: Bool = false + + var shareButtonEnabled: Bool { + !showLoadingIndicator + } + + let errorSubject = PassthroughSubject() + + var bindings = LocationSharingViewStateBindings() +} + +struct LiveLocationSharingViewerViewStateBindings { + var alertInfo: AlertInfo? +} + +enum LiveLocationSharingViewerViewAction { + case done + case stopSharing + case tapListItem(_ userId: String) + case share(_ annotation: UserLocationAnnotation) +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift new file mode 100644 index 000000000..c34be9bdb --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift @@ -0,0 +1,241 @@ +// +// 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 +import Combine +import Mapbox + +@available(iOS 14, *) +typealias LiveLocationSharingViewerViewModelType = StateStoreViewModel +@available(iOS 14, *) +class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType, LiveLocationSharingViewerViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + private var liveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol + + private var mapViewErrorAlertInfoBuilder: MapViewErrorAlertInfoBuilder + + private var screenUpdateTimer: Timer? + + // MARK: Public + + var completion: ((LiveLocationSharingViewerViewModelResult) -> Void)? + + // MARK: - Setup + + init(mapStyleURL: URL, service: LiveLocationSharingViewerServiceProtocol) { + + let viewState = LiveLocationSharingViewerViewState(mapStyleURL: mapStyleURL, annotations: [], highlightedAnnotation: nil, listItemsViewData: []) + + liveLocationSharingViewerService = service + mapViewErrorAlertInfoBuilder = MapViewErrorAlertInfoBuilder() + + super.init(initialViewState: viewState) + + state.errorSubject.sink { [weak self] error in + guard let self = self else { return } + self.processError(error) + }.store(in: &cancellables) + + self.setupLocationSharingService() + self.setupScreenUpdateTimer() + } + + // MARK: - Public + + override func process(viewAction: LiveLocationSharingViewerViewAction) { + switch viewAction { + case .done: + completion?(.done) + case .stopSharing: + stopUserLocationSharing() + case .tapListItem(let userId): + self.highlighAnnotation(with: userId) + case .share(let userLocationAnnotation): + completion?(.share(userLocationAnnotation.coordinate)) + } + } + + // MARK: - Private + + private func setupLocationSharingService() { + self.updateUsersLiveLocation(highlightFirstLocation: true) + + liveLocationSharingViewerService.didUpdateUsersLiveLocation = { [weak self] liveLocations in + self?.update(with: liveLocations, highlightFirstLocation: false) + } + self.liveLocationSharingViewerService.startListeningLiveLocationUpdates() + } + + private func updateUsersLiveLocation(highlightFirstLocation: Bool) { + self.update(with: liveLocationSharingViewerService.usersLiveLocation, highlightFirstLocation: highlightFirstLocation) + } + + private func setupScreenUpdateTimer() { + self.screenUpdateTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] timer in + + self?.updateUsersLiveLocation(highlightFirstLocation: false) + } + } + + private func showNoUserLocationsAlert() { + let alertInfo: AlertInfo = AlertInfo(id: .userLocatingError, title: VectorL10n.locationSharingLiveNoUserLocationsErrorTitle, primaryButton:(VectorL10n.ok, { [weak self] in + self?.completion?(.done) + })) + + state.bindings.alertInfo = alertInfo + } + + private func processError(_ error: LocationSharingViewError) { + guard state.bindings.alertInfo == nil else { + return + } + + let alertInfo = mapViewErrorAlertInfoBuilder.build(with: error) { [weak self] in + + switch error { + case .invalidLocationAuthorization: + if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { + UIApplication.shared.open(applicationSettingsURL) + } else { + self?.completion?(.done) + } + default: + self?.completion?(.done) + } + } + + state.bindings.alertInfo = alertInfo + } + + private func userLocationAnnotations(from usersLiveLocation: [UserLiveLocation]) -> [UserLocationAnnotation] { + + return usersLiveLocation.map { userLiveLocation in + return UserLocationAnnotation(avatarData: userLiveLocation.avatarData, coordinate: userLiveLocation.coordinate) + } + } + + private func currentUserLocationAnnotation(from annotations: [UserLocationAnnotation]) -> UserLocationAnnotation? { + annotations.first { annotation in + return liveLocationSharingViewerService.isCurrentUserId(annotation.userId) + } + } + + private func getHighlightedAnnotation(from annotations: [UserLocationAnnotation]) -> UserLocationAnnotation? { + + if let userAnnotation = self.currentUserLocationAnnotation(from: annotations) { + return userAnnotation + } else { + return annotations.first + } + } + + private func listItemsViewData(from usersLiveLocation: [UserLiveLocation]) -> [LiveLocationListItemViewData] { + + var listItemsViewData: [LiveLocationListItemViewData] = [] + + let sortedUsersLiveLocation = usersLiveLocation.sorted { userLiveLocation1, userLiveLocation2 in + return userLiveLocation1.displayName > userLiveLocation2.displayName + } + + listItemsViewData = sortedUsersLiveLocation.map({ userLiveLocation in + return self.listItemViewData(from: userLiveLocation) + }) + + + let currentUserIndex = listItemsViewData.firstIndex { viewData in + return viewData.isCurrentUser + } + + // Move current user as first item + if let currentUserIndex = currentUserIndex { + + let currentUserViewData = listItemsViewData[currentUserIndex] + listItemsViewData.remove(at: currentUserIndex) + listItemsViewData.insert(currentUserViewData, at: 0) + } + + return listItemsViewData + } + + private func listItemViewData(from userLiveLocation: UserLiveLocation) -> LiveLocationListItemViewData { + + let isCurrentUser = self.liveLocationSharingViewerService.isCurrentUserId(userLiveLocation.userId) + + let expirationDate = userLiveLocation.timestamp + userLiveLocation.timeout + + return LiveLocationListItemViewData(userId: userLiveLocation.userId, isCurrentUser: isCurrentUser, avatarData: userLiveLocation.avatarData, displayName: userLiveLocation.displayName, expirationDate: expirationDate, lastUpdate: userLiveLocation.lastUpdate) + } + + private func update(with usersLiveLocation: [UserLiveLocation], highlightFirstLocation: Bool) { + + let annotations: [UserLocationAnnotation] = self.userLocationAnnotations(from: usersLiveLocation) + + var highlightedAnnotation: UserLocationAnnotation? + + if highlightFirstLocation { + highlightedAnnotation = self.getHighlightedAnnotation(from: annotations) + } + + let listViewItems = self.listItemsViewData(from: usersLiveLocation) + + self.state.annotations = annotations + self.state.highlightedAnnotation = highlightedAnnotation + self.state.listItemsViewData = listViewItems + + if usersLiveLocation.isEmpty { + // Advertize user that there is no locations + // Avoid to let the screen empty + self.showNoUserLocationsAlert() + } + } + + private func highlighAnnotation(with userId: String) { + let foundUserAnnotation = self.state.annotations.first { annotation in + annotation.userId == userId + } + + guard let foundUserAnnotation = foundUserAnnotation else { + return + } + + self.state.highlightedAnnotation = foundUserAnnotation + } + + private func stopUserLocationSharing() { + + self.state.showLoadingIndicator = true + + self.liveLocationSharingViewerService.stopUserLiveLocationSharing { result in + self.state.showLoadingIndicator = false + + switch result { + case .success: + break + case.failure: + self.state.bindings.alertInfo = AlertInfo(id: .stopLocationSharingError, + title: VectorL10n.error, + message: VectorL10n.locationSharingLiveStopSharingError, + primaryButton: (VectorL10n.ok, nil)) + } + } + } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModelProtocol.swift new file mode 100644 index 000000000..64f489745 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModelProtocol.swift @@ -0,0 +1,24 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol LiveLocationSharingViewerViewModelProtocol { + + var completion: ((LiveLocationSharingViewerViewModelResult) -> Void)? { get set } + @available(iOS 14, *) + var context: LiveLocationSharingViewerViewModelType.Context { get } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/MockLiveLocationSharingViewerScreenState.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/MockLiveLocationSharingViewerScreenState.swift new file mode 100644 index 000000000..10e198471 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/MockLiveLocationSharingViewerScreenState.swift @@ -0,0 +1,63 @@ +// +// 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. +@available(iOS 14.0, *) +enum MockLiveLocationSharingViewerScreenState: MockScreenState, CaseIterable { + // A case for each state you want to represent + // with specific, minimal associated data that will allow you + // mock that screen. + case currentUser + case multipleUsers + + /// The associated screen + var screenType: Any.Type { + LiveLocationSharingViewer.self + } + + /// A list of screen state definitions + static var allCases: [MockLiveLocationSharingViewerScreenState] { + return [.currentUser, .multipleUsers] + } + + /// Generate the view struct for the screen state. + var screenView: ([Any], AnyView) { + + let service: LiveLocationSharingViewerServiceProtocol + + switch self { + case .currentUser: + service = MockLiveLocationSharingViewerService() + case .multipleUsers: + service = MockLiveLocationSharingViewerService(generateRandomUsers: true) + } + + let mapStyleURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")! + + let viewModel = LiveLocationSharingViewerViewModel(mapStyleURL: mapStyleURL, service: service) + + // can simulate service and viewModel actions here if needs be. + + return ( + [service, viewModel], + AnyView(LiveLocationSharingViewer(viewModel: viewModel.context)) + ) + } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift new file mode 100644 index 000000000..5e23bd64e --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift @@ -0,0 +1,38 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Combine +import CoreLocation + +@available(iOS 14.0, *) +protocol LiveLocationSharingViewerServiceProtocol { + + /// All shared users live location + var usersLiveLocation: [UserLiveLocation] { get } + + /// Called when users live location are updated (new location, location stopped, …). + var didUpdateUsersLiveLocation: (([UserLiveLocation]) -> Void)? { get set } + + func isCurrentUserId(_ userId: String) -> Bool + + func startListeningLiveLocationUpdates() + + func stopListeningLiveLocationUpdates() + + /// Stop current user location sharing + func stopUserLiveLocationSharing(completion: @escaping (Result) -> Void) +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift new file mode 100644 index 000000000..560a4e9c5 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift @@ -0,0 +1,115 @@ +// +// 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 CoreLocation +import MatrixSDK + +@available(iOS 14.0, *) +class LiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol { + + // MARK: - Properties + + private(set) var usersLiveLocation: [UserLiveLocation] = [] + private let roomId: String + private var beaconInfoSummaryListener: Any? + + // MARK: Private + + private let session: MXSession + + // MARK: Public + + var didUpdateUsersLiveLocation: (([UserLiveLocation]) -> Void)? + + // MARK: - Setup + + init(session: MXSession, roomId: String) { + self.session = session + self.roomId = roomId + + self.updateUsersLiveLocation(notifyUpdate: false) + } + + // MARK: - Public + + func isCurrentUserId(_ userId: String) -> Bool { + return self.session.myUserId == userId + } + + func startListeningLiveLocationUpdates() { + self.beaconInfoSummaryListener = self.session.aggregations.beaconAggregations.listenToBeaconInfoSummaryUpdateInRoom(withId: self.roomId) { [weak self] _ in + + self?.updateUsersLiveLocation(notifyUpdate: true) + } + } + + func stopListeningLiveLocationUpdates() { + if let listener = beaconInfoSummaryListener { + self.session.aggregations.removeListener(listener) + self.beaconInfoSummaryListener = nil + } + } + + func stopUserLiveLocationSharing(completion: @escaping (Result) -> Void) { + self.session.locationService.stopUserLocationSharing(inRoomWithId: roomId) { response in + + switch response { + case .success: + completion(.success(Void())) + case .failure(let error): + completion(.failure(error)) + } + } + } + + // MARK: - Private + + private func updateUsersLiveLocation(notifyUpdate: Bool) { + let beaconInfoSummaries = self.session.locationService.getDisplayableBeaconInfoSummaries(inRoomWithId: roomId) + self.usersLiveLocation = Self.usersLiveLocation(fromBeaconInfoSummaries: beaconInfoSummaries, session: session) + + if notifyUpdate { + self.didUpdateUsersLiveLocation?(self.usersLiveLocation) + } + } + + class private func usersLiveLocation(fromBeaconInfoSummaries beaconInfoSummaries: [MXBeaconInfoSummaryProtocol], session: MXSession) -> [UserLiveLocation] { + + return beaconInfoSummaries.compactMap { beaconInfoSummary in + + let beaconInfo = beaconInfoSummary.beaconInfo + + guard let lastBeacon = beaconInfoSummary.lastBeacon else { + return nil + } + + let avatarData = session.avatarInput(for: beaconInfoSummary.userId) + + let timestamp = TimeInterval(beaconInfo.timestamp/1000) + let timeout = TimeInterval(beaconInfo.timeout/1000) + let lastUpdate = TimeInterval(lastBeacon.timestamp/1000) + + let coordinate = CLLocationCoordinate2D(latitude: lastBeacon.location.latitude, longitude: lastBeacon.location.longitude) + + return UserLiveLocation(avatarData: avatarData, + timestamp: timestamp, + timeout: timeout, + lastUpdate: lastUpdate, + coordinate: coordinate) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift new file mode 100644 index 000000000..d941ddfdc --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift @@ -0,0 +1,115 @@ +// +// 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 Combine +import CoreLocation + +@available(iOS 14.0, *) +class MockLiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol { + + // MARK: Properties + + private(set) var usersLiveLocation: [UserLiveLocation] = [] + + var didUpdateUsersLiveLocation: (([UserLiveLocation]) -> Void)? + + // MARK: Setup + + init(generateRandomUsers: Bool = false) { + + let firstUserLiveLocation = self.createFirstUserLiveLocation() + + let secondUserLiveLocation = self.createSecondUserLiveLocation() + + var usersLiveLocation: [UserLiveLocation] = [firstUserLiveLocation, secondUserLiveLocation] + + + if generateRandomUsers { + for _ in 1...20 { + let randomUser = self.createRandomUserLiveLocation() + usersLiveLocation.append(randomUser) + } + } + + self.usersLiveLocation = usersLiveLocation + } + + // MARK: Public + + func isCurrentUserId(_ userId: String) -> Bool { + return "@alice:matrix.org" == userId + } + + func startListeningLiveLocationUpdates() { + + } + + func stopListeningLiveLocationUpdates() { + + } + + func stopUserLiveLocationSharing(completion: @escaping (Result) -> Void) { + + } + + // MARK: Private + + private func createFirstUserLiveLocation() -> UserLiveLocation { + let userAvatarData = AvatarInput(mxContentUri: nil, matrixItemId: "@alice:matrix.org", displayName: "Alice") + let userCoordinate = CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096) + + let currentTimeInterval = Date().timeIntervalSince1970 + let timestamp = currentTimeInterval - 300 + let timeout: TimeInterval = 800 + let lastUpdate = currentTimeInterval - 100 + + return UserLiveLocation(avatarData: userAvatarData, timestamp: timestamp, timeout: timeout, lastUpdate: lastUpdate, coordinate: userCoordinate) + } + + private func createSecondUserLiveLocation() -> UserLiveLocation { + + let userAvatarData = AvatarInput(mxContentUri: nil, matrixItemId: "@bob:matrix.org", displayName: "Bob") + let coordinate = CLLocationCoordinate2D(latitude: 51.4952641, longitude: -0.259096) + + let currentTimeInterval = Date().timeIntervalSince1970 + + let timestamp = currentTimeInterval - 600 + let timeout: TimeInterval = 1200 + let lastUpdate = currentTimeInterval - 300 + + return UserLiveLocation(avatarData: userAvatarData, timestamp: timestamp, timeout: timeout, lastUpdate: lastUpdate, coordinate: coordinate) + } + + + private func createRandomUserLiveLocation() -> UserLiveLocation { + + let uuidString = UUID().uuidString.suffix(8) + + let random = Double.random(in: 0.005...0.010) + + let userAvatarData = AvatarInput(mxContentUri: nil, matrixItemId: "@user_\(uuidString):matrix.org", displayName: "User \(uuidString)") + let coordinate = CLLocationCoordinate2D(latitude: 51.4952641 + random, longitude: -0.259096 + random) + + let currentTimeInterval = Date().timeIntervalSince1970 + + let timestamp = currentTimeInterval - 600 + let timeout: TimeInterval = 1200 + let lastUpdate = currentTimeInterval - 300 + + return UserLiveLocation(avatarData: userAvatarData, timestamp: timestamp, timeout: timeout, lastUpdate: lastUpdate, coordinate: coordinate) + } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/UserLiveLocation.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/UserLiveLocation.swift new file mode 100644 index 000000000..015d09088 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/UserLiveLocation.swift @@ -0,0 +1,43 @@ +// +// 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 +import CoreLocation + +/// Represents user live location +struct UserLiveLocation { + + var userId: String { + return avatarData.matrixItemId + } + + var displayName: String { + return avatarData.displayName ?? self.userId + } + + let avatarData: AvatarInputProtocol + + /// Location sharing start date + let timestamp: TimeInterval + + /// Sharing duration from the start sharing date + let timeout: TimeInterval + + /// Last coordinatore update date + let lastUpdate: TimeInterval + + let coordinate: CLLocationCoordinate2D +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/UI/LiveLocationSharingViewerUITests.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/UI/LiveLocationSharingViewerUITests.swift new file mode 100644 index 000000000..3137d1381 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/UI/LiveLocationSharingViewerUITests.swift @@ -0,0 +1,26 @@ +// +// 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 XCTest +import RiotSwiftUI + +@available(iOS 14.0, *) +class LiveLocationSharingViewerUITests: MockScreenTest { + + override class var screenType: MockScreenState.Type { + return MockLiveLocationSharingViewerScreenState.self + } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift new file mode 100644 index 000000000..ec6f6a59b --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift @@ -0,0 +1,35 @@ +// +// 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 XCTest +import Combine + +@testable import RiotSwiftUI + +@available(iOS 14.0, *) +class LiveLocationSharingViewerViewModelTests: XCTestCase { + + var service: MockLiveLocationSharingViewerService! + var viewModel: LiveLocationSharingViewerViewModelProtocol! + var context: LiveLocationSharingViewerViewModelType.Context! + var cancellables = Set() + + override func setUpWithError() throws { + service = MockLiveLocationSharingViewerService() + viewModel = LiveLocationSharingViewerViewModel(mapStyleURL: BuildSettings.tileServerMapStyleURL, service: service) + context = viewModel.context + } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItem.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItem.swift new file mode 100644 index 000000000..86d192a0c --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItem.swift @@ -0,0 +1,192 @@ +// +// 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 + +@available(iOS 14.0, *) +struct LiveLocationListItem: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + let viewData: LiveLocationListItemViewData + + var timeoutText: String { + + let timeLeftString: String + + if let elapsedTimeString = self.elapsedTimeString(from: viewData.expirationDate, isPastDate: false) { + timeLeftString = VectorL10n.locationSharingLiveListItemTimeLeft(elapsedTimeString) + } else { + timeLeftString = VectorL10n.locationSharingLiveListItemSharingExpired + } + + return timeLeftString + } + + var lastUpdateText: String { + + let timeLeftString: String + + if let elapsedTimeString = self.elapsedTimeString(from: viewData.lastUpdate, isPastDate: true) { + timeLeftString = VectorL10n.locationSharingLiveListItemLastUpdate(elapsedTimeString) + } else { + timeLeftString = VectorL10n.locationSharingLiveListItemLastUpdateInvalid + } + + return timeLeftString + } + + var displayName: String { + return viewData.isCurrentUser ? VectorL10n.locationSharingLiveListItemCurrentUserDisplayName : viewData.displayName + } + + var onStopSharingAction: (() -> (Void))? = nil + + var onBackgroundTap: ((String) -> (Void))? = nil + + // MARK: - Body + + var body: some View { + HStack { + HStack(spacing: 18) { + AvatarImage(avatarData: viewData.avatarData, size: .medium) + .border() + VStack(alignment: .leading, spacing: 2) { Text(displayName) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) + Text(timeoutText) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.primaryContent) + Text(lastUpdateText) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.secondaryContent) + } + } + if viewData.isCurrentUser { + Spacer() + Button(VectorL10n.locationSharingLiveListItemStopSharingAction) { + onStopSharingAction?() + } + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.alert) + } + } + .onTapGesture { + onBackgroundTap?(self.viewData.userId) + } + } + + // MARK: - Private + + private func elapsedTimeString(from timestamp: TimeInterval, isPastDate: Bool) -> String? { + + let formatter = DateComponentsFormatter() + + formatter.unitsStyle = .abbreviated + formatter.allowedUnits = [.hour, .minute, .second] + + let date = Date(timeIntervalSince1970: timestamp) + + let elaspedTimeinterval = date.timeIntervalSinceNow + + var timeLeftString: String? + + // Negative value indicate that the timestamp is in the past + // Positive value indicate that the timestamp is in the future + // Return nil if the sign is not the one as expected + if (isPastDate && elaspedTimeinterval <= 0) || (!isPastDate && elaspedTimeinterval >= 0) { + timeLeftString = formatter.string(from: abs(elaspedTimeinterval)) + } + + return timeLeftString + } +} + +@available(iOS 14.0, *) +struct LiveLocationListPreview: View { + + let liveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol = MockLiveLocationSharingViewerService() + + var viewDataList: [LiveLocationListItemViewData] { + return self.listItemsViewData(from: liveLocationSharingViewerService.usersLiveLocation) + } + + var body: some View { + VStack(alignment: .leading, spacing: 14) { + ForEach(viewDataList) { viewData in + LiveLocationListItem(viewData: viewData, onStopSharingAction: { + + }, onBackgroundTap: { userId in + + }) + } + Spacer() + } + .padding() + } + + private func listItemsViewData(from usersLiveLocation: [UserLiveLocation]) -> [LiveLocationListItemViewData] { + + var listItemsViewData: [LiveLocationListItemViewData] = [] + + let sortedUsersLiveLocation = usersLiveLocation.sorted { userLiveLocation1, userLiveLocation2 in + return userLiveLocation1.displayName > userLiveLocation2.displayName + } + + listItemsViewData = sortedUsersLiveLocation.map({ userLiveLocation in + return self.listItemViewData(from: userLiveLocation) + }) + + let currentUserIndex = listItemsViewData.firstIndex { viewData in + return viewData.isCurrentUser + } + + // Move current user as first item + if let currentUserIndex = currentUserIndex { + + let currentUserViewData = listItemsViewData[currentUserIndex] + listItemsViewData.remove(at: currentUserIndex) + listItemsViewData.insert(currentUserViewData, at: 0) + } + + return listItemsViewData + } + + private func listItemViewData(from userLiveLocation: UserLiveLocation) -> LiveLocationListItemViewData { + + let isCurrentUser = self.liveLocationSharingViewerService.isCurrentUserId(userLiveLocation.userId) + + let expirationDate = userLiveLocation.timestamp + userLiveLocation.timeout + + return LiveLocationListItemViewData(userId: userLiveLocation.userId, isCurrentUser: isCurrentUser, avatarData: userLiveLocation.avatarData, displayName: userLiveLocation.displayName, expirationDate: expirationDate, lastUpdate: userLiveLocation.lastUpdate) + } +} + +@available(iOS 14.0, *) +struct LiveLocationListItem_Previews: PreviewProvider { + static var previews: some View { + Group { + LiveLocationListPreview().theme(.light).preferredColorScheme(.light) + LiveLocationListPreview().theme(.dark).preferredColorScheme(.dark) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItemViewData.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItemViewData.swift new file mode 100644 index 000000000..3e2392225 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItemViewData.swift @@ -0,0 +1,39 @@ +// +// 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 + +/// View data for LiveLocationListItem +struct LiveLocationListItemViewData: Identifiable { + + var id: String { + return userId + } + + let userId: String + + let isCurrentUser: Bool + + let avatarData: AvatarInputProtocol + + let displayName: String + + /// The location sharing expiration date + let expirationDate: TimeInterval + + /// Last coordinatore update + let lastUpdate: TimeInterval +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift new file mode 100644 index 000000000..b5d3cb688 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift @@ -0,0 +1,131 @@ +// +// 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 +import DSBottomSheet + +@available(iOS 14.0, *) +struct LiveLocationSharingViewer: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + var isBottomSheetVisible = true + @State private var isBottomSheetExpanded = false + + // MARK: Public + + @ObservedObject var viewModel: LiveLocationSharingViewerViewModel.Context + + var body: some View { + ZStack(alignment: .bottom) { + LocationSharingMapView(tileServerMapURL: viewModel.viewState.mapStyleURL, + annotations: viewModel.viewState.annotations, + highlightedAnnotation: viewModel.viewState.highlightedAnnotation, + userAvatarData: nil, + showsUserLocation: false, + userAnnotationCanShowCallout: true, + userLocation: Binding.constant(nil), + mapCenterCoordinate: Binding.constant(nil), + onCalloutTap: { annotation in + if let userLocationAnnotation = annotation as? UserLocationAnnotation { + viewModel.send(viewAction: .share(userLocationAnnotation)) + } + }, + errorSubject: viewModel.viewState.errorSubject) + VStack(alignment: .center) { + Spacer() + MapCreditsView() + .offset(y: -130) + } + } + .navigationTitle(VectorL10n.locationSharingLiveViewerTitle) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(VectorL10n.cancel) { + viewModel.send(viewAction: .done) + } + } + } + .accentColor(theme.colors.accent) + .bottomSheet(sheet, if: isBottomSheetVisible) + .alert(item: $viewModel.alertInfo) { info in + info.alert + } + .activityIndicator(show: viewModel.viewState.showLoadingIndicator) + } + + var userLocationList: some View { + ScrollView { + VStack(alignment: .leading, spacing: 14) { + ForEach(viewModel.viewState.listItemsViewData) { viewData in + LiveLocationListItem(viewData: viewData, onStopSharingAction: { + viewModel.send(viewAction: .stopSharing) + }, onBackgroundTap: { userId in + // Push bottom sheet down on item tap + isBottomSheetExpanded = false + viewModel.send(viewAction: .tapListItem(userId)) + }) + } + } + .padding() + } + } +} + +// MARK: - Bottom sheet +@available(iOS 14.0, *) +extension LiveLocationSharingViewer { + + var sheetStyle: BottomSheetStyle { + var bottomSheetStyle = BottomSheetStyle.standard + + bottomSheetStyle.snapRatio = 0.16 + + let backgroundColor = theme.colors.background + + let handleStyle = BottomSheetHandleStyle(backgroundColor: backgroundColor, dividerColor: backgroundColor) + bottomSheetStyle.handleStyle = handleStyle + + return bottomSheetStyle + } + + var sheet: some BottomSheetView { + BottomSheet( + isExpanded: $isBottomSheetExpanded, + minHeight: .points(150), + maxHeight: .available, + style: sheetStyle) { + userLocationList + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct LiveLocationSharingViewer_Previews: PreviewProvider { + static let stateRenderer = MockLiveLocationSharingViewerScreenState.stateRenderer + static var previews: some View { + Group { + stateRenderer.screenGroup().theme(.light).preferredColorScheme(.light) + stateRenderer.screenGroup().theme(.dark).preferredColorScheme(.dark) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationAnnotation.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationAnnotation.swift index 545348f0e..3ddded983 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationAnnotation.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationAnnotation.swift @@ -17,26 +17,36 @@ import Foundation import Mapbox +/// Base class to handle a map annotation class LocationAnnotation: NSObject, MGLAnnotation { // MARK: - Properties + // Title property is needed to enable annotation selection and callout view showing + var title: String? + let coordinate: CLLocationCoordinate2D // MARK: - Setup init(coordinate: CLLocationCoordinate2D) { - self.coordinate = coordinate + super.init() } } +/// POI map annotation class PinLocationAnnotation: LocationAnnotation {} +/// User map annotation class UserLocationAnnotation: LocationAnnotation { // MARK: - Properties + var userId: String { + return avatarData.matrixItemId + } + let avatarData: AvatarInputProtocol // MARK: - Setup @@ -45,7 +55,8 @@ class UserLocationAnnotation: LocationAnnotation { coordinate: CLLocationCoordinate2D) { self.avatarData = avatarData - + super.init(coordinate: coordinate) + super.title = self.avatarData.displayName ?? self.userId } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift index b0d9340e2..b4ade5e31 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift @@ -25,12 +25,20 @@ enum LocationSharingCoordinateType { case pin } +enum LiveLocationSharingTimeout: TimeInterval { + // Timer are in milliseconde because timestamp are in millisecond in Matrix SDK + case short = 900000 // 15 minutes + case medium = 3600000 // 1 hour + case long = 28800000 // 8 hours +} + enum LocationSharingViewAction { case cancel case share case sharePinLocation case goToUserLocation - case shareLiveLocation + case startLiveSharing + case shareLiveLocation(timeout: LiveLocationSharingTimeout) } enum LocationSharingViewModelResult { @@ -87,6 +95,7 @@ struct LocationSharingViewStateBindings { var alertInfo: AlertInfo? var userLocation: CLLocationCoordinate2D? var pinLocation: CLLocationCoordinate2D? + var showingTimerSelector = false } enum LocationSharingAlertType { @@ -94,4 +103,5 @@ enum LocationSharingAlertType { case userLocatingError case authorizationError case locationSharingError + case stopLocationSharingError } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift index e6a7f6226..e8429ec34 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -25,12 +25,6 @@ typealias LocationSharingViewModelType = StateStoreViewModel Void)?) -> AlertInfo? { + func build(with error: LocationSharingViewError, primaryButtonCompletion: (() -> Void)?) -> AlertInfo? { let alertInfo: AlertInfo? @@ -26,20 +26,16 @@ struct MapViewErrorAlertInfoBuilder { case .failedLoadingMap: alertInfo = AlertInfo(id: .mapLoadingError, title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.ok, dimissalCallback)) + primaryButton: (VectorL10n.ok, primaryButtonCompletion)) case .failedLocatingUser: alertInfo = AlertInfo(id: .userLocatingError, title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.ok, dimissalCallback)) + primaryButton: (VectorL10n.ok, primaryButtonCompletion)) case .invalidLocationAuthorization: alertInfo = AlertInfo(id: .authorizationError, title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, dimissalCallback), - secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, { - if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { - UIApplication.shared.open(applicationSettingsURL) - } - })) + primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, primaryButtonCompletion), + secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, primaryButtonCompletion)) default: alertInfo = nil } @@ -48,4 +44,3 @@ struct MapViewErrorAlertInfoBuilder { } } - diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift index 9e947d111..97836ef64 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift @@ -43,6 +43,9 @@ struct LocationSharingMapView: UIViewRepresentable { /// True to indicate to show and follow current user location var showsUserLocation: Bool = false + + /// True to indicate that a touch on user annotation can show a callout + var userAnnotationCanShowCallout: Bool = false /// Last user location if `showsUserLocation` has been enabled @Binding var userLocation: CLLocationCoordinate2D? @@ -50,6 +53,9 @@ struct LocationSharingMapView: UIViewRepresentable { /// Coordinate of the center of the map @Binding var mapCenterCoordinate: CLLocationCoordinate2D? + /// Called when an annotation callout view is tapped + var onCalloutTap: ((MGLAnnotation) -> Void)? + /// Publish view errors if any let errorSubject: PassthroughSubject @@ -160,6 +166,27 @@ extension LocationSharingMapView { } locationSharingMapView.mapCenterCoordinate = mapCenterCoordinate } + + // MARK: Callout + + func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool { + return annotation is UserLocationAnnotation && locationSharingMapView.userAnnotationCanShowCallout + } + + func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? { + if let userLocationAnnotation = annotation as? UserLocationAnnotation { + return UserAnnotationCalloutView(userLocationAnnotation: userLocationAnnotation) + } + + return nil + } + + func mapView(_ mapView: MGLMapView, tapOnCalloutFor annotation: MGLAnnotation) { + + locationSharingMapView.onCalloutTap?(annotation) + // Hide the callout + mapView.deselectAnnotation(annotation, animated: true) + } } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMarkerView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMarkerView.swift index 3910b053f..3c36e7d50 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMarkerView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMarkerView.swift @@ -40,7 +40,6 @@ struct LocationSharingMarkerView: View { markerImage .frame(width: 40, height: 40) } - .offset(x: 0, y: -23) } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift index f71c7f2df..52ec07f22 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift @@ -112,7 +112,7 @@ struct LocationSharingView: View { // Hide for now until live location sharing is finished if context.viewState.isLiveLocationSharingEnabled { LocationSharingOptionButton(text: VectorL10n.locationSharingLiveShareTitle) { - context.send(viewAction: .shareLiveLocation) + context.send(viewAction: .startLiveSharing) } buttonIcon: { Image(uiImage: Asset.Images.locationLiveIcon.image) .resizable() @@ -129,6 +129,24 @@ struct LocationSharingView: View { .disabled(!context.viewState.shareButtonEnabled) } } + .actionSheet(isPresented: $context.showingTimerSelector) { + ActionSheet(title: Text(VectorL10n.locationSharingLiveTimerSelectorTitle), + buttons: [ + .default(Text(VectorL10n.locationSharingLiveTimerSelectorShort)) { + context.send(viewAction: .shareLiveLocation(timeout: .short)) + + }, + .default(Text(VectorL10n.locationSharingLiveTimerSelectorMedium)) { + context.send(viewAction: .shareLiveLocation(timeout: .medium)) + + }, + .default(Text(VectorL10n.locationSharingLiveTimerSelectorLong)) { + context.send(viewAction: .shareLiveLocation(timeout: .long)) + + }, + .cancel() + ]) + } .frame(maxWidth: .infinity, alignment: .leading) .padding() } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.swift new file mode 100644 index 000000000..419213972 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.swift @@ -0,0 +1,85 @@ +// +// 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 +import Reusable + +class UserAnnotationCalloutContentView: UIView, Themable, NibLoadable { + + // MARK: - Constants + + private static let sizingView = UserAnnotationCalloutContentView.instantiate() + + private enum Constants { + static let height: CGFloat = 44.0 + static let cornerRadius: CGFloat = 8.0 + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet var backgroundView: UIView! + @IBOutlet var titleLabel: UILabel! + @IBOutlet var shareButton: UIButton! + + // MARK: - Setup + + static func instantiate() -> UserAnnotationCalloutContentView { + return UserAnnotationCalloutContentView.loadFromNib() + } + + // MARK: - Public + + func update(theme: Theme) { + self.backgroundView.backgroundColor = theme.colors.background + self.titleLabel.textColor = theme.colors.secondaryContent + self.titleLabel.font = theme.fonts.callout + self.shareButton.tintColor = theme.colors.secondaryContent + } + + // MARK: - Life cycle + + override func awakeFromNib() { + super.awakeFromNib() + + self.titleLabel.text = VectorL10n.locationSharingLiveMapCalloutTitle + self.backgroundView.layer.masksToBounds = true + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.backgroundView.layer.cornerRadius = Constants.cornerRadius + } + + static func contentViewSize() -> CGSize { + let sizingView = self.sizingView + + sizingView.frame = CGRect(x: 0, y: 0, width: 1, height: Constants.height) + + sizingView.setNeedsLayout() + sizingView.layoutIfNeeded() + + let fittingSize = CGSize(width: UIView.layoutFittingCompressedSize.width, height: Constants.height) + + let size = sizingView.systemLayoutSizeFitting(fittingSize, + withHorizontalFittingPriority: .fittingSizeLevel, + verticalFittingPriority: .required) + + return size + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.xib b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.xib new file mode 100644 index 000000000..45c9be07f --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.xib @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutView.swift new file mode 100644 index 000000000..64edcfd30 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutView.swift @@ -0,0 +1,156 @@ +// +// 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 Mapbox + +class UserAnnotationCalloutView: UIView, MGLCalloutView, Themable { + + // MARK: - Constants + + private enum Constants { + static let animationDuration: TimeInterval = 0.2 + static let bottomMargin: CGFloat = 3.0 + } + + // MARK: - Properties + + // MARK: Overrides + + var representedObject: MGLAnnotation + + lazy var leftAccessoryView: UIView = UIView() + + lazy var rightAccessoryView: UIView = UIView() + + var delegate: MGLCalloutViewDelegate? + + // Allow the callout to remain open during panning. + let dismissesAutomatically: Bool = false + + let isAnchoredToAnnotation: Bool = true + + // https://github.com/mapbox/mapbox-gl-native/issues/9228 + override var center: CGPoint { + set { + var newCenter = newValue + newCenter.y -= bounds.midY + Constants.bottomMargin + super.center = newCenter + } + get { + return super.center + } + } + + // MARK: Private + + lazy var contentView: UserAnnotationCalloutContentView = { + return UserAnnotationCalloutContentView.instantiate() + }() + + // MARK: - Setup + + required init(userLocationAnnotation: UserLocationAnnotation) { + + self.representedObject = userLocationAnnotation + + super.init(frame: .zero) + + self.vc_addSubViewMatchingParent(self.contentView) + + self.update(theme: ThemeService.shared().theme) + + let size = UserAnnotationCalloutContentView.contentViewSize() + + self.frame = CGRect(origin: .zero, size: size) + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Public + + func update(theme: Theme) { + self.contentView.update(theme: theme) + } + + // MARK: - Overrides + + func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) { + + // Set callout above the marker view + + self.center = view.center.applying(CGAffineTransform(translationX: 0, y: view.bounds.height/2 + self.bounds.height)) + + delegate?.calloutViewWillAppear?(self) + + view.addSubview(self) + + if isCalloutTappable() { + // Handle taps and eventually try to send them to the delegate (usually the map view). + self.contentView.shareButton.addTarget(self, action: #selector(UserAnnotationCalloutView.calloutTapped), for: .touchUpInside) + } else { + // Disable tapping and highlighting. + self.contentView.shareButton.isUserInteractionEnabled = false + } + + if animated { + alpha = 0 + + UIView.animate(withDuration: Constants.animationDuration) { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.alpha = 1 + strongSelf.delegate?.calloutViewDidAppear?(strongSelf) + } + } else { + delegate?.calloutViewDidAppear?(self) + } + } + + func dismissCallout(animated: Bool) { + if (superview != nil) { + if animated { + UIView.animate(withDuration: Constants.animationDuration, animations: { [weak self] in + self?.alpha = 0 + }, completion: { [weak self] _ in + self?.removeFromSuperview() + }) + } else { + removeFromSuperview() + } + } + } + + // MARK: - Callout interaction handlers + + func isCalloutTappable() -> Bool { + if let delegate = delegate { + if delegate.responds(to: #selector(MGLCalloutViewDelegate.calloutViewShouldHighlight)) { + return delegate.calloutViewShouldHighlight!(self) + } + } + return false + } + + @objc func calloutTapped() { + if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped)) { + delegate!.calloutViewTapped!(self) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotationView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotationView.swift index 414f60fc6..5b9956111 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotationView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotationView.swift @@ -21,30 +21,41 @@ import Mapbox @available(iOS 14, *) class LocationAnnotationView: MGLUserLocationAnnotationView { + // MARK: - Constants + + private enum Constants { + static let defaultFrame = CGRect(x: 0, y: 0, width: 46, height: 46) + } + // MARK: Private @Environment(\.theme) private var theme: ThemeSwiftUI // MARK: - Setup - init(avatarData: AvatarInputProtocol) { - super.init(frame: .zero) - + override init(annotation: MGLAnnotation?, reuseIdentifier: String?) { + super.init(annotation: annotation, reuseIdentifier: + reuseIdentifier) + self.frame = Constants.defaultFrame + } + + convenience init(avatarData: AvatarInputProtocol) { + self.init(annotation: nil, reuseIdentifier: nil) self.addUserMarkerView(with: avatarData) } - init(userLocationAnnotation: UserLocationAnnotation) { + convenience init(userLocationAnnotation: UserLocationAnnotation) { // TODO: Use a reuseIdentifier - super.init(annotation: userLocationAnnotation, reuseIdentifier: nil) + self.init(annotation: userLocationAnnotation, reuseIdentifier: nil) self.addUserMarkerView(with: userLocationAnnotation.avatarData) - } - init(pinLocationAnnotation: PinLocationAnnotation) { + convenience init(pinLocationAnnotation: PinLocationAnnotation) { + // TODO: Use a reuseIdentifier - super.init(annotation: pinLocationAnnotation, reuseIdentifier: nil) + self.init(annotation: pinLocationAnnotation, reuseIdentifier: nil) self.addPinMarkerView() } @@ -57,32 +68,34 @@ class LocationAnnotationView: MGLUserLocationAnnotationView { private func addUserMarkerView(with avatarData: AvatarInputProtocol) { - guard let avatarImageView = UIHostingController(rootView: LocationSharingMarkerView(backgroundColor: theme.userColor(for: avatarData.matrixItemId)) { + guard let avatarMarkerView = UIHostingController(rootView: LocationSharingMarkerView(backgroundColor: theme.userColor(for: avatarData.matrixItemId)) { AvatarImage(avatarData: avatarData, size: .medium) .border() }).view else { return } - addMarkerView(with: avatarImageView) + + addMarkerView(avatarMarkerView) } private func addPinMarkerView() { - guard let pinImageView = UIHostingController(rootView: LocationSharingMarkerView(backgroundColor: theme.colors.accent) { + guard let pinMarkerView = UIHostingController(rootView: LocationSharingMarkerView(backgroundColor: theme.colors.accent) { Image(uiImage: Asset.Images.locationPinIcon.image) .resizable() .shapedBorder(color: theme.colors.accent, borderWidth: 3, shape: Circle()) }).view else { return } - addMarkerView(with: pinImageView) + + addMarkerView(pinMarkerView) } - private func addMarkerView(with imageView: UIView) { - addSubview(imageView) + private func addMarkerView(_ markerView: UIView) { - addConstraints([topAnchor.constraint(equalTo: imageView.topAnchor), - leadingAnchor.constraint(equalTo: imageView.leadingAnchor), - bottomAnchor.constraint(equalTo: imageView.bottomAnchor), - trailingAnchor.constraint(equalTo: imageView.trailingAnchor)]) + markerView.backgroundColor = .clear + + addSubview(markerView) + + markerView.frame = self.bounds } } diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/UI/RoomAccessTypeChooserUITests.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/UI/RoomAccessTypeChooserUITests.swift index 184d08dfd..628d50ccb 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/UI/RoomAccessTypeChooserUITests.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/UI/RoomAccessTypeChooserUITests.swift @@ -19,17 +19,5 @@ import RiotSwiftUI @available(iOS 14.0, *) class RoomAccessTypeChooserUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockRoomAccessTypeChooserScreenState.self - } - - override class func createTest() -> MockScreenTest { - return RoomAccessTypeChooserUITests(selector: #selector(verifyRoomAccessTypeChooserScreen)) - } - - func verifyRoomAccessTypeChooserScreen() throws { - guard let screenState = screenState as? MockRoomAccessTypeChooserScreenState else { fatalError("no screen") } - } - + // Tests to be implemented. } diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift index c7463ab27..e0e61b67e 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift @@ -19,17 +19,5 @@ import RiotSwiftUI @available(iOS 14.0, *) class RoomUpgradeUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockRoomUpgradeScreenState.self - } - - override class func createTest() -> MockScreenTest { - return RoomUpgradeUITests(selector: #selector(verifyRoomUpgradeScreen)) - } - - func verifyRoomUpgradeScreen() throws { - guard let screenState = screenState as? MockRoomUpgradeScreenState else { fatalError("no screen") } - } - + // Tests to be implemented. } diff --git a/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift b/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift index 17af5c4cb..508aa6631 100644 --- a/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift @@ -78,7 +78,17 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static } let alertInfo = mapViewErrorAlertInfoBuilder.build(with: error) { [weak self] in - self?.completion?(.close) + + switch error { + case .invalidLocationAuthorization: + if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { + UIApplication.shared.open(applicationSettingsURL) + } else { + self?.completion?(.close) + } + default: + self?.completion?(.close) + } } state.bindings.alertInfo = alertInfo diff --git a/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift b/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift index 8e73be761..e2338dfbb 100644 --- a/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift +++ b/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift @@ -19,21 +19,27 @@ import RiotSwiftUI @available(iOS 14.0, *) class StaticLocationViewingUITests: MockScreenTest { - - private var app: XCUIApplication! - override func setUp() { - continueAfterFailure = false - - app = XCUIApplication() - app.launch() + override class var screenType: MockScreenState.Type { + return MockStaticLocationViewingScreenState.self + } + + override class func createTest() -> MockScreenTest { + return StaticLocationViewingUITests(selector: #selector(verifyStaticLocationViewingScreen)) + } + + func verifyStaticLocationViewingScreen() { + guard let screenState = screenState as? MockStaticLocationViewingScreenState else { fatalError("no screen") } + + switch screenState { + case .showUserLocation: + verifyInitialExistingLocation() + case .showPinLocation: + verifyInitialExistingLocation() + } } - func testInitialExistingLocation() { - goToScreenWithIdentifier(MockStaticLocationViewingScreenState.showUserLocation.title) - - XCTAssertTrue(app.buttons["Cancel"].exists) - XCTAssertTrue(app.buttons["StaticLocationView.shareButton"].exists) - XCTAssertTrue(app.otherElements["Map"].exists) + func verifyInitialExistingLocation() { + // This test has issues running consistently on CI. Removed for now until the issue has been fixed. } } diff --git a/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/View/StaticLocationView.swift b/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/View/StaticLocationView.swift index 1ba73aac0..a217d3afc 100644 --- a/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/View/StaticLocationView.swift +++ b/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/View/StaticLocationView.swift @@ -61,9 +61,9 @@ struct StaticLocationView: View { viewModel.send(viewAction: .share) } label: { Image(uiImage: Asset.Images.locationShareIcon.image) - .accessibilityIdentifier("LocationSharingView.shareButton") } .disabled(!viewModel.viewState.shareButtonEnabled) + .accessibilityIdentifier("shareButton") } } .navigationBarTitleDisplayMode(.inline) diff --git a/RiotSwiftUI/Modules/Spaces/LeaveSpace/Coordinator/LeaveSpaceViewProvider.swift b/RiotSwiftUI/Modules/Spaces/LeaveSpace/Coordinator/LeaveSpaceViewProvider.swift new file mode 100644 index 000000000..63276111c --- /dev/null +++ b/RiotSwiftUI/Modules/Spaces/LeaveSpace/Coordinator/LeaveSpaceViewProvider.swift @@ -0,0 +1,31 @@ +// +// 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 + +class LeaveSpaceViewProvider: MatrixItemChooserCoordinatorViewProvider { + + private let navTitle: String? + + init(navTitle: String?) { + self.navTitle = navTitle + } + + @available(iOS 14, *) + func view(with viewModel: MatrixItemChooserViewModelType.Context) -> AnyView { + return AnyView(LeaveSpace(viewModel: viewModel, navTitle: navTitle)) + } +} diff --git a/RiotSwiftUI/Modules/Spaces/LeaveSpace/Service/MatrixSDK/LeaveSpaceItemsProcessor.swift b/RiotSwiftUI/Modules/Spaces/LeaveSpace/Service/MatrixSDK/LeaveSpaceItemsProcessor.swift new file mode 100644 index 000000000..19ad74020 --- /dev/null +++ b/RiotSwiftUI/Modules/Spaces/LeaveSpace/Service/MatrixSDK/LeaveSpaceItemsProcessor.swift @@ -0,0 +1,105 @@ +// +// 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 MatrixSDK + +class LeaveSpaceItemsProcessor: MatrixItemChooserProcessorProtocol { + + // MARK: Private + + private let spaceId: String + private let session: MXSession + + // MARK: Setup + + init(spaceId: String, session: MXSession) { + self.spaceId = spaceId + self.session = session + self.dataSource = MatrixItemChooserDirectChildrenDataSource(parentId: spaceId) + } + + // MARK: MatrixItemChooserSelectionProcessorProtocol + + private(set) var dataSource: MatrixItemChooserDataSource + + var loadingText: String? { + VectorL10n.roomAccessSettingsScreenSettingRoomAccess + } + + func computeSelection(withIds itemsIds: [String], completion: @escaping (Result) -> Void) { + guard let space = self.session.spaceService.getSpace(withId: self.spaceId) else { + return + } + + self.leaveAllRooms(from: itemsIds, at: 0) { [weak self] result in + guard let self = self else { return } + + switch result { + case .success: + self.leaveSpace(space, completion: completion) + case .failure(let error): + completion(.failure(error)) + } + } + } + + func isItemIncluded(_ item: (MatrixListItemData)) -> Bool { + return true + } + + // MARK: Private + + /// Leave room with room ID from `roomIds` at `index`. + /// Recurse to the next index once done. + private func leaveAllRooms(from roomIds: [String], at index: Int, completion: @escaping (Result) -> Void) { + guard index < roomIds.count else { + completion(.success(())) + return + } + + guard let room = self.session.room(withRoomId: roomIds[index]), !room.isDirect else { + self.leaveAllRooms(from: roomIds, at: index+1, completion: completion) + return + } + + MXLog.debug("[LeaveSpaceItemsProcessor] leaving room \(room.displayName ?? room.roomId)") + room.leave { [weak self] response in + guard let self = self else { return } + + switch response { + case .success: + self.leaveAllRooms(from: roomIds, at: index+1, completion: completion) + case .failure(let error): + MXLog.error("[LeaveSpaceItemsProcessor] failed to leave room with error: \(error)") + completion(.failure(error)) + } + } + } + + private func leaveSpace(_ space: MXSpace, completion: @escaping (Result) -> Void) { + MXLog.debug("[LeaveSpaceItemsProcessor] leaving space") + space.room?.leave(completion: { response in + switch response { + case .success: + completion(.success(())) + case .failure(let error): + MXLog.error("[LeaveSpaceItemsProcessor] failed to leave space with error: \(error)") + completion(.failure(error)) + } + }) + } +} diff --git a/RiotSwiftUI/Modules/Spaces/LeaveSpace/View/LeaveSpace.swift b/RiotSwiftUI/Modules/Spaces/LeaveSpace/View/LeaveSpace.swift new file mode 100644 index 000000000..1cb7bce66 --- /dev/null +++ b/RiotSwiftUI/Modules/Spaces/LeaveSpace/View/LeaveSpace.swift @@ -0,0 +1,58 @@ +// +// 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 + +@available(iOS 14.0, *) +struct LeaveSpace: View { + + // MARK: Properties + + @ObservedObject var viewModel: MatrixItemChooserViewModel.Context + let navTitle: String? + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + @ViewBuilder + var body: some View { + mainView + .background(theme.colors.background) + } + + // MARK: - Private + + @ViewBuilder + private var mainView: some View { + ZStack(alignment: .bottom) { + MatrixItemChooser(viewModel: viewModel, listBottomPadding: 72) + footerView + } + } + + @ViewBuilder + private var footerView: some View { + Button { + viewModel.send(viewAction: .done) + } label: { + Text(viewModel.viewState.selectedItemIds.isEmpty ? VectorL10n.leaveSpaceAction : (viewModel.viewState.selectedItemIds.count == 1 ? VectorL10n.leaveSpaceAndOneRoom : VectorL10n.leaveSpaceAndMoreRooms("\(viewModel.viewState.selectedItemIds.count)"))) + } + .buttonStyle(PrimaryActionButtonStyle(customColor: theme.colors.alert)) + .padding() + } + +} diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift index d22d62587..920dc8b66 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift @@ -31,11 +31,13 @@ struct MatrixItemChooserCoordinatorParameters { let selectedItemsIds: [String] let viewProvider: MatrixItemChooserCoordinatorViewProvider? let itemsProcessor: MatrixItemChooserProcessorProtocol + let selectionHeader: MatrixItemChooserSelectionHeader? init(session: MXSession, title: String? = nil, detail: String? = nil, selectedItemsIds: [String] = [], + selectionHeader: MatrixItemChooserSelectionHeader? = nil, viewProvider: MatrixItemChooserCoordinatorViewProvider? = nil, itemsProcessor: MatrixItemChooserProcessorProtocol) { self.session = session @@ -44,6 +46,7 @@ struct MatrixItemChooserCoordinatorParameters { self.selectedItemsIds = selectedItemsIds self.viewProvider = viewProvider self.itemsProcessor = itemsProcessor + self.selectionHeader = selectionHeader } } @@ -69,7 +72,7 @@ final class MatrixItemChooserCoordinator: Coordinator, Presentable { @available(iOS 14.0, *) init(parameters: MatrixItemChooserCoordinatorParameters) { self.parameters = parameters - let viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserService(session: parameters.session, selectedItemIds: parameters.selectedItemsIds, itemsProcessor: parameters.itemsProcessor), title: parameters.title, detail: parameters.detail) + let viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserService(session: parameters.session, selectedItemIds: parameters.selectedItemsIds, itemsProcessor: parameters.itemsProcessor), title: parameters.title, detail: parameters.detail, selectionHeader: parameters.selectionHeader) matrixItemChooserViewModel = viewModel if let viewProvider = parameters.viewProvider { let view = viewProvider.view(with: viewModel.context).addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserModels.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserModels.swift index b82422529..da1374498 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserModels.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserModels.swift @@ -27,13 +27,6 @@ enum MatrixItemChooserType { // MARK: View model -enum MatrixItemChooserStateAction { - case loadingState(Bool) - case updateError(Error?) - case updateSections([MatrixListItemSectionData]) - case updateSelection(Set) -} - enum MatrixItemChooserViewModelResult { case cancel case done([String]) @@ -77,15 +70,23 @@ struct MatrixListItemData { extension MatrixListItemData: Identifiable, Equatable {} +struct MatrixItemChooserSelectionHeader { + var title: String + var selectAllTitle: String + var selectNoneTitle: String +} + struct MatrixItemChooserViewState: BindableState { var title: String? var message: String? var emptyListMessage: String var sections: [MatrixListItemSectionData] + var itemCount: Int var selectedItemIds: Set var loadingText: String? var loading: Bool var error: String? + var selectionHeader: MatrixItemChooserSelectionHeader? } enum MatrixItemChooserViewAction { @@ -94,4 +95,6 @@ enum MatrixItemChooserViewAction { case done case cancel case back + case selectAll + case selectNone } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModel.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModel.swift index 596a246b9..e29f81d51 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModel.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModel.swift @@ -19,7 +19,7 @@ import Combine @available(iOS 14, *) typealias MatrixItemChooserViewModelType = StateStoreViewModel @available(iOS 14, *) class MatrixItemChooserViewModel: MatrixItemChooserViewModelType, MatrixItemChooserViewModelProtocol { @@ -30,40 +30,49 @@ class MatrixItemChooserViewModel: MatrixItemChooserViewModelType, MatrixItemChoo private var matrixItemChooserService: MatrixItemChooserServiceProtocol + private var isLoading: Bool = false { + didSet { + state.loading = isLoading + if isLoading { + state.error = nil + } + } + } + // MARK: Public var completion: ((MatrixItemChooserViewModelResult) -> Void)? // MARK: - Setup - static func makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?) -> MatrixItemChooserViewModelProtocol { - return MatrixItemChooserViewModel(matrixItemChooserService: matrixItemChooserService, title: title, detail: detail) + static func makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?, selectionHeader: MatrixItemChooserSelectionHeader?) -> MatrixItemChooserViewModelProtocol { + return MatrixItemChooserViewModel(matrixItemChooserService: matrixItemChooserService, title: title, detail: detail, selectionHeader: selectionHeader) } - private init(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?) { + private init(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?, selectionHeader: MatrixItemChooserSelectionHeader?) { self.matrixItemChooserService = matrixItemChooserService - super.init(initialViewState: Self.defaultState(matrixItemChooserService: matrixItemChooserService, title: title, detail: detail)) + super.init(initialViewState: Self.defaultState(service: matrixItemChooserService, title: title, detail: detail, selectionHeader: selectionHeader)) startObservingItems() } - private static func defaultState(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?) -> MatrixItemChooserViewState { + private static func defaultState(service: MatrixItemChooserServiceProtocol, title: String?, detail: String?, selectionHeader: MatrixItemChooserSelectionHeader?) -> MatrixItemChooserViewState { let title = title let message = detail let emptyListMessage = VectorL10n.spacesNoResultFoundTitle - return MatrixItemChooserViewState(title: title, message: message, emptyListMessage: emptyListMessage, sections: matrixItemChooserService.sectionsSubject.value, selectedItemIds: matrixItemChooserService.selectedItemIdsSubject.value, loadingText: matrixItemChooserService.loadingText, loading: false) + return MatrixItemChooserViewState(title: title, message: message, emptyListMessage: emptyListMessage, sections: service.sectionsSubject.value, itemCount: service.itemCount, selectedItemIds: service.selectedItemIdsSubject.value, loadingText: service.loadingText, loading: false, selectionHeader: selectionHeader) } private func startObservingItems() { - let sectionsUpdatePublisher = matrixItemChooserService.sectionsSubject - .map(MatrixItemChooserStateAction.updateSections) - .eraseToAnyPublisher() - dispatch(actionPublisher: sectionsUpdatePublisher) - - let selectionPublisher = matrixItemChooserService.selectedItemIdsSubject - .map(MatrixItemChooserStateAction.updateSelection) - .eraseToAnyPublisher() - dispatch(actionPublisher: selectionPublisher) + matrixItemChooserService.sectionsSubject.sink { [weak self] sections in + self?.state.sections = sections + self?.state.itemCount = self?.matrixItemChooserService.itemCount ?? 0 + } + .store(in: &cancellables) + matrixItemChooserService.selectedItemIdsSubject.sink { [weak self] selectedItemIds in + self?.state.selectedItemIds = selectedItemIds + } + .store(in: &cancellables) } // MARK: - Public @@ -75,11 +84,11 @@ class MatrixItemChooserViewModel: MatrixItemChooserViewModelType, MatrixItemChoo case .back: back() case .done: - dispatch(action: .loadingState(true)) + isLoading = true matrixItemChooserService.processSelection { [weak self] result in guard let self = self else { return } - self.dispatch(action: .loadingState(false)) + self.isLoading = false switch result { case .success: @@ -87,31 +96,20 @@ class MatrixItemChooserViewModel: MatrixItemChooserViewModelType, MatrixItemChoo self.done(selectedItemsId: selectedItemsId) case .failure(let error): self.matrixItemChooserService.refresh() - self.dispatch(action: .updateError(error)) + self.state.error = error.localizedDescription } } case .searchTextChanged(let searchText): self.matrixItemChooserService.searchText = searchText case .itemTapped(let itemId): self.matrixItemChooserService.reverseSelectionForItem(withId: itemId) + case .selectAll: + self.matrixItemChooserService.selectAllItems() + case .selectNone: + self.matrixItemChooserService.deselectAllItems() } } - - override class func reducer(state: inout MatrixItemChooserViewState, action: MatrixItemChooserStateAction) { - switch action { - case .updateSections(let sections): - state.sections = sections - case .updateSelection(let selectedItemIds): - state.selectedItemIds = selectedItemIds - case .loadingState(let loading): - state.loading = loading - state.error = nil - case .updateError(let error): - state.error = error?.localizedDescription - } - UILog.debug("[MatrixItemChooserViewModel] reducer with action \(action) produced state: \(state)") - } - + private func done(selectedItemsId: [String]) { completion?(.done(selectedItemsId)) } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModelProtocol.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModelProtocol.swift index d91b21aca..d6fd3d363 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModelProtocol.swift @@ -20,7 +20,7 @@ protocol MatrixItemChooserViewModelProtocol { var completion: ((MatrixItemChooserViewModelResult) -> Void)? { get set } @available(iOS 14, *) - static func makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?) -> MatrixItemChooserViewModelProtocol + static func makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?, selectionHeader: MatrixItemChooserSelectionHeader?) -> MatrixItemChooserViewModelProtocol @available(iOS 14, *) var context: MatrixItemChooserViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift index a8f1d729b..d26ac330b 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift @@ -27,6 +27,7 @@ enum MockMatrixItemChooserScreenState: MockScreenState, CaseIterable { case noItems case items case selectedItems + case selectionHeader /// The associated screen var screenType: Any.Type { @@ -36,15 +37,25 @@ enum MockMatrixItemChooserScreenState: MockScreenState, CaseIterable { /// Generate the view struct for the screen state. var screenView: ([Any], AnyView) { let service: MockMatrixItemChooserService + let selectionHeader: MatrixItemChooserSelectionHeader? switch self { case .noItems: + selectionHeader = nil service = MockMatrixItemChooserService(type: .room, sections: [MatrixListItemSectionData()]) case .items: + selectionHeader = nil service = MockMatrixItemChooserService() case .selectedItems: + selectionHeader = nil + service = MockMatrixItemChooserService(type: .room, sections: MockMatrixItemChooserService.mockSections, selectedItemIndexPaths: [IndexPath(row: 0, section: 0), IndexPath(row: 2, section: 0), IndexPath(row: 1, section: 1)]) + case .selectionHeader: + selectionHeader = MatrixItemChooserSelectionHeader(title: "Selection Title", selectAllTitle: "Select all items", selectNoneTitle: "Select no items") service = MockMatrixItemChooserService(type: .room, sections: MockMatrixItemChooserService.mockSections, selectedItemIndexPaths: [IndexPath(row: 0, section: 0), IndexPath(row: 2, section: 0), IndexPath(row: 1, section: 1)]) } - let viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: service, title: "Some title", detail: "Detail text describing the current screen") + let viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: service, + title: VectorL10n.spacesCreationAddRoomsTitle, + detail: VectorL10n.spacesCreationAddRoomsMessage, + selectionHeader: selectionHeader) // can simulate service and viewModel actions here if needs be. diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixItemChooserServiceProtocol.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixItemChooserServiceProtocol.swift index d87e4116b..4aac468d3 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixItemChooserServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixItemChooserServiceProtocol.swift @@ -23,8 +23,11 @@ protocol MatrixItemChooserServiceProtocol { var selectedItemIdsSubject: CurrentValueSubject, Never> { get } var searchText: String { get set } var loadingText: String? { get } + var itemCount: Int { get } func reverseSelectionForItem(withId itemId: String) func processSelection(completion: @escaping (Result) -> Void) func refresh() + func selectAllItems() + func deselectAllItems() } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserDirectChildrenDataSource.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserDirectChildrenDataSource.swift new file mode 100644 index 000000000..c48d6abdb --- /dev/null +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserDirectChildrenDataSource.swift @@ -0,0 +1,50 @@ +// +// 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 MatrixItemChooserDirectChildrenDataSource: MatrixItemChooserDataSource { + + // MARK: - Private + + private let parentId: String + + // MARK: - Setup + + init(parentId: String) { + self.parentId = parentId + } + + // MARK: - MatrixItemChooserDataSource + + var preselectedItemIds: Set? { nil } + + func sections(with session: MXSession, completion: @escaping (Result<[MatrixListItemSectionData], Error>) -> Void) { + let space = session.spaceService.getSpace(withId: parentId) + let children: [MatrixListItemData] = space?.childRoomIds.compactMap({ roomId in + guard let room = session.room(withRoomId: roomId), !room.isDirect else { + return nil + } + + return MatrixListItemData(mxRoom: room, spaceService: session.spaceService) + }) ?? [] + completion(Result(catching: { + [ + MatrixListItemSectionData(items: children) + ] + })) + } +} diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserService.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserService.swift index 61406eac9..cc74171b2 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserService.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserService.swift @@ -61,6 +61,13 @@ class MatrixItemChooserService: MatrixItemChooserServiceProtocol { var loadingText: String? { itemsProcessor.loadingText } + var itemCount: Int { + var itemCount = 0 + for section in sections { + itemCount += section.items.count + } + return itemCount + } // MARK: - Setup @@ -118,6 +125,22 @@ class MatrixItemChooserService: MatrixItemChooserServiceProtocol { } } } + + func selectAllItems() { + var newSelection: Set = Set() + for section in sections { + for item in section.items { + newSelection.insert(item.id) + } + } + self.selectedItemIds = newSelection + selectedItemIdsSubject.send(selectedItemIds) + } + + func deselectAllItems() { + self.selectedItemIds = Set() + selectedItemIdsSubject.send(selectedItemIds) + } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/Mock/MockMatrixItemChooserService.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/Mock/MockMatrixItemChooserService.swift index a6def2772..18891c742 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/Mock/MockMatrixItemChooserService.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/Mock/MockMatrixItemChooserService.swift @@ -39,6 +39,13 @@ class MockMatrixItemChooserService: MatrixItemChooserServiceProtocol { var loadingText: String? { nil } + var itemCount: Int { + var itemCount = 0 + for section in sectionsSubject.value { + itemCount += section.items.count + } + return itemCount + } init(type: MatrixItemChooserType = .room, sections: [MatrixListItemSectionData] = mockSections, selectedItemIndexPaths: [IndexPath] = []) { sectionsSubject = CurrentValueSubject(sections) @@ -79,4 +86,20 @@ class MockMatrixItemChooserService: MatrixItemChooserServiceProtocol { func refresh() { } + + func selectAllItems() { + var newSelection: Set = Set() + for section in sectionsSubject.value { + for item in section.items { + newSelection.insert(item.id) + } + } + self.selectedItemIds = newSelection + selectedItemIdsSubject.send(selectedItemIds) + } + + func deselectAllItems() { + self.selectedItemIds = Set() + selectedItemIdsSubject.send(selectedItemIds) + } } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift index e292151b9..6a0918694 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift @@ -37,6 +37,8 @@ class MatrixItemChooserUITests: MockScreenTest { verifyPopulatedScreen() case .selectedItems: verifyPopulatedWithSelectionScreen() + case .selectionHeader: + break } } @@ -45,21 +47,18 @@ class MatrixItemChooserUITests: MockScreenTest { XCTAssertEqual(app.staticTexts["messageText"].label, VectorL10n.spacesCreationAddRoomsMessage) XCTAssertEqual(app.staticTexts["emptyListMessage"].exists, true) XCTAssertEqual(app.staticTexts["emptyListMessage"].label, VectorL10n.spacesNoResultFoundTitle) - XCTAssertEqual(app.buttons["doneButton"].label, VectorL10n.skip) } func verifyPopulatedScreen() { XCTAssertEqual(app.staticTexts["titleText"].label, VectorL10n.spacesCreationAddRoomsTitle) XCTAssertEqual(app.staticTexts["messageText"].label, VectorL10n.spacesCreationAddRoomsMessage) XCTAssertEqual(app.staticTexts["emptyListMessage"].exists, false) - XCTAssertEqual(app.buttons["doneButton"].label, VectorL10n.skip) } func verifyPopulatedWithSelectionScreen() { XCTAssertEqual(app.staticTexts["titleText"].label, VectorL10n.spacesCreationAddRoomsTitle) XCTAssertEqual(app.staticTexts["messageText"].label, VectorL10n.spacesCreationAddRoomsMessage) XCTAssertEqual(app.staticTexts["emptyListMessage"].exists, false) - XCTAssertEqual(app.buttons["doneButton"].label, VectorL10n.next) } } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/Unit/MatrixItemChooserViewModelTests.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/Unit/MatrixItemChooserViewModelTests.swift index 22c2f0d86..1e256775f 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/Unit/MatrixItemChooserViewModelTests.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/Unit/MatrixItemChooserViewModelTests.swift @@ -28,7 +28,7 @@ class MatrixItemChooserViewModelTests: XCTestCase { override func setUpWithError() throws { service = MockMatrixItemChooserService(type: .room) - viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: service, title: VectorL10n.spacesCreationAddRoomsTitle, detail: VectorL10n.spacesCreationAddRoomsMessage) + viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: service, title: VectorL10n.spacesCreationAddRoomsTitle, detail: VectorL10n.spacesCreationAddRoomsMessage, selectionHeader: nil) context = viewModel.context } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift index 16c0aac98..1c2808261 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift @@ -90,6 +90,7 @@ struct MatrixItemChooser: View { .frame(maxHeight: .infinity, alignment: .top) .animation(nil) } + .animation(nil) } @ViewBuilder @@ -116,8 +117,36 @@ struct MatrixItemChooser: View { .onChange(of: searchText) { value in viewModel.send(viewAction: .searchTextChanged(searchText)) } + if let selectionHeader = viewModel.viewState.selectionHeader, searchText.isEmpty { + Spacer().frame(height: spacerHeight) + itemSelectionHeader(with: selectionHeader) + } } } + + private func itemSelectionHeader(with selectionHeader: MatrixItemChooserSelectionHeader) -> some View { + VStack(alignment:.leading) { + HStack { + Text(selectionHeader.title) + .font(theme.fonts.calloutSB) + .foregroundColor(theme.colors.primaryContent) + Text("\(viewModel.viewState.itemCount)") + .font(theme.fonts.calloutSB) + .foregroundColor(theme.colors.tertiaryContent) + } + HStack { + RadioButton(title: selectionHeader.selectAllTitle, selected: viewModel.viewState.itemCount > 0 && viewModel.viewState.selectedItemIds.count == viewModel.viewState.itemCount) { + viewModel.send(viewAction: .selectAll) + } + RadioButton(title: selectionHeader.selectNoneTitle, selected: viewModel.viewState.selectedItemIds.isEmpty) { + viewModel.send(viewAction: .selectNone) + } + } + } + .padding(.vertical, 4) + .padding(.horizontal) + .background(theme.colors.tile) + } } // MARK: - Previews @@ -127,9 +156,9 @@ struct MatrixItemChooser_Previews: PreviewProvider { static let stateRenderer = MockMatrixItemChooserScreenState.stateRenderer static var previews: some View { - stateRenderer.screenGroup(addNavigation: true) + stateRenderer.screenGroup(addNavigation: false) .theme(.light).preferredColorScheme(.light) - stateRenderer.screenGroup(addNavigation: true) + stateRenderer.screenGroup(addNavigation: false) .theme(.dark).preferredColorScheme(.dark) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift index 2e47c2f76..1449bd93d 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift @@ -19,17 +19,5 @@ import RiotSwiftUI @available(iOS 14.0, *) class SpaceSettingsUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockSpaceSettingsScreenState.self - } - - override class func createTest() -> MockScreenTest { - return SpaceSettingsUITests(selector: #selector(verifySpaceSettingsScreen)) - } - - func verifySpaceSettingsScreen() throws { - guard let screenState = screenState as? MockSpaceSettingsScreenState else { fatalError("no screen") } - } - + // Tests to be implemented. } diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml index 58f3e74f3..f2feb1940 100644 --- a/RiotSwiftUI/target.yml +++ b/RiotSwiftUI/target.yml @@ -52,6 +52,7 @@ targets: - path: ../Riot/Categories/Character.swift - path: ../Riot/Categories/UIColor.swift - path: ../Riot/Categories/UISearchBar.swift + - path: ../Riot/Categories/UIView.swift - path: ../Riot/Assets/en.lproj/Vector.strings - path: ../Riot/Modules/Analytics/AnalyticsScreen.swift - path: ../Riot/Assets/en.lproj/Untranslated.strings diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml index 324fcb0d0..b7a0f9d00 100644 --- a/RiotSwiftUI/targetUITests.yml +++ b/RiotSwiftUI/targetUITests.yml @@ -39,6 +39,7 @@ targets: PRODUCT_BUNDLE_IDENTIFIER: org.matrix.RiotSwiftUITests$(rfc1034identifier) SWIFT_OBJC_BRIDGING_HEADER: $(SRCROOT)/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h + GENERATE_INFOPLIST_FILE: YES sources: # Source included/excluded here here are similar to RiotSwiftUI as we # need access to ScreenStates @@ -60,6 +61,7 @@ targets: - path: ../Riot/Categories/Character.swift - path: ../Riot/Categories/UIColor.swift - path: ../Riot/Categories/UISearchBar.swift + - path: ../Riot/Categories/UIView.swift - path: ../Riot/Assets/en.lproj/Vector.strings - path: ../Riot/Modules/Analytics/AnalyticsScreen.swift - path: ../Riot/Assets/en.lproj/Untranslated.strings diff --git a/RiotTests/AuthenticationServiceTests.swift b/RiotTests/AuthenticationServiceTests.swift new file mode 100644 index 000000000..c5f520c76 --- /dev/null +++ b/RiotTests/AuthenticationServiceTests.swift @@ -0,0 +1,85 @@ +// +// 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 XCTest + +@testable import Riot + +@available(iOS 14.0, *) +class AuthenticationServiceTests: XCTestCase { + func testRegistrationWizardWhenStartingLoginFlow() async throws { + // Given a fresh service. + let service = AuthenticationService() + XCTAssertNil(service.registrationWizard, "A new service shouldn't have a registration wizard.") + + // When starting a new login flow. + try await service.startFlow(.login, for: "https://matrix.org") + + // Then a registration wizard shouldn't have been created. + XCTAssertNil(service.registrationWizard, "The registration wizard should not exist if startFlow was called for login.") + } + + func testRegistrationWizard() async throws { + // Given a fresh service. + let service = AuthenticationService() + XCTAssertNil(service.registrationWizard, "A new service shouldn't provide a registration wizard.") + XCTAssertNil(service.state.homeserver.registrationFlow, "A new service shouldn't provide a registration flow for the homeserver.") + + // When starting a new registration flow. + try await service.startFlow(.registration, for: "https://matrix.org") + + // Then a registration wizard should be available for use. + XCTAssertNotNil(service.registrationWizard, "The registration wizard should exist after starting a registration flow.") + XCTAssertNotNil(service.state.homeserver.registrationFlow, "The supported registration flow should be stored after starting a registration flow.") + } + + func testReset() async throws { + // Given a service that has begun registration. + let service = AuthenticationService() + try await service.startFlow(.registration, for: "https://matrix.org") + _ = try await service.registrationWizard?.createAccount(username: UUID().uuidString, password: UUID().uuidString, initialDeviceDisplayName: "Test") + XCTAssertNotNil(service.loginWizard, "The login wizard should exist after starting a registration flow.") + XCTAssertNotNil(service.registrationWizard, "The registration wizard should exist after starting a registration flow.") + XCTAssertNotNil(service.state.homeserver.registrationFlow, "The supported registration flow should be stored after starting a registration flow.") + XCTAssertTrue(service.isRegistrationStarted, "The service should show as having started registration.") + XCTAssertEqual(service.state.flow, .registration, "The service should show as using a registration flow.") + + // When resetting the service. + service.reset() + + // Then the wizards should no longer exist. + XCTAssertNil(service.loginWizard, "The login wizard should be cleared after calling reset.") + XCTAssertNil(service.registrationWizard, "The registration wizard should be cleared after calling reset.") + XCTAssertNil(service.state.homeserver.registrationFlow, "The supported registration flow should be cleared when calling reset.") + XCTAssertFalse(service.isRegistrationStarted, "The service should not indicate it has started registration after calling reset.") + XCTAssertEqual(service.state.flow, .login, "The flow should have been set back to login when calling reset.") + } + + func testHomeserverState() async throws { + // Given a service that has begun login for one homeserver. + let service = AuthenticationService() + try await service.startFlow(.login, for: "https://glasgow.social") + XCTAssertEqual(service.state.homeserver.addressFromUser, "https://glasgow.social", "The initial address entered by the user should be stored.") + XCTAssertEqual(service.state.homeserver.address, "https://matrix.glasgow.social", "The initial address discovered from the well-known should be stored.") + + // When switching to a different homeserver + try await service.startFlow(.login, for: "https://matrix.org") + + // The the homeserver state should update to represent the new server + XCTAssertEqual(service.state.homeserver.addressFromUser, "https://matrix.org", "The new address entered by the user should be stored.") + XCTAssertEqual(service.state.homeserver.address, "https://matrix-client.matrix.org", "The new address discovered from the well-known should be stored.") + } +} diff --git a/Tools/SwiftGen/Templates/Strings/flat-swift4-vector.stencil b/Tools/SwiftGen/Templates/Strings/flat-swift4-vector.stencil index 3e3e6f594..133587548 100644 --- a/Tools/SwiftGen/Templates/Strings/flat-swift4-vector.stencil +++ b/Tools/SwiftGen/Templates/Strings/flat-swift4-vector.stencil @@ -64,17 +64,20 @@ import Foundation extension {{className}} { static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { - let format = NSLocalizedString(key, tableName: table, bundle: Bundle.app, comment: "") - let locale: Locale - - if let providedLocale = LocaleProvider.locale { - locale = providedLocale - } else { - locale = Locale.current - } - - return String(format: format, locale: locale, arguments: args) + let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "") + let locale = LocaleProvider.locale ?? Locale.current + + return String(format: format, locale: locale, arguments: args) + } + /// The bundle to load strings from. This will be the app's bundle unless running + /// the UI tests target, in which case the strings are contained in the tests bundle. + static let bundle: Bundle = { + if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil { + // The tests bundle is embedded inside a runner. Find the bundle for VectorL10n. + return Bundle(for: VectorL10n.self) } + return Bundle.app + }() } {% else %} diff --git a/changelog.d/6046_uisi_context b/changelog.d/6046_uisi_context new file mode 100644 index 000000000..317480d92 --- /dev/null +++ b/changelog.d/6046_uisi_context @@ -0,0 +1 @@ +Analytics: Log decryption error details as context in AnalyticsEvent