diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..c6b229539 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'bug' +assignees: '' + +--- + +#### Describe the bug. +A clear and concise description of what the bug is. + +#### Steps to reproduce: +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +#### Expected behavior +A clear and concise description of what you expected to happen. + +#### Screenshots +If applicable, add screenshots to help explain your problem. + +#### Contextual information: + + - Device: + - OS: + + - App Version: \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 000000000..b30282798 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Element iOS Community Support + url: "https://matrix.to/#/#element-ios:matrix.org" + about: General Element iOS support questions can be asked here. + - name: Matrix Security Policy + url: https://www.matrix.org/security-disclosure-policy/ + about: Learn more about our security disclosure policy. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..5a372c146 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'feature' +assignees: '' + +--- + +#### Is your feature request related to a problem? Please describe. +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +#### Describe the solution you'd like. +A clear and concise description of what you want to happen. + +#### Describe alternatives you've considered. +A clear and concise description of any alternative solutions or features you've considered. + +#### Additional context. +Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci-build.yml similarity index 56% rename from .github/workflows/ci.yml rename to .github/workflows/ci-build.yml index 3e9e33f5c..d1cd1b3a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci-build.yml @@ -1,4 +1,4 @@ -name: CI +name: Build CI on: # Triggers the workflow on any pull request and push to develop @@ -52,43 +52,3 @@ jobs: # Main step - name: Build iOS simulator run: bundle exec fastlane build - - - tests: - name: Tests - runs-on: macos-latest - - steps: - - uses: actions/checkout@v2 - - # Common cache - # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job - - uses: actions/cache@v2 - with: - path: Pods - key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} - restore-keys: | - ${{ runner.os }}-pods- - - uses: actions/cache@v2 - with: - path: vendor/bundle - key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} - restore-keys: | - ${{ runner.os }}-gems- - - # Make sure we use the latest version of MatrixKit - - name: Reset MatrixKit pod - run: rm -rf Pods/MatrixKit - - # Common setup - # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job - - name: Bundle install - run: | - bundle config path vendor/bundle - bundle install --jobs 4 --retry 3 - - name: Use right MatrixKit and MatrixSDK versions - run: bundle exec fastlane point_dependencies_to_related_branches - - # Main step - - name: Unit tests - run: bundle exec fastlane test diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml new file mode 100644 index 000000000..eac0c885d --- /dev/null +++ b/.github/workflows/ci-tests.yml @@ -0,0 +1,54 @@ +name: Tests CI + +on: + # Triggers the workflow on any pull request and push to develop + push: + branches: [ develop ] + pull_request: + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +env: + # Make the git branch for a PR available to our Fastfile + MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + +jobs: + tests: + name: Tests + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + + # Common cache + # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job + - uses: actions/cache@v2 + with: + path: Pods + key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-pods- + - uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems- + + # Make sure we use the latest version of MatrixKit + - name: Reset MatrixKit pod + run: rm -rf Pods/MatrixKit + + # Common setup + # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job + - name: Bundle install + run: | + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + - name: Use right MatrixKit and MatrixSDK versions + run: bundle exec fastlane point_dependencies_to_related_branches + + # Main step + - name: Unit tests + run: bundle exec fastlane test diff --git a/CHANGES.rst b/CHANGES.rst index 7bb3eb350..bf77aedef 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,44 @@ +Changes in 1.4.8 (2021-07-29) +================================================= + +✨ Features + * + +🙌 Improvements + * Room: Added support for Voice Messages (#4090, #4091, #4092, #4094, #4095, #4096) + * Rooms Tab: Remove the directory section (#4521). + * Notifications: Show decrypted content is enabled by default (#4519). + * People Tab: Remove the local contacts section (#4523). + * Contacts: Delay access to local contacts until they're needed for display (#4616). + * RecentsDataSource: Factorize section reset in one place (target #4591). + * Voice Messages: Tap/hold to send voice messages isn't intuitive (#4601). + * Voice Messages: copy could be improved (#4604). + * Slide to lock should be more generous (#4602). + +🐛 Bugfix + * Room: Fixed mentioning users from room info member details (#4583) + * Settings: Disabled autocorrection when entering an identity server (#4593). + * Room Notification Settings: Fix Crash when opening the new Room Notification Settings Screen (Not yet released) (#4599). + * AuthenticationViewController: Fix crash on authentication if an intermediate view was presented (#4606). + * Room: Fixed crash when opening a read-only room (#4620). + * Voice Messages: Tapping on waveform in composer glitches UI (#4603). + +⚠️ API Changes + * + +🗣 Translations + * + +🧱 Build + * + +Others + * Separated CI jobs into individual actions + * Update Gemfile.lock + +Improvements: + * Upgrade MatrixKit version ([v0.15.6](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.15.6)). + Changes in 1.4.7 (2021-07-22) ================================================= @@ -20,7 +61,7 @@ Changes in 1.4.7 (2021-07-22) * Others - * + * Updated issue templates. Improvements: * Upgrade MatrixKit version ([v0.15.5](https://github.com/matrix-org/matrix-ios-kit/releases/tag/v0.15.5)). diff --git a/Config/AppIdentifiers.xcconfig b/Config/AppIdentifiers.xcconfig index a5e2e60c9..1628e83d8 100644 --- a/Config/AppIdentifiers.xcconfig +++ b/Config/AppIdentifiers.xcconfig @@ -22,8 +22,8 @@ APPLICATION_GROUP_IDENTIFIER = group.im.vector APPLICATION_SCHEME = element // Version -MARKETING_VERSION = 1.4.7 -CURRENT_PROJECT_VERSION = 1.4.7 +MARKETING_VERSION = 1.4.8 +CURRENT_PROJECT_VERSION = 1.4.8 // Team diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 19c2ec733..ed613fa9b 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -148,7 +148,7 @@ final class BuildSettings: NSObject { // MARK: - Public rooms Directory - static let publicRoomsShowDirectory: Bool = true + #warning("Unused build setting: should this be implemented in ShowDirectory?") static let publicRoomsAllowServerChange: Bool = true // List of homeservers for the public rooms directory static let publicRoomsDirectoryServers = [ @@ -309,6 +309,13 @@ final class BuildSettings: NSObject { static let messageDetailsAllowCopyMedia: Bool = true static let messageDetailsAllowPasteMedia: Bool = true + // MARK: - Voice Message + + static let voiceMessagesEnabled = false + + // MARK: - Notifications + static let decryptNotificationsByDefault: Bool = true + // MARK: - HTTP /// Additional HTTP headers will be sent by all requests. Not recommended to use request-specific headers, like `Authorization`. /// Empty dictionary by default. diff --git a/DesignKit/Source/Colors.swift b/DesignKit/Source/Colors.swift index b5dc66261..fc96f3608 100644 --- a/DesignKit/Source/Colors.swift +++ b/DesignKit/Source/Colors.swift @@ -45,6 +45,10 @@ import UIKit /// - Icons var quarterlyContent: UIColor { get } + /// - Text + /// - Icons + var quinaryContent: UIColor { get } + /// Separating line var separator: UIColor { get } diff --git a/DesignKit/Variants/Colors/Dark/DarkColors.swift b/DesignKit/Variants/Colors/Dark/DarkColors.swift index a1cf2ac54..39a3b0893 100644 --- a/DesignKit/Variants/Colors/Dark/DarkColors.swift +++ b/DesignKit/Variants/Colors/Dark/DarkColors.swift @@ -32,6 +32,8 @@ public class DarkColors: Colors { public let quarterlyContent: UIColor = UIColor(rgb: 0x6F7882) + public let quinaryContent: UIColor = UIColor(rgb: 0x394049) + public let separator: UIColor = UIColor(rgb: 0x21262C) public let tile: UIColor = UIColor(rgb: 0x394049) diff --git a/DesignKit/Variants/Colors/Light/LightColors.swift b/DesignKit/Variants/Colors/Light/LightColors.swift index d8b5e108b..53a9566e2 100644 --- a/DesignKit/Variants/Colors/Light/LightColors.swift +++ b/DesignKit/Variants/Colors/Light/LightColors.swift @@ -32,6 +32,8 @@ public class LightColors: Colors { public let quarterlyContent: UIColor = UIColor(rgb: 0xC1C6CD) + public let quinaryContent: UIColor = UIColor(rgb: 0xE3E8F0) + public let separator: UIColor = UIColor(rgb: 0xE3E8F0) public let tile: UIColor = UIColor(rgb: 0xF3F8FD) diff --git a/Gemfile.lock b/Gemfile.lock index 31e7491fe..3548311ee 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,7 +7,7 @@ GEM i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - addressable (2.7.0) + addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) @@ -252,4 +252,4 @@ DEPENDENCIES xcode-install BUNDLED WITH - 2.2.14 + 2.2.21 diff --git a/Podfile b/Podfile index d9a65728f..53b05e0e6 100644 --- a/Podfile +++ b/Podfile @@ -11,7 +11,7 @@ use_frameworks! # - `{ {kit spec hash} => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for each repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixKitVersion = '= 0.15.5' +$matrixKitVersion = '= 0.15.6' # $matrixKitVersion = :local # $matrixKitVersion = {'develop' => 'develop'} @@ -69,6 +69,8 @@ abstract_target 'RiotPods' do pod 'SwiftBase32', '~> 0.9.0' pod 'SwiftJWT', '~> 3.6.200' pod 'SideMenu', '~> 6.5' + pod 'DSWaveformImage', '~> 6.1.1' + pod 'ffmpeg-kit-ios-audio', '~> 4.4.LTS' pod 'FLEX', '~> 4.4.1', :configurations => ['Debug'] diff --git a/Podfile.lock b/Podfile.lock index caf60feb9..8a8b129b9 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -19,6 +19,7 @@ PODS: - BlueRSA (1.0.34) - DGCollectionViewLeftAlignFlowLayout (1.0.4) - Down (0.11.0) + - DSWaveformImage (6.1.1) - DTCoreText (1.6.26): - DTCoreText/Core (= 1.6.26) - DTFoundation/Core (~> 1.7.5) @@ -36,6 +37,7 @@ PODS: - DTFoundation/Core - DTFoundation/UIKit (1.7.18): - DTFoundation/Core + - ffmpeg-kit-ios-audio (4.4) - FLEX (4.4.1) - FlowCommoniOS (1.10.0) - GBDeviceInfo (6.6.0): @@ -104,7 +106,7 @@ PODS: - BlueRSA (~> 1.0) - KituraContracts (~> 1.2) - LoggerAPI (~> 1.7) - - SwiftLint (0.43.0) + - SwiftLint (0.43.1) - SwiftyBeaver (1.9.5) - zxcvbn-ios (1.0.4) - ZXingObjC (3.6.5): @@ -113,6 +115,8 @@ PODS: DEPENDENCIES: - DGCollectionViewLeftAlignFlowLayout (~> 1.0.4) + - DSWaveformImage (~> 6.1.1) + - ffmpeg-kit-ios-audio (~> 4.4.LTS) - FLEX (~> 4.4.1) - FlowCommoniOS (~> 1.10.0) - GBDeviceInfo (~> 6.6.0) @@ -142,8 +146,10 @@ SPEC REPOS: - BlueRSA - DGCollectionViewLeftAlignFlowLayout - Down + - DSWaveformImage - DTCoreText - DTFoundation + - ffmpeg-kit-ios-audio - FLEX - FlowCommoniOS - GBDeviceInfo @@ -180,8 +186,10 @@ SPEC CHECKSUMS: BlueRSA: 6f9776d62d9773502415a7db3bcbb2bbb3f71fc3 DGCollectionViewLeftAlignFlowLayout: a0fa58797373ded039cafba8133e79373d048399 Down: b6ba1bc985c9d2f4e15e3b293d2207766fa12612 + DSWaveformImage: 3c718a0cf99291887ee70d1d0c18d80101d3d9ce DTCoreText: ec749e013f2e1f76de5e7c7634642e600a7467ce DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536 + ffmpeg-kit-ios-audio: ddfc3dac6f574e83d53f8ae33586711162685d3e FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab FlowCommoniOS: bcdf81a5f30717e711af08a8c812eb045411ba94 GBDeviceInfo: ed0db16230d2fa280e1cbb39a5a7f60f6946aaec @@ -206,11 +214,11 @@ SPEC CHECKSUMS: SwiftBase32: 9399c25a80666dc66b51e10076bf591e3bbb8f17 SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 SwiftJWT: 88c412708f58c169d431d344c87bc79a87c830ae - SwiftLint: 0c645fdc6feed3e390c1701ab3cc669f88b42752 + SwiftLint: 99f82d07b837b942dd563c668de129a03fc3fb52 SwiftyBeaver: 84069991dd5dca07d7069100985badaca7f0ce82 zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 6e4ab90565384faccee0cb985abe05663c36f517 +PODFILE CHECKSUM: c7386ecfb38fc4302613c915aef79eebdb98a53d COCOAPODS: 1.10.1 diff --git a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme index 84ecb908a..a9bea1d96 100644 --- a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme +++ b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme @@ -4,7 +4,8 @@ version = "1.3"> + buildImplicitDependencies = "YES" + runPostActionsOnFailure = "NO"> - + - + @@ -356,22 +356,6 @@ - - - - - - - - - - - - - - - - @@ -479,7 +463,6 @@ - @@ -560,25 +543,6 @@ - - - - - - - - - - - - - - - - - - - diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_delete.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_delete.imageset/Contents.json index bb4344e92..a255919c5 100644 --- a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_delete.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_delete.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark.png b/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark.png index cb0316cf9..e48bf546b 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark.png and b/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark@2x.png b/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark@2x.png index b58c78907..b35cce7f6 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark@2x.png and b/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark@3x.png b/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark@3x.png index b04f2a977..4c3eb73ea 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark@3x.png and b/Riot/Assets/Images.xcassets/Room/Input/upload_icon_dark.imageset/upload_icon_dark@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/Contents.json new file mode 100644 index 000000000..246bbbbf8 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "voice_message_cancel_gradient.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_cancel_gradient@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_cancel_gradient@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/voice_message_cancel_gradient.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/voice_message_cancel_gradient.png new file mode 100644 index 000000000..aad31deb7 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/voice_message_cancel_gradient.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/voice_message_cancel_gradient@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/voice_message_cancel_gradient@2x.png new file mode 100644 index 000000000..5d0595c22 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/voice_message_cancel_gradient@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/voice_message_cancel_gradient@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/voice_message_cancel_gradient@3x.png new file mode 100644 index 000000000..6c08777d3 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_cancel_gradient.imageset/voice_message_cancel_gradient@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/Contents.json new file mode 100644 index 000000000..6b40d80e4 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "voice_message_lock_chevron.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_lock_chevron@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_lock_chevron@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron.png new file mode 100644 index 000000000..332200ef8 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron@2x.png new file mode 100644 index 000000000..ff29e0808 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron@3x.png new file mode 100644 index 000000000..b9f5c04d5 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/Contents.json new file mode 100644 index 000000000..93de75077 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "voice_message_lock_icon_locked.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_lock_icon_locked@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_lock_icon_locked@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked.png new file mode 100644 index 000000000..35a6569c0 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked@2x.png new file mode 100644 index 000000000..615ab682d Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked@3x.png new file mode 100644 index 000000000..3108b7652 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/Contents.json new file mode 100644 index 000000000..43a311cf4 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "voice_message_lock_icon_unlocked.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_lock_icon_unlocked@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_lock_icon_unlocked@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked.png new file mode 100644 index 000000000..1a3c05594 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked@2x.png new file mode 100644 index 000000000..bcd1b2a44 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked@3x.png new file mode 100644 index 000000000..a2087f349 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/Contents.json new file mode 100644 index 000000000..bfc1545db --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "voice_message_pause_button.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_pause_button@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_pause_button@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button.png new file mode 100644 index 000000000..aa185b04b Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@2x.png new file mode 100644 index 000000000..321565890 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@3x.png new file mode 100644 index 000000000..93bdf2f28 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_pause_button.imageset/voice_message_pause_button@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/Contents.json new file mode 100644 index 000000000..3c88e866c --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "voice_message_play_button.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_play_button@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_play_button@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button.png new file mode 100644 index 000000000..274211f74 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button@2x.png new file mode 100644 index 000000000..0c8da2212 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button@3x.png new file mode 100644 index 000000000..22176d475 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_play_button.imageset/voice_message_play_button@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/Contents.json new file mode 100644 index 000000000..ead86edbb --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "action_voice_message.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "action_voice_message@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "action_voice_message@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/action_voice_message.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/action_voice_message.png new file mode 100644 index 000000000..b969cb3aa Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/action_voice_message.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/action_voice_message@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/action_voice_message@2x.png new file mode 100644 index 000000000..32c6236a6 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/action_voice_message@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/action_voice_message@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/action_voice_message@3x.png new file mode 100644 index 000000000..e8cc54c29 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/action_voice_message@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Contents.json new file mode 100644 index 000000000..900874ca1 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "voice_message_record_button_recording.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_record_button_recording@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_record_button_recording@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording.png new file mode 100644 index 000000000..8fa147c18 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@2x.png new file mode 100644 index 000000000..f00a46204 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@3x.png new file mode 100644 index 000000000..7fdf91c21 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/Contents.json new file mode 100644 index 000000000..521b5c2dc --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "voice_message_record_icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_record_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_record_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/voice_message_record_icon.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/voice_message_record_icon.png new file mode 100644 index 000000000..fb7a1660c Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/voice_message_record_icon.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/voice_message_record_icon@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/voice_message_record_icon@2x.png new file mode 100644 index 000000000..72830fabe Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/voice_message_record_icon@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/voice_message_record_icon@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/voice_message_record_icon@3x.png new file mode 100644 index 000000000..c792f8173 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_icon.imageset/voice_message_record_icon@3x.png differ diff --git a/Riot/Assets/bg.lproj/Vector.strings b/Riot/Assets/bg.lproj/Vector.strings index d9fa91f5a..04a669524 100644 --- a/Riot/Assets/bg.lproj/Vector.strings +++ b/Riot/Assets/bg.lproj/Vector.strings @@ -106,7 +106,6 @@ // Room recents "room_recents_directory_section" = "ДИРЕКТОРИЯ СЪС СТАИ"; "room_creation_invite_another_user" = "Търси потребител по ID, име, имейл"; -"room_recents_directory_section_network" = "Мрежа"; "room_recents_favourites_section" = "ЛЮБИМИ"; "room_recents_people_section" = "ХОРА"; "room_recents_conversations_section" = "СТАИ"; diff --git a/Riot/Assets/ca.lproj/Vector.strings b/Riot/Assets/ca.lproj/Vector.strings index f11645924..75c7c7fe2 100644 --- a/Riot/Assets/ca.lproj/Vector.strings +++ b/Riot/Assets/ca.lproj/Vector.strings @@ -106,7 +106,6 @@ "room_creation_invite_another_user" = "Cerca / convida per l'identificador d'usuari, nom o correu electrònic"; // Room recents "room_recents_directory_section" = "Directori de Sales"; -"room_recents_directory_section_network" = "Xarxa"; "room_recents_favourites_section" = "Favorits"; "room_recents_people_section" = "Contactes"; "room_recents_conversations_section" = "Sales"; diff --git a/Riot/Assets/cs.lproj/Localizable.strings b/Riot/Assets/cs.lproj/Localizable.strings new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Riot/Assets/cs.lproj/Localizable.strings @@ -0,0 +1 @@ + diff --git a/Riot/Assets/cs.lproj/Vector.strings b/Riot/Assets/cs.lproj/Vector.strings index ce499a826..23f79ca59 100644 --- a/Riot/Assets/cs.lproj/Vector.strings +++ b/Riot/Assets/cs.lproj/Vector.strings @@ -4,11 +4,11 @@ "title_people" = "Lidé"; "title_rooms" = "Místnosti"; "title_groups" = "Komunity"; -"warning" = "Varování!"; +"warning" = "Varování"; // Actions -"view" = "Zobrazit zdroj"; +"view" = "Zobrazit"; "next" = "Další"; -"back" = "Pro zabezpečení se při odhlašování odstraní všechny šifrovací klíče typu end-to-end, a i po přihlašení bude historie šifrovaných kanálů nečitelná.\nZvolte export, chcete-li je zálohovat, než se odhlásíte."; +"back" = "Zpět"; "continue" = "Pokračovat"; "create" = "Vytvořit"; "start" = "Spustit"; @@ -52,7 +52,7 @@ "auth_repeat_password_placeholder" = "Zopakovat heslo"; "auth_repeat_new_password_placeholder" = "Potvrďte své nové heslo"; "auth_home_server_placeholder" = "URL (např. https://matrix.org)"; -"auth_identity_server_placeholder" = "URL (např. https://matrix.org)"; +"auth_identity_server_placeholder" = "URL (např. https://vector.im)"; "auth_invalid_login_param" = "Nesprávné uživatelské jméno nebo heslo"; "auth_invalid_user_name" = "Uživatelské jméno může obsahovat pouze písmena, číslice, tečky, pomlčky a podtržítka"; "auth_invalid_password" = "Heslo je velmi krátké (min 6)"; @@ -130,7 +130,7 @@ // Directory "directory_cell_title" = "Procházet adresář"; "directory_cell_description" = "%tu místnosti"; -"directory_search_results_title" = "Procházet výsledky výhledávání v adresáři"; +"directory_search_results_title" = "Procházet výsledky v adresáři"; "directory_search_results" = "%tu výsledky nalezeny pro %@"; "directory_search_results_more_than" = ">%tu výsledky nalezeny pro %@"; "directory_searching_title" = "Vyhledávání v adresáři…"; @@ -139,7 +139,7 @@ "contacts_address_book_section" = "LOKÁLNÍ KONTAKTY"; "contacts_address_book_matrix_users_toggle" = "Pouze Matrix uživatelé"; "contacts_address_book_no_contact" = "Žádné lokální kontakty"; -"contacts_address_book_permission_denied" = "Nepovolil jste přístup aplikace Riot k místním kontaktům"; +"contacts_address_book_permission_denied" = "Nepovolil jste přístup aplikace Element k místním kontaktům"; "contacts_user_directory_section" = "UŽIVATELSKÝ ADRESÁŘ"; "contacts_user_directory_offline_section" = "UŽIVATELSKÝ ADRESÁŘ (offline)"; // Chat participants @@ -162,8 +162,8 @@ "room_participants_ago" = "před"; "room_participants_action_section_admin_tools" = "Nástroje pro správce"; "room_participants_action_section_direct_chats" = "Přímé chaty"; -"room_participants_action_section_devices" = "Zařízení"; -"room_participants_action_section_other" = "Další"; +"room_participants_action_section_devices" = "Relace"; +"room_participants_action_section_other" = "Možnosti"; "room_participants_action_invite" = "Pozvat"; "room_participants_action_leave" = "Odejít z místnosti"; "room_participants_action_remove" = "Odebrat z této místnosti"; @@ -178,7 +178,7 @@ "room_participants_action_start_video_call" = "Začít video hovor"; "room_participants_action_mention" = "Zmínka"; // Chat -"room_jump_to_first_unread" = "Přeskočit na první nepřečtenou zprávu"; +"room_jump_to_first_unread" = "Přeskočit na nepřečtenou zprávu"; "room_new_message_notification" = "%d nová zpráva"; "room_new_messages_notification" = "%d nových zpráv"; "room_one_user_is_typing" = "%@ právě píše…"; @@ -196,7 +196,7 @@ "room_delete_unsent_messages" = "Smazat neodeslané zprávy"; "room_event_action_copy" = "Kopírovat"; "room_event_action_quote" = "Citovat"; -"room_event_action_redact" = "Redigovat"; +"room_event_action_redact" = "Odstranit"; "room_event_action_more" = "Víc"; "room_event_action_share" = "Sdílet"; "room_event_action_permalink" = "Trvalý odkaz"; @@ -211,12 +211,12 @@ "room_event_action_view_encryption" = "Informace o šifrování"; "room_event_failed_to_send" = "Odeslání se nezdařilo"; // Unknown devices -"unknown_devices_alert_title" = "V místnosti jsou neznámá zařízení"; +"unknown_devices_alert_title" = "V místnosti jsou neznámé relace"; "unknown_devices_send_anyway" = "Přesto poslat"; "unknown_devices_call_anyway" = "Přesto zavolat"; "unknown_devices_answer_anyway" = "Přesto přijmout"; "unknown_devices_verify" = "Ověřit…"; -"unknown_devices_title" = "Neznámá zařízení"; +"unknown_devices_title" = "Neznámé relace"; // Room Title "room_title_new_room" = "Nová místnost"; "room_participants_invite_another_user" = "Hledat / Pozvat podle uživatelského ID, jména nebo emailu"; @@ -231,10 +231,117 @@ "settings_title" = "Nastavení"; "account_logout_all" = "Odhlásit všechny účty"; "settings_report_bug" = "Nahlásit chybu"; -"settings_config_home_server" = "Domácího serveru je %@"; +"settings_config_home_server" = "Domácí server je %@"; "settings_config_identity_server" = "Server identit je %@"; "settings_config_user_id" = "Přihlášen/a jako %@"; "auth_msisdn_validation_message" = "Odeslali jsme vám SMS aktivační kod. Prosím, zadejte jej níže."; "auth_msisdn_validation_error" = "Nelze ověřit telefonní číslo."; -"auth_reset_password_success_message" = "Vaše heslo bylo úspěšně resetováno.\n\nByl jste právě odhlášen na všech vašich zařízeních a nebudete vánm nadále zasíláno oznámení. Pro znovu povolení zasílání oznámení, přihlašte se znovu na každém zařízení."; +"auth_reset_password_success_message" = "Vaše heslo bylo úspěšně resetováno.\n\nByl jste právě odhlášen na všech vašich relací a nebudete vánm nadále zasíláno oznámení. Pro znovu povolení zasílání oznámení, přihlašte se znovu na každém zařízení."; "store_full_description" = "Element je novým typem komunikátoru a propojovací aplikace která:\n\n1. Dává vám kontrolu nad vaším soukromím\n2. Vás nechá komunikovat s kýmkoli v Matrix síti a dokonce mimo ni, díky integrace s aplikacemi jako je například Slack\n3. Vás chrání před reklamou, těžbou Vašich dat, nechráněnými přístupy nebo nezdokumentovanými fukncemi\n4. Zabezpečuje Vaši komunikaci pomocí koncového šifrování s distribuovaným ověřením ostatních\n\nElement se liší od ostatních komunikačních řešeních především tím, že je decentralizovaný a open-source\n\nElement vám umožňuje provozovat vlastní server anebo si vybrat nějakyý z veřejných, takže máte controlu nad vašimi konverzacemi a soukromím. Dává vám přístup do otevřené sítě, takže nejste odkázání jen ke komunikaci s ostatními uživateli Elementu. A je vysoce bezpečný.\n\nElement je toho všeho schopen díky svému operačnímu protokolu - Matrix, otevřeného standartu pro decentralizovanou komunikaci.\n\nElement vás nechává vybrat, kdo bude hostovat vaše konverzace. Přímo z aplikace si můžete vybrat několik rozdílných řešení:\n\n1. Účet zdarma na veřejném serveru matrix.org\n2. Vlastní hosting serveru na vlastním hardwaru\n3. Účet na přizpůsobeném serveru jednodýúchým přihlášením na hosting Element Matrix Services\n\nProč Element?\n\nVLASTNĚTE SVÁ DATA: Vy rozhodujete kde jsou vaše data a zprávy uchovávány. Svá data vlastníte a spravujete Vy, ne nějaká obří korporace, která o vás sbírá osobní data nebo poskytuje přístup dalším stranám.\n\nOTEVŘENÁ KOMUNIKACE A SPOLUPRÁCE: Máte možnost spojit se s kýmkoli v síti Matrix bez ohledu na jeho softwarové řešení, a dokonce se můžete připojit i na jiné komunikační protokoly, jako je Slack, IRC nebo XMPP (Jabber). Komunita podporuje i komunikátory jako Whatsapp, Telegram nebo iMessage.\n\nNEPROLOMITELNÉ ŠIFROVÁNÍ: Skutečné koncové šifrování (pouze přímí účastníci konverzace mají možnost rozšifrovat jejich zprávy) a pokročilé ověřování kontaktů.\n\nVŠESTRANNÉ KOMUNIKAČNÍ MOŽNOSTI: Textové zprávy, hlasové nebo videohovory, přenos souborů, sdílení obrazovky a mnoho dalších funkcí a možností pro implementaci. Vytvářejte místnosti a komunity a zůstaňte v kontaktu.\n\nKDEKOLI SE NACHÁZÍTE: Přístup k plně synchronizované historii konverzací máte kdekoli se nacházíte, ať už z aplikace anebo webového rozhraní na https://element.io/app."; +"user_verification_sessions_list_session_untrusted" = "Nedůvěryhodná"; +"user_verification_sessions_list_session_trusted" = "Důvěryhodná"; +"user_verification_sessions_list_table_title" = "Relace"; +"user_verification_sessions_list_information" = "Zprávy s tímto uživatelem v této místnosti jsou šifrovány end-to-end a nemohou je číst třetí strany."; +"user_verification_sessions_list_user_trust_level_unknown_title" = "Není známo"; +"user_verification_sessions_list_user_trust_level_warning_title" = "Varování"; + +// Sessions list + +"user_verification_sessions_list_user_trust_level_trusted_title" = "Důvěryhodný"; +"user_verification_start_additional_information" = "Abyste byli v bezpečí, dělejte to osobně nebo použijte jiný způsob komunikace."; +"user_verification_start_waiting_partner" = "Čekám na %@…"; +"user_verification_start_information_part2" = " kontrolou jednorázového kódu na obou zařízeních."; +"user_verification_start_information_part1" = "Pro větší bezpečnost ověřit "; + +// MARK: - User verification + +// Start + +"user_verification_start_verify_action" = "Spustit ověřování"; +"key_verification_user_title" = "Ověřit je"; +"room_participants_security_information_room_encrypted" = "Zprávy v této místnosti jsou šifrovány end-to-end.\n\nVaše zprávy jsou zabezpečeny zámky a jedinečnými klíči je můžete odemknout pouze vy a příjemce."; +"room_participants_security_information_room_not_encrypted" = "Zprávy v této místnosti nejsou šifrovány end-to-end."; +"room_participants_security_loading" = "Načítání…"; +"room_participants_action_security_status_warning" = "Varování"; +"room_participants_action_security_status_verify" = "Ověřit"; +"room_participants_action_security_status_verified" = "Ověřeno"; +"room_participants_action_section_security" = "Zabezpečení"; +"room_participants_action_ban" = "Vyhodit z této místnosti"; +"room_creation_error_invite_user_by_email_without_identity_server" = "Není nakonfigurován žádný server identity, takže nemůžete přidat účastníka pomocí e-mailu."; +"auth_softlogout_recover_encryption_keys" = "Přihlaste se a obnovte šifrovací klíče uložené výhradně v tomto zařízení. Potřebujete je ke čtení všech zabezpečených zpráv na jakémkoli zařízení."; +"auth_softlogout_reason" = "Váš správce domovského serveru (%1$@) vás odhlásil z vašeho účtu %2 @ (%3$@)."; +"auth_softlogout_signed_out" = "Jste odhlášeni"; +"auth_autodiscover_invalid_response" = "Neplatná odpověď na objevení domovského serveru"; +"auth_accept_policies" = "Přečtěte si a přijměte zásady tohoto domovského serveru:"; +"auth_add_email_and_phone_warning" = "Registrace pomocí e-mailu a telefonního čísla najednou ještě není podporována, dokud neexistuje rozhraní API. Zohledněno bude pouze telefonní číslo. Svůj e-mail můžete přidat do svého profilu v nastavení."; +"auth_reset_password_error_is_required" = "Není nakonfigurován žádný server identity: pro obnovení hesla přidejte jeden server z možností ."; +"auth_forgot_password_error_no_configured_identity_server" = "Není nakonfigurován žádný server identity: pro obnovení hesla jeden přidejte ."; +"settings_key_backup_button_delete" = "Smazat zálohu"; +"settings_key_backup_button_restore" = "Obnovit ze zálohy"; +"auth_login_single_sign_on" = "Přihlásit se"; + +// Accessibility +"accessibility_checkbox_label" = "zaškrtávací políčko"; +"callbar_only_single_active_group" = "Klepnutím se připojíte ke skupinovému hovoru (%@)"; +"callbar_return" = "Vrátit se"; +"callbar_only_multiple_paused" = "%@ pozastavené hovory"; +"callbar_only_single_paused" = "Pozastavený hovor"; +"callbar_active_and_multiple_paused" = "1 aktivní hovor (%@) · %@ pozastavené hovory"; +"callbar_active_and_single_paused" = "1 aktivní hovor (%@) · 1 pozastavený hovor"; + +// Call Bar +"callbar_only_single_active" = "Klepnutím se vrátíte k hovoru (%@)"; +"less" = "Méně"; +"more" = "Více"; +"switch" = "Přepnout"; +"joined" = "Připojil se"; +"skip" = "Přeskočit"; +"close" = "Zavřít"; +"store_promotional_text" = "Aplikace pro chat a spolupráci chránící soukromí v otevřené síti. Žádný sběr dat, žádná zadní vrátka a žádný přístup třetích stran."; +// String for App Store +"store_short_description" = "Zabezpečený decentralizovaný chat/VoIP"; +"e2e_key_backup_wrong_version_button_settings" = "Nastavení"; +"side_menu_action_settings" = "Nastavení"; +"room_details_photo" = "Obrázek místnosti"; +"room_details_settings" = "Nastavení"; +"room_details_integrations" = "Integrace"; +"room_details_search" = "Vyhledat místnost"; +"room_details_files" = "Nahrávání"; +"room_details_people" = "Členové"; +"room_details_title_for_dm" = "Podrobnosti"; +"room_avatar_view_accessibility_hint" = "Změnit avatar místnosti"; +"room_creation_appearance_picture" = "Obrázek chatu (volitelné)"; + +// Errors +"error_user_already_logged_in" = "Vypadá to, že se pokoušíte připojit k jinému domovskému serveru. Chcete se odhlásit?"; +"social_login_button_title_sign_up" = "Zaregistrovat se pomocí %@"; +"social_login_button_title_sign_in" = "Přihlásit se pomocí %@"; +"social_login_button_title_continue" = "Pokračovat s %@"; +"social_login_list_title_sign_up" = "nebo"; +"social_login_list_title_sign_in" = "nebo"; + +// Social login + +"social_login_list_title_continue" = "Pokračovat s"; +"auth_softlogout_clear_data_sign_out" = "Odhlásit se"; +"auth_softlogout_clear_data_sign_out_msg" = "Opravdu chcete vymazat všechny údaje aktuálně uložené v tomto zařízení? Chcete-li získat přístup k údajům a zprávám svého účtu, znovu se přihlaste."; +"auth_softlogout_clear_data_sign_out_title" = "Jste si jisti?"; +"auth_softlogout_clear_data_button" = "Vymazat všechny údaje"; +"auth_softlogout_clear_data_message_2" = "Vymažte, pokud jste s tímto zařízením skončili nebo se chcete přihlásit k jinému účtu."; +"auth_softlogout_clear_data_message_1" = "Varování: Vaše osobní údaje (včetně šifrovacích klíčů) jsou stále uloženy v tomto zařízení."; +"auth_softlogout_clear_data" = "Vymazat osobní údaje"; +"auth_softlogout_sign_in" = "Přihlásit se"; +"biometrics_setup_subtitle" = "Ušetřete si čas"; +"biometrics_setup_enable_button_title_x" = "Povolit %@"; +"biometrics_setup_title_x" = "Povolit %@"; +"biometrics_settings_enable_x" = "Povolit %@"; +"biometrics_mode_face_id" = "Face ID"; +"biometrics_cant_unlocked_alert_title" = "Aplikaci nelze odemknout"; +"biometrics_usage_reason" = "Pro přístup k vaší aplikaci je nutné ověření"; +"biometrics_desetup_disable_button_title_x" = "Zakázat %@"; +"biometrics_desetup_title_x" = "Zakázat %@"; +"public_room_section_title" = "Veřejné místnosti (v %@):"; +"homeserver_connection_lost" = "Nelze se připojit k domovskému serveru."; +"network_offline_prompt" = "Zdá se, že připojení k internetu je offline."; +"yesterday" = "Včera"; +"today" = "Dnes"; diff --git a/Riot/Assets/cy.lproj/Vector.strings b/Riot/Assets/cy.lproj/Vector.strings index e1524bbc8..868341391 100644 --- a/Riot/Assets/cy.lproj/Vector.strings +++ b/Riot/Assets/cy.lproj/Vector.strings @@ -130,7 +130,6 @@ "room_creation_error_invite_user_by_email_without_identity_server" = "Nid oes unrhyw weinydd adnabod wedi'i osod felly ni allwch ychwanegu cyfranogwr gydag e-bost."; // Room recents "room_recents_directory_section" = "CYFEIRIADUR YSTAFELLOEDD"; -"room_recents_directory_section_network" = "Rhwydwaith"; "room_recents_favourites_section" = "FFEFRYNAU"; "room_recents_people_section" = "POBL"; "room_recents_conversations_section" = "YSTAFELLOEDD"; diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 6cb67c557..df37ce7c2 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -79,7 +79,6 @@ "room_creation_wait_for_creation" = "Es wird gerade schon ein Raum erstellt. Bitte warten."; // Room recents "room_recents_directory_section" = "RAUM VERZEICHNIS"; -"room_recents_directory_section_network" = "Netzwerk"; "room_recents_favourites_section" = "FAVORITEN"; "room_recents_people_section" = "PERSONEN"; "room_recents_conversations_section" = "RÄUME"; diff --git a/Riot/Assets/en.lproj/InfoPlist.strings b/Riot/Assets/en.lproj/InfoPlist.strings index 6387f123d..468d42cce 100644 --- a/Riot/Assets/en.lproj/InfoPlist.strings +++ b/Riot/Assets/en.lproj/InfoPlist.strings @@ -17,7 +17,7 @@ // Permissions usage explanations "NSCameraUsageDescription" = "The camera is used to take photos and videos, make video calls."; "NSPhotoLibraryUsageDescription" = "The photo library is used to send photos and videos."; -"NSMicrophoneUsageDescription" = "The microphone is used to take videos, make calls."; +"NSMicrophoneUsageDescription" = "Element needs to access your microphone to make and receive calls, take videos, and record voice messages."; "NSContactsUsageDescription" = "To discover contacts already using Matrix, Element can send email addresses and phone numbers in your address book to your chosen Matrix identity server. Where supported, personal data is hashed before sending - please check your identity server's privacy policy for more details."; "NSCalendarsUsageDescription" = "See your scheduled meetings in the app."; "NSFaceIDUsageDescription" = "Face ID is used to access your app."; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 4954f8c88..01a6f2ef1 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -182,7 +182,6 @@ // Room recents "room_recents_directory_section" = "ROOM DIRECTORY"; -"room_recents_directory_section_network" = "Network"; "room_recents_favourites_section" = "FAVOURITES"; "room_recents_people_section" = "PEOPLE"; "room_recents_conversations_section" = "ROOMS"; @@ -534,6 +533,7 @@ Tap the + to start adding people."; "settings_labs_create_conference_with_jitsi" = "Create conference calls with jitsi"; "settings_labs_message_reaction" = "React to messages with emoji"; "settings_labs_enable_ringing_for_group_calls" = "Ring for group calls"; +"settings_labs_voice_messages" = "Voice messages"; "settings_version" = "Version %@"; "settings_olm_version" = "Olm Version %@"; @@ -1681,3 +1681,9 @@ Tap the + to start adding people."; "side_menu_action_help" = "Help"; "side_menu_action_feedback" = "Feedback"; "side_menu_app_version" = "Version %@"; + +// Mark: - Voice Messages + +"voice_message_release_to_send" = "Hold to record, release to send"; +"voice_message_remaining_recording_time" = "%@s left"; +"voice_message_stop_locked_mode_recording" = "Tap on your recording to stop or listen"; diff --git a/Riot/Assets/eo.lproj/Vector.strings b/Riot/Assets/eo.lproj/Vector.strings index 8a4575e64..bf7636ecb 100644 --- a/Riot/Assets/eo.lproj/Vector.strings +++ b/Riot/Assets/eo.lproj/Vector.strings @@ -244,7 +244,6 @@ "room_recents_server_notice_section" = "SISTEMAJ AVERTOJ"; "room_recents_low_priority_section" = "MALALTA PRIORITATO"; "room_recents_favourites_section" = "ELSTARIGITAJ"; -"room_recents_directory_section_network" = "Reto"; // Room recents "room_recents_directory_section" = "KATALOGO DE ĈAMBROJ"; diff --git a/Riot/Assets/es.lproj/Vector.strings b/Riot/Assets/es.lproj/Vector.strings index c14cdda6b..216722a92 100644 --- a/Riot/Assets/es.lproj/Vector.strings +++ b/Riot/Assets/es.lproj/Vector.strings @@ -144,7 +144,6 @@ "room_creation_invite_another_user" = "Buscar / invitar por ID de Usuario, Nombre o correo electrónico"; // Room recents "room_recents_directory_section" = "DIRECTORIO DE SALAS"; -"room_recents_directory_section_network" = "Red"; "room_recents_favourites_section" = "FAVORITOS"; "room_recents_people_section" = "PERSONAS"; "room_recents_conversations_section" = "SALAS"; diff --git a/Riot/Assets/et.lproj/InfoPlist.strings b/Riot/Assets/et.lproj/InfoPlist.strings index df3ae7fb3..238ddd486 100644 --- a/Riot/Assets/et.lproj/InfoPlist.strings +++ b/Riot/Assets/et.lproj/InfoPlist.strings @@ -1,7 +1,7 @@ // Permissions usage explanations "NSCameraUsageDescription" = "Kaameraga salvestatakse pilte ning videosid ja tehakse videokõnesid."; "NSPhotoLibraryUsageDescription" = "Fotogaleriid kasutatakse fotode ja videote saatmiseks teistele kasutajatele."; -"NSMicrophoneUsageDescription" = "Mikrofoni kasutatakse videote salvestamisel ning kõnede tegemisel."; +"NSMicrophoneUsageDescription" = "Kõnede tegemiseks, videote ja häälsõnumite salvestamiseks vajab Element ligipääsu sinu seadme mikrofonile."; "NSCalendarsUsageDescription" = "Vaata päevakavasse lisatud koosolekuid vastvast rakendusest."; "NSContactsUsageDescription" = "Selleks, et leida Matrixi võrgu kasutajaid, võib Element saata sinu aadressiraamatus leiduvad e-posti aadressid ja telefoninumbrid sinu valitud Matrixi isikutuvastusserverile. Kui server seda toetab, siis andmed muudetakse enne saatmist räsideks - täpsema teabe leiad oma isikutuvastusserveri privaatsuspoliitikast."; "NSFaceIDUsageDescription" = "Ligipääsuks sinu rakendusele on kasutusel Face ID."; diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index fb751e153..1349ade0d 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -218,7 +218,6 @@ "auth_login_single_sign_on" = "Logi sisse"; // Room recents "room_recents_directory_section" = "JUTUTUBADE LOEND"; -"room_recents_directory_section_network" = "Võrk"; "room_recents_favourites_section" = "LEMMIKUD"; "room_recents_people_section" = "INIMESED"; "room_recents_conversations_section" = "JUTUTOAD"; @@ -1097,7 +1096,7 @@ "deactivate_account_informations_part3" = "\n\nSinu konto kustutamine "; "deactivate_account_informations_part4_emphasize" = "vaikimisi ei tähenda, et unustatakse ka sinu saadetud sõnumid. "; "deactivate_account_informations_part5" = "Kui sa siiski soovid seda, siis palun tee märge alljärgnevasse kasti.\n\nMatrix'i sõnumite nähtavus on sarnane e-posti kirjadega. Sõnumite unustamine tegelikult tähendab seda, et sinu varemsaadetud sõnumeid ei jagata uute või veel registreerumata kasutajatega, kuid registeerunud kasutajad, kes juba on need sõnumid saanud, võivad neid ka jätkuvalt lugeda."; -"rerequest_keys_alert_message" = "Palun käivita Element mõnes muus seadmes, mis suudab neid sõnumeid dekrüptoda ja seega saata krüptovõtmeid siia sessiooni."; +"rerequest_keys_alert_message" = "Palun käivita Element mõnes muus seadmes, mis suudab neid sõnumeid dekrüptida ja seega saata krüptovõtmeid siia sessiooni."; "settings_discovery_three_pids_management_information_part1" = "Halda missuguse e-posti aadressi ja telefoninumbri alusel teised kasutajad saavad sind kutsuda jututubadesse. Lisa või eemalda e-posti aadresse ja telefoninumbreid sellest loendist "; "settings_discovery_three_pid_details_information_email" = "Halda selle e-posti aadressi eelistusi, mille alusel teised kasutajad saavad sind leida ja kutsuda jututubade liikmeks. E-posti aadresse lisada ja muuta saad kasutajakonto seadistustest."; "settings_discovery_three_pid_details_title_phone_number" = "Halda telefoninumbrit"; @@ -1362,3 +1361,10 @@ // Room Notification Settings "room_notifs_settings_notify_me_for" = "Teavita mind"; "room_details_notifs" = "Teavitused"; +"voice_message_stop_locked_mode_recording" = "Salvestuse peatamiseks ja taasesituseks vajuta lainekese nuppu"; +"voice_message_remaining_recording_time" = "salvestusaega jäänud %@s"; + +// Mark: - Voice Messages + +"voice_message_release_to_send" = "Salvestamiseks vajuta nuppu, saatmiseks lase nupp lahti"; +"settings_labs_voice_messages" = "Häälsõnumid"; diff --git a/Riot/Assets/eu.lproj/Vector.strings b/Riot/Assets/eu.lproj/Vector.strings index 38a6e4d34..9278d4a1d 100644 --- a/Riot/Assets/eu.lproj/Vector.strings +++ b/Riot/Assets/eu.lproj/Vector.strings @@ -238,7 +238,6 @@ "room_creation_invite_another_user" = "Bilatu / gonbidatu erabiltzaile ID-a, izena edo e-maila erabiliz"; // Room recents "room_recents_directory_section" = "GELEN DIREKTORIOA"; -"room_recents_directory_section_network" = "Sarea"; "room_recents_favourites_section" = "GOGOKOAK"; "room_recents_invites_section" = "GONBIDAPENAK"; // People tab diff --git a/Riot/Assets/fr.lproj/Vector.strings b/Riot/Assets/fr.lproj/Vector.strings index 8e250f532..2f632e10c 100644 --- a/Riot/Assets/fr.lproj/Vector.strings +++ b/Riot/Assets/fr.lproj/Vector.strings @@ -97,7 +97,6 @@ "room_creation_invite_another_user" = "Rechercher/inviter par identifiant, nom ou e-mail"; // Room recents "room_recents_directory_section" = "RÉPERTOIRE DES SALONS"; -"room_recents_directory_section_network" = "Réseau"; "room_recents_favourites_section" = "FAVORIS"; "room_recents_people_section" = "PERSONNES"; "room_recents_conversations_section" = "SALONS"; diff --git a/Riot/Assets/hu.lproj/InfoPlist.strings b/Riot/Assets/hu.lproj/InfoPlist.strings index ee665818e..f874c48d5 100644 --- a/Riot/Assets/hu.lproj/InfoPlist.strings +++ b/Riot/Assets/hu.lproj/InfoPlist.strings @@ -1,7 +1,7 @@ // Permissions usage explanations "NSCameraUsageDescription" = "A kamera fényképek, videók készítéséhez és videóhívásokhoz lesz használva."; "NSPhotoLibraryUsageDescription" = "A fénykép galéria fényképek és videók küldéséhez lesz használva."; -"NSMicrophoneUsageDescription" = "A mikrofon videók készítéséhez és hívásokhoz lesz használva."; +"NSMicrophoneUsageDescription" = "A hívás indításához és fogadásához, videó és hangüzenet felvételéhez az Elementnek hozzáférési engedélyre van szüksége a mikrofonhoz."; "NSContactsUsageDescription" = "Az olyan ismerősök felderítéséhez akik már használják a Matrixot, Elementet el tudja küldeni a címjegyzékben található e-mail címeket és telefonszámokat az általad választott Matrix azonosítási szervernek. Ahol lehetséges a személyes adatok hash-elve lesznek - kérlek ellenőrizd az azonosítási szervered adatvédelmi szabályait."; "NSCalendarsUsageDescription" = "Nézd meg a találkozóidat az alkalmazásban."; "NSFaceIDUsageDescription" = "Arc felismerés használata az alkalmazás eléréséhez."; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 718f7de9b..3e991b86a 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -108,7 +108,6 @@ "room_creation_invite_another_user" = "Keresés / meghívás felhasználói azonosítás, név vagy e-mail cím alapján"; // Room recents "room_recents_directory_section" = "SZOBA KÖNYVTÁR"; -"room_recents_directory_section_network" = "Hálózat"; "room_recents_favourites_section" = "KEDVENCEK"; "room_recents_people_section" = "EMBEREK"; "room_recents_conversations_section" = "SZOBÁK"; @@ -1412,3 +1411,23 @@ "settings_ui_theme_picker_message_invert_colours" = "„Autó” az eszközöd „invertált színek” beállítását használja"; "room_recents_unknown_room_error_message" = "A szoba nem található. Győződj meg róla, hogy létezik"; "room_creation_dm_error" = "Nem lehet elkészíteni a közvetlen beszélgetést. Ellenőrizd a meghívni kívánt felhasználót és próbáld újra."; +"key_verification_verify_qr_code_scan_code_other_device_action" = "Beolvasás ezzel az eszközzel"; +"room_notifs_settings_encrypted_room_notice" = "Megjegyzendő, hogy titkosított szobákban a megemlítésekre és kulcsszavakra való értesítés mobil eszközökön nem érhető el."; +"room_notifs_settings_account_settings" = "Fiók beállítások"; +"room_notifs_settings_manage_notifications" = "Értesítések kezelése itt: %@"; +"room_notifs_settings_cancel_action" = "Mégsem"; +"room_notifs_settings_done_action" = "Kész"; +"room_notifs_settings_none" = "Semmi"; +"room_notifs_settings_mentions_and_keywords" = "Csak megemlítések és kulcsszavak"; +"room_notifs_settings_all_messages" = "Minden üzenet"; + +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "Értesítés ezért:"; +"room_details_notifs" = "Értesítések"; +"voice_message_stop_locked_mode_recording" = "Megállításhoz és visszajátszáshoz koppints a hullámhosszra"; +"voice_message_remaining_recording_time" = "%@s távozott"; + +// Mark: - Voice Messages + +"voice_message_release_to_send" = "Felvételhez tartsd nyomva, a küldéshez engedd el"; +"settings_labs_voice_messages" = "Hang üzenetek"; diff --git a/Riot/Assets/is.lproj/Vector.strings b/Riot/Assets/is.lproj/Vector.strings index f6fcd78c3..bd4577fda 100644 --- a/Riot/Assets/is.lproj/Vector.strings +++ b/Riot/Assets/is.lproj/Vector.strings @@ -63,7 +63,6 @@ "room_creation_appearance_name" = "Heiti"; "room_creation_privacy" = "Meðferð persónuupplýsinga"; "room_creation_make_private" = "Gera einka"; -"room_recents_directory_section_network" = "Netkerfi"; "room_recents_favourites_section" = "Eftirlæti"; "room_recents_people_section" = "FÓLK"; "room_recents_conversations_section" = "SPJALLRÁSIR"; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 3a0fd2639..0a7d64e0e 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -112,7 +112,6 @@ "room_creation_invite_another_user" = "Cerca / invita per ID utente, nome o email"; // Room recents "room_recents_directory_section" = "ELENCO STANZE"; -"room_recents_directory_section_network" = "Rete"; "room_recents_favourites_section" = "PREFERITI"; "room_recents_people_section" = "CHAT DIRETTE"; "room_recents_conversations_section" = "STANZE"; diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index a77584240..6ccb42bc3 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -105,7 +105,6 @@ "room_creation_invite_another_user" = "ユーザID, 表示名, 電子メールで検索と招待"; // Room recents "room_recents_directory_section" = "ルーム一覧"; -"room_recents_directory_section_network" = "通信回線"; "room_recents_favourites_section" = "お気に入り"; "room_recents_people_section" = "対話"; "room_recents_conversations_section" = "ルーム"; diff --git a/Riot/Assets/kab.lproj/Vector.strings b/Riot/Assets/kab.lproj/Vector.strings index 812f4def9..1fa265a06 100644 --- a/Riot/Assets/kab.lproj/Vector.strings +++ b/Riot/Assets/kab.lproj/Vector.strings @@ -383,7 +383,6 @@ "room_recents_no_conversation" = "Ulac tixxamin"; "room_recents_conversations_section" = "TIXXAMIN"; "room_recents_people_section" = "IMDANEN"; -"room_recents_directory_section_network" = "Aẓeṭṭa"; "room_creation_make_private" = "Err-it d uslig"; "room_creation_privacy" = "Tabaḍnit"; "room_creation_appearance_name" = "Isem"; diff --git a/Riot/Assets/nb-NO.lproj/Vector.strings b/Riot/Assets/nb-NO.lproj/Vector.strings index 74f8d9e00..744f54033 100644 --- a/Riot/Assets/nb-NO.lproj/Vector.strings +++ b/Riot/Assets/nb-NO.lproj/Vector.strings @@ -64,7 +64,6 @@ "room_creation_appearance" = "Utseende"; "room_creation_appearance_name" = "Navn"; "room_creation_privacy" = "Personvern"; -"room_recents_directory_section_network" = "Nettverk"; "room_recents_create_empty_room" = "Opprett et rom"; "room_recents_join_room" = "Bli med i rommet"; "room_recents_join_room_title" = "Bli med i et rom"; diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings index 661b7a890..5c7a00cae 100644 --- a/Riot/Assets/nl.lproj/Vector.strings +++ b/Riot/Assets/nl.lproj/Vector.strings @@ -112,7 +112,6 @@ "room_creation_invite_another_user" = "Zoeken/uitnodigen met gebruikers-ID, naam of e-mailadres"; // Room recents "room_recents_directory_section" = "GESPREKSCATALOGUS"; -"room_recents_directory_section_network" = "Netwerk"; "room_recents_favourites_section" = "FAVORIETEN"; "room_recents_people_section" = "PERSONEN"; "room_recents_conversations_section" = "GESPREKKEN"; diff --git a/Riot/Assets/pl.lproj/Vector.strings b/Riot/Assets/pl.lproj/Vector.strings index 9e57955c2..9fc3c92a9 100644 --- a/Riot/Assets/pl.lproj/Vector.strings +++ b/Riot/Assets/pl.lproj/Vector.strings @@ -86,7 +86,6 @@ "room_creation_appearance" = "Wygląd"; "room_creation_appearance_name" = "Nazwa"; "room_creation_privacy" = "Prywatność"; -"room_recents_directory_section_network" = "Sieć"; "room_recents_favourites_section" = "ULUBIONE"; "room_recents_people_section" = "OSOBY"; "room_recents_conversations_section" = "POKOJE"; diff --git a/Riot/Assets/pt_BR.lproj/InfoPlist.strings b/Riot/Assets/pt_BR.lproj/InfoPlist.strings index 035cfaba2..6449e513c 100644 --- a/Riot/Assets/pt_BR.lproj/InfoPlist.strings +++ b/Riot/Assets/pt_BR.lproj/InfoPlist.strings @@ -1,7 +1,7 @@ // Permissions usage explanations "NSCameraUsageDescription" = "A câmera é usada para tirar fotos e vídeos, fazer chamadas de vídeo."; "NSPhotoLibraryUsageDescription" = "A biblioteca de fotos é usada para enviar fotos e vídeos."; -"NSMicrophoneUsageDescription" = "O microfone é usado para tirar vídeos, fazer chamadas."; +"NSMicrophoneUsageDescription" = "Element precisa acessar seu microfone para fazer e receber chamadas, tirar vídeos, e gravar mensagens de voz."; "NSContactsUsageDescription" = "Para descobrir contatos já usando Matrix, Element pode enviar endereços de email e números de telefone em seu livro de endereços para seu servidor de identidade Matrix escolhido. Onde suportado, dados pessoais são hashados antes do envio - por favor cheque a política de privacidade de seu servidor de identidade para mais detalhes."; "NSCalendarsUsageDescription" = "Ver suas reuniões agendadas no app."; "NSFaceIDUsageDescription" = "Face ID é usada para acessar seu app."; diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index 305fac89f..626dbaa96 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -101,7 +101,6 @@ "room_creation_make_private" = "Fazer privado"; "room_creation_wait_for_creation" = "Uma sala já está sendo criada. Por favor espere."; "room_creation_invite_another_user" = "Buscar / convidar por ID de usuária(o), Nome ou email"; -"room_recents_directory_section_network" = "Rede"; "room_recents_favourites_section" = "FAVORITOS"; "room_recents_people_section" = "PESSOAS"; "room_recents_conversations_section" = "SALAS"; @@ -1393,3 +1392,10 @@ // Room Notification Settings "room_notifs_settings_notify_me_for" = "Notifique-me para"; "room_details_notifs" = "Notificações"; +"voice_message_remaining_recording_time" = "%@s restando"; +"voice_message_stop_locked_mode_recording" = "Toque no comprimento de onda para parar e dar playback"; + +// Mark: - Voice Messages + +"voice_message_release_to_send" = "Segure para gravar, solte para enviar"; +"settings_labs_voice_messages" = "Mensagens de voz"; diff --git a/Riot/Assets/ru.lproj/Vector.strings b/Riot/Assets/ru.lproj/Vector.strings index 4b717f3f8..0ad6dbef6 100644 --- a/Riot/Assets/ru.lproj/Vector.strings +++ b/Riot/Assets/ru.lproj/Vector.strings @@ -98,7 +98,6 @@ "room_creation_invite_another_user" = "Поиск / приглашение по идентификатору пользователя, имени или адресу электронной почты"; // Room recents "room_recents_directory_section" = "КАТАЛОГ КОМНАТ"; -"room_recents_directory_section_network" = "Сеть"; "room_recents_favourites_section" = "ИЗБРАННЫЕ"; "room_recents_people_section" = "ЛЮДИ"; "room_recents_conversations_section" = "КОМНАТЫ"; diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings index aac94c32c..295c8a932 100644 --- a/Riot/Assets/sq.lproj/Vector.strings +++ b/Riot/Assets/sq.lproj/Vector.strings @@ -84,7 +84,6 @@ "room_creation_make_private" = "Bëje private"; // Room recents "room_recents_directory_section" = "DREJTORI DHOMASH"; -"room_recents_directory_section_network" = "Rrjet"; "room_recents_people_section" = "PERSONA"; "room_recents_conversations_section" = "DHOMA"; "room_recents_no_conversation" = "S’ka dhoma"; diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings index 14a8abd72..1bbec2ef6 100644 --- a/Riot/Assets/sv.lproj/Vector.strings +++ b/Riot/Assets/sv.lproj/Vector.strings @@ -100,7 +100,6 @@ "room_creation_make_private" = "Gör privat"; "room_creation_wait_for_creation" = "Ett rum håller redan på att skapas. Vänligen vänta."; "room_creation_invite_another_user" = "Sök / bjud in efter användar-ID, namn eller e-postadress"; -"room_recents_directory_section_network" = "Nätverk"; "room_recents_favourites_section" = "FAVORITER"; "room_recents_people_section" = "PERSONER"; "room_recents_conversations_section" = "RUM"; diff --git a/Riot/Assets/third_party_licenses.html b/Riot/Assets/third_party_licenses.html index 4dfc32ac7..6e491fdca 100644 --- a/Riot/Assets/third_party_licenses.html +++ b/Riot/Assets/third_party_licenses.html @@ -1688,7 +1688,203 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

