diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 24e8ab2e6..68a169237 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -11,6 +11,7 @@ on: env: # Make the git branch for a PR available to our Fastfile MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + MapTilerAPIKey: ${{ secrets.MAPTILER_API_KEY }} jobs: build: diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 00d4fa2f9..3fcc19ed3 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -12,6 +12,7 @@ on: env: # Make the git branch for a PR available to our Fastfile MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + MapTilerAPIKey: ${{ secrets.MAPTILER_API_KEY }} jobs: tests: diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml index dfbd7ac6a..7ab8b331d 100644 --- a/.github/workflows/release-alpha.yml +++ b/.github/workflows/release-alpha.yml @@ -11,6 +11,7 @@ on: env: # Make the git branch for a PR available to our Fastfile MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + MapTilerAPIKey: ${{ secrets.MAPTILER_API_KEY }} jobs: build: diff --git a/CHANGES.md b/CHANGES.md index e530b5806..8c2112418 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,15 @@ +## Changes in 1.6.12 (2022-01-11) + +🙌 Improvements + +- Upgrade MatrixSDK version ([v0.20.16](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.20.16)). +- Analytics: Replace Matomo with PostHog. ([#5035](https://github.com/vector-im/element-ios/issues/5035)) + +🐛 Bugfixes + +- RoomVC: Fix left room reason label memory management. ([#5311](https://github.com/vector-im/element-ios/issues/5311)) + + ## Changes in 1.6.11 (2021-12-14) ✨ Features diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index febe520e8..b213c8623 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.6.11 -CURRENT_PROJECT_VERSION = 1.6.11 +MARKETING_VERSION = 1.6.12 +CURRENT_PROJECT_VERSION = 1.6.12 diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 525870b51..ccdafb43f 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -15,6 +15,7 @@ // import Foundation +import Keys /// BuildSettings provides settings computed at build time. /// In future, it may be automatically generated from xcconfig files @@ -22,13 +23,6 @@ import Foundation final class BuildSettings: NSObject { // MARK: - Bundle Settings - static var bundleDisplayName: String { - guard let bundleDisplayName = Bundle.app.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String else { - fatalError("CFBundleDisplayName should be defined") - } - return bundleDisplayName - } - static var applicationGroupIdentifier: String { guard let applicationGroupIdentifier = Bundle.app.object(forInfoDictionaryKey: "applicationGroupIdentifier") as? String else { fatalError("applicationGroupIdentifier should be defined") @@ -165,8 +159,20 @@ final class BuildSettings: NSObject { static let roomsAllowToJoinPublicRooms: Bool = true // MARK: - Analytics - static let analyticsServerUrl = URL(string: "https://piwik.riot.im/piwik.php") - static let analyticsAppId = "14" + #if DEBUG + /// Host to use for PostHog analytics during development. Set to nil to disable analytics in debug builds. + static let analyticsHost: String? = "https://posthog-poc.lab.element.dev" + /// Public key for submitting analytics during development. Set to nil to disable analytics in debug builds. + static let analyticsKey: String? = "rs-pJjsYJTuAkXJfhaMmPUNBhWliDyTKLOOxike6ck8" + #else + /// Host to use for PostHog analytics. Set to nil to disable analytics. + static let analyticsHost: String? = "https://posthog.hss.element.io" + /// Public key for submitting analytics. Set to nil to disable analytics. + static let analyticsKey: String? = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO" + #endif + + /// The URL to open with more information about analytics terms. + static let analyticsTermsURL = URL(string: "https://element.io/cookie-policy")! // MARK: - Bug report @@ -352,4 +358,16 @@ final class BuildSettings: NSObject { return true } + + // MARK: - Location Sharing + + static let tileServerMapURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=" + RiotKeys().mapTilerAPIKey)! + + static var locationSharingEnabled: Bool { + guard #available(iOS 14, *) else { + return false + } + + return false + } } diff --git a/Gemfile b/Gemfile index 53efbaf92..ea061a17e 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,7 @@ source "https://rubygems.org" gem "xcode-install" gem "fastlane" gem "cocoapods", '~>1.11.2' +gem "cocoapods-keys" plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index a8674758c..78fe028a3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,12 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.4) + CFPropertyList (3.0.5) rexml - activesupport (6.1.4.1) + RubyInline (3.12.5) + ZenTest (~> 4.3) + ZenTest (4.12.0) + activesupport (6.1.4.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -17,17 +20,17 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.510.0) - aws-sdk-core (3.121.1) + aws-partitions (1.541.0) + aws-sdk-core (3.124.0) aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.239.0) + aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.48.0) - aws-sdk-core (~> 3, >= 3.120.0) + aws-sdk-kms (1.52.0) + aws-sdk-core (~> 3, >= 3.122.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.103.0) - aws-sdk-core (~> 3, >= 3.120.0) + aws-sdk-s3 (1.109.0) + aws-sdk-core (~> 3, >= 3.122.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.4.0) @@ -64,6 +67,9 @@ GEM typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) cocoapods-downloader (1.5.1) + cocoapods-keys (2.2.1) + dotenv + osx_keychain cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -84,9 +90,9 @@ GEM dotenv (2.7.6) emoji_regex (3.2.3) escape (0.0.4) - ethon (0.14.0) + ethon (0.15.0) ffi (>= 1.15.0) - excon (0.86.0) + excon (0.89.0) faraday (1.8.0) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -109,10 +115,10 @@ GEM faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - faraday_middleware (1.1.0) + faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.5) - fastlane (2.195.0) + fastimage (2.2.6) + fastlane (2.199.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -161,7 +167,7 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.11.0) + google-apis-androidpublisher_v3 (0.14.0) google-apis-core (>= 0.4, < 2.a) google-apis-core (0.4.1) addressable (~> 2.5, >= 2.5.1) @@ -172,11 +178,11 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.7.0) + google-apis-iamcredentials_v1 (0.9.0) google-apis-core (>= 0.4, < 2.a) - google-apis-playcustomapp_v1 (0.5.0) + google-apis-playcustomapp_v1 (0.6.0) google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.8.0) + google-apis-storage_v1 (0.10.0) google-apis-core (>= 0.4, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) @@ -184,15 +190,15 @@ GEM google-cloud-env (1.5.0) faraday (>= 0.17.3, < 2.0) google-cloud-errors (1.2.0) - google-cloud-storage (1.34.1) - addressable (~> 2.5) + google-cloud-storage (1.35.0) + addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) google-apis-storage_v1 (~> 0.1) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.0.0) + googleauth (1.1.0) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -204,18 +210,18 @@ GEM http-cookie (1.0.4) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.8.10) + i18n (1.8.11) concurrent-ruby (~> 1.0) jmespath (1.4.0) - json (2.5.1) + json (2.6.1) jwt (2.3.0) memoist (0.16.2) - mime-types (3.3.1) + mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2021.0901) + mime-types-data (3.2021.1115) mini_magick (4.11.0) - mini_mime (1.1.1) - minitest (5.14.4) + mini_mime (1.1.2) + minitest (5.15.0) molinillo (0.8.0) multi_json (1.15.0) multipart-post (2.0.0) @@ -224,7 +230,9 @@ GEM naturally (2.2.1) netrc (0.11.0) optparse (0.1.1) - os (1.1.1) + os (1.1.4) + osx_keychain (1.0.2) + RubyInline (~> 3) plist (3.6.0) public_suffix (4.0.6) rake (13.0.6) @@ -255,7 +263,7 @@ GEM terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) - trailblazer-option (0.1.1) + trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.1) tty-spinner (0.9.3) @@ -285,13 +293,14 @@ GEM rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) - zeitwerk (2.4.2) + zeitwerk (2.5.1) PLATFORMS ruby DEPENDENCIES cocoapods (~> 1.11.2) + cocoapods-keys fastlane fastlane-plugin-diawi fastlane-plugin-versioning diff --git a/Podfile b/Podfile index 92a7bdad0..6a5620b94 100644 --- a/Podfile +++ b/Podfile @@ -3,7 +3,7 @@ source 'https://cdn.cocoapods.org/' # Uncomment this line to define a global platform for your project platform :ios, '12.1' -# Use frameforks to allow usage of pod written in Swift (like PiwikTracker) +# Use frameworks to allow usage of pods written in Swift use_frameworks! # Different flavours of pods to MatrixSDK. Can be one of: @@ -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.20.15' +$matrixSDKVersion = '= 0.20.16' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } @@ -67,8 +67,10 @@ abstract_target 'RiotPods' do pod 'KeychainAccess', '~> 4.2.2' pod 'WeakDictionary', '~> 2.0' - # Piwik for analytics - pod 'MatomoTracker', '~> 7.4.1' + # PostHog for analytics + pod 'PostHog', '~> 1.4.4' + pod 'AnalyticsEvents', :git => 'https://github.com/matrix-org/matrix-analytics-events.git', :branch => 'release/swift' + # pod 'AnalyticsEvents', :path => '../matrix-analytics-events/AnalyticsEvents.podspec' # Remove warnings from "bad" pods pod 'OLMKit', :inhibit_warnings => true @@ -93,7 +95,7 @@ abstract_target 'RiotPods' do pod 'SwiftJWT', '~> 3.6.200' pod 'SideMenu', '~> 6.5' pod 'DSWaveformImage', '~> 6.1.1' - pod 'ffmpeg-kit-ios-audio', '~> 4.5' + pod 'ffmpeg-kit-ios-audio', '4.5.1' pod 'FLEX', '~> 4.5.0', :configurations => ['Debug'] @@ -127,6 +129,10 @@ abstract_target 'RiotPods' do end +plugin 'cocoapods-keys', { + :project => "Riot", + :keys => ["MapTilerAPIKey"] +} post_install do |installer| installer.pods_project.targets.each do |target| diff --git a/Podfile.lock b/Podfile.lock index 3f62ea753..749fe065f 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -14,6 +14,7 @@ PODS: - AFNetworking/Serialization (4.0.1) - AFNetworking/UIKit (4.0.1): - AFNetworking/NSURLSession + - AnalyticsEvents (0.1.0) - BlueCryptor (1.0.32) - BlueECC (1.2.5) - BlueRSA (1.0.200) @@ -37,7 +38,7 @@ PODS: - DTFoundation/Core - DTFoundation/UIKit (1.7.18): - DTFoundation/Core - - ffmpeg-kit-ios-audio (4.5) + - ffmpeg-kit-ios-audio (4.5.1) - FLEX (4.5.0) - FlowCommoniOS (1.12.2) - GBDeviceInfo (6.6.0): @@ -48,6 +49,7 @@ PODS: - Introspect (0.1.3) - JitsiMeetSDK (3.10.2) - KeychainAccess (4.2.2) + - Keys (1.0.1) - KituraContracts (1.2.1): - LoggerAPI (~> 1.7) - KTCenterFlowLayout (1.3.1) @@ -56,19 +58,16 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatomoTracker (7.4.1): - - MatomoTracker/Core (= 7.4.1) - - MatomoTracker/Core (7.4.1) - - MatrixSDK (0.20.15): - - MatrixSDK/Core (= 0.20.15) - - MatrixSDK/Core (0.20.15): + - MatrixSDK (0.20.16): + - MatrixSDK/Core (= 0.20.16) + - MatrixSDK/Core (0.20.16): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) - OLMKit (~> 3.2.5) - Realm (= 10.16.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.20.15): + - MatrixSDK/JingleCallStack (0.20.16): - JitsiMeetSDK (= 3.10.2) - MatrixSDK/Core - OLMKit (3.2.5): @@ -76,6 +75,7 @@ PODS: - OLMKit/olmcpp (= 3.2.5) - OLMKit/olmc (3.2.5) - OLMKit/olmcpp (3.2.5) + - PostHog (1.4.4) - ReadMoreTextView (3.0.1) - Realm (10.16.0): - Realm/Headers (= 10.16.0) @@ -103,23 +103,25 @@ PODS: - ZXingObjC/All (3.6.5) DEPENDENCIES: + - AnalyticsEvents (from `https://github.com/matrix-org/matrix-analytics-events.git`, branch `release/swift`) - DGCollectionViewLeftAlignFlowLayout (~> 1.0.4) - Down (~> 0.11.0) - DSWaveformImage (~> 6.1.1) - DTCoreText (~> 1.6.25) - - ffmpeg-kit-ios-audio (~> 4.5) + - ffmpeg-kit-ios-audio (= 4.5.1) - FLEX (~> 4.5.0) - FlowCommoniOS (~> 1.12.0) - GBDeviceInfo (~> 6.6.0) - HPGrowingTextView (~> 1.1) - Introspect (~> 0.1) - KeychainAccess (~> 4.2.2) + - Keys (from `Pods/CocoaPodsKeys`) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatomoTracker (~> 7.4.1) - - MatrixSDK (= 0.20.15) - - MatrixSDK/JingleCallStack (= 0.20.15) + - MatrixSDK (= 0.20.16) + - MatrixSDK/JingleCallStack (= 0.20.16) - OLMKit + - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) - Reusable (~> 4.1) - SideMenu (~> 6.5) @@ -157,9 +159,9 @@ SPEC REPOS: - libPhoneNumber-iOS - LoggerAPI - Logging - - MatomoTracker - MatrixSDK - OLMKit + - PostHog - ReadMoreTextView - Realm - Reusable @@ -173,8 +175,21 @@ SPEC REPOS: - zxcvbn-ios - ZXingObjC +EXTERNAL SOURCES: + AnalyticsEvents: + :branch: release/swift + :git: https://github.com/matrix-org/matrix-analytics-events.git + Keys: + :path: Pods/CocoaPodsKeys + +CHECKOUT OPTIONS: + AnalyticsEvents: + :commit: f1805ad7c3fafa7fd9c6e2eaa9e0165f8142ecd2 + :git: https://github.com/matrix-org/matrix-analytics-events.git + SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce + AnalyticsEvents: 333bf47d67dc628fadd29ce887b7ac93d8bd6e05 BlueCryptor: b0aee3d9b8f367b49b30de11cda90e1735571c24 BlueECC: 0d18e93347d3ec6d41416de21c1ffa4d4cd3c2cc BlueRSA: dfeef51db96bcc4edec654956c1581adbda4e6a3 @@ -183,7 +198,7 @@ SPEC CHECKSUMS: DSWaveformImage: 3c718a0cf99291887ee70d1d0c18d80101d3d9ce DTCoreText: ec749e013f2e1f76de5e7c7634642e600a7467ce DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536 - ffmpeg-kit-ios-audio: 8c44d93054e1a9743a7014ec3dd26cd1ad8f2a59 + ffmpeg-kit-ios-audio: 662ce2064e56733ca7d8216705efbc38d9e1c3fe FLEX: e51461dd6f0bfb00643c262acdfea5d5d12c596b FlowCommoniOS: ca92071ab526dc89905495a37844fd7e78d1a7f2 GBDeviceInfo: ed0db16230d2fa280e1cbb39a5a7f60f6946aaec @@ -192,15 +207,16 @@ SPEC CHECKSUMS: Introspect: 2be020f30f084ada52bb4387fff83fa52c5c400e JitsiMeetSDK: 2f118fa770f23e518f3560fc224fae3ac7062223 KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51 + Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 KituraContracts: e845e60dc8627ad0a76fa55ef20a45451d8f830b KTCenterFlowLayout: 6e02b50ab2bd865025ae82fe266ed13b6d9eaf97 libbase58: 7c040313537b8c44b6e2d15586af8e21f7354efd libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatomoTracker: 24a846c9d3aa76933183fe9d47fd62c9efa863fb - MatrixSDK: 2f4d3aacb1c53e2785f0be71d24b8e62e5c5c056 + MatrixSDK: af6a70532bb43af59f43a1f4dae512a26afeab0b OLMKit: 9fb4799c4a044dd2c06bda31ec31a12191ad30b5 + PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d Realm: b6027801398f3743fc222f096faa85281b506e6c Reusable: 6bae6a5e8aa793c9c441db0213c863a64bce9136 @@ -214,6 +230,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 989bcc8b1857dc64a9b810ddaf4446903adbe162 +PODFILE CHECKSUM: 2493587902f8f28bb2638303dd583c47e9f24d8b COCOAPODS: 1.11.2 diff --git a/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsCheckmark.imageset/AnalyticsTick.pdf b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsCheckmark.imageset/AnalyticsTick.pdf new file mode 100644 index 000000000..9696208e3 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsCheckmark.imageset/AnalyticsTick.pdf @@ -0,0 +1,123 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 1.000000 -1.000000 cm +0.049479 0.742188 0.545395 scn +10.000000 23.000000 m +3.947715 23.000000 -1.000000 18.052284 -1.000000 12.000000 c +1.000000 12.000000 l +1.000000 16.947716 5.052285 21.000000 10.000000 21.000000 c +10.000000 23.000000 l +h +-1.000000 12.000000 m +-1.000000 5.947716 3.947715 1.000000 10.000000 1.000000 c +10.000000 3.000000 l +5.052285 3.000000 1.000000 7.052285 1.000000 12.000000 c +-1.000000 12.000000 l +h +10.000000 1.000000 m +16.052284 1.000000 21.000000 5.947716 21.000000 12.000000 c +19.000000 12.000000 l +19.000000 7.052285 14.947715 3.000000 10.000000 3.000000 c +10.000000 1.000000 l +h +21.000000 12.000000 m +21.000000 18.052284 16.052284 23.000000 10.000000 23.000000 c +10.000000 21.000000 l +14.947715 21.000000 19.000000 16.947716 19.000000 12.000000 c +21.000000 12.000000 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 5.545532 4.655060 cm +0.049479 0.742188 0.545395 scn +0.717378 6.153159 m +0.332610 6.549356 -0.300487 6.558620 -0.696684 6.173852 c +-1.092881 5.789084 -1.102146 5.155987 -0.717378 4.759790 c +0.717378 6.153159 l +h +3.257576 2.102139 m +2.540198 1.405455 l +2.728505 1.211555 2.987285 1.102139 3.257576 1.102139 c +3.527867 1.102139 3.786646 1.211555 3.974954 1.405455 c +3.257576 2.102139 l +h +11.626469 9.284243 m +12.011237 9.680439 12.001972 10.313537 11.605776 10.698304 c +11.209579 11.083073 10.576482 11.073808 10.191713 10.677610 c +11.626469 9.284243 l +h +-0.717378 4.759790 m +2.540198 1.405455 l +3.974954 2.798823 l +0.717378 6.153159 l +-0.717378 4.759790 l +h +3.974954 1.405455 m +11.626469 9.284243 l +10.191713 10.677610 l +2.540198 2.798823 l +3.974954 1.405455 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 1679 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 22.000000 22.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001769 00000 n +0000001792 00000 n +0000001965 00000 n +0000002039 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2098 +%%EOF \ No newline at end of file diff --git a/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsCheckmark.imageset/Contents.json b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsCheckmark.imageset/Contents.json new file mode 100644 index 000000000..146a290ce --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsCheckmark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "AnalyticsTick.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsLogo.imageset/AnalyticsLogo.pdf b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsLogo.imageset/AnalyticsLogo.pdf new file mode 100644 index 000000000..096c22d4b --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsLogo.imageset/AnalyticsLogo.pdf @@ -0,0 +1,641 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << /ExtGState << /E2 << /ca 0.400000 >> + /E1 << /ca 0.400000 >> + >> >> + /BBox [ 0.000000 0.000000 119.000000 93.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 14.875000 -0.000015 cm +0.049479 0.742188 0.545395 scn +0.000000 44.521278 m +0.000000 69.109695 20.036579 89.042557 44.625000 89.042557 c +44.625000 89.042557 l +69.213417 89.042557 89.250000 69.109695 89.250000 44.521278 c +89.250000 44.521278 l +89.250000 19.932861 69.213417 0.000000 44.625000 0.000000 c +44.625000 0.000000 l +20.036579 0.000000 0.000000 19.932861 0.000000 44.521278 c +0.000000 44.521278 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 52.227402 46.594299 cm +1.000000 1.000000 1.000000 scn +0.000000 20.730219 m +0.000000 22.447567 1.395428 23.839752 3.116776 23.839752 c +14.592426 23.839752 23.895279 14.558517 23.895279 3.109533 c +23.895279 1.392185 22.499851 0.000000 20.778503 0.000000 c +19.057156 0.000000 17.661728 1.392185 17.661728 3.109533 c +17.661728 11.123821 11.149731 17.620686 3.116776 17.620686 c +1.395428 17.620686 0.000000 19.012871 0.000000 20.730219 c +h +f* +n +Q +q +-1.000000 0.000000 -0.000000 -1.000000 66.772202 42.448242 cm +1.000000 1.000000 1.000000 scn +0.000000 20.730206 m +0.000000 22.447552 1.395429 23.839737 3.116779 23.839737 c +14.592443 23.839737 23.895306 14.558502 23.895306 3.109520 c +23.895306 1.392172 22.499878 -0.000011 20.778526 -0.000011 c +19.057177 -0.000011 17.661749 1.392172 17.661749 3.109520 c +17.661749 11.123808 11.149744 17.620672 3.116779 17.620672 c +1.395429 17.620672 0.000000 19.012857 0.000000 20.730206 c +h +f* +n +Q +q +-0.000000 1.000000 -1.000000 -0.000000 57.366512 37.265598 cm +1.000000 1.000000 1.000000 scn +0.000000 20.722975 m +0.000000 22.444323 1.392186 23.839752 3.109534 23.839752 c +14.558520 23.839752 23.839758 14.536892 23.839758 3.061234 c +23.839758 1.339886 22.447573 -0.055544 20.730225 -0.055544 c +19.012875 -0.055544 17.620689 1.339886 17.620689 3.061234 c +17.620689 11.094195 11.123824 17.606197 3.109534 17.606197 c +1.392186 17.606197 0.000000 19.001625 0.000000 20.722975 c +h +f* +n +Q +q +-0.000000 -1.000000 1.000000 -0.000000 61.633194 51.777008 cm +1.000000 1.000000 1.000000 scn +0.000000 20.722975 m +0.000000 22.444324 1.392186 23.839752 3.109534 23.839752 c +14.558520 23.839752 23.839758 14.536893 23.839758 3.061237 c +23.839758 1.339888 22.447573 -0.055540 20.730225 -0.055540 c +19.012875 -0.055540 17.620689 1.339888 17.620689 3.061237 c +17.620689 11.094196 11.123824 17.606197 3.109534 17.606197 c +1.392186 17.606197 0.000000 19.001627 0.000000 20.722975 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 28.123089 16.695465 cm +1.000000 1.000000 1.000000 scn +10.448288 0.000008 m +11.057861 0.000008 11.560491 0.448122 11.646045 1.077618 c +12.576446 7.617959 13.464069 8.514189 19.795069 9.229038 c +20.436726 9.303724 20.917965 9.826525 20.917965 10.434681 c +20.917965 11.053506 20.447418 11.554968 19.805763 11.640323 c +13.506846 12.461866 12.694082 13.262072 11.646045 19.802414 c +11.539103 20.431910 11.057861 20.869354 10.448288 20.869354 c +9.849410 20.869354 9.346780 20.431910 9.250531 19.791744 c +8.330826 13.251402 7.443202 12.355172 1.112203 11.640323 c +0.470547 11.565637 0.000000 11.053506 0.000000 10.434681 c +0.000000 9.826525 0.459853 9.314394 1.112203 9.229038 c +7.411120 8.354148 8.191800 7.607290 9.250531 1.066948 c +9.368169 0.437452 9.860105 0.000008 10.448288 0.000008 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 28.123089 15.195465 cm +0.049479 0.742188 0.545395 scn +11.646045 2.577618 m +10.903506 2.683247 l +10.902877 2.678619 l +11.646045 2.577618 l +h +19.795069 10.729038 m +19.879219 9.983770 l +19.881781 9.984068 l +19.795069 10.729038 l +h +19.805763 13.140323 m +19.904661 13.883777 l +19.902761 13.884024 l +19.805763 13.140323 l +h +11.646045 21.302414 m +12.386630 21.421087 l +12.385450 21.428030 l +11.646045 21.302414 l +h +9.250531 21.291744 m +8.508834 21.403259 l +8.507838 21.396183 l +9.250531 21.291744 l +h +1.112203 13.140323 m +1.028052 13.885592 l +1.025491 13.885293 l +1.112203 13.140323 l +h +1.112203 10.729038 m +1.215387 11.471931 l +1.209505 11.472700 l +1.112203 10.729038 l +h +9.250531 2.566948 m +8.510169 2.447100 l +8.511623 2.438120 l +8.513294 2.429176 l +9.250531 2.566948 l +h +10.448288 0.750008 m +11.450765 0.750008 12.255557 1.493191 12.389213 2.476614 c +10.902877 2.678619 l +10.865425 2.403053 10.664957 2.250008 10.448288 2.250008 c +10.448288 0.750008 l +h +12.388570 2.471989 m +12.620602 4.103085 12.844038 5.334888 13.138243 6.288948 c +13.429995 7.235054 13.776924 7.858273 14.225985 8.307880 c +15.140809 9.223818 16.666159 9.620979 19.879219 9.983774 c +19.710920 11.474303 l +16.592981 11.122249 14.509018 10.713870 13.164680 9.367895 c +12.484159 8.686545 12.039045 7.814714 11.704848 6.730967 c +11.373105 5.655174 11.136688 4.322321 10.903521 2.683245 c +12.388570 2.471989 l +h +19.881781 9.984068 m +20.873766 10.099530 21.667965 10.918526 21.667965 11.934681 c +20.167965 11.934681 l +20.167965 11.734525 19.999683 11.507918 19.708359 11.474010 c +19.881781 9.984068 l +h +21.667965 11.934681 m +21.667965 12.966555 20.881077 13.753887 19.904659 13.883774 c +19.706867 12.396872 l +20.013762 12.356048 20.167965 12.140457 20.167965 11.934681 c +21.667965 11.934681 l +h +19.902761 13.884024 m +18.330524 14.089085 17.151228 14.287123 16.235826 14.561028 c +15.331247 14.831694 14.731587 15.163220 14.286853 15.607450 c +13.840208 16.053589 13.492528 16.670565 13.191763 17.611713 c +12.888441 18.560867 12.648228 19.788363 12.386598 21.421082 c +10.905493 21.183746 l +11.167881 19.546295 11.422276 18.221136 11.762950 17.155106 c +12.106180 16.081072 12.551881 15.220337 13.226795 14.546188 c +13.903622 13.870131 14.753702 13.438795 15.805837 13.123979 c +16.847151 12.812399 18.131544 12.602333 19.708765 12.396622 c +19.902761 13.884024 l +h +12.385450 21.428030 m +12.223795 22.379583 11.461336 23.119354 10.448288 23.119354 c +10.448288 21.619354 l +10.654386 21.619354 10.854410 21.484234 10.906639 21.176800 c +12.385450 21.428030 l +h +10.448288 23.119354 m +9.455997 23.119354 8.656921 22.387987 8.508867 21.403254 c +9.992196 21.180237 l +10.036638 21.475830 10.242823 21.619354 10.448288 21.619354 c +10.448288 23.119354 l +h +8.507838 21.396183 m +8.278477 19.765110 8.056973 18.533388 7.764218 17.579443 c +7.473907 16.633463 7.127907 16.010515 6.679636 15.561167 c +5.766263 14.645599 4.241331 14.248406 1.028053 13.885587 c +1.196352 12.395059 l +4.314074 12.747088 6.398453 13.155437 7.741569 14.501781 c +8.421542 15.183390 8.865580 16.055490 9.198210 17.139366 c +9.528394 18.215273 9.762733 19.548208 9.993224 21.187307 c +8.507838 21.396183 l +h +1.025491 13.885293 m +0.028613 13.769261 -0.750000 12.956783 -0.750000 11.934681 c +0.750000 11.934681 l +0.750000 12.150229 0.912482 12.362013 1.198914 12.395352 c +1.025491 13.885293 l +h +-0.750000 11.934681 m +-0.750000 10.922595 0.016880 10.115961 1.014900 9.985377 c +1.209505 11.472700 l +0.902826 11.512827 0.750000 11.730454 0.750000 11.934681 c +-0.750000 11.934681 l +h +1.009022 9.986170 m +2.582546 9.767614 3.760892 9.563175 4.675031 9.286510 c +5.578484 9.013078 6.174878 8.682938 6.616406 8.241908 c +7.059544 7.799271 7.404263 7.187467 7.703608 6.250257 c +8.005574 5.304842 8.245770 4.080437 8.510169 2.447100 c +9.990894 2.686794 l +9.725928 4.323629 9.471515 5.645210 9.132493 6.706642 c +8.790851 7.776280 8.348207 8.632186 7.676467 9.303166 c +7.003119 9.975756 6.157125 10.405144 5.109545 10.722197 c +4.072651 11.036015 2.791318 11.253017 1.215384 11.471908 c +1.009022 9.986170 l +h +8.513294 2.429176 m +8.688872 1.489628 9.455215 0.750008 10.448288 0.750008 c +10.448288 2.250008 l +10.264993 2.250008 10.047464 2.385277 9.987769 2.704720 c +8.513294 2.429176 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 72.573807 58.608093 cm +1.000000 1.000000 1.000000 scn +8.619835 -0.000011 m +9.122732 -0.000011 9.537402 0.373419 9.607985 0.897997 c +10.375565 6.348283 11.107853 7.095141 16.330927 7.690849 c +16.860292 7.753087 17.257317 8.188755 17.257317 8.695551 c +17.257317 9.211239 16.869114 9.629124 16.339748 9.700253 c +11.143145 10.384872 10.472614 11.051710 9.607985 16.501997 c +9.519756 17.026575 9.122732 17.391113 8.619835 17.391113 c +8.125761 17.391113 7.711091 17.026575 7.631686 16.493105 c +6.872929 11.042820 6.140640 10.295961 0.917567 9.700253 c +0.388201 9.638015 0.000000 9.211239 0.000000 8.695551 c +0.000000 8.188755 0.379379 7.761978 0.917567 7.690849 c +6.114172 6.961774 6.758233 6.339392 7.631686 0.889107 c +7.728737 0.364527 8.134583 -0.000011 8.619835 -0.000011 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 72.573807 57.608093 cm +0.049479 0.742188 0.545395 scn +9.607985 1.897997 m +9.112861 1.967726 l +9.112450 1.964672 l +9.607985 1.897997 l +h +16.330927 8.690849 m +16.387587 8.194067 l +16.389311 8.194269 l +16.330927 8.690849 l +h +16.339748 10.700253 m +16.406334 11.195801 l +16.405056 11.195970 l +16.339748 10.700253 l +h +9.607985 17.501997 m +10.101830 17.580339 l +10.101059 17.584925 l +9.607985 17.501997 l +h +7.631686 17.493105 m +7.137113 17.566721 l +7.136462 17.562048 l +7.631686 17.493105 l +h +0.917567 10.700253 m +0.860907 11.197035 l +0.859183 11.196833 l +0.917567 10.700253 l +h +0.917567 8.690849 m +0.987038 9.186015 l +0.983079 9.186539 l +0.917567 8.690849 l +h +7.631686 1.889107 m +7.137843 1.809963 l +7.140029 1.798147 l +7.631686 1.889107 l +h +8.619835 0.499989 m +9.388696 0.499989 10.001672 1.074373 10.103518 1.831324 c +9.112450 1.964672 l +9.073133 1.672462 8.856770 1.499989 8.619835 1.499989 c +8.619835 0.499989 l +h +10.103099 1.828268 m +10.294624 3.188222 10.480069 4.223436 10.725984 5.028956 c +10.970345 5.829387 11.264832 6.369568 11.654185 6.763333 c +12.442908 7.560994 13.744701 7.892640 16.387587 8.194070 c +16.274267 9.187629 l +13.694078 8.893350 12.018190 8.553713 10.943106 7.466446 c +10.400556 6.917747 10.041607 6.212054 9.769561 5.320940 c +9.499068 4.434915 9.305134 3.332915 9.112870 1.967726 c +10.103099 1.828268 l +h +16.389311 8.194269 m +17.155991 8.284410 17.757317 8.920855 17.757317 9.695551 c +16.757317 9.695551 l +16.757317 9.456655 16.564592 9.221766 16.272543 9.187428 c +16.389311 8.194269 l +h +17.757317 9.695551 m +17.757317 10.482604 17.162529 11.094193 16.406334 11.195800 c +16.273165 10.204706 l +16.575699 10.164056 16.757317 9.939874 16.757317 9.695551 c +17.757317 9.695551 l +h +16.405056 11.195970 m +15.107601 11.366901 14.126670 11.532858 13.361839 11.764020 c +12.604449 11.992933 12.090017 12.277006 11.704502 12.665974 c +11.317406 13.056538 11.022324 13.591100 10.770527 14.386979 c +10.517109 15.187984 10.317721 16.219311 10.101810 17.580338 c +9.114160 17.423656 l +9.330563 16.059540 9.539227 14.963654 9.817105 14.085340 c +10.096603 13.201900 10.456060 12.505035 10.994250 11.962027 c +11.534022 11.417421 12.215625 11.065775 13.072525 10.806786 c +13.921983 10.550046 14.973595 10.375915 16.274443 10.204536 c +16.405056 11.195970 l +h +10.101059 17.584925 m +9.977288 18.320837 9.395552 18.891113 8.619835 18.891113 c +8.619835 17.891113 l +8.849913 17.891113 9.062225 17.732313 9.114909 17.419067 c +10.101059 17.584925 l +h +8.619835 18.891113 m +7.859531 18.891113 7.250215 18.326427 7.137135 17.566717 c +8.126238 17.419493 l +8.171968 17.726725 8.391990 17.891113 8.619835 17.891113 c +8.619835 18.891113 l +h +7.136462 17.562048 m +6.947139 16.202108 6.763302 15.166948 6.518592 14.361504 c +6.275430 13.561155 5.981721 13.021152 5.592998 12.627558 c +4.805452 11.830145 3.503936 11.498478 0.860908 11.197033 c +0.974226 10.203474 l +3.554271 10.497736 5.230436 10.837353 6.304493 11.924868 c +6.846570 12.473737 7.204643 13.179608 7.475406 14.070805 c +7.744622 14.956905 7.936855 16.058958 8.126910 17.424164 c +7.136462 17.562048 l +h +0.859183 11.196833 m +0.089259 11.106312 -0.500000 10.475979 -0.500000 9.695551 c +0.500000 9.695551 l +0.500000 9.946500 0.687143 10.169718 0.975950 10.203673 c +0.859183 11.196833 l +h +-0.500000 9.695551 m +-0.500000 8.923503 0.079786 8.297226 0.852054 8.195160 c +0.983079 9.186539 l +0.678971 9.226731 0.500000 9.454006 0.500000 9.695551 c +-0.500000 9.695551 l +h +0.848098 8.195699 m +2.146421 8.013546 3.126409 7.842214 3.889960 7.608789 c +4.646159 7.377612 5.157835 7.094736 5.540681 6.708459 c +5.924912 6.320785 6.217544 5.790504 6.468155 4.997945 c +6.720429 4.200129 6.919805 3.171420 7.137986 1.809986 c +8.125387 1.968225 l +7.906841 3.331934 7.698164 4.424881 7.421624 5.299438 c +7.143422 6.179253 6.786478 6.872063 6.250936 7.412404 c +5.714010 7.954142 5.035716 8.304206 4.182313 8.565100 c +3.336262 8.823746 2.287016 9.003614 0.987036 9.186000 c +0.848098 8.195699 l +h +7.140029 1.798147 m +7.274719 1.070122 7.860904 0.499989 8.619835 0.499989 c +8.619835 1.499989 l +8.408263 1.499989 8.182755 1.658932 8.123343 1.980066 c +7.140029 1.798147 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 107.227104 75.129669 cm +0.049479 0.742188 0.545395 scn +5.619252 -0.000010 m +5.947090 -0.000010 6.217412 0.238985 6.263425 0.574716 c +6.763808 4.062898 7.241186 4.540888 10.646096 4.922141 c +10.991189 4.961974 11.250008 5.240800 11.250008 5.565150 c +11.250008 5.895190 10.996941 6.162637 10.651848 6.208159 c +7.264192 6.646316 6.827075 7.073092 6.263425 10.561275 c +6.205909 10.897006 5.947090 11.130310 5.619252 11.130310 c +5.297166 11.130310 5.026845 10.897006 4.975080 10.555585 c +4.480448 7.067402 4.003070 6.589413 0.598160 6.208159 c +0.253068 6.168327 0.000000 5.895190 0.000000 5.565150 c +0.000000 5.240800 0.247316 4.967664 0.598160 4.922141 c +3.985816 4.455533 4.405678 4.057208 4.975080 0.569025 c +5.038347 0.233294 5.302918 -0.000010 5.619252 -0.000010 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 103.008408 84.868683 cm +0.049479 0.742188 0.545395 scn +3.160831 -0.000001 m +3.345240 -0.000001 3.497297 0.134433 3.523178 0.323282 c +3.804644 2.285384 4.073170 2.554254 5.988433 2.768708 c +6.182547 2.791114 6.328133 2.947954 6.328133 3.130401 c +6.328133 3.316049 6.185782 3.466487 5.991668 3.492094 c +4.086111 3.738557 3.840232 3.978619 3.523178 5.940721 c +3.490826 6.129570 3.345240 6.260803 3.160831 6.260803 c +2.979658 6.260803 2.827601 6.129570 2.798484 5.937521 c +2.520254 3.975418 2.251728 3.706549 0.336465 3.492094 c +0.142351 3.469688 0.000000 3.316049 0.000000 3.130401 c +0.000000 2.947954 0.139115 2.794315 0.336465 2.768708 c +2.242023 2.506241 2.478195 2.282184 2.798484 0.320081 c +2.834072 0.131233 2.982893 -0.000001 3.160831 -0.000001 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 103.711479 69.564484 cm +0.049479 0.742188 0.545395 scn +3.863233 0.000004 m +4.088622 0.000004 4.274468 0.164312 4.306101 0.395127 c +4.650115 2.793253 4.978312 3.121871 7.319186 3.383983 c +7.556437 3.411368 7.734375 3.603061 7.734375 3.826052 c +7.734375 4.052955 7.560391 4.236824 7.323141 4.268121 c +4.994129 4.569354 4.693611 4.862762 4.306101 7.260888 c +4.266560 7.491703 4.088622 7.652100 3.863233 7.652100 c +3.641799 7.652100 3.455953 7.491703 3.420365 7.256976 c +3.080306 4.858850 2.752109 4.530232 0.411235 4.268121 c +0.173984 4.240736 0.000000 4.052955 0.000000 3.826052 c +0.000000 3.603061 0.170030 3.415280 0.411235 3.383983 c +2.740246 3.063190 3.028902 2.789341 3.420365 0.391215 c +3.463861 0.160400 3.645753 0.000004 3.863233 0.000004 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 -0.070938 78.607880 cm +0.049479 0.742188 0.545395 scn +5.268045 -0.000012 m +5.575393 -0.000012 5.828820 0.224045 5.871957 0.538792 c +6.341066 3.808963 6.788608 4.257079 9.980709 4.614503 c +10.304233 4.651846 10.546875 4.913247 10.546875 5.217325 c +10.546875 5.526737 10.309625 5.777468 9.986101 5.820146 c +6.810176 6.230918 6.400379 6.631021 5.871957 9.901192 c +5.818036 10.215940 5.575393 10.434662 5.268045 10.434662 c +4.966090 10.434662 4.712663 10.215940 4.664135 9.895857 c +4.200418 6.625686 3.752876 6.177571 0.560775 5.820146 c +0.237251 5.782803 0.000000 5.526737 0.000000 5.217325 c +0.000000 4.913247 0.231859 4.657181 0.560775 4.614503 c +3.736700 4.177058 4.130321 3.803629 4.664135 0.533457 c +4.723447 0.218710 4.971482 -0.000012 5.268045 -0.000012 c +h +f +n +Q +q +/E1 gs +1.000000 0.000000 -0.000000 1.000000 9.772858 88.346924 cm +0.049479 0.742188 0.545395 scn +2.458421 -0.000008 m +2.601850 -0.000008 2.720115 0.104552 2.740246 0.251434 c +2.959164 1.777514 3.168016 1.986634 4.657663 2.153433 c +4.808641 2.170859 4.921874 2.292846 4.921874 2.434749 c +4.921874 2.579142 4.811157 2.696150 4.660179 2.716066 c +3.178081 2.907759 2.986843 3.094474 2.740246 4.620554 c +2.715083 4.767436 2.601850 4.869507 2.458421 4.869507 c +2.317508 4.869507 2.199242 4.767436 2.176596 4.618064 c +1.960194 3.091985 1.751342 2.882864 0.261695 2.716066 c +0.110717 2.698639 0.000000 2.579142 0.000000 2.434749 c +0.000000 2.292846 0.108201 2.173349 0.261695 2.153433 c +1.743793 1.949291 1.927482 1.775024 2.176596 0.248944 c +2.204275 0.102062 2.320024 -0.000008 2.458421 -0.000008 c +h +f +n +Q +q +/E2 gs +1.000000 0.000000 -0.000000 1.000000 13.288479 82.086121 cm +0.049479 0.742188 0.545395 scn +2.458421 -0.000008 m +2.601850 -0.000008 2.720116 0.104552 2.740247 0.251434 c +2.959164 1.777514 3.168017 1.986634 4.657664 2.153433 c +4.808642 2.170859 4.921875 2.292846 4.921875 2.434749 c +4.921875 2.579142 4.811158 2.696150 4.660181 2.716066 c +3.178082 2.907759 2.986844 3.094474 2.740247 4.620554 c +2.715084 4.767436 2.601850 4.869507 2.458421 4.869507 c +2.317509 4.869507 2.199243 4.767436 2.176596 4.618064 c +1.960195 3.091985 1.751342 2.882864 0.261695 2.716066 c +0.110717 2.698639 0.000000 2.579142 0.000000 2.434749 c +0.000000 2.292846 0.108201 2.173349 0.261695 2.153433 c +1.743793 1.949291 1.927483 1.775024 2.176596 0.248944 c +2.204276 0.102062 2.320025 -0.000008 2.458421 -0.000008 c +h +f +n +Q + +endstream +endobj + +2 0 obj + 17245 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 119.000000 93.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 93.000000 m +119.000000 93.000000 l +119.000000 0.000000 l +0.000000 0.000000 l +0.000000 93.000000 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 234 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 46 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 119.000000 93.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000017630 00000 n +0000017654 00000 n +0000018137 00000 n +0000018159 00000 n +0000018457 00000 n +0000018559 00000 n +0000018580 00000 n +0000018754 00000 n +0000018828 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +18888 +%%EOF \ No newline at end of file diff --git a/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsLogo.imageset/Contents.json b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsLogo.imageset/Contents.json new file mode 100644 index 000000000..7d49fc335 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/Analytics/AnalyticsLogo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "AnalyticsLogo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Riot/Assets/Images.xcassets/Authentication/Analytics/Contents.json b/Riot/Assets/Images.xcassets/Authentication/Analytics/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Authentication/Analytics/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/Contents.json new file mode 100644 index 000000000..5bb98bf57 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "action_location.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "action_location@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "action_location@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location.png b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location.png new file mode 100644 index 000000000..be968b77d Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@2x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@2x.png new file mode 100644 index 000000000..b77c9e11b Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@3x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@3x.png new file mode 100644 index 000000000..8ce3cacae Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/action_location@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/Contents.json new file mode 100644 index 000000000..707b2f06b --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "location_marker_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_marker_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_marker_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon.png b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon.png new file mode 100644 index 000000000..be968b77d Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@2x.png new file mode 100644 index 000000000..b77c9e11b Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@3x.png new file mode 100644 index 000000000..8ce3cacae Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_marker_icon.imageset/location_marker_icon@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/Contents.json new file mode 100644 index 000000000..c3b8dbc7a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "location_share_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_share_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_share_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon.png b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon.png new file mode 100644 index 000000000..da7d7530b Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@2x.png new file mode 100644 index 000000000..41969eb70 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@3x.png new file mode 100644 index 000000000..43bf9dcb3 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_share_icon.imageset/location_share_icon@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/Contents.json new file mode 100644 index 000000000..175219374 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "location_user_marker.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_user_marker@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_user_marker@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} 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 new file mode 100644 index 000000000..eedadad5e Binary files /dev/null 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 new file mode 100644 index 000000000..fce4dcb04 Binary files /dev/null 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 new file mode 100644 index 000000000..ff8ce3aad Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@3x.png differ diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 63ea02f51..f0fb19fc2 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -55,7 +55,7 @@ "auth_missing_phone" = "Fehlende Telefon-Nummer"; "auth_missing_email_or_phone" = "Fehlende E-Mail-Adresse oder Telefon-Nummer"; "auth_password_dont_match" = "Passwörter stimmen nicht überein"; -"auth_username_in_use" = "Nutzername bereits verwendet"; +"auth_username_in_use" = "Benutzername bereits verwendet"; "auth_forgot_password" = "Passwort vergessen?"; "auth_msisdn_validation_title" = "Verifizierung ausstehend"; "auth_msisdn_validation_message" = "Bitte gib unten den Aktivierungs-Code ein, den wir per SMS verschickt haben."; @@ -114,10 +114,10 @@ "contacts_address_book_no_contact" = "Keine lokalen Kontakte"; "contacts_address_book_permission_required" = "Berechtigungen benötigt um auf lokale Kontakte zuzugreifen"; // Chat participants -"room_participants_title" = "Teilnehmer"; +"room_participants_title" = "Teilnehmende"; "room_participants_add_participant" = "Teilnehmer hinzufügen"; "room_participants_one_participant" = "1 Teilnehmer"; -"room_participants_multi_participants" = "%d Teilnehmer"; +"room_participants_multi_participants" = "%d Teilnehmende"; "room_participants_leave_prompt_title" = "Raum verlassen"; "room_participants_leave_prompt_msg" = "Bist du sicher, dass du den Raum verlassen willst?"; "room_participants_remove_prompt_title" = "Bestätigung"; @@ -140,7 +140,7 @@ "room_participants_action_ban" = "Aus diesem Raum bannen"; "room_participants_action_ignore" = "Alle Nachrichten von diesem Nutzer verbergen"; "room_participants_action_unignore" = "Zeige alle Nachrichten von diesem Nutzer"; -"room_participants_action_set_moderator" = "Mache zu Moderator"; +"room_participants_action_set_moderator" = "Gib Moderationsrechte"; "room_participants_action_set_admin" = "Mache zum Administrator"; "room_participants_action_start_new_chat" = "Starte neuen Chat"; "room_participants_action_start_video_call" = "Starte Video-Anruf"; @@ -206,9 +206,9 @@ "settings_user_settings" = "NUTZER-EINSTELLUNGEN"; "settings_notifications_settings" = "BENACHRICHTIGUNGS-EINSTELLUNGEN"; "settings_ignored_users" = "IGNORIERTE NUTZER"; -"settings_contacts" = "LOKALE KONTAKTE"; +"settings_contacts" = "GERÄTEKONTAKTE"; "settings_advanced" = "ERWEITERT"; -"settings_other" = "WEITERES"; +"settings_other" = "Weiteres"; "settings_devices" = "SITZUNGEN"; "settings_cryptography" = "VERSCHLÜSSELUNG"; "settings_sign_out" = "Abmelden"; @@ -397,7 +397,7 @@ "bug_report_send_screenshot" = "Sende Bildschirmfoto"; "bug_report_progress_zipping" = "Sammele Protokolle"; "bug_report_progress_uploading" = "Bericht hochladen"; -"bug_report_send" = "Gesendet"; +"bug_report_send" = "Senden"; "collapse" = "zusammenklappen"; "auth_email_in_use" = "Diese E-Mail-Adresse wird bereits verwendet"; "auth_phone_in_use" = "Diese Telefonnummer wird bereits verwendet"; @@ -547,8 +547,8 @@ "room_resource_limit_exceeded_message_contact_2_link" = "kontaktiere deinen Dienst-Administrator"; "room_resource_limit_exceeded_message_contact_3" = " um diesen Dienst weiter zu nutzen."; "homeserver_connection_lost" = "Konnte keine Verbindung zum Heimserver herstellen."; -"room_resource_usage_limit_reached_message_1_default" = "Dieser Heimserver hat einer seiner Ressourcengrenzen überschritten, sodass "; -"room_resource_usage_limit_reached_message_1_monthly_active_user" = "Dieser Heimserver hat seine Begrenzung an monatlich aktiven Benutzer überschritten, sodass "; +"room_resource_usage_limit_reached_message_1_default" = "Dieser Heimserver hat eine seiner Ressourcengrenzen überschritten, sodass "; +"room_resource_usage_limit_reached_message_1_monthly_active_user" = "Dieser Heimserver hat seine Begrenzung an monatlich aktiven Benutzern überschritten, sodass "; "room_resource_usage_limit_reached_message_2" = "einige Benutzer nicht in der Lage sein werden, sich anzumelden."; "room_resource_usage_limit_reached_message_contact_3" = " um diese Obergrenze erhöhen zu lassen."; "auth_accept_policies" = "Bitte Regeln dieses Heimservers ansehen und akzeptieren:"; @@ -1001,10 +1001,10 @@ "skip" = "Überspringen"; "security_settings_crosssigning_info_not_bootstrapped" = "Quersignierung ist bisher nicht konfiguriert."; "room_member_power_level_admin_in" = "Admin in %@"; -"room_member_power_level_moderator_in" = "Moderator in %@"; +"room_member_power_level_moderator_in" = "Mod in %@"; "room_member_power_level_custom_in" = "Benutzerdefiniert (%@) in %@"; "room_member_power_level_short_admin" = "Admin"; -"room_member_power_level_short_moderator" = "Moderator"; +"room_member_power_level_short_moderator" = "Mod"; "room_member_power_level_short_custom" = "Benutzerdefiniert"; "security_settings_secure_backup" = "SICHERE SICHERHEITSKOPIE"; "security_settings_secure_backup_synchronise" = "Synchronisiere"; @@ -1019,7 +1019,7 @@ "store_promotional_text" = "Privatsphäre-wahrende Kollaborations-App in einem offenen Netzwerk. Dezentral, um dir die Kontrolle zu geben. Keine Datenerfassung, keine Hintertüren und kein Zugriff durch Dritte."; "room_participants_action_security_status_complete_security" = "Vollständige Sicherheit"; "external_link_confirmation_title" = "Überprüfe diesen Link genau"; -"external_link_confirmation_message" = "Der Link %@ braucht zu lange auf der anderen Seite: %@\n\nSicher, dass du fortfahren möchtest?"; +"external_link_confirmation_message" = "Der Link %@ bringt dich auf eine andere Seite: %@\n\nSicher, dass du fortfahren möchtest?"; "security_settings_crypto_sessions_description_2" = "Wenn du dich nicht angemeldet hast, ändere dein Passwort und setze die Sichere Sicherheitskopie zurück."; "security_settings_secure_backup_description" = "Sichere die Schlüssel, um Datenverlust zu verhindern. Sie werden mit einem Sicherungsschlüssel gesichert."; "security_settings_crosssigning_info_exists" = "Dein Konto hat eine Quersignatur-Identität, aber dieser Sitzung wird noch nicht vertraut. Vervollständige die Sicherheit auf diese Sitzung."; @@ -1349,7 +1349,7 @@ "event_formatter_call_ringing" = "Läuten…"; "event_formatter_call_connecting" = "Verbinden…"; "settings_labs_enable_ringing_for_group_calls" = "Bei Gruppenanrufen klingeln"; -"room_no_privileges_to_create_group_call" = "Du musst Admin oder Moderator sein, um einen Anruf zu starten."; +"room_no_privileges_to_create_group_call" = "Du musst Admin oder Mod sein, um einen Anruf zu starten."; "room_join_group_call" = "Beitreten"; // Chat @@ -1404,7 +1404,7 @@ "event_formatter_call_incoming_video" = "Eingehender Videoanruf"; "event_formatter_call_has_ended_with_time" = "Anruf beendet • %@"; "voice_message_stop_locked_mode_recording" = "Klicke, um die Aufnahme zu starten oder stoppen"; -"settings_device_notifications" = "Gerätbenachrichtigungen"; +"settings_device_notifications" = "Gerätebenachrichtigungen"; "voice_message_lock_screen_placeholder" = "Sprachnachricht"; "voice_message_remaining_recording_time" = "%@s übrig"; @@ -1439,7 +1439,7 @@ "settings_notify_me_for" = "Benachrichtige mich bei"; "settings_mentions_and_keywords" = "Erwähnungen und Schlüsselwörter"; "settings_confirm_media_size_description" = "Wenn dies aktiviert ist, wirst du beim Senden von Bildern und Videos gefragt, in welcher Größe sie gesendet werden sollen."; -"settings_confirm_media_size" = "Größe beim Senden bestätigen"; +"settings_confirm_media_size" = "Größe beim Senden auswählen"; "settings_notifications" = "BENACHRICHTIGUNGEN"; "settings_mentions_and_keywords_encryption_notice" = "Auf deinem Mobilgerät wirst du keine Benachrichtigungen für Erwähnungen und Schlüsselwörter in verschlüsselten Räumen erhalten."; "version_check_modal_subtitle_supported" = "Wir haben daran gearbeitet %@ zu verbessern um ein schnelleres und bereinigteres Erlebnis zu schaffen. Leider ist deine aktuelle iOS-Version mit einigen dieser Verbesserungen nicht kompatibel und wird daher nicht mehr unterstützt werden.\nWir empfehlen dir die Aktualisierung deines Betriebssystems um %@ weiterhin vollumfänglich zu nutzen."; @@ -1487,7 +1487,7 @@ "settings_contacts_enable_sync" = "Finde deine Kontakte"; "space_home_show_all_rooms" = "Alle Räume anzeigen"; "service_terms_modal_information_description_integration_manager" = "Ein Integrationsmanager erlaubt dir, externe Funktionen hinzuzufügen."; -"service_terms_modal_information_description_identity_server" = "Der Identitätsserver sucht anhand der Telefonnummern und E-Mails in deinen Kontakten, ob diese einen Matrix-Account haben."; +"service_terms_modal_information_description_identity_server" = "Der Identitätsserver sucht anhand der Telefonnummern und E-Mails deiner Kontakte nach ihren Matrix-Accounts."; "service_terms_modal_information_title_integration_manager" = "Integrationsmanager"; // Alert explaining what an identity server / integration manager is. @@ -1502,8 +1502,68 @@ "service_terms_modal_title_message" = "Zum Fortfahren musst du die Nutzungsbedingungen akzeptieren"; "settings_contacts_enable_sync_description" = "Dies verwendet deinen Identitätsserver um dich mit deinen Kontakten zu verbinden."; "settings_phone_contacts" = "KONTAKTE AM HANDY"; -"room_event_action_forward" = "Weiter"; +"room_event_action_forward" = "Weiterleiten"; "find_your_contacts_identity_service_error" = "Konnte keine Verbindung zum Identitätsserver aufbauen."; "find_your_contacts_button_title" = "Finde deine Kontakte"; "contacts_address_book_permission_denied_alert_message" = "Um Kontakte zu aktivieren, öffne die Einstellungen deines Gerätes."; "contacts_address_book_permission_denied_alert_title" = "Kontakte deaktiviert"; +"poll_edit_form_create_options" = "Erstelle Optionen"; +"settings_discovery_accept_terms" = "Bedingungen des Identitätsservers akzeptieren"; +"find_your_contacts_message" = "Lass dir von %@ deine Kontakte anzeigen um schnell mit denen zu chatten, die du am besten kennst."; +"poll_timeline_votes_count" = "%lu Stimmen"; +"poll_timeline_one_vote" = "1 Stimme"; +"poll_edit_form_add_option" = "Option hinzufügen"; +"poll_edit_form_option_number" = "Option %lu"; +"poll_edit_form_question_or_topic" = "Frage oder Thematik"; +"room_event_action_end_poll" = "Umfrage beenden"; +"room_event_action_remove_poll" = "Umfrage entfernen"; + +// Mark: - Polls + +"poll_edit_form_create_poll" = "Umfrage erstellen"; +"settings_labs_enabled_polls" = "Umfragen"; +"share_extension_send_now" = "Jetzt senden"; +"accessibility_button_label" = "Knopf"; +"settings_analytics_and_crash_data" = "Sende Absturz- und Analysedaten"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Wir senden keine Informationen an Dritte"; +"analytics_prompt_terms_link_new_user" = "hier"; + +// Analytics +"analytics_prompt_title" = "Hilf dabei %@ zu verbessern"; +"settings_about" = "ÜBER"; +"enable" = "Aktivieren"; +"analytics_prompt_message_upgrade" = "Du hast in der Vergangenheit bereits zugestimmt anonyme Nutzungsdaten mit uns zu teilen. Jetzt werden wir als Hilfe, um zu verstehen, wie Personen mehrere Geräte benutzen, eine zufällige Kennung generieren, die zwischen deinen Geräten geteilt wird."; +"analytics_prompt_message_new_user" = "Hilf uns dabei Probleme zu identifizieren und Element zu verbessern, indem du anonyme Nutzungsdaten teilst. Um zu verstehen, wie Personen mehrere Geräte benutzen, werden wir eine zufällige Kennung generieren, die zwischen deinen Geräten geteilt wird."; +"find_your_contacts_title" = "Starte mit der Auflistung deiner Kontakte"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Du kannst unsere gesamten Bedingungen %@ nachlesen."; +"poll_timeline_total_votes" = "%lu Stimmen abgegeben"; +"poll_timeline_total_one_vote" = "1 Stimme abgegeben"; +"poll_timeline_total_no_votes" = "Keine Stimmen abgegeben"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_edit_form_post_failure_action" = "OK"; +"poll_edit_form_poll_question_or_topic" = "Frage oder Thema der Umfrage"; +"poll_edit_form_input_placeholder" = "Schreib etwas"; +"analytics_prompt_terms_link_upgrade" = "hier"; +"poll_timeline_not_closed_title" = "Fehler beim Beenden der Abstimmung"; +"poll_timeline_vote_not_registered_subtitle" = "Wir konnten deine Stimme leider nicht erfassen. Versuche es bitte erneut"; +"poll_timeline_total_final_results" = "Es wurden %lu Stimmen abgegeben"; +"poll_timeline_total_final_results_one_vote" = "Es wurde 1 Stimme abgegeben"; +"poll_timeline_total_votes_not_voted" = "%lu Stimmen abgegeben. Stimme ab, um die Ergebnisse zu sehen"; +"poll_timeline_total_one_vote_not_voted" = "1 Stimme abgegeben. Stimme ab, um die Ergebnisse zu sehen"; +"poll_timeline_not_closed_subtitle" = "Versuche es bitte erneut"; +"poll_timeline_vote_not_registered_title" = "Stimme nicht erfasst"; +"poll_edit_form_post_failure_subtitle" = "Versuche es bitte erneut"; +"poll_edit_form_post_failure_title" = "Fehler beim Senden der Abstimmung"; +"share_extension_low_quality_video_message" = "Für eine bessere Qualität sende es in %@ oder sende es in niedriger Qualität."; +"share_extension_low_quality_video_title" = "Das Video wird in niedriger Qualität gesendet werden"; +"analytics_prompt_stop" = "Teilen beenden"; +"analytics_prompt_not_now" = "Nicht jetzt"; +"analytics_prompt_yes" = "Das ist Okay"; +"analytics_prompt_point_3" = "Du kannst dies jederzeit in den Einstellungen deaktivieren"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Wir erfassen und analysieren keine Accountdaten"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Alle unsere Bedingungen lesen %@. Bist du damit einverstanden?"; diff --git a/Riot/Assets/en.lproj/InfoPlist.strings b/Riot/Assets/en.lproj/InfoPlist.strings index 6d3378a2a..a9513a6ed 100644 --- a/Riot/Assets/en.lproj/InfoPlist.strings +++ b/Riot/Assets/en.lproj/InfoPlist.strings @@ -21,3 +21,4 @@ "NSContactsUsageDescription" = "Element will show your contacts so you can invite them to chat."; "NSCalendarsUsageDescription" = "See your scheduled meetings in the app."; "NSFaceIDUsageDescription" = "Face ID is used to access your app."; +"NSLocationWhenInUseUsageDescription" = "When you share your location to people, Element needs access to show them a map."; diff --git a/Riot/Assets/en.lproj/Localizable.strings b/Riot/Assets/en.lproj/Localizable.strings index 914bb11e2..f3547d667 100644 --- a/Riot/Assets/en.lproj/Localizable.strings +++ b/Riot/Assets/en.lproj/Localizable.strings @@ -71,6 +71,9 @@ /* New file message from a specific person, not referencing a room. */ "FILE_FROM_USER" = "%@ sent a file %@"; +/* New file message from a specific person, not referencing a room. */ +"LOCATION_FROM_USER" = "%@ shared their location"; + /* A single unread message in a room */ "SINGLE_UNREAD_IN_ROOM" = "You received a message in %@"; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 768862c3b..86bb2c62b 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -41,6 +41,7 @@ "retry" = "Retry"; "on" = "On"; "off" = "Off"; +"enable" = "Enable"; "cancel" = "Cancel"; "save" = "Save"; "join" = "Join"; @@ -65,6 +66,7 @@ "less" = "Less"; "open" = "Open"; "done" = "Done"; +"ok" = "OK"; // Call Bar "callbar_only_single_active" = "Tap to return to the call (%@)"; @@ -77,6 +79,7 @@ // Accessibility "accessibility_checkbox_label" = "checkbox"; +"accessibility_button_label" = "button"; // Authentication "auth_login" = "Log in"; @@ -577,7 +580,7 @@ Tap the + to start adding people."; "settings_term_conditions" = "Terms & Conditions"; "settings_privacy_policy" = "Privacy Policy"; "settings_third_party_notices" = "Third-party Notices"; -"settings_send_crash_report" = "Send anon crash & usage data"; +"settings_analytics_and_crash_data" = "Send crash and analytics data"; "settings_enable_rageshake" = "Rage shake to report bug"; "settings_clear_cache" = "Clear cache"; @@ -945,8 +948,24 @@ Tap the + to start adding people."; "no_voip_title" = "Incoming call"; "no_voip" = "%@ is calling you but %@ does not support calls yet.\nYou can ignore this notification and answer the call from another device or you can reject it."; -// Crash report -"google_analytics_use_prompt" = "Would you like to help improve %@ by automatically reporting anonymous crash reports and usage data?"; +// Analytics +"analytics_prompt_title" = "Help improve %@"; +"analytics_prompt_message_new_user" = "Help us identify issues and improve %@ by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; +"analytics_prompt_message_upgrade" = "You previously consented to share anonymous usage data with us. Now, to help understand how people use multiple devices, we’ll generate a random identifier, shared by your devices."; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "You can read all our terms %@."; +"analytics_prompt_terms_link_new_user" = "here"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Read all our terms %@. Is that OK?"; +"analytics_prompt_terms_link_upgrade" = "here"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "We don't record or profile any account data"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "We don't share information with third parties"; +"analytics_prompt_point_3" = "You can turn this off anytime in settings"; +"analytics_prompt_not_now" = "Not now"; +"analytics_prompt_yes" = "Yes, that's fine"; +"analytics_prompt_stop" = "Stop sharing"; // Crypto "e2e_enabling_on_app_update" = "%@ now supports end-to-end encryption but you need to log in again to enable it.\n\nYou can do it now or later from the application settings."; @@ -1733,7 +1752,7 @@ Tap the + to start adding people."; "spaces_coming_soon_title" = "Coming soon"; "spaces_add_rooms_coming_soon_title" = "Adding rooms coming soon"; "spaces_invites_coming_soon_title" = "Invites coming soon"; -"spaces_coming_soon_detail" = "This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with Element on your computer."; +"spaces_coming_soon_detail" = "This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with %@ on your computer."; "space_participants_action_remove" = "Remove from this space"; "space_participants_action_ban" = "Ban from this space"; "space_home_show_all_rooms" = "Show all rooms"; @@ -1803,8 +1822,6 @@ Tap the + to start adding people."; "poll_edit_form_post_failure_subtitle" = "Please try again"; -"poll_edit_form_post_failure_action" = "OK"; - "poll_timeline_one_vote" = "1 vote"; "poll_timeline_votes_count" = "%lu votes"; @@ -1827,10 +1844,32 @@ Tap the + to start adding people."; "poll_timeline_vote_not_registered_subtitle" = "Sorry, your vote was not registered, please try again"; -"poll_timeline_vote_not_registered_action" = "OK"; - "poll_timeline_not_closed_title" = "Failed to end poll"; "poll_timeline_not_closed_subtitle" = "Please try again"; -"poll_timeline_not_closed_action" = "OK"; +// MARK: - Location sharing + +"location_sharing_title" = "Location"; + +"location_sharing_close_action" = "Close"; + +"location_sharing_share_action" = "Share"; + +"location_sharing_loading_map_error_title" = "%@ could not load the map. Please try again later."; + +"location_sharing_locating_user_error_title" = "%@ could not access your location. Please try again later."; + +"location_sharing_invalid_authorization_error_title" = "%@ does not have permission to access your location. You can enable access in Settings > Location"; + +"location_sharing_invalid_authorization_not_now" = "Not now"; + +"location_sharing_invalid_authorization_settings" = "Settings"; + +"location_sharing_open_apple_maps" = "Open in Apple Maps"; + +"location_sharing_open_google_maps" = "Open in Google Maps"; + +"location_sharing_settings_header" = "Location sharing"; + +"location_sharing_settings_toggle_title" = "Enable location sharing"; diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 9e209a1f2..2a1abaf1d 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -1474,7 +1474,7 @@ "share_extension_low_quality_video_title" = "Saadame video madalama kvalitediga"; "settings_about" = "TEAVE MEIST"; "poll_edit_form_add_option" = "Lisa valik"; -"poll_edit_form_option_number" = "Valik %d"; +"poll_edit_form_option_number" = "Valik %lu"; "poll_edit_form_create_options" = "Koosta valikud"; "poll_edit_form_input_placeholder" = "Kirjuta midagi"; "poll_edit_form_question_or_topic" = "Küsimus või teema"; @@ -1484,3 +1484,46 @@ "poll_edit_form_create_poll" = "Koosta üks küsitlus"; "settings_discovery_accept_terms" = "Nõustu isikutuvastusserveri tingimustega"; +"poll_timeline_not_closed_action" = "Sobib"; +"poll_timeline_not_closed_subtitle" = "Palun proovi uuesti"; +"poll_timeline_not_closed_title" = "Küsitluse lõpetamine ei õnnestunud"; +"poll_timeline_vote_not_registered_action" = "Sobib"; +"poll_timeline_vote_not_registered_subtitle" = "Vabandust, aga sinu valik jäi salvestamata. Palun proovi uuesti"; +"poll_timeline_vote_not_registered_title" = "Hääl ei salvestunud"; +"poll_timeline_total_final_results" = "%lu'l häälel põhinev lõpptulemus"; +"poll_timeline_total_final_results_one_vote" = "Ühel häälel põhinev lõpptulemus"; +"poll_timeline_total_votes_not_voted" = "%lu hääletanut. Tulemuste nägemiseks osale ise ka küsitluses"; +"poll_timeline_total_one_vote_not_voted" = "1 hääletanu. Tulemuste nägemiseks osale ise ka küsitluses"; +"poll_timeline_total_votes" = "%lu hääletanut"; +"poll_timeline_total_one_vote" = "1 hääletanu"; +"poll_timeline_total_no_votes" = "Hääletanuid ei ole"; +"poll_timeline_votes_count" = "%lu häält"; +"poll_timeline_one_vote" = "1 hääl"; +"poll_edit_form_post_failure_action" = "Sobib"; +"poll_edit_form_post_failure_subtitle" = "Palun proovi uuesti"; +"poll_edit_form_post_failure_title" = "Küsitluse üleslaadimine ei õnnestunud"; +"settings_labs_enabled_polls" = "Küsitlused"; +"room_event_action_end_poll" = "Lõpeta küsitlus"; +"room_event_action_remove_poll" = "Kustuta küsitlus"; +"analytics_prompt_stop" = "Lõpeta andmete jagamine"; +"analytics_prompt_yes" = "Jah, see on sobilik"; +"analytics_prompt_not_now" = "Mitte praegu"; +"analytics_prompt_point_3" = "Seadistustest saad alati määrata, et see funktsionaalsus pole kasutusel"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Meie ei jaga teavet kolmandate osapooltega"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Meie ei salvesta ega profileeri sinu kasutajakonto andmeid"; +"analytics_prompt_terms_link_upgrade" = "siit"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Kõik meie tingimused leiad %@. Kas sa oled nõus?"; +"analytics_prompt_terms_link_new_user" = "siit"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Meie kasutustingimused leiad %@."; +"analytics_prompt_message_upgrade" = "Sa oled varem nõustunud meiega anonüümsete andmete jagamisega. Selleks, et mõistaksime, kuidas kasutajad erinevaid seadmeid pruugivad, me loome sinu seadmetele ühise juhusliku tunnuse."; +"analytics_prompt_message_new_user" = "Võimalike vigade leidmiseks ja Element'i arendamiseks jaga meiega anonüümseid andmeid. Selleks, et mõistaksime, kuidas kasutajad erinevaid seadmeid pruugivad me loome sinu seadmetele ühise juhusliku tunnuse."; + +// Analytics +"analytics_prompt_title" = "Aita arendada %@ rakendust"; +"settings_analytics_and_crash_data" = "Saada rakenduse vigade ja analüütika andmeid"; +"accessibility_button_label" = "nupp"; +"enable" = "Võta kasutusele"; diff --git a/Riot/Assets/fr.lproj/InfoPlist.strings b/Riot/Assets/fr.lproj/InfoPlist.strings index 464aeac9e..38d044ce1 100644 --- a/Riot/Assets/fr.lproj/InfoPlist.strings +++ b/Riot/Assets/fr.lproj/InfoPlist.strings @@ -2,6 +2,6 @@ "NSCameraUsageDescription" = "L’appareil photo est utilisé pour prendre des photos, des vidéos et pour passer des appels vidéo."; "NSPhotoLibraryUsageDescription" = "La photothèque est utilisée pour envoyer des photos et des vidéos."; "NSMicrophoneUsageDescription" = "Element doit avoir accès au microphone pour passer des appels, capturer des vidéos et enregistrer des messages vocaux."; -"NSContactsUsageDescription" = "Pour découvrir vos contacts qui utilisent déjà Matrix, Element peut envoyer les adresses e-mail et les numéros de téléphone de votre carnet d’adresse à votre serveur d’identité Matrix. Si votre serveur d’identité le prend en charge, les données personnelles sont hachées avant l’envoi − vérifiez sa politique de confidentialité pour plus de détails."; +"NSContactsUsageDescription" = "Element affichera vos contacts pour que vous puissiez les inviter à parler."; "NSCalendarsUsageDescription" = "Voir vos rendez-vous dans l’application."; "NSFaceIDUsageDescription" = "Face ID est utilisé pour accéder à votre application."; diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index 6ff0e4091..aafd1db9a 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -62,7 +62,7 @@ "auth_missing_phone" = "Numéro de téléphone manquant"; "auth_missing_email_or_phone" = "Adresse e-mail ou numéro de téléphone manquant"; "auth_password_dont_match" = "Les mots de passe ne correspondent pas"; -"auth_username_in_use" = ""; +"auth_username_in_use" = "Nom d’utilisateur déjà pris"; "auth_forgot_password" = "Mot de passe oublié ?"; "auth_use_server_options" = "Utiliser un serveur personnalisé (avancé)"; "auth_email_validation_message" = "Merci de vérifier vos e-mails pour continuer l’inscription"; @@ -94,7 +94,7 @@ "room_creation_keep_private" = "Garder privée"; "room_creation_make_private" = "Rendre privée"; "room_creation_wait_for_creation" = "Un salon est déjà en cours de création. Veuillez patienter."; -"room_creation_invite_another_user" = "Rechercher/inviter par identifiant, nom ou e-mail"; +"room_creation_invite_another_user" = "Identifiant, nom ou e-mail"; // Room recents "room_recents_directory_section" = "RÉPERTOIRE DES SALONS"; "room_recents_favourites_section" = "FAVORIS"; @@ -251,9 +251,9 @@ "settings_user_settings" = "PRÉFÉRENCES UTILISATEUR"; "settings_notifications_settings" = "PRÉFÉRENCES DE NOTIFICATIONS"; "settings_ignored_users" = "UTILISATEURS IGNORÉS"; -"settings_contacts" = "CONTACTS LOCAUX"; +"settings_contacts" = "CONTACTS DE L’APPAREIL"; "settings_advanced" = "AVANCÉ"; -"settings_other" = "AUTRES"; +"settings_other" = "Autres"; "settings_labs" = "EXPÉRIMENTAL"; "settings_devices" = "SESSIONS"; "settings_cryptography" = "CHIFFREMENT"; @@ -1520,3 +1520,87 @@ "room_recents_suggested_rooms_section" = "SALONS RECOMMANDÉS"; "done" = "Terminé"; "open" = "Ouvrir"; +"poll_timeline_not_closed_action" = "Ok"; +"poll_timeline_not_closed_subtitle" = "Merci de réessayer"; +"poll_timeline_not_closed_title" = "Échec de la fermeture du sondage"; +"poll_timeline_vote_not_registered_action" = "Ok"; +"poll_timeline_vote_not_registered_subtitle" = "Désolé votre vote n’a pas été enregistré, veuillez réessayer"; +"poll_timeline_vote_not_registered_title" = "Vote non enregistré"; +"poll_timeline_total_final_results" = "Résultats finaux basés sur %lu votes"; +"poll_timeline_total_final_results_one_vote" = "Résultats finaux basés sur 1 vote"; +"poll_timeline_total_votes_not_voted" = "%lu votes recueillis. Votez pour consulter les résultats"; +"poll_timeline_total_one_vote_not_voted" = "1 vote recueilli. Votez pour connaître les résultats"; +"poll_timeline_total_votes" = "%lu votes recueillis"; +"poll_timeline_total_one_vote" = "1 vote recueilli"; +"poll_timeline_total_no_votes" = "Aucun vote recueilli"; +"poll_timeline_votes_count" = "%lu votes"; +"poll_timeline_one_vote" = "1 vote"; +"poll_edit_form_post_failure_action" = "Ok"; +"poll_edit_form_post_failure_subtitle" = "Merci de réessayer"; +"poll_edit_form_post_failure_title" = "Échec de la publication du sondage"; +"poll_edit_form_add_option" = "Ajouter une option"; +"poll_edit_form_option_number" = "Option %lu"; +"poll_edit_form_create_options" = "Ajouter des options"; +"poll_edit_form_input_placeholder" = "Écrivez quelque chose"; +"poll_edit_form_question_or_topic" = "Question ou sujet"; +"poll_edit_form_poll_question_or_topic" = "Question ou sujet du sondage"; + +// Mark: - Polls + +"poll_edit_form_create_poll" = "Créer un sondage"; +"space_home_show_all_rooms" = "Afficher tous les salons"; +"service_terms_modal_information_description_integration_manager" = "Un gestionnaire d’intégrations vous permet d’ajouter des fonctionnalités de tierces-parties."; +"service_terms_modal_information_description_identity_server" = "Un serveur d’identité vous aide à trouver vos contacts, en recherchant leur numéro de téléphone ou adresse e-mail pour vérifier s’ils ont déjà un compte."; +"service_terms_modal_information_title_integration_manager" = "Gestionnaire d’intégrations"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Serveur d’identité"; +"service_terms_modal_description_integration_manager" = "Ceci vous permettra d’utiliser les robots, passerelles, widgets et jeux d’autocollants."; +"service_terms_modal_description_identity_server" = "Ceci permettra aux personnes qui ont votre numéro de téléphone ou adresse e-mail de sauvegardées dans leurs contacts de vous trouver."; +"service_terms_modal_table_header_integration_manager" = "CONDITIONS D’UTILISATION DU GESTIONNAIRE D’INTÉGRATIONS"; +"service_terms_modal_table_header_identity_server" = "CONDITIONS D’UTILISATION DU SERVEUR D’IDENTITÉ"; +"service_terms_modal_footer" = "Ceci peut être désactivé a n’importe quel moment dans les paramètres."; + +// Service terms +"service_terms_modal_title_message" = "Pour poursuivre, acceptez nos conditions d’utilisation"; +"share_extension_send_now" = "Envoyer maintenant"; +"share_extension_low_quality_video_message" = "Envoyez depuis %@ pour une meilleure qualité. Ou envoyez en mauvaise qualité ci-dessous."; +"share_extension_low_quality_video_title" = "La vidéo sera envoyée en mauvaise qualité"; +"analytics_prompt_stop" = "Arrêter de partager"; +"analytics_prompt_yes" = "Oui, ça me va"; +"analytics_prompt_not_now" = "Pas maintenant"; +"analytics_prompt_point_3" = "Vous pouvez désactiver ceci à tout moment dans les paramètres"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Nous ne partageons pas les données avec des entités tierces"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Nous n’enregistrons et ne profilons pas de données liées à votre compte"; +"analytics_prompt_terms_link_upgrade" = "ici"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Lisez nos conditions d’utilisation %@. Êtes vous d’accord ?"; +"analytics_prompt_terms_link_new_user" = "ici"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Vous pouvez lire nos conditions d’utilisation %@."; +"analytics_prompt_message_upgrade" = "Vous aviez consenti précédemment à partager des rapports d’utilisation avec nous. Désormais, pour nous aider à comprendre comment les gens utilisent cette application sur plusieurs appareils, nous allons générer un identifiant aléatoire commun à tous vos appareils."; +"analytics_prompt_message_new_user" = "Aidez nous à identifier les problèmes et améliorer Element en envoyant des rapports d’usage anonymes. Pour comprendre de quelle manière les gens utilisent Element sur plusieurs appareils, nous créeront un identifiant aléatoire commun à tous vos appareils."; + +// Analytics +"analytics_prompt_title" = "Aidez à améliorer %@"; +"settings_discovery_accept_terms" = "Accepter les conditions du serveur d’identité"; +"settings_analytics_and_crash_data" = "Envoyer les rapports de plantages et d’utilisation"; +"settings_labs_enabled_polls" = "Sondages"; +"settings_about" = "À PROPOS"; +"settings_phone_contacts" = "CONTACTS DU TÉLÉPHONE"; +"room_event_action_forward" = "Transférer"; +"room_event_action_end_poll" = "Mettre fin au sondage"; +"room_event_action_remove_poll" = "Supprimer le sondage"; +"find_your_contacts_identity_service_error" = "Impossible de se connecter au serveur d’identité."; +"find_your_contacts_button_title" = "Trouvez vos contacts"; +"find_your_contacts_message" = "Permettez à %@ d’accéder à vos contacts pour commencer à discuter rapidement avec ceux que vous connaissez le mieux."; +"contacts_address_book_permission_denied_alert_message" = "Pour activer les contacts, rendez vous dans les paramètres de votre appareil."; +"contacts_address_book_permission_denied_alert_title" = "Contacts désactivés"; +"accessibility_button_label" = "bouton"; +"enable" = "Activer"; +"find_your_contacts_footer" = "Cette fonctionnalité peut être désactivé à tout moment à partir des paramètres."; +"find_your_contacts_title" = "Commencez par lister vos contacts"; +"settings_contacts_enable_sync_description" = "Cette fonctionnalité utilisera votre serveur d'identité pour vous connecter avec vos contacts, ainsi que pour les aider à vous trouver."; +"settings_contacts_enable_sync" = "Trouvez vos contacts"; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 0d8050079..6060f16c3 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -1533,7 +1533,7 @@ "space_home_show_all_rooms" = "Minden szoba megjelenítése"; "room_event_action_forward" = "Továbbítás"; "poll_edit_form_add_option" = "Lehetőség hozzáadása"; -"poll_edit_form_option_number" = "%d lehetőség"; +"poll_edit_form_option_number" = "%lu lehetőség"; "poll_edit_form_create_options" = "Lehetőségek hozzáadása"; "poll_edit_form_input_placeholder" = "Írjon valamit"; "poll_edit_form_question_or_topic" = "Kérdés vagy téma"; @@ -1547,3 +1547,46 @@ "share_extension_low_quality_video_title" = "Alacsony minőségű videó lesz elküldve"; "settings_discovery_accept_terms" = "Azonosítási Szolgáltatás felhasználási feltételeinek elfogadása"; "settings_about" = "NÉVJEGY"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_not_closed_subtitle" = "Kérlek próbáld újra"; +"poll_timeline_not_closed_title" = "Nem sikerült a szavazás lezárása"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_timeline_vote_not_registered_subtitle" = "Sajnos a szavazatod nem lett rögzítve. Kérlek ismételd meg újra"; +"poll_timeline_vote_not_registered_title" = "Szavazás nem sikerült"; +"poll_timeline_total_final_results" = "Végeredmény %lu szavazat alapján"; +"poll_timeline_total_final_results_one_vote" = "Eredmény 1 szavazat alapján"; +"poll_timeline_total_votes_not_voted" = "%lu szavazatot adtak le. Szavazz az eredmény megtekintéséhez"; +"poll_timeline_total_one_vote_not_voted" = "1 szavazatot adtak le. Szavazz az eredmény megtekintéséhez"; +"poll_timeline_total_votes" = "%lu szavazatot adtak le"; +"poll_timeline_total_one_vote" = "1 szavazatot adtak le"; +"poll_timeline_total_no_votes" = "Nem adtak le szavazatot"; +"poll_timeline_votes_count" = "%lu szavazat"; +"poll_timeline_one_vote" = "1 szavazat"; +"poll_edit_form_post_failure_action" = "OK"; +"poll_edit_form_post_failure_subtitle" = "Kérlek próbáld újra"; +"poll_edit_form_post_failure_title" = "A szavazást nem sikerült beküldeni"; +"analytics_prompt_stop" = "Megosztás megállítása"; +"analytics_prompt_yes" = "Igen, rendben van"; +"analytics_prompt_not_now" = "Nem most"; +"analytics_prompt_point_3" = "Ezt bármikor kikapcsolhatod a beállításokban"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Nem osztjuk meg az információt harmadik féllel"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Nem küldünk és nem profilozunk semmilyen fiók adatot"; +"analytics_prompt_terms_link_upgrade" = "itt"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Olvasd el minden feltételünket: %@. Rendben?"; +"analytics_prompt_terms_link_new_user" = "itt"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Az összes feltételünket elolvashatod itt: %@."; +"analytics_prompt_message_new_user" = "Segíts észrevennünk a hibákat, és jobbá tenni az Element-et a névtelen használati adatok küldése által. Ahhoz, hogy megértsük, hogyan használnak a felhasználók egyszerre több eszközt, egy véletlenszerű azonosítót generálunk, ami az eszközeid között meg lesz osztva."; + +// Analytics +"analytics_prompt_title" = "Segíts jobbá tenni %@"; +"settings_analytics_and_crash_data" = "Összeomlás és analitikai adatok küldése"; +"settings_labs_enabled_polls" = "Szavazások"; +"room_event_action_end_poll" = "Szavazás lezárása"; +"room_event_action_remove_poll" = "Szavazás törlése"; +"accessibility_button_label" = "gomb"; +"enable" = "Engedélyezés"; +"analytics_prompt_message_upgrade" = "Korábban beleegyeztél, hogy velünk anonimizált adatokat osztasz meg. Most, hogy jobban megértsük, hogyan használnak több eszközt az emberek, véletlenszerű azonosítót állítunk elő amit az eszközeid használni fognak."; diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 62d27b60d..e92ace022 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -102,7 +102,7 @@ "joined" = "Bergabung"; "collapse" = "tutup"; "store_promotional_text" = "Aplikasi perpesanan dan kolaborasi yang menjaga privasi, pada jaringan terbuka. Terdesentralisasi untuk Anda kendali. Tidak ada penambangan data, tidak ada pintu belakang dan tidak ada akses pihak ketiga."; -"store_full_description" = "Element adalah aplikasi messenger dan kolaborasi tipe baru yang:\n\n1. Menempatkan Anda dalam kendali untuk mempertahankan privasi Anda\n2. Memungkinkan Anda berkomunikasi dengan siapa pun di jaringan Matrix, dan bahkan di luar dengan mengintegrasikan dengan aplikasi seperti Slack\n3. Melindungi Anda dari iklan, menambangan data, pintu belakang, dan taman berdinding\n4. Mengamankan Anda melalui enkripsi ujung-ke-ujung, dengan penandatanganan silang untuk memverifikasi orang lain\n\nElement benar-benar berbeda dari aplikasi perpesanan dan kolaborasi lain karena Element terdesentralisasi dan sumber terbuka.\n\nElement memungkinkan Anda host sendiri - atau memilih host - sehingga Anda memiliki privasi, kepemilikan, dan kontrol data dan obrolan Anda. Ini memberi Anda akses ke jaringan terbuka, jadi Anda tidak hanya terjebak berbicara dengan pengguna Element. Itu sangat aman.\n\nElement dapat melakukan semua ini karena beroperasi pada Matrix - standar untuk komunikasi terdesentralisasi terbuka.\n\nElement menempatkan Anda dalam kendali dengan membiarkan Anda memilih siapa yang menghost percakapan Anda. Dari aplikasi Element, Anda dapat memilih untuk menghost dengan cara yang berbeda:\n\n1. Dapatkan akun gratis pada server publik matrix.org\n2. Host sendiri akun Anda dengan menjalankan server pada perangkat keras Anda sendiri\n3. Mendaftar untuk akun di server khusus dengan hanya berlangganan platform hosting Element Matrix Services\n\nMengapa memilih Element?\n\nMILIKI DATA ANDA: Anda memutuskan di mana untuk menyimpan data dan pesan Anda. Anda memilikinya dan mengendalikannya, bukan perusahaan besar yang menambang data Anda atau memberikan akses ke pihak ketiga.\n\nPESAN DAN KOLABORASI TERBUKA: Anda dapat mengobrol dengan orang lain di jaringan Matrix, jika mereka menggunakan Element atau aplikasi Matrix lain, dan bahkan jika mereka menggunakan sistem perpesanan seperti Slack, IRC atau XMPP.\n\nSANGAT AMAN: Enkripsi ujung-ke-ujung yang nyata (hanya mereka yang dalam percakapan dapat mendekripsi pesan), dan penandatanganan silang untuk memverifikasi perangkat anggota obrolan.\n\nKOMUNIKASI LENGKAP: Perpesanan, panggilan suara dan video, pembagian file, pembagian layar dan banyak integrasi, bot dan widget. Buat ruangan, komunitas, tetap terhubung dan selesaikan hal-hal.\n\nDI MANA PUN ANDA BERADA: Tetap berkomunikasi di mana pun Anda berada dengan riwayat pesan yang sepenuhnya disinkronkan di semua perangkat Anda dan di web di https://app.element.io/."; +"store_full_description" = "Element adalah aplikasi messenger dan kolaborasi tipe baru yang:\n\n1. Menempatkan Anda dalam kendali untuk mempertahankan privasi Anda\n2. Memungkinkan Anda berkomunikasi dengan siapa pun di jaringan Matrix, dan bahkan di luar dengan mengintegrasikan dengan aplikasi seperti Slack\n3. Melindungi Anda dari iklan, menambangan data, pintu belakang, dan taman berdinding\n4. Mengamankan Anda melalui enkripsi ujung-ke-ujung, dengan penandatanganan silang untuk memverifikasi orang lain\n\nElement benar-benar berbeda dari aplikasi perpesanan dan kolaborasi lain karena Element terdesentralisasi dan sumber terbuka.\n\nElement memungkinkan Anda host sendiri — atau memilih host — sehingga Anda memiliki privasi, kepemilikan, dan kontrol data dan obrolan Anda. Ini memberi Anda akses ke jaringan terbuka, jadi Anda tidak hanya terjebak berbicara dengan pengguna Element. Itu sangat aman.\n\nElement dapat melakukan semua ini karena beroperasi pada Matrix — standar untuk komunikasi terdesentralisasi terbuka.\n\nElement menempatkan Anda dalam kendali dengan membiarkan Anda memilih siapa yang menghost percakapan Anda. Dari aplikasi Element, Anda dapat memilih untuk menghost dengan cara yang berbeda:\n\n1. Dapatkan akun gratis pada server publik matrix.org\n2. Host sendiri akun Anda dengan menjalankan server pada perangkat keras Anda sendiri\n3. Mendaftar untuk akun di server khusus dengan hanya berlangganan platform hosting Element Matrix Services\n\nMengapa memilih Element?\n\nMILIKI DATA ANDA: Anda memutuskan di mana untuk menyimpan data dan pesan Anda. Anda memilikinya dan mengendalikannya, bukan perusahaan besar yang menambang data Anda atau memberikan akses ke pihak ketiga.\n\nPESAN DAN KOLABORASI TERBUKA: Anda dapat mengobrol dengan orang lain di jaringan Matrix, jika mereka menggunakan Element atau aplikasi Matrix lain, dan bahkan jika mereka menggunakan sistem perpesanan seperti Slack, IRC atau XMPP.\n\nSANGAT AMAN: Enkripsi ujung-ke-ujung yang nyata (hanya mereka yang dalam percakapan dapat mendekripsi pesan), dan penandatanganan silang untuk memverifikasi perangkat anggota obrolan.\n\nKOMUNIKASI LENGKAP: Perpesanan, panggilan suara dan video, pembagian file, pembagian layar dan banyak integrasi, bot dan widget. Buat ruangan, komunitas, tetap terhubung dan selesaikan hal-hal.\n\nDI MANA PUN ANDA BERADA: Tetap berkomunikasi di mana pun Anda berada dengan riwayat pesan yang sepenuhnya disinkronkan di semua perangkat Anda dan di web di https://app.element.io/."; // String for App Store "store_short_description" = "Obrolan/VoIP terdesentralisasi aman"; @@ -617,7 +617,7 @@ "spaces_empty_space_detail" = "Beberapa ruangan mungkin disembunyikan karena ruangannya pribadi dan Anda memerlukan sebuah undangan."; "leave_space_message" = "Apakah anda Anda yakin ingin keluar dari %@? Apakah Anda juga ingin meninggalkan semua ruangan dan space lainnya di space ini?"; "space_beta_announce_information" = "Space adalah cara baru untuk mengelompokkan ruangan dan orang. Mereka belum ada di iOS, tetapi Anda dapat menggunakannya sekarang di Web dan Desktop."; -"favourites_empty_view_information" = "Anda dapat memfavoritkan dengan beberapa cara - yang tercepat adalah dengan menekan dan menahan. Ketuk ikon bintang dan mereka akan secara otomatis muncul di sini."; +"favourites_empty_view_information" = "Anda dapat memfavoritkan dengan beberapa cara — yang tercepat adalah dengan menekan dan menahan. Ketuk ikon bintang dan mereka akan secara otomatis muncul di sini."; "home_empty_view_information" = "Aplikasi obrolan aman semua-dalam-satu untuk tim, teman, dan organisasi. Ketuk tombol + di bawah untuk menambahkan orang dan ruangan."; "pin_protection_explanatory" = "Menyiapkan PIN memungkinkan Anda melindungi data seperti pesan dan kontak, jadi hanya Anda yang dapat mengaksesnya dengan memasukkan PIN di awal aplikasi."; "major_update_information" = "Kami senang mengumumkan bahwa kami telah mengubah nama kami! Aplikasi Anda telah diperbarui dan Anda masuk ke akun Anda."; @@ -1538,7 +1538,7 @@ "space_feature_unavailable_information" = "Space adalah cara baru untuk mengelompokkan ruangan dan pengguna.\n\nMereka akan segera datang. Untuk saat ini, jika Anda bergabung sebuah space di platform lain, Anda akan dapat mengakses ruang mana saja yang Anda ikuti di sini."; // Success from passphrase -"key_backup_setup_success_from_passphrase_info" = "Kunci Anda sedang dicadangkan.\n\nKunci Keamanan Anda adalah jaring pengaman - Anda dapat menggunakannya untuk memulihkan akses ke pesan terenkripsi jika Anda lupa frasa sandi.\n\nSimpan Kunci Keamanan Anda di suatu tempat yang sangat aman, seperti pengelola kata sandi (atau brankas)."; +"key_backup_setup_success_from_passphrase_info" = "Kunci Anda sedang dicadangkan.\n\nKunci Keamanan Anda adalah jaring pengaman — Anda dapat menggunakannya untuk memulihkan akses ke pesan terenkripsi jika Anda lupa frasa sandi.\n\nSimpan Kunci Keamanan Anda di suatu tempat yang sangat aman, seperti pengelola kata sandi (atau brankas)."; "key_backup_setup_passphrase_info" = "Kami akan menyimpan salinan terenkripsi dari kunci Anda di server kami. Lindungi cadangan Anda dengan frasa agar tetap aman.\n\nUntuk keamanan maksimum, ini harus berbeda dari kata sandi akun Anda."; "key_backup_setup_intro_info" = "Pesan di ruang terenkripsi diamankan dengan enkripsi ujung-ke-ujung. Hanya Anda dan penerima yang memiliki kunci untuk membaca pesan ini.\n\nCadangkan kunci Anda dengan aman untuk menghindari kehilangannya."; "deactivate_account_informations_part5" = "Jika Anda ingin kami melupakan pesan Anda, silakan centang kotak di bawah ini\n\nVisibilitas pesan di Matrix mirip dengan email. Kami melupakan pesan Anda berarti bahwa pesan yang telah Anda kirim tidak akan dibagikan dengan pengguna baru atau tidak terdaftar, tetapi pengguna terdaftar yang sudah memiliki akses ke pesan ini akan tetap memiliki akses ke salinannya."; @@ -1649,7 +1649,7 @@ "group_details_title" = "Detail Komunitas"; "room_event_action_forward" = "Teruskan"; "poll_edit_form_add_option" = "Tambahkan opsi"; -"poll_edit_form_option_number" = "Opsi %d"; +"poll_edit_form_option_number" = "Opsi %lu"; "poll_edit_form_create_options" = "Buat opsi"; "poll_edit_form_input_placeholder" = "Tulis sesuatu"; "poll_edit_form_question_or_topic" = "Pertanyaan atau topik"; @@ -1663,3 +1663,46 @@ "share_extension_low_quality_video_title" = "Video akan dikirim dalam kualitas rendah"; "settings_discovery_accept_terms" = "Terima Persyaratan Server Identitas"; "settings_about" = "TENTANG"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_not_closed_subtitle" = "Mohon coba lagi"; +"poll_timeline_not_closed_title" = "Gagal untuk mengakhiri poll"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_timeline_vote_not_registered_subtitle" = "Maaf, suara Anda tidak diberikan, mohon coba lagi"; +"poll_timeline_vote_not_registered_title" = "Suara tidak diberikan"; +"poll_timeline_total_final_results" = "Hasil akhir berdasarkan %lu suara"; +"poll_timeline_total_final_results_one_vote" = "Hasil akhir berdasarkan 1 suara"; +"poll_timeline_total_votes_not_voted" = "%lu suara diberikan. Berikan suara untuk melihat hasilnya"; +"poll_timeline_total_one_vote_not_voted" = "1 suara diberikan. Berikan suara untuk melihat hasilnya"; +"poll_timeline_total_no_votes" = "Tidak ada suara yang diberikan"; +"poll_timeline_total_votes" = "%lu suara diberikan"; +"poll_timeline_total_one_vote" = "1 suara diberikan"; +"poll_timeline_votes_count" = "%lu suara"; +"poll_timeline_one_vote" = "1 suara"; +"poll_edit_form_post_failure_action" = "OK"; +"poll_edit_form_post_failure_subtitle" = "Silakan coba lagi"; +"poll_edit_form_post_failure_title" = "Gagal untuk mengirim poll"; +"settings_labs_enabled_polls" = "Poll"; +"room_event_action_end_poll" = "Akhiri poll"; +"room_event_action_remove_poll" = "Hapus poll"; +"analytics_prompt_stop" = "Berhenti membagikan"; +"analytics_prompt_yes" = "Iya, saya tidak keberatan"; +"analytics_prompt_not_now" = "Jangan sekarang"; +"analytics_prompt_point_3" = "Anda dapat mematikannya kapan saja di pengaturan"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Kami tidak membagikan informasi ini dengan pihak ketiga"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Kami tidak merekam atau memprofil data akun apapun"; +"analytics_prompt_terms_link_upgrade" = "di sini"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Baca semua kebijakan kami %@. Apakah Anda tidak keberatan?"; +"analytics_prompt_terms_link_new_user" = "di sini"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Anda dapat membaca semua kebijakan kami %@."; +"analytics_prompt_message_upgrade" = "Anda sebelumnya setuju untuk mengirimkan data penggunaan anonim dengan kami. Sekarang, supaya kami dapat memahami bagaimana orang-orang menggunakan beberapa perangkat-perangkat, kami akan membuat pengenal acak, yang dibagikan oleh perangkat Anda."; +"analytics_prompt_message_new_user" = "Bantu kami mengidentifikasi masalah-masalah dan membuat Element lebih baik dengan membagikan data penggunaan anonim. Untuk memahami bagaimana orang-orang menggunakan beberapa perangkat-perangkat, kami akan membuat pengenal acak, yang dibagikan oleh perangkat Anda."; + +// Analytics +"analytics_prompt_title" = "Bantu membuat %@ lebih baik"; +"settings_analytics_and_crash_data" = "Kirim data crash dan analitik"; +"accessibility_button_label" = "tombol"; +"enable" = "Aktifkan"; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index ce4d6c96d..797589e89 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -1504,7 +1504,7 @@ "space_home_show_all_rooms" = "Mostra tutte le stanze"; "room_event_action_forward" = "Inoltra"; "poll_edit_form_add_option" = "Aggiungi opzione"; -"poll_edit_form_option_number" = "Opzione %d"; +"poll_edit_form_option_number" = "Opzione %lu"; "poll_edit_form_create_options" = "Crea opzioni"; "poll_edit_form_input_placeholder" = "Scrivi qualcosa"; "poll_edit_form_question_or_topic" = "Domanda o argomento"; @@ -1518,3 +1518,46 @@ "share_extension_low_quality_video_title" = "Il video verrà inviato in bassa qualità"; "settings_discovery_accept_terms" = "Accetta termini del server d'identità"; "settings_about" = "INFORMAZIONI"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_not_closed_subtitle" = "Riprova"; +"poll_timeline_not_closed_title" = "Chiusura del sondaggio fallita"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_timeline_vote_not_registered_subtitle" = "Spiacenti, il tuo voto non è stato registrato, riprova"; +"poll_timeline_vote_not_registered_title" = "Voto non registrato"; +"poll_timeline_total_final_results" = "Risultato finale basato su %lu voti"; +"poll_timeline_total_final_results_one_vote" = "Risultato finale basato su 1 voto"; +"poll_timeline_total_votes_not_voted" = "%lu voti inviati. Vota per vedere i risultati"; +"poll_timeline_total_one_vote_not_voted" = "1 voto inviato. Vota per vedere i risultati"; +"poll_timeline_total_votes" = "%lu voti"; +"poll_timeline_total_one_vote" = "1 voto"; +"poll_timeline_total_no_votes" = "Nessun voto"; +"poll_timeline_votes_count" = "%lu voti"; +"poll_timeline_one_vote" = "1 voto"; +"poll_edit_form_post_failure_action" = "OK"; +"poll_edit_form_post_failure_subtitle" = "Riprova"; +"poll_edit_form_post_failure_title" = "Invio del sondaggio fallito"; +"settings_labs_enabled_polls" = "Sondaggi"; +"room_event_action_end_poll" = "Termina sondaggio"; +"room_event_action_remove_poll" = "Rimuovi sondaggio"; +"analytics_prompt_stop" = "Non condividere più"; +"analytics_prompt_yes" = "Sì, va bene"; +"analytics_prompt_not_now" = "Non ora"; +"analytics_prompt_point_3" = "Puoi disattivarlo in qualsiasi momento nelle impostazioni"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Non condividiamo informazioni con terze parti"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Non registriamo o profiliamo alcun dato dell'account"; +"analytics_prompt_terms_link_upgrade" = "qui"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Leggi i nostri termini di servizio %@. Accetti?"; +"analytics_prompt_terms_link_new_user" = "qui"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Puoi leggere i nostri termini di servizio %@."; +"analytics_prompt_message_upgrade" = "Hai acconsentito precedentemente a condividere con noi dati di utilizzo anonimi. Ora, per capire come le persone usano diversi dispositivi, genereremo un identificativo casuale, condiviso dai tuoi dispositivi."; +"analytics_prompt_message_new_user" = "Aiutaci a identificare problemi e a migliorare Element condividendo dati di utilizzo anonimi. Per capire come le persone usano diversi dispositivi, genereremo un identificativo casuale, condiviso dai tuoi dispositivi."; + +// Analytics +"analytics_prompt_title" = "Aiuta a migliorare %@"; +"settings_analytics_and_crash_data" = "Invia crash e dati analitici"; +"accessibility_button_label" = "pulsante"; +"enable" = "Attiva"; diff --git a/Riot/Assets/ko.lproj/Vector.strings b/Riot/Assets/ko.lproj/Vector.strings index 0baff0850..a88ae30cd 100644 --- a/Riot/Assets/ko.lproj/Vector.strings +++ b/Riot/Assets/ko.lproj/Vector.strings @@ -1068,3 +1068,64 @@ "contacts_address_book_permission_denied_alert_message" = "연락처를 사용하려면, 설정으로 이동하십시오."; "contacts_address_book_permission_denied_alert_title" = "연락처 사용 안 함"; "rooms_empty_view_title" = "방"; + +// AuthenticatedSessionViewControllerFactory +"authenticated_session_flow_not_supported" = "이 앱은 해당 홈서버의 인증 구조를 지원하지 않습니다."; +"security_settings_user_password_description" = "신원을 확인하기 위해 계정 비밀번호를 입력해주세요."; +"event_formatter_widget_removed_by_you" = "위젯을 제거함 : %@"; + +// Events formatter with you +"event_formatter_widget_added_by_you" = "위젯을 추가함 : %@"; +"event_formatter_group_call_leave" = "떠나기"; +"event_formatter_group_call_join" = "들어가기"; +"event_formatter_group_call" = "그룹 전화"; +"event_formatter_call_end_call" = "통화 종료"; +"event_formatter_call_retry" = "재시도"; +"event_formatter_call_decline" = "거부"; +"event_formatter_call_back" = "통화 재시도"; +"event_formatter_call_connection_failed" = "연결 실패"; +"event_formatter_call_missed_voice" = "음성 통화 부재중"; +"event_formatter_call_missed_video" = "영상 통화 부재중"; +"event_formatter_call_you_declined" = "통화 거부"; +"event_formatter_call_active_voice" = "음성 통화 활성"; +"event_formatter_call_active_video" = "영상 통화 활성"; +"event_formatter_call_incoming_video" = "영상 통화 수신"; +"event_formatter_call_incoming_voice" = "음성 통화 수신"; +"event_formatter_call_has_ended_with_time" = "통화 끊김 %@"; +"event_formatter_call_has_ended" = "통화 끊기"; +"event_formatter_call_ringing" = "통화중…"; +"event_formatter_call_connecting" = "연결중…"; +"room_notifs_settings_encrypted_room_notice" = "암호화된 방에서는 멘션 및 키워드 알림이 작동하지 않습니다."; +"room_notifs_settings_account_settings" = "계정 설정"; +"room_notifs_settings_cancel_action" = "취소"; +"room_notifs_settings_done_action" = "적용"; +"room_notifs_settings_none" = "알림받지 않기"; +"room_notifs_settings_mentions_and_keywords" = "멘션과 키워드만"; +"room_notifs_settings_all_messages" = "모든 메시지"; +"room_details_advanced_e2e_encryption_disabled_for_dm" = "이 방의 암호화가 활성화되지 않음."; +"room_details_advanced_e2e_encryption_enabled_for_dm" = "이 방의 암호화 활성화됨"; +"room_details_advanced_room_id_for_dm" = "아이디 :"; +"room_details_no_local_addresses_for_dm" = "이 방은 로컬 주소를 가지고 있지 않음"; +"room_details_access_section_anyone_for_dm" = "게스트를 포함한 초대 링크를 알고 있는 누구나"; +"room_details_access_section_anyone_apart_from_guest_for_dm" = "게스트를 제외하고 초대 링크를 알고 있는 누구나"; +"room_details_access_section_for_dm" = "누가 접근할 수 있나요?"; +"room_details_notifs" = "알림"; +"room_details_room_name_for_dm" = "이름"; +"room_details_photo_for_dm" = "사진"; +"room_details_integrations" = "통합"; +"room_details_search" = "방 검색"; +"room_details_title_for_dm" = "정보"; +"manage_session_sign_out" = "세션 연결 끊기"; +"manage_session_not_trusted" = "신뢰하지 않음"; +"manage_session_trusted" = "신뢰하도록 설정됨"; +"manage_session_name" = "세션 이름"; +"manage_session_info" = "세션 정보"; + +// Manage session +"manage_session_title" = "세션 관리"; +"settings_analytics_and_crash_data" = "오류 및 분석 데이터 전송"; +"settings_labs_enabled_polls" = "투표"; +"room_event_action_end_poll" = "투표 종료"; +"room_event_action_remove_poll" = "투표 제거"; +"accessibility_button_label" = "버튼"; +"enable" = "활성화"; diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index 7f4072640..65b0d41c6 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -1633,7 +1633,7 @@ "contacts_address_book_permission_denied_alert_message" = "Om contacten in te schakelen, ga naar uw apparaatinstellingen."; "contacts_address_book_permission_denied_alert_title" = "Contacten uitgeschakeld"; "poll_edit_form_add_option" = "Optie toevoegen"; -"poll_edit_form_option_number" = "Optie %d"; +"poll_edit_form_option_number" = "Optie %lu"; "poll_edit_form_create_options" = "Opties maken"; "poll_edit_form_input_placeholder" = "Schrijf iets"; "poll_edit_form_question_or_topic" = "Vraag of onderwerp"; @@ -1647,3 +1647,46 @@ "share_extension_low_quality_video_title" = "Video zal in lage kwaliteit worden verstuurd"; "settings_discovery_accept_terms" = "Identiteitsserver-voorwaarden aanvaarden"; "settings_about" = "OVER"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_not_closed_subtitle" = "Probeer het opnieuw"; +"poll_timeline_not_closed_title" = "Sluiten van de poll mislukt"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_timeline_vote_not_registered_subtitle" = "Sorry, uw stem is niet geregistreerd. Probeer het opnieuw"; +"poll_timeline_vote_not_registered_title" = "Stem niet geregistreerd"; +"poll_timeline_total_final_results" = "Uitslag gebaseerd op %lu stemmen"; +"poll_timeline_total_final_results_one_vote" = "Uitslag gebaseerd op 1 stem"; +"poll_timeline_total_votes_not_voted" = "%lu stemmen uitgebracht. Stem om de resultaten te zien"; +"poll_timeline_total_one_vote_not_voted" = "1 stem uitgebracht. Stem om de resultaten te zien"; +"poll_timeline_total_votes" = "%lu stemmen uitgebracht"; +"poll_timeline_total_one_vote" = "1 stem uitgebracht"; +"poll_timeline_total_no_votes" = "Geen stemmen uitgebracht"; +"poll_timeline_votes_count" = "%lu stemmen"; +"poll_timeline_one_vote" = "1 stem"; +"poll_edit_form_post_failure_action" = "OK"; +"poll_edit_form_post_failure_subtitle" = "Probeer het opnieuw"; +"poll_edit_form_post_failure_title" = "Poll plaatsen mislukt"; +"analytics_prompt_stop" = "Delen stoppen"; +"analytics_prompt_yes" = "Ja, dat is prima"; +"analytics_prompt_not_now" = "Niet nu"; +"analytics_prompt_point_3" = "U kunt dit op elk moment uitzetten in de instellingen"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Wij delen geen informatie met derden"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "We registreren of profileren geen accountgegevens"; +"analytics_prompt_terms_link_upgrade" = "hier"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Lees al onze voorwaarden %@. Is dit akkoord?"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "U kunt al onze voorwaarden %@ lezen."; +"analytics_prompt_terms_link_new_user" = "hier"; +"analytics_prompt_message_upgrade" = "U heeft eerder toestemming gegeven om anonieme gebruiksgegevens met ons te delen. Om beter te begrijpen hoe mensen meerdere apparaten gebruiken, genereren we nu een willekeurige identificatiecode die door uw apparaten wordt gedeeld."; +"analytics_prompt_message_new_user" = "Help ons bij het identificeren van problemen en het verbeteren van Element door anonieme gebruiksgegevens te delen. Om te begrijpen hoe mensen meerdere apparaten gebruiken genereren we een willekeurige identificatie die we verspreiden over uw apparaten."; + +// Analytics +"analytics_prompt_title" = "Help %@ verbeteren"; +"settings_analytics_and_crash_data" = "Crash en analytische data versturen"; +"settings_labs_enabled_polls" = "Polls"; +"room_event_action_end_poll" = "Poll sluiten"; +"room_event_action_remove_poll" = "Poll verwijderen"; +"accessibility_button_label" = "knop"; +"enable" = "Inschakelen"; diff --git a/Riot/Assets/nn.lproj/Vector.strings b/Riot/Assets/nn.lproj/Vector.strings new file mode 100644 index 000000000..1eec1c558 --- /dev/null +++ b/Riot/Assets/nn.lproj/Vector.strings @@ -0,0 +1,11 @@ + + +"title_rooms" = "Rom"; +"title_people" = "Folk"; +"title_favourites" = "Favorittar"; + +// Titles +"title_home" = "Heim"; +"warning" = "Åtvaring"; +// String for App Store +"store_short_description" = "Sikker desentralisert chat/IP-telefoni"; diff --git a/Riot/Assets/pt.lproj/InfoPlist.strings b/Riot/Assets/pt.lproj/InfoPlist.strings new file mode 100644 index 000000000..9cf939453 --- /dev/null +++ b/Riot/Assets/pt.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ + + +"NSContactsUsageDescription" = "O Element vai mostrar os seus contactos para que os possa convidar para conversar."; +"NSMicrophoneUsageDescription" = "O Element necessita de aceder ao seu microfone para fazer e receber chamadas e para gravar mensagens de voz."; +"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."; diff --git a/Riot/Assets/pt.lproj/Localizable.strings b/Riot/Assets/pt.lproj/Localizable.strings new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Riot/Assets/pt.lproj/Localizable.strings @@ -0,0 +1 @@ + diff --git a/Riot/Assets/pt.lproj/Vector.strings b/Riot/Assets/pt.lproj/Vector.strings new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Riot/Assets/pt.lproj/Vector.strings @@ -0,0 +1 @@ + diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index ba473313e..7820fc1f4 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -1501,7 +1501,7 @@ "space_home_show_all_rooms" = "Mostrar todas as salas"; "room_event_action_forward" = "Encaminhar"; "poll_edit_form_add_option" = "Adicionar opção"; -"poll_edit_form_option_number" = "Opção %d"; +"poll_edit_form_option_number" = "Opção %lu"; "poll_edit_form_create_options" = "Criar opções"; "poll_edit_form_input_placeholder" = "Escreva algo"; "poll_edit_form_question_or_topic" = "Pergunta ou tópico"; @@ -1515,3 +1515,46 @@ "share_extension_low_quality_video_title" = "Vídeo vai ser enviado em baixa qualidade"; "settings_discovery_accept_terms" = "Aceitar Termos de Servidor de Identidade"; "settings_about" = "SOBRE"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_not_closed_subtitle" = "Por favor tente de novo"; +"poll_timeline_not_closed_title" = "Falha para terminar sondagem"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_timeline_vote_not_registered_subtitle" = "Desculpe, seu voto não foi registrado, por favor tente de novo"; +"poll_timeline_vote_not_registered_title" = "Voto não registrado"; +"poll_timeline_total_final_results" = "Resultados finais baseados em %lu votos"; +"poll_timeline_total_final_results_one_vote" = "Resultados finais baseados em 1 voto"; +"poll_timeline_total_votes_not_voted" = "%lu votos lançados. Vote para ver os resultados"; +"poll_timeline_total_one_vote_not_voted" = "1 voto lançado. Vote para ver os resultados"; +"poll_timeline_total_votes" = "%lu votos lançados"; +"poll_timeline_total_one_vote" = "1 voto lançado"; +"poll_timeline_total_no_votes" = "Nenhum voto lançado"; +"poll_timeline_votes_count" = "%lu votos"; +"poll_timeline_one_vote" = "1 voto"; +"poll_edit_form_post_failure_action" = "OK"; +"poll_edit_form_post_failure_subtitle" = "Por favor tente de novo"; +"poll_edit_form_post_failure_title" = "Falha para postar sondagem"; +"settings_labs_enabled_polls" = "Sondagens"; +"room_event_action_end_poll" = "Terminar sondagem"; +"room_event_action_remove_poll" = "Remover sondagem"; +"analytics_prompt_stop" = "Parar de compartilhar"; +"analytics_prompt_yes" = "Sim, pode ser"; +"analytics_prompt_not_now" = "Não agora"; +"analytics_prompt_point_3" = "Você pode desativar isto a qualquer hora em configurações"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Nós não compartilhamos informação com terceiros"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Nós não gravamos ou perfilamos quaisquer dados de conta"; +"analytics_prompt_terms_link_upgrade" = "aqui"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Leia todos os nossos termos %@. Isso está OK?"; +"analytics_prompt_terms_link_new_user" = "aqui"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Você pode ler todos os nossos termos %@."; +"analytics_prompt_message_upgrade" = "Você previamente consentiu a compartilhar dados de uso anônimos conosco. Agora, para ajudar a entender como pessoas usam múltiplos dispositivos, nós vamos gerar um identificador aleatório, compartilhado por seus dispositivos."; +"analytics_prompt_message_new_user" = "Ajude-nos a identificar problemas e melhorar Element ao compartilhar dados de uso anônimos. Para entender como pessoas usam múltiplos dispositivos, nós geramos um identificador aleatório, compartilhado por seus dispositivos."; + +// Analytics +"analytics_prompt_title" = "Ajudar a melhorar %@"; +"settings_analytics_and_crash_data" = "Enviar dados de crash e analítica"; +"accessibility_button_label" = "botão"; +"enable" = "Habilitar"; diff --git a/Riot/Assets/sk.lproj/Localizable.strings b/Riot/Assets/sk.lproj/Localizable.strings index 950c96d36..0ba5126ce 100644 --- a/Riot/Assets/sk.lproj/Localizable.strings +++ b/Riot/Assets/sk.lproj/Localizable.strings @@ -75,3 +75,93 @@ /** General **/ "NOTIFICATION" = "Oznámenia"; + +/* New message from a specific person in a named room */ +"MSG_FROM_USER_IN_ROOM" = "%@ napísal v %@"; + +/** Key verification **/ + +"KEY_VERIFICATION_REQUEST_FROM_USER" = "%@ žiada o overenie"; + +/* Group call from user, CallKit caller name */ +"GROUP_CALL_FROM_USER" = "%@ (Skupinový hovor)"; + +/* A user added a Jitsi call to a room */ +"GROUP_CALL_STARTED" = "Začal sa skupinový hovor"; + +/* Incoming named video conference invite from a specific person */ +"VIDEO_CONF_NAMED_FROM_USER" = "Skupinový videohovor od používateľa %@: '%@'"; + +/* Incoming named voice conference invite from a specific person */ +"VOICE_CONF_NAMED_FROM_USER" = "Skupinový hovor od používateľa %@: '%@'"; + +/* Incoming unnamed video conference invite from a specific person */ +"VIDEO_CONF_FROM_USER" = "Skupinový videohovor od používateľa %@"; + +/* Incoming unnamed voice conference invite from a specific person */ +"VOICE_CONF_FROM_USER" = "Skupinový hovor od používateľa %@"; + +/** Calls **/ + +/* Incoming one-to-one voice call */ +"VOICE_CALL_FROM_USER" = "Hovor od používateľa %@"; + +/* Incoming one-to-one video call */ +"VIDEO_CALL_FROM_USER" = "Videohovor od používateľa %@"; + +/* A user's membership has updated in an unknown way */ +"USER_MEMBERSHIP_UPDATED" = "%@ aktualizoval/a svoj profil"; + +/* A user has change their avatar */ +"USER_UPDATED_AVATAR" = "%@ zmenil/a svoj obrázok"; + +/* A user has change their name to a new name which we don't know */ +"GENERIC_USER_UPDATED_DISPLAYNAME" = "%@ zmenil/a svoje meno"; + +/** Membership Updates **/ + +/* A user has change their name to a new name */ +"USER_UPDATED_DISPLAYNAME" = "%@ zmenil/a svoje meno na %@"; + +/* A user has invited you to a named room */ +"USER_INVITE_TO_NAMED_ROOM" = "%@ vás pozval/a do %@"; + +/* A user has invited you to an (unamed) group chat */ +"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ vás pozval/a na skupinovú konverzáciu"; + +/** Invites **/ + +/* A user has invited you to a chat */ +"USER_INVITE_TO_CHAT" = "%@ vás pozval/a na konverzáciu"; + +/* A user has reacted to a message, but the reaction content is unknown */ +"GENERIC_REACTION_FROM_USER" = "%@ poslal/a reakciu"; + +/** Reactions **/ + +/* A user has reacted to a message, including the reaction e.g. "Alice reacted 👍". */ +"REACTION_FROM_USER" = "%@ reagoval/a %@"; + +/* Look, stuff's happened, alright? Just open the app. */ +"MSGS_IN_TWO_PLUS_ROOMS" = "%@ nových správ v %@, %@ a ďalších"; + +/* Multiple messages in two rooms */ +"MSGS_IN_TWO_ROOMS" = "%@ nových správ v %@ a %@"; + +/* Multiple unread messages from two plus people (ie. for 4+ people: 'others' replaces the third person) */ +"MSGS_FROM_TWO_PLUS_USERS" = "%@ nových správ od %@, %@ a ďalších"; + +/* Sticker from a specific person, not referencing a room. */ +"STICKER_FROM_USER" = "%@ poslal/a nálepku"; + +/* New file message from a specific person, not referencing a room. */ +"FILE_FROM_USER" = "%@ poslal/a súbor %@"; + +/* New voice message from a specific person, not referencing a room. */ +"VOICE_MESSAGE_FROM_USER" = "%@ poslal/a zvukovú správu"; + +/* New audio message from a specific person, not referencing a room. */ +"AUDIO_FROM_USER" = "%@ poslal/a zvukový súbor %@"; + +/* New message reply from a specific person in a named room. */ +"REPLY_FROM_USER_IN_ROOM_TITLE" = "%@ odpovedal/a v %@"; diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index f485f1f64..ded23fa74 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -165,3 +165,1457 @@ "callbar_only_single_active" = "Ťuknutím sa vrátite k hovoru (%@)"; "switch" = "Prepnúť"; "sending" = "Odosielanie"; +"settings_discovery_three_pids_management_information_part2" = "Používateľské nastavenia"; +"settings_key_backup_delete_confirmation_prompt_title" = "Vymazať zálohu"; +"settings_key_backup_button_delete" = "Vymazať zálohu"; +"settings_key_backup_info_algorithm" = "Algoritmus: %@"; +"settings_crypto_export" = "Exportovať kľúče"; +"settings_crypto_device_key" = "\nKľúč relácie:\n"; +"settings_crypto_device_id" = "\nID relácie: "; +"settings_crypto_device_name" = "Názov relácie: "; +"settings_confirm_password" = "Potvrdiť heslo"; +"settings_new_password" = "nové heslo"; +"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"; +"settings_messages_containing_user_name" = "My používateľské meno"; +"settings_group_messages" = "Skupinové správy"; +"settings_direct_messages" = "Priame správy"; +"settings_default" = "Predvolené oznámenia"; +"settings_notifications_disabled_alert_title" = "Oznámenia vypnuté"; +"settings_device_notifications" = "Oznámenia zariadenia"; +"settings_night_mode" = "Nočný režim"; +"settings_change_password" = "Zmeniť heslo"; +"settings_first_name" = "Krstné meno"; +"settings_display_name" = "Zobrazované meno"; +"settings_profile_picture" = "Profilový obrázok"; +"settings_sign_out" = "Odhlásiť sa"; +"settings_deactivate_account" = "DEAKTIVOVAŤ ÚČET"; +"settings_key_backup" = "ZÁLOHA KĽÚČA"; +"settings_phone_contacts" = "KONTAKTY TELEFÓNU"; +"settings_contacts" = "KONTAKTY ZARIADENIA"; +"settings_ignored_users" = "IGNOROVANÍ POUŽÍVATELIA"; +"settings_user_interface" = "POUŽÍVATEĽSKÉ ROZHRANIE"; +"settings_identity_server_settings" = "SERVER TOTOŽNOSTÍ"; +"settings_user_settings" = "POUŽÍVATEĽSKÉ NASTAVENIA"; +"settings_clear_cache" = "Vyprázdniť vyrovnávaciu pamäť"; +"settings_report_bug" = "Nahlásiť chybu"; +"room_preview_try_join_an_unknown_room_default" = "miestnosť"; +"room_title_one_member" = "1 člen"; +"room_title_members" = "%@ členovia"; +"room_title_invite_members" = "Pozvať členov"; + +// Room Title +"room_title_new_room" = "Nová miestnosť"; +"unknown_devices_title" = "Neznáme relácie"; +"unknown_devices_call_anyway" = "Napriek tomu zavolať"; +"unknown_devices_send_anyway" = "Napriek tomu odoslať"; +"room_place_voice_call" = "Audio hovor"; +"room_accessibility_hangup" = "Zavesiť"; +"room_accessibility_video_call" = "Video hovor"; +"room_action_send_file" = "Poslať súbor"; +"room_action_send_sticker" = "Odoslať nálepku"; +"room_event_action_reaction_show_less" = "Zobraziť menej"; +"room_event_action_reaction_show_all" = "Zobraziť všetko"; +"room_event_action_cancel_download" = "Zrušiť sťahovanie"; +"room_event_action_cancel_send" = "Zrušiť odosielanie"; +"room_event_action_report" = "Nahlásiť obsah"; +"room_event_action_view_source" = "Zobraziť zdroj"; +"room_prompt_cancel" = "Zrušiť všetky"; +"room_prompt_resend" = "Znovu odoslať všetky"; +"directory_cell_description" = "%tu miestnosti"; +"search_no_result" = "Žiadne výsledky"; + +// Room recents +"room_recents_directory_section" = "PRIEČINOK MIESTNOSTI"; + +// Social login + +"social_login_list_title_continue" = "Pokračovať s"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_edit_form_post_failure_action" = "OK"; +"settings_labs_enabled_polls" = "Ankety"; +"side_menu_action_feedback" = "Spätná väzba"; +"side_menu_action_help" = "Pomocník"; +"side_menu_action_settings" = "Nastavenia"; + +// Mark: - User avatar view + +"user_avatar_view_accessibility_label" = "obrázok"; + +// Mark: Avatar + +"space_avatar_view_accessibility_label" = "obrázok"; +"spaces_suggested_room" = "Navrhované"; +"spaces_left_panel_title" = "Priestory"; +"spaces_home_space_title" = "Domov"; +"space_beta_announce_badge" = "BETA"; +"room_intro_cell_information_dm_sentence1_part3" = ". "; +"room_intro_cell_information_room_sentence1_part3" = ". "; + +// Mark: - Room avatar view + +"room_avatar_view_accessibility_label" = "obrázok"; +"call_transfer_error_title" = "Chyba"; +"call_transfer_contacts_all" = "Všetky"; +"call_transfer_contacts_recent" = "Nedávne"; +"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_name" = "Názov"; +"biometrics_cant_unlocked_alert_message_retry" = "Skúsiť znovu"; +"pin_protection_settings_section_header" = "PIN"; +"pin_protection_reset_alert_action_reset" = "Obnoviť predvolené"; +"pin_protection_choose_pin_welcome_after_register" = "Vitajte."; +"secrets_reset_reset_action" = "Obnoviť predvolené"; +"secrets_setup_recovery_passphrase_confirm_passphrase_title" = "Potvrdiť"; +"secrets_setup_recovery_passphrase_validate_action" = "Hotovo"; +"secrets_setup_recovery_key_done_action" = "Hotovo"; +"secrets_setup_recovery_key_export_action" = "Uložiť"; +"secrets_setup_recovery_key_loading" = "Načítavanie…"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part3" = "."; + +// Session details + +"user_verification_session_details_trusted_title" = "Dôveryhodné"; +"user_verification_sessions_list_session_trusted" = "Dôveryhodné"; +"user_verification_sessions_list_table_title" = "Relácie"; +"user_verification_sessions_list_user_trust_level_unknown_title" = "Neznámy"; +"user_verification_sessions_list_user_trust_level_warning_title" = "Upozornenie"; + +// Sessions list + +"user_verification_sessions_list_user_trust_level_trusted_title" = "Dôveryhodné"; +"key_verification_tile_conclusion_done_title" = "Overené"; +"key_verification_tile_request_incoming_approval_decline" = "Odmietnuť"; +"key_verification_tile_request_incoming_approval_accept" = "Prijať"; +"key_verification_tile_request_status_waiting" = "Čakanie…"; + +// MARK: - Key Verification + +"key_verification_bootstrap_not_setup_title" = "Chyba"; + +// MARK: Reaction history +"reaction_history_title" = "Reakcie"; +"emoji_picker_symbols_category" = "Symboly"; +"emoji_picker_objects_category" = "Objekty"; +"emoji_picker_activity_category" = "Aktivity"; +"device_verification_emoji_headphones" = "Slúchadlá"; +"device_verification_emoji_anchor" = "Kotva"; +"device_verification_emoji_trumpet" = "Trúbka"; +"device_verification_emoji_guitar" = "Gitara"; +"device_verification_emoji_ball" = "Lopta"; +"device_verification_emoji_trophy" = "Trofej"; +"device_verification_emoji_rocket" = "Raketa"; +"device_verification_emoji_aeroplane" = "Lietadlo"; +"device_verification_emoji_bicycle" = "Bicykel"; +"device_verification_emoji_flag" = "Zástava"; +"device_verification_emoji_telephone" = "Telefón"; +"device_verification_emoji_hammer" = "Kladivo"; +"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_pencil" = "Ceruzka"; +"device_verification_emoji_book" = "Kniha"; +"device_verification_emoji_gift" = "Darček"; +"device_verification_emoji_clock" = "Budík"; +"device_verification_emoji_hourglass" = "Presýpacie hodiny"; +"device_verification_emoji_umbrella" = "Dáždnik"; +"device_verification_emoji_santa" = "Mikuláš"; +"device_verification_emoji_glasses" = "Okuliare"; +"device_verification_emoji_hat" = "Klobúk"; +"device_verification_emoji_robot" = "Robot"; +"device_verification_emoji_smiley" = "Úsmev"; +"device_verification_emoji_heart" = "Srdce"; +"device_verification_emoji_cake" = "Koláč"; +"device_verification_emoji_pizza" = "Pizza"; +"device_verification_emoji_corn" = "Kukurica"; +"device_verification_emoji_strawberry" = "Jahoda"; +"device_verification_emoji_apple" = "Jablko"; +"device_verification_emoji_banana" = "Banán"; +"device_verification_emoji_fire" = "Oheň"; +"device_verification_emoji_cloud" = "Oblak"; +"device_verification_emoji_moon" = "Mesiac"; +"device_verification_emoji_globe" = "Zemeguľa"; +"device_verification_emoji_mushroom" = "Huba"; +"device_verification_emoji_cactus" = "Kaktus"; +"device_verification_emoji_tree" = "Strom"; +"device_verification_emoji_flower" = "Kvet"; +"device_verification_emoji_butterfly" = "Motýľ"; +"device_verification_emoji_octopus" = "Chobotnica"; +"device_verification_emoji_fish" = "Ryba"; +"device_verification_emoji_turtle" = "Korytnačka"; +"device_verification_emoji_penguin" = "Tučniak"; +"device_verification_emoji_rooster" = "Kohút"; +"device_verification_emoji_panda" = "Panda"; +"device_verification_emoji_rabbit" = "Zajac"; +"device_verification_emoji_elephant" = "Slon"; +"device_verification_emoji_pig" = "Prasa"; +"device_verification_emoji_unicorn" = "Jednorožec"; +"device_verification_emoji_horse" = "Kôň"; +"device_verification_emoji_lion" = "Lev"; +"device_verification_emoji_cat" = "Mačka"; + +// MARK: Emoji +"device_verification_emoji_dog" = "Pes"; + +// MARK: Verified + +// Device + +"device_verification_verified_title" = "Overený!"; +"key_verification_manually_verify_device_validate_action" = "Overiť"; +"key_verification_self_verify_current_session_alert_validate_action" = "Overiť"; +"device_verification_self_verify_start_waiting" = "Čakanie…"; +"device_verification_self_verify_alert_validate_action" = "Overiť"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_backup_action" = "Zálohovať"; +"key_backup_recover_done_action" = "Hotovo"; +"key_backup_recover_from_recovery_key_recovery_key_title" = "Enter"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "."; +"key_backup_recover_from_passphrase_passphrase_title" = "Enter"; +"key_backup_setup_success_from_passphrase_done_action" = "Hotovo"; + +// Success + +"key_backup_setup_success_title" = "Úspech!"; +"key_backup_setup_passphrase_confirm_passphrase_valid" = "Super!"; +"key_backup_setup_passphrase_confirm_passphrase_title" = "Potvrdiť"; +"key_backup_setup_passphrase_passphrase_valid" = "Super!"; +"key_backup_setup_passphrase_passphrase_title" = "Enter"; +"key_backup_setup_intro_manual_export_info" = "(Pokročilé)"; +"key_backup_setup_skip_alert_skip_action" = "Preskočiť"; +"deactivate_account_forget_messages_information_part2_emphasize" = "Upozornenie"; +"service_terms_modal_decline_button" = "Odmietnuť"; +"service_terms_modal_accept_button" = "Prijať"; + +// Widget Picker +"widget_picker_title" = "Integrácie"; +"widget_menu_refresh" = "Obnoviť"; +"bug_report_send" = "Odoslať"; +"e2e_key_backup_wrong_version_button_settings" = "Nastavenia"; +"large_badge_value_k_format" = "%.1fK"; +"yesterday" = "Včera"; +"today" = "Dnes"; + +// Others +"or" = "alebo"; +"event_formatter_group_call_leave" = "Opustiť"; +"event_formatter_group_call_join" = "Vstúpiť"; +"event_formatter_call_retry" = "Skúsiť znovu"; +"event_formatter_call_answer" = "Odpovedať"; +"event_formatter_call_decline" = "Odmietnuť"; +"event_formatter_call_ringing" = "Vyzváňanie…"; +"event_formatter_call_connecting" = "Pripájanie…"; +"event_formatter_message_edited_mention" = "(upravené)"; +"directory_server_placeholder" = "matrix.org"; + +// Directory +"directory_title" = "Priečinok"; +"media_picker_select" = "Vybrať"; +"media_picker_library" = "Knižnica"; +"group_participants_invited_section" = "POZVANÍ"; +"group_participants_invite_prompt_title" = "Potvrdenie"; +"group_participants_remove_prompt_title" = "Potvrdenie"; +"group_details_rooms" = "Miestnosti"; +"group_details_people" = "Ľudia"; +"group_details_home" = "Domov"; +"room_notifs_settings_cancel_action" = "Zrušiť"; +"room_notifs_settings_done_action" = "Hotovo"; +"room_notifs_settings_none" = "Žiadne"; +"room_details_advanced_room_id_for_dm" = "ID:"; +"room_details_advanced_section" = "Pokročilé"; +"room_details_addresses_section" = "Adresy"; +"room_details_history_section_anyone" = "Ktokoľvek"; +"room_details_notifs" = "Oznámenia"; +"room_details_favourite_tag" = "Obľúbiť"; +"room_details_topic" = "Téma"; +"room_details_room_name_for_dm" = "Názov"; +"room_details_photo_for_dm" = "Fotka"; +"room_details_settings" = "Nastavenia"; +"room_details_integrations" = "Integrácie"; +"room_details_people" = "Členovia"; +"room_details_title_for_dm" = "Podrobnosti"; +"identity_server_settings_alert_disconnect_button" = "Odpojiť"; +"identity_server_settings_disconnect" = "Odpojiť"; +"identity_server_settings_change" = "Zmeniť"; +"identity_server_settings_add" = "Pridať"; +"security_settings_advanced" = "POKROČILÉ"; +"security_settings_cryptography" = "KRYPTOGRAFIA"; +"security_settings_crosssigning_reset" = "Obnoviť predvolené"; +"security_settings_crosssigning" = "KRÍŽOVÉ PODPISOVANIE"; +"security_settings_secure_backup_reset" = "Obnoviť predvolené"; +"security_settings_secure_backup_info_checking" = "Kontrolovanie…"; + +// Security settings +"security_settings_title" = "Zabezpečenie"; +"settings_discovery_three_pid_details_revoke_action" = "Odvolať"; +"settings_discovery_three_pid_details_share_action" = "Zdieľať"; +"settings_discovery_three_pids_management_information_part3" = "."; +"settings_key_backup_info_checking" = "Kontrolovanie…"; +"settings_copyright" = "Autorské práva"; +"settings_ui_theme_black" = "Čierny"; +"settings_ui_theme_dark" = "Tmavý"; +"settings_ui_theme_light" = "Svetlý"; +"settings_ui_theme" = "Vzhľad"; +"settings_ui_language" = "Jazyk"; +"settings_messages_containing_keywords" = "Kľúčové slová"; +"settings_messages_containing_at_room" = "@miestnosť"; +"settings_other" = "Ostatné"; +"settings_security" = "BEZPEČNOSŤ"; +"settings_three_pids_management_information_part3" = "."; +"settings_three_pids_management_information_part2" = "Objavovanie"; +"settings_phone_number" = "Telefón"; +"settings_email_address" = "Email"; +"settings_remove_prompt_title" = "Potvrdenie"; +"settings_surname" = "Priezvisko"; +"settings_cryptography" = "KRYPTOGRAFIA"; +"settings_devices" = "RELÁCIE"; +"settings_about" = "O APLIKÁCII"; +"settings_advanced" = "POKROČILÉ"; +"settings_integrations" = "INTEGRÁCIE"; +"settings_discovery_settings" = "OBJAVOVANIE"; +"settings_calls_settings" = "HOVORY"; +"settings_notifications" = "OZNÁMENIA"; +"settings_links" = "ODKAZY"; + +// Settings +"settings_title" = "Nastavenia"; +"unknown_devices_verify" = "Overiť…"; +"media_type_accessibility_sticker" = "Nálepka"; +"media_type_accessibility_file" = "Súbor"; +"media_type_accessibility_location" = "Miesto"; +"media_type_accessibility_video" = "Video"; +"media_type_accessibility_audio" = "Audio"; +"media_type_accessibility_image" = "Obrázok"; +"room_join_group_call" = "Vstúpiť"; +"room_accessibility_call" = "Hovor"; +"room_accessibility_upload" = "Nahrať"; +"room_accessibility_integrations" = "Integrácie"; +"room_accessibility_search" = "Hľadať"; +"room_resource_limit_exceeded_message_contact_1" = " Prosím "; +"room_action_reply" = "Odpovedať"; +"room_event_action_edit" = "Upraviť"; +"room_event_action_reply" = "Odpovedať"; +"room_event_action_delete" = "Vymazať"; +"room_event_action_resend" = "Znovu odoslať"; +"room_event_action_save" = "Uložiť"; +"room_event_action_permalink" = "Trvalý odkaz"; +"room_event_action_forward" = "Preposlať"; +"room_event_action_share" = "Zdieľať"; +"room_event_action_more" = "Viac"; +"room_event_action_redact" = "Odstrániť"; +"room_event_action_quote" = "Citovať"; +"room_event_action_copy" = "Kopírovať"; +"room_ongoing_conference_call_close" = "Zavrieť"; +"search_in_progress" = "Prehľadávanie…"; +"search_messages" = "Správy"; + +// Search +"search_rooms" = "Miestnosti"; +"group_section" = "KOMUNITY"; + +// Groups tab +"group_invite_section" = "POZVANIA"; + +// MARK: - Home + +"home_empty_view_title" = "Vitajte v %@,\n%@"; +"room_member_power_level_short_custom" = "Vlastná úroveň"; +"room_member_power_level_short_moderator" = "Moderátor"; +"room_member_power_level_short_admin" = "Správca"; +"room_member_power_level_custom_in" = "Vlastné (%@) v %@"; +"room_member_power_level_moderator_in" = "Moderátor v %@"; +"room_member_power_level_admin_in" = "Správca v %@"; +"room_participants_security_information_room_encrypted_for_dm" = "Správy sú tu šifrované end-to-end.\n\nVaše správy sú zabezpečené zámkami a jedinečné kľúče na ich odomknutie máte len vy a príjemca."; +"room_participants_security_information_room_encrypted" = "Správy v tejto miestnosti sú šifrované end-to-end.\n\nVaše správy sú zabezpečené zámkami a jedinečné kľúče na ich odomknutie máte len vy a príjemca."; +"room_participants_security_information_room_not_encrypted" = "Správy v tejto miestnosti nie sú šifrované end-to-end (od vás až k príjemcovi)."; +"room_participants_security_information_room_not_encrypted_for_dm" = "Správy tu nie sú šifrované end-to-end (od vás až k príjemcovi)."; +"room_participants_security_loading" = "Načítavanie…"; +"room_participants_action_security_status_loading" = "Načítavanie…"; +"room_participants_action_security_status_warning" = "Upozornenie"; +"room_participants_action_security_status_verify" = "Overiť"; +"room_participants_action_security_status_verified" = "Overený"; +"room_participants_action_mention" = "Zmieniť sa"; +"room_participants_action_start_video_call" = "Uskutočniť video hovor"; +"room_participants_action_start_voice_call" = "Uskutočniť hlasový hovor"; +"room_participants_action_start_new_chat" = "Začať novú konverzáciu"; +"room_participants_action_set_admin" = "Zmeniť na správcu"; +"room_participants_action_set_moderator" = "Udeliť stav moderátora"; +"room_participants_action_unignore" = "Zobraziť všetky správy od tohoto používateľa"; +"room_participants_action_ignore" = "Skryť všetky správy od tohoto používateľa"; +"room_participants_action_unban" = "Zrušiť zákaz"; +"room_participants_action_ban" = "Vylúčiť z tejto miestnosti"; +"room_participants_action_remove" = "Odstrániť z tejto miestnosti"; +"room_participants_action_leave" = "Opustiť túto miestnosť"; +"room_participants_action_invite" = "Pozvať"; +"room_participants_action_section_security" = "Zabezpečenie"; +"room_participants_action_section_other" = "Možnosti"; +"room_participants_action_section_devices" = "Relácie"; +"room_participants_action_section_direct_chats" = "Priame konverzácie"; +"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ť"; +"room_participants_multi_participants" = "%d účastníkov"; +"room_participants_one_participant" = "1 účastník"; +"room_participants_add_participant" = "Pridať účastníka"; +"contacts_address_book_permission_denied" = "Aplikácii %@ ste nepovolili prístup k svojim lokálnym kontaktom"; + +// Contacts +"contacts_address_book_section" = "LOKÁLNE KONTAKTY"; +"contacts_address_book_no_contact" = "Žiadne lokálne kontakty"; +"contacts_address_book_no_identity_server" = "Nebol nastavený server totožností"; +"contacts_address_book_matrix_users_toggle" = "Len používatelia Matrix"; +"directory_searching_title" = "Vyhľadávanie v priečinku…"; + +// Directory +"directory_cell_title" = "Prezrieť priečinok"; +"receipt_status_read" = "Prečítané: "; +"room_details_files" = "Nahrané súbory"; +"settings_ui_theme_auto" = "Automaticky"; + +// Success from passphrase +"key_backup_setup_success_from_passphrase_info" = "Vaše kľúče sa zálohujú.\n\nVáš bezpečnostný kľúč je bezpečnostná sieť - môžete ho použiť na obnovenie prístupu k zašifrovaným správam, ak zabudnete svoju prístupovú frázu.\n\nBezpečnostný kľúč uchovávajte na veľmi bezpečnom mieste, napríklad v správcovi hesiel (alebo v trezore)."; +"poll_timeline_not_closed_subtitle" = "Prosím, skúste to znova"; +"settings_unignore_user" = "Zobraziť všetky správy od %@?"; +"accessibility_button_label" = "tlačidlo"; +"settings_add_3pid_invalid_password_message" = "Neplatné poverovacie údaje"; +"security_settings_crypto_sessions" = "MOJE RELÁCIE"; + +// Deactivate account + +"deactivate_account_title" = "Deaktivovať účet"; +"service_terms_modal_information_title_integration_manager" = "Správca integrácie"; + +// Alert explaining what an identity server / integration manager is. +"service_terms_modal_information_title_identity_server" = "Server totožností"; +"e2e_room_key_request_ignore_request" = "Ignorovať žiadosť"; +"e2e_room_key_request_start_verification" = "Spustiť overovanie…"; +"share_extension_send_now" = "Odoslať teraz"; +"room_widget_permission_room_id_permission" = "ID miestnosti"; +"room_widget_permission_widget_id_permission" = "ID widgetu"; +"room_widget_permission_theme_permission" = "Váš vzhľad"; + +// Room widget permissions +"room_widget_permission_title" = "Načítať Widget"; +"widget_picker_manage_integrations" = "Spravovať integrácie…"; +"bug_report_send_screenshot" = "Odoslať snímku obrazovky"; +"event_formatter_group_call" = "Skupinový hovor"; +"event_formatter_call_end_call" = "Ukončiť hovor"; +"event_formatter_call_connection_failed" = "Pripojenie zlyhalo"; +"event_formatter_call_you_declined" = "Hovor odmietnutý"; +"event_formatter_call_has_ended" = "Hovor ukončený"; + +// Image picker +"image_picker_action_camera" = "Urobiť fotografiu"; +"group_participants_invite_malformed_id_title" = "Chyba pozvania"; +"group_participants_leave_prompt_title" = "Opustiť skupinu"; + +// Group participants +"group_participants_add_participant" = "Pridať účastníka"; +"group_home_multi_rooms_format" = "%tu miestnosti"; +"group_home_one_room_format" = "1 miestnosť"; +"group_home_multi_members_format" = "%tu členovia"; + +// Group Home +"group_home_one_member_format" = "1 člen"; + +// Group Details +"group_details_title" = "Podrobnosti o komunite"; +"room_notifs_settings_account_settings" = "Nastavenia účtu"; +"room_notifs_settings_all_messages" = "Všetky správy"; +"room_details_advanced_room_id" = "ID miestnosti:"; +"room_details_banned_users_section" = "Zakázaní používatelia"; +"room_details_flair_invalid_id_prompt_title" = "Neplatný formát"; +"room_details_direct_chat" = "Priama konverzácia"; +"room_details_mute_notifs" = "Stlmiť oznámenia"; +"room_details_low_priority_tag" = "Nízka priorita"; +"room_details_room_name" = "Názov miestnosti"; +"room_details_photo" = "Obrázok miestnosti"; +"room_details_search" = "Hľadať miestnosť"; +"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "Napriek tomu sa odpojiť"; + +// Identity server settings +"identity_server_settings_title" = "Server totožností"; +"manage_session_not_trusted" = "Nedôveryhodné"; +"manage_session_name" = "Názov relácie"; +"manage_session_info" = "INFORMÁCIE O RELÁCII"; + +// Manage session +"manage_session_title" = "Spravovať reláciu"; +"security_settings_secure_backup_setup" = "Nastaviť"; +"security_settings_secure_backup" = "BEZPEČNÉ ZÁLOHOVANIE"; +"security_settings_crypto_sessions_loading" = "Načítavanie relácií…"; +"security_settings_blacklist_unverified_devices" = "Nikdy neodosielať správy do nedôveryhodných relácií"; +"security_settings_crosssigning_bootstrap" = "Nastaviť"; +"security_settings_crosssigning_info_not_bootstrapped" = "Krížové podpisovanie ešte nie je nastavené."; +"security_settings_backup" = "ZÁLOHOVANIE SPRÁVY"; +"security_settings_secure_backup_delete" = "Vymazať zálohu"; +"bug_report_description" = "Popíšte prosím chybu. Čo ste urobili? Čo ste očakávali, že sa stane? Čo sa skutočne stalo?"; + +// Bug report +"bug_report_title" = "Hlásenie o chybe"; +"e2e_need_log_in_again" = "Musíte sa znova prihlásiť, aby ste vygenerovali end-to-end šifrovacie kľúče pre túto reláciu a odoslali verejný kľúč na váš domovský server.\nToto je jednorazové; ospravedlňujeme sa za nepríjemnosti."; + +// No VoIP support +"no_voip_title" = "Prichádzajúci hovor"; +"call_no_stun_server_error_message_2" = "Prípadne môžete skúsiť použiť verejný server na adrese %@, ale nebude to tak spoľahlivé a vaša IP adresa bude zdieľaná s týmto serverom. Môžete to spravovať aj v nastaveniach"; +"call_incoming_voice" = "Prichádzajúci hovor…"; + +// Call +"call_incoming_voice_prompt" = "Prichádzajúci hlasový hovor od %@"; +"call_incoming_video_prompt" = "Prichádzajúci videohovor od %@"; +"room_does_not_exist" = "%@ neexistuje"; +"camera_unavailable" = "Fotoaparát nie je k dispozícii na vašom zariadení"; +"camera_access_not_granted" = "%@ nemá povolenie používať fotoaparát, zmeňte to prosím nastaveniach ochrany súkromia"; +"do_not_ask_again" = "Viac sa nepýtať"; +"bug_report_prompt" = "Posledné spustenie aplikácie skončilo pádom. Chcete odoslať správu o páde?"; +"public_room_section_title" = "Verejné miestnosti (na %@):"; +"homeserver_connection_lost" = "Nepodarilo sa pripojiť k domovskému serveru."; +"key_backup_recover_invalid_passphrase_title" = "Nesprávna bezpečnostná fráza"; + +// MARK: Key backup recover + +"key_backup_recover_title" = "Zabezpečené správy"; + +// Success from secure backup +"key_backup_setup_success_from_secure_backup_info" = "Vaše kľúče sa zálohujú."; +"key_backup_setup_success_from_recovery_key_made_copy_action" = "Vytvoril som si kópiu"; +"key_backup_setup_success_from_recovery_key_make_copy_action" = "Vytvorte si kópiu"; +"key_backup_setup_success_from_recovery_key_recovery_key_title" = "Bezpečnostný kľúč"; +"key_backup_setup_success_from_passphrase_save_recovery_key_action" = "Uložte svoj bezpečnostný kľúč"; +"key_backup_setup_passphrase_set_passphrase_action" = "Nastaviť frázu"; +"key_backup_setup_passphrase_confirm_passphrase_placeholder" = "Potvrdiť frázu"; +"key_backup_setup_passphrase_passphrase_placeholder" = "Zadajte frázu"; + +// Passphrase + +"key_backup_setup_passphrase_title" = "Zabezpečte si svoju zálohu pomocou bezpečnostnej frázy"; +"key_backup_setup_intro_setup_connect_action_with_existing_backup" = "Pripojenie tohto zariadenia k službe Zálohovanie kľúčov"; +"key_backup_setup_intro_setup_action_without_existing_backup" = "Začať používať zálohovanie kľúčov"; +"key_backup_setup_skip_alert_message" = "Ak sa odhlásite alebo stratíte svoje zariadenie, môžete prísť o zabezpečené správy."; + + +// MARK: Key backup setup + +"key_backup_setup_title" = "Zálohovanie kľúčov"; +"secure_backup_setup_banner_subtitle" = "Zabezpečte sa proti strate šifrovaných správ a údajov"; + +// Banner + +"secure_backup_setup_banner_title" = "Bezpečné zálohovanie"; +"secure_key_backup_setup_intro_use_security_passphrase_title" = "Použiť bezpečnostnú frázu"; +"secure_key_backup_setup_intro_use_security_key_title" = "Použiť bezpečnostný kľúč"; +"secure_key_backup_setup_intro_info" = "Zabezpečte sa pred stratou šifrovaných správ a údajov zálohovaním šifrovacích kľúčov na domovskom serveri."; + +// MARK: Secure backup setup + +// Intro + +"secure_key_backup_setup_intro_title" = "Bezpečné zálohovanie"; +"rerequest_keys_alert_message" = "Spustite prosím %@ na inom zariadení, ktoré dokáže dešifrovať správu, aby mohlo poslať kľúče do tejto relácie."; + +// Re-request confirmation dialog +"rerequest_keys_alert_title" = "Žiadosť odoslaná"; +"deactivate_account_password_alert_message" = "Aby ste mohli pokračovať, prosím zadajte svoje heslo"; +"deactivate_account_password_alert_title" = "Deaktivovať účet"; +"deactivate_account_validate_action" = "Deaktivovať účet"; +"deactivate_account_informations_part2_emphasize" = "Táto akcia je nezvratná."; + +// Recovery passphrase + +"secrets_setup_recovery_passphrase_title" = "Nastaviť bezpečnostnú frázu"; +"secrets_setup_recovery_key_storage_alert_title" = "Udržujte ho v bezpečí"; +"secrets_setup_recovery_key_storage_alert_message" = "✓ Vytlačte ho a uložte na bezpečné miesto\n✓ Uložte ho na USB kľúč alebo záložný disk\n✓ Skopírujte si ho do osobného cloudového úložiska"; +"secrets_setup_recovery_key_information" = "Bezpečnostný kľúč uložte na bezpečné miesto. Môžete ho použiť na odomknutie zašifrovaných správ a údajov."; + +// MARK: - Secrets set up + +// Recovery Key + +"secrets_setup_recovery_key_title" = "Uložte svoj bezpečnostný kľúč"; +"secrets_recovery_with_key_invalid_recovery_key_message" = "Skontrolujte, či ste zadali správny bezpečnostný kľúč."; +"secrets_recovery_with_key_invalid_recovery_key_title" = "Nie je možné získať prístup k tajnému úložisku"; +"secrets_recovery_with_key_recovery_key_placeholder" = "Zadajte bezpečnostný kľúč"; +"secrets_recovery_with_passphrase_invalid_passphrase_message" = "Skontrolujte prosím, či ste zadali správnu bezpečnostnú frázu."; +"secrets_recovery_with_passphrase_invalid_passphrase_title" = "Nie je možné získať prístup k tajnému úložisku"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "použiť váš bezpečnostný kľúč"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "Neviete svoju bezpečnostnú frázu? Môžete "; +"secrets_recovery_with_passphrase_passphrase_placeholder" = "Vložiť bezpečnostnú frázu"; +"secrets_recovery_with_passphrase_information_verify_device" = "Na overenie tohto zariadenia použite bezpečnostnú frázu."; +"user_verification_session_details_information_untrusted_current_user" = "Overte túto reláciu, aby ste ju označili za dôveryhodnú a udelili jej prístup k zašifrovaným správam:"; +"user_verification_session_details_information_trusted_other_user_part2" = " ste ju overili:"; +"user_verification_session_details_information_trusted_other_user_part1" = "Táto relácia je dôveryhodná pre bezpečné zasielanie správ, pretože "; +"user_verification_session_details_information_trusted_current_user" = "Táto relácia je dôveryhodná pre bezpečné zasielanie správ, pretože ste ju overili:"; +"user_verification_sessions_list_information" = "Správy s týmto používateľom v tejto miestnosti sú end-to-end šifrované a tretie strany ich nemôžu čítať."; +"user_verification_start_additional_information" = "Aby ste si boli istý, urobte to osobne alebo použite iný dôveryhodný spôsob komunikácie."; +"user_verification_start_waiting_partner" = "Čaká sa na %@…"; +"user_verification_start_information_part2" = " skontrolovaním jednorazového kódu na oboch zariadeniach."; +"user_verification_start_information_part1" = "Pre väčšiu bezpečnosť overte "; +"key_verification_scan_confirmation_scanned_device_information" = "Zobrazuje druhé zariadenie rovnaký štít?"; +"key_verification_scan_confirmation_scanned_user_information" = "Zobrazuje %@ rovnaký štít?"; +"key_verification_scan_confirmation_scanning_device_waiting_other" = "Čakanie na druhé zariadenie…"; +"key_verification_scan_confirmation_scanning_user_waiting_other" = "Čaká sa na %@…"; + +// MARK: Scan confirmation + +// Scanning +"key_verification_scan_confirmation_scanning_title" = "Už je to takmer hotové! Čaká sa na potvrdenie…"; +"key_verification_verify_qr_code_scan_other_code_success_message" = "QR kód bol úspešne overený."; +"key_verification_verify_qr_code_other_scan_my_code_title" = "Naskenoval druhý používateľ QR kód úspešne?"; +"key_verification_verify_qr_code_start_emoji_action" = "Overte pomocou emoji"; +"key_verification_verify_qr_code_scan_code_other_device_action" = "Skenovať pomocou tohto zariadenia"; +"key_verification_verify_qr_code_scan_code_action" = "Naskenujte ich kód"; +"key_verification_verify_qr_code_emoji_information" = "Overenie porovnaním jedinečnej kombinácie emotikonov."; +"key_verification_verify_qr_code_information_other_device" = "Naskenujte nižšie uvedený kód na overenie:"; +"key_verification_verify_qr_code_information" = "Naskenujte kód, aby ste sa navzájom bezpečne overili."; + +// MARK: QR code + +"key_verification_verify_qr_code_title" = "Overte naskenovaním"; + +// Incoming key verification request + +"key_verification_incoming_request_incoming_alert_message" = "%@ žiada o overenie"; +"key_verification_tile_conclusion_warning_title" = "Nedôveryhodné prihlásenie"; +"error_not_supported_on_mobile" = "Toto nemôžete urobiť z mobilného telefónu %@."; +"emoji_picker_flags_category" = "Vlajky"; +"emoji_picker_places_category" = "Cestovanie a miesta"; +"emoji_picker_foods_category" = "Jedlo a nápoje"; +"emoji_picker_nature_category" = "Zvieratá a príroda"; + +// MARK: Emoji picker +"emoji_picker_title" = "Reakcie"; +"device_verification_emoji_bell" = "Zvonec"; +"device_verification_emoji_train" = "Vlak"; +"device_verification_emoji_spanner" = "Vidlicový kľúč"; + +// User + +"key_verification_verified_user_information" = "Správy s týmto používateľom sú end-to-end šifrované a tretie strany ich nemôžu čítať."; +"key_verification_verified_this_session_information" = "Na tomto zariadení teraz môžete čítať zabezpečené správy a ostatní používatelia budú vedieť, že mu môžu dôverovať."; +"key_verification_verified_new_session_information" = "Na novom zariadení teraz môžete čítať zabezpečené správy a ostatní používatelia budú vedieť, že mu môžu dôverovať."; +"key_verification_verified_other_session_information" = "Teraz môžete čítať zabezpečené správy vo vašej druhej relácii a ostatní používatelia budú vedieť, že jej môžu dôverovať."; +"key_verification_verified_new_session_title" = "Nová relácia overená!"; +"room_recents_unknown_room_error_message" = "Túto miestnosť nemôžeme nájsť. Uistite sa, že existuje"; +"room_creation_error_invite_user_by_email_without_identity_server" = "Nie je nakonfigurovaný žiadny server totožnosti, takže nemôžete pridať účastníka s e-mailom."; +"room_creation_appearance_picture" = "Obrázok konverzácie (nepovinné)"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Môžete si prečítať všetky naše podmienky %@."; +"analytics_prompt_terms_link_new_user" = "tu"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Prečítajte si všetky naše podmienky %@. Je to v poriadku?"; +"analytics_prompt_terms_link_upgrade" = "tu"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Nezaznamenávame ani neprofilujeme žiadne údaje o účte"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Nezdieľame informácie s tretími stranami"; +"analytics_prompt_point_3" = "Túto funkciu môžete kedykoľvek vypnúť v nastaveniach"; +"analytics_prompt_not_now" = "Teraz nie"; +"room_message_reply_to_placeholder" = "Odoslať odpoveď (nešifrovanú)…"; + + +// Room Details +"room_details_title" = "Podrobnosti o miestnosti"; +"settings_sending_media" = "ODOSIELANIE OBRÁZKOV A VIDEÍ"; +"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 +"room_preview_invitation_format" = "Do tejto miestnosti vás pozval používateľ %@"; +"unknown_devices_answer_anyway" = "Odpovedať aj tak"; +"unknown_devices_alert" = "Táto miestnosť obsahuje neznáme relácie, ktoré neboli overené.\nTo znamená, že nie je zaručené, že relácie patria naozaj tým používateľom, o ktorých to tvrdia.\nPred pokračovaním odporúčame prejsť procesom overenia každej relácie, ale ak chcete, môžete správu poslať znova bez overenia."; + +// Unknown devices +"unknown_devices_alert_title" = "V miestnosti sú neznáme relácie"; +"external_link_confirmation_message" = "Odkaz %@ vás presmeruje na inú stránku: %@\n\nSte si istí, že chcete pokračovať?"; +"room_no_privileges_to_create_group_call" = "Ak chcete začať hovor, musíte byť administrátorom alebo moderátorom."; +"room_open_dialpad" = "Číselník"; +"room_message_edits_history_title" = "Úpravy správy"; +"room_resource_usage_limit_reached_message_2" = "niektorí používatelia sa nebudú môcť prihlásiť."; +"room_resource_usage_limit_reached_message_1_monthly_active_user" = "Tento domovský server dosiahol svoj mesačný limit aktívnych používateľov, takže "; +"room_resource_usage_limit_reached_message_1_default" = "Bol prekročený limit využitia prostriedkov pre tento domovský server, takže "; +"room_resource_limit_exceeded_message_contact_2_link" = "kontaktovať vášho správcu služieb"; +"room_predecessor_link" = "Kliknutím sem zobrazíte staršie správy."; +"room_predecessor_information" = "Táto miestnosť je pokračovaním inej konverzácie."; +"room_replacement_link" = "Konverzácia pokračuje tu."; +"room_replacement_information" = "Táto miestnosť bola nahradená a nie je viac aktívna."; +"room_action_send_photo_or_video" = "Odoslať fotografiu alebo video"; +"room_action_camera" = "Spraviť fotografiu alebo video"; +"room_warning_about_encryption" = "End-to-end šifrovanie je vo fáze beta a nemusí byť spoľahlivé.\n\nNa zabezpečenie údajov by ste mu zatiaľ nemali dôverovať.\n\nZariadenia zatiaľ nebudú schopné dešifrovať históriu z obdobia pred ich pripojením do miestnosti.\n\nŠifrované správy nebudú viditeľné v klientoch, ktoré ešte nemajú zavedené šifrovanie."; +"room_event_action_reaction_history" = "História reakcií"; +"room_event_action_view_encryption" = "Informácie o šifrovaní"; +"room_event_action_delete_confirmation_message" = "Určite chcete túto neodoslanú správu vymazať?"; +"room_event_action_report_prompt_ignore_user" = "Chcete skryť všetky správy od tohto používateľa?"; +"room_event_action_ban_prompt_reason" = "Dôvod zákazu tohto používateľa"; +"room_event_action_kick_prompt_reason" = "Dôvod vylúčenia tohto používateľa"; +"room_event_action_report_prompt_reason" = "Dôvod nahlásenia tohto obsahu"; +"room_conference_call_no_power" = "Potrebujete povolenie na správu konferenčného hovoru v tejto miestnosti"; +"room_unsent_messages_cancel_message" = "Určite chcete vymazať všetky neodoslané správy v tejto miestnosti?"; +"room_unsent_messages_notification" = "Správy sa nepodarilo odoslať."; +"room_offline_notification" = "Spojenie so serverom bolo prerušené."; +"encrypted_room_message_reply_to_placeholder" = "Odoslať šifrovanú odpoveď…"; +"encrypted_room_message_placeholder" = "Odoslať šifrovanú správu…"; +"room_do_not_have_permission_to_post" = "Nemáte povolenie písať do tejto miestnosti"; +"room_message_replying_to" = "Odpoveď na %@"; +"room_message_unable_open_link_error_message" = "Nie je možné otvoriť odkaz."; +"room_message_placeholder" = "Odoslať správu (nešifrovane)…"; +"room_many_users_are_typing" = "%@, %@ a ďalší píšu…"; +"room_two_users_are_typing" = "%@ a %@ píšu…"; +"room_accessiblity_scroll_to_bottom" = "Prejsť na koniec"; + +// Chat +"room_slide_to_end_group_call" = "Posuňte na ukončenie výzvy pre všetkých"; +"room_participants_action_set_default_power_level" = "Obnoviť na normálnu úroveň používateľa"; +"room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Nie je nastavený žiadny server totožností, takže nemôžete začať konverzáciu s kontaktom pomocou e-mailu."; +"room_participants_invite_malformed_id_title" = "Chyba pozvania"; +"room_participants_invite_another_user" = "Vyhľadať / pozvať podľa ID používateľa, mena alebo e-mailu"; +"find_your_contacts_identity_service_error" = "Nie je možné sa pripojiť k serveru totožností."; +"find_your_contacts_footer" = "Túto funkciu môžete kedykoľvek vypnúť v nastaveniach."; +"contacts_user_directory_section" = "POUŽÍVATEĽSKÝ ADRESÁR"; +"contacts_user_directory_offline_section" = "POUŽÍVATEĽSKÝ ADRESÁR (offline)"; +"contacts_address_book_permission_denied_alert_message" = "Ak chcete povoliť kontakty, prejdite do nastavení zariadenia."; +"contacts_address_book_permission_denied_alert_title" = "Kontakty sú vypnuté"; +"contacts_address_book_permission_required" = "Povolenie potrebné na prístup k lokálnym kontaktom"; +"directory_search_results_more_than" = ">%tu nájdených výsledkov pre %@"; +"directory_search_results" = "%tu nájdených výsledkov pre %@"; +"search_people_placeholder" = "Vyhľadať podľa ID používateľa, mena alebo e-mailu"; +"rooms_empty_view_information" = "Miestnosti sú skvelé na akýkoľvek skupinový rozhovor, súkromný alebo verejný. Ťuknutím na tlačidlo + vyhľadajte existujúce miestnosti alebo vytvorte nové."; +"people_empty_view_information" = "Bezpečne komunikujte s kýmkoľvek.Ťuknutím na tlačidlo + začnite pridávať ľudí."; +"room_creation_wait_for_creation" = "Miestnosť sa už vytvára. Počkajte, prosím."; +"room_creation_make_public_prompt_msg" = "Ste si istí, že chcete túto konverzáciu zverejniť? Ktokoľvek si môže prečítať vaše správy a pripojiť sa ku konverzácii."; +"social_login_button_title_sign_in" = "Prihlásiť sa s %@"; +"auth_reset_password_error_is_required" = "Nie je nastavený žiadny server identity: pridajte ho v možnostiach servera a obnovte heslo."; +"security_settings_crosssigning_info_trusted" = "Krížové podpisovanie je povolené. Na základe krížového podpisovania môžete dôverovať iným používateľom a ostatným reláciám, ale nemôžete krížovo podpisovať z tejto relácie, pretože nemá súkromné kľúče na krížové podpisovanie. Dokončite zabezpečenie tejto relácie."; +"manage_session_sign_out" = "Odhlásiť sa z tejto relácie"; +"security_settings_crosssigning_info_ok" = "Krížové podpisovanie je pripravené na použitie."; +"settings_discovery_error_message" = "Vyskytla sa chyba. Prosím, skúste to znova."; +"settings_crypto_blacklist_unverified_devices" = "Šifrovať len overeným reláciám"; +"settings_password_updated" = "Vaše heslo bolo aktualizované"; +"settings_notifications_disabled_alert_message" = "Ak chcete povoliť upozornenia, prejdite do nastavení zariadenia."; +"settings_enable_push_notif" = "Oznámenia na tomto zariadení"; +"auth_autodiscover_invalid_response" = "Neplatná odpoveď pri zisťovaní domovského servera"; +"auth_accept_policies" = "Prosím prečítajte si a odsúhlaste zmluvné podmienky tohoto domovského servera:"; +"enable" = "Povoliť"; +// String for App Store +"store_short_description" = "Bezpečné decentralizované konverzácie/VoIP"; +"key_backup_setup_passphrase_confirm_passphrase_invalid" = "fráza sa nezhoduje"; +"key_backup_setup_intro_manual_export_action" = "Exportovať kľúče manuálne"; +"key_backup_setup_skip_alert_title" = "Ste si istí?"; + + +// Cancel + +"secure_key_backup_setup_cancel_alert_title" = "Ste si istí?"; +"deactivate_account_informations_part3" = "\n\nDeaktivácia vášho konta "; + +// Room key request dialog +"e2e_room_key_request_title" = "Žiadosť o šifrovací kľúč"; +"room_widget_permission_user_id_permission" = "Vaše ID používateľa"; +"room_widget_permission_avatar_url_permission" = "URL adresa vášho obrázku"; +"room_widget_permission_display_name_permission" = "Vaše zobrazované meno"; +"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"; +"event_formatter_call_active_voice" = "Aktívny hovor"; +"event_formatter_call_incoming_video" = "Prichádzajúci video hovor"; +"event_formatter_call_incoming_voice" = "Prichádzajúci hovor"; +"image_picker_action_library" = "Vybrať z knižnice"; + +// Read Receipts +"read_receipts_list" = "Zoznam potvrdení o prečítaní"; + +// Group rooms +"group_rooms_filter_rooms" = "Filtrovať komunitné miestnosti"; +"group_participants_filter_members" = "Filtrovať členov komunity"; +"room_details_copy_room_url" = "Kopírovať URL adresu miestnosti"; +"room_details_copy_room_address" = "Kopírovať adresu miestnosti"; +"room_details_copy_room_id" = "Kopírovať ID miestnosti"; +"room_details_addresses_invalid_address_prompt_title" = "Nesprávny formát aliasu"; +"room_details_new_address" = "Pridať novú adresu"; +"identity_server_settings_alert_disconnect_title" = "Odpojiť server totožností"; +"identity_server_settings_alert_change_title" = "Zmeniť server totožností"; +"security_settings_export_keys_manually" = "Exportovať kľúče manuálne"; +"security_settings_secure_backup_restore" = "Obnoviť zo zálohy"; +"settings_discovery_three_pid_details_cancel_email_validation_action" = "Zrušiť overenie e-mailu"; +"settings_discovery_three_pid_details_title_phone_number" = "Spravovať telefónne číslo"; +"settings_key_backup_button_restore" = "Obnoviť zo zálohy"; +"settings_deactivate_my_account" = "Deaktivovať účet"; +"settings_add_3pid_password_title_msidsn" = "Pridať telefónne číslo"; +"settings_add_3pid_password_title_email" = "Pridať emailovú adresu"; +"settings_term_conditions" = "Podmienky používania"; +"settings_olm_version" = "Olm verzia %@"; +"settings_contacts_enable_sync" = "Nájdite svoje kontakty"; +"settings_show_url_previews" = "Zobraziť náhľad webovej stránky"; +"settings_ui_theme_picker_title" = "Vybrať vzhľad"; +"settings_new_keyword" = "Pridať nové kľúčové slovo"; +"settings_messages_containing_display_name" = "Moje zobrazované meno"; +"settings_encrypted_group_messages" = "Šifrované skupinové správy"; +"settings_encrypted_direct_messages" = "Šifrované priame správy"; +"settings_mentions_and_keywords" = "Zmienky a kľúčové slová"; +"settings_show_decrypted_content" = "Zobraziť dešifrovaný obsah"; +"settings_add_phone_number" = "Pridať telefónne číslo"; +"settings_add_email_address" = "Pridať emailovú adresu"; +"settings_sign_out_confirmation" = "Ste si istí?"; +"settings_config_home_server" = "Domovský server je %@"; +"account_logout_all" = "Odhlásiť sa zo všetkých účtov"; +"room_title_one_active_member" = "%@/%@ aktívny člen"; +"room_title_multiple_active_members" = "%@/%@ aktívnych členov"; +"room_multiple_typing_notification" = "%@ a ďalší"; +"room_event_failed_to_send" = "Nepodarilo sa odoslať"; +"room_event_action_delete_confirmation_title" = "Vymazať neodoslanú správu"; +"room_event_action_view_decrypted_source" = "Zobraziť dešifrovaný zdroj"; +"room_delete_unsent_messages" = "Vymazať neodoslané správy"; +"room_resend_unsent_messages" = "Znovu odoslať neodoslané správy"; +"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ú"; +"find_your_contacts_button_title" = "Nájdite svoje kontakty"; +"social_login_button_title_continue" = "Pokračovať s %@"; +"auth_softlogout_clear_data_button" = "Vymazať všetky údaje"; +"auth_softlogout_clear_data" = "Vymazať osobné údaje"; +"auth_softlogout_signed_out" = "Ste odhlásený"; +"send_to" = "Odoslať do %@"; +"poll_timeline_one_vote" = "1 hlas"; +"room_event_action_end_poll" = "Ukončiť anketu"; +"room_event_action_remove_poll" = "Odstrániť anketu"; +"poll_edit_form_add_option" = "Pridať možnosť"; +"poll_edit_form_option_number" = "Možnosť %lu"; +"poll_edit_form_create_options" = "Vytvoriť možnosti"; +"poll_edit_form_input_placeholder" = "Napíšte niečo"; + +// Mark: - Polls + +"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 + +"side_menu_reveal_action_accessibility_label" = "Ľavý panel"; +"space_public_join_rule" = "Verejný priestor"; +"space_private_join_rule" = "Súkromný priestor"; +"spaces_coming_soon_title" = "Už čoskoro"; +"spaces_explore_rooms" = "Preskúmať miestnosti"; +"leave_space_title" = "Opustiť %@"; +"room_intro_cell_information_room_with_topic_sentence2" = "Téma: %@"; + +// Mark: - Room creation introduction cell + +"room_intro_cell_add_participants_action" = "Pridať ľudí"; +"call_transfer_dialpad" = "Číselník"; + +// MARK: - Dial Pad +"dialpad_title" = "Číselník"; +"room_info_list_several_members" = "%@ členovia"; + +// 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_enable_encryption" = "Povoliť šifrovanie"; +"create_room_section_header_encryption" = "Šifrovanie miestnosti"; +"create_room_section_header_name" = "Názov miestnosti"; + +// MARK: - Create Room + +"create_room_title" = "Nová miestnosť"; +"searchable_directory_x_network" = "%@ Sieť"; +"biometrics_desetup_disable_button_title_x" = "Vypnúť %@"; +"biometrics_desetup_title_x" = "Vypnúť %@"; +"biometrics_setup_enable_button_title_x" = "Povoliť %@"; +"biometrics_setup_title_x" = "Povoliť %@"; +"biometrics_settings_enable_x" = "Povoliť %@"; +"pin_protection_settings_change_pin" = "Zmeniť PIN"; +"pin_protection_settings_enable_pin" = "Zapnúť PIN"; + +// MARK: - PIN Protection + +"pin_protection_choose_pin_welcome_after_login" = "Vitajte späť."; +"major_update_done_action" = "Rozumiem"; +"major_update_learn_more_action" = "Zistiť viac"; + +// MARK: - Secrets reset + +"secrets_reset_title" = "Obnoviť všetko"; +"secrets_setup_recovery_passphrase_confirm_passphrase_placeholder" = "Potvrdiť frázu"; +"secrets_recovery_with_key_recover_action" = "Použiť kľúč"; + +// Recover with key + +"secrets_recovery_with_key_title" = "Bezpečnostný kľúč"; +"secrets_recovery_with_passphrase_recover_action" = "Použiť frázu"; + +// Recover with passphrase + +"secrets_recovery_with_passphrase_title" = "Bezpečnostná fráza"; +"secrets_recovery_reset_action_part_2" = "Obnoviť všetko"; +"user_verification_session_details_verify_action_other_user" = "Manuálne overiť"; +"user_verification_session_details_verify_action_current_user" = "Interaktívne overiť"; +"user_verification_session_details_untrusted_title" = "Nedôveryhodné"; +"user_verification_sessions_list_session_untrusted" = "Nedôveryhodné"; + +// MARK: - User verification + +// Start + +"user_verification_start_verify_action" = "Spustiť overenie"; + +// Scanned +"key_verification_scan_confirmation_scanned_title" = "Už to skoro bude!"; +"key_verification_verify_qr_code_scan_other_code_success_title" = "Kód overený!"; +"key_verification_verify_qr_code_cannot_scan_action" = "Nemôžete naskenovať?"; +"key_verification_tile_request_status_accepted" = "Prijali ste"; +"key_verification_tile_request_status_cancelled" = "%@ bolo zrušené"; +"key_verification_tile_request_status_cancelled_by_me" = "Zrušili ste overenie"; +"key_verification_tile_request_status_data_loading" = "Načítavanie údajov…"; +"key_verification_tile_request_outgoing_title" = "Overenie odoslané"; + +// Tiles + +"key_verification_tile_request_incoming_title" = "Žiadosť o overenie"; + +// MARK: File upload +"file_upload_error_title" = "Nahrávanie súboru"; +"device_verification_emoji_light bulb" = "Žiarovka"; +"device_verification_verified_got_it_button" = "Rozumiem"; +"key_verification_manually_verify_device_key_title" = "Kľúč relácie"; +"key_verification_manually_verify_device_id_title" = "ID relácie"; +"key_verification_manually_verify_device_name_title" = "Názov relácie"; +"key_verification_verify_sas_validate_action" = "Zhodujú sa"; + +// MARK: Verify + +"key_verification_verify_sas_title_emoji" = "Porovnať emoji"; +"device_verification_self_verify_start_verify_action" = "Spustiť overovanie"; + +// Recover from passphrase + +"key_backup_recover_from_passphrase_info" = "Použite svoju bezpečnostnú frázu na odomknutie histórie zabezpečených správ"; + +// Recover from private key +"key_backup_recover_from_private_key_info" = "Obnovenie zálohy…"; +"key_backup_recover_invalid_recovery_key" = "Zálohu sa nepodarilo dešifrovať pomocou tohto kľúča: overte prosím, či ste zadali správny bezpečnostný kľúč."; +"key_backup_recover_invalid_recovery_key_title" = "Nezhoda bezpečnostných kľúčov"; +"key_backup_recover_invalid_passphrase" = "Zálohu sa nepodarilo dešifrovať pomocou tejto frázy: overte prosím, či ste zadali správnu bezpečnostnú frázu."; +"key_backup_setup_passphrase_setup_recovery_key_action" = "( Pokročilé) Nastavenie pomocou bezpečnostného kľúča"; +"secure_key_backup_setup_cancel_alert_message" = "Ak to teraz zrušíte, môžete prísť o zašifrované správy a údaje, ak stratíte prístup k svojim prihlasovacím údajom.\n\nBezpečné zálohovanie a správu kľúčov môžete nastaviť aj v Nastaveniach."; +"secure_key_backup_setup_intro_use_security_passphrase_info" = "Zadajte tajnú frázu, ktorú poznáte len vy, a vygenerujte kľúč na zálohovanie."; +"secure_key_backup_setup_intro_use_security_key_info" = "Vygenerujte bezpečnostný kľúč a uložte ho na bezpečné miesto, napríklad do správcu hesiel alebo trezora."; +"deactivate_account_forget_messages_information_part3" = ": toto spôsobí, že budúci používatelia uvidia neúplný prehľad konverzácií)"; +"deactivate_account_forget_messages_information_part1" = "Prosím, zabudnite na všetky správy, ktoré som poslal, keď bude moje konto deaktivované ("; +"deactivate_account_informations_part5" = "Ak chcete, aby sme vaše správy zabudli, označte nižšie uvedené políčko\n\nViditeľnosť správ v Matrixe je podobná ako v prípade e-mailu. Naše zabudnutie vašich správ znamená, že správy, ktoré ste poslali, nebudú zdieľané s novými alebo neregistrovanými používateľmi, ale registrovaní používatelia, ktorí už majú prístup k týmto správam, budú mať stále prístup k ich kópii."; +"deactivate_account_informations_part1" = "Týmto sa vaše konto stane trvalo nepoužiteľným. Nebudete sa môcť prihlásiť a nikto nebude môcť opätovne zaregistrovať rovnaké ID používateľa. To spôsobí, že váš účet opustí všetky miestnosti, ktorých sa zúčastňuje, a odstráni údaje o vašom účte zo servera totožností. "; +"service_terms_modal_description_integration_manager" = "Toto vám umožní používať botov, premostenia, widgety a balíčky nálepiek."; +"service_terms_modal_description_identity_server" = "Vďaka tomu vás niekto nájde, ak má vaše telefónne číslo alebo e-mail uložený v kontaktoch telefónu."; +"widget_menu_revoke_permission" = "Zrušiť prístup pre mňa"; +"widget_sticker_picker_no_stickerpacks_alert_add_now" = "Pridať nejaké teraz?"; +"widget_creation_failure" = "Vytvorenie widgetu zlyhalo"; +"photo_library_access_not_granted" = "%@ nemá povolenie používať knižnicu fotografií, zmeňte to prosím nastaveniach ochrany súkromia"; +"event_formatter_jitsi_widget_removed_by_you" = "Odstránili ste konferenciu VoIP"; +"event_formatter_jitsi_widget_added_by_you" = "Pridali ste konferenciu VoIP"; +"event_formatter_call_back" = "Zavolať späť"; +"event_formatter_rerequest_keys_part2" = " z vašich ostatných relácií."; +"event_formatter_rerequest_keys_part1_link" = "Opätovne vyžiadať šifrovacie kľúče"; +"event_formatter_jitsi_widget_removed" = "VoIP konferenciu odstránil/a %@"; +"event_formatter_jitsi_widget_added" = "Konferenciu VoIP pridal/a %@"; +"room_notifs_settings_encrypted_room_notice" = "Upozorňujeme, že oznámenia o zmienkach a kľúčových slovách nie sú v mobilných zariadeniach k dispozícii v zašifrovaných miestnostiach."; +"room_details_fail_to_enable_encryption" = "Nepodarilo sa zapnúť šifrovanie v tejto miestnosti"; +"room_details_fail_to_update_room_directory_visibility" = "Nepodarilo sa aktualizovať viditeľnosť adresára miestnosti"; +"identity_server_settings_alert_error_terms_not_accepted" = "Ak ho chcete nastaviť %@ ako server identity, musíte prijať jeho podmienky."; +"identity_server_settings_alert_disconnect_still_sharing_3pid" = "Svoje osobné údaje stále zdieľate na serveri totožností %@.\n\nOdporúčame vám, aby ste pred odpojením odstránili svoje e-mailové adresy a telefónne čísla zo servera totožností."; +"identity_server_settings_disconnect_info" = "Odpojenie od servera totožností bude znamenať, že vás nebudú môcť objaviť iní používatelia a že nebudete môcť pozývať ostatných prostredníctvom e-mailu alebo telefónu."; +"identity_server_settings_description" = "V súčasnosti používate %@ na objavovanie existujúcich kontaktov, ktoré poznáte, a na to, aby vás mohli objaviť."; +"security_settings_crypto_sessions_description_2" = "Ak nespoznávate prihlásenie, zmeňte si heslo a vykonajte obnovenie funkcie Zabezpečené zálohovanie."; +"settings_identity_server_no_is_description" = "Momentálne nepoužívate žiadny server totožností. Ak chcete objaviť existujúce kontakty, ktoré poznáte, a byť nimi objaviteľní, pridajte jeden vyššie."; +"settings_analytics_and_crash_data" = "Odoslať údaje o páde a analytické údaje"; +"settings_labs_create_conference_with_jitsi" = "Vytvárajte konferenčné hovory pomocou aplikácie jitsi"; +"settings_callkit_info" = "Prijímajte prichádzajúce hovory na uzamknutej obrazovke. Prezrite si svoje %@ hovory v histórii hovorov systému. Ak je povolená služba iCloud, táto história hovorov bude zdieľaná so spoločnosťou Apple."; +"settings_notify_me_for" = "Informovať ma o"; +"poll_timeline_votes_count" = "%lu hlasov"; +"poll_edit_form_post_failure_subtitle" = "Prosím, skúste to znova"; +"version_check_modal_action_title_deprecated" = "Zistite ako"; +"version_check_modal_subtitle_deprecated" = "Pracovali sme na vylepšení %@, aby bolo používanie rýchlejšie a dokonalejšie. Bohužiaľ, vaša súčasná verzia systému iOS nie je kompatibilná s niektorými z týchto opráv a nebude ďalej podporovaná.\nOdporúčame vám aktualizovať operačný systém, aby ste mohli používať %@ naplno."; +"version_check_modal_title_deprecated" = "Už nepodporujeme iOS %@"; +"version_check_modal_subtitle_supported" = "Pracovali sme na vylepšení %@, aby bolo používanie rýchlejšie a dokonalejšie. Bohužiaľ, vaša súčasná verzia systému iOS nie je kompatibilná s niektorými z týchto opráv a nebude ďalej podporovaná.\nOdporúčame vám aktualizovať operačný systém, aby ste mohli používať %@ naplno."; +"version_check_modal_title_supported" = "Ukončujeme podporu pre iOS %@"; +"version_check_banner_subtitle_deprecated" = "V systéme iOS %@ už nepodporujeme %@. Ak chcete naďalej používať %@ naplno, odporúčame vám aktualizovať verziu systému iOS."; +"version_check_banner_title_deprecated" = "Už nepodporujeme iOS %@"; +"version_check_banner_subtitle_supported" = "Čoskoro ukončíme podporu pre %@ v systéme iOS %@. Ak chcete naďalej využívať plný potenciál %@, odporúčame vám aktualizovať verziu systému iOS."; + +// Mark: - Version check + +"version_check_banner_title_supported" = "Ukončujeme podporu pre iOS %@"; +"voice_message_stop_locked_mode_recording" = "Ťuknutím na nahrávku ju zastavíte alebo si ju vypočujete"; + +// Mark: - Voice Messages + +"voice_message_release_to_send" = "Podržaním nahrávate, uvoľnením odošlete"; +"user_avatar_view_accessibility_hint" = "Zmeniť obrázok používateľa"; +"space_avatar_view_accessibility_hint" = "Zmeniť obrázok priestoru"; +"space_home_show_all_rooms" = "Zobraziť všetky miestnosti"; +"space_participants_action_ban" = "Vylúčiť z tohto priestoru"; +"space_participants_action_remove" = "Odstrániť z tohto priestoru"; +"spaces_coming_soon_detail" = "Táto funkcia tu ešte nebola zapracovaná, ale je na ceste k jej zapracovaniu. Zatiaľ to môžete urobiť pomocou aplikácie Element v počítači."; +"spaces_invites_coming_soon_title" = "Pozvánky už čoskoro"; +"spaces_add_rooms_coming_soon_title" = "Pridávanie miestností už čoskoro"; +"spaces_no_member_found_detail" = "Hľadáte niekoho, kto nie je v %@? Zatiaľ ich môžete pozvať na webe alebo na počítači."; +"spaces_no_room_found_detail" = "Niektoré výsledky môžu byť skryté, pretože sú súkromné a na pripojenie k nim potrebujete pozvánku."; +"spaces_no_result_found_title" = "Nenašli sa žiadne výsledky"; +"spaces_empty_space_detail" = "Niektoré miestnosti môžu byť skryté, pretože sú súkromné a potrebujete pozvánku."; +"leave_space_only_action" = "Neopustiť žiadne miestnosti"; +"leave_space_message_admin_warning" = "Ste administrátorom tohto priestoru, pred odchodom sa uistite, že ste právo administrátora preniesli na iného člena."; +"leave_space_message" = "Ste si istí, že chcete opustiť %@? Chcete opustiť aj všetky miestnosti a priestory tohto priestoru?"; +"space_beta_announce_information" = "Priestory sú novým spôsobom zoskupovania miestností a ľudí. Zatiaľ nie sú v systéme iOS, ale už ich môžete používať na webe a počítači."; +"space_beta_announce_title" = "Priestory budú čoskoro"; +"space_feature_unavailable_information" = "Priestory sú novým spôsobom zoskupovania miestností a ľudí.\n\nBudú tu čoskoro. Zatiaľ platí, že ak sa k nim pripojíte na inej platforme, budete mať prístup ku všetkým miestnostiam, ku ktorým sa pripojíte tu."; +"space_feature_unavailable_subtitle" = "Spaces ešte nie sú v systéme iOS, ale už ich môžete používať na webe a počítači"; + +// Mark: - Spaces + +"space_feature_unavailable_title" = "Priestory tu ešte nie sú"; +"room_intro_cell_information_dm_sentence1_part1" = "Toto je začiatok vašej priamej správy s "; +"room_intro_cell_information_room_sentence1_part1" = "Toto je začiatok "; +"home_empty_view_information" = "Univerzálna aplikácia na bezpečný konverzáciu pre tímy, priateľov a organizácie. Ťuknutím na tlačidlo + nižšie pridáte ľudí a miestnosti."; +"create_room_section_footer_type" = "Do súkromnej miestnosti sa ľudia môžu pripojiť len s pozvánkou do miestnosti."; +"create_room_section_footer_encryption" = "Šifrovanie sa potom nedá vypnúť."; +"biometrics_cant_unlocked_alert_message_login" = "Prihlásiť sa znova"; +"biometrics_cant_unlocked_alert_message_x" = "Ak chcete odomknúť, použite %@ alebo sa prihláste a zapnite %@ znova"; +"biometrics_cant_unlocked_alert_title" = "Nie je možné odomknúť aplikáciu"; +"biometrics_usage_reason" = "Na prístup k aplikácii je potrebné overenie"; +"biometrics_setup_subtitle" = "Ušetrite si čas"; +"pin_protection_kick_user_alert_message" = "Príliš veľa chýb, boli ste odhlásení"; +"pin_protection_explanatory" = "Nastavenie kódu PIN umožňuje chrániť údaje, ako sú správy a kontakty, takže prístup k nim získate len vy zadaním kódu PIN na začiatku aplikácie."; +"pin_protection_not_allowed_pin" = "Z bezpečnostných dôvodov nie je tento kód PIN k dispozícii. Prosím, skúste iný PIN"; +"pin_protection_settings_enabled_forced" = "Zapnutý kód PIN"; +"pin_protection_settings_section_footer" = "Ak chcete obnoviť kód PIN, musíte sa znovu prihlásiť a vytvoriť nový kód PIN."; +"pin_protection_settings_section_header_with_biometrics" = "PIN a %@"; +"pin_protection_mismatch_too_many_times_error_message" = "Ak si nemôžete spomenúť na svoj kód PIN, ťuknite na tlačidlo Zabudnutý kód PIN."; +"pin_protection_mismatch_error_message" = "Prosím, skúste to znova"; +"pin_protection_mismatch_error_title" = "PIN kódy sa nezhodujú"; +"pin_protection_reset_alert_message" = "Ak chcete obnoviť kód PIN, musíte sa znovu prihlásiť a vytvoriť nový kód PIN"; +"secrets_reset_warning_message" = "Reštartujete bez histórie, bez správ, dôveryhodných zariadení a dôveryhodných používateľov."; +"secrets_reset_warning_title" = "Ak všetko obnovíte do pôvodného stavu"; +"secrets_reset_information" = "Urobte to len vtedy, ak nemáte žiadne iné zariadenie, pomocou ktorého by ste mohli toto zariadenie overiť."; +"secrets_recovery_with_key_information_default" = "Prístup k histórii zabezpečených správ a k totožnosti krížového podpisu na overenie iných relácií získate zadaním bezpečnostného kľúča."; +"user_verification_session_details_additional_information_untrusted_other_user" = "Pokiaľ tento používateľ nezačne tejto relácii dôverovať, správy odoslané do nej a z nej sú označené varovaním. Môžete ju tiež overiť ručne."; + +// MARK: Manually Verify Device + +"key_verification_manually_verify_device_title" = "Ručné overenie pomocou textu"; +"device_verification_self_verify_wait_recover_secrets_checking_availability" = "Kontrola ďalších možností overovania ..."; +"device_verification_self_verify_wait_recover_secrets_additional_information" = "Ak nemáte prístup k existujúcej relácii"; +"device_verification_error_cannot_load_device" = "Nie je možné načítať informácie o relácii."; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_message" = "Ak si pred odhlásením nezálohujete kľúče, stratíte prístup k zašifrovaným správam."; +"find_your_contacts_message" = "Nechajte %@ zobraziť svoje kontakty, aby ste mohli rýchlo začať komunikovať s tými, ktorých najlepšie poznáte."; +"callbar_only_multiple_paused" = "%@ pozastavené hovory"; +"identity_server_settings_alert_change" = "Odpojiť sa od servera totožnosti %1$@ a namiesto toho sa pripojiť k %2$@?"; +"identity_server_settings_alert_no_terms" = "Vami vybratý server totožností nemá podmienky používania služieb. Pokračujte len v prípade, že dôverujete vlastníkovi servera."; +"identity_server_settings_alert_no_terms_title" = "Server totožností nemá žiadne podmienky poskytovania služieb"; +"identity_server_settings_no_is_description" = "Momentálne nepoužívate server totožností. Ak chcete nájsť a byť objaviteľní existujúcimi kontaktmi, pridajte jeden vyššie."; +"security_settings_crosssigning_complete_security" = "Dokončiť zabezpečenie"; +"security_settings_crosssigning_info_exists" = "Váš účet má totožnosť krížového podpisu, ale táto relácia mu ešte nedôveruje. Dokončite zabezpečenie tejto relácie."; +"security_settings_secure_backup_description" = "Zálohujte si šifrovacie kľúče s údajmi o účte pre prípad, že stratíte prístup k reláciám. Vaše kľúče budú zabezpečené jedinečným bezpečnostným kľúčom."; +"settings_identity_server_description" = "Pomocou vyššie nastaveného servera totožnosti môžete nájsť existujúce kontakty, ktoré poznáte, a aj oni vás môžu nájsť."; +"settings_discovery_three_pid_details_information_phone_number" = "Spravujte predvoľby pre toto telefónne číslo, ktoré môžu ostatní používatelia použiť na vaše objavenie a pozvanie do miestností. Pridajte alebo odstráňte telefónne čísla v časti Účty."; +"settings_discovery_three_pids_management_information_part1" = "Spravujte, ktoré e-mailové adresy alebo telefónne čísla môžu ostatní používatelia použiť na vaše objavenie a pozvanie do miestností. E-mailové adresy alebo telefónne čísla môžete pridávať alebo odstraňovať z tohto zoznamu v "; +"settings_discovery_terms_not_signed" = "Musíte súhlasiť s podmienkami služby servera totožnosti (%@), aby vás mohli nájsť podľa e-mailovej adresy alebo telefónneho čísla."; +"settings_key_backup_button_connect" = "Pripojiť túto reláciu k zálohovaniu kľúčov"; +"settings_fail_to_update_password" = "Nepodarilo sa aktualizovať heslo"; +"settings_labs_enable_ringing_for_group_calls" = "Zvonenie pre skupinové hovory"; +"widget_integration_positive_power_level" = "Úroveň oprávnenia musí byť kladné celé číslo."; +"settings_integrations_allow_description" = "Použite správcu integrácií (%@) na spravovanie botov, premostení, widgetov a balíčkov s nálepkami. \n\nSprávcovia integrácie dostávajú konfiguračné údaje a môžu vo vašom mene upravovať widgety, posielať pozvánky do miestnosti a nastavovať úrovne oprávnení."; +"settings_mentions_and_keywords_encryption_notice" = "V mobilných zariadeniach nebudete dostávať upozornenia na zmienky a kľúčové slová v zašifrovaných miestnostiach."; +"settings_your_keywords" = "Vaše kľúčové slová"; +"settings_messages_by_a_bot" = "Správy od bota"; +"settings_call_invitations" = "Pozvánky na hovory"; +"settings_room_invitations" = "Pozvánky do miestnosti"; +"settings_global_settings_info" = "Všeobecné nastavenia oznámení sú k dispozícii vo webovom klientovi %@"; +"settings_confirm_media_size_description" = "Keď je táto funkcia zapnutá, budete vyzvaní, aby ste potvrdili, v akej veľkosti sa budú obrázky a videá odosielať."; +"settings_three_pids_management_information_part1" = "Tu môžete spravovať e-mailové adresy alebo telefónne čísla, ktoré môžete použiť na prihlásenie alebo obnovenie účtu. Ovládajte, kto vás môže nájsť v "; +"settings_fail_to_update_profile" = "Nepodarilo sa aktualizovať profil"; +"settings_email_address_placeholder" = "Zadajte svoju e-mailovú adresu"; +"settings_remove_phone_prompt_msg" = "Určite chcete odstrániť telefónne číslo %@?"; +"settings_remove_email_prompt_msg" = "Určite chcete odstrániť e-mailovú adresu %@?"; +"settings_sign_out_e2e_warn" = "Prídete o svoje end-to-end šifrovacie kľúče. To znamená, že v tomto zariadení už nebudete môcť čítať staré správy v zašifrovaných miestnostiach."; +"settings_labs" = "EXPERIMENTÁLNE"; +"room_ongoing_conference_call_with_close" = "Prebiehajúci konferenčný hovor. Pripojte sa ako %@ alebo %@. %@ to."; +"room_ongoing_conference_call" = "Prebiehajúci konferenčný hovor. Pripojte sa ako %@ alebo %@."; +"room_unsent_messages_unknown_devices_notification" = "Správu sa nepodarilo odoslať z dôvodu prítomnosti neznámych relácií."; +"room_participants_action_security_status_complete_security" = "Dokončiť zabezpečenie"; +"room_participants_invite_malformed_id" = "Chybné ID. Mala by to byť emailová adresa alebo Matrix ID ako napríklad \"@lokalnacast:domena\""; +"directory_search_fail" = "Nepodarilo sa načítať údaje"; +"directory_search_results_title" = "Prehľadávať výsledky adresára"; +"room_creation_dm_error" = "Nemohli sme vytvoriť vašu PS. Skontrolujte používateľov, ktorých chcete pozvať, a skúste to znova."; +"auth_softlogout_clear_data_sign_out_msg" = "Ste si istí, že chcete vymazať všetky údaje aktuálne uložené v tomto zariadení? Znovu sa prihláste, aby ste získali prístup k údajom a správam svojho účtu."; +"auth_softlogout_clear_data_message_2" = "Vyčistite to, ak ste skončili s používaním tohto zariadenia alebo sa chcete prihlásiť do iného účtu."; +"auth_softlogout_clear_data_message_1" = "Varovanie: Vaše osobné údaje (vrátane šifrovacích kľúčov) sú stále uložené na tomto zariadení."; +"auth_softlogout_recover_encryption_keys" = "Prihláste sa, aby ste obnovili šifrovacie kľúče uložené výlučne v tomto zariadení. Potrebujete ich na čítanie všetkých svojich zabezpečených správ na akomkoľvek zariadení."; +"auth_softlogout_reason" = "Váš správca domovského servera (%1$@) vás odhlásil z vášho účtu %2$@ (%3$@)."; +"auth_add_email_and_phone_warning" = "Registrácia pomocou e-mailu a telefónneho čísla naraz zatiaľ nie je podporovaná, kým nebude existovať api. Do úvahy sa bude brať iba telefónne číslo. Svoj e-mail môžete pridať do svojho profilu v nastaveniach."; +"auth_reset_password_success_message" = "Vaše heslo bolo obnovené.\n\nBoli ste odhlásení zo všetkých relácií a už nebudete dostávať push oznámenia. Ak chcete opätovne povoliť oznámenia, znovu sa prihláste na každom zariadení."; +"auth_untrusted_id_server" = "Server totožností nie je dôveryhodný"; +"auth_phone_is_required" = "Nie je nastavený žiadny server totožností, takže nemôžete pridať e-telefónne číslo, aby ste mohli v budúcnosti obnoviť svoje heslo."; +"auth_email_is_required" = "Nie je nastavený žiadny server totožností, takže nemôžete pridať e-mailovú adresu, aby ste mohli v budúcnosti obnoviť svoje heslo."; +"auth_add_email_phone_message_2" = "Nastavte si e-mail na obnovenie účtu. Neskôr môžete voliteľne použiť e-mail alebo telefón, aby vás mohli nájsť ľudia, ktorí vás poznajú."; +"cross_signing_setup_banner_subtitle" = "Jednoduchšie overenie ostatných zariadení"; +"room_details_advanced_e2e_encryption_blacklist_unverified_devices" = "Šifrovať len overeným reláciám"; +"spaces_empty_space_title" = "Tento priestor nemá žiadne miestnosti (zatiaľ)"; +"leave_space_and_all_rooms_action" = "Opustiť všetky miestnosti a priestory"; +"space_beta_announce_subtitle" = "Nová verzia komunít"; +"room_intro_cell_information_multiple_dm_sentence2" = "V tejto konverzácii ste len vy, pokiaľ niekto z vás niekoho nepozve, aby sa pripojil."; +"room_intro_cell_information_dm_sentence2" = "V tejto konverzácii ste len vy dvaja, nikto iný sa nemôže pripojiť."; +"room_intro_cell_information_room_without_topic_sentence2_part2" = " aby ľudia vedeli, o čom je táto miestnosť."; +"room_intro_cell_information_room_without_topic_sentence2_part1" = "Pridať tému"; +"room_avatar_view_accessibility_hint" = "Zmeniť obrázok miestnosti"; +"invite_friends_share_text" = "Hej, ozvi sa mi na %@: %@"; + +// MARK: - Invite friends + +"invite_friends_action" = "Pozvať priateľov do %@"; +"favourites_empty_view_information" = "Obľúbiť si môžete niekoľkými spôsobmi - najrýchlejšie je jednoducho stlačiť a podržať. Ťuknite na hviezdičku a automaticky sa tu zobrazia na bezpečné uloženie."; + +// 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é)"; +"searchable_directory_search_placeholder" = "Meno alebo ID"; + +// MARK: - Searchable Directory View Controller + +"searchable_directory_create_new_room" = "Vytvoriť novú miestnosť"; +"biometrics_mode_face_id" = "Face ID"; + +// MARK: - Biometrics Protection + +"biometrics_mode_touch_id" = "Touch ID"; +"pin_protection_reset_alert_title" = "Obnoviť PIN"; +"pin_protection_forgot_pin" = "Zabudli ste PIN kód"; +"pin_protection_enter_pin" = "Zadajte svoj kód PIN"; +"pin_protection_confirm_pin_to_change" = "Potvrďte kód PIN, na zmenu kódu PIN"; +"pin_protection_confirm_pin_to_disable" = "Potvrďte kód PIN, ak chcete deaktivovať kód PIN"; +"pin_protection_confirm_pin" = "Potvrďte svoj kód PIN"; +"pin_protection_choose_pin" = "Vytvoriť bezpečnostný kód PIN"; +"major_update_information" = "S radosťou oznamujeme, že sme zmenili názov! Vaša aplikácia je aktualizovaná a ste prihlásení do svojho účtu."; + +// MARK: - Major update + +"major_update_title" = "Riot je teraz %@"; + +// MARK: - Cross-signing + +// Banner + +"cross_signing_setup_banner_title" = "Nastaviť šifrovanie"; +"secrets_reset_authentication_message" = "Zadajte heslo svojho účtu pre potvrdenie"; +"secrets_setup_recovery_passphrase_summary_information" = "Zapamätajte si bezpečnostnú frázu. Môžete ju použiť na odomknutie zašifrovaných správ a údajov."; +"secrets_setup_recovery_passphrase_summary_title" = "Uložte si bezpečnostnú frázu"; +"secrets_setup_recovery_passphrase_confirm_information" = "Opätovne zadajte bezpečnostnú frázu a potvrďte ju."; +"secrets_setup_recovery_passphrase_additional_information" = "Nepoužívajte heslo k svojmu účtu."; +"secrets_setup_recovery_passphrase_information" = "Zadajte bezpečnostnú frázu, ktorú poznáte len vy a ktorá sa používa na zabezpečenie tajomstiev na vašom serveri."; +"secrets_recovery_with_key_recovery_key_title" = "Zadajte"; +"secrets_recovery_with_key_information_unlock_secure_backup_with_key" = "Ak chcete pokračovať, zadajte bezpečnostný kľúč."; +"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "Ak chcete pokračovať, zadajte bezpečnostnú frázu."; +"secrets_recovery_with_key_information_verify_device" = "Na overenie tohto zariadenia použite bezpečnostná kľúč."; +"secrets_recovery_with_passphrase_passphrase_title" = "Zadajte"; +"secrets_recovery_with_passphrase_information_default" = "Prístup k histórii zabezpečených správ a k totožnosti krížového podpisu na overenie iných relácií získate zadaním bezpečnostnej frázy."; + +// MARK: - Secrets recovery + +"secrets_recovery_reset_action_part_1" = "Zabudli ste alebo ste stratili všetky možnosti obnovy? "; +"user_verification_session_details_verify_action_current_user_manually" = "Ručné overenie pomocou textu"; +"user_verification_session_details_additional_information_untrusted_current_user" = "Ak ste sa do tejto relácie neprihlásili, vaše konto môže byť ohrozené."; +"key_verification_tile_request_status_expired" = "Vypršala platnosť"; + + +// Generic errors +"error_invite_3pid_with_no_identity_server" = "V nastaveniach pridajte server totožnosti, ak chcete pozývať e-mailom."; +"emoji_picker_people_category" = "Smajlíky a ľudia"; +"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 + +"device_verification_verify_wait_partner" = "Čakanie na potvrdenie od partnera…"; +"key_verification_manually_verify_device_additional_information" = "Ak sa nezhodujú, môže byť ohrozená bezpečnosť vašej komunikácie."; +"key_verification_manually_verify_device_instruction" = "Potvrďte to porovnaním nasledujúcich údajov s nastaveniami používateľa v inej relácii:"; +"key_verification_verify_sas_additional_information" = "V záujme maximálnej bezpečnosti použite iný dôveryhodný komunikačný prostriedok alebo to urobte osobne."; +"key_verification_verify_sas_cancel_action" = "Nezhodujú sa"; +"key_verification_verify_sas_title_number" = "Porovnať čísla"; +"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Použiť bezpečnostnú frázu alebo kľúč"; +"device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Použiť bezpečnostný kľúč"; +"device_verification_self_verify_wait_additional_information" = "Toto funguje s %@ a inými klientmi Matrix podporujúcimi krížové podpisovanie."; +"device_verification_self_verify_wait_information" = "Overte túto reláciu z niektorej z vašich ostatných relácií, čím jej udelíte prístup k zašifrovaným správam.\n\nNa ostatných zariadeniach použite najnovší %@:"; +"device_verification_self_verify_wait_new_sign_in_title" = "Overiť toto prihlásenie"; + +// MARK: Self verification wait + +"device_verification_self_verify_wait_title" = "Dokončiť zabezpečenie"; +"key_verification_self_verify_unverified_sessions_alert_validate_action" = "Overiť"; + +// Unverified sessions + +"key_verification_self_verify_unverified_sessions_alert_title" = "Zobraziť, kde ste prihlásený"; +"key_verification_self_verify_current_session_alert_message" = "Ostatní používatelia jej nemusia dôverovať."; + +// Current session + +"key_verification_self_verify_current_session_alert_title" = "Overiť túto reláciu"; +"device_verification_self_verify_start_information" = "Túto reláciu použite na overenie novej relácie, čím jej udelíte prístup k zašifrovaným správam."; +"device_verification_self_verify_alert_message" = "Overte nové prihlásenie prístupom k vášmu účtu: %@"; + +// MARK: Self verification start + +// New login +"device_verification_self_verify_alert_title" = "Nové prihlásenie. Boli ste to vy?"; +"device_verification_start_use_legacy_action" = "Použiť starší spôsob overenia"; +"device_verification_start_verify_button" = "Začať overenie"; +"device_verification_start_use_legacy" = "Nič sa neobjavuje? Nie všetci klienti ešte podporujú interaktívne overovanie. Použite staršie overovanie."; +"device_verification_start_wait_partner" = "Čaká sa na prijatie partnerom…"; + +// MARK: Start +"device_verification_start_title" = "Overiť porovnaním krátkeho textu"; +"device_verification_incoming_description_2" = "Overenie tejto relácie ju označí ako dôveryhodnú a zároveň označí vašu reláciu ako dôveryhodnú pre partnera."; +"device_verification_incoming_description_1" = "Overte túto reláciu a označte ju ako dôveryhodnú. Dôveryhodnosť relácií partnerov vám poskytuje pokoj na duši pri používaní end-to-end šifrovaných správ."; + +// Mark: Incoming +"device_verification_incoming_title" = "Prichádzajúca žiadosť o overenie"; +"device_verification_cancelled_by_me" = "Overovanie bolo zrušené. Dôvod: %@"; +"device_verification_cancelled" = "Druhá strana zrušila overenie."; +"device_verification_security_advice_number" = "Porovnajte čísla a uistite sa, že sú v rovnakom poradí."; +"device_verification_security_advice_emoji" = "Porovnajte jedinečné emoji a uistite sa, že sú zobrazené v rovnakom poradí."; +"key_verification_this_session_title" = "Overiť túto reláciu"; +"key_verification_new_session_title" = "Overte svoju novú reláciu"; + +// MARK: - Device Verification +"key_verification_other_session_title" = "Overiť reláciu"; +"sign_out_key_backup_in_progress_alert_cancel_action" = "Počkám"; +"sign_out_key_backup_in_progress_alert_discard_key_backup_action" = "Nezáleží mi na zašifrovaných správach"; +"sign_out_key_backup_in_progress_alert_title" = "Prebieha zálohovanie šifrovacích kľúčov. Ak sa teraz odhlásite, prídete o zašifrované správy."; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_sign_out_action" = "Odhlásiť sa"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_title" = "Stratíte svoje zašifrované správy"; +"sign_out_non_existing_key_backup_alert_discard_key_backup_action" = "Nezáleží mi na zašifrovaných správach"; +"sign_out_non_existing_key_backup_alert_setup_secure_backup_action" = "Začnite používať Bezpečné zálohovanie"; +"sign_out_existing_key_backup_alert_sign_out_action" = "Odhlásiť sa"; + +// MARK: Sign out warning + +"sign_out_existing_key_backup_alert_title" = "Naozaj sa chcete odhlásiť?"; + +// Success + +"key_backup_recover_success_info" = "Záloha obnovená!"; +"key_backup_recover_from_recovery_key_lost_recovery_key_action" = "Stratili ste kľúč obnovenia, v nastaveniach získate nový."; +"key_backup_recover_from_recovery_key_recover_action" = "Sprístupniť históriu"; +"key_backup_recover_from_recovery_key_recovery_key_placeholder" = "Zadajte bezpečnostný kľúč"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "použiť váš bezpečnostný kľúč"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Neviete svoju bezpečnostnú frázu? Môžete "; +"key_backup_recover_from_passphrase_recover_action" = "Sprístupniť históriu"; +"key_backup_recover_from_passphrase_passphrase_placeholder" = "Zadajte frázu"; + +// Success from recovery key +"key_backup_setup_success_from_recovery_key_info" = "Vaše kľúče sa zálohujú.\n\nVytvorte si kópiu tohto bezpečnostného kľúča a uschovajte ho na bezpečnom mieste."; +"key_backup_setup_passphrase_passphrase_invalid" = "Skúste pridať slovo"; +"key_backup_setup_passphrase_info" = "Šifrovanú kópiu vašich kľúčov uložíme na našom serveri. Chráňte svoju zálohu frázou, aby bola bezpečná.\n\nV záujme maximálneho zabezpečenia by sa mala líšiť od hesla k vášmu účtu."; +"key_backup_setup_intro_info" = "Správy v šifrovaných miestnostiach sú zabezpečené end-to-end šifrovaním. Kľúče na čítanie týchto správ máte len vy a príjemca (príjemcovia).\n\nKľúče si bezpečne zálohujte, aby ste ich nestratili."; +"secure_key_backup_setup_existing_backup_error_delete_it" = "Vymazať"; +"secure_key_backup_setup_existing_backup_error_unlock_it" = "Odomknúť"; +"secure_key_backup_setup_existing_backup_error_title" = "Záloha pre správy už existuje"; +"service_terms_modal_information_description_integration_manager" = "Správca integrácie umožňuje pridávať funkcie od tretích strán."; +"service_terms_modal_information_description_identity_server" = "Server totožností vám pomôže nájsť kontakty tak, že vyhľadá ich telefónne číslo alebo e-mailovú adresu a zistí, či už majú účet."; +"service_terms_modal_table_header_integration_manager" = "PODMIENKY SPRÁVCU INTEGRÁCIE"; +"service_terms_modal_table_header_identity_server" = "PODMIENKY SERVERA TOTOŽNOSTÍ"; +"service_terms_modal_footer" = "Toto môžete kedykoľvek vypnúť v nastaveniach."; + +// Service terms +"service_terms_modal_title_message" = "Ak chcete pokračovať, prijmite nižšie uvedené podmienky"; +"gdpr_consent_not_given_alert_review_now_action" = "Prečítať teraz"; + +// GDPR +"gdpr_consent_not_given_alert_message" = "Ak chcete pokračovať v používaní domovského servera %@, musíte si prečítať podmienky a súhlasiť s nimi."; +"e2e_room_key_request_share_without_verifying" = "Zdieľať bez overenia"; +"e2e_room_key_request_message" = "Vaša neoverená relácia '%@' požaduje šifrovacie kľúče."; +"e2e_room_key_request_message_new_device" = "Pridali ste novú reláciu \"%@\", ktorá požaduje šifrovacie kľúče."; +"share_extension_low_quality_video_message" = "Ak chcete poslať v lepšej kvalite, pošlite vo formáte %@ alebo pošlite v nízkej kvalite nižšie."; +"share_extension_low_quality_video_title" = "Video bude odoslané v nízkej kvalite"; +"room_widget_permission_information_title" = "Používaním zdieľate údaje s %@:\n"; +"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."; +"widget_integration_must_be_in_room" = "Nenachádzate sa v tejto miestnosti."; +"widget_integration_room_not_recognised" = "Nie je možné rozpoznať takúto miestnosť."; +"widget_integration_failed_to_send_request" = "Nepodarilo sa odoslať požiadavku."; +"widget_integration_unable_to_create" = "Nie je možné vytvoriť widget."; +"widget_menu_remove" = "Odstrániť pre všetkých"; +"widget_no_power_to_manage" = "Musíte mať povolenie spravovať widgety v tejto miestnosti"; + +// Widget +"widget_no_integrations_server_configured" = "Nie je nastavený žiadny server integrácií"; +"bug_report_progress_zipping" = "Získavajú sa záznamy"; +"bug_report_send_logs" = "Odoslať záznamy"; +"bug_crash_report_description" = "Opíšte prosím, čo ste robili pred pádom aplikácie:"; +"bug_crash_report_title" = "Správa o poruche"; +"e2e_key_backup_wrong_version" = "Bola zistená nová záloha zabezpečenej správy.\n\nAk ste to neboli vy, nastavte novú bezpečnostnú frázu v Nastaveniach."; + +// Key backup wrong version +"e2e_key_backup_wrong_version_title" = "Nová záloha kľúča"; + +// Crypto +"e2e_enabling_on_app_update" = "%@ teraz podporuje end-to-end šifrovanie, ale na jeho zapnutie sa musíte znova prihlásiť.\n\nMôžete to urobiť teraz alebo neskôr v nastaveniach aplikácie."; +"analytics_prompt_stop" = "Zastaviť zdieľanie"; +"analytics_prompt_yes" = "Áno, je to v poriadku"; +"analytics_prompt_message_upgrade" = "Predtým ste nám udelili súhlas so zdieľaním anonymných údajov o používaní. Teraz, aby sme pomohli pochopiť, ako ľudia používajú viacero zariadení, vygenerujeme náhodný identifikátor zdieľaný vašimi zariadeniami."; +"analytics_prompt_message_new_user" = "Pomôžte nám identifikovať problémy a zlepšiť Element zdieľaním anonymných údajov o používaní. Aby sme pochopili, ako ľudia používajú viacero zariadení, vygenerujeme náhodný identifikátor, ktorý zdieľajú vaše zariadenia."; + +// Analytics +"analytics_prompt_title" = "Pomôžte zlepšiť %@"; +"call_already_displayed" = "Telefonát už prebieha."; +"network_offline_prompt" = "Zdá sa, že nie ste pripojený na internet."; +"event_formatter_widget_removed_by_you" = "Odstránili ste widget: %@"; + +// Events formatter with you +"event_formatter_widget_added_by_you" = "Pridali ste widget: %@"; +"event_formatter_call_missed_video" = "Zmeškaný video hovor"; +"event_formatter_call_missed_voice" = "Zmeškaný hlasový hovor"; +"event_formatter_call_has_ended_with_time" = "Hovor ukončený - %@"; +"event_formatter_widget_removed" = "%@ widget odstránil %@"; +"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 +"media_picker_title" = "Knižnica médií"; +"group_participants_invite_malformed_id" = "Chybné ID. Malo by to byť Matrix ID ako napríklad “@lokalnacast:domena“"; +"group_participants_invite_another_user" = "Vyhľadať / pozvať podľa ID používateľa alebo mena"; +"group_participants_invite_prompt_msg" = "Ste si istí, že chcete pozvať %@ do tejto skupiny?"; +"group_participants_remove_prompt_msg" = "Ste si istí, že chcete odstrániť %@ z tejto skupiny?"; +"group_participants_leave_prompt_msg" = "Ste si istí, že chcete opustiť skupinu?"; +"group_invitation_format" = "%@ vás pozval do tejto komunity"; +"room_notifs_settings_manage_notifications" = "Oznámenia môžete spravovať v %@"; +"room_notifs_settings_mentions_and_keywords" = "Iba zmienky a kľúčové slová"; + +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "Upozorniť ma na"; +"room_details_unset_main_address" = "Zrušiť nastavenie ako hlavnej adresy"; +"room_details_set_main_address" = "Nastaviť ako hlavnú adresu"; +"room_details_save_changes_prompt" = "Chcete uložiť zmeny?"; +"room_details_fail_to_update_room_communities" = "Nepodarilo sa aktualizovať príbuzné komunity"; +"room_details_fail_to_update_room_canonical_alias" = "Nepodarilo sa aktualizovať hlavnú adresu"; +"room_details_fail_to_remove_room_aliases" = "Nepodarilo sa odstrániť adresy miestností"; +"room_details_fail_to_add_room_aliases" = "Nepodarilo sa pridať nové adresy miestností"; +"room_details_fail_to_update_history_visibility" = "Nepodarilo sa aktualizovať viditeľnosť histórie"; +"room_details_fail_to_update_room_guest_access" = "Nepodarilo sa aktualizovať prístup hosťa do miestnosti"; +"room_details_fail_to_update_avatar" = "Nepodarilo sa aktualizovať fotografiu miestnosti"; +"room_details_fail_to_update_topic" = "Nepodarilo sa aktualizovať tému"; +"room_details_fail_to_update_room_name" = "Nepodarilo sa aktualizovať názov miestnosti"; +"room_details_advanced_e2e_encryption_disabled_for_dm" = "Šifrovanie tu nie je zapnuté."; +"room_details_advanced_e2e_encryption_disabled" = "V tejto miestnosti nie je zapnuté šifrovanie."; +"room_details_advanced_e2e_encryption_enabled_for_dm" = "Je tu zapnuté šifrovanie"; +"room_details_advanced_e2e_encryption_enabled" = "V tejto miestnosti je zapnuté šifrovanie"; +"room_details_advanced_enable_e2e_encryption" = "Povoliť šifrovanie (Pozor: nie je možné ho znova vypnúť!)"; +"room_details_flair_invalid_id_prompt_msg" = "%@ nie je platný identifikátor komunity"; +"room_details_new_flair_placeholder" = "Pridať nové ID komunity (napr. +foo%@)"; +"room_details_addresses_invalid_address_prompt_msg" = "%@ nie je platný formát pre alias"; +"room_details_new_address_placeholder" = "Pridať novú adresu (napr. #foo%@)"; +"room_details_no_local_addresses_for_dm" = "Táto nemá žiadne lokálne adresy"; +"room_details_no_local_addresses" = "Táto miestnosť nemá žiadne lokálne adresy"; +"room_details_history_section_prompt_msg" = "Zmeny týkajúce sa toho, kto môže čítať históriu, sa budú vzťahovať len na budúce správy v tejto miestnosti. Viditeľnosť existujúcej histórie zostane nezmenená."; +"room_details_history_section_prompt_title" = "Upozornenie o ochrane súkromia"; +"room_details_history_section_members_only_since_joined" = "Len členovia (odkedy vstúpili)"; +"room_details_history_section_members_only_since_invited" = "Len členovia (odkedy boli pozvaní)"; +"room_details_history_section_members_only" = "Len členovia (odkedy je táto voľba aktivovaná)"; +"room_details_history_section" = "Kto môže čítať históriu?"; +"room_details_access_section_directory_toggle_for_dm" = "Uverejniť v adresári miestností"; +"room_details_access_section_directory_toggle" = "Uverejniť túto miestnosť v adresári miestností"; +"room_details_access_section_no_address_warning" = "Ak chcete vytvoriť odkaz do miestnosti, musíte najprv nastaviť jej adresu"; +"room_details_access_section_anyone_for_dm" = "Ktokoľvek, kto pozná odkaz, vrátane hostí"; +"room_details_access_section_anyone_apart_from_guest" = "Ktokoľvek, kto pozná odkaz na miestnosť, okrem hostí"; +"room_details_access_section_anyone_apart_from_guest_for_dm" = "Ktokoľvek, kto pozná odkaz, okrem hostí"; +"room_details_access_section_anyone" = "Ktokoľvek, kto pozná odkaz na miestnosť, vrátane hostí"; +"room_details_access_section_invited_only" = "Iba ľudia, ktorí boli pozvaní"; +"room_details_access_section_for_dm" = "Kto má k tomuto prístup?"; +"room_details_access_section" = "Kto má prístup do tejto miestnosti?"; +"identity_server_settings_alert_error_invalid_identity_server" = "%@ nie je platný server totožností."; +"identity_server_settings_alert_disconnect" = "Odpojiť sa od servera totožností %@?"; +"identity_server_settings_place_holder" = "Zadajte server totožností"; + +// AuthenticatedSessionViewControllerFactory +"authenticated_session_flow_not_supported" = "Táto aplikácia nepodporuje mechanizmus overovania na vašom domovskom serveri."; +"security_settings_user_password_description" = "Potvrďte svoju totožnosť zadaním hesla k účtu"; +"security_settings_coming_soon" = "Prepáčte. Táto akcia zatiaľ nie je dostupná v systéme %@ iOS. Na jej nastavenie použite iného klienta Matrix. Systém %@ iOS to bude používať."; +"security_settings_complete_security_alert_title" = "Dokončiť zabezpečenie"; +"security_settings_complete_security_alert_message" = "Najprv by ste mali dokončiť zabezpečenie aktuálnej relácie."; +"security_settings_blacklist_unverified_devices_description" = "Overte všetky relácie používateľov, aby ste ich mohli označiť za dôveryhodné a odosielať im správy."; +"security_settings_secure_backup_info_valid" = "Táto relácia zálohuje vaše kľúče."; +"settings_show_NSFW_public_rooms" = "Zobraziť verejné miestnosti nevhodné do práce"; +"settings_identity_server_no_is" = "Nebol nastavený server totožností"; +"settings_discovery_three_pid_details_enter_sms_code_action" = "Zadajte aktivačný kód SMS"; +"settings_discovery_three_pid_details_information_email" = "Spravujte predvoľby pre túto e-mailovú adresu, ktorú môžu ostatní používatelia použiť na vaše objavenie a pozvanie do miestností. Pridávajte alebo odstraňujte e-mailové adresy v časti Účty."; +"settings_discovery_three_pid_details_title_email" = "Spravovať e-mail"; +"settings_discovery_accept_terms" = "Akceptovať podmienky servera totožnosti"; +"settings_discovery_no_identity_server" = "V súčasnosti nepoužívate server totožností. Ak chcete, aby vás existujúce kontakty mohli nájsť, pridajte si jeden."; +"settings_devices_description" = "Verejný názov relácie je viditeľný pre ľudí, s ktorými komunikujete"; +"settings_key_backup_delete_confirmation_prompt_msg" = "Ste si istý? Ak nie sú vaše kľúče správne zálohované, prídete o svoje zašifrované správy."; +"settings_key_backup_button_create" = "Začnite používať zálohovanie kľúčov"; +"settings_key_backup_info_trust_signature_invalid_device_unverified" = "Záloha má neplatný podpis z %@"; +"settings_key_backup_info_trust_signature_invalid_device_verified" = "Záloha má neplatný podpis z %@"; +"settings_key_backup_info_trust_signature_valid_device_unverified" = "Záloha má podpis z %@"; +"settings_key_backup_info_trust_signature_valid_device_verified" = "Zálohovanie má platný podpis z %@"; +"settings_key_backup_info_trust_signature_valid" = "Zálohovanie má platný podpis z tejto relácie"; +"settings_key_backup_info_trust_signature_unknown" = "Záloha má podpis z relácie s ID: %@"; +"settings_key_backup_info_progress_done" = "Všetky kľúče sú zálohované"; +"settings_key_backup_info_progress" = "Zálohovanie %@ kľúčov…"; +"settings_key_backup_info_not_valid" = "Táto relácia nezálohuje vaše kľúče, ale máte existujúcu zálohu, ktorú môžete obnoviť a pridať do budúcnosti."; +"settings_key_backup_info_valid" = "Táto relácia zálohuje vaše kľúče."; +"settings_key_backup_info_version" = "Verzia zálohy kľúča: %@"; +"settings_key_backup_info_signout_warning" = "Zálohujte si šifrovacie kľúče pred odhlásením, aby ste o ne neprišli."; +"settings_key_backup_info_none" = "Vaše kľúče sa z tejto relácie nezálohujú."; +"settings_key_backup_info" = "Šifrované správy sú zabezpečené end-to-end šifrovaním. Kľúče na čítanie týchto správ máte len vy a príjemca (príjemcovia)."; +"settings_add_3pid_password_message" = "Aby ste mohli pokračovať, prosím zadajte svoje heslo"; +"settings_labs_message_reaction" = "Reagujte na správy pomocou emoji"; +"settings_labs_e2e_encryption_prompt_message" = "Ak chcete dokončiť nastavenie šifrovania, musíte sa znova prihlásiť."; +"settings_contacts_enable_sync_description" = "Na prepojenie s vašimi kontaktmi sa použije váš server totožností a pomôže im vás nájsť."; +"settings_show_url_previews_description" = "Náhľady sa zobrazia len v nezašifrovaných miestnostiach."; + +// Recover from recovery key + +"key_backup_recover_from_recovery_key_info" = "Použite svoju bezpečnostný kľúč na odomknutie histórie zabezpečených správ"; diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index 75f1fc60e..bca45030c 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -222,7 +222,7 @@ "settings_ignored_users" = "PËRDORUES TË SHPËRFILLUR"; "settings_contacts" = "KONTAKTE PAJISJEJE"; "settings_advanced" = "TË MËTEJSHME"; -"settings_other" = "TË TJERA"; +"settings_other" = "Tjetër"; "settings_devices" = "SESIONE"; "settings_cryptography" = "KRIPTOGRAFI"; "settings_sign_out" = "Dilni"; @@ -1519,3 +1519,63 @@ "contacts_address_book_permission_denied_alert_message" = "Që të aktivizoni kontakte, kaloni te rregullimet e pajisjes tua."; "contacts_address_book_permission_denied_alert_title" = "Kontaktet u çaktivizuan"; "space_home_show_all_rooms" = "Shfaqi krejt dhomat"; +"poll_timeline_not_closed_action" = "OK"; +"poll_timeline_not_closed_subtitle" = "Ju lutemi, riprovoni"; +"poll_timeline_not_closed_title" = "S’u arrit të përfundohej pyetësori"; +"poll_timeline_vote_not_registered_action" = "OK"; +"poll_timeline_vote_not_registered_subtitle" = "Na ndjeni, vota juaj s’u regjistrua, ju lutemi, riprovoni"; +"poll_timeline_vote_not_registered_title" = "Votë e paregjistruar"; +"poll_timeline_total_final_results" = "Rezultati përfundimtar, bazua në %lu votë"; +"poll_timeline_total_final_results_one_vote" = "Rezultati përfundimtar, bazua në 1 votë"; +"poll_timeline_total_votes_not_voted" = "%lu vota të hedhura. Që të shihni përfundimet, votoni"; +"poll_timeline_total_one_vote_not_voted" = "1 votë e hedhur. Që të shihni përfundimet, votoni"; +"poll_timeline_total_votes" = "%lu vota të hedhura"; +"poll_timeline_total_one_vote" = "1 votë e hedhur"; +"poll_timeline_total_no_votes" = "S’u votua gjë"; +"poll_timeline_votes_count" = "%lu vota"; +"poll_timeline_one_vote" = "1 votë"; +"poll_edit_form_post_failure_action" = "OK"; +"poll_edit_form_post_failure_subtitle" = "Ju lutemi, riprovoni"; +"poll_edit_form_post_failure_title" = "S’u arrit të postohej anketimi"; +"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_question_or_topic" = "Pyetje ose temë"; +"poll_edit_form_poll_question_or_topic" = "Pyetje ose temë pyetësori"; + +// Mark: - Polls + +"poll_edit_form_create_poll" = "Krijoni anketim"; +"share_extension_send_now" = "Dërgoje tani"; +"share_extension_low_quality_video_message" = "Dërgojeni në %@. për cilësi më të mirë, ose dërgojeni në cilësi të ulët si më poshtë."; +"share_extension_low_quality_video_title" = "Videoja do të dërgohet në cilësi të ulët"; +"analytics_prompt_stop" = "Resht së ndari"; +"analytics_prompt_yes" = "Po, s’ka problem"; +"analytics_prompt_not_now" = "Jo tani"; +"analytics_prompt_point_3" = "Këtë mund të çaktivizoni në çfarëdo kohe, që nga rregullimet"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Nuk u japin hollësi palëve të treta"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Nuk regjistrojmë ose profilizojmë ndonjë të dhënë llogarie"; +"analytics_prompt_terms_link_upgrade" = "këtu"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "Lexoni krejt kushtet tona %@. Në rregull?"; +"analytics_prompt_terms_link_new_user" = "këtu"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_new_user */ +"analytics_prompt_terms_new_user" = "Mund të lexoni krejt kushtet tona %@."; +"analytics_prompt_message_upgrade" = "Keni pranuar më herët të ndani me ne të dhëna anonime përdorimi. Tani, që të na ndihmoni të kuptojmë se si njerëzit përdorin pajisje të shumta, do të prodhojmë një identifikues kuturu, të përbashkët për pajisjet tuaja."; +"analytics_prompt_message_new_user" = "Ndihmonani të identifikojmë probleme dhe të përmirësojmë Element-in, duke ndarë me ne të dhëna anonime përdorimi. Për të kuptuar se si i përdorin njerëzit disa pajisje njëherësh, do të prodhojmë një identifikues kuturu, të përbashkët për pajisjet tuaja."; + +// Analytics +"analytics_prompt_title" = "Ndihmoni të përmirësohet %@"; +"settings_discovery_accept_terms" = "Pranoni Kushte Shërbyesi Identitetesh"; +"settings_analytics_and_crash_data" = "Dërgoni të dhëna vithisjesh dhe analitike"; +"settings_labs_enabled_polls" = "Pyetësorë"; +"settings_about" = "MBI"; +"room_event_action_forward" = "Përpara"; +"room_event_action_end_poll" = "Përfundoje pyetësorin"; +"room_event_action_remove_poll" = "Hiqe pyetësorin"; +"accessibility_button_label" = "kopsë"; +"open" = "Hapur"; +"enable" = "Aktivizoje"; diff --git a/Riot/Assets/third_party_licenses.html b/Riot/Assets/third_party_licenses.html index b615810b2..ee1a8d284 100644 --- a/Riot/Assets/third_party_licenses.html +++ b/Riot/Assets/third_party_licenses.html @@ -1897,6 +1897,34 @@ Library. SOFTWARE.

+
  • + PostHog iOS (https://github.com/PostHog/posthog-ios) +

    + The MIT License (MIT) +

    + Copyright (c) 2020 PostHog (part of Hiberly Inc) +

    + Copyright (c) 2016 Segment.io, Inc. +

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

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

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

    +
  • diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index abf77edfd..7694f2d5e 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -163,7 +163,7 @@ "room_participants_leave_prompt_msg_for_dm" = "Ви впевнені, що хочете вийти?"; "room_participants_leave_prompt_title_for_dm" = "Вийти"; "client_android_name" = "Element Android"; -"store_promotional_text" = "Застосунок для бесід та співпраці, що зберігає приватність у відкритій мережі. Децентралізований, щоб надати вам контроль над даними. Без обробки даних, без бекдорів, без доступу для третіх сторін."; +"store_promotional_text" = "Застосунок для спілкування та співпраці, що зберігає приватність у відкритій мережі. Децентралізований, щоб надати вам контроль над даними. Без обробки даних, без бекдорів, без доступу для третіх сторін."; "settings_three_pids_management_information_part3" = "."; "settings_three_pids_management_information_part1" = "Керуйте звідси адресами е-пошти чи номерами телефонів, які можна застосовувати для входу або відновлення облікового запису. Контролюйте хто і як може вас знайти "; "contacts_address_book_no_identity_server" = "Сервер ідентифікації не налаштований"; @@ -388,7 +388,7 @@ "settings_identity_server_settings" = "СЕРВЕР ІДЕНТИФІКАЦІЇ"; "settings_calls_settings" = "ВИКЛИКИ"; "settings_notifications" = "СПОВІЩЕННЯ"; -"settings_user_settings" = "НАЛАШТУВАННЯ КОРИСТУВАЧА"; +"settings_user_settings" = "КОРИСТУВАЦЬКІ НАЛАШТУВАННЯ"; "event_formatter_call_connecting" = "З'єднання…"; "settings_config_identity_server" = "Сервер ідентифікації %@"; "settings_config_home_server" = "Домашній сервер %@"; @@ -408,7 +408,7 @@ // Bug report "bug_report_title" = "Звіт про ваду"; -"e2e_key_backup_wrong_version_button_wasme" = "Це був я"; +"e2e_key_backup_wrong_version_button_wasme" = "Це я"; "e2e_key_backup_wrong_version_button_settings" = "Налаштування"; "settings_privacy_policy" = "Політика приватності"; "settings_term_conditions" = "Умови та положення"; @@ -433,7 +433,7 @@ "room_widget_permission_theme_permission" = "Ваша тема"; "room_widget_permission_user_id_permission" = "Ваш ID користувача"; "room_widget_permission_avatar_url_permission" = "URL-адреса вашого аватара"; -"room_widget_permission_display_name_permission" = "Ваше показуване ім'я"; +"room_widget_permission_display_name_permission" = "Ваше показуване імʼя"; "room_widget_permission_creator_info_title" = "Цей віджет додано:"; // Room widget permissions @@ -591,7 +591,7 @@ "user_avatar_view_accessibility_label" = "аватар"; "room_intro_cell_information_dm_sentence1_part3" = ". "; -"room_intro_cell_information_room_without_topic_sentence2_part1" = "Додати тему"; +"room_intro_cell_information_room_without_topic_sentence2_part1" = "Додайте тему,"; "room_intro_cell_information_room_with_topic_sentence2" = "Тема: %@"; "room_intro_cell_information_room_sentence1_part3" = ". "; "room_intro_cell_information_room_sentence1_part1" = "Це початок "; @@ -1002,7 +1002,7 @@ // Room key request dialog "e2e_room_key_request_title" = "Запит ключів шифрування"; "share_extension_send_now" = "Надіслати зараз"; -"service_terms_modal_accept_button" = "Прийняти"; +"service_terms_modal_accept_button" = "Погодитися"; "room_details_flair_invalid_id_prompt_msg" = "%@ — неправильний ідентифікатор спільноти"; "room_details_flair_invalid_id_prompt_title" = "Неправильний формат"; "room_details_new_flair_placeholder" = "Додати новий ID спільноти (напр., +foo%@)"; @@ -1022,3 +1022,683 @@ "room_details_history_section_members_only" = "Лише учасники (від часу вибору цієї опції)"; "room_details_history_section_anyone" = "Будь-хто"; "room_details_history_section" = "Хто може переглядати історію?"; +"identity_server_settings_alert_disconnect" = "Від'єднатися від сервера ідентифікації %@?"; +"identity_server_settings_alert_disconnect_title" = "Від'єднатися від сервера ідентифікації"; +"identity_server_settings_alert_change" = "Від'єднатися від сервера ідентифікації %1$@ й натомість під'єднатися до %2$@?"; +"identity_server_settings_alert_change_title" = "Змінити сервер ідентифікації"; +"identity_server_settings_alert_no_terms" = "Обраний вами сервер ідентифікації не має жодних умов використання. Продовжуйте лише якщо довіряєте власнику сервісу."; +"identity_server_settings_alert_no_terms_title" = "Сервер ідентифікації не має умов використання"; +"identity_server_settings_disconnect_info" = "Після від'єднання від сервера ідентифікації вас більше не зможуть знаходити інші користувачі, а ви не зможете запрошувати інших за е-поштою чи телефоном."; +"identity_server_settings_no_is_description" = "Зараз ви не використовуєте сервер ідентифікації. Щоб знаходити наявні контакти й вони знаходили вас, додайте сервер угорі."; +"identity_server_settings_description" = "Ви користуєтесь %@, щоб знаходити людей за наявними контактами й вони знаходили вас."; + +// AuthenticatedSessionViewControllerFactory +"authenticated_session_flow_not_supported" = "Цей застосунок не підтримує способу входу, доступного на вашому домашньому сервері."; +"security_settings_user_password_description" = "Підтвердьте свою особу, ввівши пароль свого облікового запису"; +"security_settings_coming_soon" = "Вибачте, ця дія ще не доступна в %@ iOS. Якщо налаштуєте це іншим клієнтом Matrix, %@ iOS зможе також це використати."; +"security_settings_complete_security_alert_message" = "Спершу слід доповнити захист вашого поточного сеансу."; +"security_settings_blacklist_unverified_devices_description" = "Звірте всі сеанси користувача, щоб позначити його довіреним і надіслати йому повідомлення."; +"security_settings_crosssigning_bootstrap" = "Налаштувати"; +"security_settings_crosssigning_info_ok" = "Перехресне підписування готове до використання."; +"security_settings_crosssigning_info_trusted" = "Перехресне підписування увімкнено. Ви можете робити інших користувачів і свої інші сеанси довіреними на підставі перехресного підпису, але ви не можете перехресно підписувати цим сеансом, бо в нього ще нема закритих ключів перехресного підписування. Доповніть захист цього сеансу."; +"security_settings_crosssigning_info_exists" = "Ваш обліковий запис має ідентичність перехресного підписування, але вона ще не довірена цим сеансом. Доповніть захист цього сеансу."; +"security_settings_secure_backup_description" = "Зробіть резервну копію своїх ключів шифрування й даних облікового запису на випадок втрати доступу до своїх сеансів. Ваші ключі будуть захищені унікальним ключем безпеки."; +"security_settings_crypto_sessions_description_2" = "Якщо не впізнаєте вхід, скиньте пароль і налаштування безпечного резервного копіювання."; +"settings_identity_server_no_is_description" = "Зараз ви не використовуєте сервер ідентифікації. Щоб знаходити наявні контакти й вони знаходили вас, додайте сервер угорі."; +"settings_identity_server_description" = "Налаштований угорі сервер ідентифікації дає вам змогу знаходити людей за наявними контактами, а їм знаходити вас."; +"settings_discovery_three_pid_details_information_phone_number" = "Керуйте параметрами для цього номера телефону, за яким інші користувачі можуть вас знаходити й запрошувати до кімнат. Додавайте й видаляйте номери в Облікових записах."; +"settings_discovery_three_pid_details_information_email" = "Керуйте параметрами для цієї адреси е-пошти, за якою інші користувачі можуть вас знаходити й запрошувати до кімнат. Додавайте й видаляйте адреси в Облікових записах."; +"settings_discovery_three_pids_management_information_part1" = "Керуйте, за якими адресами е-пошти й номерами телефону інші користувачі зможуть вас знаходити й запрошувати до кімнат. Щоб додати адреси й номери в цей список чи вилучити наявні, перейдіть у "; +"settings_discovery_no_identity_server" = "Зараз ви не використовуєте сервер ідентифікації. Щоб наявні контакти могли вас знаходити, додайте такий сервер."; +"settings_key_backup_info_not_valid" = "Цей сеанс не створює резервної копії ваших ключів, але у вас уже є резервна копія, яку ви можете відновити й додати у майбутньому."; +"settings_key_backup_info_version" = "Версія резервного копіювання ключів: %@"; +"settings_labs_message_reaction" = "Реагувати на повідомлення за допомогою емодзі"; +"settings_contacts_enable_sync_description" = "Це використовуватиме ваш сервер ідентифікації, щоб ви знаходили свої контакти, а вони вас."; +"settings_integrations_allow_description" = "Використовуйте менеджер інтеграцій (%@), щоб керувати ботами, мостами, віджетами й пакунками наліпок.\n\nМенеджери інтеграцій отримують ваші параметри й можуть змінювати віджети, надсилати запрошення до кімнат і надавати повноваження від вашого імені."; +"settings_calls_stun_server_fallback_description" = "Дозволити допоміжний сервер викликів %@, коли ваш домашній сервер не надає свого (ваша IP-адреса ставатиме відомою при виклику)."; +"settings_callkit_info" = "Отримувати вхідні виклики, не розблоковуючи екран. Перегляньте свої виклики (%@) в історії викликів системи. Якщо iCloud увімкнено, ця історія викликів надсилатиметься Apple."; +"settings_confirm_media_size_description" = "Коли це ввімкнено, при надсиланні зображень чи відео вам пропонуватиметься підтвердити їхній розмір."; +"settings_three_pids_management_information_part2" = "Знаходження"; +"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" = "Поки жоден сервер ідентифікації не налаштований, ви не можете почати бесіду з кимось за адресою е-пошти."; +"find_your_contacts_message" = "Дозвольте %@ показувати ваші контакти, щоб ви могли швидко почати бесіду з тими, кого знаєте найкраще."; +"find_your_contacts_title" = "Почніть із переліку своїх контактів"; +"store_full_description" = "Element — застосунок для листування й співпраці нового покоління:\n\n1. Надає вам контроль над збереженням вашої приватності\n2. Дає змогу спілкуватися з будь-ким у мережі Matrix і навіть за її межами, інтегруючись із такими застосунками, як Slack\n3. Оберігає вас від реклами, збору даних, бекдорів і прив'язаності до провайдера\n4. Захищає вас наскрізним шифруванням і звіркою інших перехресним підписуванням\n\nElement суттєво відрізняється від інших застосунків для листування й співпраці тим, що децентралізований і має відкритий код.\n\nElement дає змогу самостійно встановити сервер або обрати з-поміж загальнодоступних, щоб ви зберігали приватність своїх даних і розмов, власність і контроль над ними. Він надає вам доступ до відкритої мережі; тож ви можете спілкуватися з користувачами інших застосунків, не лише Element. А ще він добре захищений.\n\nElement здатен на це все завдяки своїй основі Matrix — стандарту відкритого, децентралізованого спілкування.\n\nElement надає вам контроль, даючи змогу обрати, в кого зберігаються ваші розмови. У застосунку Element ви можете обрати між такими шляхами:\n\n1. Зареєструвати безплатний обліковий запис на загальнодоступному сервері matrix.org\n2. Самостійно розмістити свій обліковий запис, встановивши сервер на власному обладнанні\n3. Отримати обліковий запис на виділеному сервері, просто передплативши хостинг-платформу Element Matrix Services\n\nЧому Element?\n\nВОЛОДІЙТЕ СВОЇМИ ДАНИМИ: Ви обираєте, де зберігати свої дані й повідомлення. Ви володієте й керуєте ними, не якась МЕГАКОРПОРАЦІЯ, що аналізує ваші дані й передає їх стороннім особам.\n\nВІДКРИТЕ ЛИСТУВАННЯ Й СПІВПРАЦЯ: Можете розмовляти з будь-ким іншим у мережі Matrix незалежно від того, використовують вони Element, інший застосунок Matrix чи навіть сторонню систему листування на зразок Slack, IRC чи XMPP.\n\nНАДБЕЗПЕКА: Справжнє наскрізне шифрування (лише учасники розмови можуть розшифрувати повідомлення) й звірка пристроїв учасників розмови перехресним підписуванням.\n\nДОСКОНАЛЕ СПІЛКУВАННЯ: Листуйтеся, робіть голосові й відеовиклики, діліться файлами, транслюйте екран, підʼєднуйте різноманітні інтеграції, ботів і віджети. Розбудовуйте кімнати, спільноти, будьте на звʼязку й досягайте цілей.\n\nСКРІЗЬ, ДЕ ВИ: Будьте на зв'язку, де б ви не були, завдяки повній синхронізації історії повідомлень між усіма вашим пристроями та онлайн-клієнтом https://app.element.io."; +"security_settings_crosssigning_info_not_bootstrapped" = "Перехресне підписування ще не налаштовано."; +"security_settings_crosssigning" = "ПЕРЕХРЕСНЕ ПІДПИСУВАННЯ"; +"security_settings_backup" = "РЕЗЕРВНЕ КОПІЮВАННЯ ПОВІДОМЛЕНЬ"; +"security_settings_secure_backup_restore" = "Відновити з резервної копії"; +"key_verification_manually_verify_device_key_title" = "Ключ сеансу"; +"key_verification_manually_verify_device_id_title" = "ID сеансу"; +"identity_server_settings_alert_error_invalid_identity_server" = "%@ не дійсний сервер ідентифікації."; +"identity_server_settings_alert_error_terms_not_accepted" = "Ви повинні погодитися з умовами %@, щоб налаштувати сервер ідентифікації."; +"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "Усе одно відʼєднати"; +"identity_server_settings_alert_disconnect_button" = "Відʼєднати"; +"identity_server_settings_disconnect" = "Відʼєднати"; +"identity_server_settings_change" = "Змінити"; +"identity_server_settings_add" = "Додати"; +"identity_server_settings_place_holder" = "Введіть сервер ідентифікації"; + +// Identity server settings +"identity_server_settings_title" = "Сервер ідентифікації"; +"manage_session_sign_out" = "Вийти з цього сеансу"; +"manage_session_not_trusted" = "Не довірені"; +"manage_session_trusted" = "Довірені вами"; +"key_verification_manually_verify_device_name_title" = "Назва сеансу"; +"manage_session_name" = "Назва сеансу"; +"manage_session_info" = "ВІДОМОСТІ ПРО СЕАНС"; + +// Manage session +"manage_session_title" = "Керувати сеансом"; +"security_settings_complete_security_alert_title" = "Завершити налаштування безпеки"; +"user_verification_session_details_verify_action_current_user" = "Інтерактивна перевірка"; +"secrets_recovery_reset_action_part_2" = "Скинути все"; + +// MARK: - Secrets reset + +"secrets_reset_title" = "Скинути все"; +"cross_signing_setup_banner_subtitle" = "Спростіть перевірку інших своїх пристроїв"; + +// MARK: - Cross-signing + +// Banner + +"cross_signing_setup_banner_title" = "Налаштування шифрування"; +"secrets_reset_authentication_message" = "Введіть пароль свого облікового запису, щоб продовжити"; +"security_settings_blacklist_unverified_devices" = "Ніколи не надсилати повідомлення не довіреним сеансам"; +"security_settings_advanced" = "ДОДАТКОВО"; +"security_settings_export_keys_manually" = "Експорт ключів вручну"; +"security_settings_cryptography" = "КРИПТОГРАФІЯ"; +"security_settings_crosssigning_complete_security" = "Завершити налаштування безпеки"; +"security_settings_crosssigning_reset" = "Скинути"; +"secrets_reset_reset_action" = "Скинути"; +"security_settings_secure_backup_reset" = "Скинути"; +"settings_show_NSFW_public_rooms" = "Показувати загальнодоступні кімнати з делікатним вмістом"; +"settings_identity_server_no_is" = "Сервер ідентифікації не налаштований"; +"settings_discovery_three_pid_details_enter_sms_code_action" = "Введіть код активації з СМС"; +"settings_discovery_three_pid_details_cancel_email_validation_action" = "Скасувати перевірку е-пошти"; +"settings_key_backup_info_trust_signature_invalid_device_unverified" = "Резервна копія має недійсний підпис %@"; +"settings_key_backup_info_trust_signature_invalid_device_verified" = "Резервна копія має недійсний підпис %@"; +"settings_key_backup_info_trust_signature_valid_device_unverified" = "Резервна копія має підпис %@"; +"settings_key_backup_info_trust_signature_valid_device_verified" = "Резервна копія має дійсний підпис %@"; +"settings_send_crash_report" = "Надсилати анонімні дані про збої та користування"; +"settings_show_url_previews_description" = "Попередній перегляд виконується лише у кімнатах без шифрування."; +"settings_show_url_previews" = "Попередній перегляд вебсайтів"; +"settings_ui_theme_picker_message_match_system_theme" = "«Авто» застосовує тему вашого пристрою"; +"settings_ui_theme_picker_message_invert_colours" = "«Авто» застосовує налаштування вашого пристрою «Інвертувати кольори»"; +"settings_messages_containing_display_name" = "Моє показуване імʼя"; +"settings_discovery_three_pid_details_title_phone_number" = "Керувати номером телефону"; +"settings_discovery_three_pid_details_title_email" = "Керувати е-поштою"; +"settings_discovery_error_message" = "Сталася помилка. Повторіть спробу."; +"settings_discovery_three_pids_management_information_part2" = "Користувацькі налаштування"; +"settings_discovery_accept_terms" = "Погодитися з умовами використання сервера ідентифікації"; +"settings_discovery_terms_not_signed" = "Погодьтеся з умовами використання сервера ідентифікації (%@), щоб дозволити вашу виявність за електронною адресою та номером телефону."; +"settings_key_backup_delete_confirmation_prompt_msg" = "Ви впевнені? Ви втратите ваші зашифровані повідомлення якщо копія ключів не була створена коректно."; +"settings_key_backup_delete_confirmation_prompt_title" = "Видалити резервну копію"; +"settings_key_backup_button_connect" = "Налаштувати цьому сеансу резервне копіювання ключів"; +"settings_key_backup_button_delete" = "Видалити резервну копію"; +"settings_key_backup_button_restore" = "Відновити з резервної копії"; +"settings_key_backup_button_create" = "Почати використовувати резервне копіювання ключів"; +"settings_key_backup_info_trust_signature_valid" = "Резервна копія має чинний підпис цього сеансу"; +"settings_key_backup_info_trust_signature_unknown" = "Резервна копія містить підпис невідомого сеансу з ID: %@"; +"settings_key_backup_info_progress_done" = "Резервні копії всіх ключів створено"; +"settings_key_backup_info_progress" = "Резервне копіювання %@ ключів…"; +"security_settings_secure_backup_info_valid" = "Цей сеанс створює резервну копію ваших ключів."; +"settings_key_backup_info_valid" = "Цей сеанс створює резервну копію ваших ключів."; +"settings_key_backup_info_signout_warning" = "Створіть резервну копію ключів перед виходом, щоб не втратити їх."; +"settings_key_backup_info_none" = "Для цього сеансу не створюється резервна копія ваших ключів."; +"security_settings_secure_backup_info_checking" = "Перевірка…"; +"settings_key_backup_info_checking" = "Перевірка…"; +"settings_key_backup_info" = "Зашифровані повідомлення захищені наскрізним шифруванням. Лише ви та отримувачі повідомлень мають ключі для їх читання."; +"settings_deactivate_my_account" = "Деактивувати обліковий запис"; +"settings_enable_rageshake" = "Струснути пристрій, щоб повідомити про ваду"; +"settings_third_party_notices" = "Примітки третіх сторін"; +"settings_labs_enable_ringing_for_group_calls" = "Дзвінок групових викликів"; +"settings_labs_enabled_polls" = "Опитування"; +"settings_labs_create_conference_with_jitsi" = "Створити конференц-виклик за допомогою jitsi"; +"settings_labs_e2e_encryption_prompt_message" = "Щоб завершити налаштування шифрування вам потрібно повторно увійти."; +"settings_labs_e2e_encryption" = "Наскрізне шифрування"; +"settings_unignore_user" = "Показати всі повідомлення від %@?"; +"settings_ui_theme_picker_title" = "Вибрати тему"; +"settings_ui_theme_black" = "Чорна"; +"settings_ui_theme_dark" = "Темна"; +"settings_ui_theme_light" = "Світла"; +"settings_ui_theme_auto" = "Авто"; +"settings_ui_theme" = "Тема"; +"settings_ui_language" = "Мова"; +"settings_integrations_allow_button" = "Керування інтеграціями"; +"settings_calls_stun_server_fallback_button" = "Дозволити допоміжний сервер викликів"; +"settings_enable_callkit" = "Інтегрований виклик"; +"settings_new_keyword" = "Додати нове ключове слово"; +"settings_your_keywords" = "Ваші ключові слова"; +"settings_room_upgrades" = "Поліпшення кімнати"; +"settings_messages_by_a_bot" = "Повідомлення бота"; +"settings_call_invitations" = "Запрошення до виклику"; +"settings_room_invitations" = "Запрошення до кімнати"; +"settings_messages_containing_keywords" = "Ключові слова"; +"settings_messages_containing_user_name" = "Моє користувацьке імʼя"; +"settings_messages_containing_at_room" = "@room"; +"settings_encrypted_group_messages" = "Зашифровані групові повідомлення"; +"settings_group_messages" = "Групові повідомлення"; +"settings_other" = "Інше"; +"settings_mentions_and_keywords" = "Згадки та ключові слова"; +"settings_pin_rooms_with_unread" = "Закріплювати кімнати з новими повідомленнями"; +"settings_confirm_media_size" = "Підтверджувати розмір під час надсилання"; +"settings_discovery_settings" = "ВИЯВНІСТЬ"; +"room_preview_unlinked_email_warning" = "Запрошення надіслано на адресу %@, не повʼязану з цим обліковим записом. Ви можете увійти за допомогою іншого облікового запису або додати е-пошту до цього."; +"unknown_devices_verify" = "Перевірка…"; +"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_participants_security_information_room_encrypted_for_dm" = "Повідомлення тут захищені наскрізним шифруванням.\n\nВаші повідомлення захищені замками, тож лише ви та отримувач маєте унікальні ключі для їхнього відмикання."; +"room_participants_security_information_room_encrypted" = "Повідомлення тут захищені наскрізним шифруванням.\n\nВаші повідомлення захищені замками, тож лише ви та отримувачі мають унікальні ключі для їхнього відмикання."; +"room_participants_action_security_status_complete_security" = "Завершити налаштування безпеки"; +"room_participants_idle" = "Неактивний"; +"settings_labs" = "ЛАБОРАТОРІЯ"; +"settings_about" = "ПРО"; +"settings_advanced" = "ДОДАТКОВО"; +"settings_phone_contacts" = "КОНТАКТИ ТЕЛЕФОНА"; +"settings_links" = "ПОСИЛАННЯ"; +"settings_sending_media" = "НАДСИЛАННЯ ЗОБРАЖЕНЬ І ВІДЕО"; +"room_preview_try_join_an_unknown_room" = "Ви намагаєтесь отримати доступ до %@. Бажаєте приєднатися, щоб взяти участь в обговоренні?"; +"room_preview_subtitle" = "Це попередній перегляд кімнати. Ви в режимі лише читання."; + +// Room Preview +"room_preview_invitation_format" = "%s запрошує вас приєднатися до цієї кімнати"; + +// Unknown devices +"unknown_devices_alert_title" = "Кімната містить невідомі сеанси"; +"external_link_confirmation_message" = "Посилання %@ спрямовує вас на інший сайт: %@.\n\nВи впевнені, що бажаєте продовжити?"; +"external_link_confirmation_title" = "Перевірте це посилання"; +"room_accessibility_hangup" = "Покласти слухавку"; +"room_resource_usage_limit_reached_message_contact_3" = " , щоб збільшити ліміт."; +"room_resource_usage_limit_reached_message_2" = "деякі користувачі не зможуть увійти."; +"room_resource_limit_exceeded_message_contact_3" = " , щоб продовжити користуватися цією службою."; +"room_resource_limit_exceeded_message_contact_2_link" = "зв’яжіться з адміністратором вашого сервера"; +"room_predecessor_link" = "Торкніться тут, щоб переглянути давніші повідомлення."; +"room_predecessor_information" = "Ця кімната — продовження спілкування в іншій кімнаті."; +"room_replacement_link" = "Спілкування продовжується тут."; +"room_replacement_information" = "Цю кімнату замінено й вона більше не активна."; +"room_event_action_end_poll" = "Завершити опитування"; +"room_event_action_remove_poll" = "Вилучити опитування"; +"user_verification_session_details_information_trusted_current_user" = "Цей сеанс довірений для захищеного листування, бо ви звірили його:"; +"user_verification_session_details_information_trusted_other_user_part1" = "Цей сеанс довірений для захищеного листування, бо "; +"user_verification_session_details_information_trusted_other_user_part2" = " звіряє його:"; +"user_verification_session_details_information_untrusted_other_user" = " входить у новому сеансі:"; +"user_verification_session_details_additional_information_untrusted_other_user" = "Надіслані цьому сеансу й цим сеансом повідомлення позначатимуться застереженнями, поки цей користувач йому не довірить. Або ви можете власноруч звірити сеанс."; +"user_verification_session_details_additional_information_untrusted_current_user" = "Якщо ви не входили в цей сеанс, ваш обліковий запис може бути під загрозою."; +"user_verification_session_details_verify_action_other_user" = "Звірити власноруч"; +"key_verification_bootstrap_not_setup_message" = "Спершу налаштуйте перехресне підписування."; +"key_verification_verify_qr_code_information" = "Відскануйте код, щоб безпечно звірити одне одного."; +"key_verification_verify_qr_code_scan_code_action" = "Сканувати їхній код"; +"key_verification_verify_qr_code_cannot_scan_action" = "Не вдалося сканувати?"; +"key_verification_verify_qr_code_other_scan_my_code_title" = "Чи зміг інший користувач відсканувати QR-код?"; +"key_verification_verify_qr_code_scan_other_code_success_title" = "Код підтверджено!"; +"key_verification_verify_qr_code_scan_other_code_success_message" = "QR-код успішно підтверджено."; + +// MARK: Self verification start + +// New login +"device_verification_self_verify_alert_title" = "Новий вхід. Це були ви?"; +"device_verification_self_verify_alert_message" = "Звірте новий вхід до вашого облікового запису: %@"; +"device_verification_self_verify_start_information" = "Звірте цим сеансом свій новий. Це надасть йому доступ до зашифрованих повідомлень."; +"device_verification_self_verify_start_waiting" = "Очікування…"; + +// MARK: Self verification wait + +"device_verification_self_verify_wait_title" = "Доповніть безпеку"; +"device_verification_self_verify_wait_information" = "Звірте цей сеанс одним зі своїх інших, щоб надати йому доступ до зашифрованих повідомлень.\n\nВикористайте найостанніший %@ на своїх інших пристроях:"; + +// Recover from private key +"key_backup_recover_from_private_key_info" = "Відновлення резервної копії…"; + +// MARK: Scan confirmation + +// Scanning +"key_verification_scan_confirmation_scanning_title" = "Майже все! Чекаємо на підтвердження…"; +"key_verification_scan_confirmation_scanning_user_waiting_other" = "Очікування на %@…"; +"key_verification_scan_confirmation_scanning_device_waiting_other" = "Очікування іншого пристрою…"; + +// Scanned +"key_verification_scan_confirmation_scanned_title" = "Майже все!"; +"key_verification_scan_confirmation_scanned_user_information" = "Чи показує %@ такий же щит?"; +"key_verification_scan_confirmation_scanned_device_information" = "Чи показує інший пристрій такий же щит?"; +"device_verification_self_verify_wait_new_sign_in_title" = "Звірте цей вхід"; +"device_verification_self_verify_wait_additional_information" = "Це працює з %@ та іншими клієнтами Matrix, здатними на перехресне підписування."; + +// MARK: - Device Verification +"key_verification_other_session_title" = "Звірте сеанс"; +"key_verification_new_session_title" = "Звірте свій новий сеанс"; +"key_verification_this_session_title" = "Звірити цей сеанс"; +"device_verification_security_advice_emoji" = "Порівняйте унікальні емоджі, переконавшись, що їх показано в однаковому порядку."; +"device_verification_security_advice_number" = "Порівняйте числа, переконавшись, що їх показано в однаковому порядку."; + +// MARK: Verify + +"key_verification_verify_sas_title_emoji" = "Порівняйте емодзі"; +"key_verification_verify_sas_title_number" = "Порівняйте числа"; +"key_verification_verify_sas_cancel_action" = "Вони не збігаються"; +"key_verification_verify_sas_validate_action" = "Вони збігаються"; +"key_verification_verify_sas_additional_information" = "Для більшої безпеки зв'яжіться іншим довіреним засобом чи особисто."; +"key_verification_verify_qr_code_emoji_information" = "Звірити порівнянням унікальних емодзі."; +"key_verification_verify_qr_code_start_emoji_action" = "Звірити за допомогою емодзі"; +"key_verification_verified_other_session_information" = "Тепер ви можете читати захищені повідомлення в своєму іншому сеансі, а інші користувачі знатимуть, що можуть йому довіряти."; +"key_verification_verified_new_session_information" = "Тепер ви можете читати захищені повідомлення на своєму новому пристрої, а інші користувачі знатимуть, що можуть йому довіряти."; +"key_verification_verified_this_session_information" = "Тепер ви можете читати захищені повідомлення на цьому пристрої, а інші користувачі знатимуть, що можуть йому довіряти."; + +// User + +"key_verification_verified_user_information" = "Листування з цим користувачем наскрізно зашифроване й непрочитне для сторонніх."; + +// Current session + +"key_verification_self_verify_current_session_alert_title" = "Звірити цей сеанс"; +"key_verification_self_verify_current_session_alert_message" = "Інші користувачі можуть йому не довіряти."; +"key_verification_verify_qr_code_information_other_device" = "Відскануйте код знизу для звірки:"; + +// MARK: Manually Verify Device + +"key_verification_manually_verify_device_title" = "Звірити власноруч за допомогою тексту"; +"key_verification_manually_verify_device_instruction" = "Підтвердьте шляхом порівняння наступного рядка з рядком у користувацьких налаштуваннях вашого іншого сеансу:"; +"key_verification_manually_verify_device_additional_information" = "Якщо вони відрізняються, безпека вашого зв'язку може бути під загрозою."; +"user_verification_session_details_verify_action_current_user_manually" = "Звірити власноруч за допомогою тексту"; +"device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Використати ключ безпеки"; +"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Використати фразу чи ключ безпеки"; +"device_verification_self_verify_wait_recover_secrets_additional_information" = "Якщо не маєте доступу до наявного сеансу"; + +// Recover with passphrase + +"secrets_recovery_with_passphrase_title" = "Фраза безпеки"; +"secrets_recovery_with_passphrase_information_default" = "Отримайте доступ до своєї захищеної історії повідомлень та ідентичності перехресного підписування для звірки інших сеансів, увівши свою фразу безпеки."; +"secrets_recovery_with_passphrase_information_verify_device" = "Підтвердьте цей пристрій своєю фразою безпеки."; +"secrets_recovery_with_passphrase_passphrase_placeholder" = "Введіть фразу безпеки"; +"secrets_recovery_with_passphrase_recover_action" = "Використати фразу"; +"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "Забули свою фразу безпеки? Можете "; +"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "використати ключ безпеки"; +"secrets_recovery_with_passphrase_invalid_passphrase_title" = "Не вдалося зайти до таємного сховища"; +"secrets_recovery_with_passphrase_invalid_passphrase_message" = "Переконайтеся, що вводите правильну фразу безпеки."; + +// Recover with key + +"secrets_recovery_with_key_title" = "Ключ безпеки"; +"secrets_recovery_with_key_information_default" = "Отримайте доступ до своєї захищеної історії повідомлень та ідентичності перехресного підписування для звірки інших сеансів, увівши свій ключ безпеки."; +"secrets_recovery_with_key_information_verify_device" = "Підтвердьте цей пристрій своїм ключем безпеки."; +"secrets_recovery_with_key_recovery_key_placeholder" = "Введіть ключ безпеки"; +"secrets_recovery_with_key_recover_action" = "Використати ключ"; +"secrets_recovery_with_key_invalid_recovery_key_title" = "Не вдалося зайти до таємного сховища"; +"secrets_recovery_with_key_invalid_recovery_key_message" = "Переконайтеся, що вводите правильний ключ безпеки."; +"secure_key_backup_setup_intro_use_security_key_title" = "Використати ключ безпеки"; +"secure_key_backup_setup_intro_use_security_key_info" = "Згенерувати ключ безпеки для зберігання в надійному місці, наприклад у менеджері паролів чи сейфі."; +"secure_key_backup_setup_intro_use_security_passphrase_title" = "Встановити фразу безпеки"; +"secure_key_backup_setup_intro_use_security_passphrase_info" = "Ввести таємну фразу, відому лише вам, і згенерувати ключ резервного копіювання."; + + +// Cancel + +"secure_key_backup_setup_cancel_alert_title" = "Ви впевнені?"; +"secure_key_backup_setup_cancel_alert_message" = "Якщо скасуєте це й загубите пристрій, то втратите зашифровані повідомлення й дані.\n\nВвімкнути захищене резервне копіювання й керувати своїми ключами можна в налаштуваннях."; + +// MARK: - Secrets set up + +// Recovery Key + +"secrets_setup_recovery_key_title" = "Збережіть ключ безпеки"; +"secrets_setup_recovery_key_information" = "Зберігайте ключ безпеки в надійному місці. Ним можна буде розблокувати ваші зашифровані повідомлення й дані."; +"secrets_setup_recovery_key_storage_alert_title" = "Зберігайте його у надійному місці"; + +// Recovery passphrase + +"secrets_setup_recovery_passphrase_title" = "Встановіть фразу безпеки"; +"secrets_setup_recovery_passphrase_information" = "Введіть відому лише вам фразу безпеки для захисту таємниць на вашому сервері."; +"secrets_setup_recovery_passphrase_additional_information" = "Не застосовуйте пароль облікового запису повторно."; +"secrets_setup_recovery_passphrase_confirm_information" = "Для підтвердження введіть таємну фразу ще раз."; +"secure_key_backup_setup_existing_backup_error_title" = "Резервна копія повідомлень уже існує"; +"secure_key_backup_setup_existing_backup_error_info" = "Розблокуйте її, щоб продовжити попереднє захищене резервне копіювання, або видаліть її, щоб почати захищене резервне копіювання повідомлень заново."; +"secure_key_backup_setup_existing_backup_error_unlock_it" = "Розблокувати"; +"secure_key_backup_setup_existing_backup_error_delete_it" = "Видалити"; +"sign_out_non_existing_key_backup_alert_setup_secure_backup_action" = "Налаштувати резервне копіювання ключів"; +"major_update_information" = "Ми раді повідомити, що змінили назву! Ваш застосунок оновлено й ви ввійшли у свій обліковий запис."; +"pin_protection_reset_alert_message" = "Щоб скинути PIN-код, увійдіть заново та створіть новий"; +"pin_protection_explanatory" = "Встановлення PIN-коду захищає ваші дані, як-от повідомлення чи контакти, обов'язковим запитом вашого PIN-коду при відкритті застосунку."; +"pin_protection_not_allowed_pin" = "Цей PIN-код недостатньо безпечний. Спробуйте інший"; +"pin_protection_settings_section_footer" = "Щоб скинути PIN-код, потрібно ввійти заново та створити новий."; +"pin_protection_mismatch_too_many_times_error_message" = "Не пригадуєте свій PIN-код? Торкніться кнопки \"Забули PIN-код\"."; +"biometrics_usage_reason" = "Застосунок потребує підтвердження особи"; +"biometrics_cant_unlocked_alert_message_x" = "Розблокуйте за допомогою %@ або ввійдіть заново й увімкніть %@ ще раз"; +"biometrics_cant_unlocked_alert_message_login" = "Увійти заново"; +"device_verification_self_verify_wait_recover_secrets_checking_availability" = "Перевірка інших можливостей звірки ..."; +"create_room_section_footer_type" = "Вхід до закритої кімнати — лише за запрошенням."; +"pin_protection_kick_user_alert_message" = "Забагато помилок, ваш сеанс припинено"; + +// MARK: - Secrets recovery + +"secrets_recovery_reset_action_part_1" = "Забули чи втратили всі способи відновлення? "; +"secrets_reset_information" = "Робіть це лише якщо у вас немає іншого пристрою для звірки."; +"secrets_reset_warning_title" = "Якщо ви скинете все"; +"secrets_reset_warning_message" = "Ви розпочнете знову, але без історії повідомлень, без довірених пристроїв та користувачів."; +"secrets_setup_recovery_passphrase_summary_title" = "Збережіть ключ безпеки"; +"secrets_setup_recovery_passphrase_summary_information" = "Запам'ятайте свою фразу безпеки. Нею можна буде розблокувати ваші зашифровані повідомлення й дані."; +"home_empty_view_information" = "Досконалий застосунок для захищеного спілкування команд, друзів і організацій. Торкніться кнопки + унизу, щоб додати людей і кімнати."; +"bug_report_background_mode" = "Продовжити у фоновому режимі"; +"room_intro_cell_information_room_without_topic_sentence2_part2" = " щоб люди знали, про що ця кімната."; +"room_intro_cell_information_dm_sentence2" = "У цій розмові лише ви двоє, більше ніхто не може приєднатися."; +"room_intro_cell_information_multiple_dm_sentence2" = "У цій розмові лише ви, поки хтось із вас не запросить іще когось приєднатися."; +"space_feature_unavailable_subtitle" = "Простори ще не готові на iOS, але доступні у вебпереглядачі та на комп'ютері"; +"space_feature_unavailable_information" = "Простори — новий спосіб групувати кімнати й людей.\n\nСкоро ми їх допрацюємо. Якщо приєднаєтеся до простору на іншій платформі, всі його кімнати стануть доступні й тут."; + +// Success from secure backup +"key_backup_setup_success_from_secure_backup_info" = "Створюється резервна копія ключів."; +"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "Введіть фразу безпеки для продовження."; +"secrets_recovery_with_key_information_unlock_secure_backup_with_key" = "Скористайтеся ключем безпеки для продовження."; +"key_verification_verify_qr_code_scan_code_other_device_action" = "Сканувати цим пристроєм"; +"voice_message_stop_locked_mode_recording" = "Торкніться запису, щоб зупинити або прослухати"; +"version_check_banner_subtitle_supported" = "Ми скоро припинимо підтримку %@ на iOS %@. Щоб усі можливості %@ залишалися вам доступні, радимо оновити вашу версію iOS."; +"version_check_banner_subtitle_deprecated" = "Ми більше не підтримуємо %@ на iOS %@. Щоб усі можливості %@ залишалися вам доступні, радимо оновити вашу версію iOS."; +"version_check_modal_subtitle_supported" = "Ми вдосконалюємо швидкодію та дизайн %@. На жаль, ваша версія iOS сумісна не з усіма нашими виправленнями й скоро втратить підтримку.\nРадимо оновити операційну систему, щоб усі можливості %@ були вам доступні."; +"version_check_modal_subtitle_deprecated" = "Ми вдосконалюємо швидкодію та дизайн %@. На жаль, ваша версія iOS сумісна не з усіма нашими виправленнями й більше не підтримується.\nРадимо оновити операційну систему, щоб усі можливості %@ були вам доступні."; +"leave_space_message" = "Точно вийти з %@? Вийти також із усіх кімнат і просторів цього простору?"; +"leave_space_message_admin_warning" = "Ви адмініструєте цей простір. Перед виходом переконайтесь, що передали права адміністрування іншому учаснику."; +"leave_space_only_action" = "Не виходити з жодної кімнати"; +"leave_space_and_all_rooms_action" = "Вийти з усіх кімнат і просторів"; +"spaces_explore_rooms" = "Дослідити кімнати"; +"spaces_empty_space_detail" = "Деяких кімнат може бути не видно, бо вони закриті й потребують запрошення."; +"spaces_suggested_room" = "Пропоновано"; +"room_details_access_section_anyone_for_dm" = "Будь-хто з посиланням, зокрема гості"; +"spaces_no_room_found_detail" = "Деяких результатів може бути не видно, бо вони закриті й потребують запрошення."; +"spaces_no_member_found_detail" = "Шукаєте когось, хто ще не в %@? Запросіть їх вебпереглядачем або комп'ютером."; +"spaces_invites_coming_soon_title" = "Запрошення ще в розробці"; +"spaces_coming_soon_detail" = "Ця можливість тут іще не втілена, але скоро буде. Поки що можете зробити це в Element на комп'ютері."; +"space_participants_action_remove" = "Вилучити з цього простору"; +"space_participants_action_ban" = "Заблокувати в цьому просторі"; + +// Service terms +"service_terms_modal_title_message" = "Щоб продовжити, погодьтеся з запропонованими умови й положеннями"; +"service_terms_modal_description_identity_server" = "Інші зможуть знаходити вас, якщо в контактах їхнього телефону записаний ваш номер чи е-пошта."; +"service_terms_modal_description_integration_manager" = "Це дасть змогу використовувати ботів, мости, віджети й пакунки наліпок."; +"service_terms_modal_information_description_identity_server" = "Сервер ідентифікації допомагає знаходити облікові записи за номером телефону чи адресою е-пошти."; +"service_terms_modal_information_description_integration_manager" = "Менеджер інтеграцій дає змогу додавати сторонній функціонал."; +"share_extension_low_quality_video_message" = "Надішліть в %@ у кращій якості, або нижче в низькій."; + +// Mark: - Polls + +"poll_edit_form_create_poll" = "Створити опитування"; +"poll_edit_form_poll_question_or_topic" = "Питання опитування або тема"; +"poll_edit_form_question_or_topic" = "Питання чи тема"; +"poll_edit_form_input_placeholder" = "Напишіть щось"; +"poll_edit_form_create_options" = "Створіть варіанти"; +"poll_edit_form_option_number" = "Варіант %lu"; +"poll_edit_form_add_option" = "Додати варіант"; +"poll_edit_form_post_failure_title" = "Не вдалося надіслати опитування"; +"poll_edit_form_post_failure_subtitle" = "Повторіть спробу"; +"poll_edit_form_post_failure_action" = "Гаразд"; +"poll_timeline_one_vote" = "1 голос"; +"poll_timeline_votes_count" = "%lu голосів"; +"poll_timeline_total_no_votes" = "Жодного голосу"; +"poll_timeline_total_one_vote" = "1 голос надіслано"; +"poll_timeline_total_votes" = "%lu голосів надіслано"; +"poll_timeline_total_one_vote_not_voted" = "1 голос надіслано. Проголосуйте, щоб побачити результати"; +"poll_timeline_total_votes_not_voted" = "%lu голосів надіслано. Проголосуйте, щоб побачити результати"; +"poll_timeline_total_final_results_one_vote" = "Остаточні результати на підставі 1 голосу"; +"poll_timeline_total_final_results" = "Остаточні результати на підставі %lu голосів"; +"poll_timeline_vote_not_registered_title" = "Голос не зареєстровано"; +"poll_timeline_vote_not_registered_subtitle" = "Не вдалося зареєструвати ваш голос, просимо повторити спробу"; +"poll_timeline_vote_not_registered_action" = "Гаразд"; +"poll_timeline_not_closed_title" = "Не вдалося завершити опитування"; +"poll_timeline_not_closed_subtitle" = "Повторіть спробу"; +"poll_timeline_not_closed_action" = "Гаразд"; +"e2e_need_log_in_again" = "Вам потрібно ввійти заново, щоб згенерувати ключі наскрізного шифрування для цього сеансу й надіслати відкритий ключ домашньому серверу.\nЦе одноразова дія; перепрошуємо за незручності."; + +// Crypto +"e2e_enabling_on_app_update" = "%@ тепер підтримує наскрізне шифрування. Перезайдіть, щоб його ввімкнути.\n\nМожете зробити це зараз або пізніше в налаштуваннях застосунку."; + +// Key backup wrong version +"e2e_key_backup_wrong_version_title" = "Нова резервна копія ключів"; +"e2e_key_backup_wrong_version" = "Знайдено нову резервну копію ключів до захищених повідомлень.\n\nЯкщо це були не ви, встановіть нову фразу безпеки в налаштуваннях."; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "Ми не записуємо й не аналізуємо жодних даних облікового запису"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "Ми не передаємо даних стороннім особам"; +"analytics_prompt_point_3" = "Можете вимкнути це коли завгодно в налаштуваннях"; +"analytics_prompt_not_now" = "Відкласти"; +"analytics_prompt_yes" = "Так, усе гаразд"; +"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" = "Прочитайте всі наші умови %@. Згодні з ними?"; +"analytics_prompt_terms_link_new_user" = "тут"; +/* 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" = "Допоможіть удосконалити %@"; +"widget_integration_room_not_visible" = "Кімната %@ недоступна."; +"widget_integration_missing_user_id" = "В запиті бракує user_id."; +"widget_integration_missing_room_id" = "В запиті бракує room_id."; +"widget_integration_no_permission_in_room" = "У вас нема такого дозволу в цій кімнаті."; +"widget_integration_must_be_in_room" = "Вас нема в цій кімнаті."; +"widget_integration_positive_power_level" = "Рівень повноважень має бути цілим додатним числом."; +"widget_integration_room_not_recognised" = "Кімнату не знайдено."; +"widget_integration_failed_to_send_request" = "Не вдалося надіслати запит."; +"widget_integration_unable_to_create" = "Не вдалося створити віджет."; + +// Widget Integration Manager +"widget_integration_need_to_be_able_to_invite" = "Для цього вам потрібен дозвіл запрошувати користувачів."; +"widget_creation_failure" = "Помилка створення віджету"; +"widget_no_power_to_manage" = "Для керування віджетами у цій кімнаті потрібен дозвіл"; +"bug_report_progress_uploading" = "Надсилання звіту"; +"bug_report_progress_zipping" = "Збір журналів"; +"bug_report_send_screenshot" = "Надіслати знімок екрана"; +"room_details_access_section_for_dm" = "Хто може мати доступ?"; +"room_details_access_section" = "Хто має доступ до кімнати?"; +"room_details_low_priority_tag" = "Неважливе"; +"room_details_favourite_tag" = "Улюблене"; +"identity_server_settings_alert_disconnect_still_sharing_3pid" = "Сервер ідентифікації %@ досі поширює ваші особисті дані.\n\nРадимо вилучити адреси е-пошти й номери телефонів із сервера ідентифікації, перш ніж від'єднатися."; +"settings_analytics_and_crash_data" = "Надсилати дані про збої та аналітику"; +"accessibility_button_label" = "кнопка"; +"enable" = "Увімкнути"; +"call_no_stun_server_error_use_fallback_button" = "Спробувати %@"; +"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_already_displayed" = "Виклик уже триває."; +"photo_library_access_not_granted" = "%@ не має доступу до медіатеки, просимо змінити налаштування приватності"; +"camera_unavailable" = "Камера недоступна на вашому пристрої"; +"camera_access_not_granted" = "%@ не має дозволу використовувати камеру, просимо змінити налаштування приватності"; +"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_rerequest_keys_part2" = " ваших інших сеансів."; +"event_formatter_rerequest_keys_part1_link" = "Повторити запит ключів шифрування"; +"event_formatter_jitsi_widget_removed" = "%@ вилучає голосову конференцію"; +"event_formatter_jitsi_widget_added" = "%@ додає голосову конференцію"; +"event_formatter_widget_removed" = "%@ віджет видалено %@"; +"event_formatter_widget_added" = "%@ віджет додано %@"; + +// Events formatter +"event_formatter_member_updates" = "%tu змін членства"; +"directory_server_type_homeserver" = "Загальнодоступні кімнати якого домашнього сервера перелічити?"; +"receipt_status_read" = "Прочитано: "; + +// Read Receipts +"read_receipts_list" = "Список міток прочитання"; +"room_details_addresses_disable_main_address_prompt_msg" = "Ви вилучаєте головну адресу. Усталена головна адреса для цієї кімнати буде обрана випадково"; +"room_details_access_section_no_address_warning" = "Щоб на кімнату посилалися, вона повинна мати адресу"; +"room_details_access_section_anyone" = "Будь-хто з посиланням, зокрема гості"; +"room_details_access_section_anyone_apart_from_guest_for_dm" = "Будь-хто з посиланням, окрім гостей"; +"room_details_access_section_anyone_apart_from_guest" = "Будь-хто з посиланням, окрім гостей"; +"deactivate_account_informations_part5" = "Якщо ви хочете, щоб ми вилучили ваші повідомлення, встановіть прапорець унизу\n\nВидимість повідомлень у Matrix схожа на електронну пошту. Тобто ваші повідомлення не зможуть переглянути нові або незареєстровані користувачі, але зареєстровані користувачі, які вже мають доступ до цих повідомлень і надалі матимуть доступ до їхньої копії на своїх пристроях."; +"deactivate_account_informations_part4_emphasize" = "типово не вилучає надіслані вами повідомлення. "; +"deactivate_account_informations_part2_emphasize" = "Ця дія незворотна."; +"deactivate_account_informations_part1" = "Вашим обліковим записом більше ніколи не можна буде користуватися. Ви не зможете увійти, і ніхто не зможе повторно зареєструвати той самий ID користувача. Ваш обліковий запис буде видалено з усіх кімнат, а всі його дані вилучено з вашого сервера ідентифікації. "; +"e2e_room_key_request_message" = "Ваш не звірений сеанс «%@» запитує ключі шифрування."; +"e2e_room_key_request_message_new_device" = "Ви додали новий сеанс «%@», котрий запитує ключі шифрування."; +"room_widget_permission_information_title" = "Віджет може надсилати дані до %@:\n"; +"room_widget_permission_webview_information_title" = "Віджет може встановлювати куки та надсилати дані до %@:\n"; +"share_extension_failed_to_encrypt" = "Не вдалося надіслати. Перевірте налаштування шифрування для цієї кімнати в головному застосунку"; + +// Share extension +"share_extension_auth_prompt" = "Увійдіть до основного застосунку, щоб поділитися вмістом"; +"service_terms_modal_policy_checkbox_accessibility_hint" = "Позначте, щоб погодитися на %@"; +"gdpr_consent_not_given_alert_review_now_action" = "Переглянути зараз"; + +// GDPR +"gdpr_consent_not_given_alert_message" = "Перегляньте та погодьтеся з умовами користування, щоб продовжувати використовувати сервер %@."; +"e2e_room_key_request_ignore_request" = "Знехтувати запит"; +"e2e_room_key_request_share_without_verifying" = "Надіслати без перевірки"; +"e2e_room_key_request_start_verification" = "Починається перевірка…"; +"deactivate_account_forget_messages_information_part3" = ": внаслідок цього майбутні користувачі не можуть не розуміти змісту розмов)"; +"deactivate_account_forget_messages_information_part1" = "Видаліть усі надіслані мною повідомлення після деактивації мого облікового запису ("; +"rerequest_keys_alert_message" = "Запустіть %@ на іншому пристрої, який зможе розшифрувати повідомлення та надіслати ключі цьому сеансу."; +"widget_integration_manager_disabled" = "Необхідно увімкнути менеджер інтеграцій у налаштуваннях"; +"widget_menu_remove" = "Вилучити для всіх"; +"widget_menu_revoke_permission" = "Відкликати мій доступ"; +"widget_menu_open_outside" = "Відкрити у переглядачі"; +"widget_menu_refresh" = "Оновити"; +"widget_sticker_picker_no_stickerpacks_alert_add_now" = "Додати зараз?"; +"widget_sticker_picker_no_stickerpacks_alert" = "На разі жоден пакунок наліпок не увімкнено."; +"widget_integrations_server_failed_to_connect" = "Не вдалося зʼєднатися з сервером інтеграцій"; + +// Widget +"widget_no_integrations_server_configured" = "Сервер інтеграцій не налаштовано"; +"bug_report_description" = "Опишіть ваду. Що ви робили? Виконання якої дії очікували? Що сталося натомість?"; +"bug_crash_report_title" = "Звіт про збій"; +"bug_crash_report_description" = "Будь ласка, опишіть свої дії перед збоєм:"; +"bug_report_logs_description" = "Задля діагностики, журнали клієнта будуть надіслані разом зі звітом про ваду. Якщо бажаєте надіслати лише текст угорі, зніміть галочку:"; +"bug_report_send_logs" = "Надіслати журнали"; +"group_participants_invite_another_user" = "Знайти чи запросити за ID користувача чи іменем"; +"group_participants_invite_malformed_id" = "Хибний ID. Треба Matrix ID вигляду «@localpart:domain»"; + + +// MARK: Key backup setup + +"key_backup_setup_title" = "Резервне копіювання ключів"; +"key_backup_setup_skip_alert_message" = "Ви втратите захищені повідомлення, якщо вийдете чи загубите пристрій."; + +// Intro + +"key_backup_setup_intro_title" = "Ніколи не втрачайте зашифровані повідомлення"; +"key_backup_setup_intro_info" = "Повідомлення в зашифрованих кімнатах захищені наскрізним шифруванням. Тільки ви та одержувачі маєте ключі для читання цих повідомлень.\n\nСтворіть захищену резервну копію ключів, щоб їх не втратити."; +"key_backup_setup_passphrase_info" = "Ми збережемо зашифровану копію ваших ключів на нашому сервері. Захистіть свою резервну копію парольною фразою.\n\nДля максимальної безпеки фраза повинна відрізнятися від пароля вашого облікового запису."; +"key_backup_setup_passphrase_set_passphrase_action" = "Встановити фразу"; + +// MARK: Key backup recover + +"key_backup_recover_title" = "Захищені повідомлення"; + +// Recover from passphrase + +"key_backup_recover_from_passphrase_info" = "Використайте фразу безпеки, щоб розблокувати історію зашифрованих повідомлень"; + +// Recover from recovery key + +"key_backup_recover_from_recovery_key_info" = "Використайте ключ безпеки, щоб розблокувати історію зашифрованих повідомлень"; +"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Забули свою фразу безпеки? Можете "; +"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "використати ключ безпеки"; +"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" = "Втратили ключ безпеки? Встановіть новий у налаштуваннях."; + +// Success + +"key_backup_recover_success_info" = "Резервну копію відновлено!"; +"key_backup_recover_done_action" = "Готово"; +"key_backup_setup_intro_setup_action_without_existing_backup" = "Налаштувати резервне копіювання ключів"; + +// Passphrase + +"key_backup_setup_passphrase_title" = "Захистіть резервну копію фразою безпеки"; +"key_backup_setup_passphrase_setup_recovery_key_info" = "Або захистіть резервну копію ключем безпеки, який зберігатимете в надійному місці."; +"key_backup_setup_passphrase_setup_recovery_key_action" = "(Додатково) Налаштувати ключ безпеки"; + +// Success + +"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" = "Зберегти ключ безпеки"; + +// 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_make_copy_action" = "Зробити копію"; +"key_backup_setup_success_from_recovery_key_made_copy_action" = "Вже маю копію"; +"key_backup_recover_invalid_passphrase_title" = "Хибна фраза безпеки"; +"key_backup_recover_invalid_passphrase" = "Не вдається розшифрувати резервну копію цією фразою: переконайтеся, що вводите правильну фразу безпеки."; +"key_backup_recover_invalid_recovery_key_title" = "Хибний ключ безпеки"; +"key_backup_recover_invalid_recovery_key" = "Не вдалося розшифрувати резервну копію цим ключем: переконайтеся, що вводите правильний ключ безпеки."; + +// MARK: Sign out warning + +"sign_out_existing_key_backup_alert_title" = "Точно вийти?"; +"sign_out_existing_key_backup_alert_sign_out_action" = "Вийти"; +"sign_out_non_existing_key_backup_alert_title" = "Якщо вийти зараз, ви втратите свої зашифровані повідомлення"; +"sign_out_non_existing_key_backup_alert_discard_key_backup_action" = "Мені не потрібні мої зашифровані повідомлення"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_title" = "Ви втратите доступ до ваших зашифрованих повідомлень"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_message" = "Ви втратите доступ до своїх зашифрованих повідомлень, якщо не зробите резервну копію ключів перед виходом з системи."; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_sign_out_action" = "Вийти"; +"sign_out_non_existing_key_backup_sign_out_confirmation_alert_backup_action" = "Резервна копія"; +"sign_out_key_backup_in_progress_alert_title" = "Триває резервне копіювання ключів. Якщо вийти зараз, ви втратите доступ до своїх зашифрованих повідомлень."; +"sign_out_key_backup_in_progress_alert_discard_key_backup_action" = "Мені не потрібні мої зашифровані повідомлення"; +"sign_out_key_backup_in_progress_alert_cancel_action" = "Почекаю"; +"key_backup_setup_intro_manual_export_info" = "(Розширені)"; +"key_backup_setup_intro_manual_export_action" = "Експорт ключів вручну"; +"key_backup_setup_intro_setup_connect_action_with_existing_backup" = "Налаштувати цьому пристрою резервне копіювання ключів"; +"device_verification_cancelled" = "Інша сторона скасувала звірку."; +"device_verification_cancelled_by_me" = "Звірку скасовано. Причина: %@"; +"device_verification_error_cannot_load_device" = "Не вдалося отримати дані сеансу."; + +// Mark: Incoming +"device_verification_incoming_title" = "Надійшов запит на звірку"; +"device_verification_incoming_description_1" = "Звірте цей сеанс, щоб позначити його довіреним. Довірені сеанси партнерів дають змогу впевненіше користуватися наскрізним шифруванням повідомлень."; +"device_verification_incoming_description_2" = "Звірка цього сеансу позначить його довіреним для вас, а ваш довіреним для партнера."; + +// MARK: Start +"device_verification_start_title" = "Звірити, порівнявши короткий текстовий рядок"; +"device_verification_start_wait_partner" = "Очікування підтвердження партнером…"; +"device_verification_start_use_legacy" = "Нічого не з'являється? Ще не всі клієнти підтримують інтерактивну звірку. Звірте по-старому."; +"device_verification_start_verify_button" = "Почати звірку"; +"device_verification_start_use_legacy_action" = "Звірити по-старому"; +"key_verification_tile_request_status_expired" = "Термін сплив"; +"key_verification_user_title" = "Звірте їх"; +"user_verification_start_information_part2" = " порівнявши одноразовий код на обох ваших пристроях."; +"user_verification_start_information_part1" = "Звірте безпечніше, "; + +// MARK: - User verification + +// Start + +"user_verification_start_verify_action" = "Почати звірку"; +"user_verification_start_waiting_partner" = "Очікування на %@…"; +"user_verification_start_additional_information" = "Для безпеки зробіть це особисто або скористайтеся іншим способом зв'язку."; + +// Sessions list + +"user_verification_sessions_list_user_trust_level_trusted_title" = "Довірений"; +"user_verification_sessions_list_information" = "Листування з цим користувачем у цій кімнаті наскрізно зашифроване й непрочитне для сторонніх."; +"user_verification_session_details_information_untrusted_current_user" = "Звірте цей сеанс, щоб позначити його довіреним і надати йому доступ до зашифрованих повідомлень:"; +"secrets_setup_recovery_key_storage_alert_message" = "✓ Надрукуйте його й зберігайте в надійному місці\n✓ Збережіть його на USB-ключ або носій резервного копіювання\n✓ Скопіюйте його до вашого особистого хмарного сховища"; +"call_transfer_error_message" = "Не вдалося переадресувати виклик"; +"favourites_empty_view_information" = "Ви можете додати до улюблених кількома способами, найшвидший — це просто натиснути й утримувати. Торкніться зірки, і вони автоматично зʼявляться тут для безпечного зберігання."; + +// MARK: - Favourites + +"favourites_empty_view_title" = "Улюблені кімнати й люди"; +"group_participants_filter_members" = "Фільтр учасників спільноти"; +"group_participants_invite_prompt_msg" = "Ви впевнені, що хочете запросити %@ до цієї групи?"; +"room_details_fail_to_update_room_direct" = "Не вдалося оновити прямий прапор цієї кімнати"; +"room_details_flair_section" = "Показувати значки для спільнот"; +"settings_flair" = "Показувати значок, де це дозволено"; +"room_warning_about_encryption" = "Наскрізне шифрування ще на етапі бета-тестування й може бути ненадійним.\n\nПоки що не варто довіряти йому захист даних.\n\nПристрої ще не зможуть розшифрувати історію до того, як з них приєдналися до кімнати.\n\nЗашифровані повідомлення не буде показано у клієнтах, які ще не використовують шифрування."; diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index f54deca1f..12863f8fa 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -1548,7 +1548,7 @@ "space_home_show_all_rooms" = "显示所有聊天室"; "room_event_action_forward" = "转发"; "poll_edit_form_add_option" = "添加选项"; -"poll_edit_form_option_number" = "选项 %d"; +"poll_edit_form_option_number" = "选项 %lu"; "poll_edit_form_create_options" = "创建选项"; "poll_edit_form_input_placeholder" = "写些东西"; "poll_edit_form_question_or_topic" = "问题或话题"; @@ -1562,3 +1562,46 @@ "share_extension_low_quality_video_title" = "将以低画质发送视频"; "settings_discovery_accept_terms" = "接受身份服务器条款"; "settings_about" = "关于"; +"poll_timeline_not_closed_action" = "好"; +"poll_timeline_not_closed_subtitle" = "请再试一次"; +"poll_timeline_not_closed_title" = "结束投票失败"; +"poll_timeline_vote_not_registered_action" = "好"; +"poll_timeline_vote_not_registered_subtitle" = "抱歉,您的投票未登记,请重试"; +"poll_timeline_vote_not_registered_title" = "投票未登记"; +"poll_timeline_total_final_results" = "基于 %lu 票的最终结果"; +"poll_timeline_total_final_results_one_vote" = "基于 1 票的最终结果"; +"poll_timeline_total_votes_not_voted" = "已有 %lu 票。投票查看结果"; +"poll_timeline_total_one_vote_not_voted" = "已有 1 票。 投票查看结果"; +"poll_timeline_total_votes" = "%lu 票"; +"poll_timeline_total_one_vote" = "1 票"; +"poll_timeline_total_no_votes" = "尚无投票"; +"poll_timeline_votes_count" = "%lu 票"; +"poll_timeline_one_vote" = "1 票"; +"poll_edit_form_post_failure_action" = "好"; +"poll_edit_form_post_failure_subtitle" = "请再试一次"; +"poll_edit_form_post_failure_title" = "发布投票失败"; +"settings_labs_enabled_polls" = "投票"; +"room_event_action_end_poll" = "结束投票"; +"room_event_action_remove_poll" = "删除投票"; +"analytics_prompt_stop" = "停止共享"; +"analytics_prompt_yes" = "是的,没关系"; +"analytics_prompt_not_now" = "不是现在"; +"analytics_prompt_point_3" = "您可以随时在设置中关闭此功能"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_2" = "我们与第三方共享信息"; +/* Note: The word "don't" is formatted in bold */ +"analytics_prompt_point_1" = "我们 记录任何账户数据或绘制任何账户数据的画像"; +"analytics_prompt_terms_link_upgrade" = "此处"; +/* Note: The placeholder is for the contents of analytics_prompt_terms_link_upgrade */ +"analytics_prompt_terms_upgrade" = "阅读我们所有的条款 %@。 那样行吗?"; +"analytics_prompt_terms_link_new_user" = "此处"; +/* 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" = "帮助改进 %@"; +"settings_analytics_and_crash_data" = "发送崩溃和分析数据"; +"accessibility_button_label" = "按钮"; +"enable" = "启用"; diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 3c25ee336..dca358737 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -20,6 +20,8 @@ internal typealias AssetImageTypeAlias = ImageAsset.Image // swiftlint:disable identifier_name line_length nesting type_body_length type_name internal enum Asset { internal enum Images { + internal static let analyticsCheckmark = ImageAsset(name: "AnalyticsCheckmark") + internal static let analyticsLogo = ImageAsset(name: "AnalyticsLogo") internal static let socialLoginButtonApple = ImageAsset(name: "social_login_button_apple") internal static let socialLoginButtonFacebook = ImageAsset(name: "social_login_button_facebook") internal static let socialLoginButtonGithub = ImageAsset(name: "social_login_button_github") @@ -113,6 +115,7 @@ internal enum Asset { internal static let peopleFloatingAction = ImageAsset(name: "people_floating_action") internal static let actionCamera = ImageAsset(name: "action_camera") internal static let actionFile = ImageAsset(name: "action_file") + internal static let actionLocation = ImageAsset(name: "action_location") internal static let actionMediaLibrary = ImageAsset(name: "action_media_library") internal static let actionPoll = ImageAsset(name: "action_poll") internal static let actionSticker = ImageAsset(name: "action_sticker") @@ -143,6 +146,9 @@ internal enum Asset { internal static let videoCall = ImageAsset(name: "video_call") internal static let voiceCallHangonIcon = ImageAsset(name: "voice_call_hangon_icon") internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon") + internal static let locationMarkerIcon = ImageAsset(name: "location_marker_icon") + 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") internal static let pollCheckboxSelected = ImageAsset(name: "poll_checkbox_selected") internal static let pollDeleteIcon = ImageAsset(name: "poll_delete_icon") diff --git a/Riot/Generated/MatrixKitStrings.swift b/Riot/Generated/MatrixKitStrings.swift index e5c1e7f31..b178f6d8d 100644 --- a/Riot/Generated/MatrixKitStrings.swift +++ b/Riot/Generated/MatrixKitStrings.swift @@ -695,6 +695,10 @@ public class MatrixKitL10n: NSObject { public static var messageReplyToSenderSentAnImage: String { return MatrixKitL10n.tr("message_reply_to_sender_sent_an_image") } + /// has shared their location. + public static var messageReplyToSenderSentTheirLocation: String { + return MatrixKitL10n.tr("message_reply_to_sender_sent_their_location") + } /// There are unsaved changes. Leaving will discard them. public static var messageUnsavedChanges: String { return MatrixKitL10n.tr("message_unsaved_changes") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 4ab7572a3..5764578fd 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -15,6 +15,10 @@ public class VectorL10n: NSObject { public static var accept: String { return VectorL10n.tr("Vector", "accept") } + /// button + public static var accessibilityButtonLabel: String { + return VectorL10n.tr("Vector", "accessibility_button_label") + } /// checkbox public static var accessibilityCheckboxLabel: String { return VectorL10n.tr("Vector", "accessibility_checkbox_label") @@ -31,6 +35,58 @@ public class VectorL10n: NSObject { public static func activeCallDetails(_ p1: String) -> String { return VectorL10n.tr("Vector", "active_call_details", p1) } + /// Help us identify issues and improve %@ by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices. + public static func analyticsPromptMessageNewUser(_ p1: String) -> String { + return VectorL10n.tr("Vector", "analytics_prompt_message_new_user", p1) + } + /// You previously consented to share anonymous usage data with us. Now, to help understand how people use multiple devices, we’ll generate a random identifier, shared by your devices. + public static var analyticsPromptMessageUpgrade: String { + return VectorL10n.tr("Vector", "analytics_prompt_message_upgrade") + } + /// Not now + public static var analyticsPromptNotNow: String { + return VectorL10n.tr("Vector", "analytics_prompt_not_now") + } + /// We don't record or profile any account data + public static var analyticsPromptPoint1: String { + return VectorL10n.tr("Vector", "analytics_prompt_point_1") + } + /// We don't share information with third parties + public static var analyticsPromptPoint2: String { + return VectorL10n.tr("Vector", "analytics_prompt_point_2") + } + /// You can turn this off anytime in settings + public static var analyticsPromptPoint3: String { + return VectorL10n.tr("Vector", "analytics_prompt_point_3") + } + /// Stop sharing + public static var analyticsPromptStop: String { + return VectorL10n.tr("Vector", "analytics_prompt_stop") + } + /// here + public static var analyticsPromptTermsLinkNewUser: String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_link_new_user") + } + /// here + public static var analyticsPromptTermsLinkUpgrade: String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_link_upgrade") + } + /// You can read all our terms %@. + public static func analyticsPromptTermsNewUser(_ p1: String) -> String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_new_user", p1) + } + /// Read all our terms %@. Is that OK? + public static func analyticsPromptTermsUpgrade(_ p1: String) -> String { + return VectorL10n.tr("Vector", "analytics_prompt_terms_upgrade", p1) + } + /// Help improve %@ + public static func analyticsPromptTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "analytics_prompt_title", p1) + } + /// Yes, that's fine + public static var analyticsPromptYes: String { + return VectorL10n.tr("Vector", "analytics_prompt_yes") + } /// Please review and accept the policies of this homeserver: public static var authAcceptPolicies: String { return VectorL10n.tr("Vector", "auth_accept_policies") @@ -1235,6 +1291,10 @@ public class VectorL10n: NSObject { public static var emojiPickerTitle: String { return VectorL10n.tr("Vector", "emoji_picker_title") } + /// Enable + public static var enable: String { + return VectorL10n.tr("Vector", "enable") + } /// Send an encrypted message… public static var encryptedRoomMessagePlaceholder: String { return VectorL10n.tr("Vector", "encrypted_room_message_placeholder") @@ -1439,10 +1499,6 @@ public class VectorL10n: NSObject { public static var gdprConsentNotGivenAlertReviewNowAction: String { return VectorL10n.tr("Vector", "gdpr_consent_not_given_alert_review_now_action") } - /// Would you like to help improve %@ by automatically reporting anonymous crash reports and usage data? - public static func googleAnalyticsUsePrompt(_ p1: String) -> String { - return VectorL10n.tr("Vector", "google_analytics_use_prompt", p1) - } /// Home public static var groupDetailsHome: String { return VectorL10n.tr("Vector", "group_details_home") @@ -2135,6 +2191,54 @@ public class VectorL10n: NSObject { public static var less: String { return VectorL10n.tr("Vector", "less") } + /// Close + public static var locationSharingCloseAction: String { + return VectorL10n.tr("Vector", "location_sharing_close_action") + } + /// %@ does not have permission to access your location. You can enable access in Settings > Location + public static func locationSharingInvalidAuthorizationErrorTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_error_title", p1) + } + /// Not now + public static var locationSharingInvalidAuthorizationNotNow: String { + return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_not_now") + } + /// Settings + public static var locationSharingInvalidAuthorizationSettings: String { + return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_settings") + } + /// %@ 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) + } + /// %@ could not access your location. Please try again later. + public static func locationSharingLocatingUserErrorTitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_locating_user_error_title", p1) + } + /// Open in Apple Maps + public static var locationSharingOpenAppleMaps: String { + return VectorL10n.tr("Vector", "location_sharing_open_apple_maps") + } + /// Open in Google Maps + public static var locationSharingOpenGoogleMaps: String { + return VectorL10n.tr("Vector", "location_sharing_open_google_maps") + } + /// Location sharing + public static var locationSharingSettingsHeader: String { + return VectorL10n.tr("Vector", "location_sharing_settings_header") + } + /// Enable location sharing + public static var locationSharingSettingsToggleTitle: String { + return VectorL10n.tr("Vector", "location_sharing_settings_toggle_title") + } + /// Share + public static var locationSharingShareAction: String { + return VectorL10n.tr("Vector", "location_sharing_share_action") + } + /// Location + public static var locationSharingTitle: String { + return VectorL10n.tr("Vector", "location_sharing_title") + } /// Got it public static var majorUpdateDoneAction: String { return VectorL10n.tr("Vector", "major_update_done_action") @@ -2235,6 +2339,10 @@ public class VectorL10n: NSObject { public static var off: String { return VectorL10n.tr("Vector", "off") } + /// OK + public static var ok: String { + return VectorL10n.tr("Vector", "ok") + } /// On public static var on: String { return VectorL10n.tr("Vector", "on") @@ -2387,10 +2495,6 @@ public class VectorL10n: NSObject { public static var pollEditFormPollQuestionOrTopic: String { return VectorL10n.tr("Vector", "poll_edit_form_poll_question_or_topic") } - /// OK - public static var pollEditFormPostFailureAction: String { - return VectorL10n.tr("Vector", "poll_edit_form_post_failure_action") - } /// Please try again public static var pollEditFormPostFailureSubtitle: String { return VectorL10n.tr("Vector", "poll_edit_form_post_failure_subtitle") @@ -2403,10 +2507,6 @@ public class VectorL10n: NSObject { public static var pollEditFormQuestionOrTopic: String { return VectorL10n.tr("Vector", "poll_edit_form_question_or_topic") } - /// OK - public static var pollTimelineNotClosedAction: String { - return VectorL10n.tr("Vector", "poll_timeline_not_closed_action") - } /// Please try again public static var pollTimelineNotClosedSubtitle: String { return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle") @@ -2447,10 +2547,6 @@ public class VectorL10n: NSObject { public static func pollTimelineTotalVotesNotVoted(_ p1: Int) -> String { return VectorL10n.tr("Vector", "poll_timeline_total_votes_not_voted", p1) } - /// OK - public static var pollTimelineVoteNotRegisteredAction: String { - return VectorL10n.tr("Vector", "poll_timeline_vote_not_registered_action") - } /// Sorry, your vote was not registered, please try again public static var pollTimelineVoteNotRegisteredSubtitle: String { return VectorL10n.tr("Vector", "poll_timeline_vote_not_registered_subtitle") @@ -4227,6 +4323,10 @@ public class VectorL10n: NSObject { public static var settingsAdvanced: String { return VectorL10n.tr("Vector", "settings_advanced") } + /// Send crash and analytics data + public static var settingsAnalyticsAndCrashData: String { + return VectorL10n.tr("Vector", "settings_analytics_and_crash_data") + } /// Call invitations public static var settingsCallInvitations: String { return VectorL10n.tr("Vector", "settings_call_invitations") @@ -4755,10 +4855,6 @@ public class VectorL10n: NSObject { public static var settingsSecurity: String { return VectorL10n.tr("Vector", "settings_security") } - /// Send anon crash & usage data - public static var settingsSendCrashReport: String { - return VectorL10n.tr("Vector", "settings_send_crash_report") - } /// SENDING IMAGES AND VIDEOS public static var settingsSendingMedia: String { return VectorL10n.tr("Vector", "settings_sending_media") @@ -5059,9 +5155,9 @@ public class VectorL10n: NSObject { public static var spacesAddRoomsComingSoonTitle: String { return VectorL10n.tr("Vector", "spaces_add_rooms_coming_soon_title") } - /// This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with Element on your computer. - public static var spacesComingSoonDetail: String { - return VectorL10n.tr("Vector", "spaces_coming_soon_detail") + /// This feature hasn’t been implemented here, but it’s on the way. For now, you can do that with %@ on your computer. + public static func spacesComingSoonDetail(_ p1: String) -> String { + return VectorL10n.tr("Vector", "spaces_coming_soon_detail", p1) } /// Coming soon public static var spacesComingSoonTitle: String { diff --git a/Riot/Managers/Analytics/Analytics.h b/Riot/Managers/Analytics/Analytics.h deleted file mode 100644 index 5ad851929..000000000 --- a/Riot/Managers/Analytics/Analytics.h +++ /dev/null @@ -1,65 +0,0 @@ -/* - Copyright 2018 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 - -#import - - -// Metrics related to notifications -FOUNDATION_EXPORT NSString *const AnalyticsNoficationsCategory; -FOUNDATION_EXPORT NSString *const AnalyticsNoficationsTimeToDisplayContent; -/** - The analytics value for accept/decline of the identity server's terms. - */ -FOUNDATION_EXPORT NSString *const AnalyticsContactsIdentityServerAccepted; - - -/** - `Analytics` sends analytics to an analytics tool. - */ -@interface Analytics : NSObject - -/** - Returns the shared Analytics manager. - - @return the shared Analytics manager. - */ -+ (instancetype)sharedInstance; - -/** - Start doing analytics if the settings `enableCrashReport` is enabled. - */ -- (void)start; - -/** - Stop doing analytics. - */ -- (void)stop; - -/** - Track a screen display. - - @param screenName the name of the displayed screen. - */ -- (void)trackScreen:(NSString*)screenName; - -/** - Flush analytics data. - */ -- (void)dispatch; - -@end diff --git a/Riot/Managers/Analytics/Analytics.m b/Riot/Managers/Analytics/Analytics.m deleted file mode 100644 index 6bf7269b8..000000000 --- a/Riot/Managers/Analytics/Analytics.m +++ /dev/null @@ -1,162 +0,0 @@ -/* - Copyright 2018 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 "Analytics.h" - -#import "GeneratedInterface-Swift.h" - - -NSString *const AnalyticsNoficationsCategory = @"notifications"; -NSString *const AnalyticsNoficationsTimeToDisplayContent = @"timelineDisplay"; -NSString *const AnalyticsContactsIdentityServerAccepted = @"identityServerAccepted"; - - -// Duration data will be visible under the Piwik category called "Performance". -// Other values will be visible in "Metrics". -// Some Matomo screenshots are available at https://github.com/vector-im/element-ios/pull/3789. -NSString *const kAnalyticsPerformanceCategory = @"Performance"; -NSString *const kAnalyticsMetricsCategory = @"Metrics"; - - -@import MatomoTracker; - -@interface Analytics () -{ - MatomoTracker *matomoTracker; -} - -@end - -@implementation Analytics - -+ (instancetype)sharedInstance -{ - static Analytics *sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - sharedInstance = [[Analytics alloc] init]; - }); - - return sharedInstance; -} - -- (instancetype)init -{ - self = [super init]; - if (self) - { - matomoTracker = [[MatomoTracker alloc] initWithSiteId:BuildSettings.analyticsAppId - baseURL:BuildSettings.analyticsServerUrl - userAgent:@"iOSMatomoTracker"]; - [self migrateFromFourPointFourSharedInstance]; - } - return self; -} - -- (void)migrateFromFourPointFourSharedInstance -{ - if ([[NSUserDefaults standardUserDefaults] boolForKey:@"migratedFromFourPointFourSharedInstance"]) return; - [matomoTracker copyFromOldSharedInstance]; - [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"migratedFromFourPointFourSharedInstance"]; -} - -- (void)start -{ - // Check whether the user has enabled the sending of crash reports. - if (RiotSettings.shared.enableCrashReport) - { - matomoTracker.isOptedOut = NO; - - [matomoTracker setCustomVariableWithIndex:1 name:@"App Platform" value:@"iOS Platform"]; - [matomoTracker setCustomVariableWithIndex:2 name:@"App Version" value:[AppDelegate theDelegate].appVersion]; - - // The language is either the one selected by the user within the app - // or, else, the one configured by the OS - NSString *language = [NSBundle mxk_language] ? [NSBundle mxk_language] : [[NSBundle mainBundle] preferredLocalizations][0]; - [matomoTracker setCustomVariableWithIndex:4 name:@"Chosen Language" value:language]; - - MXKAccount* account = [MXKAccountManager sharedManager].activeAccounts.firstObject; - if (account) - { - [matomoTracker setCustomVariableWithIndex:7 name:@"Homeserver URL" value:account.mxCredentials.homeServer]; - [matomoTracker setCustomVariableWithIndex:8 name:@"Identity Server URL" value:account.identityServerURL]; - } - - // TODO: We should also track device and os version - // But that needs to be decided for all platforms - - // Catch and log crashes - [MXLogger logCrashes:YES]; - [MXLogger setBuildVersion:[AppDelegate theDelegate].build]; - -#ifdef DEBUG - // Disable analytics in debug as it pollutes stats - matomoTracker.isOptedOut = YES; -#endif - } - else - { - MXLogDebug(@"[AppDelegate] The user decided to not send analytics"); - matomoTracker.isOptedOut = YES; - [MXLogger logCrashes:NO]; - } -} - -- (void)stop -{ - matomoTracker.isOptedOut = YES; - [MXLogger logCrashes:NO]; -} - -- (void)trackScreen:(NSString *)screenName -{ - // Use the same pattern as Android - NSString *appName = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"]; - NSString *appVersion = [AppDelegate theDelegate].appVersion; - - [matomoTracker trackWithView:@[@"ios", appName, appVersion, screenName] - url:nil]; -} - -- (void)dispatch -{ - [matomoTracker dispatch]; -} - -#pragma mark - MXAnalyticsDelegate - -- (void)trackDuration:(NSTimeInterval)seconds category:(NSString*)category name:(NSString*)name -{ - // Report time in ms to make figures look better in Matomo - NSNumber *value = @(seconds * 1000); - [matomoTracker trackWithEventWithCategory:kAnalyticsPerformanceCategory - action:category - name:name - number:value - url:nil]; -} - -- (void)trackValue:(NSNumber*)value category:(NSString*)category name:(NSString*)name -{ - [matomoTracker trackWithEventWithCategory:kAnalyticsMetricsCategory - action:category - name:name - number:value - url:nil]; -} - -@end diff --git a/Riot/Managers/Analytics/DecryptionFailure.h b/Riot/Managers/Analytics/DecryptionFailure.h deleted file mode 100644 index 7ea43962c..000000000 --- a/Riot/Managers/Analytics/DecryptionFailure.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - Copyright 2018 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 - -/** - Failure reasons as defined in https://docs.google.com/document/d/1es7cTCeJEXXfRCTRgZerAM2Wg5ZerHjvlpfTW-gsOfI. - */ -struct DecryptionFailureReasonStruct -{ - __unsafe_unretained NSString * const unspecified; - __unsafe_unretained NSString * const olmKeysNotSent; - __unsafe_unretained NSString * const olmIndexError; - __unsafe_unretained NSString * const unexpected; -}; -extern const struct DecryptionFailureReasonStruct DecryptionFailureReason; - -/** - `DecryptionFailure` represents a decryption failure. - */ -@interface DecryptionFailure : NSObject - -/** - The id of the event that was unabled to decrypt. - */ -@property (nonatomic) NSString *failedEventId; - -/** - The time the failure has been reported. - */ -@property (nonatomic, readonly) NSTimeInterval ts; - -/** - Decryption failure reason. - */ -@property (nonatomic) NSString *reason; - -@end diff --git a/Riot/Managers/Analytics/DecryptionFailure.m b/Riot/Managers/Analytics/DecryptionFailure.m deleted file mode 100644 index d43b0ec9f..000000000 --- a/Riot/Managers/Analytics/DecryptionFailure.m +++ /dev/null @@ -1,38 +0,0 @@ -/* - Copyright 2018 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 "DecryptionFailure.h" - -const struct DecryptionFailureReasonStruct DecryptionFailureReason = { - .unspecified = @"unspecified_error", - .olmKeysNotSent = @"olm_keys_not_sent_error", - .olmIndexError = @"olm_index_error", - .unexpected = @"unexpected_error" -}; - -@implementation DecryptionFailure - -- (instancetype)init -{ - self = [super init]; - if (self) - { - _ts = [NSDate date].timeIntervalSince1970; - } - return self; -} - -@end diff --git a/Riot/Managers/AppInfo/AppInfo.swift b/Riot/Managers/AppInfo/AppInfo.swift index 457a7af87..fde62d75b 100644 --- a/Riot/Managers/AppInfo/AppInfo.swift +++ b/Riot/Managers/AppInfo/AppInfo.swift @@ -19,17 +19,14 @@ import Foundation /// Used to handle the application information @objcMembers final class AppInfo: NSObject { - + // MARK: - Constants /// Current application information static var current: AppInfo { - let appDisplayName = BuildSettings.bundleDisplayName - let buildInfo: BuildInfo = BuildInfo() - - return AppInfo(displayName: appDisplayName, + return AppInfo(displayName: self.bundleDisplayName, appVersion: AppVersion.current, - buildInfo: buildInfo) + buildInfo: BuildInfo()) } // MARK: - Properties @@ -52,4 +49,11 @@ final class AppInfo: NSObject { self.appVersion = appVersion self.buildInfo = buildInfo } + + private static var bundleDisplayName: String { + guard let bundleDisplayName = Bundle.app.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String else { + fatalError("CFBundleDisplayName should be defined") + } + return bundleDisplayName + } } diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 5b31329d4..1813d8818 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -23,7 +23,8 @@ final class RiotSettings: NSObject { // MARK: - Constants public enum UserDefaultsKeys { - static let enableCrashReport = "enableCrashReport" + static let enableAnalytics = "enableAnalytics" + static let matomoAnalytics = "enableCrashReport" static let notificationsShowDecryptedContent = "showDecryptedContent" static let allowStunServerFallback = "allowStunServerFallback" static let pinRoomsWithMissedNotificationsOnHome = "pinRoomsWithMissedNotif" @@ -100,13 +101,31 @@ final class RiotSettings: NSObject { // MARK: Other - /// Indicate if `enableCrashReport` settings has been set once. - var isEnableCrashReportHasBeenSetOnce: Bool { - return RiotSettings.defaults.object(forKey: UserDefaultsKeys.enableCrashReport) != nil + /// Whether the user was previously shown the Matomo analytics prompt. + var hasSeenAnalyticsPrompt: Bool { + RiotSettings.defaults.object(forKey: UserDefaultsKeys.enableAnalytics) != nil } - @UserDefault(key: UserDefaultsKeys.enableCrashReport, defaultValue: false, storage: defaults) - var enableCrashReport + /// Whether the user has both seen the Matomo analytics prompt and declined it. + var hasDeclinedMatomoAnalytics: Bool { + RiotSettings.defaults.object(forKey: UserDefaultsKeys.matomoAnalytics) != nil && !RiotSettings.defaults.bool(forKey: UserDefaultsKeys.matomoAnalytics) + } + + /// Whether the user previously accepted the Matomo analytics prompt. + /// This allows these users to be shown a different prompt to explain the changes. + var hasAcceptedMatomoAnalytics: Bool { + RiotSettings.defaults.bool(forKey: UserDefaultsKeys.matomoAnalytics) + } + + /// `true` when the user has opted in to send analytics. + @UserDefault(key: UserDefaultsKeys.enableAnalytics, defaultValue: false, storage: defaults) + var enableAnalytics + + /// Indicates if the device has already called identify for this session to PostHog. + /// This is separate to `enableAnalytics` as logging out will leave analytics + /// enabled but reset identification. + @UserDefault(key: "isIdentifiedForAnalytics", defaultValue: false, storage: defaults) + var isIdentifiedForAnalytics @UserDefault(key: "enableRageShake", defaultValue: false, storage: defaults) var enableRageShake @@ -168,6 +187,9 @@ final class RiotSettings: NSObject { @UserDefault(key: "roomScreenAllowPollsAction", defaultValue: false, storage: defaults) var roomScreenAllowPollsAction + + @UserDefault(key: "roomScreenAllowLocationAction", defaultValue: false, storage: defaults) + var roomScreenAllowLocationAction @UserDefault(key: "roomScreenShowsURLPreviews", defaultValue: true, storage: defaults) var roomScreenShowsURLPreviews diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift new file mode 100644 index 000000000..535ca7b2a --- /dev/null +++ b/Riot/Modules/Analytics/Analytics.swift @@ -0,0 +1,259 @@ +// +// 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 PostHog +import AnalyticsEvents + +/// A class responsible for managing an analytics client +/// and sending events through this client. +@objcMembers class Analytics: NSObject { + + // MARK: - Properties + + /// The singleton instance to be used within the Riot target. + static let shared = Analytics() + + /// The analytics client to send events with. + private var client: AnalyticsClientProtocol = PostHogAnalyticsClient() + + /// The service used to interact with account data settings. + private var service: AnalyticsService? + + /// Whether or not the object is enabled and sending events to the server. + var isRunning: Bool { client.isRunning } + + /// Whether to show the user the analytics opt in prompt. + var shouldShowAnalyticsPrompt: Bool { + // Only show the prompt once, and when analytics are configured in BuildSettings. + !RiotSettings.shared.hasSeenAnalyticsPrompt && PHGPostHogConfiguration.standard != nil + } + + /// Indicates whether the user previously accepted Matomo analytics and should be shown the upgrade prompt. + var promptShouldDisplayUpgradeMessage: Bool { + RiotSettings.shared.hasAcceptedMatomoAnalytics + } + + // MARK: - Public + + /// Opts in to analytics tracking with the supplied session. + /// - Parameter session: An optional session to use to when reading/generating the analytics ID. + /// The session will be ignored if not running. + func optIn(with session: MXSession?) { + RiotSettings.shared.enableAnalytics = true + startIfEnabled() + + guard let session = session else { return } + useAnalyticsSettings(from: session) + } + + /// Stops analytics tracking and calls `reset` to clear any IDs and event queues. + func optOut() { + RiotSettings.shared.enableAnalytics = false + + // The order is important here. PostHog ignores the reset if stopped. + reset() + client.stop() + + MXLog.debug("[Analytics] Stopped.") + } + + /// Starts the analytics client if the user has opted in, otherwise does nothing. + func startIfEnabled() { + guard RiotSettings.shared.enableAnalytics, !isRunning else { return } + + client.start() + + // Sanity check in case something went wrong. + guard client.isRunning else { return } + + MXLog.debug("[Analytics] Started.") + + // Catch and log crashes + MXLogger.logCrashes(true) + MXLogger.setBuildVersion(AppDelegate.theDelegate().build) + } + + /// Use the analytics settings from the supplied session to configure analytics. + /// For now this is only used for (pseudonymous) identification. + /// - Parameter session: The session to read analytics settings from. + func useAnalyticsSettings(from session: MXSession) { + guard + RiotSettings.shared.enableAnalytics, + !RiotSettings.shared.isIdentifiedForAnalytics + else { return } + + let service = AnalyticsService(session: session) + self.service = service + + service.settings { [weak self] result in + guard let self = self else { return } + + switch result { + case .success(let settings): + self.identify(with: settings) + self.service = nil + case .failure: + MXLog.error("[Analytics] Failed to use analytics settings. Will continue to run without analytics ID.") + self.service = nil + } + } + } + + /// Resets the any IDs and event queues in the analytics client. This method should + /// be called on sign-out to maintain opt-in status, whilst ensuring the next + /// account used isn't associated with the previous one. + /// Note: **MUST** be called before stopping PostHog or the reset is ignored. + func reset() { + client.reset() + MXLog.debug("[Analytics] Reset.") + RiotSettings.shared.isIdentifiedForAnalytics = false + + // Stop collecting crash logs + MXLogger.logCrashes(false) + } + + /// Flushes the event queue in the analytics client, uploading all pending events. + /// Normally events are sent in batches. Call this method when you need an event + /// to be sent immediately. + func forceUpload() { + client.flush() + } + + // MARK: - Private + + /// Identify (pseudonymously) any future events with the ID from the analytics account data settings. + /// - Parameter settings: The settings to use for identification. The ID must be set *before* calling this method. + private func identify(with settings: AnalyticsSettings) { + guard let id = settings.id else { + MXLog.error("[Analytics] identify(with:) called before an ID has been generated.") + return + } + + client.identify(id: id) + MXLog.debug("[Analytics] Identified.") + RiotSettings.shared.isIdentifiedForAnalytics = true + } + + /// Capture an event in the `client`. + /// - Parameter event: The event to capture. + private func capture(event: AnalyticsEventProtocol) { + client.capture(event) + } +} + +// MARK: - Public tracking methods +// The following methods are exposed for compatibility with Objective-C as +// the `capture` method and the generated events cannot be bridged from Swift. +extension Analytics { + /// Track the presentation of a screen + /// - Parameters: + /// - screen: The screen that was shown. + /// - milliseconds: An optional value representing how long the screen was shown for in milliseconds. + func trackScreen(_ screen: AnalyticsScreen, duration milliseconds: Int?) { + let event = AnalyticsEvent.Screen(durationMs: milliseconds, screenName: screen.screenName) + client.screen(event) + } + + /// The the presentation of a screen without including a duration + /// - Parameter screen: The screen that was shown + func trackScreen(_ screen: AnalyticsScreen) { + trackScreen(screen, duration: nil) + } + + /// Track an element that has been tapped + /// - Parameters: + /// - tap: The element that was tapped + /// - index: The index of the element, if it's in a list of elements + func trackTap(_ tap: AnalyticsUIElement, index: Int?) { + let event = AnalyticsEvent.Click(index: index, name: tap.elementName) + client.capture(event) + } + + /// Track an element that has been tapped without including an index + /// - Parameters: + /// - tap: The element that was tapped + func trackTap(_ tap: AnalyticsUIElement) { + trackTap(tap, index: nil) + } + + /// 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..) -> Void) { + // Only use the session if it is running otherwise we could wipe out an existing analytics ID. + guard session.state == .running else { + MXLog.warning("[AnalyticsService] Aborting attempt to read analytics settings. The session may not be up-to-date.") + completion(.failure(AnalyticsServiceError.sessionIsNotRunning)) + return + } + + let settings = AnalyticsSettings(accountData: session.accountData) + + // The id has already be set so we are done here. + if settings.id != nil { + completion(.success(settings)) + return + } + + // Create a new ID and modify the event dictionary. + let id = UUID().uuidString + + var eventDictionary = settings.dictionary + eventDictionary[AnalyticsSettings.Constants.idKey] = id + + session.setAccountData(eventDictionary, forType: AnalyticsSettings.eventType) { [weak self] in + guard let self = self else { + completion(.failure(AnalyticsServiceError.unknown)) + return + } + + MXLog.debug("[AnalyticsService] Successfully updated analytics settings in account data.") + let settings = AnalyticsSettings(accountData: self.session.accountData) + completion(.success(settings)) + } failure: { error in + MXLog.warning("[AnalyticsService] Failed to update analytics settings.") + completion(.failure(error ?? AnalyticsServiceError.unknown)) + } + } +} diff --git a/Riot/Modules/Analytics/AnalyticsSettings.swift b/Riot/Modules/Analytics/AnalyticsSettings.swift new file mode 100644 index 000000000..e847f0668 --- /dev/null +++ b/Riot/Modules/Analytics/AnalyticsSettings.swift @@ -0,0 +1,65 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// An analytics settings event from the user's account data. +struct AnalyticsSettings { + static let eventType = "im.vector.analytics" + + enum Constants { + static let idKey = "id" + static let webOptInKey = "pseudonymousAnalyticsOptIn" + } + + /// A randomly generated analytics token for this user. + /// This is suggested to be a UUID string. + let id: String? + + /// Whether the user has opted in on web or not. This is unused on iOS but necessary + /// to store here so that it's value is preserved when updating the account data if we + /// generated an ID on iOS. + /// + /// `true` if opted in on web, `false` if opted out on web and `nil` if the web prompt is not yet seen. + private let webOptIn: Bool? +} + +extension AnalyticsSettings { + // Private as AnalyticsSettings should only be created from an MXSession + private init(dictionary: Dictionary?) { + self.id = dictionary?[Constants.idKey] as? String + self.webOptIn = dictionary?[Constants.webOptInKey] as? Bool + } + + /// A dictionary representation of the settings. + var dictionary: Dictionary { + var dictionary = [AnyHashable: Any]() + dictionary[Constants.idKey] = id + dictionary[Constants.webOptInKey] = webOptIn + + return dictionary + } +} + +// MARK: - Public initializer + +extension AnalyticsSettings { + /// Create the analytics settings from account data. + /// - Parameter accountData: The account data to read the event from. + init(accountData: MXAccountData) { + self.init(dictionary: accountData.accountData(forEventType: AnalyticsSettings.eventType)) + } +} diff --git a/Riot/Modules/Analytics/AnalyticsUIElement.swift b/Riot/Modules/Analytics/AnalyticsUIElement.swift new file mode 100644 index 000000000..93a08e7e2 --- /dev/null +++ b/Riot/Modules/Analytics/AnalyticsUIElement.swift @@ -0,0 +1,32 @@ +// +// 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 AnalyticsEvents + +/// A tappable UI element that can be track in Analytics. +@objc enum AnalyticsUIElement: Int { + case sendMessageButton + + /// The element name reported to the AnalyticsEvent. + var elementName: AnalyticsEvent.Click.Name { + switch self { + // Note: This is a test element that doesn't need to be captured. + // It will likely be removed when the AnalyticsEvent.Click is updated. + case .sendMessageButton: + return .SendMessageButton + } + } +} diff --git a/Riot/Modules/Analytics/DecryptionFailure.swift b/Riot/Modules/Analytics/DecryptionFailure.swift new file mode 100644 index 000000000..d011a0413 --- /dev/null +++ b/Riot/Modules/Analytics/DecryptionFailure.swift @@ -0,0 +1,53 @@ +// +// 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 AnalyticsEvents + +/// Failure reasons as defined in https://docs.google.com/document/d/1es7cTCeJEXXfRCTRgZerAM2Wg5ZerHjvlpfTW-gsOfI. +@objc enum DecryptionFailureReason: Int { + case unspecified + case olmKeysNotSent + case olmIndexError + case unexpected + + var errorName: AnalyticsEvent.Error.Name { + switch self { + case .unspecified: + return .OlmUnspecifiedError + case .olmKeysNotSent: + return .OlmKeysNotSentError + case .olmIndexError: + return .OlmIndexError + case .unexpected: + return .UnknownError + } + } +} + +/// `DecryptionFailure` represents a decryption failure. +@objcMembers class DecryptionFailure: NSObject { + /// The id of the event that was unabled to decrypt. + let failedEventId: String + /// The time the failure has been reported. + let ts: TimeInterval = Date().timeIntervalSince1970 + /// Decryption failure reason. + let reason: DecryptionFailureReason + + init(failedEventId: String, reason: DecryptionFailureReason) { + self.failedEventId = failedEventId + self.reason = reason + } +} diff --git a/Riot/Managers/Analytics/DecryptionFailureTracker.h b/Riot/Modules/Analytics/DecryptionFailureTracker.h similarity index 92% rename from Riot/Managers/Analytics/DecryptionFailureTracker.h rename to Riot/Modules/Analytics/DecryptionFailureTracker.h index b2dbbfc77..b8f9ca467 100644 --- a/Riot/Managers/Analytics/DecryptionFailureTracker.h +++ b/Riot/Modules/Analytics/DecryptionFailureTracker.h @@ -16,8 +16,9 @@ #import -#import "DecryptionFailure.h" +@class DecryptionFailureTracker; +@class Analytics; @import MatrixSDK; @interface DecryptionFailureTracker : NSObject @@ -32,7 +33,7 @@ /** The delegate object to receive analytics events. */ -@property (nonatomic, weak) id delegate; +@property (nonatomic, weak) Analytics *delegate; /** Report an event unable to decrypt. diff --git a/Riot/Managers/Analytics/DecryptionFailureTracker.m b/Riot/Modules/Analytics/DecryptionFailureTracker.m similarity index 83% rename from Riot/Managers/Analytics/DecryptionFailureTracker.m rename to Riot/Modules/Analytics/DecryptionFailureTracker.m index 56521eb58..0f2b2ff81 100644 --- a/Riot/Managers/Analytics/DecryptionFailureTracker.m +++ b/Riot/Modules/Analytics/DecryptionFailureTracker.m @@ -15,6 +15,7 @@ */ #import "DecryptionFailureTracker.h" +#import "GeneratedInterface-Swift.h" // Call `checkFailures` every `CHECK_INTERVAL` @@ -90,31 +91,32 @@ NSString *const kDecryptionFailureTrackerAnalyticsCategory = @"e2e.failure"; return; } - DecryptionFailure *decryptionFailure = [[DecryptionFailure alloc] init]; - decryptionFailure.failedEventId = event.eventId; + NSString *failedEventId = event.eventId; + DecryptionFailureReason reason; // Categorise the error switch (event.decryptionError.code) { case MXDecryptingErrorUnknownInboundSessionIdCode: - decryptionFailure.reason = DecryptionFailureReason.olmKeysNotSent; + reason = DecryptionFailureReasonOlmKeysNotSent; break; case MXDecryptingErrorOlmCode: - decryptionFailure.reason = DecryptionFailureReason.olmIndexError; + reason = DecryptionFailureReasonOlmIndexError; break; case MXDecryptingErrorEncryptionNotEnabledCode: case MXDecryptingErrorUnableToDecryptCode: - decryptionFailure.reason = DecryptionFailureReason.unexpected; + reason = DecryptionFailureReasonUnexpected; break; default: - decryptionFailure.reason = DecryptionFailureReason.unspecified; + reason = DecryptionFailureReasonUnspecified; break; } - reportedFailures[event.eventId] = decryptionFailure; + reportedFailures[event.eventId] = [[DecryptionFailure alloc] initWithFailedEventId:failedEventId + reason:reason]; } - (void)dispatch @@ -152,17 +154,17 @@ NSString *const kDecryptionFailureTrackerAnalyticsCategory = @"e2e.failure"; if (failuresToTrack.count) { // Sort failures by error reason - NSMutableDictionary *failuresCounts = [NSMutableDictionary dictionary]; + NSMutableDictionary *failuresCounts = [NSMutableDictionary dictionary]; for (DecryptionFailure *failure in failuresToTrack) { - failuresCounts[failure.reason] = @(failuresCounts[failure.reason].unsignedIntegerValue + 1); + failuresCounts[@(failure.reason)] = @(failuresCounts[@(failure.reason)].unsignedIntegerValue + 1); } MXLogDebug(@"[DecryptionFailureTracker] trackFailures: %@", failuresCounts); - for (NSString *reason in failuresCounts) + for (NSNumber *reason in failuresCounts) { - [_delegate trackValue:failuresCounts[reason] category:kDecryptionFailureTrackerAnalyticsCategory name:reason]; + [self.delegate trackE2EEError:reason.integerValue count:failuresCounts[reason].integerValue]; } } } diff --git a/Riot/Modules/MatrixKit/Utils/MXKAnalyticsConstants.h b/Riot/Modules/Analytics/Helpers/JoinedRoomSize+MemberCount.swift similarity index 51% rename from Riot/Modules/MatrixKit/Utils/MXKAnalyticsConstants.h rename to Riot/Modules/Analytics/Helpers/JoinedRoomSize+MemberCount.swift index 97d3c25f2..d5473d5f9 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKAnalyticsConstants.h +++ b/Riot/Modules/Analytics/Helpers/JoinedRoomSize+MemberCount.swift @@ -1,5 +1,5 @@ // -// Copyright 2020 The Matrix.org Foundation C.I.C +// Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,20 +14,23 @@ // limitations under the License. // -#import +import AnalyticsEvents - -typedef NSString *const MXKAnalyticsCategory NS_TYPED_EXTENSIBLE_ENUM; - -/** - The analytics category for local contacts. - */ -static MXKAnalyticsCategory const MXKAnalyticsCategoryContacts = @"localContacts"; - - -typedef NSString *const MXKAnalyticsName NS_TYPED_EXTENSIBLE_ENUM; - -/** - The analytics value for accept/decline of local contacts access. - */ -static MXKAnalyticsName const MXKAnalyticsNameContactsAccessGranted = @"accessGranted"; +extension AnalyticsEvent.JoinedRoom.RoomSize { + init?(memberCount: UInt) { + switch memberCount { + case 2: + self = .Two + case 3...10: + self = .ThreeToTen + case 11...100: + self = .ElevenToOneHundred + case 101...1000: + self = .OneHundredAndOneToAThousand + case 1001...: + self = .MoreThanAThousand + default: + return nil + } + } +} diff --git a/Riot/Modules/Analytics/Helpers/MXCallHangupReason+Analytics.swift b/Riot/Modules/Analytics/Helpers/MXCallHangupReason+Analytics.swift new file mode 100644 index 000000000..4b8911ce8 --- /dev/null +++ b/Riot/Modules/Analytics/Helpers/MXCallHangupReason+Analytics.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 AnalyticsEvents + +extension __MXCallHangupReason { + var errorName: AnalyticsEvent.Error.Name { + switch self { + case .userHangup: + return .VoipUserHangup + case .inviteTimeout: + return .VoipInviteTimeout + case .iceFailed: + return .VoipIceFailed + case .iceTimeout: + return .VoipIceTimeout + case .userMediaFailed: + return .VoipUserMediaFailed + case .unknownError: + return .UnknownError + default: + return .UnknownError + } + } +} diff --git a/Riot/Modules/Analytics/Helpers/MXTaskProfileName+Analytics.swift b/Riot/Modules/Analytics/Helpers/MXTaskProfileName+Analytics.swift new file mode 100644 index 000000000..99f89174e --- /dev/null +++ b/Riot/Modules/Analytics/Helpers/MXTaskProfileName+Analytics.swift @@ -0,0 +1,42 @@ +// +// 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 AnalyticsEvents + +extension MXTaskProfileName { + var analyticsName: AnalyticsEvent.PerformanceTimer.Name? { + switch self { + case .startupIncrementalSync: + return .StartupIncrementalSync + case .startupInitialSync: + return .StartupInitialSync + case .startupLaunchScreen: + return .StartupLaunchScreen + case .startupStorePreload: + return .StartupStorePreload + case .startupMountData: + return .StartupStoreReady + case .initialSyncRequest: + return .InitialSyncRequest + case .initialSyncParsing: + return .InitialSyncParsing + case .notificationsOpenEvent: + return .NotificationsOpenEvent + default: + return nil + } + } +} diff --git a/Riot/Modules/Analytics/PHGPostHogConfiguration.swift b/Riot/Modules/Analytics/PHGPostHogConfiguration.swift new file mode 100644 index 000000000..c02b85c30 --- /dev/null +++ b/Riot/Modules/Analytics/PHGPostHogConfiguration.swift @@ -0,0 +1,28 @@ +// +// 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 PostHog + +extension PHGPostHogConfiguration { + static var standard: PHGPostHogConfiguration? { + guard let apiKey = BuildSettings.analyticsKey, let host = BuildSettings.analyticsHost else { return nil } + + let configuration = PHGPostHogConfiguration(apiKey: apiKey, host: host) + configuration.shouldSendDeviceID = false + + return configuration + } +} diff --git a/Riot/Modules/Analytics/PostHogAnalyticsClient.swift b/Riot/Modules/Analytics/PostHogAnalyticsClient.swift new file mode 100644 index 000000000..1c7172112 --- /dev/null +++ b/Riot/Modules/Analytics/PostHogAnalyticsClient.swift @@ -0,0 +1,65 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import PostHog +import AnalyticsEvents + +/// An analytics client that reports events to a PostHog server. +class PostHogAnalyticsClient: AnalyticsClientProtocol { + /// The PHGPostHog object used to report events. + private var postHog: PHGPostHog? + + var isRunning: Bool { postHog?.enabled ?? false } + + func start() { + // Only start if analytics have been configured in BuildSettings + guard let configuration = PHGPostHogConfiguration.standard else { return } + + if postHog == nil { + postHog = PHGPostHog(configuration: configuration) + } + + postHog?.enable() + } + + func identify(id: String) { + postHog?.identify(id) + } + + func reset() { + postHog?.reset() + } + + func stop() { + postHog?.disable() + + // As of PostHog 1.4.4, setting the client to nil here doesn't release + // it. Keep it around to avoid having multiple instances if the user re-enables + } + + func flush() { + postHog?.flush() + } + + func capture(_ event: AnalyticsEventProtocol) { + postHog?.capture(event.eventName, properties: event.properties) + } + + func screen(_ event: AnalyticsScreenProtocol) { + postHog?.screen(event.screenName.rawValue, properties: event.properties) + } + +} diff --git a/Riot/Modules/Application/LegacyAppDelegate.h b/Riot/Modules/Application/LegacyAppDelegate.h index 72dc4f61e..40e3c2377 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.h +++ b/Riot/Modules/Application/LegacyAppDelegate.h @@ -22,7 +22,6 @@ #import "JitsiViewController.h" #import "RageShakeManager.h" -#import "Analytics.h" #import "ThemeService.h" #import "UniversalLink.h" diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 180cfe27a..fd6354ad3 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -433,16 +433,16 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni _isAppForeground = NO; _handleSelfVerificationRequest = YES; - // Configure our analytics. It will indeed start if the option is enabled - Analytics *analytics = [Analytics sharedInstance]; + // Configure our analytics. It will start if the option is enabled + Analytics *analytics = Analytics.shared; [MXSDKOptions sharedInstance].analyticsDelegate = analytics; - [DecryptionFailureTracker sharedInstance].delegate = [Analytics sharedInstance]; + [DecryptionFailureTracker sharedInstance].delegate = analytics; MXBaseProfiler *profiler = [MXBaseProfiler new]; profiler.analytics = analytics; [MXSDKOptions sharedInstance].profiler = profiler; - [analytics start]; + [analytics startIfEnabled]; self.localAuthenticationService = [[LocalAuthenticationService alloc] initWithPinCodePreferences:[PinCodePreferences shared]]; @@ -587,7 +587,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Analytics: Force to send the pending actions [[DecryptionFailureTracker sharedInstance] dispatch]; - [[Analytics sharedInstance] dispatch]; + [Analytics.shared forceUpload]; } - (void)applicationWillEnterForeground:(UIApplication *)application @@ -648,9 +648,13 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni MXLogDebug(@"[AppDelegate] afterAppUnlockedByPin"); // Check if there is crash log to send - if (RiotSettings.shared.enableCrashReport) + if (RiotSettings.shared.enableAnalytics) { + #if DEBUG + // Don't show alerts for crashes during development. + #else [self checkExceptionToReport]; + #endif } // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. @@ -1880,6 +1884,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [self.pushNotificationService checkPushKitPushersInSession:mxSession]; } + else if (mxSession.state == MXSessionStateRunning) + { + // Configure analytics from the session if necessary + [Analytics.shared useAnalyticsSettingsFrom:mxSession]; + } else if (mxSession.state == MXSessionStateClosed) { [self removeMatrixSession:mxSession]; @@ -2225,6 +2234,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // Reset push notification store [self.pushNotificationStore reset]; + // Reset analytics + [Analytics.shared reset]; + #ifdef MX_CALL_STACK_ENDPOINT // Erase all created certificates and private keys by MXEndpointCallStack for (MXKAccount *account in MXKAccountManager.sharedManager.accounts) @@ -2390,8 +2402,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni launchAnimationContainerView = launchLoadingView; - [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:kMXAnalyticsStartupLaunchScreen - category:kMXAnalyticsStartupCategory]; + [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:MXTaskProfileNameStartupLaunchScreen]; } } @@ -2400,7 +2411,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if (launchAnimationContainerView) { id profiler = MXSDKOptions.sharedInstance.profiler; - MXTaskProfile *launchTaskProfile = [profiler taskProfileWithName:kMXAnalyticsStartupLaunchScreen category:kMXAnalyticsStartupCategory]; + MXTaskProfile *launchTaskProfile = [profiler taskProfileWithName:MXTaskProfileNameStartupLaunchScreen]; if (launchTaskProfile) { [profiler stopMeasuringTaskWithProfile:launchTaskProfile]; diff --git a/Riot/Modules/Authentication/AuthenticationViewController.m b/Riot/Modules/Authentication/AuthenticationViewController.m index 188e33268..67b2a9e8b 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/AuthenticationViewController.m @@ -309,9 +309,6 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"Authentication"]; [_keyboardAvoider startAvoiding]; } @@ -330,7 +327,7 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; return; } - // Verify that the app does not show the authentification screean whereas + // Verify that the app does not show the authentication screen whereas // the user has already logged in. // This bug rarely happens (https://github.com/vector-im/riot-ios/issues/1643) // but it invites the user to log in again. They will then lose all their diff --git a/Riot/Modules/Common/Recents/RecentsViewController.h b/Riot/Modules/Common/Recents/RecentsViewController.h index a9e9c4863..9e9e357a7 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.h +++ b/Riot/Modules/Common/Recents/RecentsViewController.h @@ -18,6 +18,7 @@ #import "MatrixKit.h" @class RootTabEmptyView; +@class AnalyticsScreenTimer; /** Notification to be posted when recents data is ready. Notification object will be the RecentsViewController instance. @@ -85,16 +86,16 @@ FOUNDATION_EXPORT NSString *const RecentsViewControllerDataReadyNotification; */ @property (nonatomic) CGFloat stickyHeaderHeight; -/** - The analytics instance screen name (Default is "RecentsScreen"). - */ -@property (nonatomic) NSString *screenName; - /** Empty view to display when there is no item to show on the screen. */ @property (nonatomic, weak) RootTabEmptyView *emptyView; +/** + The screen timer used for analytics if they've been enabled. The default value is nil. + */ +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + /** Return the sticky header for the specified section of the table view diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index b8f0d0a80..0bcceb09a 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -106,9 +106,6 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; - // Set default screen name - _screenName = @"RecentsScreen"; - // Enable the search bar in the recents table, and remove the search option from the navigation bar. _enableSearchBar = YES; self.enableBarButtonSearch = NO; @@ -259,9 +256,6 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:_screenName]; - // Reset back user interactions self.userInteractionEnabled = YES; @@ -329,11 +323,14 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro // the selected room (if any) is highlighted. [self refreshCurrentSelectedCell:YES]; } + + [self.screenTimer start]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; + [self.screenTimer stop]; } - (void)viewDidLayoutSubviews diff --git a/Riot/Modules/Communities/GroupsViewController.m b/Riot/Modules/Communities/GroupsViewController.m index 91a881352..306a4e55c 100644 --- a/Riot/Modules/Communities/GroupsViewController.m +++ b/Riot/Modules/Communities/GroupsViewController.m @@ -42,6 +42,8 @@ __weak id kThemeServiceDidChangeThemeNotificationObserver; } +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation GroupsViewController @@ -74,6 +76,8 @@ // Set itself as delegate by default. self.delegate = self; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenMyGroups]; } - (void)viewDidLoad @@ -203,9 +207,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"Groups"]; - // Deselect the current selected row, it will be restored on viewDidAppear (if any) NSIndexPath *indexPath = [self.groupsTableView indexPathForSelectedRow]; if (indexPath) @@ -258,11 +259,14 @@ // the selected group (if any) is highlighted. [self refreshCurrentSelectedCell:YES]; } + + [self.screenTimer start]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; + [self.screenTimer stop]; } #pragma mark - Override MXKGroupListViewController diff --git a/Riot/Modules/Communities/Home/GroupHomeViewController.m b/Riot/Modules/Communities/Home/GroupHomeViewController.m index 018c15eb9..6e5d5d3b5 100644 --- a/Riot/Modules/Communities/Home/GroupHomeViewController.m +++ b/Riot/Modules/Communities/Home/GroupHomeViewController.m @@ -48,6 +48,8 @@ @property (nonatomic, readonly) DTHTMLAttributedStringBuilderWillFlushCallback longDescriptionSanitizationCallback; +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation GroupHomeViewController @@ -95,6 +97,8 @@ MXStrongifyAndReturnIfNil(self); [element sanitizeWith:allowedHTMLTags bodyFont:self->_groupLongDescription.font imageHandler:[self groupLongDescriptionImageHandler]]; }; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenGroup]; } - (void)viewDidLoad @@ -205,9 +209,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"GroupDetailsHome"]; - // Release the potential pushed view controller [self releasePushedViewController]; @@ -259,6 +260,18 @@ [self cancelRegistrationOnGroupChangeNotifications]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; diff --git a/Riot/Modules/Communities/Members/GroupParticipantsViewController.m b/Riot/Modules/Communities/Members/GroupParticipantsViewController.m index 7be31d255..9a87f6562 100644 --- a/Riot/Modules/Communities/Members/GroupParticipantsViewController.m +++ b/Riot/Modules/Communities/Members/GroupParticipantsViewController.m @@ -219,9 +219,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"GroupDetailsPeople"]; - // Release the potential pushed view controller [self releasePushedViewController]; diff --git a/Riot/Modules/Communities/Rooms/GroupRoomsViewController.m b/Riot/Modules/Communities/Rooms/GroupRoomsViewController.m index cabf6cdde..855c46580 100644 --- a/Riot/Modules/Communities/Rooms/GroupRoomsViewController.m +++ b/Riot/Modules/Communities/Rooms/GroupRoomsViewController.m @@ -183,9 +183,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"GroupDetailsRooms"]; - // Release the potential pushed view controller [self releasePushedViewController]; diff --git a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m index 28e7e64de..339af8584 100644 --- a/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m +++ b/Riot/Modules/Communities/TabDetail/GroupDetailsViewController.m @@ -136,9 +136,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"GroupDetails"]; } - (void)viewWillDisappear:(BOOL)animated diff --git a/Riot/Modules/Contacts/ContactsTableViewController.h b/Riot/Modules/Contacts/ContactsTableViewController.h index f1b3effaf..97c02515b 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.h +++ b/Riot/Modules/Contacts/ContactsTableViewController.h @@ -19,6 +19,7 @@ #import "ContactTableViewCell.h" @class ContactsTableViewController; +@class AnalyticsScreenTimer; /** `ContactsTableViewController` delegate. @@ -85,11 +86,6 @@ */ @property (nonatomic) BOOL shouldScrollToTopOnRefresh; -/** - The analytics instance screen name (Default is "ContactsTable"). - */ -@property (nonatomic) NSString *screenName; - /** Callback used to take into account the change of the user interface theme. */ @@ -124,5 +120,10 @@ */ @property (nonatomic, weak) id contactsTableViewControllerDelegate; +/** + The screen timer used for analytics if they've been enabled. The default value is nil. + */ +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index 1a8bec148..bf83b6372 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -76,8 +76,6 @@ // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; - - _screenName = @"ContactsTable"; } - (void)viewDidLoad @@ -159,9 +157,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:_screenName]; MXWeakify(self); @@ -182,6 +177,12 @@ [self updateFooterViewVisibility]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; @@ -206,6 +207,12 @@ } } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + #pragma mark - /** diff --git a/Riot/Modules/Contacts/Details/ContactDetailsViewController.m b/Riot/Modules/Contacts/Details/ContactDetailsViewController.m index 3d332dbaa..be2fe5596 100644 --- a/Riot/Modules/Contacts/Details/ContactDetailsViewController.m +++ b/Riot/Modules/Contacts/Details/ContactDetailsViewController.m @@ -232,9 +232,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"ContactDetails"]; // Hide the bottom border of the navigation bar to display the expander header [self hideNavigationBarBorder:YES]; diff --git a/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift b/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift index ba0e9cf8a..c97176f31 100644 --- a/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift +++ b/Riot/Modules/Contacts/Views/FindYourContactsFooterView.swift @@ -55,7 +55,7 @@ class FindYourContactsFooterView: UIView, NibLoadable, Themable { button.layer.cornerRadius = 8 titleLabel.text = VectorL10n.findYourContactsTitle - messageLabel.text = VectorL10n.findYourContactsMessage(BuildSettings.bundleDisplayName) + messageLabel.text = VectorL10n.findYourContactsMessage(AppInfo.current.displayName) button.setTitle(VectorL10n.findYourContactsButtonTitle, for: .normal) footerLabel.text = VectorL10n.findYourContactsFooter } diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift index d89e60635..63fe65197 100644 --- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift +++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewController.swift @@ -53,6 +53,7 @@ final class EnterNewRoomDetailsViewController: UIViewController { item.isEnabled = false return item }() + private var screenTimer = AnalyticsScreenTimer(screen: .createRoom) private enum RowType { case `default` @@ -215,10 +216,17 @@ final class EnterNewRoomDetailsViewController: UIViewController { self.keyboardAvoider?.startAvoiding() } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + screenTimer.start() + } + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) self.keyboardAvoider?.stopAvoiding() + + screenTimer.stop() } override var preferredStatusBarStyle: UIStatusBarStyle { diff --git a/Riot/Modules/Favorites/FavouritesViewController.m b/Riot/Modules/Favorites/FavouritesViewController.m index 12f129d68..c3c10933b 100644 --- a/Riot/Modules/Favorites/FavouritesViewController.m +++ b/Riot/Modules/Favorites/FavouritesViewController.m @@ -39,9 +39,9 @@ { [super finalizeInit]; - self.screenName = @"Favourites"; - self.enableDragging = YES; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenFavourites]; } - (void)viewDidLoad diff --git a/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m b/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m index f9dcf9ea2..6e509e089 100644 --- a/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m +++ b/Riot/Modules/GlobalSearch/Files/CellData/FilesSearchCellData.m @@ -37,7 +37,7 @@ roomId = event.roomId; // Title is here the file name stored in event body - title = [event.content[@"body"] isKindOfClass:[NSString class]] ? event.content[@"body"] : nil; + title = [event.content[kMXMessageBodyKey] isKindOfClass:[NSString class]] ? event.content[kMXMessageBodyKey] : nil; // Check attachment if any if ([searchDataSource.eventFormatter isSupportedAttachment:event]) @@ -128,7 +128,7 @@ { MXEvent *event = searchResult.result; NSString *msgtype; - MXJSONModelSetString(msgtype, event.content[@"msgtype"]); + MXJSONModelSetString(msgtype, event.content[kMXMessageTypeKey]); if ([msgtype isEqualToString:kMXMessageTypeImage]) { @@ -142,10 +142,6 @@ { return [UIImage imageNamed:@"file_video_icon"]; } - else if ([msgtype isEqualToString:kMXMessageTypeLocation]) - { - // Not supported yet - } else if ([msgtype isEqualToString:kMXMessageTypeFile]) { return [UIImage imageNamed:@"file_doc_icon"]; diff --git a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.h b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.h index bf9826858..71db0b6bc 100644 --- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.h +++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.h @@ -17,6 +17,8 @@ #import "MatrixKit.h" +@class AnalyticsScreenTimer; + /** `HomeFilesSearchViewController` displays the files search in user's rooms under a `HomeViewController` segment. */ @@ -27,4 +29,9 @@ */ @property (nonatomic, readonly) MXEvent *selectedEvent; +/** + The screen timer used for analytics if they've been enabled. The default value is nil. + */ +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end diff --git a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m index 63035f726..67ad65ab8 100644 --- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m @@ -109,9 +109,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"FilesGlobalSearch"]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionDidLeaveRoomNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionNewRoomNotification object:nil]; } @@ -124,6 +121,18 @@ [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionNewRoomNotification object:nil]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + #pragma mark - - (void)refreshSearchResult:(NSNotification *)notif diff --git a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.h b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.h index 8a3553771..9fdb9880a 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.h +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.h @@ -17,6 +17,8 @@ #import "MatrixKit.h" +@class AnalyticsScreenTimer; + /** `HomeMessagesSearchViewController` displays messages search in user's rooms under a `HomeViewController` segment. */ @@ -27,4 +29,9 @@ */ @property (nonatomic, readonly) MXEvent *selectedEvent; +/** + The screen timer used for analytics if they've been enabled. The default value is nil. + */ +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end diff --git a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m index 00fd9d20d..9fc1d5ac8 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m @@ -115,9 +115,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"MessagesGlobalSearch"]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionDidLeaveRoomNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshSearchResult:) name:kMXSessionNewRoomNotification object:nil]; @@ -131,6 +128,18 @@ [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionNewRoomNotification object:nil]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + #pragma mark - - (void)refreshSearchResult:(NSNotification *)notif diff --git a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m index 47300a956..66d724005 100644 --- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m +++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m @@ -35,6 +35,8 @@ id kThemeServiceDidChangeThemeNotificationObserver; } +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation DirectoryViewController @@ -46,6 +48,8 @@ // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenRoomDirectory]; } - (void)viewDidLoad @@ -106,9 +110,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"Directory"]; // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -135,6 +136,8 @@ // the selected room (if any) is highlighted. [self refreshCurrentSelectedCell:YES]; } + + [self.screenTimer start]; } - (void)viewWillDisappear:(BOOL)animated @@ -148,6 +151,12 @@ [super viewWillDisappear:animated]; } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + - (void)displayWitDataSource:(PublicRoomsDirectoryDataSource *)dataSource2 { // Let the data source provide cells diff --git a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m index bd0a07b3f..8c1956330 100644 --- a/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m +++ b/Riot/Modules/GlobalSearch/UnifiedSearchViewController.m @@ -79,12 +79,13 @@ [titles addObject:[VectorL10n searchRooms]]; recentsViewController = [RecentsViewController recentListViewController]; + recentsViewController.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSearchRooms]; recentsViewController.enableSearchBar = NO; - recentsViewController.screenName = @"UnifiedSearchRooms"; [viewControllers addObject:recentsViewController]; [titles addObject:[VectorL10n searchMessages]]; messagesSearchViewController = [HomeMessagesSearchViewController searchViewController]; + messagesSearchViewController.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSearchMessages]; [viewControllers addObject:messagesSearchViewController]; // Add search People tab @@ -92,11 +93,13 @@ peopleSearchViewController = [ContactsTableViewController contactsTableViewController]; peopleSearchViewController.contactsTableViewControllerDelegate = self; peopleSearchViewController.disableFindYourContactsFooter = YES; + peopleSearchViewController.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSearchPeople]; [viewControllers addObject:peopleSearchViewController]; // add Files tab [titles addObject:[VectorL10n searchFiles]]; filesSearchViewController = [HomeFilesSearchViewController searchViewController]; + filesSearchViewController.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSearchFiles]; [viewControllers addObject:filesSearchViewController]; [self initWithTitles:titles viewControllers:viewControllers defaultSelected:0]; @@ -144,9 +147,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"UnifiedSearch"]; - // Let's child display the loading not the home view controller if (self.activityIndicator) { diff --git a/Riot/Modules/Home/HomeViewController.m b/Riot/Modules/Home/HomeViewController.m index 0f7f5234b..2c2f702d7 100644 --- a/Riot/Modules/Home/HomeViewController.m +++ b/Riot/Modules/Home/HomeViewController.m @@ -69,7 +69,7 @@ selectedRoomId = nil; selectedCollectionViewContentOffset = -1; - self.screenName = @"Home"; + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenHome]; } - (void)viewDidLoad diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings index fd15a50c4..295ea3259 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/de.lproj/MatrixKit.strings @@ -492,8 +492,15 @@ "call_consulting_with_user" = "Bei %@ anfragen"; "microphone_access_not_granted_for_voice_message" = "%@ fehlt die Berechtigung, für Sprachnachrichten auf das Mikrofon zuzugreifen"; "message_reply_to_sender_sent_a_voice_message" = "hat eine Sprachnachricht gesendet."; -"attachment_size_prompt_title" = "Größe zum Senden"; +"attachment_size_prompt_title" = "Zum Senden die Größe auswählen"; "attachment_large_with_resolution" = "Groß %@ (~%@)"; "attachment_medium_with_resolution" = "Mittel %@ (~%@)"; "attachment_small_with_resolution" = "Klein %@ (~%@)"; -"attachment_size_prompt_message" = "Du kannst dies in den Einstellungen ausschalten."; +"attachment_size_prompt_message" = "Dies kannst du in den Einstellungen abschalten."; +"auth_reset_password_error_not_found" = "Nicht gefunden"; +"auth_username_in_use" = "Benutzername bereits verwendet"; +"auth_invalid_user_name" = "Ungültiger Benutzername"; +"rename" = "Umbenennen"; +"attachment_unsupported_preview_message" = "Dieser Dateityp wird nicht unterstützt."; +"attachment_unsupported_preview_title" = "Vorschau kann nicht angezeigt werden"; +"auth_reset_password_error_unauthorized" = "Nicht Authorisiert"; diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/en.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/en.lproj/MatrixKit.strings index f9e40406a..b7937f194 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/en.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/en.lproj/MatrixKit.strings @@ -258,6 +258,7 @@ "message_reply_to_sender_sent_an_audio_file" = "sent an audio file."; "message_reply_to_sender_sent_a_voice_message" = "sent a voice message."; "message_reply_to_sender_sent_a_file" = "sent a file."; +"message_reply_to_sender_sent_their_location" = "has shared their location."; "message_reply_to_message_to_reply_to_prefix" = "In reply to"; // Room members diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/fr.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/fr.lproj/MatrixKit.strings index 758c3a3cb..61a64b321 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/fr.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/fr.lproj/MatrixKit.strings @@ -470,6 +470,11 @@ "attachment_small_with_resolution" = "Petit %@ (~%@)"; "attachment_size_prompt_message" = "Vous pouvez désactiver ceci dans les paramètres."; "attachment_size_prompt_title" = "Préciser la taille pour l’envoi"; -"auth_username_in_use" = ""; +"auth_username_in_use" = "Nom d’utilisateur déjà pris"; "auth_invalid_user_name" = "Nom d’utilisateur invalide"; "rename" = "Renommer"; +"auth_reset_password_error_not_found" = "Non trouvé"; +"auth_reset_password_error_unauthorized" = "Non autorisé"; +"attachment_unsupported_preview_message" = "Ce type de fichier n’est pas pris en charge."; +"attachment_unsupported_preview_title" = "Prévisualisation impossible"; +"room_displayname_all_other_members_left" = "%@ (Quitté)"; diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/id.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/id.lproj/MatrixKit.strings index 6dc5772bd..ef186655b 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/id.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/id.lproj/MatrixKit.strings @@ -345,7 +345,7 @@ // Country picker "country_picker_title" = "Pilih sebuah negara"; "microphone_access_not_granted_for_voice_message" = "Pesan suara membutuhkan akses ke Mikrofon tetapi %@ tidak memiliki izin untuk menggunakannya"; -"local_contacts_access_discovery_warning" = "Untuk menemukan kontak Anda yang sudah menggunakan Matrix, %@ dapat mengirim alamat email dan nomor telepon di kontak Anda ke server identitas Matrix yang Anda pilih. Di mana saja yang didukung, data personal akan di-hash sebelum dikirim - mohon cek kebijakan privasi identitas server Anda untuk detail lainnya."; +"local_contacts_access_discovery_warning" = "Untuk menemukan kontak Anda yang sudah menggunakan Matrix, %@ dapat mengirim alamat email dan nomor telepon di kontak Anda ke server identitas Matrix yang Anda pilih. Di mana saja yang didukung, data personal akan di-hash sebelum dikirim — mohon cek kebijakan privasi identitas server Anda untuk detail lainnya."; "local_contacts_access_discovery_warning_title" = "Penemuan pengguna"; "local_contacts_access_not_granted" = "Penemuan pengguna dari kontak lokal membutuhkan akses ke kontak Anda tetapi %@ tidak memiliki izin untuk menggunakannya"; "microphone_access_not_granted_for_call" = "Panggilan membutuhkan akses ke Mikrofon tetapi %@ tidak memiliki izin untuk menggunakannya"; diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/pt.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/pt.lproj/MatrixKit.strings new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/pt.lproj/MatrixKit.strings @@ -0,0 +1 @@ + diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings index 8b1378917..1efe28839 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sk.lproj/MatrixKit.strings @@ -1 +1,545 @@ + +"login_error_login_email_not_yet" = "Zatiaľ ste neklikli na odkaz zaslaný emailom"; +"login_error_user_in_use" = "Toto používateľské meno sa už používa"; +"login_error_limit_exceeded" = "Bol prekročený maximálny počet požiadaviek"; +"room_left_for_dm" = "Odišli ste"; + +// Room creation +"room_creation_name_title" = "Názov miestnosti:"; +"account_msisdn_validation_title" = "Čaká sa na overenie"; +"account_email_validation_title" = "Čaká sa na overenie"; + +// Account +"account_save_changes" = "Uložiť zmeny"; +"room_event_encryption_verify_title" = "Overiť reláciu\n\n"; +"room_event_encryption_info_device_not_verified" = "Neoverené"; +"room_event_encryption_info_device_fingerprint" = "Ed25519 odtlačok prsta\n"; +"room_event_encryption_info_device_name" = "Verejný názov\n"; +"room_event_encryption_info_device_unknown" = "neznáma relácia\n"; +"room_event_encryption_info_event_decryption_error" = "Chyba dešifrovania\n"; +"room_event_encryption_info_event_session_id" = "ID relácie\n"; +"room_event_encryption_info_event_user_id" = "ID používateľa:\n"; +"room_event_encryption_info_event" = "Informácie o udalosti\n"; +"device_details_rename_prompt_title" = "Názov relácie"; +"device_details_last_seen" = "Naposledy videné\n"; +"device_details_name" = "Verejný názov\n"; + +// Devices +"device_details_title" = "Informácie o relácii\n"; +"room_displayname_all_other_members_left" = "%@ (Odišiel)"; + +// room display name +"room_displayname_empty_room" = "Prázdna miestnosť"; +"notice_invalid_attachment" = "neplatná príloha"; +"notice_file_attachment" = "súborová attachment"; +"notice_video_attachment" = "video príloha"; +"notice_audio_attachment" = "zvuková príloha"; +"notice_image_attachment" = "obrázková príloha"; +"notice_encrypted_message" = "Šifrovaná správa"; +"notice_room_created_for_dm" = "%@ sa pripojil/a."; +"end_call" = "Ukončiť hovor"; +"reject_call" = "Odmietnuť hovor"; +"show_details" = "Zobraziť podrobnosti"; +"cancel_download" = "Zrušiť sťahovanie"; +"cancel_upload" = "Zrušiť nahrávanie"; +"select_all" = "Vybrať všetko"; +"start_chat" = "Začať konverzáciu"; +"sign_up" = "Zaregistrovať sa"; +"login_error_resource_limit_exceeded_contact_button" = "Kontaktovať správcu"; +"register_error_title" = "Registrácia zlyhala"; +"login_invalid_param" = "Neplatný parameter"; +"login_error_bad_json" = "Chybné údaje vo formáte JSON"; +"login_error_forbidden" = "Neplatné používateľské meno/heslo"; + +// Room Screen + +// general errors + +// Home Screen + +// Last seen time + +// call events + +/* -*- + Automatic localization for en + + The following key/value pairs were extracted from the android i18n file: + /console/src/main/res/values/strings.xml. +*/ + + +// titles + +// button names +"ok" = "OK"; +"login_error_title" = "Prihlásenie zlyhalo"; +"login_prompt_email_token" = "Zadajte svoj e-mailový overovací token:"; +"login_email_info" = "Zadanie e-mailovej adresy umožní ostatným používateľom ľahšie vás nájsť v službe Matrix a umožní vám v budúcnosti obnoviť heslo."; +"login_display_name_placeholder" = "Zobrazované meno (napr. Fero Novák)"; +"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ý"; + +// gcm section +"settings_config_home_server" = "Domovský server: %@"; +"notification_settings_custom_sound" = "Vlastný zvuk"; +"notification_settings_always_notify" = "Vždy oznamovať"; +"notification_settings_never_notify" = "Nikdy neoznamovať"; + +// members list Screen + +// accounts list Screen + +// image size selection + +// invitation members list Screen + +// room creation dialog Screen + +// room info dialog Screen + +// room details dialog screen +"room_details_title" = "Podrobnosti o miestnosti"; +"num_members_other" = "%@ používatelia"; +"num_members_one" = "%@ používateľ"; +"create_account" = "Vytvoriť účet"; +"create_room" = "Vytvoriť miestnosť"; +"notice_room_leave_by_you" = "Odišli ste"; +"notice_room_join_by_you" = "Vstúpili ste"; +"notice_room_leave" = "%@ odišiel/a"; +"notice_room_join" = "%@ sa pripojil/a"; +"language_picker_default_language" = "Predvolené (%@)"; +"user_id_placeholder" = "napr.: @fero:domovskyserver"; +"power_level" = "Úroveň právomoci"; + +// Others +"user_id_title" = "ID používateľa:"; +"e2e_passphrase_create" = "Vytvoriť heslo"; +"e2e_passphrase_confirm" = "Potvrďte heslo"; +"e2e_passphrase_enter" = "Zadajte heslo"; + +// Search +"search_no_results" = "Žiadne výsledky"; +"contact_local_contacts" = "Lokálne kontakty"; + +// Contacts +"contact_mx_users" = "Používatelia Matrix"; +"attachment_multiselection_original" = "Aktuálna veľkosť"; +"login_optional_field" = "voliteľné"; +"ssl_remain_offline" = "Ignorovať"; +"ssl_logout_account" = "Odhlásiť sa"; +"call_ringing" = "Vyzváňanie…"; + +// Settings keys + +// call string +"call_connecting" = "Pripájanie…"; +"notification_settings_sender_hint" = "@pouzivatel:domena.sk"; + +// Settings screen +"settings_title_config" = "Nastavenie"; +"unban" = "Zrušiť zákaz"; +"ban" = "Zakázať"; +"kick" = "Vylúčiť"; +"invite" = "Pozvať"; +"membership_ban" = "Vylúčený"; +"membership_invite" = "Pozvaní"; +"login" = "Prihlásiť sa"; + +// actions +"action_logout" = "Odhlásiť sa"; +"rename" = "Premenovať"; +"view" = "Zobraziť"; +"delete" = "Vymazať"; +"share" = "Zdieľať"; +"redact" = "Odstrániť"; +"resend" = "Odoslať znovu"; +"copy_button_name" = "Kopírovať"; +"send" = "Odoslať"; +"leave" = "Opustiť"; +"save" = "Uložiť"; +"cancel" = "Zrušiť"; +"private" = "Súkromné"; +"public" = "Verejné"; +"default" = "predvolené"; +"error" = "Chyba"; +"unsent" = "Neodoslané"; +"offline" = "nedostupný"; +"e2e_export" = "Exportovať"; +"e2e_import" = "Importovať"; +"format_time_d" = "d"; +"format_time_h" = "h"; +"format_time_m" = "m"; + +// Time +"format_time_s" = "s"; +"group_section" = "Skupiny"; + +// Groups +"group_invite_section" = "Pozvánky"; +"attachment_e2e_keys_import" = "Importovať..."; +"room_creation_participants_title" = "Účastníci:"; +"room_event_encryption_verify_ok" = "Overiť"; +"room_event_encryption_info_unverify" = "Zrušiť overenie"; +"room_event_encryption_info_verify" = "Overiť..."; +"room_event_encryption_info_device_blocked" = "Na čiernej listine"; +"room_event_encryption_info_device_verified" = "Overené"; +"room_event_encryption_info_device_verification" = "Overenie\n"; +"room_event_encryption_info_device_id" = "ID\n"; +"room_event_encryption_info_event_none" = "žiadne"; +"room_event_encryption_info_event_unencrypted" = "nezašifrované"; +"room_event_encryption_info_event_algorithm" = "Algoritmus\n"; +"device_details_delete_prompt_title" = "Overenie"; +"device_details_identifier" = "ID\n"; + +// Settings +"settings" = "Nastavenia"; +"notice_sticker" = "nálepka"; +"unignore" = "Prestať ignorovať"; +"ignore" = "Ignorovať"; +"mention" = "Zmieniť sa"; +"submit" = "Odoslať"; +"retry" = "Skúsiť znovu"; +"continue" = "Pokračovať"; +"close" = "Zavrieť"; +"back" = "Späť"; +"abort" = "Prerušiť"; +"yes" = "Áno"; + +// Action +"no" = "Nie"; +"login_desktop_device" = "Desktop"; +"login_tablet_device" = "Tablet"; +"login_mobile_device" = "Mobil"; +"login_leave_fallback" = "Zrušiť"; +"login_home_server_title" = "URL domovského servera:"; +"login_email_placeholder" = "Emailová adresa"; +"login_password_placeholder" = "Heslo"; +"login_identity_server_title" = "URL adresa servera totožností:"; +"login_server_url_placeholder" = "URL (napr. https://matrix.org)"; + +// Login Screen +"login_create_account" = "Vytvoriť účet:"; +/* *********************** */ +/* iOS specific */ +/* *********************** */ + +"matrix" = "Matrix"; +"notification_settings_select_room" = "Vybrať miestnosť"; +"notification_settings_enable_notifications_warning" = "Všetky oznámenia sú v súčasnosti vypnuté pre všetky zariadenia."; +"notification_settings_enable_notifications" = "Povoliť oznámenia"; + +// Notification settings screen +"notification_settings_disable_all" = "Zakázať všetky oznámenia"; +"settings_title_notifications" = "Oznámenia"; +"notification_settings_room_rule_title" = "Miestnosť: '%@'"; +"auth_username_in_use" = "Používateľské meno sa už používa"; +"login_error_must_start_http" = "URL adresa musí začínať http[s]://"; +"auth_reset_password_error_not_found" = "Nenájdené"; +"notice_unsupported_attachment" = "Nepodporovaná príloha: %@"; +"notice_encryption_enabled_unknown_algorithm" = "%1$@ zapol end-to-end šifrovanie (nerozpoznaný algoritmus %2$@)."; +"notice_encryption_enabled_ok" = "%@ zapol end-to-end šifrovanie."; +"notice_room_related_groups" = "Skupiny pridružené k tejto miestnosti sú: %@"; +"notice_room_aliases_for_dm" = "Aliasy sú: %@"; +"notice_room_aliases" = "Aliasy miestnosti sú: %@"; +"notice_room_power_level_acting_requirement" = "Minimálne úrovne oprávnenia, ktoré musí mať používateľ pred konaním, sú:"; +"notice_room_power_level_intro" = "Úrovne oprávnenia členov miestnosti sú:"; +"notice_room_power_level_intro_for_dm" = "Úrovne oprávnenia členov sú:"; +"notice_room_join_rule_public_by_you_for_dm" = "Zverejnili ste ju."; +"notice_room_join_rule_public_by_you" = "Túto miestnosť ste zverejnili."; +"notification_settings_global_info" = "Nastavenia oznámení sa ukladajú do vášho používateľského účtu a sú zdieľané medzi všetkými klientmi, ktorí ich podporujú (vrátane oznámení na ploche).\n\nPravidlá sa uplatňujú v poradí; prvé pravidlo, ktoré sa zhoduje, určuje výsledok správy.\nTakže: Oznámenia o správe sú dôležitejšie ako oznámenia na miestnosť, ktoré sú dôležitejšie ako oznámenia na odosielateľa.\nPri viacerých pravidlách rovnakého druhu má prednosť to, ktoré sa zhoduje ako prvé v zozname."; + +// contacts list screen +"invitation_message" = "Rád by som sa s vami porozprával pomocou Matrixu. Viac informácií nájdete na webovej stránke http://matrix.org."; +"notice_room_history_visible_to_members_from_joined_point_by_you" = "Zviditeľnili ste budúcu históriu miestnosti pre všetkých členov miestnosti, od okamihu ich vstupu."; +"notice_room_history_visible_to_members_from_invited_point_by_you_for_dm" = "Zviditeľnili ste budúce správy pre všetkých, od okamihu pozvania."; +"notice_room_history_visible_to_members_from_invited_point_by_you" = "Zviditeľnili ste budúcu históriu miestnosti pre všetkých členov miestnosti, od okamihu pozvania."; +"notice_room_history_visible_to_members_by_you_for_dm" = "Zviditeľnili ste budúce správy pre všetkých členov miestnosti."; +"notice_room_history_visible_to_members_by_you" = "Zviditeľnili ste budúcu históriu miestnosti pre všetkých jej členov."; +"notice_room_history_visible_to_anyone_by_you" = "Sprístupnili ste budúcu históriu miestnosti každému."; +"notice_encryption_enabled_unknown_algorithm_by_you" = "Zapli ste end-to-end šifrovanie (nerozpoznaný algoritmus %@)."; +"notice_encryption_enabled_ok_by_you" = "Zapli ste end-to-end šifrovanie."; +"notice_room_created_by_you_for_dm" = "Vstúpili ste."; +"notice_room_created_by_you" = "Vytvorili ste a nastavili ste miestnosť."; +"notice_profile_change_redacted_by_you" = "Aktualizovali ste svoj profil %@"; +"notice_room_topic_removed_by_you" = "Odstránili ste tému"; +"notice_room_name_removed_by_you_for_dm" = "Odstránili ste názov"; +"notice_room_name_removed_by_you" = "Odstránili ste názov miestnosti"; +"notice_conference_call_request_by_you" = "Požiadali ste o VoIP konferenciu"; +"notice_declined_video_call_by_you" = "Odmietli ste hovor"; +"notice_ended_video_call_by_you" = "Ukončili ste hovor"; +"notice_room_name_changed_by_you_for_dm" = "Zmenili ste svoje meno na %@."; +"notice_room_name_changed_by_you" = "Zmenili ste názov miestnosti na %@."; +"notice_topic_changed_by_you" = "Zmenili ste tému na \"%@\"."; +"notice_display_name_removed_by_you" = "Odstránili ste svoje zobrazované meno"; +"notice_display_name_changed_from_by_you" = "Zmenili ste si zobrazované meno z %@ na %@"; +"notice_display_name_set_by_you" = "Nastavili ste si zobrazované meno na %@"; +"notice_avatar_url_changed_by_you" = "Zmenili ste si obrázok v profile"; +"notice_room_withdraw_by_you" = "Odvolali ste pozvánku pre %@"; +"notice_room_ban_by_you" = "Zakázali ste používateľa %@"; +"notice_room_unban_by_you" = "Zrušili ste zákaz pre %@"; +"notice_room_kick_by_you" = "Vylúčili ste používateľa %@"; +"notice_room_reject_by_you" = "Odmietli ste pozvanie"; +"notice_room_third_party_revoked_invite_by_you_for_dm" = "Odvolali ste pozvánku pre %@"; +"notice_room_third_party_revoked_invite_by_you" = "Odvolali ste pozvánku pre %@ do miestnosti"; +"notice_room_third_party_registered_invite_by_you" = "Prijali ste pozvanie do %@"; +"notice_room_third_party_invite_by_you_for_dm" = "Pozvali ste používateľa %@"; +"notice_room_third_party_invite_by_you" = "Odoslali ste pozvánku používateľovi %@ do miestnosti"; +"notice_room_invite_you" = "%@ vás pozval"; + +// Notice Events with "You" +"notice_room_invite_by_you" = "Pozvali ste %@"; +"notice_conference_call_finished" = "Skončila sa VoIP konferencia"; +"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 \"%@\"."; +"notice_display_name_removed" = "%@ odstránil svoje zobrazované meno"; +"notice_display_name_changed_from" = "%@ zmenil/a svoje zobrazované meno z %@ na %@"; +"notice_display_name_set" = "%@ nastavil/a svoje zobrazované meno na %@"; +"notice_avatar_url_changed" = "%@ zmenil/a svoj obrázok"; +"notice_room_reason" = ". Dôvod: %@"; +"notice_room_withdraw" = "%@ odvolal pozvanie od %@"; +"notice_room_ban" = "%@ zakázal používateľa %@"; +"notice_room_unban" = "%@ zrušil zákaz pre %@"; +"notice_room_reject" = "%@ odmietol pozvanie"; +"notice_room_third_party_revoked_invite_for_dm" = "%@ odvolal pozvánku pre %@"; +"notice_room_third_party_revoked_invite" = "%@ odvolal pozvánku pre %@, aby sa pripojil k miestnosti"; +"notice_room_third_party_registered_invite" = "%@ prijal pozvanie do %@"; +"notice_room_third_party_invite_for_dm" = "%@ pozval %@"; +"notice_room_third_party_invite" = "%@ poslal pozvánku %@ do miestnosti"; + +/* -*- + Automatic localization for en + + The following key/value pairs were extracted from the android i18n file: + /matrix-sdk/src/main/res/values/strings.xml. +*/ + +"notice_room_invite" = "%@ pozval %@"; + +// Language picker +"language_picker_title" = "Vyberte jazyk"; + +// Country picker +"country_picker_title" = "Vyberte krajnu"; +"microphone_access_not_granted_for_voice_message" = "Hlasové správy vyžadujú prístup k mikrofónu, ale %@ nemá povolenie na jeho používanie"; +"local_contacts_access_discovery_warning" = "Ak chcete vyhľadať kontakty, ktoré už používajú Matrix, %@ môže odoslať e-mailové adresy a telefónne čísla z vášho adresára na zvolený server identity Matrix. Ak je to podporované, osobné údaje sa pred odoslaním zahašujú - ďalšie podrobnosti nájdete v zásadách ochrany osobných údajov vášho servera totožností."; +"local_contacts_access_discovery_warning_title" = "Vyhľadávanie používateľov"; +"local_contacts_access_not_granted" = "Zisťovanie používateľov z lokálnych kontaktov vyžaduje prístup k vašim kontaktom, ale %@ nemá oprávnenie na ich používanie"; +"microphone_access_not_granted_for_call" = "Hovory vyžadujú prístup k mikrofónu, ale %@ nemá povolenie na jeho používanie"; + +// Permissions +"camera_access_not_granted_for_call" = "Videohovory vyžadujú prístup ku kamere, ale %@ nemá povolenie na jej používanie"; +"ssl_homeserver_url" = "URL adresa domovského servera: %@"; +"network_error_not_reachable" = "Skontrolujte prosím pripojenie k sieti"; +"not_supported_yet" = "Zatiaľ nie je podporované"; +"error_common_message" = "Vyskytla sa chyba. Skúste to prosím neskôr."; +"e2e_passphrase_not_match" = "Prístupové frázy sa musia zhodovať"; +"e2e_passphrase_empty" = "Prístupová fráza nesmie byť prázdna"; +"e2e_passphrase_too_short" = "Príliš krátka prístupová fráza (Musí mať minimálne %d znakov)"; +"e2e_export_prompt" = "Tento proces umožňuje exportovať kľúče od správ, ktoré ste prijali v zašifrovaných miestnostiach, do lokálneho súboru. Tento súbor potom budete môcť v budúcnosti importovať do iného klienta Matrix, takže tento klient bude môcť tieto správy tiež dešifrovať.\nExportovaný súbor umožní komukoľvek, kto si ho môže prečítať, dešifrovať všetky zašifrované správy, ktoré vidíte, preto by ste mali dbať na jeho bezpečnosť."; + +// E2E export +"e2e_export_room_keys" = "Exportovať kľúče miestností"; +"e2e_import_prompt" = "Tento proces umožňuje importovať šifrovacie kľúče, ktoré ste predtým exportovali z iného klienta Matrix. Potom budete môcť dešifrovať všetky správy, ktoré mohol dešifrovať iný klient.\nExportovaný súbor je chránený prístupovou frázou. Tu by ste mali zadať prístupovú frázu, aby ste súbor dešifrovali."; + +// E2E import +"e2e_import_room_keys" = "Importovať kľúče miestností"; +"search_searching" = "Prebieha vyhľadávanie..."; +"attachment_unsupported_preview_message" = "Tento typ súboru nie je podporovaný."; +"attachment_unsupported_preview_title" = "Nie je možné zobraziť náhľad"; +"attachment_e2e_keys_file_prompt" = "Tento súbor obsahuje šifrovacie kľúče exportované z klienta Matrix.\nChcete zobraziť obsah súboru alebo importovať kľúče, ktoré obsahuje?"; +"attachment_multiselection_size_prompt" = "Chcete odoslať obrázok ako:"; +"attachment_cancel_upload" = "Zrušiť nahrávanie?"; +"attachment_cancel_download" = "Zrušiť sťahovanie?"; +"attachment_large_with_resolution" = "Veľká %@ (~%@)"; +"attachment_medium_with_resolution" = "Stredná %@ (~%@)"; +"attachment_small_with_resolution" = "Malá %@ (~%@)"; +"attachment_large" = "Veľká (~%@)"; +"attachment_original" = "Skutočná veľkosť (%@)"; +"attachment_size_prompt_message" = "Túto funkciu môžete kedykoľvek vypnúť v nastaveniach."; + +// 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 members +"room_member_ignore_prompt" = "Ste si istí, že chcete skryť všetky správy od tohto používateľa?"; +"message_reply_to_sender_sent_a_file" = "poslal súbor."; +"message_reply_to_sender_sent_a_voice_message" = "poslal/a zvukovú správu."; +"message_reply_to_sender_sent_an_audio_file" = "poslal/a zvukový súbor."; +"message_reply_to_sender_sent_a_video" = "poslal video."; + +// Reply to message +"message_reply_to_sender_sent_an_image" = "poslal obrázok."; +"room_left" = "Opustili ste miestnosť"; +"room_error_timeline_event_not_found" = "Aplikácia sa pokúšala načítať konkrétny bod na časovej osi tejto miestnosti, ale nedokázala ho nájsť"; +"room_error_timeline_event_not_found_title" = "Nepodarilo sa načítať pozíciu na časovej osi"; +"room_error_cannot_load_timeline" = "Nepodarilo sa načítať časovú os"; +"room_error_topic_edition_not_authorized" = "Nie ste oprávnený upraviť tému tejto miestnosti"; +"room_error_name_edition_not_authorized" = "Nemáte oprávnenie upraviť názov tejto miestnosti"; +"room_error_join_failed_empty_room" = "V súčasnosti nie je možné znovu vstúpiť do prázdnej miestnosti."; +"room_error_join_failed_title" = "Nepodarilo sa vstúpiť do miestnosti"; + +// Room +"room_please_select" = "Prosím, vyberte si miestnosť"; +"room_creation_participants_placeholder" = "(napr. @fero:domovskyserver1; @jano:domovskyserver2...)"; +"room_creation_alias_placeholder_with_homeserver" = "(napr. #foo%@)"; +"room_creation_alias_placeholder" = "(napr. #foo:napriklad.sk)"; +"room_creation_alias_title" = "Alias miestnosti:"; +"account_error_push_not_allowed" = "Oznámenia nie sú povolené"; +"account_error_msisdn_wrong_description" = "Zdá sa, že toto nie je platné telefónne číslo"; +"account_error_msisdn_wrong_title" = "Neplatné telefónne číslo"; +"account_error_email_wrong_description" = "Toto nevyzerá ako platná e-mailová adresa"; +"account_error_email_wrong_title" = "Neplatná e-mailová adresa"; +"account_error_matrix_session_is_not_opened" = "Relácia Matrix nie je otvorená"; +"account_error_picture_change_failed" = "Zmena obrázka sa nepodarila"; +"account_error_display_name_change_failed" = "Zmena zobrazovaného mena sa nepodarila"; +"account_msisdn_validation_error" = "Nie je možné overiť telefónne číslo."; +"account_msisdn_validation_message" = "Odoslali sme SMS s aktivačným kódom. Zadajte prosím tento kód nižšie."; +"account_email_validation_error" = "Nie je možné overiť e-mailovú adresu. Skontrolujte svoj e-mail a kliknite na odkaz, ktorý obsahuje. Po vykonaní tohto kroku kliknite na tlačidlo Pokračovať"; +"account_email_validation_message" = "Prosím, skontrolujte svoj e-mail a kliknite na odkaz, ktorý obsahuje. Po dokončení tohto kroku kliknite na tlačidlo Pokračovať."; +"room_event_encryption_verify_message" = "Ak chcete overiť, či je táto relácia dôveryhodná, kontaktujte jej vlastníka iným spôsobom (napr. osobne alebo telefonicky) a opýtajte sa ho, či kľúč, ktorý vidí v nastaveniach používateľa pre túto reláciu, sa zhoduje s nižšie uvedeným kľúčom:\n\n\tNázov relácie: %@\n\tID relácie: %@\n\tKľúč relácie: %@\n\nAk sa zhoduje, stlačte tlačidlo overiť nižšie. Ak nie, potom túto reláciu zachytáva niekto iný a pravdepodobne budete chcieť namiesto toho stlačiť tlačidlo čiernej listiny.\n\nV budúcnosti bude tento proces overovania sofistikovanejší."; +"room_event_encryption_info_block" = "Pridať na čiernu listinu"; +"room_event_encryption_info_unblock" = "Odstrániť z čiernej listiny"; +"room_event_encryption_info_device" = "\nInformácie o relácii odosielateľa\n"; +"room_event_encryption_info_event_identity_key" = "Identifikačný kľúč Curve25519\n"; + +// Encryption information +"room_event_encryption_info_title" = "Informácie o end-to-end šifrovaní\n\n"; +"device_details_last_seen_format" = "%@ @ %@\n"; +"settings_enable_push_notifications" = "Zapnúť push oznámenia"; +"settings_enable_inapp_notifications" = "Zapnúť oznámenia v aplikácii"; +"room_displayname_more_than_two_members" = "%@ a %@ ďalší"; +"room_displayname_two_members" = "%@ a %@"; +"notice_crypto_error_unknown_inbound_session_id" = "Relácia odosielateľa nám neposlala kľúče pre túto správu."; +"notice_crypto_unable_to_decrypt" = "** Nepodarilo sa dešifrovať: %@ **"; +"notice_room_history_visible_to_members_from_invited_point" = "%@ zviditeľnil/a budúcu históriu miestnosti pre všetkých jej členov od okamihu, keď sú pozvaní."; +"notice_room_history_visible_to_members_for_dm" = "%@ zmenil/a nastavenie, aby budúce správy boli viditeľné pre všetkých členov miestnosti."; +"notice_room_history_visible_to_members" = "%@ zviditeľnil/a budúcu históriu miestnosti pre všetkých členov miestnosti."; +"notice_room_history_visible_to_anyone" = "%@ zviditeľnil/a budúcu históriu miestnosti pre každého."; +"notification_settings_by_default" = "Predvolene..."; +"notification_settings_other_alerts" = "Iné upozornenia"; +"room_creation_name_placeholder" = "(napr. pracovnaSkupina)"; +"message_reply_to_message_to_reply_to_prefix" = "V odpovedi na"; +"notice_in_reply_to" = "V odpovedi na"; +"notice_room_history_visible_to_members_from_joined_point_for_dm" = "%@ zviditeľnil budúce správy pre všetkých, od okamihu pripojenia."; +"notice_room_history_visible_to_members_from_joined_point" = "%@ zviditeľnil budúcu históriu miestnosti pre všetkých členov miestnosti, od okamihu ich vstupu."; +"notice_room_history_visible_to_members_from_invited_point_for_dm" = "%@ zviditeľnil budúce správy pre všetkých, od okamihu pozvania."; +"notice_error_unknown_event_type" = "Neznámy typ udalosti"; +"notice_room_power_level_event_requirement" = "Minimálne úrovne oprávnenia súvisiace s udalosťami sú:"; +"notice_room_join_rule_public_for_dm" = "%@ ju zverejnil/a."; +"notice_room_join_rule_public" = "%@ zverejnil/a túto miestnosť."; +"notice_room_join_rule_invite_by_you_for_dm" = "Ste ju vytvorili len na pozvanie."; +"notice_room_join_rule_invite_for_dm" = "%@ ju vytvoril len na pozvanie."; +"notice_event_redacted" = ""; +"capture_media" = "Spraviť fotografiu/video"; +"auth_reset_password_error_unauthorized" = "Neoprávnené"; +"auth_invalid_user_name" = "Neplatné používateľské meno"; +"ssl_only_accept" = "Certifikát akceptujte IBA vtedy, ak správca servera zverejnil odtlačok prsta, ktorý sa zhoduje s vyššie uvedeným."; +"ssl_expected_existing_expl" = "Certifikát sa zmenil z predtým dôveryhodného na nedôveryhodný. Server mohol obnoviť svoj certifikát. Obráťte sa na správcu servera, aby vám poskytol očakávaný odtlačok."; +"ssl_unexpected_existing_expl" = "Certifikát sa zmenil na iný, ktorému dôveroval váš telefón. To je VEĽMI NEOBVYKLÉ. Odporúča sa, aby ste tento nový certifikát NEPRIJALI."; +"ssl_cert_new_account_expl" = "Ak správca servera uviedol, že sa to očakáva, skontrolujte, či sa odtlačok prsta uvedený nižšie zhoduje s odtlačkom prsta, ktorý poskytol."; +"ssl_cert_not_trust" = "Môže to znamenať, že niekto úmyselne zachytáva vašu komunikáciu alebo že váš telefón nedôveruje certifikátu, ktorý poskytol vzdialený server."; +"ssl_could_not_verify" = "Nepodarilo sa overiť identitu vzdialeného servera."; + +// unrecognized SSL certificate +"ssl_trust" = "Dôverovať"; +"call_video_with_user" = "Videohovor s %@"; +"call_voice_with_user" = "Hlasový hovor s %@"; +"call_more_actions_change_audio_device" = "Zmeniť zvukové zariadenie"; +"call_more_actions_hold" = "Podržať"; +"call_holded" = "Podržali ste hovor"; +"call_remote_holded" = "%@ podržal hovor"; +"incoming_voice_call" = "Prichádzajúci hlasový hovor"; +"incoming_video_call" = "Prichádzajúci videohovor"; +"settings_config_user_id" = "ID používateľa: %@"; +"settings_config_identity_server" = "Server totožností: %@"; +"notification_settings_suppress_from_bots" = "Stlmiť oznámenia od botov"; +"notification_settings_receive_a_call" = "Upozorniť ma, keď mi niekto zavolá"; +"notification_settings_people_join_leave_rooms" = "Upozorniť ma, keď sa ľudia pridajú alebo opustia miestnosti"; +"notification_settings_invite_to_a_new_room" = "Upozorniť ma, keď budem pozvaný do novej miestnosti"; +"notification_settings_just_sent_to_me" = "Upozorniť ma zvukom na správy odoslané len mne"; +"notification_settings_contain_my_display_name" = "Upozorniť ma zvukom na správy, ktoré obsahujú moje zobrazované meno"; +"notification_settings_contain_my_user_name" = "Upozorniť ma zvukom na správy, ktoré obsahujú moje používateľské meno"; +"notification_settings_highlight" = "Zvýrazniť"; + +// Login Screen +"login_error_already_logged_in" = "Už ste prihlásení"; +"message_unsaved_changes" = "Máte neuložené zmeny. Opustením sa zrušia."; +"membership_leave" = "Odišiel"; +"notice_room_history_visible_to_members_from_joined_point_by_you_for_dm" = "Zviditeľnili ste budúce správy pre všetkých, od okamihu pripojenia."; +"notice_room_kick" = "%@ vylúčil %@"; +"attachment_medium" = "Stredná (~%@)"; +"attachment_small" = "Malá (~%@)"; +"room_no_conference_call_in_encrypted_rooms" = "Konferenčné hovory nie sú podporované v šifrovaných miestnostiach"; +"room_no_power_to_create_conference_call" = "Aby ste mohli začať konferenciu, musíte mať právo pozývať používateľov do miestnosti"; +"device_details_delete_prompt_message" = "Táto operácia si vyžaduje dodatočné overenie.\nAk chcete pokračovať, zadajte svoje heslo."; +"device_details_rename_prompt_message" = "Verejný názov relácie je viditeľný pre ľudí, s ktorými komunikujete"; +"settings_enter_validation_token_for" = "Zadajte overovací token pre %@:"; +"notice_error_unexpected_event" = "Neočakávaná udalosť"; +"notice_error_unsupported_event" = "Nepodporovaná udalosť"; +"notice_room_join_rule_invite_by_you" = "Urobili ste miestnosť len pre pozvaných."; +// New +"notice_room_join_rule_invite" = "%@ vytvoril miestnosť len na pozvanie."; +// Old +"notice_room_join_rule" = "Pravidlo pripojenia je: %@"; +"notice_room_created" = "%@ vytvoril a nastavil miestnosť."; +"notice_profile_change_redacted" = "%@ aktualizoval svoj profil %@"; +"notice_event_redacted_reason" = " [dôvod: %@]"; +"notice_event_redacted_by" = " používateľom %@"; +"notice_room_topic_removed" = "%@ odstránil tému"; +"notice_room_name_removed_for_dm" = "%@ odstránil názov"; +"notice_room_name_removed" = "%@ odstránil/a názov miestnosti"; + +// Events formatter +"notice_avatar_changed_too" = "(obrázok bol tiež zmenený)"; +"resend_message" = "Znovu odoslať správu"; +"reset_to_default" = "Obnoviť na predvolené"; +"invite_user" = "Pozvať používateľa matrix"; +"attach_media" = "Pripojiť médium z knižnice"; +"select_account" = "Vyberte účet"; +"start_video_call" = "Spustiť videohovor"; +"start_voice_call" = "Spustiť hlasový hovor"; +"set_admin" = "Nastaviť správcu"; +"set_moderator" = "Nastaviť moderátora"; +"set_default_power_level" = "Vynulovať úroveň oprávnenia"; +"set_power_level" = "Nastaviť úroveň oprávnenia"; +"submit_code" = "Odoslať kód"; +"dismiss" = "Odmietnuť"; +"discard" = "Zahodiť"; +"login_error_resource_limit_exceeded_message_contact" = "\n\nAk chcete pokračovať v používaní tejto služby, obráťte sa na správcu služby."; +"login_error_resource_limit_exceeded_message_monthly_active_user" = "Tento domovský server dosiahol svoj mesačný limit aktívnych používateľov."; +"login_error_resource_limit_exceeded_message_default" = "Tento domovský server prekročil jeden z limitov zdroja."; +"login_error_resource_limit_exceeded_title" = "Prekročený limit zdrojov"; +"login_error_forgot_password_is_not_supported" = "Zabudnuté heslo nie je v súčasnosti podporované"; +"login_error_not_json" = "Neobsahoval platný JSON"; +"login_error_unknown_token" = "Zadaný prístupový token nebol rozpoznaný"; +"login_error_registration_is_not_supported" = "Registrácia nie je v súčasnosti podporovaná"; +"login_error_do_not_support_login_flows" = "V súčasnosti nepodporujeme žiadny alebo všetky prihlasovacie toky definované týmto domovským serverom"; +"login_error_no_login_flow" = "Nepodarilo sa nám získať autentifikačné informácie z tohto domovského servera"; +"call_more_actions_audio_use_device" = "Reproduktor zariadenia"; +"call_more_actions_unhold" = "Pokračovať"; +"call_invite_expired" = "Platnosť pozvánky na hovor vypršala"; +"notification_settings_per_word_info" = "Slová sa porovnávajú bez ohľadu na veľkosť písmen a môžu obsahovať zástupný znak *. Takže:\nfoo zodpovedá reťazcu foo ohraničenému oddeľovačmi slov (napr. interpunkčnými znamienkami a medzerami alebo začiatkom/koncom riadku).\nfoo* zodpovedá každému takému slovu, ktoré začína foo.\n*foo* zodpovedá každému takému slovu, ktoré obsahuje 3 písmená foo."; +"notice_redaction_by_you" = "Upravili ste udalosť (id: %@)"; +"notice_answered_video_call_by_you" = "Prijali ste hovor"; +"notice_placed_video_call_by_you" = "Uskutočnili ste videohovor"; +"notice_placed_voice_call_by_you" = "Uskutočnili ste hlasový hovor"; +"notice_answered_video_call" = "%@ prijal hovor"; +"notice_placed_video_call" = "%@ uskutočnil videohovor"; +"notice_placed_voice_call" = "%@ uskutočnil hlasový hovor"; +"attachment_size_prompt_title" = "Potvrdiť veľkosť na odoslanie"; +"notice_redaction" = "%@ upravil udalosť (id: %@)"; +"notice_feedback" = "Udalosť spätnej väzby (id: %@): %@"; +"resume_call" = "Pokračovať"; +"answer_call" = "Prijať hovor"; diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sq.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sq.lproj/MatrixKit.strings index 48323eea3..888cbed6b 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sq.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sq.lproj/MatrixKit.strings @@ -475,3 +475,6 @@ "auth_username_in_use" = "Emër përdoruesi i përdorur"; "auth_invalid_user_name" = "Emër i pavlefshëm përdoruesi"; "rename" = "Riemërtojeni"; +"attachment_unsupported_preview_message" = "Ky lloj kartele nuk mbulohet."; +"attachment_unsupported_preview_title" = "S’arrihet të bëhet paraparje"; +"room_displayname_all_other_members_left" = "%@ (Iku)"; diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sv.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sv.lproj/MatrixKit.strings index 317d36810..2dba03f74 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sv.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/sv.lproj/MatrixKit.strings @@ -470,3 +470,11 @@ "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)"; +"auth_reset_password_error_not_found" = "Hittades inte"; +"auth_reset_password_error_unauthorized" = "Obehörig"; +"auth_username_in_use" = "Användarnamn upptaget"; +"auth_invalid_user_name" = "Ogiltigt användarnamn"; +"rename" = "Döp om"; +"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)"; diff --git a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings index f9ca88a63..955e8fd1a 100644 --- a/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings +++ b/Riot/Modules/MatrixKit/Assets/MatrixKitAssets.bundle/uk.lproj/MatrixKit.strings @@ -532,3 +532,29 @@ "attachment_size_prompt_title" = "Підтвердити розмір, щоб надіслати"; "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" = "Ви утримуєте виклик"; +"call_remote_holded" = "%@ утримує виклик"; + +// Login Screen +"login_error_already_logged_in" = "Вже ввійшли"; +"message_unsaved_changes" = "Ви маєте незбережені зміни. Якщо вийдете, їх буде скасовано."; +"attachment_unsupported_preview_message" = "Цей тип файлу не підтримується."; +"attachment_unsupported_preview_title" = "Не вдалося показати попередній перегляд"; +"attachment_e2e_keys_file_prompt" = "Цей файл містить ключі шифрування, експортовані з клієнта Matrix.\nБажаєте переглянути вміст файлу або імпортувати з нього ключі?"; +"room_member_power_level_prompt" = "Ви не зможете скасувати цю зміну, оскільки користувач отримає ті ж повноваження, що й ви.\nВи впевнені?"; +"room_error_timeline_event_not_found" = "Застосунок намагався завантажити певну точку стрічки у цій кімнаті, але не зміг її знайти"; +"room_error_timeline_event_not_found_title" = "Не вдалося завантажити позицію стрічки"; +"room_error_cannot_load_timeline" = "Не вдалося завантажити стрічку"; +"ssl_only_accept" = "Приймайте сертифікат ЛИШЕ якщо адміністратор сервера оприлюднив відбиток, збіжний із наведеним угорі."; +"ssl_expected_existing_expl" = "Сертифікат, якому ви довіряли раніше, змінився на недовірений. Можливо, сервер оновив сертифікат. Запитайте в адміністратора, який відбиток очікуваний."; +"ssl_unexpected_existing_expl" = "Сертифікат відрізняється від довіреного вашим телефоном. Це ВКРАЙ НЕЗВИЧНО. Радимо НЕ ПРИЙМАТИ цей новий сертифікат."; +"ssl_cert_new_account_expl" = "Якщо адміністратор сервера каже, що так має бути, переконайтесь, що відбиток знизу збігається з відбитком адміністратора."; +"ssl_cert_not_trust" = "Це може означати, що хтось зловмисно перехоплює ваш трафік або ваш телефон не довіряє сертифікату, наданому віддаленим сервером."; + +// contacts list screen +"invitation_message" = "Поговорімо в matrix? Сайт https://matrix.org описує, як це зробити."; +"local_contacts_access_discovery_warning" = "Щоб виявляти, які ваші контакти вже в Matrix, %@ може надсилати адреси е-пошти й номери телефонів із вашої адресної книги на обраний сервер ідентифікації Matrix. Особисті дані хешуються перед надсиланням, якщо це підтримується: перегляньте політику приватності свого сервера ідентифікації, щоб дізнатися більше."; +"local_contacts_access_not_granted" = "Для пошуку користувачів серед локальних контактів потрібен доступ до ваших контактів, але %@ не має такого дозволу"; +"e2e_export_prompt" = "Це дає змогу експортувати в локальний файл ключі до повідомлень, отриманих вами в зашифрованих кімнатах. Тоді ви зможете імпортувати файл до іншого клієнта Matrix у майбутньому, і той клієнт також зможе розшифрувати ці повідомлення.\nЕкспортований файл дасть змогу всім, хто його прочитає, розшифрувати всі видимі вам зашифровані повідомлення."; +"e2e_import_prompt" = "Це дає змогу імпортувати ключі шифрування, які ви раніше експортували з іншого клієнта Matrix. Тоді ви зможете розшифрувати всі повідомлення, які міг розшифрувати той клієнт.\nФайл експорту захищений парольною фразою. Введіть парольну фразу сюди, щоб розшифрувати файл."; diff --git a/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m b/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m index 382373a92..942eeec58 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m +++ b/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.m @@ -1031,14 +1031,16 @@ roomDataSource = nil; // Add reason label - _leftRoomReasonLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 5, self.view.frame.size.width - 20, 70)]; - _leftRoomReasonLabel.numberOfLines = 0; - _leftRoomReasonLabel.text = reason; - _leftRoomReasonLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth; + UILabel *leftRoomReasonLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 5, self.view.frame.size.width - 20, 70)]; + leftRoomReasonLabel.numberOfLines = 0; + leftRoomReasonLabel.text = reason; + leftRoomReasonLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth; _bubblesTableView.tableHeaderView = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 80)]; - [_bubblesTableView.tableHeaderView addSubview:_leftRoomReasonLabel]; + [_bubblesTableView.tableHeaderView addSubview:leftRoomReasonLabel]; [_bubblesTableView reloadData]; + _leftRoomReasonLabel = leftRoomReasonLabel; + [self updateViewControllerAppearanceOnRoomDataSourceState]; } @@ -2177,12 +2179,12 @@ if (event && event.eventType == MXEventTypeRoomMessage) { - NSString *msgtype = event.content[@"msgtype"]; + NSString *msgtype = event.content[kMXMessageTypeKey]; NSString* textMessage; if ([msgtype isEqualToString:kMXMessageTypeText]) { - textMessage = event.content[@"body"]; + textMessage = event.content[kMXMessageBodyKey]; } // Show a confirmation popup to the end user @@ -3666,9 +3668,6 @@ MXLogDebug(@"[MXKRoomVC] showAttachmentInCell on an unsent media"); } } - else if (selectedAttachment.type == MXKAttachmentTypeLocation) - { - } else if (selectedAttachment.type == MXKAttachmentTypeFile || selectedAttachment.type == MXKAttachmentTypeAudio) { // Start activity indicator as feedback on file selection. diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.h b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.h index 07e878161..287b8ef8a 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.h @@ -33,7 +33,6 @@ typedef enum : NSUInteger { MXKAttachmentTypeAudio, MXKAttachmentTypeVoiceMessage, MXKAttachmentTypeVideo, - MXKAttachmentTypeLocation, MXKAttachmentTypeFile, MXKAttachmentTypeSticker diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m index e15fd70b3..634ca547f 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.m @@ -91,7 +91,7 @@ NSString *const kMXKAttachmentFileNameBase = @"attatchment"; else { // Note: mxEvent.eventType is supposed to be MXEventTypeRoomMessage here. - NSString *msgtype = eventContent[@"msgtype"]; + NSString *msgtype = eventContent[kMXMessageTypeKey]; if ([msgtype isEqualToString:kMXMessageTypeImage]) { _type = MXKAttachmentTypeImage; @@ -109,12 +109,6 @@ NSString *const kMXKAttachmentFileNameBase = @"attatchment"; _type = MXKAttachmentTypeVideo; MXJSONModelSetDictionary(_thumbnailInfo, eventContent[@"info"][@"thumbnail_info"]); } - else if ([msgtype isEqualToString:kMXMessageTypeLocation]) - { - // Not supported yet - // _type = MXKAttachmentTypeLocation; - return nil; - } else if ([msgtype isEqualToString:kMXMessageTypeFile]) { _type = MXKAttachmentTypeFile; @@ -125,7 +119,7 @@ NSString *const kMXKAttachmentFileNameBase = @"attatchment"; } } - MXJSONModelSetString(_originalFileName, eventContent[@"body"]); + MXJSONModelSetString(_originalFileName, eventContent[kMXMessageBodyKey]); MXJSONModelSetDictionary(_contentInfo, eventContent[@"info"]); MXJSONModelSetMXJSONModel(contentFile, MXEncryptedContentFile, eventContent[@"file"]); diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataWithAppendingMode.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataWithAppendingMode.m index 680086ddd..6790bddf7 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataWithAppendingMode.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataWithAppendingMode.m @@ -102,7 +102,7 @@ static NSAttributedString *messageSeparator = nil; return NO; } } - + // Add all components of the provided message for (MXKRoomBubbleComponent* component in cellData.bubbleComponents) { diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m index 565519ba9..e40ffbf02 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m @@ -115,7 +115,7 @@ return; } - NSString *messageType = self.event.content[@"msgtype"]; + NSString *messageType = self.event.content[kMXMessageTypeKey]; if (!messageType || !([messageType isEqualToString:kMXMessageTypeText] || [messageType isEqualToString:kMXMessageTypeNotice] || [messageType isEqualToString:kMXMessageTypeEmote])) { diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h index 8559fba2d..0510033a6 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h @@ -591,6 +591,25 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; success:(void (^)(NSString *eventId))success failure:(void (^)(NSError *error))failure; +/** + Send a location message to a room. + + While sending, a fake event will be echoed in the messages list. + Once complete, this local echo will be replaced by the event saved by the homeserver. + + @param latitude the location's latitude + @param longitude the location's longitude + @param description an optional description + @param success A block object called when the operation succeeds. It returns + the event id of the event generated on the homeserver + @param failure A block object called when the operation fails. + */ +- (void)sendLocationWithLatitude:(double)latitude + longitude:(double)longitude + description:(NSString *)description + success:(void (^)(NSString *))success + failure:(void (^)(NSError *))failure; + /** Send a generic non state event to a room. diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index 43ab3e623..c439bb54b 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -1914,6 +1914,29 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } } +- (void)sendLocationWithLatitude:(double)latitude + longitude:(double)longitude + description:(NSString *)description + success:(void (^)(NSString *))success + failure:(void (^)(NSError *))failure +{ + __block MXEvent *localEchoEvent = nil; + + // Make the request to the homeserver + [_room sendLocationWithLatitude:latitude + longitude:longitude + description:description + localEcho:&localEchoEvent + success:success failure:failure]; + + if (localEchoEvent) + { + // Make the data source digest this fake local echo message + [self queueEventForProcessing:localEchoEvent withRoomState:self.roomState direction:MXTimelineDirectionForwards]; + [self processQueuedEvents:nil]; + } +} + - (void)sendEventOfType:(MXEventTypeString)eventTypeString content:(NSDictionary*)msgContent success:(void (^)(NSString *eventId))success failure:(void (^)(NSError *error))failure { __block MXEvent *localEchoEvent = nil; @@ -1951,7 +1974,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { else if ([event.type isEqualToString:kMXEventTypeStringRoomMessage]) { // And retry the send the message according to its type - NSString *msgType = event.content[@"msgtype"]; + NSString *msgType = event.content[kMXMessageTypeKey]; if ([msgType isEqualToString:kMXMessageTypeText] || [msgType isEqualToString:kMXMessageTypeEmote]) { // Resend the Matrix event by reusing the existing echo @@ -2712,7 +2735,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { return NO; } - NSString *messageType = event.content[@"msgtype"]; + NSString *messageType = event.content[kMXMessageTypeKey]; if (messageType == nil || [messageType isEqualToString:@"m.bad.encrypted"]) { return NO; } @@ -3928,7 +3951,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { if ([self canPerformActionOnEvent:event]) { - NSString *messageType = event.content[@"msgtype"]; + NSString *messageType = event.content[kMXMessageTypeKey]; if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { @@ -3971,7 +3994,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { { MXEvent *event = [self eventWithEventId:eventId]; BOOL isRoomMessage = event.eventType == MXEventTypeRoomMessage; - NSString *messageType = event.content[@"msgtype"]; + NSString *messageType = event.content[kMXMessageTypeKey]; return isRoomMessage && ([messageType isEqualToString:kMXMessageTypeText] || [messageType isEqualToString:kMXMessageTypeEmote]) @@ -3992,7 +4015,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } else { - editableTextMessage = event.content[@"body"]; + editableTextMessage = event.content[kMXMessageBodyKey]; } return editableTextMessage; @@ -4109,7 +4132,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { NSString *sanitizedText = [self sanitizedMessageText:text]; NSString *formattedText = [self htmlMessageFromSanitizedText:sanitizedText]; - NSString *eventBody = event.content[@"body"]; + NSString *eventBody = event.content[kMXMessageBodyKey]; NSString *eventFormattedBody = event.content[@"formatted_body"]; if (![sanitizedText isEqualToString:eventBody] && (!eventFormattedBody || ![formattedText isEqualToString:eventFormattedBody])) diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.m b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.m index f116c466b..b592db0d6 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.m @@ -45,6 +45,11 @@ return [MatrixKitL10n messageReplyToSenderSentAFile]; } +- (NSString *)senderSentTheirLocation +{ + return [MatrixKitL10n messageReplyToSenderSentTheirLocation]; +} + - (NSString *)messageToReplyToPrefix { return [MatrixKitL10n messageReplyToMessageToReplyToPrefix]; diff --git a/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m b/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m index 246dbd87d..d1daa83ad 100644 --- a/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m +++ b/Riot/Modules/MatrixKit/Models/Search/MXKSearchCellData.m @@ -56,7 +56,7 @@ date = [searchDataSource.eventFormatter dateStringFromEvent:searchResult.result withTime:YES]; // Code from [MXEventFormatter stringFromEvent] for the particular case of a text message - message = [searchResult.result.content[@"body"] isKindOfClass:[NSString class]] ? searchResult.result.content[@"body"] : nil; + message = [searchResult.result.content[kMXMessageBodyKey] isKindOfClass:[NSString class]] ? searchResult.result.content[kMXMessageBodyKey] : nil; } return self; } diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 9177cc5be..46abe46ab 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -175,10 +175,6 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; { isSupportedAttachment = hasUrl || hasFile; } - else if ([msgtype isEqualToString:kMXMessageTypeLocation]) - { - // Not supported yet - } else if ([msgtype isEqualToString:kMXMessageTypeFile]) { isSupportedAttachment = hasUrl || hasFile; @@ -1252,7 +1248,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; else { NSString *msgtype; - MXJSONModelSetString(msgtype, event.content[@"msgtype"]); + MXJSONModelSetString(msgtype, event.content[kMXMessageTypeKey]); NSString *body; BOOL isHTML = NO; @@ -1267,12 +1263,12 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; else if (eventThreadIdentifier) { isHTML = YES; - MXJSONModelSetString(body, event.content[@"body"]); + MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); MXEvent *threadRootEvent = [mxSession.store eventWithEventId:eventThreadIdentifier inRoom:event.roomId]; NSString *threadRootEventContent; - MXJSONModelSetString(threadRootEventContent, threadRootEvent.content[@"body"]); + MXJSONModelSetString(threadRootEventContent, threadRootEvent.content[kMXMessageBodyKey]); body = [NSString stringWithFormat:@"
    In reply to %@
    %@
    %@", [MXTools permalinkToEvent:eventThreadIdentifier inRoom:event.roomId], [MXTools permalinkToUserWithUserId:threadRootEvent.sender], @@ -1283,7 +1279,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; } else { - MXJSONModelSetString(body, event.content[@"body"]); + MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); } if (body) @@ -1333,23 +1329,6 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; *error = MXKEventFormatterErrorUnsupported; } } - else if ([msgtype isEqualToString:kMXMessageTypeLocation]) - { - body = body? body : [MatrixKitL10n noticeLocationAttachment]; - if (![self isSupportedAttachment:event]) - { - MXLogDebug(@"[MXKEventFormatter] Warning: Unsupported attachment %@", event.description); - if (_isForSubtitle || !_settings.showUnsupportedEventsInRoomHistory) - { - body = [MatrixKitL10n noticeInvalidAttachment]; - } - else - { - body = [MatrixKitL10n noticeUnsupportedAttachment:event.description]; - } - *error = MXKEventFormatterErrorUnsupported; - } - } else if ([msgtype isEqualToString:kMXMessageTypeFile]) { body = body? body : [MatrixKitL10n noticeFileAttachment]; @@ -1582,7 +1561,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; else { NSString *body; - MXJSONModelSetString(body, event.content[@"body"]); + MXJSONModelSetString(body, event.content[kMXMessageBodyKey]); // Check sticker validity if (![self isSupportedAttachment:event]) @@ -2000,7 +1979,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; if (event.eventType == MXEventTypeRoomMessage) { - NSString *msgtype = event.content[@"msgtype"]; + NSString *msgtype = event.content[kMXMessageTypeKey]; if ([msgtype isEqualToString:kMXMessageTypeEmote] == NO) { NSString *senderDisplayName = [self senderDisplayNameForEvent:event withRoomState:roomState]; @@ -2121,7 +2100,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; else if (!_isForSubtitle && event.eventType == MXEventTypeRoomMessage && (_emojiOnlyTextFont || _singleEmojiTextFont)) { NSString *message; - MXJSONModelSetString(message, event.content[@"body"]); + MXJSONModelSetString(message, event.content[kMXMessageBodyKey]); if (_emojiOnlyTextFont && [MXKTools isEmojiOnlyString:message]) { diff --git a/Riot/Modules/MatrixKit/Utils/MXKTools.m b/Riot/Modules/MatrixKit/Utils/MXKTools.m index 76753eed5..c941fbc98 100644 --- a/Riot/Modules/MatrixKit/Utils/MXKTools.m +++ b/Riot/Modules/MatrixKit/Utils/MXKTools.m @@ -27,7 +27,6 @@ #import "MXKAppSettings.h" #import #import "MXKSwiftHeader.h" -#import "MXKAnalyticsConstants.h" #pragma mark - Constants definitions @@ -884,9 +883,7 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo // Request address book access [[CNContactStore new] requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) { - [MXSDKOptions.sharedInstance.analyticsDelegate trackValue:[NSNumber numberWithBool:granted] - category:MXKAnalyticsCategoryContacts - name:MXKAnalyticsNameContactsAccessGranted]; + [MXSDKOptions.sharedInstance.analyticsDelegate trackContactsAccessGranted:granted]; dispatch_async(dispatch_get_main_queue(), ^{ diff --git a/Riot/Modules/MediaPicker/Library/MediaAlbumContentViewController.m b/Riot/Modules/MediaPicker/Library/MediaAlbumContentViewController.m index b87015e59..c01c845e6 100644 --- a/Riot/Modules/MediaPicker/Library/MediaAlbumContentViewController.m +++ b/Riot/Modules/MediaPicker/Library/MediaAlbumContentViewController.m @@ -163,9 +163,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"MediaAlbumContent"]; self.navigationItem.title = _assetsCollection.localizedTitle; diff --git a/Riot/Modules/MediaPicker/MediaPickerViewController.m b/Riot/Modules/MediaPicker/MediaPickerViewController.m index 92261789c..190af4d17 100644 --- a/Riot/Modules/MediaPicker/MediaPickerViewController.m +++ b/Riot/Modules/MediaPicker/MediaPickerViewController.m @@ -212,9 +212,6 @@ [super viewWillAppear:animated]; [self userInterfaceThemeDidChange]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"MediaPicker"]; if (!userAlbumsQueue) { diff --git a/Riot/Modules/People/InviteFriendsPresenter.swift b/Riot/Modules/People/InviteFriendsPresenter.swift index 325b7cf71..8f6f9f8b6 100644 --- a/Riot/Modules/People/InviteFriendsPresenter.swift +++ b/Riot/Modules/People/InviteFriendsPresenter.swift @@ -61,7 +61,7 @@ final class InviteFriendsPresenter: NSObject { private func buildShareText(with userId: String) -> String { let userMatrixToLink: String = MXTools.permalinkToUser(withUserId: userId) - return VectorL10n.inviteFriendsShareText(BuildSettings.bundleDisplayName, userMatrixToLink) + return VectorL10n.inviteFriendsShareText(AppInfo.current.displayName, userMatrixToLink) } private func present(_ viewController: UIViewController, animated: Bool) { @@ -73,5 +73,7 @@ final class InviteFriendsPresenter: NSObject { } self.presentingViewController?.present(viewController, animated: animated, completion: nil) + + Analytics.shared.trackScreen(.inviteFriends, duration: nil) } } diff --git a/Riot/Modules/People/PeopleViewController.m b/Riot/Modules/People/PeopleViewController.m index 1cb481a90..ace78de54 100644 --- a/Riot/Modules/People/PeopleViewController.m +++ b/Riot/Modules/People/PeopleViewController.m @@ -51,7 +51,7 @@ directRoomsSectionNumber = 0; - self.screenName = @"People"; + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenPeople]; } - (void)viewDidLoad diff --git a/Riot/Modules/Room/Attachements/AttachmentsViewController.m b/Riot/Modules/Room/Attachements/AttachmentsViewController.m index 596c6a3b5..31bb29519 100644 --- a/Riot/Modules/Room/Attachements/AttachmentsViewController.m +++ b/Riot/Modules/Room/Attachements/AttachmentsViewController.m @@ -73,14 +73,6 @@ return ThemeService.shared.theme.statusBarStyle; } -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"AttachmentsViewer"]; -} - - (void)destroy { [super destroy]; diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index ca0058be5..706776efb 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -32,7 +32,8 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) RoomBubbleCellDataTagCall, RoomBubbleCellDataTagGroupCall, RoomBubbleCellDataTagRoomCreationIntro, - RoomBubbleCellDataTagPoll + RoomBubbleCellDataTagPoll, + RoomBubbleCellDataTagLocation }; /** diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 4c9e9a4e2..81867be76 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -174,8 +174,17 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat self.displayTimestampForSelectedComponentOnLeftWhenPossible = NO; } } - } + break; + } + case MXEventTypeRoomMessage: + { + if (event.location) { + self.tag = RoomBubbleCellDataTagLocation; + self.collapsable = NO; + self.collapsed = NO; + } + } default: break; } @@ -273,6 +282,11 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat return NO; } + if (self.tag == RoomBubbleCellDataTagLocation) + { + return NO; + } + return [super hasNoDisplay]; } @@ -845,6 +859,9 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat case RoomBubbleCellDataTagPoll: shouldAddEvent = NO; break; + case RoomBubbleCellDataTagLocation: + shouldAddEvent = NO; + break; default: break; } @@ -857,7 +874,12 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat { case MXEventTypeRoomMessage: { - NSString *messageType = event.content[@"msgtype"]; + if (event.location) { + shouldAddEvent = NO; + break; + } + + NSString *messageType = event.content[kMXMessageTypeKey]; if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { @@ -991,7 +1013,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat break; case MXEventTypeRoomMessage: { - NSString *msgType = event.content[@"msgtype"]; + NSString *msgType = event.content[kMXMessageTypeKey]; if ([msgType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { @@ -1044,7 +1066,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat { NSString *mediaName = [self accessibilityLabelForAttachmentType:self.attachment.type]; - MXJSONModelSetString(accessibilityLabel, self.events.firstObject.content[@"body"]); + MXJSONModelSetString(accessibilityLabel, self.events.firstObject.content[kMXMessageBodyKey]); if (accessibilityLabel) { accessibilityLabel = [NSString stringWithFormat:@"%@ %@", mediaName, accessibilityLabel]; @@ -1075,9 +1097,6 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat case MXKAttachmentTypeVideo: accessibilityLabel = [VectorL10n mediaTypeAccessibilityVideo]; break; - case MXKAttachmentTypeLocation: - accessibilityLabel = [VectorL10n mediaTypeAccessibilityLocation]; - break; case MXKAttachmentTypeFile: accessibilityLabel = [VectorL10n mediaTypeAccessibilityFile]; break; diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index f160a851f..1f6b51137 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -836,7 +836,7 @@ const CGFloat kTypingCellHeight = 24; break; case MXEventTypeRoomMessage: { - NSString *msgType = event.content[@"msgtype"]; + NSString *msgType = event.content[kMXMessageTypeKey]; if ([msgType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { diff --git a/Riot/Modules/Room/Files/RoomFilesViewController.h b/Riot/Modules/Room/Files/RoomFilesViewController.h index e8f3b7f29..92b799344 100644 --- a/Riot/Modules/Room/Files/RoomFilesViewController.h +++ b/Riot/Modules/Room/Files/RoomFilesViewController.h @@ -16,6 +16,8 @@ limitations under the License. #import "MatrixKit.h" +@class AnalyticsScreenTimer; + /** This view controller displays the attachments of a room. Only one matrix session is handled by this view controller. */ @@ -23,4 +25,9 @@ limitations under the License. @property (nonatomic) BOOL showCancelBarButtonItem; +/** + The screen timer used for analytics if they've been enabled. The default value is nil. + */ +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end diff --git a/Riot/Modules/Room/Files/RoomFilesViewController.m b/Riot/Modules/Room/Files/RoomFilesViewController.m index 1e8e007c1..077311835 100644 --- a/Riot/Modules/Room/Files/RoomFilesViewController.m +++ b/Riot/Modules/Room/Files/RoomFilesViewController.m @@ -110,6 +110,14 @@ [UIView setAnimationsEnabled:NO]; [self roomInputToolbarView:self.inputToolbarView heightDidChanged:0 completion:nil]; [UIView setAnimationsEnabled:YES]; + + [self.screenTimer start]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; } - (void)userInterfaceThemeDidChange diff --git a/Riot/Modules/Room/Location/LocationUserMarkerView.swift b/Riot/Modules/Room/Location/LocationUserMarkerView.swift new file mode 100644 index 000000000..995d1ae56 --- /dev/null +++ b/Riot/Modules/Room/Location/LocationUserMarkerView.swift @@ -0,0 +1,33 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import Reusable +import Mapbox + +class LocationUserMarkerView: MGLAnnotationView, NibLoadable { + + @IBOutlet private var avatarView: UserAvatarView! + + override func awakeFromNib() { + super.awakeFromNib() + translatesAutoresizingMaskIntoConstraints = false + } + + func setAvatarData(_ avatarData: AvatarViewDataProtocol) { + avatarView.fill(with: avatarData) + } +} diff --git a/Riot/Modules/Room/Location/LocationUserMarkerView.xib b/Riot/Modules/Room/Location/LocationUserMarkerView.xib new file mode 100644 index 000000000..26495f925 --- /dev/null +++ b/Riot/Modules/Room/Location/LocationUserMarkerView.xib @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift new file mode 100644 index 000000000..82cebe7cd --- /dev/null +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -0,0 +1,114 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit +import Reusable +import Mapbox +import Keys + +class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegate { + + // MARK: - Constants + + private struct Constants { + static let mapHeight: CGFloat = 300.0 + static let mapTilerKey = RiotKeys().mapTilerAPIKey + static let mapZoomLevel = 15.0 + static let cellBorderRadius: CGFloat = 1.0 + static let cellCornerRadius: CGFloat = 8.0 + } + + // MARK: - Properties + // MARK: Private + + @IBOutlet private var descriptionContainerView: UIView! + @IBOutlet private var descriptionLabel: UILabel! + + private var mapView: MGLMapView! + private var annotationView: LocationUserMarkerView? + + // MARK: Public + + var locationDescription: String? { + get { + descriptionLabel.text + } + set { + descriptionLabel.text = newValue + descriptionContainerView.isHidden = (newValue?.count ?? 0 == 0) + } + } + + override func awakeFromNib() { + super.awakeFromNib() + + mapView = MGLMapView(frame: .zero, styleURL: BuildSettings.tileServerMapURL) + mapView.delegate = self + mapView.logoView.isHidden = true + mapView.attributionButton.isHidden = true + mapView.isUserInteractionEnabled = false + + mapView.translatesAutoresizingMaskIntoConstraints = false + mapView.addConstraint(mapView.heightAnchor.constraint(equalToConstant: Constants.mapHeight)) + vc_addSubViewMatchingParent(mapView) + sendSubviewToBack(mapView) + + clipsToBounds = true + layer.borderWidth = Constants.cellBorderRadius + layer.cornerRadius = Constants.cellCornerRadius + } + + // MARK: - Public + + public func displayLocation(_ location: CLLocationCoordinate2D, + userIdentifier: String, + userDisplayName: String, + userAvatarURLString: String?, + mediaManager: MXMediaManager) { + + annotationView = LocationUserMarkerView.loadFromNib() + + annotationView?.setAvatarData(AvatarViewData(matrixItemId: userIdentifier, + displayName: userDisplayName, + avatarUrl: userAvatarURLString, + mediaManager: mediaManager, + fallbackImage: .matrixItem(userIdentifier, userDisplayName))) + + 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) + } + + // MARK: - Themable + + func update(theme: Theme) { + descriptionLabel.textColor = theme.colors.primaryContent + descriptionLabel.font = theme.fonts.footnote + layer.borderColor = theme.colors.quinaryContent.cgColor + } + + // MARK: - MGLMapViewDelegate + + func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { + return annotationView + } +} diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib new file mode 100644 index 000000000..8beacacbe --- /dev/null +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m index d1888c6d7..687587dfe 100644 --- a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m +++ b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m @@ -104,6 +104,8 @@ @property(nonatomic, strong) UserVerificationCoordinatorBridgePresenter *userVerificationCoordinatorBridgePresenter; +@property(nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation RoomMemberDetailsViewController @@ -139,6 +141,8 @@ // Keep visible the status bar by default. isStatusBarHidden = NO; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenUser]; } - (void)viewDidLoad @@ -239,9 +243,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"RoomMemberDetails"]; - [self userInterfaceThemeDidChange]; // Hide the bottom border of the navigation bar to display the expander header @@ -264,6 +265,18 @@ self.bottomImageView.hidden = YES; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id )coordinator { [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; diff --git a/Riot/Modules/Room/Members/RoomParticipantsViewController.h b/Riot/Modules/Room/Members/RoomParticipantsViewController.h index 757bb2fca..e3bb56543 100644 --- a/Riot/Modules/Room/Members/RoomParticipantsViewController.h +++ b/Riot/Modules/Room/Members/RoomParticipantsViewController.h @@ -92,6 +92,11 @@ */ @property (nonatomic, weak) id delegate; +/** + The screen timer used for analytics if they've been enabled. The default value is nil. + */ +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + /** Returns the `UINib` object initialized for a `RoomParticipantsViewController`. diff --git a/Riot/Modules/Room/Members/RoomParticipantsViewController.m b/Riot/Modules/Room/Members/RoomParticipantsViewController.m index 1776b8775..5df03b91e 100644 --- a/Riot/Modules/Room/Members/RoomParticipantsViewController.m +++ b/Riot/Modules/Room/Members/RoomParticipantsViewController.m @@ -245,9 +245,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"RoomParticipants"]; // Refresh display [self refreshTableView]; @@ -268,6 +265,8 @@ [contactsPickerViewController destroy]; contactsPickerViewController = nil; } + + [self.screenTimer start]; } - (void)viewWillDisappear:(BOOL)animated @@ -284,6 +283,12 @@ [self searchBarCancelButtonClicked:_searchBarView]; } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + - (void)withdrawViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion { // Check whether the current view controller is displayed inside a segmented view controller in order to withdraw the right item diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index 7f860168f..fec16dd93 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -30,8 +30,6 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { private let activityIndicatorPresenter: ActivityIndicatorPresenterType private var selectedEventId: String? - private var pollEditFormCoordinator: PollEditFormCoordinator? - private var roomDataSourceManager: MXKRoomDataSourceManager { return MXKRoomDataSourceManager.sharedManager(forMatrixSession: self.parameters.session) } @@ -198,6 +196,56 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { completion?() } + + private func startLocationCoordinatorWithEvent(_ event: MXEvent? = nil, bubbleData: MXKRoomBubbleCellDataStoring? = nil) { + guard #available(iOS 14.0, *) else { + return + } + + guard let navigationRouter = self.navigationRouter, + let mediaManager = mxSession?.mediaManager, + let user = mxSession?.myUser else { + MXLog.error("[RoomCoordinator] Invalid location sharing coordinator parameters. Returning.") + return + } + + var avatarData: AvatarInputProtocol + if event != nil, let bubbleData = bubbleData { + avatarData = AvatarInput(mxContentUri: bubbleData.senderAvatarUrl, + matrixItemId: bubbleData.senderId, + displayName: bubbleData.senderDisplayName) + } else { + avatarData = AvatarInput(mxContentUri: user.avatarUrl, + matrixItemId: user.userId, + displayName: user.displayname) + } + + var location: CLLocationCoordinate2D? + if let locationContent = event?.location { + location = CLLocationCoordinate2D(latitude: locationContent.latitude, longitude: locationContent.longitude) + } + + let parameters = LocationSharingCoordinatorParameters(roomDataSource: roomViewController.roomDataSource, + mediaManager: mediaManager, + avatarData: avatarData, + location: location) + + let coordinator = LocationSharingCoordinator(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() + } } // MARK: - RoomIdentifiable @@ -261,10 +309,30 @@ extension RoomCoordinator: RoomViewControllerDelegate { return } - let parameters = PollEditFormCoordinatorParameters(navigationRouter: self.navigationRouter, room: roomViewController.roomDataSource.room) - pollEditFormCoordinator = PollEditFormCoordinator(parameters: parameters) + let parameters = PollEditFormCoordinatorParameters(room: roomViewController.roomDataSource.room) + let coordinator = PollEditFormCoordinator(parameters: parameters) - pollEditFormCoordinator?.start() + 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() + } + + func roomViewControllerDidRequestLocationSharingFormPresentation(_ roomViewController: RoomViewController) { + startLocationCoordinatorWithEvent() + } + + func roomViewController(_ roomViewController: RoomViewController, didRequestLocationPresentationFor event: MXEvent, bubbleData: MXKRoomBubbleCellDataStoring) { + startLocationCoordinatorWithEvent(event, bubbleData: bubbleData) } func roomViewController(_ roomViewController: RoomViewController, canEndPollWithEventIdentifier eventIdentifier: String) -> Bool { diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift index ccfe76a7e..4eaf2328d 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift @@ -39,9 +39,11 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { participants.enableMention = true participants.mxRoom = self.room participants.delegate = self + participants.screenTimer = AnalyticsScreenTimer(screen: .roomMembers) let files = RoomFilesViewController() files.finalizeInit() + files.screenTimer = AnalyticsScreenTimer(screen: .roomUploads) MXKRoomDataSource.load(withRoomId: self.room.roomId, andMatrixSession: self.session) { (dataSource) in guard let dataSource = dataSource as? MXKRoomDataSource else { return } dataSource.filterMessagesWithURL = true @@ -52,6 +54,7 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType { let settings = RoomSettingsViewController() settings.finalizeInit() + settings.screenTimer = AnalyticsScreenTimer(screen: .roomSettings) settings.initWith(self.session, andRoomId: self.room.roomId) if self.room.isDirect { diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift index 3590b5526..78b5af425 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift @@ -40,6 +40,7 @@ final class RoomInfoListViewController: UIViewController { private var errorPresenter: MXKErrorPresentation! private var activityPresenter: ActivityIndicatorPresenter! private var isRoomDirect: Bool = false + private var screenTimer = AnalyticsScreenTimer(screen: .roomDetails) private lazy var closeButton: CloseButton = { let button = CloseButton() @@ -128,12 +129,22 @@ final class RoomInfoListViewController: UIViewController { return self.theme.statusBarStyle } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + screenTimer.start() + } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() mainTableView.vc_relayoutHeaderView() } + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + screenTimer.stop() + } + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { coordinator.animate(alongsideTransition: {_ in self.basicInfoView.updateTrimmingOnTopic() diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 74941c31e..f3b9f6fa6 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -183,6 +183,24 @@ handleUniversalLinkWithParameters:(UniversalLinkParameters*)parameters; */ - (void)roomViewControllerDidRequestPollCreationFormPresentation:(RoomViewController *)roomViewController; +/** + Ask the coordinator to invoke the location sharing form coordinator. + + @param roomViewController the `RoomViewController` instance. + */ +- (void)roomViewControllerDidRequestLocationSharingFormPresentation:(RoomViewController *)roomViewController; + +/** + Ask the coordinator to invoke the location sharing form coordinator. + + @param roomViewController the `RoomViewController` instance. + @param event the event containing location information + @param bubbleData the bubble data containing sender details + */ +- (void)roomViewController:(RoomViewController *)roomViewController +didRequestLocationPresentationForEvent:(MXEvent *)event + bubbleData:(id)bubbleData; + - (BOOL)roomViewController:(RoomViewController *)roomViewController canEndPollWithEventIdentifier:(NSString *)eventIdentifier; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 46c83de52..10af1f3bc 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -256,6 +256,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator; @property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView; +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation RoomViewController @@ -338,6 +340,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; _voiceMessageController = [[VoiceMessageController alloc] initWithThemeService:ThemeService.shared mediaServiceProvider:VoiceMessageMediaServiceProvider.sharedProvider]; self.voiceMessageController.delegate = self; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenRoom]; } - (void)viewDidLoad @@ -418,6 +422,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self.bubblesTableView registerClass:PollBubbleCell.class forCellReuseIdentifier:PollBubbleCell.defaultReuseIdentifier]; [self.bubblesTableView registerClass:PollWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:PollWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; [self.bubblesTableView registerClass:PollWithPaginationTitleBubbleCell.class forCellReuseIdentifier:PollWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + + [self.bubblesTableView registerClass:LocationBubbleCell.class forCellReuseIdentifier:LocationBubbleCell.defaultReuseIdentifier]; + [self.bubblesTableView registerClass:LocationWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:LocationWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [self.bubblesTableView registerClass:LocationWithPaginationTitleBubbleCell.class forCellReuseIdentifier:LocationWithPaginationTitleBubbleCell.defaultReuseIdentifier]; [self vc_removeBackTitle]; @@ -567,9 +575,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"ChatRoom"]; - // Refresh the room title view [self refreshRoomTitle]; @@ -610,8 +615,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self.roomDataSource reload]; [LegacyAppDelegate theDelegate].lastNavigatedRoomIdFromPush = nil; - notificationTaskProfile = [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:AnalyticsNoficationsTimeToDisplayContent - category:AnalyticsNoficationsCategory]; + notificationTaskProfile = [MXSDKOptions.sharedInstance.profiler startMeasuringTaskWithName:MXTaskProfileNameNotificationsOpenEvent]; } } @@ -707,6 +711,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; hasJitsiCall = NO; [self reloadBubblesTable:YES]; } + + // Screen tracking + [self.screenTimer start]; } - (void)viewDidDisappear:(BOOL)animated @@ -742,6 +749,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; hasJitsiCall = YES; [self reloadBubblesTable:YES]; } + + [self.screenTimer stop]; } - (void)viewDidLayoutSubviews @@ -2027,6 +2036,16 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self.delegate roomViewControllerDidRequestPollCreationFormPresentation:self]; }]]; } + if (RiotSettings.shared.roomScreenAllowLocationAction) + { + [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_location"] andAction:^{ + MXStrongifyAndReturnIfNil(self); + if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) { + ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO; + } + [self.delegate roomViewControllerDidRequestLocationSharingFormPresentation:self]; + }]]; + } if (RiotSettings.shared.roomScreenAllowCameraAction) { [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_camera"] andAction:^{ @@ -2726,6 +2745,21 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; cellViewClass = PollBubbleCell.class; } } + else if (bubbleData.tag == RoomBubbleCellDataTagLocation) + { + if (bubbleData.isPaginationFirstBubble) + { + cellViewClass = LocationWithPaginationTitleBubbleCell.class; + } + else if (bubbleData.shouldHideSenderInformation) + { + cellViewClass = LocationWithoutSenderInfoBubbleCell.class; + } + else + { + cellViewClass = LocationBubbleCell.class; + } + } else if (bubbleData.isIncoming) { if (bubbleData.isAttachmentWithThumbnail) @@ -2926,7 +2960,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } else { - [self showContextualMenuForEvent:tappedEvent fromSingleTapGesture:YES cell:cell animated:YES]; + if (tappedEvent.location) { + [_delegate roomViewController:self didRequestLocationPresentationForEvent:tappedEvent bubbleData:bubbleData]; + } else { + [self showContextualMenuForEvent:tappedEvent fromSingleTapGesture:YES cell:cell animated:YES]; + } } } } @@ -3243,7 +3281,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } - if (selectedEvent.sentState == MXEventSentStateSent && selectedEvent.eventType != MXEventTypePollStart) + if (selectedEvent.sentState == MXEventSentStateSent && + selectedEvent.eventType != MXEventTypePollStart && + !selectedEvent.location) { [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] style:UIAlertActionStyleDefault @@ -6096,7 +6136,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; switch (event.eventType) { case MXEventTypeRoomMessage: { - NSString *messageType = event.content[@"msgtype"]; + NSString *messageType = event.content[kMXMessageTypeKey]; if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { diff --git a/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m b/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m index 0e14b53ee..c063cc7b9 100644 --- a/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m +++ b/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m @@ -109,9 +109,6 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"RoomFilesSearch"]; // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { diff --git a/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m b/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m index 4e93f01d6..6848a1665 100644 --- a/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m +++ b/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m @@ -111,9 +111,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"RoomMessagesSearch"]; - // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { diff --git a/Riot/Modules/Room/Search/RoomSearchViewController.m b/Riot/Modules/Room/Search/RoomSearchViewController.m index 2a0983263..e6f576fe8 100644 --- a/Riot/Modules/Room/Search/RoomSearchViewController.m +++ b/Riot/Modules/Room/Search/RoomSearchViewController.m @@ -33,6 +33,8 @@ MXKSearchDataSource *filesSearchDataSource; } +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation RoomSearchViewController @@ -49,6 +51,8 @@ [super finalizeInit]; // The navigation bar tint color and the rageShake Manager are handled by super (see SegmentedViewController). + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenRoomSearch]; } - (void)viewDidLoad @@ -106,9 +110,6 @@ [self.activityIndicator stopAnimating]; self.activityIndicator = nil; } - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"RoomsSearch"]; // Enable the search field by default at the screen opening if (self.searchBarHidden) @@ -124,6 +125,8 @@ // Refresh the search results. // Note: We wait for 'viewDidAppear' call to consider the actual view size during this update. [self updateSearch]; + + [self.screenTimer start]; } - (void)viewWillDisappear:(BOOL)animated @@ -138,6 +141,12 @@ [super viewWillDisappear:animated]; } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + - (UIStatusBarStyle)preferredStatusBarStyle { return ThemeService.shared.theme.statusBarStyle; diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.h b/Riot/Modules/Room/Settings/RoomSettingsViewController.h index dd960e8a6..1882a38cb 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.h +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.h @@ -19,6 +19,8 @@ #import "MediaPickerViewController.h" #import "TableViewCellWithCheckBoxes.h" +@class AnalyticsScreenTimer; + /** List the settings fields. Used to preselect/edit a field */ @@ -52,5 +54,10 @@ typedef enum : NSUInteger { */ @property (nonatomic) RoomSettingsViewControllerField selectedRoomSettingsField; +/** + The screen timer used for analytics if they've been enabled. The default value is nil. + */ +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.m b/Riot/Modules/Room/Settings/RoomSettingsViewController.m index 7ca017b57..47c14e013 100644 --- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m +++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m @@ -311,9 +311,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"RoomSettings"]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didUpdateRules:) name:kMXNotificationCenterDidUpdateRules object:nil]; @@ -334,6 +331,8 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti { self.selectedRoomSettingsField = _selectedRoomSettingsField; } + + [self.screenTimer start]; } - (void)viewWillDisappear:(BOOL)animated @@ -351,6 +350,12 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti } } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + // Those methods are called when the viewcontroller is added or removed from a container view controller. - (void)willMoveToParentViewController:(nullable UIViewController *)parent { diff --git a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib index 56c26c56c..adbd1b79a 100644 --- a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib +++ b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib @@ -140,14 +140,14 @@ - + - + diff --git a/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.swift new file mode 100644 index 000000000..2795d2647 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Location/LocationBubbleCell.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 + +class LocationBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable { + + private var locationView: RoomTimelineLocationView! + + 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 + else { + return + } + + locationView.update(theme: ThemeService.shared().theme) + locationView.locationDescription = locationContent.locationDescription + + let location = CLLocationCoordinate2D(latitude: locationContent.latitude, longitude: locationContent.longitude) + + locationView.displayLocation(location, + userIdentifier: bubbleData.senderId, + userDisplayName: bubbleData.senderDisplayName, + userAvatarURLString: bubbleData.senderAvatarUrl, + mediaManager: bubbleData.mxSession.mediaManager) + } + + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.backgroundColor = .clear + bubbleCellContentView?.showSenderInfo = true + bubbleCellContentView?.showPaginationTitle = false + + guard #available(iOS 14.0, *), + let contentView = bubbleCellContentView?.innerContentView else { + return + } + + locationView = RoomTimelineLocationView.loadFromNib() + + contentView.vc_addSubViewMatchingParent(locationView) + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithPaginationTitleBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithPaginationTitleBubbleCell.swift new file mode 100644 index 000000000..e3f6dd84f --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithPaginationTitleBubbleCell.swift @@ -0,0 +1,25 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class LocationWithPaginationTitleBubbleCell: LocationBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showPaginationTitle = true + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithoutSenderInfoBubbleCell.swift new file mode 100644 index 000000000..1329221dd --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Location/LocationWithoutSenderInfoBubbleCell.swift @@ -0,0 +1,25 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class LocationWithoutSenderInfoBubbleCell: LocationBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showSenderInfo = false + } +} diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift index 0c521b1b8..9b3f85cec 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift @@ -35,7 +35,7 @@ struct VoiceMessageAudioConverter { static func mediaDurationAt(_ sourceURL: URL, completion: @escaping (Result) -> Void) { FFprobeKit.getMediaInformationAsync(sourceURL.path) { session in - guard let session = session as? MediaInformationSession else { + guard let session = session else { completion(.failure(.generic("Invalid session"))) return } @@ -46,14 +46,14 @@ struct VoiceMessageAudioConverter { } DispatchQueue.main.async { - if returnCode.isSuccess() { + if returnCode.isValueSuccess() { let mediaInfo = session.getMediaInformation() if let duration = try? TimeInterval(value: mediaInfo?.getDuration() ?? "0") { completion(.success(duration)) } else { completion(.failure(.generic("Failed to get media duration"))) } - } else if returnCode.isCancel() { + } else if returnCode.isValueCancel() { completion(.failure(.cancelled)) } else { completion(.failure(.generic(String(returnCode.getValue())))) @@ -82,9 +82,9 @@ struct VoiceMessageAudioConverter { } DispatchQueue.main.async { - if returnCode.isSuccess() { + if returnCode.isValueSuccess() { completion(.success(())) - } else if returnCode.isCancel() { + } else if returnCode.isValueCancel() { completion(.failure(.cancelled)) } else { completion(.failure(.generic(String(returnCode.getValue())))) diff --git a/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m b/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m index d04214673..5064dcb15 100644 --- a/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m +++ b/Riot/Modules/Rooms/DirectoryPicker/DirectoryServerPickerViewController.m @@ -38,6 +38,9 @@ // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. id kThemeServiceDidChangeThemeNotificationObserver; } + +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation DirectoryServerPickerViewController @@ -49,6 +52,8 @@ // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSwitchDirectory]; } - (void)destroy @@ -145,9 +150,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"DirectoryServerPicker"]; - // Observe kAppDelegateDidTapStatusBarNotificationObserver. kAppDelegateDidTapStatusBarNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kAppDelegateDidTapStatusBarNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -158,6 +160,12 @@ [dataSource loadData]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + - (void)viewWillDisappear:(BOOL)animated { if (kAppDelegateDidTapStatusBarNotificationObserver) @@ -169,6 +177,12 @@ [super viewWillDisappear:animated]; } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + - (void)displayWithDataSource:(MXKDirectoryServersDataSource*)theDataSource onComplete:(void (^)(id cellData))onComplete; { diff --git a/Riot/Modules/Rooms/RoomsViewController.m b/Riot/Modules/Rooms/RoomsViewController.m index 25ae25ce9..183db0046 100644 --- a/Riot/Modules/Rooms/RoomsViewController.m +++ b/Riot/Modules/Rooms/RoomsViewController.m @@ -40,7 +40,7 @@ { [super finalizeInit]; - self.screenName = @"Rooms"; + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenRooms]; } - (void)viewDidLoad diff --git a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryCoordinator.swift b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryCoordinator.swift index 75a8365a7..6ab5e2517 100644 --- a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryCoordinator.swift +++ b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryCoordinator.swift @@ -63,6 +63,7 @@ final class ShowDirectoryCoordinator: ShowDirectoryCoordinatorType { private func createDirectoryServerPickerViewController() -> DirectoryServerPickerViewController { let controller = DirectoryServerPickerViewController() + controller.finalizeInit() let dataSource: MXKDirectoryServersDataSource = MXKDirectoryServersDataSource(matrixSession: session) dataSource.finalizeInitialization() dataSource.roomDirectoryServers = BuildSettings.publicRoomsDirectoryServers diff --git a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewController.swift b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewController.swift index 1e2cf5daf..ce22a3440 100644 --- a/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewController.swift +++ b/Riot/Modules/Rooms/ShowDirectory/ShowDirectoryViewController.swift @@ -68,6 +68,8 @@ final class ShowDirectoryViewController: UIViewController { }() private var sections: [ShowDirectorySection] = [] + + private let screenTimer = AnalyticsScreenTimer(screen: .roomDirectory) // MARK: - Setup @@ -104,10 +106,17 @@ final class ShowDirectoryViewController: UIViewController { self.keyboardAvoider?.startAvoiding() } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + screenTimer.start() + } + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) self.keyboardAvoider?.stopAvoiding() + + screenTimer.stop() } override var preferredStatusBarStyle: UIStatusBarStyle { diff --git a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift index 4b05e230c..b4c04c610 100644 --- a/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift +++ b/Riot/Modules/ServiceTerms/Modal/ServiceTermsModalCoordinator.swift @@ -107,7 +107,7 @@ extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelega func serviceTermsModalScreenCoordinatorDidAccept(_ coordinator: ServiceTermsModalScreenCoordinatorType) { if serviceTerms.serviceType == MXServiceTypeIdentityService { - Analytics.sharedInstance().trackValue(1, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted) + Analytics.shared.trackIdentityServerAccepted(true) } self.delegate?.serviceTermsModalCoordinatorDidAccept(self) @@ -119,7 +119,7 @@ extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelega func serviceTermsModalScreenCoordinatorDidDecline(_ coordinator: ServiceTermsModalScreenCoordinatorType) { if serviceTerms.serviceType == MXServiceTypeIdentityService { - Analytics.sharedInstance().trackValue(1, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted) + Analytics.shared.trackIdentityServerAccepted(false) disableIdentityServer() } @@ -131,7 +131,7 @@ extension ServiceTermsModalCoordinator: ServiceTermsModalScreenCoordinatorDelega extension ServiceTermsModalCoordinator: UIAdaptivePresentationControllerDelegate { func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { if serviceTerms.serviceType == MXServiceTypeIdentityService { - Analytics.sharedInstance().trackValue(0, category: MXKAnalyticsCategory.contacts.rawValue, name: AnalyticsContactsIdentityServerAccepted) + Analytics.shared.trackIdentityServerAccepted(false) } self.delegate?.serviceTermsModalCoordinatorDidDismissInteractively(self) diff --git a/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m b/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m index 3e4b4297e..0efbe7bdc 100644 --- a/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m +++ b/Riot/Modules/Settings/DeactivateAccount/DeactivateAccountViewController.m @@ -47,6 +47,8 @@ static CGFloat const kTextFontSize = 15.0; @property (weak, nonatomic) id themeDidChangeNotificationObserver; +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end #pragma mark - Implementation @@ -62,6 +64,12 @@ static CGFloat const kTextFontSize = 15.0; return viewController; } +- (void)finalizeInit +{ + [super finalizeInit]; + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenDeactivateAccount]; +} + - (void)destroy { id notificationObserver = self.themeDidChangeNotificationObserver; @@ -95,9 +103,12 @@ static CGFloat const kTextFontSize = 15.0; [super viewWillAppear:animated]; [self userInterfaceThemeDidChange]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"DeactivateAccount"]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; } - (void)viewDidLayoutSubviews @@ -107,6 +118,12 @@ static CGFloat const kTextFontSize = 15.0; [self.deactivateAcccountButton.layer setCornerRadius:kButtonCornerRadius]; } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + - (UIStatusBarStyle)preferredStatusBarStyle { return ThemeService.shared.theme.statusBarStyle; diff --git a/Riot/Modules/Settings/Language/LanguagePickerViewController.m b/Riot/Modules/Settings/Language/LanguagePickerViewController.m index aac54c076..7c4c69055 100644 --- a/Riot/Modules/Settings/Language/LanguagePickerViewController.m +++ b/Riot/Modules/Settings/Language/LanguagePickerViewController.m @@ -106,14 +106,6 @@ } } -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"CountryPicker"]; -} - - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath; { cell.textLabel.textColor = ThemeService.shared.theme.textPrimaryColor; diff --git a/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m b/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m index 0ab2fe0d8..bdbc14ee1 100644 --- a/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m +++ b/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m @@ -95,14 +95,6 @@ return ThemeService.shared.theme.statusBarStyle; } -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"CountryPicker"]; -} - - (void)destroy { [super destroy]; diff --git a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m index b9b9ac474..42920094f 100644 --- a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m +++ b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m @@ -161,9 +161,6 @@ enum { { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"ManageSession"]; - // Release the potential pushed view controller [self releasePushedViewController]; diff --git a/Riot/Modules/Settings/Security/SecurityViewController.m b/Riot/Modules/Settings/Security/SecurityViewController.m index 5c6d93aab..99b7f7c93 100644 --- a/Riot/Modules/Settings/Security/SecurityViewController.m +++ b/Riot/Modules/Settings/Security/SecurityViewController.m @@ -119,6 +119,8 @@ TableViewSectionsDelegate> @property (nonatomic, strong) SetPinCoordinatorBridgePresenter *setPinCoordinatorBridgePresenter; @property (nonatomic, strong) CrossSigningSetupCoordinatorBridgePresenter *crossSigningSetupCoordinatorBridgePresenter; +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation SecurityViewController @@ -142,6 +144,8 @@ TableViewSectionsDelegate> // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSettingsSecurity]; } - (void)viewDidLoad @@ -250,9 +254,6 @@ TableViewSectionsDelegate> { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"Security"]; - // Release the potential pushed view controller [self releasePushedViewController]; @@ -268,6 +269,12 @@ TableViewSectionsDelegate> [self loadCrossSigning]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [self.screenTimer start]; +} + - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; @@ -279,6 +286,12 @@ TableViewSectionsDelegate> } } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + #pragma mark - Internal methods - (void)updateSections diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 1993ab9f7..2970c51f7 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -47,10 +47,11 @@ NSString* const kSettingsViewControllerPhoneBookCountryCellId = @"kSettingsViewControllerPhoneBookCountryCellId"; -enum +typedef NS_ENUM(NSUInteger, SECTION_TAG) { SECTION_TAG_SIGN_OUT = 0, SECTION_TAG_USER_SETTINGS, + SECTION_TAG_LOCATION_SHARING, SECTION_TAG_SENDING_MEDIA, SECTION_TAG_LINKS, SECTION_TAG_SECURITY, @@ -69,7 +70,7 @@ enum SECTION_TAG_DEACTIVATE_ACCOUNT }; -enum +typedef NS_ENUM(NSUInteger, USER_SETTINGS_INDEX) { USER_SETTINGS_PROFILE_PICTURE_INDEX = 0, USER_SETTINGS_DISPLAYNAME_INDEX, @@ -80,24 +81,29 @@ enum USER_SETTINGS_ADD_PHONENUMBER_INDEX }; -enum +typedef NS_ENUM(NSUInteger, USER_SETTINGS_OFFSET) { USER_SETTINGS_EMAILS_OFFSET = 2000, USER_SETTINGS_PHONENUMBERS_OFFSET = 1000 }; -enum +typedef NS_ENUM(NSUInteger, LOCATION_SHARING) +{ + LOCATION_SHARING_ENABLED +}; + +typedef NS_ENUM(NSUInteger, SENDING_MEDIA) { SENDING_MEDIA_CONFIRM_SIZE = 0 }; -enum +typedef NS_ENUM(NSUInteger, LINKS_SHOW_URL_PREVIEWS) { LINKS_SHOW_URL_PREVIEWS_INDEX = 0, LINKS_SHOW_URL_PREVIEWS_DESCRIPTION_INDEX }; -enum +typedef NS_ENUM(NSUInteger, NOTIFICATION_SETTINGS) { NOTIFICATION_SETTINGS_ENABLE_PUSH_INDEX = 0, NOTIFICATION_SETTINGS_SYSTEM_SETTINGS, @@ -109,33 +115,34 @@ enum NOTIFICATION_SETTINGS_OTHER_SETTINGS_INDEX, }; -enum +typedef NS_ENUM(NSUInteger, CALLS_ENABLE_STUN_SERVER) { CALLS_ENABLE_STUN_SERVER_FALLBACK_INDEX = 0 }; -enum +typedef NS_ENUM(NSUInteger, INTEGRATIONS) { INTEGRATIONS_INDEX }; -enum { +typedef NS_ENUM(NSUInteger, LOCAL_CONTACTS) +{ LOCAL_CONTACTS_SYNC_INDEX, LOCAL_CONTACTS_PHONEBOOK_COUNTRY_INDEX }; -enum +typedef NS_ENUM(NSUInteger, USER_INTERFACE) { USER_INTERFACE_LANGUAGE_INDEX = 0, USER_INTERFACE_THEME_INDEX }; -enum +typedef NS_ENUM(NSUInteger, IDENTITY_SERVER) { IDENTITY_SERVER_INDEX }; -enum +typedef NS_ENUM(NSUInteger, ADVANCED) { ADVANCED_SHOW_NSFW_ROOMS_INDEX = 0, ADVANCED_CRASH_REPORT_INDEX, @@ -145,7 +152,7 @@ enum ADVANCED_REPORT_BUG_INDEX, }; -enum +typedef NS_ENUM(NSUInteger, ABOUT) { ABOUT_COPYRIGHT_INDEX = 0, ABOUT_TERM_CONDITIONS_INDEX, @@ -159,7 +166,7 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE) LABS_ENABLE_POLLS }; -enum +typedef NS_ENUM(NSUInteger, SECURITY) { SECURITY_BUTTON_INDEX = 0, }; @@ -283,6 +290,8 @@ TableViewSectionsDelegate> @property (nonatomic) BOOL isPreparingIdentityService; @property (nonatomic, strong) ServiceTermsModalCoordinatorBridgePresenter *serviceTermsModalCoordinatorBridgePresenter; +@property (nonatomic) AnalyticsScreenTimer *screenTimer; + @end @implementation SettingsViewController @@ -315,6 +324,8 @@ TableViewSectionsDelegate> isSavingInProgress = NO; isResetPwdInProgress = NO; is3PIDBindingInProgress = NO; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenSettings]; } - (void)updateSections @@ -370,6 +381,14 @@ TableViewSectionsDelegate> sectionUserSettings.headerTitle = [VectorL10n settingsUserSettings]; [tmpSections addObject:sectionUserSettings]; + if (BuildSettings.locationSharingEnabled) + { + Section *sectionLocationSharing = [Section sectionWithTag:SECTION_TAG_LOCATION_SHARING]; + [sectionLocationSharing addRowWithTag:LOCATION_SHARING_ENABLED]; + sectionLocationSharing.headerTitle = VectorL10n.locationSharingSettingsHeader.uppercaseString; + [tmpSections addObject:sectionLocationSharing]; + } + if (BuildSettings.settingsScreenShowConfirmMediaSize) { Section *sectionMedia = [Section sectionWithTag:SECTION_TAG_SENDING_MEDIA]; @@ -776,9 +795,6 @@ TableViewSectionsDelegate> - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"Settings"]; // Refresh display [self refreshSettings]; @@ -808,6 +824,8 @@ TableViewSectionsDelegate> [self releasePushedViewController]; [self.settingsDiscoveryTableViewSection reload]; + + [self.screenTimer start]; } - (void)viewWillDisappear:(BOOL)animated @@ -851,6 +869,12 @@ TableViewSectionsDelegate> } } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + [self.screenTimer stop]; +} + #pragma mark - Internal methods - (void)pushViewController:(UIViewController*)viewController @@ -1933,6 +1957,21 @@ TableViewSectionsDelegate> cell = passwordCell; } } + else if (section == SECTION_TAG_LOCATION_SHARING) + { + if (row == LOCATION_SHARING_ENABLED) + { + MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = VectorL10n.locationSharingSettingsToggleTitle; + labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.roomScreenAllowLocationAction; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + labelAndSwitchCell.mxkSwitch.enabled = YES; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleLocationSharing:) forControlEvents:UIControlEventTouchUpInside]; + + cell = labelAndSwitchCell; + } + } else if (section == SECTION_TAG_SENDING_MEDIA) { if (row == SENDING_MEDIA_CONFIRM_SIZE) @@ -2251,11 +2290,11 @@ TableViewSectionsDelegate> { MXKTableViewCellWithLabelAndSwitch* sendCrashReportCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; - sendCrashReportCell.mxkLabel.text = [VectorL10n settingsSendCrashReport]; - sendCrashReportCell.mxkSwitch.on = RiotSettings.shared.enableCrashReport; + sendCrashReportCell.mxkLabel.text = VectorL10n.settingsAnalyticsAndCrashData; + sendCrashReportCell.mxkSwitch.on = RiotSettings.shared.enableAnalytics; sendCrashReportCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; sendCrashReportCell.mxkSwitch.enabled = YES; - [sendCrashReportCell.mxkSwitch addTarget:self action:@selector(toggleSendCrashReport:) forControlEvents:UIControlEventTouchUpInside]; + [sendCrashReportCell.mxkSwitch addTarget:self action:@selector(toggleAnalytics:) forControlEvents:UIControlEventTouchUpInside]; cell = sendCrashReportCell; } @@ -2955,6 +2994,11 @@ TableViewSectionsDelegate> } } +- (void)toggleLocationSharing:(UISwitch *)sender +{ + RiotSettings.shared.roomScreenAllowLocationAction = sender.on; +} + - (void)toggleConfirmMediaSize:(UISwitch *)sender { RiotSettings.shared.showMediaCompressionPrompt = sender.on; @@ -3115,27 +3159,20 @@ TableViewSectionsDelegate> [[MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession] reset]; } -- (void)toggleSendCrashReport:(id)sender +- (void)toggleAnalytics:(UISwitch *)sender { - BOOL enable = RiotSettings.shared.enableCrashReport; - if (enable) + if (sender.isOn) { - MXLogDebug(@"[SettingsViewController] disable automatic crash report and analytics sending"); - - RiotSettings.shared.enableCrashReport = NO; - - [[Analytics sharedInstance] stop]; - - // Remove potential crash file. - [MXLogger deleteCrashLog]; + MXLogDebug(@"[SettingsViewController] enable automatic crash report and analytics sending"); + [Analytics.shared optInWith:self.mainSession]; } else { - MXLogDebug(@"[SettingsViewController] enable automatic crash report and analytics sending"); + MXLogDebug(@"[SettingsViewController] disable automatic crash report and analytics sending"); + [Analytics.shared optOut]; - RiotSettings.shared.enableCrashReport = YES; - - [[Analytics sharedInstance] start]; + // Remove potential crash file. + [MXLogger deleteCrashLog]; } } diff --git a/Riot/Modules/SideMenu/SideMenuViewController.swift b/Riot/Modules/SideMenu/SideMenuViewController.swift index f7fecb60c..cec36f497 100644 --- a/Riot/Modules/SideMenu/SideMenuViewController.swift +++ b/Riot/Modules/SideMenu/SideMenuViewController.swift @@ -48,6 +48,7 @@ final class SideMenuViewController: UIViewController { private var keyboardAvoider: KeyboardAvoider? private var errorPresenter: MXKErrorPresentation! private var activityPresenter: ActivityIndicatorPresenter! + private var screenTimer = AnalyticsScreenTimer(screen: .sidebar) private var sideMenuActionViews: [SideMenuActionView] = [] private weak var sideMenuVersionView: SideMenuVersionView? @@ -86,8 +87,14 @@ final class SideMenuViewController: UIViewController { navigationController?.setNavigationBarHidden(true, animated: animated) } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + screenTimer.start() + } + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) + screenTimer.stop() } override var preferredStatusBarStyle: UIStatusBarStyle { diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift index d05076758..9356ee904 100644 --- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift +++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift @@ -154,7 +154,7 @@ final class SpaceMemberListViewController: RoomParticipantsViewController { // MARK: - Actions @objc private func onAddParticipantButtonPressed() { - self.errorPresenter.presentError(from: self, title: VectorL10n.spacesInvitesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail, animated: true, handler: nil) + self.errorPresenter.presentError(from: self, title: VectorL10n.spacesInvitesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil) } private func cancelButtonAction() { @@ -184,11 +184,11 @@ final class SpaceMemberListViewController: RoomParticipantsViewController { override func roomMemberDetailsViewController(_ roomMemberDetailsViewController: MXKRoomMemberDetailsViewController!, startChatWithMemberId matrixId: String!, completion: (() -> Void)!) { completion() - self.errorPresenter.presentError(from: self, title: VectorL10n.spacesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail, animated: true, handler: nil) + self.errorPresenter.presentError(from: self, title: VectorL10n.spacesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil) } override func roomMemberDetailsViewController(_ roomMemberDetailsViewController: MXKRoomMemberDetailsViewController!, placeVoipCallWithMemberId matrixId: String!, andVideo isVideoCall: Bool) { - self.errorPresenter.presentError(from: self, title: VectorL10n.spacesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail, animated: true, handler: nil) + self.errorPresenter.presentError(from: self, title: VectorL10n.spacesComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil) } } diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift index fc04d7473..5c13e08f8 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift @@ -225,7 +225,7 @@ final class SpaceExploreRoomViewController: UIViewController { } @objc private func addRoomAction(semder: UIView) { - self.errorPresenter.presentError(from: self, title: VectorL10n.spacesAddRoomsComingSoonTitle, message: VectorL10n.spacesComingSoonDetail, animated: true, handler: nil) + self.errorPresenter.presentError(from: self, title: VectorL10n.spacesAddRoomsComingSoonTitle, message: VectorL10n.spacesComingSoonDetail(AppInfo.current.displayName), animated: true, handler: nil) } // MARK: - UISearchBarDelegate diff --git a/Riot/Modules/StartChat/InviteFriendsHeaderView.swift b/Riot/Modules/StartChat/InviteFriendsHeaderView.swift index 30325e704..2c61559c9 100644 --- a/Riot/Modules/StartChat/InviteFriendsHeaderView.swift +++ b/Riot/Modules/StartChat/InviteFriendsHeaderView.swift @@ -50,7 +50,7 @@ final class InviteFriendsHeaderView: UIView, NibLoadable, Themable { override func awakeFromNib() { super.awakeFromNib() - button.setTitle(VectorL10n.inviteFriendsAction(BuildSettings.bundleDisplayName), for: .normal) + button.setTitle(VectorL10n.inviteFriendsAction(AppInfo.current.displayName), for: .normal) button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside) button.layer.cornerRadius = 8 button.layer.borderWidth = 2 diff --git a/Riot/Modules/StartChat/StartChatViewController.m b/Riot/Modules/StartChat/StartChatViewController.m index 18ef614dd..4f2d71dc1 100644 --- a/Riot/Modules/StartChat/StartChatViewController.m +++ b/Riot/Modules/StartChat/StartChatViewController.m @@ -73,8 +73,6 @@ { [super finalizeInit]; - self.screenName = @"StartChat"; - _isAddParticipantSearchBarEditing = NO; // Prepare room participants @@ -82,6 +80,8 @@ // Assign itself as delegate self.contactsTableViewControllerDelegate = self; + + self.screenTimer = [[AnalyticsScreenTimer alloc] initWithScreen:AnalyticsScreenStartChat]; } - (void)viewDidLoad diff --git a/Riot/Modules/TabBar/MasterTabBarController.h b/Riot/Modules/TabBar/MasterTabBarController.h index 170e912d1..b607c0ea9 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.h +++ b/Riot/Modules/TabBar/MasterTabBarController.h @@ -193,5 +193,6 @@ typedef NS_ENUM(NSUInteger, MasterTabBarIndex) { - (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectRoomPreviewWithParameters:(RoomPreviewNavigationParameters*)roomPreviewNavigationParameters completion:(void (^)(void))completion; - (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectContact:(MXKContact*)contact withPresentationParameters:(ScreenPresentationParameters*)presentationParameters; - (void)masterTabBarController:(MasterTabBarController *)masterTabBarController didSelectGroup:(MXGroup*)group inMatrixSession:(MXSession*)matrixSession presentationParameters:(ScreenPresentationParameters*)presentationParameters; +- (void)masterTabBarController:(MasterTabBarController *)masterTabBarController shouldPresentAnalyticsPromptForMatrixSession:(MXSession*)matrixSession; @end diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index 4a4751d83..6d4e175b8 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -70,6 +70,11 @@ @property(nonatomic) BOOL reviewSessionAlertHasBeenDisplayed; +/** + A flag to indicate that the analytics prompt should be shown during `-addMatrixSession:`. + */ +@property(nonatomic) BOOL presentAnalyticsPromptOnAddSession; + @end @implementation MasterTabBarController @@ -196,11 +201,18 @@ if (!authIsShown) { - // Check whether the user has been already prompted to send crash reports. - // (Check whether 'enableCrashReport' flag has been set once) - if (!RiotSettings.shared.isEnableCrashReportHasBeenSetOnce) + // Check whether the user should be prompted to send analytics. + if (Analytics.shared.shouldShowAnalyticsPrompt) { - [self promptUserBeforeUsingAnalytics]; + MXSession *mxSession = self.mxSessions.firstObject; + if (mxSession) + { + [self promptUserBeforeUsingAnalyticsForSession:mxSession]; + } + else + { + self.presentAnalyticsPromptOnAddSession = YES; + } } [self refreshTabBarBadges]; @@ -405,6 +417,12 @@ return; } + if (self.presentAnalyticsPromptOnAddSession) + { + self.presentAnalyticsPromptOnAddSession = NO; + [self promptUserBeforeUsingAnalyticsForSession:mxSession]; + } + // Check whether the controller'€™s view is loaded into memory. if (self.homeViewController) { @@ -921,50 +939,14 @@ #pragma mark - -- (void)promptUserBeforeUsingAnalytics +- (void)promptUserBeforeUsingAnalyticsForSession:(MXSession *)mxSession { - MXLogDebug(@"[MasterTabBarController]: Invite the user to send crash reports"); - - __weak typeof(self) weakSelf = self; - - [currentAlert dismissViewControllerAnimated:NO completion:nil]; - - NSString *appDisplayName = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"]; - - currentAlert = [UIAlertController alertControllerWithTitle:[VectorL10n googleAnalyticsUsePrompt:appDisplayName] message:nil preferredStyle:UIAlertControllerStyleAlert]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n no] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - RiotSettings.shared.enableCrashReport = NO; - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - } - - }]]; - - [currentAlert addAction:[UIAlertAction actionWithTitle:[MatrixKitL10n yes] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - - RiotSettings.shared.enableCrashReport = YES; - - if (weakSelf) - { - typeof(self) self = weakSelf; - self->currentAlert = nil; - } - - [[Analytics sharedInstance] start]; - - }]]; - - [currentAlert mxk_setAccessibilityIdentifier: @"HomeVCUseAnalyticsAlert"]; - [self presentViewController:currentAlert animated:YES completion:nil]; + // Analytics aren't collected on iOS 12 & 13. + if (@available(iOS 14.0, *)) + { + MXLogDebug(@"[MasterTabBarController]: Invite the user to send analytics"); + [self.masterTabBarDelegate masterTabBarController:self shouldPresentAnalyticsPromptForMatrixSession:mxSession]; + } } #pragma mark - Review session diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 63bf354a2..37342d973 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -487,6 +487,24 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { self.splitViewMasterPresentableDelegate?.splitViewMasterPresentableWantsToResetDetail(self) } + @available(iOS 14.0, *) + private func presentAnalyticsPrompt(with session: MXSession) { + let parameters = AnalyticsPromptCoordinatorParameters(session: session) + let coordinator = AnalyticsPromptCoordinator(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() + } + // MARK: UserSessions management private func registerUserSessionsServiceNotifications() { @@ -578,6 +596,12 @@ extension TabBarCoordinator: MasterTabBarControllerDelegate { self.masterTabBarController.navigationItem.leftBarButtonItem = sideMenuBarButtonItem } + + func masterTabBarController(_ masterTabBarController: MasterTabBarController!, shouldPresentAnalyticsPromptForMatrixSession matrixSession: MXSession!) { + if #available(iOS 14.0, *) { + presentAnalyticsPrompt(with: matrixSession) + } + } } // MARK: - RoomCoordinatorDelegate diff --git a/Riot/Modules/UserDevices/UsersDevicesViewController.m b/Riot/Modules/UserDevices/UsersDevicesViewController.m index 5f83b5bc5..308f0afcd 100644 --- a/Riot/Modules/UserDevices/UsersDevicesViewController.m +++ b/Riot/Modules/UserDevices/UsersDevicesViewController.m @@ -120,9 +120,6 @@ { [super viewWillAppear:animated]; - // Screen tracking - [[Analytics sharedInstance] trackScreen:@"UnknowDevices"]; - [self.tableView reloadData]; } diff --git a/Riot/SupportingFiles/Info.plist b/Riot/SupportingFiles/Info.plist index 5bcda43e6..9d95fe929 100644 --- a/Riot/SupportingFiles/Info.plist +++ b/Riot/SupportingFiles/Info.plist @@ -65,6 +65,8 @@ The photo library is used to send photos and videos. NSSiriUsageDescription Siri is used to perform calls even from the lock screen. + NSLocationWhenInUseUsageDescription + When you share your location to people, Element needs access to show them a map. UIBackgroundModes audio diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index a53ae6f03..c5a22e8a7 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -45,6 +45,7 @@ #import "RoomInputToolbarView.h" #import "NSArray+Element.h" #import "ShareItemSender.h" +#import "HTMLFormatter.h" // MatrixKit common imports, shared with all targets #import "MatrixKit-Bridging-Header.h" @@ -62,4 +63,3 @@ #import "MXKRoomDataSourceManager.h" #import "MXRoom+Sync.h" #import "UIAlertController+MatrixKit.h" -#import "MXKAnalyticsConstants.h" diff --git a/Riot/Utils/HTMLFormatter.h b/Riot/Utils/HTMLFormatter.h new file mode 100644 index 000000000..605ee9d08 --- /dev/null +++ b/Riot/Utils/HTMLFormatter.h @@ -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 +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface HTMLFormatter : NSObject + +/** Builds an attributed string from a string containing html. + @param htmlString The html string to use. + @param allowedTags The html tags that should be allowed. + @param fontSize The default font size to use. + + Note: It is recommended to include "p" and "body" tags in + `allowedTags` as these are often added when parsing. + */ +- (NSAttributedString * _Nonnull)formatHTML:(NSString * _Nonnull)htmlString + withAllowedTags:(NSArray * _Nonnull)allowedTags + fontSize:(CGFloat)fontSize; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Riot/Utils/HTMLFormatter.m b/Riot/Utils/HTMLFormatter.m new file mode 100644 index 000000000..260ab83f5 --- /dev/null +++ b/Riot/Utils/HTMLFormatter.m @@ -0,0 +1,61 @@ +// +// 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 "HTMLFormatter.h" +#import "GeneratedInterface-Swift.h" + +@implementation HTMLFormatter + +- (NSAttributedString *)formatHTML:(NSString *)htmlString withAllowedTags:(NSArray *)allowedTags fontSize:(CGFloat)fontSize +{ + // TODO: This method should be more general purpose and usable from MXKEventFormatter and GroupHomeViewController + // FIXME: The implementation is currently in Objective-C as there is a crash in the callback when implemented in Swift + UIFont *font = [UIFont systemFontOfSize:fontSize]; + + // Do some sanitisation before finalizing the string + DTHTMLAttributedStringBuilderWillFlushCallback sanitizeCallback = ^(DTHTMLElement *element) { + [element sanitizeWith:allowedTags bodyFont:font imageHandler:nil]; + }; + + NSDictionary *options = @{ + DTUseiOS6Attributes: @(YES), // Enable it to be able to display the attributed string in a UITextView + DTDefaultFontFamily: font.familyName, + DTDefaultFontName: font.fontName, + DTDefaultFontSize: @(font.pointSize), + DTDefaultLinkDecoration: @(NO), + DTWillFlushBlockCallBack: sanitizeCallback + }; + + // Do not use the default HTML renderer of NSAttributedString because this method + // runs on the UI thread which we want to avoid because renderHTMLString is called + // most of the time from a background thread. + // Use DTCoreText HTML renderer instead. + // Using DTCoreText, which renders static string, helps to avoid code injection attacks + // that could happen with the default HTML renderer of NSAttributedString which is a + // webview. + NSAttributedString *string = [[NSAttributedString alloc] initWithHTMLData:[htmlString dataUsingEncoding:NSUTF8StringEncoding] options:options documentAttributes:NULL]; + + // Apply additional treatments + string = [MXKTools removeDTCoreTextArtifacts:string]; + + if (!string) { + return [[NSAttributedString alloc] initWithString:htmlString]; + } + + return string; +} + +@end diff --git a/Riot/Utils/HTMLFormatter.swift b/Riot/Utils/HTMLFormatter.swift new file mode 100644 index 000000000..8319388e1 --- /dev/null +++ b/Riot/Utils/HTMLFormatter.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 Foundation + +extension HTMLFormatter { + /// Builds an attributed string by replacing a `%@` placeholder with the supplied link text and URL. + /// - Parameters: + /// - string: The string to be formatted. + /// - link: The link text to be inserted. + /// - url: The URL to be linked to. + /// - Returns: An attributed string. + func format(_ string: String, with link: String, using url: URL) -> NSAttributedString { + let baseString = NSMutableAttributedString(string: string) + let attributedLink = NSAttributedString(string: link, attributes: [.link: url]) + + let linkRange = (baseString.string as NSString).range(of: "%@") + baseString.replaceCharacters(in: linkRange, with: attributedLink) + + return baseString + } +} diff --git a/Riot/target.yml b/Riot/target.yml index aaac28a9b..81a912fbc 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -35,6 +35,7 @@ targets: - target: SiriIntents - target: RiotNSE - target: DesignKit + - package: Mapbox configFiles: Debug: Debug.xcconfig diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index 2dfaec7f3..c77a9dd86 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -385,8 +385,8 @@ class NotificationService: UNNotificationServiceExtension { } } - let msgType = event.content["msgtype"] as? String - let messageContent = event.content["body"] as? String + let msgType = event.content[kMXMessageTypeKey] as? String + let messageContent = event.content[kMXMessageBodyKey] as? String let isReply = event.isReply() if isReply { @@ -401,6 +401,11 @@ class NotificationService: UNNotificationServiceExtension { break } + if event.location != nil { + notificationBody = NSString.localizedUserNotificationString(forKey: "LOCATION_FROM_USER", arguments: [eventSenderName]) + break + } + switch msgType { case kMXMessageTypeEmote: notificationBody = NSString.localizedUserNotificationString(forKey: "ACTION_FROM_USER", arguments: [eventSenderName, messageContent as Any]) diff --git a/RiotSwiftUI/Common.xcconfig b/RiotSwiftUI/Common.xcconfig index 9c6902651..37b97f356 100644 --- a/RiotSwiftUI/Common.xcconfig +++ b/RiotSwiftUI/Common.xcconfig @@ -27,4 +27,6 @@ INFOPLIST_FILE = RiotSwiftUI/Info.plist SKIP_INSTALL = YES +SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h + SWIFT_OBJC_INTERFACE_HEADER_NAME = GeneratedInterface-Swift.h diff --git a/RiotSwiftUI/Info.plist b/RiotSwiftUI/Info.plist index 0a5393324..859c68836 100644 --- a/RiotSwiftUI/Info.plist +++ b/RiotSwiftUI/Info.plist @@ -20,5 +20,9 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) + CFBundleDisplayName + RiotSwiftUI + NSLocationWhenInUseUsageDescription + When you share your location to people, Element needs access to show them a map. diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift new file mode 100644 index 000000000..b9678fdbd --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift @@ -0,0 +1,128 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// The state is never modified so this is unnecessary. +enum AnalyticsPromptStateAction { } + +enum AnalyticsPromptViewAction { + /// Enable analytics. + case enable + /// Disable analytics. + case disable + /// Open the service terms link. + case openTermsURL +} + +enum AnalyticsPromptViewModelResult { + /// Enable analytics. + case enable + /// Disable analytics. + case disable +} + +struct AnalyticsPromptViewState: BindableState { + /// The type of prompt to display. + let promptType: AnalyticsPromptType + /// Localized attributed strings created in the coordinator. + let strings: AnalyticsPromptStringsProtocol +} + +/// A collection of strings for the UI that need to be created in +/// the coordinator or mocked in the RiotSwiftUI target. +protocol AnalyticsPromptStringsProtocol { + var appDisplayName: String { get } + + var point1: NSAttributedString { get } + var point2: NSAttributedString { get } + + var termsNewUser: NSAttributedString { get } + var termsUpgrade: NSAttributedString { get } +} + +enum AnalyticsPromptType { + case newUser(termsString: NSAttributedString) + case upgrade(termsString: NSAttributedString) +} + +extension AnalyticsPromptType { + /// The main description string that should be displayed. + var message: String { + switch self { + case .newUser: + return VectorL10n.analyticsPromptMessageNewUser(AppInfo.current.displayName) + case .upgrade: + return VectorL10n.analyticsPromptMessageUpgrade + } + } + + /// The terms string that should be displayed. + var termsStrings: NSAttributedString { + switch self { + case .newUser(let termsString), .upgrade(let termsString): + return termsString + } + } + + /// The title for the enable button. + var enableButtonTitle: String { + switch self { + case .newUser: + return VectorL10n.enable + case .upgrade: + return VectorL10n.analyticsPromptYes + } + } + + /// The title for the disable button. + var disableButtonTitle: String { + switch self { + case .newUser: + return VectorL10n.analyticsPromptNotNow + case .upgrade: + return VectorL10n.analyticsPromptStop + } + } +} + +extension AnalyticsPromptType: CaseIterable { + static var allCases: [AnalyticsPromptType] { + let strings = MockAnalyticsPromptStrings() + return [ + .newUser(termsString: strings.termsNewUser), + .upgrade(termsString: strings.termsUpgrade) + ] + } +} + +extension AnalyticsPromptType: Identifiable { + var id: String { + switch self { + case .newUser: + return "newUser" + case .upgrade: + return "upgrade" + } + } +} + +// For the RiotSwiftUI target presentation. +extension AnalyticsPromptType: CustomStringConvertible { + var description: String { id } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift new file mode 100644 index 000000000..999e2a95f --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift @@ -0,0 +1,78 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt +// +// 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 AnalyticsPromptViewModelType = StateStoreViewModel +@available(iOS 14, *) +class AnalyticsPromptViewModel: AnalyticsPromptViewModelType { + + // MARK: - Properties + + // MARK: Private + + let termsURL: URL + + // MARK: Public + + var completion: ((AnalyticsPromptViewModelResult) -> Void)? + + // MARK: - Setup + + /// Initialize a view model with the specified prompt type and app display name. + init(promptType: AnalyticsPromptType, strings: AnalyticsPromptStringsProtocol, termsURL: URL) { + self.termsURL = termsURL + super.init(initialViewState: AnalyticsPromptViewState(promptType: promptType, strings: strings)) + } + + // MARK: - Public + + override func process(viewAction: AnalyticsPromptViewAction) { + switch viewAction { + case .enable: + enable() + case .disable: + disable() + case .openTermsURL: + openTermsURL() + } + } + + override class func reducer(state: inout AnalyticsPromptViewState, action: AnalyticsPromptStateAction) { + // There is no mutable state to reduce :) + } + + /// Enable analytics. The call to the Analytics class is made in the completion. + private func enable() { + completion?(.enable) + } + + /// Disable analytics. The call to the Analytics class is made in the completion. + private func disable() { + completion?(.disable) + } + + /// Open the service terms link. + private func openTermsURL() { + UIApplication.shared.open(termsURL) + } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift new file mode 100644 index 000000000..c9e5f44b4 --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift @@ -0,0 +1,98 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt +/* + 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 AnalyticsPromptCoordinatorParameters { + /// The session to use if analytics are enabled. + let session: MXSession +} + +final class AnalyticsPromptCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: AnalyticsPromptCoordinatorParameters + private let analyticsPromptHostingController: UIViewController + private var _analyticsPromptViewModel: Any? = nil + + @available(iOS 14.0, *) + fileprivate var analyticsPromptViewModel: AnalyticsPromptViewModel { + return _analyticsPromptViewModel as! AnalyticsPromptViewModel + } + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: (() -> Void)? + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: AnalyticsPromptCoordinatorParameters) { + self.parameters = parameters + + let strings = AnalyticsPromptStrings() + let promptType: AnalyticsPromptType + + if Analytics.shared.promptShouldDisplayUpgradeMessage { + promptType = .upgrade(termsString: strings.termsUpgrade) + } else { + promptType = .newUser(termsString: strings.termsNewUser) + } + + let viewModel = AnalyticsPromptViewModel(promptType: promptType, strings: strings, termsURL: BuildSettings.analyticsTermsURL) + + let view = AnalyticsPrompt(viewModel: viewModel.context) + _analyticsPromptViewModel = viewModel + analyticsPromptHostingController = VectorHostingController(rootView: view) + } + + // MARK: - Public + + func start() { + guard #available(iOS 14.0, *) else { + MXLog.debug("[AnalyticsPromptCoordinator] start: Invalid iOS version, returning.") + return + } + + MXLog.debug("[AnalyticsPromptCoordinator] did start.") + + analyticsPromptViewModel.completion = { [weak self] result in + MXLog.debug("[AnalyticsPromptCoordinator] AnalyticsPromptViewModel did complete with result: \(result).") + + guard let self = self else { return } + + switch result { + case .enable: + Analytics.shared.optIn(with: self.parameters.session) + self.completion?() + case .disable: + Analytics.shared.optOut() + self.completion?() + } + } + } + + func toPresentable() -> UIViewController { + return self.analyticsPromptHostingController + } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift new file mode 100644 index 000000000..4ce8ab20d --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift @@ -0,0 +1,33 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@available(iOS 14.0, *) +struct AnalyticsPromptStrings: AnalyticsPromptStringsProtocol { + let appDisplayName = AppInfo.current.displayName + + let point1 = HTMLFormatter().formatHTML(VectorL10n.analyticsPromptPoint1, withAllowedTags: ["b", "p"], fontSize: UIFont.systemFontSize) + let point2 = HTMLFormatter().formatHTML(VectorL10n.analyticsPromptPoint2, withAllowedTags: ["b", "p"], fontSize: UIFont.systemFontSize) + + let termsNewUser = HTMLFormatter().format(VectorL10n.analyticsPromptTermsNewUser("%@"), + with: VectorL10n.analyticsPromptTermsLinkNewUser, + using: BuildSettings.analyticsTermsURL) + let termsUpgrade = HTMLFormatter().format(VectorL10n.analyticsPromptTermsUpgrade("%@"), + with: VectorL10n.analyticsPromptTermsLinkUpgrade, + using: BuildSettings.analyticsTermsURL) +} + diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift new file mode 100644 index 000000000..9c303bbbe --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift @@ -0,0 +1,56 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt +// +// 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 MockAnalyticsPromptScreenState: MockScreenState, CaseIterable { + /// The type of prompt to display. + case promptType(AnalyticsPromptType) + + /// The associated screen + var screenType: Any.Type { + AnalyticsPrompt.self + } + + /// A list of screen state definitions + static var allCases: [MockAnalyticsPromptScreenState] { + AnalyticsPromptType.allCases.map { MockAnalyticsPromptScreenState.promptType($0) } + } + + /// Generate the view struct for the screen state. + var screenView: ([Any], AnyView) { + let promptType: AnalyticsPromptType + switch self { + case .promptType(let analyticsPromptType): + promptType = analyticsPromptType + } + let viewModel = AnalyticsPromptViewModel(promptType: promptType, + strings: MockAnalyticsPromptStrings(), + termsURL: URL(string: "https://element.io/cookie-policy")!) + + return ( + [promptType, viewModel], + AnyView(AnalyticsPrompt(viewModel: viewModel.context) + .addDependency(MockAvatarService.example)) + ) + } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift new file mode 100644 index 000000000..ee0e59ed0 --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift @@ -0,0 +1,52 @@ +// +// 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 + +struct MockAnalyticsPromptStrings: AnalyticsPromptStringsProtocol { + var appDisplayName = "Element" + + let point1: NSAttributedString + let point2: NSAttributedString + + let termsNewUser: NSAttributedString + let termsUpgrade: NSAttributedString + + let shortString = NSAttributedString(string: "This is a short string.") + let longString = NSAttributedString(string: "This is a very long string that will be used to test the layout over multiple lines of text to ensure everything is correct.") + + init() { + let point1 = NSMutableAttributedString(string: "We ") + point1.append(NSAttributedString(string: "don't", attributes: [.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)])) + point1.append(NSAttributedString(string: " record or profile any account data")) + self.point1 = point1 + + let point2 = NSMutableAttributedString(string: "We ") + point2.append(NSAttributedString(string: "don't", attributes: [.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)])) + point2.append(NSAttributedString(string: " share information with third parties")) + self.point2 = point2 + + let termsNewUser = NSMutableAttributedString(string: "You can read all our terms ") + termsNewUser.append(NSAttributedString(string: "here", attributes: [.link: URL(string: "https://element.io/cookie-policy")!])) + termsNewUser.append(NSAttributedString(string: ".")) + self.termsNewUser = termsNewUser + + let termsUpgrade = NSMutableAttributedString(string: "Read all our terms ") + termsUpgrade.append(NSAttributedString(string: "here", attributes: [.link: URL(string: "https://element.io/cookie-policy")!])) + termsUpgrade.append(NSAttributedString(string: ". Is that OK?")) + self.termsUpgrade = termsUpgrade + } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift new file mode 100644 index 000000000..b8a38a117 --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift @@ -0,0 +1,65 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt +// +// 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 AnalyticsPromptUITests: MockScreenTest { + + override class var screenType: MockScreenState.Type { + return MockAnalyticsPromptScreenState.self + } + + override class func createTest() -> MockScreenTest { + return AnalyticsPromptUITests(selector: #selector(verifyAnalyticsPromptScreen)) + } + + func verifyAnalyticsPromptScreen() throws { + guard let screenState = screenState as? MockAnalyticsPromptScreenState else { fatalError("no screen") } + switch screenState { + case .promptType(let promptType): + verifyAnalyticsPromptType(promptType) + } + } + + /// Verify that the prompt is displayed correctly for new users compared to upgrading from Matomo + func verifyAnalyticsPromptType(_ promptType: AnalyticsPromptType) { + let enableButton = app.buttons["enableButton"] + let disableButton = app.buttons["disableButton"] + + XCTAssert(enableButton.exists) + XCTAssert(disableButton.exists) + + switch promptType { + case .newUser: + XCTAssertEqual(enableButton.label, VectorL10n.enable) + XCTAssertEqual(disableButton.label, VectorL10n.locationSharingInvalidAuthorizationNotNow) + case .upgrade: + XCTAssertEqual(enableButton.label, VectorL10n.analyticsPromptYes) + XCTAssertEqual(disableButton.label, VectorL10n.analyticsPromptStop) + } + } + + func verifyAnalyticsPromptLongName(name: String) { + let displayNameText = app.staticTexts["displayNameText"] + XCTAssert(displayNameText.exists) + XCTAssertEqual(displayNameText.label, name) + } + +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift new file mode 100644 index 000000000..8f7acf49d --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift @@ -0,0 +1,142 @@ +// File created from SimpleUserProfileExample +// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt +// +// 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, *) +/// A prompt that asks the user whether they would like to enable Analytics or not. +struct AnalyticsPrompt: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + private var horizontalPadding: CGFloat { + horizontalSizeClass == .regular ? 50 : 16 + } + + // MARK: Public + + @ObservedObject var viewModel: AnalyticsPromptViewModel.Context + + // MARK: Views + + /// The text that explains what analytics will do. + private var messageText: some View { + VStack { + Text("\(viewModel.viewState.promptType.message)\n") + + AnalyticsPromptTermsText(attributedString: viewModel.viewState.promptType.termsStrings) + .accessibilityLabel(Text(viewModel.viewState.promptType.termsStrings.string)) + .accessibilityValue(Text(VectorL10n.accessibilityButtonLabel)) + .onTapGesture { + viewModel.send(viewAction: .openTermsURL) + } + } + } + + /// The list of re-assurances about analytics. + private var checkmarkList: some View { + VStack(alignment: .leading) { + AnalyticsPromptCheckmarkItem(attributedString: viewModel.viewState.strings.point1) + .accessibilityLabel(Text(viewModel.viewState.strings.point1.string)) + + AnalyticsPromptCheckmarkItem(attributedString: viewModel.viewState.strings.point2) + .accessibilityLabel(Text(viewModel.viewState.strings.point2.string)) + + AnalyticsPromptCheckmarkItem(string: VectorL10n.analyticsPromptPoint3) + } + .font(theme.fonts.body) + .frame(maxWidth: .infinity) + } + + private var mainContent: some View { + VStack { + Image(uiImage: Asset.Images.analyticsLogo.image) + .padding(.bottom, 25) + + Text(VectorL10n.analyticsPromptTitle(viewModel.viewState.strings.appDisplayName)) + .font(theme.fonts.title2B) + .foregroundColor(theme.colors.primaryContent) + .padding(.bottom, 2) + + messageText + .font(theme.fonts.body) + .foregroundColor(theme.colors.secondaryContent) + .multilineTextAlignment(.center) + + Divider() + .background(theme.colors.quinaryContent) + .padding(.vertical, 28) + + checkmarkList + .foregroundColor(theme.colors.secondaryContent) + .padding(.bottom, 16) + } + } + + /// The stack of enable/disable buttons. + private var buttons: some View { + VStack { + Button { viewModel.send(viewAction: .enable) } label: { + Text(viewModel.viewState.promptType.enableButtonTitle) + .font(theme.fonts.bodySB) + } + .buttonStyle(PrimaryActionButtonStyle()) + .accessibilityIdentifier("enableButton") + + Button { viewModel.send(viewAction: .disable) } label: { + Text(viewModel.viewState.promptType.disableButtonTitle) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.accent) + .padding(12) + } + .accessibilityIdentifier("disableButton") + } + } + + var body: some View { + GeometryReader { geometry in + VStack { + ScrollView(showsIndicators: false) { + mainContent + .padding(.top, 50) + .padding(.horizontal, horizontalPadding) + } + + buttons + .padding(.horizontal, horizontalPadding) + .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16) + } + .background(theme.colors.background.ignoresSafeArea()) + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct AnalyticsPrompt_Previews: PreviewProvider { + static let stateRenderer = MockAnalyticsPromptScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptCheckmarkItem.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptCheckmarkItem.swift new file mode 100644 index 000000000..9019e9033 --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptCheckmarkItem.swift @@ -0,0 +1,92 @@ +// +// 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 AnalyticsPromptCheckmarkItem: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme + + /// A string with a bold property. + private struct StringComponent { + let string: String + let isBold: Bool + } + + /// Internal representation of the string as composable parts. + private let components: [StringComponent] + + // MARK: - Setup + + init(attributedString: NSAttributedString) { + var components = [StringComponent]() + let range = NSRange(location: 0, length: attributedString.length) + let string = attributedString.string as NSString + + attributedString.enumerateAttributes(in: range, options: []) { attributes, range, stop in + var isBold = false + + if let font = attributes[.font] as? UIFont { + isBold = font.fontDescriptor.symbolicTraits.contains(.traitBold) + } + + components.append(StringComponent(string: string.substring(with: range), isBold: isBold)) + } + + self.components = components + } + + init(string: String) { + self.components = [StringComponent(string: string, isBold: false)] + } + + // MARK: - Views + + var label: Text { + components.reduce(Text("")) { + $0 + Text($1.string).font($1.isBold ? theme.fonts.bodySB : theme.fonts.body) + } + } + + var body: some View { + Label { label } icon: { + Image(uiImage: Asset.Images.analyticsCheckmark.image) + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct AnalyticsPromptCheckmarkItem_Previews: PreviewProvider { + + static let strings = MockAnalyticsPromptStrings() + + static var previews: some View { + VStack(alignment:.leading) { + AnalyticsPromptCheckmarkItem(attributedString: strings.point1) + AnalyticsPromptCheckmarkItem(attributedString: strings.point2) + AnalyticsPromptCheckmarkItem(attributedString: strings.longString) + AnalyticsPromptCheckmarkItem(attributedString: strings.shortString) + } + .padding() + } +} diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptTermsText.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptTermsText.swift new file mode 100644 index 000000000..7616e1084 --- /dev/null +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptTermsText.swift @@ -0,0 +1,74 @@ +// +// 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, *) +/// The last line of text in the description with highlighting on the link string. +struct AnalyticsPromptTermsText: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme + + /// A string with a link attribute. + private struct StringComponent { + let string: String + let isLink: Bool + } + + /// Internal representation of the string as composable parts. + private let components: [StringComponent] + + // MARK: - Setup + + init(attributedString: NSAttributedString) { + var components = [StringComponent]() + let range = NSRange(location: 0, length: attributedString.length) + let string = attributedString.string as NSString + + attributedString.enumerateAttributes(in: range, options: []) { attributes, range, stop in + let isLink = attributes.keys.contains(.link) + components.append(StringComponent(string: string.substring(with: range), isLink: isLink)) + } + + self.components = components + } + + // MARK: - Views + + var body: some View { + components.reduce(Text("")) { + $0 + Text($1.string).foregroundColor($1.isLink ? theme.colors.accent : nil) + } + } +} + +// MARK: - Previews +@available(iOS 14.0, *) +struct AnalyticsPromptTermsText_Previews: PreviewProvider { + + static let strings = MockAnalyticsPromptStrings() + + static var previews: some View { + VStack(spacing: 8) { + AnalyticsPromptTermsText(attributedString: strings.termsNewUser) + AnalyticsPromptTermsText(attributedString: strings.termsUpgrade) + } + } +} diff --git a/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift b/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift index 821c71ef0..1b083d1e0 100644 --- a/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift +++ b/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift @@ -21,14 +21,17 @@ import SwiftUI /// A modifier for showing the activity indicator centered over a view. struct ActivityIndicatorModifier: ViewModifier { var show: Bool - + @ViewBuilder func body(content: Content) -> some View { + content + .overlay(activityIndicator, alignment: .center) + } + + @ViewBuilder + private var activityIndicator: some View { if show { - content - .overlay(ActivityIndicator(), alignment: .center) - } else { - content + ActivityIndicator() } } } diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index bceefcd26..f64a1a51f 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -20,12 +20,14 @@ import Foundation @available(iOS 14.0, *) enum MockAppScreens { static let appScreens: [MockScreenState.Type] = [ - MockTemplateUserProfileScreenState.self, - MockTemplateRoomListScreenState.self, - MockTemplateRoomChatScreenState.self, + MockLocationSharingScreenState.self, + MockAnalyticsPromptScreenState.self, MockUserSuggestionScreenState.self, MockPollEditFormScreenState.self, - MockPollTimelineScreenState.self + MockPollTimelineScreenState.self, + MockTemplateUserProfileScreenState.self, + MockTemplateRoomListScreenState.self, + MockTemplateRoomChatScreenState.self ] } diff --git a/RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift b/RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift index 4bf0df19d..d4ac2a9e7 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift @@ -22,7 +22,6 @@ protocol MockScreenState { static var screenStates: [MockScreenState] { get } var screenType: Any.Type { get } var screenView: ([Any], AnyView) { get } - var stateTitle: String { get } } @available(iOS 14.0, *) @@ -33,44 +32,32 @@ extension MockScreenState { let depsAndViews = screenStates.map(\.screenView) let deps = depsAndViews.map({ $0.0 }) let views = depsAndViews.map({ $0.1 }) - let stateTitles = screenStates.map(\.stateTitle) - let fullScreenTitles = screenStates.map(\.fullScreenTitle) + let titles = screenStates.map(\.title) var states = [ScreenStateInfo]() for i in 0.. String { + String(describing: type).components(separatedBy: .punctuationCharacters).filter { $0.count > 0}.last! } - - /// A title to represent the screen and it's screen state - var fullScreenTitle: String { - "\(screenName): \(stateTitle)" - } - } @available(iOS 14.0, *) diff --git a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift index ce9f09f35..5e29d179b 100644 --- a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift +++ b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift @@ -33,8 +33,7 @@ struct ScreenList: View { ForEach(0.. some View { configuration.label @@ -28,10 +29,18 @@ struct PrimaryActionButtonStyle: ButtonStyle { .frame(maxWidth: .infinity) .foregroundColor(.white) .font(theme.fonts.body) - .background(configuration.isPressed ? theme.colors.accent.opacity(0.6) : theme.colors.accent) - .opacity(enabled ? 1.0 : 0.6) + .background(backgroundColor(configuration.isPressed)) + .opacity(isEnabled ? 1.0 : 0.6) .cornerRadius(8.0) } + + func backgroundColor(_ isPressed: Bool) -> Color { + if let customColor = customColor { + return customColor + } + + return isPressed ? theme.colors.accent.opacity(0.6) : theme.colors.accent + } } @available(iOS 14.0, *) @@ -40,11 +49,20 @@ struct PrimaryActionButtonStyle_Previews: PreviewProvider { Group { VStack { Button("Enabled") { } - .buttonStyle(PrimaryActionButtonStyle(enabled: true)) + .buttonStyle(PrimaryActionButtonStyle()) Button("Disabled") { } - .buttonStyle(PrimaryActionButtonStyle(enabled: false)) + .buttonStyle(PrimaryActionButtonStyle()) .disabled(true) + + Button { } label: { + Text("Clear BG") + .foregroundColor(.red) + } + .buttonStyle(PrimaryActionButtonStyle(customColor: .clear)) + + Button("Red BG") { } + .buttonStyle(PrimaryActionButtonStyle(customColor: .red)) } .padding() } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift new file mode 100644 index 000000000..931c6a3d7 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift @@ -0,0 +1,120 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit +import SwiftUI +import Keys + +struct LocationSharingCoordinatorParameters { + let roomDataSource: MXKRoomDataSource + let mediaManager: MXMediaManager + let avatarData: AvatarInputProtocol + let location: CLLocationCoordinate2D? +} + +final class LocationSharingCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: LocationSharingCoordinatorParameters + private let locationSharingHostingController: UIViewController + private var _locationSharingViewModel: Any? = nil + + @available(iOS 14.0, *) + fileprivate var locationSharingViewModel: LocationSharingViewModel { + return _locationSharingViewModel as! LocationSharingViewModel + } + + // MARK: Public + + var childCoordinators: [Coordinator] = [] + + var completion: (() -> Void)? + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: LocationSharingCoordinatorParameters) { + self.parameters = parameters + + let viewModel = LocationSharingViewModel(tileServerMapURL: BuildSettings.tileServerMapURL, + avatarData: parameters.avatarData, + location: parameters.location) + let view = LocationSharingView(context: viewModel.context) + .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) + + _locationSharingViewModel = viewModel + locationSharingHostingController = VectorHostingController(rootView: view) + } + + // MARK: - Public + func start() { + guard #available(iOS 14.0, *) else { + MXLog.error("[LocationSharingCoordinator] start: Invalid iOS version, returning.") + return + } + + locationSharingViewModel.completion = { [weak self] result in + guard let self = self else { return } + + switch result { + case .cancel: + self.completion?() + case .share(let latitude, let longitude): + if let location = self.parameters.location { + self.showActivityControllerForLocation(location) + return + } + + self.locationSharingViewModel.dispatch(action: .startLoading) + + self.parameters.roomDataSource.sendLocation(withLatitude: latitude, + longitude: longitude, + description: nil) { [weak self] _ in + guard let self = self else { return } + + self.locationSharingViewModel.dispatch(action: .stopLoading(nil)) + self.completion?() + } failure: { [weak self] error in + guard let self = self else { return } + + MXLog.error("[LocationSharingCoordinator] Failed sharing location with error: \(String(describing: error))") + self.locationSharingViewModel.dispatch(action: .stopLoading(error)) + } + } + + } + } + + // MARK: - Presentable + + func toPresentable() -> UIViewController { + return locationSharingHostingController + } + + // MARK: - Private + + private func showActivityControllerForLocation(_ location: CLLocationCoordinate2D) { + let vc = UIActivityViewController(activityItems: [ShareToMapsAppActivity.urlForMapsAppType(.apple, location: location)], + applicationActivities: [ShareToMapsAppActivity(type: .apple, location: location), + ShareToMapsAppActivity(type: .google, location: location)]) + + locationSharingHostingController.present(vc, animated: true) + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/ShareToMapsAppActivity.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/ShareToMapsAppActivity.swift new file mode 100644 index 000000000..6f7bd3780 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/ShareToMapsAppActivity.swift @@ -0,0 +1,78 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +extension UIActivity.ActivityType { + static let shareToMapsApp = UIActivity.ActivityType("Element.ShareToMapsApp") +} + +class ShareToMapsAppActivity: UIActivity { + enum MapsAppType { + case apple + case google + } + + let type: MapsAppType + let location: CLLocationCoordinate2D + + private override init() { + fatalError() + } + + init(type: MapsAppType, location: CLLocationCoordinate2D) { + self.type = type + self.location = location + } + + static func urlForMapsAppType(_ type: MapsAppType, location: CLLocationCoordinate2D) -> URL { + switch type { + case .apple: + return URL(string: "https://maps.apple.com?ll=\(location.latitude),\(location.longitude)&q=Pin")! + case .google: + return URL(string: "https://www.google.com/maps/search/?api=1&query=\(location.latitude),\(location.longitude)")! + } + } + + override var activityTitle: String? { + switch type { + case .apple: + return VectorL10n.locationSharingOpenAppleMaps + case .google: + return VectorL10n.locationSharingOpenGoogleMaps + } + } + + var activityCategory: UIActivity.Category { + return .action + } + + override var activityType: UIActivity.ActivityType { + return .shareToMapsApp + } + + override func canPerform(withActivityItems activityItems: [Any]) -> Bool { + return true + } + + override func prepare(withActivityItems activityItems: [Any]) { + let url = Self.urlForMapsAppType(type, location: location) + + UIApplication.shared.open(url, options: [:]) { [weak self] result in + self?.activityDidFinish(result) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift new file mode 100644 index 000000000..be9c188f9 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift @@ -0,0 +1,85 @@ +// +// 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 +import Combine +import CoreLocation + +enum LocationSharingViewError { + case failedLoadingMap + case failedLocatingUser + case invalidLocationAuthorization + case failedSharingLocation +} + +enum LocationSharingStateAction { + case error(LocationSharingViewError, LocationSharingViewModelCallback?) + case startLoading + case stopLoading(Error?) +} + +enum LocationSharingViewAction { + case cancel + case share +} + +typealias LocationSharingViewModelCallback = ((LocationSharingViewModelResult) -> Void) + +enum LocationSharingViewModelResult { + case cancel + case share(latitude: Double, longitude: Double) +} + +@available(iOS 14, *) +struct LocationSharingViewState: BindableState { + let tileServerMapURL: URL + let avatarData: AvatarInputProtocol + let location: CLLocationCoordinate2D? + + var showLoadingIndicator: Bool = false + + var shareButtonVisible: Bool { + return location == nil + } + + var shareButtonEnabled: Bool { + !showLoadingIndicator + } + + let errorSubject = PassthroughSubject() + + var bindings = LocationSharingViewStateBindings() +} + +struct LocationSharingViewStateBindings { + var alertInfo: ErrorAlertInfo? + var userLocation: CLLocationCoordinate2D? +} + +struct ErrorAlertInfo: Identifiable { + enum AlertType { + case mapLoadingError + case userLocatingError + case authorizationError + case locationSharingError + } + + let id: AlertType + let title: String + let primaryButton: (title: String, action: (() -> Void)?) + let secondaryButton: (title: String, action: (() -> Void)?)? +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift new file mode 100644 index 000000000..f316d761e --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift @@ -0,0 +1,46 @@ +// +// 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 +import Keys +import CoreLocation + +@available(iOS 14.0, *) +enum MockLocationSharingScreenState: MockScreenState, CaseIterable { + case shareUserLocation + case displayExistingLocation + + var screenType: Any.Type { + MockLocationSharingScreenState.self + } + + var screenView: ([Any], AnyView) { + + var location: CLLocationCoordinate2D? + if self == .displayExistingLocation { + location = CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096) + } + + let mapURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=" + RiotKeys().mapTilerAPIKey)! + let viewModel = LocationSharingViewModel(tileServerMapURL: mapURL, + avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: "Alice"), + location: location) + return ([viewModel], + AnyView(LocationSharingView(context: viewModel.context) + .addDependency(MockAvatarService.example))) + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift new file mode 100644 index 000000000..765c0b558 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -0,0 +1,110 @@ +// +// 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 CoreLocation + +@available(iOS 14, *) +typealias LocationSharingViewModelType = StateStoreViewModel< LocationSharingViewState, + LocationSharingStateAction, + LocationSharingViewAction > +@available(iOS 14, *) +class LocationSharingViewModel: LocationSharingViewModelType { + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var completion: ((LocationSharingViewModelResult) -> Void)? + + // MARK: - Setup + + init(tileServerMapURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D? = nil) { + let viewState = LocationSharingViewState(tileServerMapURL: tileServerMapURL, avatarData: avatarData, location: location) + super.init(initialViewState: viewState) + + state.errorSubject.sink { [weak self] error in + guard let self = self else { return } + self.dispatch(action: .error(error, self.completion)) + }.store(in: &cancellables) + } + + // MARK: - Public + + override func process(viewAction: LocationSharingViewAction) { + switch viewAction { + case .cancel: + completion?(.cancel) + case .share: + if let location = state.location { + completion?(.share(latitude: location.latitude, longitude: location.longitude)) + return + } + + guard let location = state.bindings.userLocation else { + dispatch(action: .error(.failedLocatingUser, completion)) + return + } + + completion?(.share(latitude: location.latitude, longitude: location.longitude)) + } + } + + override class func reducer(state: inout LocationSharingViewState, action: LocationSharingStateAction) { + switch action { + case .error(let error, let completion): + + switch error { + case .failedLoadingMap: + state.bindings.alertInfo = ErrorAlertInfo(id: .mapLoadingError, + title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName) , + primaryButton: (VectorL10n.ok, { completion?(.cancel) }), + secondaryButton: nil) + case .failedLocatingUser: + state.bindings.alertInfo = ErrorAlertInfo(id: .userLocatingError, + title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, { completion?(.cancel) }), + secondaryButton: nil) + case .invalidLocationAuthorization: + state.bindings.alertInfo = ErrorAlertInfo(id: .authorizationError, + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, { completion?(.cancel) }), + secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, { + if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { + UIApplication.shared.open(applicationSettingsURL) + } + })) + default: + break + } + + case .startLoading: + state.showLoadingIndicator = true + case .stopLoading(let error): + state.showLoadingIndicator = false + + if error != nil { + state.bindings.alertInfo = ErrorAlertInfo(id: .locationSharingError, + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, nil), + secondaryButton: nil) + } + } + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift new file mode 100644 index 000000000..6e68f3556 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift @@ -0,0 +1,53 @@ +// +// 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 LocationSharingUITests: XCTestCase { + + private var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + } + + func testInitialUserLocation() { + goToScreenWithIdentifier(MockLocationSharingScreenState.shareUserLocation.title) + + XCTAssertTrue(app.buttons["Cancel"].exists) + XCTAssertTrue(app.buttons["Share"].exists) + XCTAssertTrue(app.otherElements["Map"].exists) + } + + func testInitialExistingLocation() { + goToScreenWithIdentifier(MockLocationSharingScreenState.displayExistingLocation.title) + + XCTAssertTrue(app.buttons["Cancel"].exists) + XCTAssertTrue(app.buttons["location share icon"].exists) + XCTAssertTrue(app.otherElements["Map"].exists) + } + + // Need a delay when showing the map otherwise the simulator breaks + private func goToScreenWithIdentifier(_ identifier: String) { + app.goToScreenWithIdentifier(identifier) + sleep(2) + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift new file mode 100644 index 000000000..e3371727c --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift @@ -0,0 +1,128 @@ +// +// 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 +import CoreLocation + +@testable import RiotSwiftUI + +@available(iOS 14.0, *) +class LocationSharingViewModelTests: XCTestCase { + + var cancellables = Set() + + func testInitialState() { + let viewModel = buildViewModel(withLocation: false) + + XCTAssertTrue(viewModel.context.viewState.shareButtonEnabled) + XCTAssertTrue(viewModel.context.viewState.shareButtonVisible) + XCTAssertFalse(viewModel.context.viewState.showLoadingIndicator) + + XCTAssertNotNil(viewModel.context.viewState.tileServerMapURL) + XCTAssertNotNil(viewModel.context.viewState.avatarData) + + XCTAssertNil(viewModel.context.viewState.location) + XCTAssertNil(viewModel.context.viewState.bindings.userLocation) + XCTAssertNil(viewModel.context.viewState.bindings.alertInfo) + } + + func testCancellation() { + let viewModel = buildViewModel(withLocation: false) + + let expectation = self.expectation(description: "Cancellation completion should be invoked") + + viewModel.completion = { result in + switch result { + case .share: + XCTFail() + case .cancel: + expectation.fulfill() + } + } + + viewModel.context.send(viewAction: .cancel) + + waitForExpectations(timeout: 3) + } + + func testShareNoUserLocation() { + let viewModel = buildViewModel(withLocation: false) + + XCTAssertNil(viewModel.context.viewState.bindings.userLocation) + XCTAssertNil(viewModel.context.viewState.location) + + viewModel.context.send(viewAction: .share) + + XCTAssertNotNil(viewModel.context.viewState.bindings.alertInfo) + XCTAssertEqual(viewModel.context.viewState.bindings.alertInfo?.id, .userLocatingError) + } + + func testShareExistingLocation() { + let viewModel = buildViewModel(withLocation: true) + + let expectation = self.expectation(description: "Share completion should be invoked") + + viewModel.completion = { result in + switch result { + case .share(let latitude, let longitude): + XCTAssertEqual(latitude, viewModel.context.viewState.location?.latitude) + XCTAssertEqual(longitude, viewModel.context.viewState.location?.longitude) + expectation.fulfill() + case .cancel: + XCTFail() + } + } + + XCTAssertNil(viewModel.context.viewState.bindings.userLocation) + XCTAssertNotNil(viewModel.context.viewState.location) + + viewModel.context.send(viewAction: .share) + + XCTAssertNil(viewModel.context.viewState.bindings.alertInfo) + + waitForExpectations(timeout: 3) + } + + func testLoading() { + let viewModel = buildViewModel(withLocation: false) + + viewModel.dispatch(action: .startLoading) + + XCTAssertFalse(viewModel.context.viewState.shareButtonEnabled) + XCTAssertTrue(viewModel.context.viewState.showLoadingIndicator) + + viewModel.dispatch(action: .stopLoading(nil)) + + XCTAssertTrue(viewModel.context.viewState.shareButtonEnabled) + XCTAssertFalse(viewModel.context.viewState.showLoadingIndicator) + } + + func testInvalidLocationAuthorization() { + let viewModel = buildViewModel(withLocation: false) + + viewModel.context.viewState.errorSubject.send(.invalidLocationAuthorization) + + XCTAssertNotNil(viewModel.context.alertInfo) + XCTAssertEqual(viewModel.context.viewState.bindings.alertInfo?.id, .authorizationError) + } + + private func buildViewModel(withLocation: Bool) -> LocationSharingViewModel { + LocationSharingViewModel(tileServerMapURL: URL(string: "http://empty.com")!, + avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: ""), + location: (withLocation ? CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096) : nil)) + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift new file mode 100644 index 000000000..53064ae54 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift @@ -0,0 +1,132 @@ +// +// 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, *) +struct LocationSharingMapView: UIViewRepresentable { + private struct Constants { + static let mapZoomLevel = 15.0 + } + + let tileServerMapURL: URL + let avatarData: AvatarInputProtocol + let location: CLLocationCoordinate2D? + + let errorSubject: PassthroughSubject + @Binding var userLocation: CLLocationCoordinate2D? + + func makeUIView(context: Context) -> some UIView { + let mapView = MGLMapView(frame: .zero, styleURL: tileServerMapURL) + mapView.delegate = context.coordinator + + mapView.logoView.isHidden = true + mapView.attributionButton.isHidden = true + + if let location = location { + mapView.setCenter(location, zoomLevel: Constants.mapZoomLevel, animated: false) + + let pointAnnotation = MGLPointAnnotation() + pointAnnotation.coordinate = location + mapView.addAnnotation(pointAnnotation) + } else { + mapView.showsUserLocation = true + mapView.userTrackingMode = .follow + } + + return mapView + } + + func updateUIView(_ uiView: UIViewType, context: Context) { + + } + + func makeCoordinator() -> LocationSharingMapViewCoordinator { + LocationSharingMapViewCoordinator(avatarData: avatarData, + errorSubject: errorSubject, + userLocation: $userLocation) + } +} + +@available(iOS 14, *) +class LocationSharingMapViewCoordinator: NSObject, MGLMapViewDelegate { + + private let avatarData: AvatarInputProtocol + private let errorSubject: PassthroughSubject + @Binding var userLocation: CLLocationCoordinate2D? + + init(avatarData: AvatarInputProtocol, + errorSubject: PassthroughSubject, + userLocation: Binding) { + self.avatarData = avatarData + self.errorSubject = errorSubject + self._userLocation = userLocation + } + + // MARK: - MGLMapViewDelegate + + func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { + return UserLocationAnnotatonView(avatarData: avatarData) + } + + func mapViewDidFailLoadingMap(_ mapView: MGLMapView, withError error: Error) { + errorSubject.send(.failedLoadingMap) + } + + func mapView(_ mapView: MGLMapView, didFailToLocateUserWithError error: Error) { + errorSubject.send(.failedLocatingUser) + } + + func mapView(_ mapView: MGLMapView, didUpdate userLocation: MGLUserLocation?) { + self.userLocation = userLocation?.coordinate + } + + func mapView(_ mapView: MGLMapView, didChangeLocationManagerAuthorization manager: MGLLocationManager) { + switch manager.authorizationStatus { + case .restricted: + fallthrough + case .denied: + errorSubject.send(.failedLocatingUser) + default: + break + } + } +} + +@available(iOS 14, *) +private class UserLocationAnnotatonView: MGLUserLocationAnnotationView { + + init(avatarData: AvatarInputProtocol) { + super.init(frame: .zero) + + guard let avatarImageView = UIHostingController(rootView: LocationSharingUserMarkerView(avatarData: avatarData)).view else { + return + } + + addSubview(avatarImageView) + + addConstraints([topAnchor.constraint(equalTo: avatarImageView.topAnchor), + leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor), + bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), + trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor)]) + } + + required init?(coder: NSCoder) { + fatalError() + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift new file mode 100644 index 000000000..0727e0b5b --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingUserMarkerView.swift @@ -0,0 +1,53 @@ +// +// 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 LocationSharingUserMarkerView: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + let avatarData: AvatarInputProtocol + + var body: some View { + ZStack(alignment: .center) { + Image(uiImage: Asset.Images.locationUserMarker.image) + AvatarImage(avatarData: avatarData, size: .large) + .offset(.init(width: 0.0, height: -1.5)) + } + .accentColor(theme.colors.accent) + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct LocationSharingUserMarkerView_Previews: PreviewProvider { + static var previews: some View { + let avatarData = AvatarInput(mxContentUri: "", + matrixItemId: "", + displayName: "Alice") + + LocationSharingUserMarkerView(avatarData: avatarData) + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift new file mode 100644 index 000000000..9bc932536 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift @@ -0,0 +1,106 @@ +// +// 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 CoreLocation + +@available(iOS 14.0, *) +struct LocationSharingView: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + @ObservedObject var context: LocationSharingViewModel.Context + + var body: some View { + NavigationView { + LocationSharingMapView(tileServerMapURL: context.viewState.tileServerMapURL, + avatarData: context.viewState.avatarData, + location: context.viewState.location, + errorSubject: context.viewState.errorSubject, + userLocation: $context.userLocation) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button(VectorL10n.cancel, action: { + context.send(viewAction: .cancel) + }) + } + ToolbarItem(placement: .principal) { + Text(VectorL10n.locationSharingTitle) + .font(.headline) + .foregroundColor(theme.colors.primaryContent) + } + ToolbarItem(placement: .navigationBarTrailing) { + if context.viewState.location != nil { + Button { + context.send(viewAction: .share) + } label: { + Image(uiImage: Asset.Images.locationShareIcon.image) + } + .disabled(!context.viewState.shareButtonEnabled) + } else { + Button(VectorL10n.locationSharingShareAction, action: { + context.send(viewAction: .share) + }) + .disabled(!context.viewState.shareButtonEnabled) + } + } + } + .navigationBarTitleDisplayMode(.inline) + .ignoresSafeArea() + .alert(item: $context.alertInfo) { info in + if let secondaryButton = info.secondaryButton { + return Alert(title: Text(info.title), + primaryButton: .default(Text(info.primaryButton.title)) { + info.primaryButton.action?() + }, + secondaryButton: .default(Text(secondaryButton.title)) { + secondaryButton.action?() + }) + } else { + return Alert(title: Text(info.title), + dismissButton: .default(Text(info.primaryButton.title)) { + info.primaryButton.action?() + }) + } + } + } + .accentColor(theme.colors.accent) + .activityIndicator(show: context.viewState.showLoadingIndicator) + } + + @ViewBuilder + private var activityIndicator: some View { + if context.viewState.showLoadingIndicator { + ActivityIndicator() + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct LocationSharingView_Previews: PreviewProvider { + static let stateRenderer = MockLocationSharingScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift index da63d7098..ee0f60a78 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -21,11 +21,10 @@ import UIKit import SwiftUI struct PollEditFormCoordinatorParameters { - let navigationRouter: NavigationRouterType? let room: MXRoom } -final class PollEditFormCoordinator: Coordinator { +final class PollEditFormCoordinator: Coordinator, Presentable { // MARK: - Properties @@ -42,9 +41,10 @@ final class PollEditFormCoordinator: Coordinator { // MARK: Public - // Must be used only internally var childCoordinators: [Coordinator] = [] + var completion: (() -> Void)? + // MARK: - Setup @available(iOS 14.0, *) @@ -65,13 +65,11 @@ final class PollEditFormCoordinator: Coordinator { return } - parameters.navigationRouter?.present(pollEditFormHostingController, animated: true) - pollEditFormViewModel.completion = { [weak self] result in guard let self = self else { return } switch result { case .cancel: - self.parameters.navigationRouter?.dismissModule(animated: true, completion: nil) + self.completion?() case .create(let question, let answerOptions): var options = [MXEventContentPollStartAnswerOption]() for answerOption in answerOptions { @@ -88,8 +86,8 @@ final class PollEditFormCoordinator: Coordinator { self.parameters.room.sendPollStart(withContent: pollStartContent, localEcho: nil) { [weak self] result in guard let self = self else { return } - self.parameters.navigationRouter?.dismissModule(animated: true, completion: nil) self.pollEditFormViewModel.dispatch(action: .stopLoading(nil)) + self.completion?() } failure: { [weak self] error in guard let self = self else { return } @@ -99,4 +97,10 @@ final class PollEditFormCoordinator: Coordinator { } } } + + // MARK: - Private + + func toPresentable() -> UIViewController { + return pollEditFormHostingController + } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift index daee82cbd..2780739cd 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift @@ -29,7 +29,7 @@ class PollEditFormUITests: XCTestCase { app = XCUIApplication() app.launch() - app.buttons[MockPollEditFormScreenState.screenStateKeys.first!].tap() + app.goToScreenWithIdentifier(MockPollEditFormScreenState.standard.title) } func testInitialStateComponents() { diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift index bdf54e7bd..57f3a63a3 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift @@ -79,7 +79,7 @@ struct PollEditForm: View { Button(VectorL10n.pollEditFormCreatePoll) { viewModel.send(viewAction: .create) } - .buttonStyle(PrimaryActionButtonStyle(enabled: viewModel.viewState.confirmationButtonEnabled)) + .buttonStyle(PrimaryActionButtonStyle()) .disabled(!viewModel.viewState.confirmationButtonEnabled) } .padding() @@ -87,7 +87,7 @@ struct PollEditForm: View { .alert(isPresented: $viewModel.showsFailureAlert) { Alert(title: Text(VectorL10n.pollEditFormPostFailureTitle), message: Text(VectorL10n.pollEditFormPostFailureSubtitle), - dismissButton: .default(Text(VectorL10n.pollEditFormPostFailureAction))) + dismissButton: .default(Text(VectorL10n.ok))) } .frame(minHeight: proxy.size.height) // Make the VStack fill the ScrollView's parent .toolbar { diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift b/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift index 8d201264f..78c53acec 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift @@ -27,7 +27,7 @@ struct PollTimelineCoordinatorParameters { } @available(iOS 14.0, *) -final class PollTimelineCoordinator: Coordinator, PollAggregatorDelegate { +final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDelegate { // MARK: - Properties diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/Test/UI/PollTimelineUITests.swift b/RiotSwiftUI/Modules/Room/PollTimeline/Test/UI/PollTimelineUITests.swift index 798c07f3c..f36660e54 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/Test/UI/PollTimelineUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollTimeline/Test/UI/PollTimelineUITests.swift @@ -32,7 +32,7 @@ class PollTimelineUITests: XCTestCase { } func testOpenPoll() { - app.buttons[MockPollTimelineScreenState.screenStateKeys.first!].tap() + app.goToScreenWithIdentifier(MockPollTimelineScreenState.open.title) XCTAssert(app.staticTexts["Question"].exists) XCTAssert(app.staticTexts["20 votes cast"].exists) @@ -70,7 +70,7 @@ class PollTimelineUITests: XCTestCase { } func testClosedPoll() { - app.buttons[MockPollTimelineScreenState.screenStateKeys.last!].tap() + app.goToScreenWithIdentifier(MockPollTimelineScreenState.closed.title) XCTAssert(app.staticTexts["Question"].exists) XCTAssert(app.staticTexts["Final results based on 20 votes"].exists) diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift b/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift index 22a94bb68..70efcc067 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift +++ b/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift @@ -50,7 +50,7 @@ struct PollTimelineView: View { .alert(isPresented: $viewModel.showsClosingFailureAlert) { Alert(title: Text(VectorL10n.pollTimelineNotClosedTitle), message: Text(VectorL10n.pollTimelineNotClosedSubtitle), - dismissButton: .default(Text(VectorL10n.pollTimelineNotClosedAction))) + dismissButton: .default(Text(VectorL10n.ok))) } } .disabled(poll.closed) @@ -62,7 +62,7 @@ struct PollTimelineView: View { .alert(isPresented: $viewModel.showsAnsweringFailureAlert) { Alert(title: Text(VectorL10n.pollTimelineVoteNotRegisteredTitle), message: Text(VectorL10n.pollTimelineVoteNotRegisteredSubtitle), - dismissButton: .default(Text(VectorL10n.pollTimelineVoteNotRegisteredAction))) + dismissButton: .default(Text(VectorL10n.ok))) } } .padding([.horizontal, .top], 2.0) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index e7308710d..d960a9796 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -26,7 +26,7 @@ protocol UserSuggestionCoordinatorDelegate: AnyObject { } @available(iOS 14.0, *) -final class UserSuggestionCoordinator: Coordinator { +final class UserSuggestionCoordinator: Coordinator, Presentable { // MARK: - Properties diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift index 190d12a9f..da0b965be 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift @@ -14,15 +14,13 @@ // limitations under the License. // -import Foundation -import UIKit import SwiftUI struct TemplateUserProfileCoordinatorParameters { let session: MXSession } -final class TemplateUserProfileCoordinator: Coordinator { +final class TemplateUserProfileCoordinator: Coordinator, Presentable { // MARK: - Properties diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift index c7a79d69f..d7af25881 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift @@ -19,7 +19,7 @@ import UIKit @objcMembers -final class TemplateRoomsCoordinator: Coordinator { +final class TemplateRoomsCoordinator: Coordinator, Presentable { // MARK: - Properties diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift index feef94130..a4a75ef88 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift @@ -14,8 +14,6 @@ // limitations under the License. // -import Foundation -import UIKit import SwiftUI struct TemplateRoomChatCoordinatorParameters { diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift index a749cde50..821f4ccac 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift @@ -96,14 +96,14 @@ class TemplateRoomChatService: TemplateRoomChatServiceProtocol { return events .filter({ event in event.type == kMXEventTypeStringRoomMessage - && event.content["msgtype"] as? String == kMXMessageTypeText + && event.content[kMXMessageTypeKey] as? String == kMXMessageTypeText // TODO: New to our SwiftUI Template? Why not implement another message type like image? }) .compactMap({ event -> TemplateRoomChatMessage? in guard let eventId = event.eventId, - let body = event.content["body"] as? String, + let body = event.content[kMXMessageBodyKey] as? String, let sender = senderForMessage(event: event) else { return nil } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift index 4f0453231..6c7cfd36f 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift @@ -14,8 +14,6 @@ // limitations under the License. // -import Foundation -import UIKit import SwiftUI struct TemplateRoomListCoordinatorParameters { diff --git a/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h b/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h new file mode 100644 index 000000000..b75953b6e --- /dev/null +++ b/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h @@ -0,0 +1,5 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "BuildInfo.h" diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml index ecbf8a91c..66d318a6a 100644 --- a/RiotSwiftUI/target.yml +++ b/RiotSwiftUI/target.yml @@ -31,6 +31,7 @@ targets: platform: iOS dependencies: - target: DesignKit + - package: Mapbox sources: - path: . excludes: @@ -38,6 +39,8 @@ targets: - "**/MatrixSDK/**" - "**/Coordinator/**" - "**/Test/**" + - path: ../Riot/Managers/AppInfo/ + - path: ../Riot/Categories/Bundle.swift - path: ../Riot/Generated/Strings.swift - path: ../Riot/Generated/Images.swift - path: ../Riot/Managers/Theme/ThemeIdentifier.swift diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml index 0e2a100df..68de22ac5 100644 --- a/RiotSwiftUI/targetUITests.yml +++ b/RiotSwiftUI/targetUITests.yml @@ -37,6 +37,8 @@ targets: base: TEST_TARGET_NAME: RiotSwiftUI 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 sources: # Source included/excluded here here are similar to RiotSwiftUI as we # need access to ScreenStates @@ -45,6 +47,8 @@ targets: - "**/MatrixSDK/**" - "**/Coordinator/**" - "**/Test/Unit/**" + - path: ../Riot/Managers/AppInfo/ + - path: ../Riot/Categories/Bundle.swift - path: ../Riot/Generated/Strings.swift - path: ../Riot/Generated/Images.swift - path: ../Riot/Managers/Theme/ThemeIdentifier.swift diff --git a/RiotTests/AnalyticsTests.swift b/RiotTests/AnalyticsTests.swift new file mode 100644 index 000000000..5e15bf523 --- /dev/null +++ b/RiotTests/AnalyticsTests.swift @@ -0,0 +1,73 @@ +// +// 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 Riot + +class AnalyticsTests: XCTestCase { + func testAnalyticsPromptNewUser() { + // Given a fresh install of the app (with neither PostHog nor Matomo analytics having been set). + RiotSettings.defaults.removeObject(forKey: RiotSettings.UserDefaultsKeys.enableAnalytics) + RiotSettings.defaults.removeObject(forKey: RiotSettings.UserDefaultsKeys.matomoAnalytics) + + // When the user is prompted for analytics. + let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt + let displayUpgradeMessage = Analytics.shared.promptShouldDisplayUpgradeMessage + + // Then the regular prompt should be shown. + XCTAssertTrue(showPrompt, "A prompt should be shown for a new user.") + XCTAssertFalse(displayUpgradeMessage, "The prompt should not ask about upgrading from Matomo.") + } + + func testAnalyticsPromptUpgradeFromMatomo() { + // Given an existing install of the app where the user previously accepted Matomo analytics + RiotSettings.defaults.removeObject(forKey: RiotSettings.UserDefaultsKeys.enableAnalytics) + RiotSettings.defaults.set(true, forKey: RiotSettings.UserDefaultsKeys.matomoAnalytics) + + // When the user is prompted for analytics + let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt + let displayUpgradeMessage = Analytics.shared.promptShouldDisplayUpgradeMessage + + // Then an upgrade prompt should be shown. + XCTAssertTrue(showPrompt, "A prompt should be shown to the user.") + XCTAssertTrue(displayUpgradeMessage, "The prompt should ask about upgrading from Matomo.") + } + + func testAnalyticsPromptUserDeclinedMatomo() { + // Given an existing install of the app where the user previously declined Matomo analytics + RiotSettings.defaults.removeObject(forKey: RiotSettings.UserDefaultsKeys.enableAnalytics) + RiotSettings.defaults.set(false, forKey: RiotSettings.UserDefaultsKeys.matomoAnalytics) + + // When the user is prompted for analytics + let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt + let displayUpgradeMessage = Analytics.shared.promptShouldDisplayUpgradeMessage + + // Then the regular prompt should be shown. + XCTAssertTrue(showPrompt, "A prompt should be shown to the user.") + XCTAssertFalse(displayUpgradeMessage, "The prompt should not ask about upgrading from Matomo.") + } + + func testAnalyticsPromptUserAcceptedPostHog() { + // Given an existing install of the app where the user previously accepted PostHog + RiotSettings.defaults.set(true, forKey: RiotSettings.UserDefaultsKeys.enableAnalytics) + + // When the user is prompted for analytics + let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt + + // Then no prompt should be shown. + XCTAssertFalse(showPrompt, "A prompt should not be shown any more.") + } +} diff --git a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m index fb3e28af7..440022c84 100644 --- a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m +++ b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m @@ -51,10 +51,8 @@ anEvent.eventId = @"anEventId"; anEvent.wireType = kMXEventTypeStringRoomMessage; anEvent.originServerTs = (uint64_t) ([[NSDate date] timeIntervalSince1970] * 1000); - anEvent.wireContent = @{ - @"msgtype": kMXMessageTypeText, - @"body": @"deded", - }; + anEvent.wireContent = @{ kMXMessageTypeKey: kMXMessageTypeText, + kMXMessageBodyKey: @"deded" }; maxHeaderSize = ceil(eventFormatter.defaultTextFont.pointSize * 1.2); } diff --git a/project.yml b/project.yml index 44c6eeb12..dcd15f63b 100644 --- a/project.yml +++ b/project.yml @@ -35,3 +35,9 @@ include: - path: RiotSwiftUI/target.yml - path: RiotSwiftUI/targetUnitTests.yml - path: RiotSwiftUI/targetUITests.yml + +packages: + Mapbox: + url: https://github.com/maplibre/maplibre-gl-native-distribution + minVersion: 5.12.2 + maxVersion: 5.13.0 \ No newline at end of file