- + +
  • + DSWaveformImage (https://github.com/dmrschmidt/DSWaveformImage) +

    + The MIT License (MIT) +

    + Copyright (c) 2013 Dennis Schmidt +

    + 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. +

    +
  • +
  • + ffmpeg-kit-ios-audio (https://github.com/tanersener/ffmpeg-kit) +

    +
    +                   GNU LESSER GENERAL PUBLIC LICENSE
    +                       Version 3, 29 June 2007
    +
    + Copyright (C) 2007 Free Software Foundation, Inc. 
    + Everyone is permitted to copy and distribute verbatim copies
    + of this license document, but changing it is not allowed.
    +
    +
    +  This version of the GNU Lesser General Public License incorporates
    +the terms and conditions of version 3 of the GNU General Public
    +License, supplemented by the additional permissions listed below.
    +
    +  0. Additional Definitions.
    +
    +  As used herein, "this License" refers to version 3 of the GNU Lesser
    +General Public License, and the "GNU GPL" refers to version 3 of the GNU
    +General Public License.
    +
    +  "The Library" refers to a covered work governed by this License,
    +other than an Application or a Combined Work as defined below.
    +
    +  An "Application" is any work that makes use of an interface provided
    +by the Library, but which is not otherwise based on the Library.
    +Defining a subclass of a class defined by the Library is deemed a mode
    +of using an interface provided by the Library.
    +
    +  A "Combined Work" is a work produced by combining or linking an
    +Application with the Library.  The particular version of the Library
    +with which the Combined Work was made is also called the "Linked
    +Version".
    +
    +  The "Minimal Corresponding Source" for a Combined Work means the
    +Corresponding Source for the Combined Work, excluding any source code
    +for portions of the Combined Work that, considered in isolation, are
    +based on the Application, and not on the Linked Version.
    +
    +  The "Corresponding Application Code" for a Combined Work means the
    +object code and/or source code for the Application, including any data
    +and utility programs needed for reproducing the Combined Work from the
    +Application, but excluding the System Libraries of the Combined Work.
    +
    +  1. Exception to Section 3 of the GNU GPL.
    +
    +  You may convey a covered work under sections 3 and 4 of this License
    +without being bound by section 3 of the GNU GPL.
    +
    +  2. Conveying Modified Versions.
    +
    +  If you modify a copy of the Library, and, in your modifications, a
    +facility refers to a function or data to be supplied by an Application
    +that uses the facility (other than as an argument passed when the
    +facility is invoked), then you may convey a copy of the modified
    +version:
    +
    +   a) under this License, provided that you make a good faith effort to
    +   ensure that, in the event an Application does not supply the
    +   function or data, the facility still operates, and performs
    +   whatever part of its purpose remains meaningful, or
    +
    +   b) under the GNU GPL, with none of the additional permissions of
    +   this License applicable to that copy.
    +
    +  3. Object Code Incorporating Material from Library Header Files.
    +
    +  The object code form of an Application may incorporate material from
    +a header file that is part of the Library.  You may convey such object
    +code under terms of your choice, provided that, if the incorporated
    +material is not limited to numerical parameters, data structure
    +layouts and accessors, or small macros, inline functions and templates
    +(ten or fewer lines in length), you do both of the following:
    +
    +   a) Give prominent notice with each copy of the object code that the
    +   Library is used in it and that the Library and its use are
    +   covered by this License.
    +
    +   b) Accompany the object code with a copy of the GNU GPL and this license
    +   document.
    +
    +  4. Combined Works.
    +
    +  You may convey a Combined Work under terms of your choice that,
    +taken together, effectively do not restrict modification of the
    +portions of the Library contained in the Combined Work and reverse
    +engineering for debugging such modifications, if you also do each of
    +the following:
    +
    +   a) Give prominent notice with each copy of the Combined Work that
    +   the Library is used in it and that the Library and its use are
    +   covered by this License.
    +
    +   b) Accompany the Combined Work with a copy of the GNU GPL and this license
    +   document.
    +
    +   c) For a Combined Work that displays copyright notices during
    +   execution, include the copyright notice for the Library among
    +   these notices, as well as a reference directing the user to the
    +   copies of the GNU GPL and this license document.
    +
    +   d) Do one of the following:
    +
    +       0) Convey the Minimal Corresponding Source under the terms of this
    +       License, and the Corresponding Application Code in a form
    +       suitable for, and under terms that permit, the user to
    +       recombine or relink the Application with a modified version of
    +       the Linked Version to produce a modified Combined Work, in the
    +       manner specified by section 6 of the GNU GPL for conveying
    +       Corresponding Source.
    +
    +       1) Use a suitable shared library mechanism for linking with the
    +       Library.  A suitable mechanism is one that (a) uses at run time
    +       a copy of the Library already present on the user's computer
    +       system, and (b) will operate properly with a modified version
    +       of the Library that is interface-compatible with the Linked
    +       Version.
    +
    +   e) Provide Installation Information, but only if you would otherwise
    +   be required to provide such information under section 6 of the
    +   GNU GPL, and only to the extent that such information is
    +   necessary to install and execute a modified version of the
    +   Combined Work produced by recombining or relinking the
    +   Application with a modified version of the Linked Version. (If
    +   you use option 4d0, the Installation Information must accompany
    +   the Minimal Corresponding Source and Corresponding Application
    +   Code. If you use option 4d1, you must provide the Installation
    +   Information in the manner specified by section 6 of the GNU GPL
    +   for conveying Corresponding Source.)
    +
    +  5. Combined Libraries.
    +
    +  You may place library facilities that are a work based on the
    +Library side by side in a single library together with other library
    +facilities that are not Applications and are not covered by this
    +License, and convey such a combined library under terms of your
    +choice, if you do both of the following:
    +
    +   a) Accompany the combined library with a copy of the same work based
    +   on the Library, uncombined with any other library facilities,
    +   conveyed under the terms of this License.
    +
    +   b) Give prominent notice with the combined library that part of it
    +   is a work based on the Library, and explaining where to find the
    +   accompanying uncombined form of the same work.
    +
    +  6. Revised Versions of the GNU Lesser General Public License.
    +
    +  The Free Software Foundation may publish revised and/or new versions
    +of the GNU Lesser General Public License from time to time. Such new
    +versions will be similar in spirit to the present version, but may
    +differ in detail to address new problems or concerns.
    +
    +  Each version is given a distinguishing version number. If the
    +Library as you received it specifies that a certain numbered version
    +of the GNU Lesser General Public License "or any later version"
    +applies to it, you have the option of following the terms and
    +conditions either of that published version or of any later version
    +published by the Free Software Foundation. If the Library as you
    +received it does not specify a version number of the GNU Lesser
    +General Public License, you may choose any version of the GNU Lesser
    +General Public License ever published by the Free Software Foundation.
    +
    +  If the Library as you received it specifies that a proxy can decide
    +whether future versions of the GNU Lesser General Public License shall
    +apply, that proxy's public statement of acceptance of any version is
    +permanent authorization for you to choose that version for the
    +Library.
    +        
    +
  • diff --git a/Riot/Assets/uk.lproj/InfoPlist.strings b/Riot/Assets/uk.lproj/InfoPlist.strings index ba2fc5b9d..6973b0ae6 100644 --- a/Riot/Assets/uk.lproj/InfoPlist.strings +++ b/Riot/Assets/uk.lproj/InfoPlist.strings @@ -1,7 +1,7 @@ // Permissions usage explanations "NSCameraUsageDescription" = "Камера використовується для знімків фото і відео, а також для відео-викликів."; "NSPhotoLibraryUsageDescription" = "Фотографії використовуються для надсилання фото і відео."; -"NSMicrophoneUsageDescription" = "Мікрофон використовується для відео і викликів."; +"NSMicrophoneUsageDescription" = "Element потребує доступу до вашого мікрофона, щоб здійснювати та отримувати виклики, знімати відео та записувати голосові повідомлення."; "NSContactsUsageDescription" = "Щоб показати, які з ваших контактів вже використовують Matrix, Element може надіслати адреси електронної пошти і номери телефонів з вашої адресної книги до вашого ідентифікаційного сервера Matrix. При наявності підтримки, перед надсиланням створюється хеш особистих даних. Для докладних відомостей ознайомтеся з політикою приватності свого ідентифікаційного сервера."; "NSCalendarsUsageDescription" = "Переглядайте свої заплановані зустрічі в додатку."; "NSFaceIDUsageDescription" = "Face ID використовується для доступу до вашого додатку."; diff --git a/Riot/Assets/vi.lproj/Vector.strings b/Riot/Assets/vi.lproj/Vector.strings index 9b2d328c4..88fc75f85 100644 --- a/Riot/Assets/vi.lproj/Vector.strings +++ b/Riot/Assets/vi.lproj/Vector.strings @@ -105,7 +105,6 @@ "room_creation_invite_another_user" = "Tìm / mời bằng ID người dùng, tên hoặc email"; // Room recents "room_recents_directory_section" = "DANH MỤC PHÒNG"; -"room_recents_directory_section_network" = "Mạng"; "room_recents_favourites_section" = "YÊU THÍCH"; "room_recents_people_section" = "DANH BẠ"; "room_recents_conversations_section" = "PHÒNG"; diff --git a/Riot/Assets/zh_Hans.lproj/InfoPlist.strings b/Riot/Assets/zh_Hans.lproj/InfoPlist.strings index b548711be..b4daf9c55 100644 --- a/Riot/Assets/zh_Hans.lproj/InfoPlist.strings +++ b/Riot/Assets/zh_Hans.lproj/InfoPlist.strings @@ -1,7 +1,7 @@ // Permissions usage explanations "NSCameraUsageDescription" = "摄像头权限用于拍摄照片、录制视频或进行视频聊天。"; "NSPhotoLibraryUsageDescription" = "照片库访问权限用于发送图片与视频。"; -"NSMicrophoneUsageDescription" = "麦克风权限用于录制视频或进行通话。"; +"NSMicrophoneUsageDescription" = "Element 需要访问您的麦克风才能拨打和接听电话、拍摄视频和录制语音消息。"; "NSContactsUsageDescription" = "为了发现已在使用 Matrix 的联系人,Element 可以把你地址簿里的邮箱地址和电话号码发送到你所选择的 Matrix 身份认证服务器。如果支持的话,个人数据在发送前会被哈希处理——请检查你的身份认证服务器的隐私政策以获取详细信息。"; "NSCalendarsUsageDescription" = "在此应用中查看你计划的会议。"; "NSFaceIDUsageDescription" = "Face ID 权限用于访问您的应用。"; diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index 43a665081..38f1729f1 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -96,7 +96,6 @@ "room_creation_invite_another_user" = "通过用户 ID、名称或电子邮件进行搜索/邀请"; // Room recents "room_recents_directory_section" = "聊天室目录"; -"room_recents_directory_section_network" = "网络"; "room_recents_favourites_section" = "收藏夹"; "room_recents_people_section" = "联系人"; "room_recents_conversations_section" = "聊天室"; @@ -1427,3 +1426,23 @@ "settings_ui_theme_picker_message_invert_colours" = "“自动”使用您设备的“反转颜色”设置"; "room_recents_unknown_room_error_message" = "找不到这个房间。 确保它存在"; "room_creation_dm_error" = "我们无法创建您的 DM。 请检查您要邀请的用户,然后重试。"; +"voice_message_stop_locked_mode_recording" = "轻按波长停止和回放消息"; +"voice_message_remaining_recording_time" = "剩 %@s"; + +// Mark: - Voice Messages + +"voice_message_release_to_send" = "按住录音,松开发送"; +"key_verification_verify_qr_code_scan_code_other_device_action" = "用这部设备扫描"; +"room_notifs_settings_encrypted_room_notice" = "请注意,移动设备上的加密聊天室不提供提及和关键字通知。"; +"room_notifs_settings_account_settings" = "账户设置"; +"room_notifs_settings_manage_notifications" = "你可以管理 %@ 中的消息"; +"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 Notification Settings +"room_notifs_settings_notify_me_for" = "通知内容"; +"room_details_notifs" = "通知"; +"settings_labs_voice_messages" = "语音消息"; diff --git a/Riot/Assets/zh_Hant.lproj/Vector.strings b/Riot/Assets/zh_Hant.lproj/Vector.strings index 685722293..de6fbf1ab 100644 --- a/Riot/Assets/zh_Hant.lproj/Vector.strings +++ b/Riot/Assets/zh_Hant.lproj/Vector.strings @@ -184,7 +184,6 @@ "room_creation_invite_another_user" = "透過使用者ID、名稱、電子郵件地址來搜尋/邀請"; // Room recents "room_recents_directory_section" = "聊天室目錄"; -"room_recents_directory_section_network" = "網路"; "room_recents_favourites_section" = "收藏夾"; "room_recents_people_section" = "聯絡人"; "room_recents_conversations_section" = "聊天室"; diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m index a85ea69be..11a0fa5a1 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m @@ -397,7 +397,7 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = } // Move this view in front - [self.contentView bringSubviewToFront:self.bubbleOverlayContainer]; + [self.bubbleOverlayContainer.superview bringSubviewToFront:self.bubbleOverlayContainer]; } else { diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 2cec2c63b..709ed216d 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -135,6 +135,15 @@ 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 voiceMessageCancelGradient = ImageAsset(name: "voice_message_cancel_gradient") + internal static let voiceMessageLockChevron = ImageAsset(name: "voice_message_lock_chevron") + internal static let voiceMessageLockIconLocked = ImageAsset(name: "voice_message_lock_icon_locked") + internal static let voiceMessageLockIconUnlocked = ImageAsset(name: "voice_message_lock_icon_unlocked") + internal static let voiceMessagePauseButton = ImageAsset(name: "voice_message_pause_button") + internal static let voiceMessagePlayButton = ImageAsset(name: "voice_message_play_button") + internal static let voiceMessageRecordButtonDefault = ImageAsset(name: "voice_message_record_button_default") + internal static let voiceMessageRecordButtonRecording = ImageAsset(name: "voice_message_record_button_recording") + internal static let voiceMessageRecordIcon = ImageAsset(name: "voice_message_record_icon") internal static let addMemberFloatingAction = ImageAsset(name: "add_member_floating_action") internal static let addParticipant = ImageAsset(name: "add_participant") internal static let addParticipants = ImageAsset(name: "add_participants") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 3168d5ad4..7f2cb1930 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -3358,10 +3358,6 @@ internal enum VectorL10n { internal static var roomRecentsDirectorySection: String { return VectorL10n.tr("Vector", "room_recents_directory_section") } - /// Network - internal static var roomRecentsDirectorySectionNetwork: String { - return VectorL10n.tr("Vector", "room_recents_directory_section_network") - } /// FAVOURITES internal static var roomRecentsFavouritesSection: String { return VectorL10n.tr("Vector", "room_recents_favourites_section") @@ -4378,6 +4374,10 @@ internal enum VectorL10n { internal static var settingsLabsMessageReaction: String { return VectorL10n.tr("Vector", "settings_labs_message_reaction") } + /// Voice messages + internal static var settingsLabsVoiceMessages: String { + return VectorL10n.tr("Vector", "settings_labs_voice_messages") + } /// Mark all messages as read internal static var settingsMarkAllAsRead: String { return VectorL10n.tr("Vector", "settings_mark_all_as_read") @@ -4882,6 +4882,18 @@ internal enum VectorL10n { internal static var voice: String { return VectorL10n.tr("Vector", "voice") } + /// Hold to record, release to send + internal static var voiceMessageReleaseToSend: String { + return VectorL10n.tr("Vector", "voice_message_release_to_send") + } + /// %@s left + internal static func voiceMessageRemainingRecordingTime(_ p1: String) -> String { + return VectorL10n.tr("Vector", "voice_message_remaining_recording_time", p1) + } + /// Tap on your recording to stop or listen + internal static var voiceMessageStopLockedModeRecording: String { + return VectorL10n.tr("Vector", "voice_message_stop_locked_mode_recording") + } /// Warning internal static var warning: String { return VectorL10n.tr("Vector", "warning") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 5a6817aca..7d41bc4df 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -52,6 +52,7 @@ final class RiotSettings: NSObject { static let roomCreationScreenRoomIsPublic = "roomCreationScreenRoomIsPublic" static let allowInviteExernalUsers = "allowInviteExernalUsers" static let enableRingingForGroupCalls = "enableRingingForGroupCalls" + static let enableVoiceMessages = "enableVoiceMessages" static let roomSettingsScreenShowLowPriorityOption = "roomSettingsScreenShowLowPriorityOption" static let roomSettingsScreenShowDirectChatOption = "roomSettingsScreenShowDirectChatOption" static let roomSettingsScreenAllowChangingAccessSettings = "roomSettingsScreenAllowChangingAccessSettings" @@ -93,6 +94,11 @@ final class RiotSettings: NSObject { return userDefaults }() + private override init() { + super.init() + defaults.register(defaults: [UserDefaultsKeys.enableVoiceMessages: BuildSettings.voiceMessagesEnabled]) + } + // MARK: Servers var homeserverUrlString: String { @@ -215,6 +221,14 @@ final class RiotSettings: NSObject { } } + var enableVoiceMessages: Bool { + get { + return defaults.bool(forKey: UserDefaultsKeys.enableVoiceMessages) + } set { + defaults.set(newValue, forKey: UserDefaultsKeys.enableVoiceMessages) + } + } + // MARK: Calls /// Indicate if `allowStunServerFallback` settings has been set once. diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 32010f9bb..a6df6cb48 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1854,16 +1854,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [account addObserver:self forKeyPath:@"enableInAppNotifications" options:0 context:nil]; } - // Load the local contacts on first account creation. - if ([MXKAccountManager sharedManager].accounts.count == 1) - { - dispatch_async(dispatch_get_main_queue(), ^{ - - [self refreshLocalContacts]; - - }); - } - [self.delegate legacyAppDelegate:self didAddAccount:account]; }]; @@ -1976,14 +1966,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // during this blocking task. dispatch_after(dispatch_walltime(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ [[MXKContactManager sharedManager] addMatrixSession:mxSession]; - - // Load the local contacts on first account - if ([MXKAccountManager sharedManager].accounts.count == 1) - { - dispatch_async(dispatch_get_main_queue(), ^{ - [self refreshLocalContacts]; - }); - } }); // Register the session to the widgets manager @@ -2939,54 +2921,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni }]; } -- (void)refreshLocalContacts -{ - if (!BuildSettings.allowLocalContactsAccess) - { - return; - } - - // Do not scan local contacts in background if the user has not decided yet about using - // an identity server - BOOL doRefreshLocalContacts = NO; - for (MXSession *session in mxSessionArray) - { - if (session.hasAccountDataIdentityServerValue) - { - doRefreshLocalContacts = YES; - break; - } - } - - // Check whether the application is allowed to access the local contacts. - if (doRefreshLocalContacts - && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) - { - // Check the user permission for syncing local contacts. This permission was handled independently on previous application version. - if (![MXKAppSettings standardAppSettings].syncLocalContacts) - { - // Check whether it was not requested yet. - if (![MXKAppSettings standardAppSettings].syncLocalContactsPermissionRequested) - { - [MXKAppSettings standardAppSettings].syncLocalContactsPermissionRequested = YES; - - [MXKContactManager requestUserConfirmationForLocalContactsSyncInViewController:self.presentedViewController completionHandler:^(BOOL granted) { - - if (granted) - { - // Allow local contacts sync in order to discover matrix users. - [MXKAppSettings standardAppSettings].syncLocalContacts = YES; - } - - }]; - } - } - - // Refresh the local contacts list. - [[MXKContactManager sharedManager] refreshLocalContacts]; - } -} - #pragma mark - Matrix Groups handling - (void)showGroup:(MXGroup*)group withMatrixSession:(MXSession*)mxSession @@ -4202,18 +4136,16 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [[NSUserDefaults standardUserDefaults] registerDefaults:defaults]; + // Migrates old UserDefaults values if showDecryptedContentInNotifications hasn't been set if (!RiotSettings.shared.isUserDefaultsMigrated) { [RiotSettings.shared migrate]; } - // Now use RiotSettings and NSUserDefaults to store `showDecryptedContentInNotifications` setting option - // Migrate this information from main MXKAccount to RiotSettings, if value is not in UserDefaults - + // Show encrypted message notification content by default. if (!RiotSettings.shared.isShowDecryptedContentInNotificationsHasBeenSetOnce) { - MXKAccount *currentAccount = [MXKAccountManager sharedManager].activeAccounts.firstObject; - RiotSettings.shared.showDecryptedContentInNotifications = currentAccount.showDecryptedContentInNotifications; + RiotSettings.shared.showDecryptedContentInNotifications = BuildSettings.decryptNotificationsByDefault; } } diff --git a/Riot/Modules/Authentication/AuthenticationViewController.m b/Riot/Modules/Authentication/AuthenticationViewController.m index 4d7e65964..ad064f196 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/AuthenticationViewController.m @@ -354,8 +354,6 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; { [_keyboardAvoider stopAvoiding]; - [self.authenticationActivityIndicator removeObserver:self forKeyPath:@"hidden"]; - [super viewDidDisappear:animated]; } @@ -384,6 +382,8 @@ static const CGFloat kAuthInputContainerViewMinHeightConstraintConstant = 150.0; [[NSNotificationCenter defaultCenter] removeObserver:universalLinkDidChangeNotificationObserver]; universalLinkDidChangeNotificationObserver = nil; } + + [self.authenticationActivityIndicator removeObserver:self forKeyPath:@"hidden"]; autoDiscovery = nil; _keyVerificationCoordinatorBridgePresenter = nil; diff --git a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m index 3857f61fc..123d9da69 100644 --- a/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m +++ b/Riot/Modules/Common/Recents/DataSources/RecentsDataSource.m @@ -19,7 +19,6 @@ #import "RecentCellData.h" #import "SectionHeaderView.h" -#import "DirectorySectionHeaderContainerView.h" #import "ThemeService.h" @@ -37,7 +36,6 @@ #define RECENTSDATASOURCE_SECTION_PEOPLE 0x40 #define RECENTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT 30.0 -#define RECENTSDATASOURCE_DIRECTORY_SECTION_HEADER_HEIGHT 65.0 NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSourceTapOnDirectoryServerChange"; @@ -48,10 +46,6 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou NSInteger shrinkedSectionsBitMask; - DirectorySectionHeaderContainerView *directorySectionContainer; - UILabel *networkLabel; - UILabel *directoryServerLabel; - NSMutableDictionary *roomTagsListenerByUserId; // Timer to not refresh publicRoomsDirectoryDataSource on every keystroke. @@ -77,17 +71,9 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou processingQueue = dispatch_queue_create("RecentsDataSource", DISPATCH_QUEUE_SERIAL); _crossSigningBannerDisplay = CrossSigningBannerDisplayNone; - crossSigningBannerSection = -1; - _secureBackupBannerDisplay = SecureBackupBannerDisplayNone; - secureBackupBannerSection = -1; - directorySection = -1; - invitesSection = -1; - favoritesSection = -1; - peopleSection = -1; - conversationSection = -1; - lowPrioritySection = -1; - serverNoticeSection = -1; + + [self resetSectionIndexes]; _areSectionsShrinkable = NO; shrinkedSectionsBitMask = 0; @@ -102,6 +88,19 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou return self; } +- (void)resetSectionIndexes +{ + crossSigningBannerSection = -1; + secureBackupBannerSection = -1; + directorySection = -1; + invitesSection = -1; + favoritesSection = -1; + peopleSection = -1; + conversationSection = -1; + lowPrioritySection = -1; + serverNoticeSection = -1; +} + #pragma mark - Properties @@ -451,7 +450,7 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou // Check whether all data sources are ready before rendering recents if (self.state == MXKDataSourceStateReady) { - crossSigningBannerSection = secureBackupBannerSection = directorySection = favoritesSection = peopleSection = conversationSection = lowPrioritySection = invitesSection = serverNoticeSection = -1; + [self resetSectionIndexes]; if (self.crossSigningBannerDisplay != CrossSigningBannerDisplayNone) { @@ -483,13 +482,6 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou conversationSection = sectionsCount++; } - if (_recentsDataSourceMode == RecentsDataSourceModeRooms - && BuildSettings.publicRoomsShowDirectory) - { - // Add the directory section after "ROOMS" - directorySection = sectionsCount++; - } - if (self.lowPriorityCellDataArray.count > 0) { lowPrioritySection = sectionsCount++; @@ -572,12 +564,6 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou { return 0.0; } - else if (section == directorySection - && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_DIRECTORY) - && BuildSettings.publicRoomsAllowServerChange) - { - return RECENTSDATASOURCE_DIRECTORY_SECTION_HEADER_HEIGHT; - } return RECENTSDATASOURCE_DEFAULT_SECTION_HEADER_HEIGHT; } @@ -823,57 +809,6 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou [sectionHeader addSubview:headerLabel]; sectionHeader.headerLabel = headerLabel; - if (section == directorySection - && _recentsDataSourceMode == RecentsDataSourceModeRooms - && !(shrinkedSectionsBitMask & RECENTSDATASOURCE_SECTION_DIRECTORY) - && BuildSettings.publicRoomsAllowServerChange) - { - if (!directorySectionContainer) - { - directorySectionContainer = [[DirectorySectionHeaderContainerView alloc] initWithFrame:CGRectZero]; - directorySectionContainer.backgroundColor = [UIColor clearColor]; - - // Add the "Network" label at the left - networkLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 30)]; - networkLabel.font = [UIFont systemFontOfSize:16.0]; - networkLabel.text = NSLocalizedStringFromTable(@"room_recents_directory_section_network", @"Vector", nil); - [directorySectionContainer addSubview:networkLabel]; - directorySectionContainer.networkLabel = networkLabel; - - // Add label for selected directory server - directoryServerLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0, 30)]; - directoryServerLabel.font = [UIFont systemFontOfSize:16.0]; - directoryServerLabel.textAlignment = NSTextAlignmentRight; - [directorySectionContainer addSubview:directoryServerLabel]; - directorySectionContainer.directoryServerLabel = directoryServerLabel; - - // Chevron - UIImageView *chevronImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 6, 12)]; - chevronImageView.contentMode = UIViewContentModeScaleAspectFit; - chevronImageView.image = [UIImage imageNamed:@"disclosure_icon"]; - chevronImageView.tintColor = ThemeService.shared.theme.textSecondaryColor; - [directorySectionContainer addSubview:chevronImageView]; - directorySectionContainer.disclosureView = chevronImageView; - - // Set a tap listener on all the container - UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onDirectoryServerPickerTap:)]; - [tapGesture setNumberOfTouchesRequired:1]; - [tapGesture setNumberOfTapsRequired:1]; - [directorySectionContainer addGestureRecognizer:tapGesture]; - } - - // Apply the current UI theme. - networkLabel.textColor = ThemeService.shared.theme.textPrimaryColor; - directoryServerLabel.textColor = ThemeService.shared.theme.textSecondaryColor; - - // Set the current directory server name - directoryServerLabel.text = _publicRoomsDirectoryDataSource.directoryServerDisplayname; - - // Add the check box container - [sectionHeader addSubview:directorySectionContainer]; - sectionHeader.bottomView = directorySectionContainer; - } - return sectionHeader; } @@ -1189,10 +1124,8 @@ NSString *const kRecentsDataSourceTapOnDirectoryServerChange = @"kRecentsDataSou #pragma mark - MXKDataSourceDelegate -- (void)refreshRoomsSection:(void (^)(void))onComplete; +- (void)refreshRoomsSection:(void (^)(void))onComplete { - secureBackupBannerSection = directorySection = favoritesSection = peopleSection = conversationSection = lowPrioritySection = serverNoticeSection = invitesSection = -1; - if (displayedRecentsDataSourceArray.count > 0) { // FIXME manage multi accounts diff --git a/Riot/Modules/Contacts/ContactsTableViewController.m b/Riot/Modules/Contacts/ContactsTableViewController.m index 5437cd149..c9af2e4c5 100644 --- a/Riot/Modules/Contacts/ContactsTableViewController.m +++ b/Riot/Modules/Contacts/ContactsTableViewController.m @@ -174,6 +174,15 @@ [self refreshContactsTable]; } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + // Load the local contacts for display. + // In viewDidAppear as it may trigger a request for contacts access. + [self refreshLocalContacts]; +} + - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; @@ -212,6 +221,54 @@ } } +- (void)refreshLocalContacts +{ + if (!BuildSettings.allowLocalContactsAccess) + { + return; + } + + // Do not scan local contacts in background if the user has not decided yet about using + // an identity server + BOOL doRefreshLocalContacts = NO; + for (MXSession *session in self.mxSessions) + { + if (session.hasAccountDataIdentityServerValue) + { + doRefreshLocalContacts = YES; + break; + } + } + + // Check whether the application is allowed to access the local contacts. + if (doRefreshLocalContacts + && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized) + { + // Check the user permission for syncing local contacts. This permission was handled independently on previous application version. + if (![MXKAppSettings standardAppSettings].syncLocalContacts) + { + // Check whether it was not requested yet. + if (![MXKAppSettings standardAppSettings].syncLocalContactsPermissionRequested) + { + [MXKAppSettings standardAppSettings].syncLocalContactsPermissionRequested = YES; + + [MXKContactManager requestUserConfirmationForLocalContactsSyncInViewController:self completionHandler:^(BOOL granted) { + + if (granted) + { + // Allow local contacts sync in order to discover matrix users. + [MXKAppSettings standardAppSettings].syncLocalContacts = YES; + } + + }]; + } + } + + // Refresh the local contacts list. + [[MXKContactManager sharedManager] refreshLocalContacts]; + } +} + - (void)refreshContactsTable { [self.contactsTableView reloadData]; diff --git a/Riot/Modules/GlobalSearch/Files/Views/FilesSearchTableViewCell.m b/Riot/Modules/GlobalSearch/Files/Views/FilesSearchTableViewCell.m index b853631cd..4dfbe686f 100644 --- a/Riot/Modules/GlobalSearch/Files/Views/FilesSearchTableViewCell.m +++ b/Riot/Modules/GlobalSearch/Files/Views/FilesSearchTableViewCell.m @@ -112,6 +112,9 @@ case MXKAttachmentTypeAudio: image = [UIImage imageNamed:@"file_music_icon"]; break; + case MXKAttachmentTypeVoiceMessage: + image = [UIImage imageNamed:@"file_music_icon"]; + break; case MXKAttachmentTypeVideo: image = [UIImage imageNamed:@"file_video_icon"]; break; diff --git a/Riot/Modules/People/PeopleViewController.h b/Riot/Modules/People/PeopleViewController.h index 8cc3607a4..696ebbb5b 100644 --- a/Riot/Modules/People/PeopleViewController.h +++ b/Riot/Modules/People/PeopleViewController.h @@ -20,7 +20,7 @@ /** 'PeopleViewController' instance is used to display/filter the direct rooms and a list of contacts. */ -@interface PeopleViewController : RecentsViewController +@interface PeopleViewController : RecentsViewController + (instancetype)instantiate; diff --git a/Riot/Modules/People/PeopleViewController.m b/Riot/Modules/People/PeopleViewController.m index 5985ce346..26b0ca3f0 100644 --- a/Riot/Modules/People/PeopleViewController.m +++ b/Riot/Modules/People/PeopleViewController.m @@ -14,7 +14,6 @@ limitations under the License. */ -#import #import "PeopleViewController.h" #import "UIViewController+RiotSearch.h" @@ -25,17 +24,11 @@ #import "RecentTableViewCell.h" #import "InviteRecentTableViewCell.h" -#import "ContactTableViewCell.h" - #import "Riot-Swift.h" @interface PeopleViewController () { NSInteger directRoomsSectionNumber; - - ContactsDataSource *contactsDataSource; - NSInteger contactsSectionNumber; - RecentsDataSource *recentsDataSource; } @@ -55,7 +48,6 @@ [super finalizeInit]; directRoomsSectionNumber = 0; - contactsSectionNumber = 0; self.screenName = @"People"; } @@ -76,14 +68,6 @@ plusButtonImageView = [self vc_addFABWithImage:[UIImage imageNamed:@"people_floating_action"] target:self action:@selector(onPlusButtonPressed)]; - - // Register table view cell for contacts. - [self.recentsTableView registerClass:ContactTableViewCell.class forCellReuseIdentifier:ContactTableViewCell.defaultReuseIdentifier]; - - // Change the table data source. It must be the people view controller itself. - self.recentsTableView.dataSource = self; - - self.enableStickyHeaders = YES; } - (void)didReceiveMemoryWarning @@ -92,302 +76,36 @@ // Dispose of any resources that can be recreated. } -- (void)destroy -{ - contactsDataSource.delegate = nil; - [contactsDataSource destroy]; - contactsDataSource = nil; - - [super destroy]; -} - - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - if (BuildSettings.allowLocalContactsAccess) - { - // Check whether the access to the local contacts has not been already asked - // and check that the user has decided to use or not to use an identity server - if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusNotDetermined - || !contactsDataSource.mxSession.hasAccountDataIdentityServerValue) - { - // Allow by default the local contacts sync in order to discover matrix users. - // This setting change will trigger the loading of the local contacts, which will automatically - // ask user permission to access their local contacts. - [MXKAppSettings standardAppSettings].syncLocalContacts = YES; - } - - // Refresh the local contacts list. - [[MXKContactManager sharedManager] refreshLocalContacts]; - } [AppDelegate theDelegate].masterTabBarController.navigationItem.title = NSLocalizedStringFromTable(@"title_people", @"Vector", nil); [AppDelegate theDelegate].masterTabBarController.tabBar.tintColor = ThemeService.shared.theme.tintColor; - if (recentsDataSource) + if ([self.dataSource isKindOfClass:RecentsDataSource.class]) { // Take the lead on the shared data source. + recentsDataSource = (RecentsDataSource*)self.dataSource; recentsDataSource.areSectionsShrinkable = NO; [recentsDataSource setDelegate:self andRecentsDataSourceMode:RecentsDataSourceModePeople]; } } -#pragma mark - - -- (void)displayList:(MXKRecentsDataSource *)listDataSource -{ - [super displayList:listDataSource]; - - // Change the table data source. It must be the people view controller itself. - self.recentsTableView.dataSource = self; - - // Keep a ref on the recents data source - if ([listDataSource isKindOfClass:RecentsDataSource.class]) - { - recentsDataSource = (RecentsDataSource*)listDataSource; - } - - if (BuildSettings.allowLocalContactsAccess) - { - if (!contactsDataSource) - { - // Prepare its contacts data source - contactsDataSource = [[ContactsDataSource alloc] initWithMatrixSession:listDataSource.mxSession]; - contactsDataSource.contactCellAccessoryImage = [[UIImage imageNamed: @"disclosure_icon"] vc_tintedImageUsingColor:ThemeService.shared.theme.textSecondaryColor]; - contactsDataSource.delegate = self; - } - } -} - -#pragma mark - MXKDataSourceDelegate - -- (Class)cellViewClassForCellData:(MXKCellData*)cellData -{ - if ([cellData isKindOfClass:MXKContact.class]) - { - return ContactTableViewCell.class; - } - - return [super cellViewClassForCellData:cellData]; -} - -#pragma mark - UITableView data source - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - // Retrieve the current number of sections related to the direct rooms. - // Sanity check: check whether the recents data source is correctly configured. - directRoomsSectionNumber = 0; - - if (recentsDataSource.recentsDataSourceMode == RecentsDataSourceModePeople) - { - directRoomsSectionNumber = [self.dataSource numberOfSectionsInTableView:self.recentsTableView]; - } - - // Retrieve the current number of sections related to the contacts - contactsSectionNumber = [contactsDataSource numberOfSectionsInTableView:self.recentsTableView]; - - return (directRoomsSectionNumber + contactsSectionNumber); -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - NSInteger count = 0; - - if (section < directRoomsSectionNumber) - { - count = [self.dataSource tableView:tableView numberOfRowsInSection:section]; - } - else - { - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - count = [contactsDataSource tableView:tableView numberOfRowsInSection:section]; - } - } - - return count; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - NSInteger section = indexPath.section; - - if (section < directRoomsSectionNumber) - { - return [self.dataSource tableView:tableView cellForRowAtIndexPath:indexPath]; - } - else - { - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - return [contactsDataSource tableView:tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:section]]; - } - } - - // Return a fake cell to prevent app from crashing. - return [[UITableViewCell alloc] init]; -} - -- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath -{ - NSInteger section = indexPath.section; - - if (section < directRoomsSectionNumber) - { - return [self.dataSource tableView:tableView canEditRowAtIndexPath:indexPath]; - } - else - { - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - return [contactsDataSource tableView:tableView canEditRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:section]]; - } - } - - return NO; -} - #pragma mark - UITableView delegate - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { - if (section >= directRoomsSectionNumber) - { - // Let the contact dataSource provide the height of the section header. - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - return [contactsDataSource heightForHeaderInSection:section]; - } - else - { - return 0.0; - } - } - - return [super tableView:tableView heightForHeaderInSection:section]; + return 0.0; } - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { - if (section >= directRoomsSectionNumber) - { - // Let the contact dataSource provide the section header. - CGRect frame = [tableView rectForHeaderInSection:section]; - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - UIView *sectionHeader = [contactsDataSource viewForHeaderInSection:section withFrame:frame]; - sectionHeader.tag = section + directRoomsSectionNumber; - - if (self.enableStickyHeaders) - { - while (sectionHeader.gestureRecognizers.count) - { - UIGestureRecognizer *gestureRecognizer = sectionHeader.gestureRecognizers.lastObject; - [sectionHeader removeGestureRecognizer:gestureRecognizer]; - } - - // Handle tap gesture - UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapOnSectionHeader:)]; - [tap setNumberOfTouchesRequired:1]; - [tap setNumberOfTapsRequired:1]; - [sectionHeader addGestureRecognizer:tap]; - } - - return sectionHeader; - } - else - { - return nil; - } - } - - return [super tableView:tableView viewForHeaderInSection:section]; -} - -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath -{ - NSInteger section = indexPath.section; - if (section >= directRoomsSectionNumber) - { - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - if ([contactsDataSource contactAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:section]]) - { - // Return the default height of the contact cell - return 74.0; - } - - return 50; - } - else - { - return 0.0; - } - } - - return [super tableView:tableView heightForRowAtIndexPath:indexPath]; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - NSInteger section = indexPath.section; - if (section >= directRoomsSectionNumber) - { - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - MXKContact *mxkContact = [contactsDataSource contactAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:section]]; - - if (mxkContact) - { - [[AppDelegate theDelegate].masterTabBarController selectContact:mxkContact]; - - // Keep selected the cell by default. - return; - } - } - else - { - [tableView deselectRowAtIndexPath:indexPath animated:NO]; - return; - } - } - - return [super tableView:tableView didSelectRowAtIndexPath:indexPath]; + return nil; } #pragma mark - Override RecentsViewController -- (UIView *)tableView:(UITableView *)tableView viewForStickyHeaderInSection:(NSInteger)section -{ - CGRect frame = [tableView rectForHeaderInSection:section]; - frame.size.height = self.stickyHeaderHeight; - - if (section >= directRoomsSectionNumber) - { - // Let the contact dataSource provide this header. - section -= directRoomsSectionNumber; - if (section < contactsSectionNumber) - { - return [contactsDataSource viewForStickyHeaderInSection:section withFrame:frame]; - } - } - else if (recentsDataSource) - { - return [recentsDataSource viewForStickyHeaderInSection:section withFrame:frame]; - } - - return nil; -} - - (void)refreshCurrentSelectedCell:(BOOL)forceVisible { // Check whether the recents data source is correctly configured. @@ -396,41 +114,7 @@ return; } - // Update here the index of the current selected cell (if any) - Useful in landscape mode with split view controller. - NSIndexPath *currentSelectedCellIndexPath = nil; - MasterTabBarController *masterTabBarController = [AppDelegate theDelegate].masterTabBarController; - if (masterTabBarController.currentContactDetailViewController) - { - // Look for the rank of this selected contact - currentSelectedCellIndexPath = [contactsDataSource cellIndexPathWithContact:masterTabBarController.selectedContact]; - - if (currentSelectedCellIndexPath) - { - // Select the right row - currentSelectedCellIndexPath = [NSIndexPath indexPathForRow:currentSelectedCellIndexPath.row inSection:(directRoomsSectionNumber + currentSelectedCellIndexPath.section)]; - [self.recentsTableView selectRowAtIndexPath:currentSelectedCellIndexPath animated:YES scrollPosition:UITableViewScrollPositionNone]; - - if (forceVisible) - { - // Scroll table view to make the selected row appear at second position - NSInteger topCellIndexPathRow = currentSelectedCellIndexPath.row ? currentSelectedCellIndexPath.row - 1: currentSelectedCellIndexPath.row; - NSIndexPath* indexPath = [NSIndexPath indexPathForRow:topCellIndexPathRow inSection:currentSelectedCellIndexPath.section]; - [self.recentsTableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:NO]; - } - } - else - { - NSIndexPath *indexPath = [self.recentsTableView indexPathForSelectedRow]; - if (indexPath) - { - [self.recentsTableView deselectRowAtIndexPath:indexPath animated:NO]; - } - } - } - else - { - [super refreshCurrentSelectedCell:forceVisible]; - } + [super refreshCurrentSelectedCell:forceVisible]; } - (void)onPlusButtonPressed @@ -449,24 +133,6 @@ } } -#pragma mark - UISearchBarDelegate - -- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText -{ - // Apply filter on contact source - [contactsDataSource searchWithPattern:searchText forceReset:NO]; - - [super searchBar:searchBar textDidChange:searchText]; -} - -- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar -{ - // Reset filtering - [contactsDataSource searchWithPattern:nil forceReset:NO]; - - [super searchBarCancelButtonClicked:searchBar]; -} - #pragma mark - Empty view management - (void)updateEmptyView @@ -503,35 +169,7 @@ - (NSUInteger)totalItemCounts { return recentsDataSource.invitesCellDataArray.count - + recentsDataSource.conversationCellDataArray.count - + recentsDataSource.peopleCellDataArray.count - + [self numberOfContactsInContactsDataSource]; -} - -- (NSUInteger)numberOfContactsInContactsDataSource -{ - BOOL areLocalContactsAccessAuthorized = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized; - - NSInteger nbOfItemsInContactDataSource = 0; - - for (NSInteger i = 0; i < contactsSectionNumber; i++) - { - nbOfItemsInContactDataSource += [contactsDataSource tableView:self.recentsTableView numberOfRowsInSection:i]; - } - - NSInteger numberOfContactsInContactsDataSource; - - // No local contacts to show and no search in directory - if (!areLocalContactsAccessAuthorized && contactsSectionNumber == 1 && nbOfItemsInContactDataSource <= 1) - { - numberOfContactsInContactsDataSource = 0; - } - else - { - numberOfContactsInContactsDataSource = nbOfItemsInContactDataSource; - } - - return numberOfContactsInContactsDataSource; + + recentsDataSource.conversationCellDataArray.count; } @end diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 423b84426..2a216c44f 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -986,6 +986,9 @@ static NSAttributedString *timestampVerticalWhitespace = nil; case MXKAttachmentTypeAudio: accessibilityLabel = NSLocalizedStringFromTable(@"media_type_accessibility_audio", @"Vector", nil); break; + case MXKAttachmentTypeVoiceMessage: + accessibilityLabel = NSLocalizedStringFromTable(@"media_type_accessibility_audio", @"Vector", nil); + break; case MXKAttachmentTypeVideo: accessibilityLabel = NSLocalizedStringFromTable(@"media_type_accessibility_video", @"Vector", nil); break; diff --git a/Riot/Modules/Room/NotificationSettings/RoomNotificationsSettingsService.swift b/Riot/Modules/Room/NotificationSettings/RoomNotificationsSettingsService.swift index 1bd0e4b6b..b62c7ea60 100644 --- a/Riot/Modules/Room/NotificationSettings/RoomNotificationsSettingsService.swift +++ b/Riot/Modules/Room/NotificationSettings/RoomNotificationsSettingsService.swift @@ -264,11 +264,17 @@ fileprivate extension MXRoom { } var overridePushRule: MXPushRule? { - getRoomRule(from: mxSession.notificationCenter.rules.global.override) + guard let overrideRules = mxSession.notificationCenter.rules.global.override else { + return nil + } + return getRoomRule(from: overrideRules) } var roomPushRule: MXPushRule? { - getRoomRule(from: mxSession.notificationCenter.rules.global.room) + guard let roomRules = mxSession.notificationCenter.rules.global.room else { + return nil + } + return getRoomRule(from: roomRules) } var notificationState: RoomNotificationState { diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift index 495d3468b..569803fbf 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift @@ -194,7 +194,8 @@ extension RoomInfoCoordinator: RoomInfoListCoordinatorDelegate { extension RoomInfoCoordinator: RoomParticipantsViewControllerDelegate { func roomParticipantsViewController(_ roomParticipantsViewController: RoomParticipantsViewController!, mention member: MXRoomMember!) { - + self.navigationRouter.popToRootModule(animated: true) + self.delegate?.roomInfoCoordinator(self, didRequestMentionForMember: member) } } diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift index 44a46cbd0..48dc70d3e 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorBridgePresenter.swift @@ -20,6 +20,7 @@ import Foundation @objc protocol RoomInfoCoordinatorBridgePresenterDelegate { func roomInfoCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter) + func roomInfoCoordinatorBridgePresenter(_ coordinatorBridgePresenter: RoomInfoCoordinatorBridgePresenter, didRequestMentionForMember member: MXRoomMember) } /// RoomInfoCoordinatorBridgePresenter enables to start RoomInfoCoordinator from a view controller. @@ -115,6 +116,10 @@ extension RoomInfoCoordinatorBridgePresenter: RoomInfoCoordinatorDelegate { self.delegate?.roomInfoCoordinatorBridgePresenterDelegateDidComplete(self) } + func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didRequestMentionForMember member: MXRoomMember) { + self.delegate?.roomInfoCoordinatorBridgePresenter(self, didRequestMentionForMember: member) + } + } // MARK: - UIAdaptivePresentationControllerDelegate diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift index ff2e04626..092a3b874 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinatorType.swift @@ -20,6 +20,7 @@ import Foundation protocol RoomInfoCoordinatorDelegate: AnyObject { func roomInfoCoordinatorDidComplete(_ coordinator: RoomInfoCoordinatorType) + func roomInfoCoordinator(_ coordinator: RoomInfoCoordinatorType, didRequestMentionForMember member: MXRoomMember) } /// `RoomInfoCoordinatorType` is a protocol describing a Coordinator that handle keybackup setup navigation flow. diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index fbd8a25f0..56542e5cc 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -135,7 +135,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @interface RoomViewController () + RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate> { // The preview header @@ -240,6 +240,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, getter=isActivitiesViewExpanded) BOOL activitiesViewExpanded; @property (nonatomic, getter=isScrollToBottomHidden) BOOL scrollToBottomHidden; +@property (nonatomic, strong) VoiceMessageController *voiceMessageController; + @end @implementation RoomViewController @@ -313,6 +315,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Show / hide actions button in document preview according BuildSettings self.allowActionsInDocumentPreview = BuildSettings.messageDetailsAllowShare; + + _voiceMessageController = [[VoiceMessageController alloc] initWithThemeService:ThemeService.shared mediaServiceProvider:VoiceMessageMediaServiceProvider.sharedProvider]; + self.voiceMessageController.delegate = self; } - (void)viewDidLoad @@ -386,6 +391,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self.bubblesTableView registerNib:RoomTypingBubbleCell.nib forCellReuseIdentifier:RoomTypingBubbleCell.defaultReuseIdentifier]; + [self.bubblesTableView registerClass:VoiceMessageBubbleCell.class forCellReuseIdentifier:VoiceMessageBubbleCell.defaultReuseIdentifier]; + [self.bubblesTableView registerClass:VoiceMessageWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceMessageWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [self.bubblesTableView registerClass:VoiceMessageWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceMessageWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + [self vc_removeBackTitle]; [self setupRemoveJitsiWidgetRemoveView]; @@ -607,6 +616,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.roomDataSource.showReadMarker = YES; self.updateRoomReadMarker = NO; isAppeared = NO; + + [VoiceMessageMediaServiceProvider.sharedProvider stopAllServices]; } - (void)viewDidAppear:(BOOL)animated @@ -1114,6 +1125,13 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; if (!self.inputToolbarView || ![self.inputToolbarView isMemberOfClass:roomInputToolbarViewClass]) { [super setRoomInputToolbarViewClass:roomInputToolbarViewClass]; + + // The voice message toolbar cannot be set on DisabledInputToolbarView. + if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) + { + [(RoomInputToolbarView *)self.inputToolbarView setVoiceMessageToolbarView:self.voiceMessageController.voiceMessageToolbarView]; + } + [self updateInputToolBarViewHeight]; } } @@ -2359,6 +2377,16 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { cellViewClass = RoomGroupCallStatusBubbleCell.class; } + else if (bubbleData.attachment.type == MXKAttachmentTypeVoiceMessage) + { + if (bubbleData.isPaginationFirstBubble) { + cellViewClass = VoiceMessageWithPaginationTitleBubbleCell.class; + } else if (bubbleData.shouldHideSenderInformation) { + cellViewClass = VoiceMessageWithoutSenderInfoBubbleCell.class; + } else { + cellViewClass = VoiceMessageBubbleCell.class; + } + } else if (bubbleData.isIncoming) { if (bubbleData.isAttachmentWithThumbnail) @@ -2718,12 +2746,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [actionIdentifier isEqualToString:RoomGroupCallStatusBubbleCell.answerAction]) { MXWeakify(self); - NSString *appDisplayName = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"]; // Check app permissions first [MXKTools checkAccessForCall:YES - manualChangeMessageForAudio:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"microphone_access_not_granted_for_call"], appDisplayName] - manualChangeMessageForVideo:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"camera_access_not_granted_for_call"], appDisplayName] + manualChangeMessageForAudio:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"microphone_access_not_granted_for_call"], AppInfo.current.displayName] + manualChangeMessageForVideo:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"camera_access_not_granted_for_call"], AppInfo.current.displayName] showPopUpInViewController:self completionHandler:^(BOOL granted) { MXStrongifyAndReturnIfNil(self); @@ -3715,12 +3742,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { __weak __typeof(self) weakSelf = self; - NSString *appDisplayName = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"]; - // Check app permissions first [MXKTools checkAccessForCall:video - manualChangeMessageForAudio:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"microphone_access_not_granted_for_call"], appDisplayName] - manualChangeMessageForVideo:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"camera_access_not_granted_for_call"], appDisplayName] + manualChangeMessageForAudio:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"microphone_access_not_granted_for_call"], AppInfo.current.displayName] + manualChangeMessageForVideo:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"camera_access_not_granted_for_call"], AppInfo.current.displayName] showPopUpInViewController:self completionHandler:^(BOOL granted) { if (weakSelf) @@ -3936,13 +3961,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self cancelEventSelection]; } -#pragma mark - RoomParticipantsViewControllerDelegate - -- (void)roomParticipantsViewController:(RoomParticipantsViewController *)roomParticipantsViewController mention:(MXRoomMember*)member -{ - [self mention:member]; -} - #pragma mark - MXKRoomMemberDetailsViewControllerDelegate - (void)roomMemberDetailsViewController:(MXKRoomMemberDetailsViewController *)roomMemberDetailsViewController startChatWithMemberId:(NSString *)matrixId completion:(void (^)(void))completion @@ -5810,7 +5828,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; MXWeakify(self); RoomContextualMenuItem *replyMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionReply]; - replyMenuItem.isEnabled = [self.roomDataSource canReplyToEventWithId:event.eventId]; + replyMenuItem.isEnabled = [self.roomDataSource canReplyToEventWithId:event.eventId] && !self.voiceMessageController.isRecordingAudio; replyMenuItem.action = ^{ MXStrongifyAndReturnIfNil(self); @@ -6121,6 +6139,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.roomInfoCoordinatorBridgePresenter = nil; } +- (void)roomInfoCoordinatorBridgePresenter:(RoomInfoCoordinatorBridgePresenter *)coordinatorBridgePresenter didRequestMentionForMember:(MXRoomMember *)member +{ + [self mention:member]; +} + #pragma mark - RemoveJitsiWidgetViewDelegate - (void)removeJitsiWidgetViewDidCompleteSliding:(RemoveJitsiWidgetView *)view @@ -6154,4 +6177,32 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]; } +#pragma mark - VoiceMessageControllerDelegate + +- (void)voiceMessageControllerDidRequestMicrophonePermission:(VoiceMessageController *)voiceMessageController +{ + NSString *message = [NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"microphone_access_not_granted_for_voice_message"], AppInfo.current.displayName]; + + [MXKTools checkAccessForMediaType:AVMediaTypeAudio + manualChangeMessage: message + showPopUpInViewController:self completionHandler:^(BOOL granted) { + + }]; +} + +- (void)voiceMessageController:(VoiceMessageController *)voiceMessageController + didRequestSendForFileAtURL:(NSURL *)url + duration:(NSUInteger)duration + samples:(NSArray *)samples + completion:(void (^)(BOOL))completion +{ + [self.roomDataSource sendVoiceMessage:url mimeType:nil duration:duration samples:samples success:^(NSString *eventId) { + MXLogDebug(@"Success with event id %@", eventId); + completion(YES); + } failure:^(NSError *error) { + MXLogError(@"Failed sending voice message"); + completion(NO); + }]; +} + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift new file mode 100644 index 000000000..397fc9fe7 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift @@ -0,0 +1,55 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class VoiceMessageBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable { + + private var playbackController: VoiceMessagePlaybackController! + + override func render(_ cellData: MXKCellData!) { + super.render(cellData) + + guard let data = cellData as? RoomBubbleCellData else { + return + } + + guard data.attachment.type == MXKAttachmentTypeVoiceMessage else { + fatalError("Invalid attachment type passed to a voice message cell.") + } + + if playbackController.attachment != data.attachment { + playbackController.attachment = data.attachment + } + } + + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.backgroundColor = .clear + bubbleCellContentView?.showSenderInfo = true + bubbleCellContentView?.showPaginationTitle = false + + guard let contentView = bubbleCellContentView?.innerContentView else { + return + } + + playbackController = VoiceMessagePlaybackController(mediaServiceProvider: VoiceMessageMediaServiceProvider.sharedProvider, + cacheManager: VoiceMessageAttachmentCacheManager.sharedManager) + + contentView.vc_addSubViewMatchingParent(playbackController.playbackView) + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageWithPaginationTitleBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageWithPaginationTitleBubbleCell.swift new file mode 100644 index 000000000..b7e516675 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageWithPaginationTitleBubbleCell.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 VoiceMessageWithPaginationTitleBubbleCell: VoiceMessageBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showPaginationTitle = true + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageWithoutSenderInfoBubbleCell.swift new file mode 100644 index 000000000..cc091b39d --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageWithoutSenderInfoBubbleCell.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 VoiceMessageWithoutSenderInfoBubbleCell: VoiceMessageBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showSenderInfo = false + } +} diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h index b2ad883b8..430daa2bf 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h @@ -58,7 +58,6 @@ typedef enum : NSUInteger @property (weak, nonatomic) IBOutlet NSLayoutConstraint *mainToolbarMinHeightConstraint; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *mainToolbarHeightConstraint; -@property (weak, nonatomic) IBOutlet NSLayoutConstraint *messageComposerContainerLeadingConstraint; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *messageComposerContainerTrailingConstraint; @property (weak, nonatomic) IBOutlet UIButton *attachMediaButton; @@ -70,6 +69,7 @@ typedef enum : NSUInteger @property (weak, nonatomic) IBOutlet UILabel *inputContextLabel; @property (weak, nonatomic) IBOutlet UIButton *inputContextButton; @property (weak, nonatomic) IBOutlet RoomActionsBar *actionsBar; +@property (weak, nonatomic) UIView *voiceMessageToolbarView; /** Tell whether the filled data will be sent encrypted. NO by default. diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 26a2f4c69..2234cbf9a 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -34,6 +34,7 @@ const CGFloat kActionMenuAttachButtonSpringVelocity = 7; const CGFloat kActionMenuAttachButtonSpringDamping = .45; const NSTimeInterval kActionMenuContentAlphaAnimationDuration = .2; const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; +const CGFloat kComposerContainerTrailingPadding = 12; @interface RoomInputToolbarView() { @@ -75,6 +76,24 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; [self.rightInputToolbarButton setTitle:nil forState:UIControlStateHighlighted]; self.isEncryptionEnabled = _isEncryptionEnabled; + + [self updateUIWithTextMessage:nil animated:NO]; +} + +- (void)setVoiceMessageToolbarView:(UIView *)voiceMessageToolbarView +{ + if (RiotSettings.shared.enableVoiceMessages == NO) { + return; + } + + _voiceMessageToolbarView = voiceMessageToolbarView; + self.voiceMessageToolbarView.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:self.voiceMessageToolbarView]; + + [NSLayoutConstraint activateConstraints:@[[self.mainToolbarView.topAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.topAnchor], + [self.mainToolbarView.leftAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.leftAnchor], + [self.mainToolbarView.bottomAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.bottomAnchor], + [self.mainToolbarView.rightAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.rightAnchor]]]; } #pragma mark - Override MXKView @@ -133,7 +152,7 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; - (void)setTextMessage:(NSString *)textMessage { - [self updateSendButtonWithMessage:textMessage]; + [self updateUIWithTextMessage:textMessage animated:YES]; [super setTextMessage:textMessage]; } @@ -290,7 +309,7 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; - (BOOL)growingTextView:(HPGrowingTextView *)growingTextView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { NSString *newText = [growingTextView.text stringByReplacingCharactersInRange:range withString:text]; - [self updateSendButtonWithMessage:newText]; + [self updateUIWithTextMessage:newText animated:YES]; return YES; } @@ -354,24 +373,6 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; [super destroy]; } -- (void)updateSendButtonWithMessage:(NSString *)textMessage -{ - self.actionMenuOpened = NO; - - if (textMessage.length) - { - self.rightInputToolbarButton.alpha = 1; - self.messageComposerContainerTrailingConstraint.constant = self.frame.size.width - self.rightInputToolbarButton.frame.origin.x + 12; - } - else - { - self.rightInputToolbarButton.alpha = 0; - self.messageComposerContainerTrailingConstraint.constant = 12; - } - - [self layoutIfNeeded]; -} - #pragma mark - properties - (void)setActionMenuOpened:(BOOL)actionMenuOpened @@ -406,6 +407,10 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; [UIView animateWithDuration:kActionMenuContentAlphaAnimationDuration delay:_actionMenuOpened ? 0 : .1 options:UIViewAnimationOptionCurveEaseIn animations:^{ self->messageComposerContainer.alpha = actionMenuOpened ? 0 : 1; self.rightInputToolbarButton.alpha = self->growingTextView.text.length == 0 || actionMenuOpened ? 0 : 1; + if (RiotSettings.shared.enableVoiceMessages) + { + self.voiceMessageToolbarView.alpha = self->growingTextView.text.length > 0 || actionMenuOpened ? 0 : 1; + } } completion:nil]; [UIView animateWithDuration:kActionMenuComposerHeightAnimationDuration animations:^{ @@ -432,4 +437,25 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3; [super paste:sender]; } +#pragma mark - Private + +- (void)updateUIWithTextMessage:(NSString *)textMessage animated:(BOOL)animated +{ + self.actionMenuOpened = NO; + + if (RiotSettings.shared.enableVoiceMessages == NO) { + self.rightInputToolbarButton.alpha = textMessage.length ? 1.0f : 0.0f; + self.messageComposerContainerTrailingConstraint.constant = (textMessage.length ? self.frame.size.width - self.rightInputToolbarButton.frame.origin.x : 0.0f) + kComposerContainerTrailingPadding; + + [self layoutIfNeeded]; + + return; + } + + [UIView animateWithDuration:(animated ? 0.15f : 0.0f) animations:^{ + self.rightInputToolbarButton.alpha = textMessage.length ? 1.0f : 0.0f; + self.voiceMessageToolbarView.alpha = textMessage.length ? 0.0f : 1.0; + }]; +} + @end diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib index aef8b2f81..f0f0c35b3 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib @@ -16,7 +16,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift new file mode 100644 index 000000000..f0b27fd46 --- /dev/null +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -0,0 +1,402 @@ +// +// 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 + +protocol VoiceMessageToolbarViewDelegate: AnyObject { + func voiceMessageToolbarViewDidRequestRecordingStart(_ toolbarView: VoiceMessageToolbarView) + func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) + func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) + func voiceMessageToolbarViewDidRequestLockedModeRecording(_ toolbarView: VoiceMessageToolbarView) + func voiceMessageToolbarViewDidRequestPlaybackToggle(_ toolbarView: VoiceMessageToolbarView) + func voiceMessageToolbarViewDidRequestSend(_ toolbarView: VoiceMessageToolbarView) +} + +enum VoiceMessageToolbarViewUIState { + case idle + case record + case lockedModeRecord + case lockedModePlayback +} + +struct VoiceMessageToolbarViewDetails { + var state: VoiceMessageToolbarViewUIState = .idle + var elapsedTime: String = "" + var audioSamples: [Float] = [] + var isPlaying: Bool = false + var progress: Double = 0.0 + var toastMessage: String? +} + +class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGestureRecognizerDelegate, VoiceMessagePlaybackViewDelegate { + + private enum Constants { + static let longPressMinimumDuration: TimeInterval = 0.0 + static let animationDuration: TimeInterval = 0.25 + static let lockModeTransitionAnimationDuration: TimeInterval = 0.5 + static let panDirectionChangeThreshold: CGFloat = 20.0 + static let toastContainerCornerRadii: CGFloat = 8.0 + static let toastDisplayTimeout: TimeInterval = 5.0 + } + + @IBOutlet private var backgroundView: UIView! + + @IBOutlet private var recordingContainerView: UIView! + + @IBOutlet private var recordButtonsContainerView: UIView! + @IBOutlet private var primaryRecordButton: UIButton! + @IBOutlet private var secondaryRecordButton: UIButton! + + @IBOutlet private var recordingChromeContainerView: UIView! + @IBOutlet private var recordingIndicatorView: UIView! + + @IBOutlet private var elapsedTimeLabel: UILabel! + + @IBOutlet private var slideToCancelContainerView: UIView! + @IBOutlet private var slideToCancelLabel: UILabel! + @IBOutlet private var slideToCancelChevron: UIImageView! + @IBOutlet private var slideToCancelGradient: UIImageView! + + @IBOutlet private var lockContainerView: UIView! + @IBOutlet private var lockContainerBackgroundView: UIView! + + @IBOutlet private var lockButtonsContainerView: UIView! + @IBOutlet private var primaryLockButton: UIButton! + @IBOutlet private var secondaryLockButton: UIButton! + @IBOutlet private var lockChevron: UIView! + + @IBOutlet private var lockedModeContainerView: UIView! + @IBOutlet private var deleteButton: UIButton! + @IBOutlet private var playbackViewContainerView: UIView! + @IBOutlet private var sendButton: UIButton! + + @IBOutlet private var toastNotificationContainerView: UIView! + @IBOutlet private var toastNotificationLabel: UILabel! + + private var playbackView: VoiceMessagePlaybackView! + + private var cancelLabelToRecordButtonDistance: CGFloat = 0.0 + private var lockChevronToRecordButtonDistance: CGFloat = 0.0 + private var lockChevronToLockButtonDistance: CGFloat = 0.0 + private var panDirection: UISwipeGestureRecognizer.Direction? + + private var details: VoiceMessageToolbarViewDetails? + + private var currentTheme: Theme? { + didSet { + updateUIWithDetails(details, animated: true) + } + } + + weak var delegate: VoiceMessageToolbarViewDelegate? + + override func awakeFromNib() { + super.awakeFromNib() + + lockContainerBackgroundView.layer.cornerRadius = lockContainerBackgroundView.bounds.width / 2.0 + lockButtonsContainerView.layer.cornerRadius = lockButtonsContainerView.bounds.width / 2.0 + toastNotificationContainerView.layer.cornerRadius = Constants.toastContainerCornerRadii + + let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) + longPressGesture.delegate = self + longPressGesture.minimumPressDuration = Constants.longPressMinimumDuration + recordButtonsContainerView.addGestureRecognizer(longPressGesture) + + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan)) + longPressGesture.delegate = self + recordButtonsContainerView.addGestureRecognizer(panGesture) + + playbackView = VoiceMessagePlaybackView.loadFromNib() + playbackView.delegate = self + playbackViewContainerView.vc_addSubViewMatchingParent(playbackView) + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleWaveformTap)) + playbackView.waveformView.addGestureRecognizer(tapGesture) + + updateUIWithDetails(VoiceMessageToolbarViewDetails(), animated: false) + } + + func configureWithDetails(_ details: VoiceMessageToolbarViewDetails) { + elapsedTimeLabel.text = details.elapsedTime + + self.updateToastNotificationsWithDetails(details) + self.updatePlaybackViewWithDetails(details) + + if self.details?.state != details.state { + switch details.state { + case .record: + var convertedFrame = self.convert(slideToCancelLabel.frame, from: slideToCancelContainerView) + cancelLabelToRecordButtonDistance = recordButtonsContainerView.frame.minX - convertedFrame.maxX + + convertedFrame = self.convert(lockChevron.frame, from: lockContainerView) + lockChevronToRecordButtonDistance = recordButtonsContainerView.frame.midY + convertedFrame.maxY + + lockChevronToLockButtonDistance = (lockChevron.frame.minY - lockButtonsContainerView.frame.midY) / 2 + + startAnimatingRecordingIndicator() + default: + cancelDrag() + } + + if details.state == .lockedModeRecord && self.details?.state == .record { + UIView.animate(withDuration: Constants.animationDuration) { + self.lockButtonsContainerView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1) + } completion: { _ in + self.updateUIWithDetails(details, animated: true) + } + } else { + updateUIWithDetails(details, animated: true) + } + } + + self.details = details + } + + func getRequiredNumberOfSamples() -> Int { + return playbackView.getRequiredNumberOfSamples() + } + + // MARK: - Themable + + func update(theme: Theme) { + currentTheme = theme + playbackView.update(theme: theme) + } + + // MARK: - UIGestureRecognizerDelegate + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + // MARK: - VoiceMessagePlaybackViewDelegate + + func voiceMessagePlaybackViewDidRequestPlaybackToggle() { + delegate?.voiceMessageToolbarViewDidRequestPlaybackToggle(self) + } + + func voiceMessagePlaybackViewDidChangeWidth() { + + } + + // MARK: - Private + + @objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { + switch gestureRecognizer.state { + case UIGestureRecognizer.State.began: + delegate?.voiceMessageToolbarViewDidRequestRecordingStart(self) + case UIGestureRecognizer.State.ended: + delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self) + default: + break + } + } + + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard details?.state == .record && gestureRecognizer.state == .changed else { + return + } + + let translation = gestureRecognizer.translation(in: self) + + if abs(translation.x) <= Constants.panDirectionChangeThreshold && abs(translation.y) <= Constants.panDirectionChangeThreshold { + panDirection = nil + } else if panDirection == nil { + if abs(translation.x) >= abs(translation.y) { + panDirection = .left + } else { + panDirection = .up + } + } + + if panDirection == .left { + secondaryRecordButton.transform = CGAffineTransform(translationX: min(translation.x, 0.0), y: 0.0) + slideToCancelContainerView.transform = CGAffineTransform(translationX: min(translation.x + cancelLabelToRecordButtonDistance, 0.0), y: 0.0) + + if abs(translation.x - recordButtonsContainerView.frame.width / 2.0) > self.bounds.width / 2.0 { + delegate?.voiceMessageToolbarViewDidRequestRecordingCancel(self) + } + } else if panDirection == .up { + secondaryRecordButton.transform = CGAffineTransform(translationX: 0.0, y: min(0.0, translation.y)) + + let yTranslation = min(max(translation.y + lockChevronToRecordButtonDistance, -lockChevronToLockButtonDistance), 0.0) + lockChevron.transform = CGAffineTransform(translationX: 0.0, y: yTranslation) + + let transitionPercentage = abs(yTranslation) / lockChevronToLockButtonDistance + + lockChevron.alpha = 1.0 - transitionPercentage + secondaryRecordButton.alpha = 1.0 - transitionPercentage + primaryLockButton.alpha = 1.0 - transitionPercentage + lockContainerBackgroundView.alpha = 1.0 - transitionPercentage + secondaryLockButton.alpha = transitionPercentage + + if transitionPercentage >= 1.0 { + self.delegate?.voiceMessageToolbarViewDidRequestLockedModeRecording(self) + } + + } else { + secondaryRecordButton.transform = CGAffineTransform(translationX: min(0.0, translation.x), y: min(0.0, translation.y)) + } + } + + private func cancelDrag() { + recordButtonsContainerView.gestureRecognizers?.forEach { gestureRecognizer in + gestureRecognizer.isEnabled = false + gestureRecognizer.isEnabled = true + } + } + + private func updateUIWithDetails(_ details: VoiceMessageToolbarViewDetails?, animated: Bool) { + guard let details = details else { + return + } + + UIView.animate(withDuration: (animated ? Constants.animationDuration : 0.0), delay: 0.0, options: .beginFromCurrentState) { + switch details.state { + case .record: + self.lockContainerBackgroundView.alpha = 1.0 + case .idle: + self.lockContainerBackgroundView.alpha = 1.0 + self.primaryLockButton.alpha = 1.0 + self.secondaryLockButton.alpha = 0.0 + self.lockChevron.alpha = 1.0 + default: + break + } + + self.backgroundView.alpha = (details.state == .idle ? 0.0 : 1.0) + self.primaryRecordButton.alpha = (details.state == .idle ? 1.0 : 0.0) + self.secondaryRecordButton.alpha = (details.state == .record ? 1.0 : 0.0) + self.recordingChromeContainerView.alpha = (details.state == .record ? 1.0 : 0.0) + self.lockContainerView.alpha = (details.state == .record ? 1.0 : 0.0) + self.lockedModeContainerView.alpha = (details.state == .lockedModePlayback || details.state == .lockedModeRecord ? 1.0 : 0.0) + self.recordingContainerView.alpha = (details.state == .idle || details.state == .record ? 1.0 : 0.0) + + guard let theme = self.currentTheme else { + return + } + + self.backgroundView.backgroundColor = theme.colors.background + self.slideToCancelGradient.tintColor = theme.colors.background + + self.primaryRecordButton.tintColor = theme.colors.tertiaryContent + self.slideToCancelLabel.textColor = theme.colors.secondaryContent + self.slideToCancelChevron.tintColor = theme.colors.secondaryContent + self.elapsedTimeLabel.textColor = theme.colors.secondaryContent + + self.lockContainerBackgroundView.backgroundColor = theme.colors.navigation + self.lockButtonsContainerView.backgroundColor = theme.colors.navigation + + } completion: { _ in + switch details.state { + case .idle: + self.secondaryRecordButton.transform = .identity + self.slideToCancelContainerView.transform = .identity + self.lockChevron.transform = .identity + self.lockButtonsContainerView.transform = .identity + default: + break + } + } + } + + private var toastIdleTimer: Timer? + private var lastUIState: VoiceMessageToolbarViewUIState = .idle + + private func updateToastNotificationsWithDetails(_ details: VoiceMessageToolbarViewDetails, animated: Bool = true) { + + guard self.toastNotificationLabel.text != details.toastMessage || lastUIState != details.state else { + return + } + + lastUIState = details.state + + let shouldShowNotification = details.state != .idle && details.toastMessage != nil + let requiredAlpha: CGFloat = shouldShowNotification ? 1.0 : 0.0 + + toastIdleTimer?.invalidate() + toastIdleTimer = nil + + if shouldShowNotification { + self.toastNotificationLabel.text = details.toastMessage + } + + UIView.animate(withDuration: (animated ? Constants.animationDuration : 0.0)) { + self.toastNotificationContainerView.alpha = requiredAlpha + } + + if shouldShowNotification { + toastIdleTimer = Timer.scheduledTimer(withTimeInterval: Constants.toastDisplayTimeout, repeats: false) { [weak self] timer in + guard let self = self else { + return + } + + self.toastIdleTimer?.invalidate() + self.toastIdleTimer = nil + + UIView.animate(withDuration: Constants.animationDuration) { + self.toastNotificationContainerView.alpha = 0 + } + } + } + } + + private func updatePlaybackViewWithDetails(_ details: VoiceMessageToolbarViewDetails, animated: Bool = true) { + UIView.animate(withDuration: (animated ? Constants.animationDuration : 0.0)) { + var playbackViewDetails = VoiceMessagePlaybackViewDetails() + playbackViewDetails.recording = (details.state == .record || details.state == .lockedModeRecord) + playbackViewDetails.playing = details.isPlaying + playbackViewDetails.progress = details.progress + playbackViewDetails.currentTime = details.elapsedTime + playbackViewDetails.samples = details.audioSamples + playbackViewDetails.playbackEnabled = true + self.playbackView.configureWithDetails(playbackViewDetails) + } + } + + private func startAnimatingRecordingIndicator() { + if self.details?.state != .record { + return + } + + UIView.animate(withDuration: Constants.lockModeTransitionAnimationDuration) { + if self.recordingIndicatorView.alpha > 0.0 { + self.recordingIndicatorView.alpha = 0.0 + } else { + self.recordingIndicatorView.alpha = 1.0 + } + } completion: { [weak self] _ in + self?.startAnimatingRecordingIndicator() + } + } + + @IBAction private func onTrashButtonTap(_ sender: UIBarItem) { + delegate?.voiceMessageToolbarViewDidRequestRecordingCancel(self) + } + + @IBAction private func onSendButtonTap(_ sender: UIBarItem) { + delegate?.voiceMessageToolbarViewDidRequestSend(self) + } + + @objc private func handleWaveformTap(_ gestureRecognizer: UITapGestureRecognizer) { + guard self.lastUIState == .lockedModeRecord else { + return + } + + delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self) + } +} diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib new file mode 100644 index 000000000..f11674470 --- /dev/null +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift new file mode 100644 index 000000000..8685109c4 --- /dev/null +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift @@ -0,0 +1,122 @@ +// +// 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 + +class VoiceMessageWaveformView: UIView { + + private let lineWidth: CGFloat = 2.0 + private let linePadding: CGFloat = 2.0 + private let renderingQueue: DispatchQueue = DispatchQueue(label: "io.element.VoiceMessageWaveformView.queue", qos: .userInitiated) + + var samples: [Float] = [] { + didSet { + computeWaveForm() + } + } + + var primaryLineColor = UIColor.lightGray { + didSet { + backgroundLayer.strokeColor = primaryLineColor.cgColor + backgroundLayer.fillColor = primaryLineColor.cgColor + } + } + var secondaryLineColor = UIColor.darkGray { + didSet { + progressLayer.strokeColor = secondaryLineColor.cgColor + progressLayer.fillColor = secondaryLineColor.cgColor + } + } + + private let backgroundLayer = CAShapeLayer() + private let progressLayer = CAShapeLayer() + + var progress = 0.0 { + didSet { + CATransaction.begin() + CATransaction.setDisableActions(true) + progressLayer.frame = CGRect(origin: self.bounds.origin, size: CGSize(width: self.bounds.width * CGFloat(self.progress), height: self.bounds.height)) + CATransaction.commit() + } + } + + var requiredNumberOfSamples: Int { + return Int(self.bounds.size.width / (lineWidth + linePadding)) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + setupAndAdd(backgroundLayer, with: primaryLineColor) + setupAndAdd(progressLayer, with: secondaryLineColor) + progressLayer.masksToBounds = true + + computeWaveForm() + } + + required init?(coder: NSCoder) { + fatalError() + } + + override func layoutSubviews() { + super.layoutSubviews() + + backgroundLayer.frame = self.bounds + progressLayer.frame = CGRect(origin: self.bounds.origin, size: CGSize(width: self.bounds.width * CGFloat(self.progress), height: self.bounds.height)) + computeWaveForm() + } + + // MARK: - Private + + private func computeWaveForm() { + renderingQueue.async { [samples] in // Capture the current samples as a way to provide atomicity + let path = UIBezierPath() + + let drawMappingFactor = self.bounds.size.height + let minimumGraphAmplitude: CGFloat = 1 + + var xOffset: CGFloat = self.lineWidth / 2 + var index = 0 + + while xOffset < self.bounds.width - self.lineWidth { + let sample = CGFloat(index >= samples.count ? 1 : samples[index]) + let invertedDbSample = 1 - sample // sample is in dB, linearly normalized to [0, 1] (1 -> -50 dB) + let drawingAmplitude = max(minimumGraphAmplitude, invertedDbSample * drawMappingFactor) + + path.move(to: CGPoint(x: xOffset, y: self.bounds.midY - drawingAmplitude / 2)) + path.addLine(to: CGPoint(x: xOffset, y: self.bounds.midY + drawingAmplitude / 2)) + + xOffset += self.lineWidth + self.linePadding + + index += 1 + } + + DispatchQueue.main.async { + self.backgroundLayer.path = path.cgPath + self.progressLayer.path = path.cgPath + } + } + } + + private func setupAndAdd(_ shapeLayer: CAShapeLayer, with color: UIColor) { + shapeLayer.frame = self.bounds + shapeLayer.strokeColor = color.cgColor + shapeLayer.fillColor = color.cgColor + shapeLayer.lineCap = .round + shapeLayer.lineWidth = lineWidth + self.layer.addSublayer(shapeLayer) + } +} diff --git a/Riot/Modules/Rooms/RoomsViewController.m b/Riot/Modules/Rooms/RoomsViewController.m index a9dc73b5d..979b30610 100644 --- a/Riot/Modules/Rooms/RoomsViewController.m +++ b/Riot/Modules/Rooms/RoomsViewController.m @@ -18,16 +18,11 @@ #import "RecentsDataSource.h" -#import "DirectoryServerPickerViewController.h" - #import "Riot-Swift.h" @interface RoomsViewController () { RecentsDataSource *recentsDataSource; - - // The animated view displayed at the table view bottom when paginating the room directory - UIView* footerSpinnerView; } @end @@ -63,8 +58,6 @@ plusButtonImageView = [self vc_addFABWithImage:[UIImage imageNamed:@"rooms_floating_action"] target:self action:@selector(onPlusButtonPressed)]; - - self.enableStickyHeaders = YES; } - (void)viewWillAppear:(BOOL)animated @@ -74,24 +67,12 @@ [AppDelegate theDelegate].masterTabBarController.navigationItem.title = NSLocalizedStringFromTable(@"title_rooms", @"Vector", nil); [AppDelegate theDelegate].masterTabBarController.tabBar.tintColor = ThemeService.shared.theme.tintColor; - // TODO: Notify RiotSettings.shared.showNSFWPublicRooms change for iPad as viewWillAppear may not be called - recentsDataSource.publicRoomsDirectoryDataSource.showNSFWRooms = RiotSettings.shared.showNSFWPublicRooms; - if ([self.dataSource isKindOfClass:RecentsDataSource.class]) { - BOOL isFirstTime = (recentsDataSource != self.dataSource); - // Take the lead on the shared data source. recentsDataSource = (RecentsDataSource*)self.dataSource; recentsDataSource.areSectionsShrinkable = NO; [recentsDataSource setDelegate:self andRecentsDataSourceMode:RecentsDataSourceModeRooms]; - - if (isFirstTime) - { - // The first time the screen is displayed, make publicRoomsDirectoryDataSource - // start loading data - [recentsDataSource.publicRoomsDirectoryDataSource paginate:nil failure:nil]; - } } } @@ -113,32 +94,19 @@ [super refreshCurrentSelectedCell:forceVisible]; } -- (UIView *)tableView:(UITableView *)tableView viewForStickyHeaderInSection:(NSInteger)section -{ - CGRect frame = [tableView rectForHeaderInSection:section]; - frame.size.height = self.stickyHeaderHeight; - - return [recentsDataSource viewForHeaderInSection:section withFrame:frame]; -} - -- (void)dataSource:(MXKDataSource *)dataSource didRecognizeAction:(NSString *)actionIdentifier inCell:(id)cell userInfo:(NSDictionary *)userInfo -{ - if ([actionIdentifier isEqualToString:kRecentsDataSourceTapOnDirectoryServerChange]) - { - // Show the directory server picker - [self performSegueWithIdentifier:@"presentDirectoryServerPicker" sender:self]; - } - else - { - [super dataSource:dataSource didRecognizeAction:actionIdentifier inCell:cell userInfo:userInfo]; - } -} - - (void)onPlusButtonPressed { [self showRoomDirectory]; } +#pragma mark - UITableView delegate + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section +{ + // Hide the header to merge Invites and Rooms into a single list. + return 0.0; +} + #pragma mark - - (void)scrollToNextRoomWithMissedNotifications @@ -150,181 +118,6 @@ } } -#pragma mark - Navigation - -- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender -{ - [super prepareForSegue:segue sender:sender]; - - UIViewController *pushedViewController = [segue destinationViewController]; - - if ([[segue identifier] isEqualToString:@"presentDirectoryServerPicker"]) - { - UINavigationController *pushedNavigationViewController = (UINavigationController*)pushedViewController; - DirectoryServerPickerViewController* directoryServerPickerViewController = (DirectoryServerPickerViewController*)pushedNavigationViewController.viewControllers.firstObject; - - MXKDirectoryServersDataSource *directoryServersDataSource = [[MXKDirectoryServersDataSource alloc] initWithMatrixSession:recentsDataSource.publicRoomsDirectoryDataSource.mxSession]; - [directoryServersDataSource finalizeInitialization]; - - // Add directory servers from the app settings - directoryServersDataSource.roomDirectoryServers = BuildSettings.publicRoomsDirectoryServers; - - __weak typeof(self) weakSelf = self; - - [directoryServerPickerViewController displayWithDataSource:directoryServersDataSource onComplete:^(id cellData) { - if (weakSelf && cellData) - { - typeof(self) self = weakSelf; - - // Use the selected directory server - if (cellData.thirdPartyProtocolInstance) - { - self->recentsDataSource.publicRoomsDirectoryDataSource.thirdpartyProtocolInstance = cellData.thirdPartyProtocolInstance; - } - else if (cellData.homeserver) - { - self->recentsDataSource.publicRoomsDirectoryDataSource.includeAllNetworks = cellData.includeAllNetworks; - self->recentsDataSource.publicRoomsDirectoryDataSource.homeserver = cellData.homeserver; - } - - // Refresh data - [self addSpinnerFooterView]; - - [self->recentsDataSource.publicRoomsDirectoryDataSource paginate:^(NSUInteger roomsAdded) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - - // The table view is automatically filled - [self removeSpinnerFooterView]; - - // Make the directory section appear full-page - [self.recentsTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:self->recentsDataSource.directorySection] atScrollPosition:UITableViewScrollPositionTop animated:YES]; - } - - } failure:^(NSError *error) { - - if (weakSelf) - { - typeof(self) self = weakSelf; - [self removeSpinnerFooterView]; - } - }]; - } - }]; - - // Hide back button title - pushedViewController.navigationController.navigationItem.backBarButtonItem =[[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; - } -} - -#pragma mark - UITableView delegate - -- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section -{ - if (section == recentsDataSource.directorySection) - { - // Let the recents dataSource provide the height of this section header - return [recentsDataSource heightForHeaderInSection:section]; - } - - return [super tableView:tableView heightForHeaderInSection:section]; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (indexPath.section == recentsDataSource.directorySection) - { - // Sanity check - MXPublicRoom *publicRoom = [recentsDataSource.publicRoomsDirectoryDataSource roomAtIndexPath:indexPath]; - if (publicRoom) - { - [self openPublicRoomAtIndexPath:indexPath]; - } - } - else - { - [super tableView:tableView didSelectRowAtIndexPath:indexPath]; - } -} - -- (void)scrollViewDidScroll:(UIScrollView *)scrollView -{ - // Trigger inconspicuous pagination on directy when user scrolls down - if ((scrollView.contentSize.height - scrollView.contentOffset.y - scrollView.frame.size.height) < 300) - { - [self triggerDirectoryPagination]; - } - - [super scrollViewDidScroll:scrollView]; -} - -#pragma mark - Private methods - -- (void)openPublicRoomAtIndexPath:(NSIndexPath *)indexPath -{ - MXPublicRoom *publicRoom = [recentsDataSource.publicRoomsDirectoryDataSource roomAtIndexPath:indexPath]; - - [self openPublicRoom:publicRoom]; -} - -- (void)triggerDirectoryPagination -{ - if (!recentsDataSource - || recentsDataSource.state == MXKDataSourceStateUnknown - || recentsDataSource.publicRoomsDirectoryDataSource.hasReachedPaginationEnd - || footerSpinnerView) - { - // We are not yet ready or being killed or we got all public rooms or we are already paginating - // Do nothing - return; - } - - [self addSpinnerFooterView]; - - [recentsDataSource.publicRoomsDirectoryDataSource paginate:^(NSUInteger roomsAdded) { - - // The table view is automatically filled - [self removeSpinnerFooterView]; - - } failure:^(NSError *error) { - - [self removeSpinnerFooterView]; - }]; -} - -- (void)addSpinnerFooterView -{ - if (!footerSpinnerView) - { - UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; - spinner.transform = CGAffineTransformMakeScale(0.75f, 0.75f); - CGRect frame = spinner.frame; - frame.size.height = 80; // 80 * 0.75 = 60 - spinner.bounds = frame; - - spinner.color = [UIColor darkGrayColor]; - spinner.hidesWhenStopped = NO; - spinner.backgroundColor = [UIColor clearColor]; - [spinner startAnimating]; - - // No need to manage constraints here, iOS defines them - self.recentsTableView.tableFooterView = footerSpinnerView = spinner; - } -} - -- (void)removeSpinnerFooterView -{ - if (footerSpinnerView) - { - footerSpinnerView = nil; - - // Hide line separators of empty cells - self.recentsTableView.tableFooterView = [[UIView alloc] init];; - } -} - #pragma mark - Empty view management - (void)updateEmptyView @@ -362,7 +155,6 @@ - (NSUInteger)totalItemCounts { return recentsDataSource.conversationCellDataArray.count - + recentsDataSource.publicRoomsDirectoryDataSource.roomsCount + recentsDataSource.invitesCellDataArray.count; } diff --git a/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.storyboard b/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.storyboard index ab35e9993..b33325642 100644 --- a/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.storyboard +++ b/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.storyboard @@ -1,11 +1,9 @@ - - - - + + - + @@ -19,7 +17,7 @@ - + @@ -38,9 +36,8 @@ - - + @@ -63,7 +60,7 @@ -