diff --git a/.github/workflows/ci-ui-tests.yml b/.github/workflows/ci-ui-tests.yml
index 37a103035b..39c90d5097 100644
--- a/.github/workflows/ci-ui-tests.yml
+++ b/.github/workflows/ci-ui-tests.yml
@@ -1,9 +1,6 @@
name: UI Tests CI
on:
- push:
- branches: [ develop ]
-
pull_request:
workflow_dispatch:
diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml
index 0d6b6689a9..632e8b5382 100644
--- a/.github/workflows/triage-move-labelled.yml
+++ b/.github/workflows/triage-move-labelled.yml
@@ -17,7 +17,8 @@ jobs:
contains(github.event.issue.labels.*.name, 'Z-IA') ||
contains(github.event.issue.labels.*.name, 'A-Themes-Custom') ||
contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') ||
- contains(github.event.issue.labels.*.name, 'A-Tags')
+ contains(github.event.issue.labels.*.name, 'A-Tags') ||
+ contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor')
steps:
- uses: actions/github-script@v5
with:
@@ -44,7 +45,13 @@ jobs:
name: P1 X-Needs-Design to Design project board
runs-on: ubuntu-latest
if: >
- contains(github.event.issue.labels.*.name, 'X-Needs-Design')
+ contains(github.event.issue.labels.*.name, 'X-Needs-Design') &&
+ (contains(github.event.issue.labels.*.name, 'S-Critical') &&
+ (contains(github.event.issue.labels.*.name, 'O-Frequent') ||
+ contains(github.event.issue.labels.*.name, 'O-Occasional')) ||
+ (contains(github.event.issue.labels.*.name, 'S-Major') &&
+ contains(github.event.issue.labels.*.name, 'O-Frequent')) ||
+ contains(github.event.issue.labels.*.name, 'A11y'))
steps:
- uses: octokit/graphql-action@v2.x
id: add_to_project
@@ -202,3 +209,105 @@ jobs:
env:
PROJECT_ID: "PN_kwDOAM0swc4AArk0"
GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+
+ ps_features1:
+ name: Add labelled issues to PS features team 1
+ runs-on: ubuntu-latest
+ if: >
+ contains(github.event.issue.labels.*.name, 'A-Polls') ||
+ contains(github.event.issue.labels.*.name, 'A-Location-Sharing') ||
+ (contains(github.event.issue.labels.*.name, 'A-Voice-Messages') &&
+ !contains(github.event.issue.labels.*.name, 'A-Broadcast')) ||
+ (contains(github.event.issue.labels.*.name, 'A-Session-Mgmt') &&
+ contains(github.event.issue.labels.*.name, 'A-User-Settings'))
+ steps:
+ - uses: octokit/graphql-action@v2.x
+ id: add_to_project
+ with:
+ headers: '{"GraphQL-Features": "projects_next_graphql"}'
+ query: |
+ mutation add_to_project($projectid:ID!,$contentid:ID!) {
+ addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
+ item {
+ id
+ }
+ }
+ }
+ projectid: ${{ env.PROJECT_ID }}
+ contentid: ${{ github.event.issue.node_id }}
+ env:
+ PROJECT_ID: "PVT_kwDOAM0swc4AHJKF"
+ GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+
+ ps_features2:
+ name: Add labelled issues to PS features team 2
+ runs-on: ubuntu-latest
+ if: >
+ contains(github.event.issue.labels.*.name, 'A-DM-Start') ||
+ contains(github.event.issue.labels.*.name, 'A-Broadcast')
+ steps:
+ - uses: octokit/graphql-action@v2.x
+ id: add_to_project
+ with:
+ headers: '{"GraphQL-Features": "projects_next_graphql"}'
+ query: |
+ mutation add_to_project($projectid:ID!,$contentid:ID!) {
+ addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
+ item {
+ id
+ }
+ }
+ }
+ projectid: ${{ env.PROJECT_ID }}
+ contentid: ${{ github.event.issue.node_id }}
+ env:
+ PROJECT_ID: "PVT_kwDOAM0swc4AHJKd"
+ GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+
+ ps_features3:
+ name: Add labelled issues to PS features team 3
+ runs-on: ubuntu-latest
+ if: >
+ contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor')
+ steps:
+ - uses: octokit/graphql-action@v2.x
+ id: add_to_project
+ with:
+ headers: '{"GraphQL-Features": "projects_next_graphql"}'
+ query: |
+ mutation add_to_project($projectid:ID!,$contentid:ID!) {
+ addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
+ item {
+ id
+ }
+ }
+ }
+ projectid: ${{ env.PROJECT_ID }}
+ contentid: ${{ github.event.issue.node_id }}
+ env:
+ PROJECT_ID: "PVT_kwDOAM0swc4AHJKW"
+ GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+
+ voip:
+ name: Add labelled issues to VoIP project board
+ runs-on: ubuntu-latest
+ if: >
+ contains(github.event.issue.labels.*.name, 'Team: VoIP')
+ steps:
+ - uses: octokit/graphql-action@v2.x
+ id: add_to_project
+ with:
+ headers: '{"GraphQL-Features": "projects_next_graphql"}'
+ query: |
+ mutation add_to_project($projectid:ID!,$contentid:ID!) {
+ addProjectV2ItemById(input: {projectId: $projectid contentId: $contentid}) {
+ item {
+ id
+ }
+ }
+ }
+ projectid: ${{ env.PROJECT_ID }}
+ contentid: ${{ github.event.issue.node_id }}
+ env:
+ PROJECT_ID: "PVT_kwDOAM0swc4ABMIk"
+ GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
diff --git a/CHANGES.md b/CHANGES.md
index 5aa8502c99..8f8e82c1ed 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,40 @@
+## Changes in 1.9.10 (2022-11-01)
+
+âš Features
+
+- Changed the info in the background audio message player. ([#6870](https://github.com/vector-im/element-ios/pull/6870))
+- Added voice message support to the Rich Text Composer ([#6941](https://github.com/vector-im/element-ios/issues/6941))
+
+đ Improvements
+
+- Improves external links interaction UX. ([#6936](https://github.com/vector-im/element-ios/pull/6936))
+- Verification: Deprecate legacy device-to-device verification ([#6937](https://github.com/vector-im/element-ios/pull/6937))
+- Crypto: Define MXCrypto and MXCrossSigning as protocols ([#6943](https://github.com/vector-im/element-ios/pull/6943))
+- Hide the old session list when the new device manager is enabled. ([#6999](https://github.com/vector-im/element-ios/pull/6999))
+- Upgrade MatrixSDK version ([v0.24.2](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.2)).
+- Added a responsive placeholder text to the Rich Text Composer ([#6935](https://github.com/vector-im/element-ios/issues/6935))
+- Added the maximise/minimise toggle button to the Rich Text Composer ([#6954](https://github.com/vector-im/element-ios/issues/6954))
+
+đ Bugfixes
+
+- Timeline: Fix layout for SwiftUI content views. ([#5326](https://github.com/vector-im/element-ios/issues/5326))
+- Updates the avatar image loading logics. ([#6847](https://github.com/vector-im/element-ios/issues/6847))
+- Fixes input text view height when containing multiple lines of text. ([#6849](https://github.com/vector-im/element-ios/issues/6849))
+- Fixed the placeholder flickering in the input toolbar when there is an height change. ([#6949](https://github.com/vector-im/element-ios/issues/6949))
+
+𧱠Build
+
+- Add Z-Labs tag for rich text editor and update to the new label naming. ([#6996](https://github.com/vector-im/element-ios/pull/6996))
+
+đ§ In development đ§
+
+- Device Manager: Multi-session selection. ([#6928](https://github.com/vector-im/element-ios/issues/6928))
+
+Others
+
+- Updated templates readme file. ([#6925](https://github.com/vector-im/element-ios/issues/6925))
+
+
## Changes in 1.9.9 (2022-10-18)
âš Features
diff --git a/Config/AppConfiguration.swift b/Config/AppConfiguration.swift
index 7f9e29b5f6..70b1d78d5e 100644
--- a/Config/AppConfiguration.swift
+++ b/Config/AppConfiguration.swift
@@ -33,7 +33,7 @@ class AppConfiguration: CommonConfiguration {
// Get additional events (modular widget, voice broadcast...)
MXKAppSettings.standard()?.addSupportedEventTypes([kWidgetMatrixEventTypeString,
kWidgetModularEventTypeString,
- VoiceBroadcastSettings.eventType])
+ VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType])
// Hide undecryptable messages that were sent while the user was not in the room
MXKAppSettings.standard()?.hidePreJoinedUndecryptableEvents = true
diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig
index ec3d81ec8e..f891de3978 100644
--- a/Config/AppVersion.xcconfig
+++ b/Config/AppVersion.xcconfig
@@ -15,5 +15,5 @@
//
// Version
-MARKETING_VERSION = 1.9.9
-CURRENT_PROJECT_VERSION = 1.9.9
+MARKETING_VERSION = 1.9.10
+CURRENT_PROJECT_VERSION = 1.9.10
diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift
index df349eaee2..aa0de1873d 100644
--- a/Config/BuildSettings.swift
+++ b/Config/BuildSettings.swift
@@ -234,6 +234,8 @@ final class BuildSettings: NSObject {
static let allowInviteExernalUsers: Bool = true
+ static let allowBackgroundAudioMessagePlayback: Bool = true
+
// MARK: - Side Menu
static let enableSideMenu: Bool = true && !newAppLayoutEnabled
static let sideMenuShowInviteFriends: Bool = true
@@ -406,7 +408,7 @@ final class BuildSettings: NSObject {
static let locationSharingEnabled = true
// MARK: - Voice Broadcast
- static let voiceBroadcastChunkLength: Int = 600
+ static let voiceBroadcastChunkLength: Int = 120
static let voiceBroadcastMaxLength: UInt64 = 144000
// MARK: - MXKAppSettings
diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift
index a89427c3ae..fee3796ff1 100644
--- a/Config/CommonConfiguration.swift
+++ b/Config/CommonConfiguration.swift
@@ -172,7 +172,7 @@ class CommonConfiguration: NSObject, Configurable {
func setupSettingsWhenLoaded(for matrixSession: MXSession) {
// Do not warn for unknown devices. We have cross-signing now
- matrixSession.crypto?.warnOnUnknowDevices = false
+ (matrixSession.crypto as? MXLegacyCrypto)?.warnOnUnknowDevices = false
}
}
diff --git a/Podfile b/Podfile
index c0144e7adb..d89c32f611 100644
--- a/Podfile
+++ b/Podfile
@@ -16,7 +16,7 @@ use_frameworks!
# - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => âŠ, :podspec => âŠ) for MatrixSDK repo. Used by Fastfile during CI
#
# Warning: our internal tooling depends on the name of this variable name, so be sure not to change it
-$matrixSDKVersion = '= 0.24.1'
+$matrixSDKVersion = '= 0.24.2'
# $matrixSDKVersion = :local
# $matrixSDKVersion = { :branch => 'develop'}
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }
@@ -154,5 +154,14 @@ post_install do |installer|
config.build_settings['WARNING_CFLAGS'] ||= ['$(inherited)','-Wno-nullability-completeness']
config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)', '-Xcc', '-Wno-nullability-completeness']
end
+
+ # Fix Xcode 14 resource bundle signing issues
+ # https://github.com/CocoaPods/CocoaPods/issues/11402#issuecomment-1259231655
+ if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle"
+ target.build_configurations.each do |config|
+ config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
+ end
+ end
+
end
end
diff --git a/Podfile.lock b/Podfile.lock
index 0a680b6d52..0def666833 100644
--- a/Podfile.lock
+++ b/Podfile.lock
@@ -55,9 +55,9 @@ PODS:
- LoggerAPI (1.9.200):
- Logging (~> 1.1)
- Logging (1.4.0)
- - MatrixSDK (0.24.1):
- - MatrixSDK/Core (= 0.24.1)
- - MatrixSDK/Core (0.24.1):
+ - MatrixSDK (0.24.2):
+ - MatrixSDK/Core (= 0.24.2)
+ - MatrixSDK/Core (0.24.2):
- AFNetworking (~> 4.0.0)
- GZIP (~> 1.3.0)
- libbase58 (~> 0.1.4)
@@ -65,12 +65,12 @@ PODS:
- OLMKit (~> 3.2.5)
- Realm (= 10.27.0)
- SwiftyBeaver (= 1.9.5)
- - MatrixSDK/CryptoSDK (0.24.1):
- - MatrixSDKCrypto (= 0.1.2)
- - MatrixSDK/JingleCallStack (0.24.1):
+ - MatrixSDK/CryptoSDK (0.24.2):
+ - MatrixSDKCrypto (= 0.1.5)
+ - MatrixSDK/JingleCallStack (0.24.2):
- JitsiMeetSDK (= 5.0.2)
- MatrixSDK/Core
- - MatrixSDKCrypto (0.1.2)
+ - MatrixSDKCrypto (0.1.5)
- OLMKit (3.2.12):
- OLMKit/olmc (= 3.2.12)
- OLMKit/olmcpp (= 3.2.12)
@@ -122,8 +122,8 @@ DEPENDENCIES:
- KeychainAccess (~> 4.2.2)
- KTCenterFlowLayout (~> 1.3.1)
- libPhoneNumber-iOS (~> 0.9.13)
- - MatrixSDK (= 0.24.1)
- - MatrixSDK/JingleCallStack (= 0.24.1)
+ - MatrixSDK (= 0.24.2)
+ - MatrixSDK/JingleCallStack (= 0.24.2)
- OLMKit
- PostHog (~> 1.4.4)
- ReadMoreTextView (~> 3.0.1)
@@ -220,8 +220,8 @@ SPEC CHECKSUMS:
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d
Logging: beeb016c9c80cf77042d62e83495816847ef108b
- MatrixSDK: 0cb0727dd82f88d0115da93ce94ed9556a195c9b
- MatrixSDKCrypto: e6e69cb16f9e459761567d078af0c17929f6a3c2
+ MatrixSDK: 1b64384084050652fcffafdf8641200f1ab25060
+ MatrixSDKCrypto: dcab554bc7157cad31c01fc1137cf5acb01959a4
OLMKit: da115f16582e47626616874e20f7bb92222c7a51
PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f
ReadMoreTextView: 19147adf93abce6d7271e14031a00303fe28720d
@@ -241,6 +241,6 @@ SPEC CHECKSUMS:
zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
-PODFILE CHECKSUM: 82fb79d0a6b074f77950ec73304a749eb0329d12
+PODFILE CHECKSUM: 96a971e076c61e54ae5bb7bf30ecba80563eeacf
COCOAPODS: 1.11.2
diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 482bfb1c93..ef28187e4f 100644
--- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -23,7 +23,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift",
"state" : {
- "revision" : "11dad16e3e589dba423f6cc5707e9df8aace89b0"
+ "revision" : "d5ef7054fb43924d5b92d5d627347ca2bc333717"
}
},
{
diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_not_selected.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_not_selected.imageset/Contents.json
new file mode 100644
index 0000000000..132fb89375
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_not_selected.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "user_session_list_item_not_selected.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_not_selected.imageset/user_session_list_item_not_selected.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_not_selected.imageset/user_session_list_item_not_selected.svg
new file mode 100644
index 0000000000..7b73d0c6e3
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_not_selected.imageset/user_session_list_item_not_selected.svg
@@ -0,0 +1,3 @@
+
diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_selected.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_selected.imageset/Contents.json
new file mode 100644
index 0000000000..7c5fd86984
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_selected.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "user_session_list_item_selected.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_selected.imageset/user_session_list_item_selected.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_selected.imageset/user_session_list_item_selected.svg
new file mode 100644
index 0000000000..13680d43a0
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_selected.imageset/user_session_list_item_selected.svg
@@ -0,0 +1,4 @@
+
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
index ead86edbb9..04b38da3ef 100644
--- 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
@@ -1,17 +1,17 @@
{
"images" : [
{
- "filename" : "action_voice_message.png",
+ "filename" : "Microphone icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
- "filename" : "action_voice_message@2x.png",
+ "filename" : "Microphone icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
- "filename" : "action_voice_message@3x.png",
+ "filename" : "Microphone icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/Microphone icon.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/Microphone icon.png
new file mode 100644
index 0000000000..8a6b3eb14c
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/Microphone icon.png differ
diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/Microphone icon@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/Microphone icon@2x.png
new file mode 100644
index 0000000000..5b404b74c6
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/Microphone icon@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/Microphone icon@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/Microphone icon@3x.png
new file mode 100644
index 0000000000..520e22e94e
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/Microphone icon@3x.png differ
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
deleted file mode 100644
index b969cb3aad..0000000000
Binary files a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/action_voice_message.png and /dev/null 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
deleted file mode 100644
index 32c6236a66..0000000000
Binary files a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/action_voice_message@2x.png and /dev/null 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
deleted file mode 100644
index e8cc54c293..0000000000
Binary files a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_default.imageset/action_voice_message@3x.png and /dev/null 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
index 900874ca12..bc412b2cfd 100644
--- 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
@@ -1,17 +1,17 @@
{
"images" : [
{
- "filename" : "voice_message_record_button_recording.png",
+ "filename" : "Microphone asset.png",
"idiom" : "universal",
"scale" : "1x"
},
{
- "filename" : "voice_message_record_button_recording@2x.png",
+ "filename" : "Microphone asset@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
- "filename" : "voice_message_record_button_recording@3x.png",
+ "filename" : "Microphone asset@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset.png
new file mode 100644
index 0000000000..ffeb00aafd
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset.png differ
diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset@2x.png
new file mode 100644
index 0000000000..8582e2d234
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset@3x.png
new file mode 100644
index 0000000000..e48d9a36b1
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/Microphone asset@3x.png differ
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
deleted file mode 100644
index 5972e1272d..0000000000
Binary files a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording.png and /dev/null 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
deleted file mode 100644
index 802268ba04..0000000000
Binary files a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@2x.png and /dev/null 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
deleted file mode 100644
index b1def35e1d..0000000000
Binary files a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@3x.png and /dev/null differ
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/Contents.json
new file mode 100644
index 0000000000..73c00596a7
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_live.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_live.imageset/Contents.json
new file mode 100644
index 0000000000..fa6650d1cf
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_live.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "voice_broadcast_live.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_live.imageset/voice_broadcast_live.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_live.imageset/voice_broadcast_live.svg
new file mode 100644
index 0000000000..fd78cfc25e
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_live.imageset/voice_broadcast_live.svg
@@ -0,0 +1,7 @@
+
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_pause.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_pause.imageset/Contents.json
new file mode 100644
index 0000000000..4f275b2b0b
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_pause.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "voice_broadcast_pause.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_pause.imageset/voice_broadcast_pause.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_pause.imageset/voice_broadcast_pause.svg
new file mode 100644
index 0000000000..babd787167
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_pause.imageset/voice_broadcast_pause.svg
@@ -0,0 +1,5 @@
+
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_play.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_play.imageset/Contents.json
new file mode 100644
index 0000000000..6302334b39
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_play.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "voice_broadcast_play.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_play.imageset/voice_broadcast_play.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_play.imageset/voice_broadcast_play.svg
new file mode 100644
index 0000000000..65849ae58b
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_play.imageset/voice_broadcast_play.svg
@@ -0,0 +1,4 @@
+
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record.imageset/Contents.json
new file mode 100644
index 0000000000..48ffc5e34a
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "voice_broadcast_record.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record.imageset/voice_broadcast_record.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record.imageset/voice_broadcast_record.svg
new file mode 100644
index 0000000000..4ca9bd42cb
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record.imageset/voice_broadcast_record.svg
@@ -0,0 +1,6 @@
+
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record_pause.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record_pause.imageset/Contents.json
new file mode 100644
index 0000000000..157748565a
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record_pause.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "voice_broadcast_record_pause.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record_pause.imageset/voice_broadcast_record_pause.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record_pause.imageset/voice_broadcast_record_pause.svg
new file mode 100644
index 0000000000..ba12bc64c7
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_record_pause.imageset/voice_broadcast_record_pause.svg
@@ -0,0 +1,5 @@
+
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_stop.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_stop.imageset/Contents.json
new file mode 100644
index 0000000000..8431bfd581
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_stop.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "voice_broadcast_stop.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_stop.imageset/voice_broadcast_stop.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_stop.imageset/voice_broadcast_stop.svg
new file mode 100644
index 0000000000..1fed1640b7
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_stop.imageset/voice_broadcast_stop.svg
@@ -0,0 +1,6 @@
+
diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings
index 6fc34bb329..5e7c7dee16 100644
--- a/Riot/Assets/de.lproj/Vector.strings
+++ b/Riot/Assets/de.lproj/Vector.strings
@@ -153,15 +153,15 @@
"room_two_users_are_typing" = "%@ und %@ tippenâŠ";
"room_many_users_are_typing" = "%@, %@ und andere tippenâŠ";
"room_message_placeholder" = "Nachricht senden (unverschlĂŒsselt)âŠ";
-"encrypted_room_message_placeholder" = "VerschlĂŒsselte NachrichtâŠ";
-"room_message_short_placeholder" = "Sende eine NachrichtâŠ";
+"encrypted_room_message_placeholder" = "VerschlĂŒsselte Nachricht senden âŠ";
+"room_message_short_placeholder" = "Nachricht senden âŠ";
"room_offline_notification" = "Verbindung zum Server wurde unterbrochen.";
-"room_unsent_messages_notification" = "Nachrichten wurden nicht gesendet.";
-"room_unsent_messages_unknown_devices_notification" = "Nachrichten wurden nicht gesendet, da unbekannte Sitzungen vorhanden waren.";
+"room_unsent_messages_notification" = "Senden der Nachrichten fehlgeschlagen.";
+"room_unsent_messages_unknown_devices_notification" = "Senden der Nachrichten aufgrund unbekannter Sitzungen fehlgeschlagen.";
"room_prompt_resend" = "Alle erneut senden";
-"room_prompt_cancel" = "Alles abbrechen";
+"room_prompt_cancel" = "Alle abbrechen";
"room_resend_unsent_messages" = "Ungesendete Nachrichten erneut senden";
-"room_delete_unsent_messages" = "Lösche ungesendete Nachrichten";
+"room_delete_unsent_messages" = "Nicht gesendete Nachrichten löschen";
"room_event_action_copy" = "Kopieren";
"room_event_action_quote" = "Zitieren";
"room_event_action_more" = "Mehr";
@@ -301,7 +301,7 @@
"room_participants_action_unban" = "Entsperren";
"room_participants_action_set_default_power_level" = "Besondere Berechtigungen entziehen";
"room_participants_action_start_voice_call" = "Starte Sprach-Anruf";
-"room_ongoing_conference_call" = "Laufender Konferenz-Anruf. Trete bei als %@ oder %@.";
+"room_ongoing_conference_call" = "Laufender Konferenzanruf. Tritt als %@ oder %@ bei.";
"room_event_action_redact" = "Entfernen";
"room_warning_about_encryption" = "Ende-zu-Ende-VerschlĂŒsselung ist in Beta und ist evtl. nicht zuverlĂ€ssig.\n\nMan sollte noch nicht darauf vertrauen, dass die Daten sicher sind.\n\nGerĂ€te werden Nachrichten von vor dem Beitritt des Raumes nicht entschlĂŒsseln können.\n\nVerschlĂŒsselte Nachrichten sind nicht lesbar in Anwendungen, die die VerschlĂŒsselung noch nicht implementiert haben.";
"unknown_devices_alert" = "Dieser Raum enthĂ€lt unbekannte Sitzungen, die nicht verifiziert wurden.\nDas bedeutet, es gibt keine Garantie, dass sie den angegebenen Benutzern gehört.\nWir empfehlen eine ĂberprĂŒfung fĂŒr jedes GerĂ€t, bevor du weitermachst. Du kannst die Nachricht auch ohne Verifizierung erneut senden.";
@@ -411,14 +411,14 @@
"auth_home_server_placeholder" = "URL (z.B. https://matrix.org)";
"auth_identity_server_placeholder" = "URL (z. B. https://vector.im)";
"room_ongoing_conference_call_close" = "SchlieĂen";
-"room_conference_call_no_power" = "Du brauchst die Berechtigung KonferenzgesprÀche in diesem Raum zu verwalten";
+"room_conference_call_no_power" = "Du bist nicht berechtigt, KonferenzgesprÀche in diesem Raum zu verwalten";
"settings_labs_create_conference_with_jitsi" = "Erstelle KonferenzgesprÀche mit Jitsi";
"call_already_displayed" = "Es existiert bereits ein GesprÀch.";
"call_jitsi_error" = "KonferenzgesprÀch konnte nicht betreten werden.";
// Widget
"widget_no_power_to_manage" = "Du brauchst die Berechtigung um Widgets in diesem Raum zu verwalten";
"widget_creation_failure" = "Widget-Erstellung fehlgeschlagen";
-"room_ongoing_conference_call_with_close" = "Laufendes KonferenzgesprÀch. Trete mit %@ oder %@ bei. %@ es.";
+"room_ongoing_conference_call_with_close" = "Laufendes KonferenzgesprÀch. Tritt als %@ oder %@ bei. %@ es.";
"settings_ui_theme" = "Thema";
"settings_ui_theme_auto" = "Auto";
"settings_ui_theme_light" = "Hell";
@@ -436,13 +436,13 @@
"call_incoming_voice" = "Eingehender AnrufâŠ";
"call_incoming_video" = "Eingehender VideoanrufâŠ";
// Widget Integration Manager
-"widget_integration_need_to_be_able_to_invite" = "Du musst Benutzer einladen können um das zu tun.";
+"widget_integration_need_to_be_able_to_invite" = "Du musst Benutzer einladen dĂŒrfen, um dies zu tun.";
"widget_integration_unable_to_create" = "Erstellen des Widgets nicht möglich.";
"widget_integration_failed_to_send_request" = "Senden der Anfrage fehlgeschlagen.";
"widget_integration_room_not_recognised" = "Dieser Raum wurde nicht erkannt.";
"widget_integration_positive_power_level" = "Berechtigungslevel muss eine positive Zahl sein.";
"widget_integration_must_be_in_room" = "Du bist nicht in diesem Raum.";
-"widget_integration_no_permission_in_room" = "Du hast keine Berechtigung dies in diesem Raum zu tun.";
+"widget_integration_no_permission_in_room" = "Du bist nicht berechtigt, dies in diesem Raum zu tun.";
"widget_integration_missing_room_id" = "room_id fehlt in der Anfrage.";
"widget_integration_missing_user_id" = "user_id fehlt in der Anfrage.";
"widget_integration_room_not_visible" = "Raum %@ ist nicht sichtbar.";
@@ -502,7 +502,7 @@
// Group rooms
"group_rooms_filter_rooms" = "Filtere Community-RĂ€ume";
"e2e_room_key_request_message_new_device" = "Du hast die neue Sitzung '%@' hinzugefĂŒgt, welche VerschlĂŒsselungs-SchlĂŒssel anfordert.";
-"room_do_not_have_permission_to_post" = "Du hast keine Berechtigung Nachrichten in diesem Raum zu senden";
+"room_do_not_have_permission_to_post" = "Du bist nicht berechtigt, Nachrichten in diesem Raum zu senden";
"room_event_action_kick_prompt_reason" = "Grund fĂŒr das Entfernen des Benutzers";
"room_event_action_ban_prompt_reason" = "Grund fĂŒr die Verbannung der Person";
"room_action_send_photo_or_video" = "Foto oder Video senden";
@@ -532,8 +532,8 @@
"rerequest_keys_alert_title" = "Anfrage gesendet";
"rerequest_keys_alert_message" = "Bitte %@ auf einem anderen GerĂ€t öffnen, das die Nachricht entschlĂŒsseln kann, damit es die SchlĂŒssel an diese Sitzung senden kann.";
"room_message_reply_to_placeholder" = "Antwort senden (unverschlĂŒsselt)âŠ";
-"encrypted_room_message_reply_to_placeholder" = "Sende eine verschlĂŒsselte AntwortâŠ";
-"room_message_reply_to_short_placeholder" = "Sende eine AntwortâŠ";
+"encrypted_room_message_reply_to_placeholder" = "VerschlĂŒsselte Antwort senden âŠ";
+"room_message_reply_to_short_placeholder" = "Antwort senden âŠ";
"room_replacement_information" = "Dieser Raum wurde ersetzt und ist nicht lÀnger aktiv.";
"room_replacement_link" = "Die Konversation wird hier fortgesetzt.";
"room_predecessor_information" = "Dieser Raum ist die Fortsetzung einer anderen Konversation.";
@@ -743,8 +743,8 @@
"room_action_send_file" = "Datei senden";
"room_message_edits_history_title" = "Bearbeitungsverlauf";
// Widget
-"widget_no_integrations_server_configured" = "Kein Integrationsserver konfiguriert";
-"widget_integrations_server_failed_to_connect" = "Verbindung zum Integrationsserver fehlgeschlagen";
+"widget_no_integrations_server_configured" = "Kein Integrations-Server konfiguriert";
+"widget_integrations_server_failed_to_connect" = "Verbindung zum Integrations-Server fehlgeschlagen";
"device_verification_security_advice" = "FĂŒr maximale Sicherheit empfehlen wir, dies persönlich zu tun oder ein anderes vertrauenswĂŒrdiges Kommunikationsmittel zu verwenden";
"device_verification_incoming_description_1" = "ĂberprĂŒfe diese Sitzung, um sie als vertrauenswĂŒrdig zu markieren. Sitzungen von Partnern zu vertrauen gibt dir zusĂ€tzliche Sicherheit bei der Verwendung von Ende-zu-Ende verschlĂŒsselten Nachrichten.";
"device_verification_incoming_description_2" = "Wenn du diese Sitzung verifizierst, wird sie fĂŒr dich und fĂŒr dein GegenĂŒber als vertrauenswĂŒrdig gekennzeichnet.";
@@ -821,7 +821,7 @@
"media_type_accessibility_video" = "Video";
"media_type_accessibility_location" = "Standort";
"media_type_accessibility_file" = "Datei";
-"media_type_accessibility_sticker" = "Aufkleber";
+"media_type_accessibility_sticker" = "Sticker";
"settings_identity_server_settings" = "IDENTITĂTSERVER";
"settings_three_pids_management_information_part1" = "Verwalte hier, mit welchen E-Mail-Adressen oder Telefonnummern du dich anmeldest, oder dein Konto wiederherstellen kannst. Kontrolliere, wer dich finden kann ";
"settings_three_pids_management_information_part3" = ".";
@@ -902,7 +902,7 @@
"room_participants_security_loading" = "LadeâŠ";
"room_participants_security_information_room_not_encrypted" = "Nachrichten in diesem Raum sind nicht Ende-zu-Ende verschlĂŒsselt.";
"settings_security" = "SICHERHEIT";
-"settings_integrations_allow_description" = "Benutze einen Integrationsmanager (%@), um Bots, Bridges, Widgets und Aufkleberpakete zu verwalten.\n\nIntegrationsmanager erhalten Konfigurationsdaten und können Widgets verÀndern, Raum-Einladungen versenden sowie Berechtigungen in deinem Namen einstellen.";
+"settings_integrations_allow_description" = "Nutze einen Integrationsassistenten (%@), um Bots, BrĂŒcken, Widgets und Sticker-Pakete zu verwalten.\n\nIntegrationsassistenten erhalten Konfigurationsdaten und können Widgets verĂ€ndern, Raumeinladungen versenden sowie Berechtigungen in deinem Namen einstellen.";
"settings_labs_enable_cross_signing" = "Aktiviere Cross-Signing, um deinen GesprÀchspartner anstatt dessen GerÀt zu verifizieren (in Entwicklung)";
// Security settings
"security_settings_title" = "Sicherheit";
@@ -1301,8 +1301,8 @@
"settings_show_NSFW_public_rooms" = "Ăffentliche RĂ€ume mit anstöĂigen Inhalte anzeigen";
"room_open_dialpad" = "WĂ€hltastatur";
"room_place_voice_call" = "Sprachanruf";
-"room_unsent_messages_cancel_message" = "Bist du dir sicher alle nicht gesendete Nachrichten in diesem Raum zu löschen?";
-"room_unsent_messages_cancel_title" = "Lösche nicht gesendete Nachrichten";
+"room_unsent_messages_cancel_message" = "Bist du dir sicher, dass du alle nicht gesendeten Nachrichten in diesem Raum löschen möchtest?";
+"room_unsent_messages_cancel_title" = "Nicht gesendete Nachrichten löschen";
"callbar_return" = "ZurĂŒck";
"callbar_only_multiple_paused" = "%@ pausierte Anrufe";
"callbar_only_single_paused" = "Pausierter Anruf";
@@ -1483,7 +1483,7 @@
// Alert explaining what an identity server / integration manager is.
"service_terms_modal_information_title_identity_server" = "IndentitÀtsserver";
-"service_terms_modal_description_integration_manager" = "Das erlaubt dir Bots, Bridges und Stickerpacks zu verwenden.";
+"service_terms_modal_description_integration_manager" = "Dies wird dir die Verwendung von Bots, BrĂŒcken und Sticker-Paketen ermöglichen.";
"service_terms_modal_description_identity_server" = "Dies erlaubt Personen, die deine Telefonnummer oder E-Mail in ihren Kontakten hat, dich zu finden.";
"service_terms_modal_table_header_identity_server" = "NUTZUNGSBEDINGUNGEN IDENTITĂTSSERVER";
"service_terms_modal_table_header_integration_manager" = "NUTZUNGSBEDINGUNGEN INTEGRATIONSMANAGER";
@@ -1506,12 +1506,12 @@
"poll_edit_form_add_option" = "Option hinzufĂŒgen";
"poll_edit_form_option_number" = "Option %lu";
"poll_edit_form_question_or_topic" = "Frage oder Thematik";
-"room_event_action_end_poll" = "Umfrage beenden";
-"room_event_action_remove_poll" = "Umfrage entfernen";
+"room_event_action_end_poll" = "Abstimmung beenden";
+"room_event_action_remove_poll" = "Abstimmung entfernen";
// Mark: - Polls
-"poll_edit_form_create_poll" = "Umfrage erstellen";
+"poll_edit_form_create_poll" = "Abstimmung erstellen";
"settings_labs_enabled_polls" = "Umfragen";
"share_extension_send_now" = "Jetzt senden";
"accessibility_button_label" = "Knopf";
@@ -1538,7 +1538,7 @@
"poll_edit_form_poll_question_or_topic" = "Frage oder Thema der Umfrage";
"poll_edit_form_input_placeholder" = "Schreib etwas";
"analytics_prompt_terms_link_upgrade" = "hier";
-"poll_timeline_not_closed_title" = "Fehler beim Beenden der Abstimmung";
+"poll_timeline_not_closed_title" = "Beenden der Abstimmung fehlgeschlagen";
"poll_timeline_vote_not_registered_subtitle" = "Wir konnten deine Stimme leider nicht erfassen. Versuche es bitte erneut";
"poll_timeline_total_final_results" = "Es wurden %lu Stimmen abgegeben";
"poll_timeline_total_final_results_one_vote" = "Es wurde 1 Stimme abgegeben";
@@ -1547,7 +1547,7 @@
"poll_timeline_not_closed_subtitle" = "Versuche es bitte erneut";
"poll_timeline_vote_not_registered_title" = "Stimme nicht erfasst";
"poll_edit_form_post_failure_subtitle" = "Versuche es bitte erneut";
-"poll_edit_form_post_failure_title" = "Fehler beim Senden der Abstimmung";
+"poll_edit_form_post_failure_title" = "Absenden der Abstimmung fehlgeschlagen";
"share_extension_low_quality_video_message" = "FĂŒr eine bessere QualitĂ€t sende es in %@ oder sende es in niedriger QualitĂ€t.";
"share_extension_low_quality_video_title" = "Das Video wird in niedriger QualitÀt gesendet werden";
"analytics_prompt_stop" = "Teilen beenden";
@@ -1588,11 +1588,11 @@
"onboarding_splash_register_button_title" = "Konto erstellen";
"settings_enable_room_message_bubbles" = "Nachrichtenblasen";
"poll_edit_form_update_failure_subtitle" = "Bitte erneut versuchen";
-"poll_edit_form_poll_type" = "Umfragetyp";
-"poll_edit_form_poll_type_closed_description" = "Ergebnisse werden erst angezeigt, wenn du die Umfrage beendest";
-"poll_edit_form_poll_type_closed" = "Geschlossene Umfrage";
-"poll_edit_form_poll_type_open_description" = "Ergebnisse werden direkt nach Stimmabgabe angezeigt";
-"poll_edit_form_poll_type_open" = "Offene Umfrage";
+"poll_edit_form_poll_type" = "Abstimmungsart";
+"poll_edit_form_poll_type_closed_description" = "Die Ergebnisse werden erst sichtbar, sobald du die Umfrage beendest";
+"poll_edit_form_poll_type_closed" = "Abgeschlossene Abstimmung";
+"poll_edit_form_poll_type_open_description" = "Abstimmende können die Ergebnisse nach Stimmabgabe sehen";
+"poll_edit_form_poll_type_open" = "Laufende Abstimmung";
"poll_edit_form_update_failure_title" = "Aktualisierung der Umfrage fehlgeschlagen";
"threads_empty_tip" = "Hinweis: Tippe auf eine Nachricht und wĂ€hle âThreadâ um einen neuen zu starten.";
"threads_empty_info_my" = "Antworte auf einen laufenden Thread oder tippe auf eine Nachricht und wĂ€hle âThreadâ um einen neuen zu starten.";
@@ -1766,7 +1766,7 @@
"notice_room_history_visible_to_members_from_joined_point_for_dm" = "%@ hat den zukĂŒnftigen Verlauf fĂŒr alle Raumteilnehmer ab deren Einladung sichtbar gemacht.";
"notice_crypto_unable_to_decrypt" = "** EntschlĂŒsselung nicht möglich: %@ **";
"notice_crypto_error_unknown_inbound_session_id" = "Die absendende Sitzung hat uns keine SchlĂŒssel fĂŒr diese Nachricht gesendet.";
-"notice_sticker" = "Aufkleber";
+"notice_sticker" = "Sticker";
"notice_in_reply_to" = "Als Antwort auf";
// room display name
"room_displayname_empty_room" = "Leerer Raum";
@@ -2365,7 +2365,7 @@
"spaces_explore_rooms_room_number" = "%@ RĂ€ume";
"spaces_create_space_title" = "Einen Space erstellen";
"spaces_add_space_title" = "Space erstellen";
-"space_invite_not_enough_permission" = "Du hast keine Berechtigung, Personen zu diesem Space einzuladen";
+"space_invite_not_enough_permission" = "Du hast keine Berechtigung, Personen in diesen Space einzuladen";
"room_invite_not_enough_permission" = "Du hast keine Berechtigung, Personen zu diesem Raum einzuladen";
"room_invite_to_room_option_detail" = "Sie werden kein Teil von %@ sein.";
"room_invite_to_room_option_title" = "Nur zu diesem Raum";
@@ -2639,3 +2639,15 @@
// Send Media Actions
"wysiwyg_composer_start_action_media_picker" = "Fotobibliothek";
"settings_labs_enable_wysiwyg_composer" = "Probiere den Rich-Text-Editor aus (bald auch mit Plain-Text-Modus)";
+"wysiwyg_composer_start_action_voice_broadcast" = "SprachĂŒbertragung";
+"voice_broadcast_already_in_progress_message" = "Du zeichnest bereits eine SprachĂŒbertragung auf. Bitte beende die laufende Ăbertragung, um eine neue zu beginnen.";
+"voice_broadcast_blocked_by_someone_else_message" = "Jemand anderes nimmt bereits eine SprachĂŒbertragung auf. Warte auf das Ende der Ăbertragung, bevor du eine neue startest.";
+"voice_broadcast_permission_denied_message" = "Du hast nicht die nötigen Berechtigungen, um eine SprachĂŒbertragung in diesem Raum zu starten. Kontaktiere einen Raumadministrator, um deine Berechtigungen anzupassen.";
+
+// Mark: - Voice broadcast
+"voice_broadcast_unauthorized_title" = "SprachĂŒbertragung kann nicht gestartet werden";
+"settings_labs_enable_voice_broadcast" = "SprachĂŒbertragung (in aktiver Entwicklung)";
+"voice_broadcast_playback_loading_error" = "Wiedergabe der SprachĂŒbertragung nicht möglich.";
+"deselect_all" = "Alle abwÀhlen";
+"user_other_session_menu_select_sessions" = "Sitzungen auswÀhlen";
+"user_other_session_selected_count" = "%@ ausgewÀhlt";
diff --git a/Riot/Assets/en.lproj/Untranslated.strings b/Riot/Assets/en.lproj/Untranslated.strings
index 9136db0861..6d9320f4a1 100644
--- a/Riot/Assets/en.lproj/Untranslated.strings
+++ b/Riot/Assets/en.lproj/Untranslated.strings
@@ -20,4 +20,3 @@
"image_picker_action_files" = "Choose from files";
"voice_broadcast_in_timeline_title" = "Voice broadcast detected (under active development)";
-"voice_broadcast_in_timeline_body" = "We currently only detect voice broadcast in the room timeline, this is not possible to send or listen an actual voice broadcast";
diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings
index 025415be39..1f04c58a2f 100644
--- a/Riot/Assets/en.lproj/Vector.strings
+++ b/Riot/Assets/en.lproj/Vector.strings
@@ -798,7 +798,7 @@ Tap the + to start adding people.";
"settings_labs_enable_new_client_info_feature" = "Record the client name, version, and url to recognise sessions more easily in session manager";
"settings_labs_enable_new_app_layout" = "New Application Layout";
"settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor (plain text mode coming soon)";
-"settings_labs_enable_voice_broadcast" = "Voice broadcast (under active development). We currently only detect voice broadcast in the room timeline, this is not possible to send or listen an actual voice broadcast";
+"settings_labs_enable_voice_broadcast" = "Voice broadcast (under active development)";
"settings_version" = "Version %@";
"settings_olm_version" = "Olm Version %@";
@@ -2189,6 +2189,13 @@ Tap the + to start adding people.";
"voice_message_stop_locked_mode_recording" = "Tap on your recording to stop or listen";
"voice_message_lock_screen_placeholder" = "Voice message";
+// Mark: - Voice broadcast
+"voice_broadcast_unauthorized_title" = "Can't start a new voice broadcast";
+"voice_broadcast_permission_denied_message" = "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.";
+"voice_broadcast_blocked_by_someone_else_message" = "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.";
+"voice_broadcast_already_in_progress_message" = "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.";
+"voice_broadcast_playback_loading_error" = "Unable to play this voice broadcast.";
+
// Mark: - Version check
"version_check_banner_title_supported" = "Weâre ending support for iOS %@";
@@ -2453,6 +2460,8 @@ To enable access, tap Settings> Location and select Always";
"user_other_session_no_verified_sessions" = "No verified sessions found.";
"user_other_session_no_unverified_sessions" = "No unverified sessions found.";
"user_other_session_clear_filter" = "Clear filter";
+"user_other_session_selected_count" = "%@ selected";
+"user_other_session_menu_select_sessions" = "Select sessions";
// First item is client name and second item is session display name
"user_session_name" = "%@: %@";
@@ -2503,6 +2512,7 @@ To enable access, tap Settings> Location and select Always";
"wysiwyg_composer_start_action_location" = "Location";
"wysiwyg_composer_start_action_camera" = "Camera";
"wysiwyg_composer_start_action_text_formatting" = "Text Formatting";
+"wysiwyg_composer_start_action_voice_broadcast" = "Voice broadcast";
// Formatting Actions
"wysiwyg_composer_format_action_bold" = "Apply bold format";
@@ -2582,6 +2592,7 @@ To enable access, tap Settings> Location and select Always";
"reset_to_default" = "Reset to default";
"resend_message" = "Resend the message";
"select_all" = "Select All";
+"deselect_all" = "Deselect All";
"cancel_upload" = "Cancel Upload";
"cancel_download" = "Cancel Download";
"show_details" = "Show Details";
diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings
index f9bdd08c56..1dd7c1b42a 100644
--- a/Riot/Assets/et.lproj/Vector.strings
+++ b/Riot/Assets/et.lproj/Vector.strings
@@ -2506,3 +2506,86 @@
"authentication_qr_login_start_subtitle" = "Kasuta selle seadme kaamerat ja logi sisse teises seadmes kuvatud QR-koodi alusel:";
"authentication_qr_login_start_title" = "Loe QR-koodi";
"authentication_login_with_qr" = "Logi sisse QR-koodi abil";
+"wysiwyg_composer_format_action_strikethrough" = "Kasuta allajoonitud kirja";
+"wysiwyg_composer_format_action_underline" = "Kasuta lÀbijoonitud kirja";
+"wysiwyg_composer_format_action_italic" = "Kasuta kaldkirja";
+
+// Formatting Actions
+"wysiwyg_composer_format_action_bold" = "Kasuta paksu kirja";
+"wysiwyg_composer_start_action_voice_broadcast" = "RinghÀÀlingukÔne";
+"wysiwyg_composer_start_action_text_formatting" = "Tekstivorming";
+"wysiwyg_composer_start_action_camera" = "Kaamera";
+"wysiwyg_composer_start_action_location" = "Asukoht";
+"wysiwyg_composer_start_action_polls" = "KĂŒsitlused";
+"wysiwyg_composer_start_action_attachments" = "Manused";
+"wysiwyg_composer_start_action_stickers" = "Kleepsud";
+
+
+// Mark: - WYSIWYG Composer
+
+// Send Media Actions
+"wysiwyg_composer_start_action_media_picker" = "Fotode kogu";
+"user_session_details_last_activity" = "Viimati kasutusel";
+"device_type_name_unknown" = "Tundmatu seadmetĂŒĂŒp";
+"device_type_name_mobile" = "Mobiiltelefon";
+"device_type_name_web" = "Veebiliides";
+"device_type_name_desktop" = "Töölauarakendus";
+"user_inactive_session_item_with_date" = "Pole olnud kasutusel ĂŒle 90 pĂ€eva (%@)";
+"user_inactive_session_item" = "Pole olnud kasutusel ĂŒle 90 pĂ€eva";
+"user_session_item_details_last_activity" = "Viimati kasutusel %@";
+"user_other_session_clear_filter" = "Eemalda filter";
+"user_other_session_no_unverified_sessions" = "Verifitseerimata sessioone ei leidu.";
+"user_other_session_no_verified_sessions" = "Verifitseeritud sessioone ei leidu.";
+"user_other_session_no_inactive_sessions" = "Ei leidu sessioone, mis pole aktiivses kasutuses.";
+"user_other_session_filter_menu_inactive" = "Pole pidevas kasutuses";
+"user_other_session_filter_menu_unverified" = "Verifitseerimata";
+"user_other_session_filter_menu_verified" = "Verifitseeritud";
+"user_other_session_filter_menu_all" = "KÔik sessioonid";
+"user_other_session_filter" = "Filtreeri";
+"user_other_session_verified_sessions_header_subtitle" = "Parima turvalisuse nimel logi vÀlja neist sessioonidest, mida sa enam ei kasuta vÔi ei tunne Àra.";
+"user_other_session_current_session_details" = "Sinu praegune sessioon";
+"user_other_session_unverified_sessions_header_subtitle" = "Turvalise sÔnumvahetuse nimel verifitseeri kÔik oma sessioonid ning logi neist vÀlja, mida sa enam ei kasuta vÔi ei tunne enam Àra.";
+"user_other_session_security_recommendation_title" = "Turvalisusega seotud soovitused";
+"user_other_session_verified_additional_info" = "See sessioon on valmis turvaliseks sÔnumivahetuseks.";
+"user_other_session_unverified_additional_info" = "Parima turvalisuse ja töökindluse nimel verifitseeri see sessioon vÔi logi ta vÔrgust vÀlja.";
+"user_session_verification_unknown_additional_info" = "Selle sessiooni olekut ei saa tuvastada enne kui oled ta verifitseerinud.";
+"user_session_verification_unknown_short" = "Teadmata olek";
+"user_session_verification_unknown" = "Verifitseerimise olek on mÀÀratlemata";
+"user_sessions_overview_link_device" = "Seo teise seadmega";
+
+// MARK: User sessions management
+
+// Parameter is the application display name (e.g. "Element")
+"user_sessions_default_session_display_name" = "%@ iOS";
+"voice_broadcast_playback_loading_error" = "Selle ringhÀÀlingukÔne esitamine ei Ônnestu.";
+"voice_broadcast_already_in_progress_message" = "Sa juba salvestad ringhÀÀlingukÔnet. Uue alustamiseks palun lÔpeta eelmine salvestus.";
+"voice_broadcast_blocked_by_someone_else_message" = "Keegi juba salvestab ringhÀÀlingukÔnet. Uue ringhÀÀlingukÔne salvestamiseks palun oota, kuni see teine ringhÀÀlingukÔne on lÔppenud.";
+"voice_broadcast_permission_denied_message" = "Sul pole piisavalt Ă”igusi selles jututoas ringhÀÀlingukĂ”ne algatamiseks. Ăiguste lisamiseks palun vĂ”ta ĂŒhendust jututoa haldajaga.";
+
+// Mark: - Voice broadcast
+"voice_broadcast_unauthorized_title" = "Uue ringhÀÀlingukÔne alustamine pole vÔimalik";
+"sign_out_confirmation_message" = "Kas sa oled kindel et soovid vÀlja logida?";
+
+// MARK: Sign out warning
+
+"sign_out" = "Logi vÀlja";
+"manage_session_rename" = "Muuda sessiooni nime";
+"manage_session_name_info_link" = "Lisateave";
+/* The placeholder will be replaces with manage_session_name_info_link */
+"manage_session_name_info" = "Palun arvesta, et sessioonide nimed on nÀha ka kÔikidele osapooltele, kellega sa suhtled. %@";
+"manage_session_name_hint" = "Sinu enda kirjutatud sessiooninimede alusel on sul oma seadmeid lihtsam Àra tunda.";
+"settings_labs_enable_voice_broadcast" = "RinghÀÀlingukÔne (aktiivses arenduses)";
+"settings_labs_enable_wysiwyg_composer" = "Proovi vormindatud teksti alusel töötavat tekstitoimetit (varsti lisandub ka vormindamata teksti reĆŸiim)";
+"authentication_qr_login_failure_retry" = "Proovi uuesti";
+"authentication_qr_login_failure_request_timed_out" = "Sidumine ei lÔppenud etteantud aja jooksul.";
+"authentication_qr_login_failure_request_denied" = "Teine seade lĂŒkkas pĂ€ringu tagasi.";
+"authentication_qr_login_failure_invalid_qr" = "QR-kood on vigane.";
+"authentication_qr_login_failure_title" = "Seose loomine ei Ônenstunud";
+"authentication_qr_login_loading_signed_in" = "Sa oled oma teises seadmes sisse loginud Matrix'i vÔrku.";
+"authentication_qr_login_loading_waiting_signin" = "Ootame, et teine seade logiks vÔrku.";
+"authentication_qr_login_loading_connecting_device" = "Loon ĂŒhendust seadmega";
+"authentication_qr_login_confirm_alert" = "Palun vaata, et sa kindlasti tead, kust see QR-kood kuvatakse. Sellisel viisil seadmete sidumisel sa annad oma kasutajakontole tÀiemahulise ligipÀÀsu.";
+"authentication_qr_login_confirm_subtitle" = "Kontrolli, et jÀrgnev kood klapib teises seadmes kuvatava koodiga:";
+"deselect_all" = "Eemalda kÔik valikud";
+"user_other_session_menu_select_sessions" = "Vali sessioonid";
+"user_other_session_selected_count" = "%@ valitud";
diff --git a/Riot/Assets/fa.lproj/Vector.strings b/Riot/Assets/fa.lproj/Vector.strings
index 8ae59ee6b7..fe51dcd78f 100644
--- a/Riot/Assets/fa.lproj/Vector.strings
+++ b/Riot/Assets/fa.lproj/Vector.strings
@@ -1270,3 +1270,23 @@
"microphone_access_not_granted_for_voice_message" = "ŰŹÙŰȘ Ű§Ű±ŰłŰ§Ù ÙŸÛۧÙ
Ű”ÙŰȘÛ ÙÛۧŰČ ŰšÙ ŰŻŰłŰȘŰ±ŰłÛ ŰšÙ Ù
Ûک۱ÙÙÙÙ ÙŰŹÙŰŻ ۯۧ۱ۯ ۧÙ
ۧ %@ ŰŻŰłŰȘŰ±ŰłÛ Ű§ŰłŰȘÙŰ§ŰŻÙ Ű§ŰČ ŰąÙ Ű±Ű§ Ùۯۧ۱ۯ";
"e2e_passphrase_too_short" = "Ú©ÙÙ
Ù ŰčŰšÙ۱ ŰšÛŰŽ ۧŰČ ŰŰŻ Ú©ÙŰȘŰ§Ù Ű§ŰłŰȘ (ŰۯۧÙÙ Ù
ÛâۚۧÛŰłŰȘ %d کۧ۱ۧکŰȘ۱ ۚۧێۯ)";
"message_reply_to_sender_sent_a_voice_message" = "ÛÚ© ÙŸÛۧÙ
Ű”ÙŰȘÛ Ű§Ű±ŰłŰ§Ù Ú©ÙÛŰŻ.";
+"onboarding_splash_page_1_title" = "۔ۧŰŰš ÚŻÙŰȘÚŻÙÙŰ§Û ŰźÙŰŻ ŰŽÙÛŰŻ.";
+"onboarding_splash_login_button_title" = "Ù
Ù Ű§ŰČ ÙŰšÙ Ű۳ۧۚ Ú©Ű§Ű±ŰšŰ±Û ŰŻŰ§Ű±Ù
";
+
+// MARK: Onboarding
+"onboarding_splash_register_button_title" = "۳ۧ۟ŰȘ Ű۳ۧۚ کۧ۱ۚ۱Û";
+"accessibility_button_label" = "ŰŻÚ©Ù
Ù";
+"saving" = "ۯ۱ ŰŰ§Ù Ű°ŰźÛ۱Ù";
+
+// Activities
+"loading" = "ۯ۱ ŰŰ§Ù ŰšŰ§Ű±ÚŻŰČۧ۱Û";
+"invite_to" = "ŰŻŰčÙŰȘ ŰšÙ %@";
+"confirm" = "ŰȘŰŁÛÛŰŻ";
+"edit" = "ÙÛ۱ۧÛŰŽ";
+"suggest" = "ÙŸÛŰŽÙÙۧۯ";
+"add" = "ۧÙŰČÙŰŻÙ";
+"existing" = "۟۱ÙŰŹ";
+"new_word" = "ŰŹŰŻÛŰŻ";
+"stop" = "ŰȘÙÙÙ";
+"joining" = "ÙŸÛÙŰłŰȘÙ";
+"enable" = "ÙŰčۧÙ";
diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings
index dc87963f70..1f72aab670 100644
--- a/Riot/Assets/hu.lproj/Vector.strings
+++ b/Riot/Assets/hu.lproj/Vector.strings
@@ -2625,3 +2625,15 @@
"authentication_qr_login_start_subtitle" = "HasznĂĄld a kamerĂĄt ezen az eszközön a mĂĄsik eszközödön megjelenĆ QR kĂłd beolvasĂĄsĂĄra:";
"authentication_qr_login_start_title" = "QR kĂłd beolvasĂĄsa";
"authentication_login_with_qr" = "Belépés QR kóddal";
+"settings_labs_enable_voice_broadcast" = "Hang közvetĂtĂ©s (aktĂv fejlesztĂ©s alatt)";
+"wysiwyg_composer_start_action_voice_broadcast" = "Hang közvetĂtĂ©s";
+"voice_broadcast_playback_loading_error" = "A hang közvetĂtĂ©s nem jĂĄtszhatĂł le.";
+"voice_broadcast_already_in_progress_message" = "Egy hang közvetĂtĂ©s mĂĄr folyamatban van. ElĆször fejezd be a jelenlegi közvetĂtĂ©st egy Ășj indĂtĂĄsĂĄhoz.";
+"voice_broadcast_blocked_by_someone_else_message" = "Valaki mĂĄr elindĂtott egy hang közvetĂtĂ©st. VĂĄrd meg a közvetĂtĂ©s vĂ©gĂ©t az Ășj indĂtĂĄsĂĄhoz.";
+"voice_broadcast_permission_denied_message" = "Nincs jogosultsĂĄgod hang közvetĂtĂ©st indĂtani ebben a szobĂĄban. Vedd fel a kapcsolatot a szoba adminisztrĂĄtorĂĄval a szĂŒksĂ©ges jogosultsĂĄg megszerzĂ©sĂ©hez.";
+
+// Mark: - Voice broadcast
+"voice_broadcast_unauthorized_title" = "Az Ășj hang közvetĂtĂ©s nem indĂthatĂł el";
+"deselect_all" = "Semmit nem jelöl ki";
+"user_other_session_menu_select_sessions" = "Munkamenetek kivĂĄlasztĂĄsa";
+"user_other_session_selected_count" = "%@ kivĂĄlasztva";
diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings
index 210d60bb34..be31a8bdb3 100644
--- a/Riot/Assets/id.lproj/Vector.strings
+++ b/Riot/Assets/id.lproj/Vector.strings
@@ -2832,3 +2832,15 @@
"manage_session_name_info" = "Harap diketahui bahwa nama sesi juga terlihat ke orang-orang yang Anda berkomunikasi. %@";
"manage_session_name_hint" = "Nama sesi khusus dapat membantu Anda mengenal perangkat Anda dengan lebih mudah.";
"settings_labs_enable_wysiwyg_composer" = "Coba editor teks kaya (mode teks biasa akan datang)";
+"wysiwyg_composer_start_action_voice_broadcast" = "Siaran suara";
+"voice_broadcast_playback_loading_error" = "Tidak dapat memainkan siaran suara ini.";
+"voice_broadcast_already_in_progress_message" = "Anda saat ini merekam sebuah siaran suara. Mohon akhiri siaran suara Anda saat ini untuk memulai yang baru.";
+"voice_broadcast_blocked_by_someone_else_message" = "Ada orang lain yang saat ini merekam sebuah siaran suara. Tunggu siaran suaranya berakhir untuk memulai yang baru.";
+"voice_broadcast_permission_denied_message" = "Anda tidak memiliki izin untuk memulai sebuah siaran suara di ruangan ini. Hubungi sebuah administrator ruangan untuk meningkatkan izin Anda.";
+
+// Mark: - Voice broadcast
+"voice_broadcast_unauthorized_title" = "Tidak dapat memulai sebuah siaran suara baru";
+"settings_labs_enable_voice_broadcast" = "Siaran suara (dalam pengembangan aktif)";
+"deselect_all" = "Batalkan Semua Pilihan";
+"user_other_session_menu_select_sessions" = "Pilih sesi";
+"user_other_session_selected_count" = "%@ dipilih";
diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings
index 671e2d08bc..0637f0fe9a 100644
--- a/Riot/Assets/it.lproj/Vector.strings
+++ b/Riot/Assets/it.lproj/Vector.strings
@@ -2605,4 +2605,15 @@
"manage_session_name_info" = "Ricorda che i nomi di sessione sono anche visibili alle persone con cui comunichi. %@";
"manage_session_name_hint" = "I nomi di sessione personalizzati possono aiutarti a riconoscere i tuoi dispositivi piĂč facilmente.";
"settings_labs_enable_wysiwyg_composer" = "Prova l'editor in rich text (il testo semplice Ăš in arrivo)";
-"settings_labs_enable_voice_broadcast" = "Broadcast voce (in sviluppo attivo). Attualmente rileviamo solo il broadcast vocale nella linea temporale della stanza, non Ăš possibile inviare o ascoltare un vero broadcast vocale";
+"settings_labs_enable_voice_broadcast" = "Trasmissione vocale (in sviluppo attivo)";
+"wysiwyg_composer_start_action_voice_broadcast" = "Trasmissione vocale";
+"voice_broadcast_playback_loading_error" = "Impossibile avviare questa trasmissione vocale.";
+"voice_broadcast_already_in_progress_message" = "Stai giĂ registrando una trasmissione vocale. Termina quella in corso per iniziarne una nuova.";
+"voice_broadcast_blocked_by_someone_else_message" = "Qualcun altro sta giĂ registrando una trasmissione vocale. Aspetta che finisca prima di iniziarne una nuova.";
+"voice_broadcast_permission_denied_message" = "Non hai l'autorizzazione necessaria per iniziare un broadcast vocale in questa stanza. Contatta un amministratore della stanza per aggiornare le tue autorizzazioni.";
+
+// Mark: - Voice broadcast
+"voice_broadcast_unauthorized_title" = "Impossibile iniziare una nuova trasmissione vocale";
+"deselect_all" = "Deseleziona tutti";
+"user_other_session_menu_select_sessions" = "Seleziona sessioni";
+"user_other_session_selected_count" = "%@ selezionate";
diff --git a/Riot/Assets/nl.lproj/Vector.strings b/Riot/Assets/nl.lproj/Vector.strings
index 254398f9dc..297ff79b09 100644
--- a/Riot/Assets/nl.lproj/Vector.strings
+++ b/Riot/Assets/nl.lproj/Vector.strings
@@ -2463,7 +2463,7 @@
"room_access_settings_screen_upgrade_alert_note" = "Houd er rekening mee dat bij het upgraden een nieuwe versie van de kamer wordt gemaakt. Alle huidige berichten blijven in deze gearchiveerde ruimte.";
"room_access_settings_screen_upgrade_alert_message_no_param" = "Iedereen in een bovenliggende space kan deze ruimte vinden en er lid van worden. Het is niet nodig om iedereen handmatig uit te nodigen. U kunt dit op elk moment wijzigen in de kamerinstellingen.";
"room_access_settings_screen_upgrade_alert_message" = "Iedereen in %@ kan deze ruimte vinden en er lid van worden - het is niet nodig om iedereen handmatig uit te nodigen. U kunt dit op elk moment wijzigen in de kamerinstellingen.";
-"settings_presence_offline_mode_description" = "Indien ingeschakeld, verschijnt u altijd offline voor andere personen, zelfs wanneer u de applicatie gebruikt.";
+"settings_presence_offline_mode_description" = "Indien ingeschakeld, verschijnt u altijd offline voor andere personen, zelfs wanneer u de toepassing gebruikt.";
"settings_presence_offline_mode" = "Offline modus";
"settings_presence" = "Aanwezigheid";
"threads_discourage_information_2" = "\n\nWilt u toch threads inschakelen?";
@@ -2652,3 +2652,156 @@
// User sessions management
"user_sessions_settings" = "Beheer sessies";
"invite_to" = "Uitnodigen %@";
+"room_event_encryption_info_key_authenticity_not_guaranteed" = "De authenticiteit van dit versleutelde bericht kan niet worden gegarandeerd op dit apparaat.";
+"deselect_all" = "Deselecteer alles";
+"wysiwyg_composer_format_action_strikethrough" = "Onderstrepen formaat toepassen";
+"wysiwyg_composer_format_action_underline" = "Doorstrepen formaat toepassen";
+"wysiwyg_composer_format_action_italic" = "Cursief formaat toepassen";
+
+// Formatting Actions
+"wysiwyg_composer_format_action_bold" = "Vet formaat toepassen";
+"wysiwyg_composer_start_action_voice_broadcast" = "Spraakuitzending";
+"wysiwyg_composer_start_action_text_formatting" = "Tekst opmaak";
+"wysiwyg_composer_start_action_camera" = "Camera";
+"wysiwyg_composer_start_action_location" = "Locatie";
+"wysiwyg_composer_start_action_polls" = "Peilingen";
+"wysiwyg_composer_start_action_attachments" = "Bijlagen";
+"wysiwyg_composer_start_action_stickers" = "Stikkers";
+
+
+// Mark: - WYSIWYG Composer
+
+// Send Media Actions
+"wysiwyg_composer_start_action_media_picker" = "Fotobibliotheek";
+"user_session_overview_session_details_button_title" = "Sessie details";
+"user_session_overview_session_title" = "Sessie";
+"user_session_overview_current_session_title" = "Huidige sessie";
+"user_session_details_application_url" = "URL";
+"user_session_details_application_version" = "Versie";
+"user_session_details_application_name" = "Naam";
+"user_session_details_device_os" = "Besturingssysteem";
+"user_session_details_device_browser" = "Browser";
+"user_session_details_device_model" = "Model";
+"user_session_details_device_ip_location" = "IP locatie";
+"user_session_details_device_ip_address" = "IP adres";
+"user_session_details_last_activity" = "Laatste activiteit";
+"user_session_details_session_section_footer" = "Kopieer alle gegevens door erop te tikken en ingedrukt te houden.";
+"user_session_details_session_id" = "Sessie ID";
+"user_session_details_session_name" = "Sessie naam";
+"user_session_details_device_section_header" = "Apparaat";
+"device_name_unknown" = "Onbekende toepassing";
+"settings_labs_enable_new_app_layout" = "Nieuwe toepassing-indeling";
+"settings_labs_enable_new_client_info_feature" = "Noteer de naam, versie en url van de toepassing om sessies gemakkelijker te herkennen in sessiebeheer";
+"user_session_details_application_section_header" = "Toepassing";
+"user_session_details_session_section_header" = "Sessie";
+"user_session_details_title" = "Toon details";
+"device_type_name_unknown" = "Onbekend";
+"device_type_name_mobile" = "Mobiel";
+"device_type_name_web" = "Web";
+"device_type_name_desktop" = "Desktop";
+"device_name_mobile" = "%@ Mobiel";
+"device_name_web" = "%@ Web";
+"device_name_desktop" = "%@ Desktop";
+"user_inactive_session_item_with_date" = "Meer dan 90 dagen inactief (%@)";
+"user_inactive_session_item" = "90+ dagen inactief";
+"user_session_item_details_last_activity" = "Laatste activiteit %@";
+
+/* %1$@ will be the verification state and %2$@ will be user_session_item_details_verification_unknown or user_other_session_current_session_details */
+"user_session_item_details" = "%1$@ · %2$@";
+// First item is client name and second item is session display name
+"user_session_name" = "%@: %@";
+"user_other_session_menu_select_sessions" = "Selecteer sessies";
+"user_other_session_selected_count" = "%@ geselecteerd";
+"user_other_session_clear_filter" = "Leeg filter";
+"user_other_session_no_unverified_sessions" = "Geen niet geverifieerde sessies gevonden.";
+"user_other_session_no_verified_sessions" = "Geen geverifieerde sessies gevonden.";
+"user_other_session_no_inactive_sessions" = "Geen inactieve sessies gevonden.";
+"user_other_session_filter_menu_inactive" = "Inactief";
+"user_other_session_filter_menu_unverified" = "Niet geverifieerd";
+"user_other_session_filter_menu_verified" = "Geverifieerd";
+"user_other_session_filter_menu_all" = "Alle sessies";
+"user_other_session_filter" = "Filter";
+"user_other_session_verified_sessions_header_subtitle" = "Voor de beste beveiliging logt u uit bij elke sessie die u niet meer herkent of gebruikt.";
+"user_other_session_current_session_details" = "Uw huidige sessie";
+"user_other_session_unverified_sessions_header_subtitle" = "Verifieer uw sessies voor verbeterde beveiligde berichtenuitwisseling of meld u af bij sessies die u niet meer herkent of gebruikt.";
+"user_other_session_security_recommendation_title" = "Beveiligingsaanbeveling";
+"user_session_push_notifications_message" = "Indien ingeschakeld, ontvangt deze sessie pushmeldingen.";
+"user_session_push_notifications" = "Pushmeldingen";
+"user_other_session_verified_additional_info" = "Deze sessie is klaar voor beveiligde berichtenuitwisseling.";
+"user_other_session_unverified_additional_info" = "Verifieer of meld u af bij deze sessie voor de beste beveiliging en betrouwbaarheid.";
+"user_session_verification_unknown_additional_info" = "Verifieer uw huidige sessie om de verificatiestatus van deze sessie weer te geven.";
+"user_session_unverified_additional_info" = "Verifieer uw huidige sessie voor verbeterde beveiligde berichtenuitwisseling.";
+"user_session_verified_additional_info" = "Uw huidige sessie is klaar voor beveiligde berichtenuitwisseling.";
+"user_session_learn_more" = "Meer lezen";
+"user_session_view_details" = "Bekijk details";
+"user_session_verify_action" = "Sessie verifiëren";
+"user_session_verification_unknown_short" = "Onbekend";
+"user_session_unverified_short" = "Niet geverifieerd";
+"user_session_verified_short" = "Geverifieerd";
+"user_session_verification_unknown" = "Onbekende verificatiestatus";
+"user_session_unverified" = "Niet geverifieerde sessie";
+"user_session_verified" = "Geverifieerde sessie";
+"user_sessions_view_all_action" = "Alles bekijken (%d)";
+"user_sessions_overview_link_device" = "Een apparaat koppelen";
+"user_sessions_overview_current_session_section_title" = "Huidige sessie";
+"user_sessions_overview_other_sessions_section_info" = "Voor de beste beveiliging verifieert u uw sessies en meldt u zich af bij elke sessie die u niet meer herkent of gebruikt.";
+"user_sessions_overview_other_sessions_section_title" = "Andere sessies";
+"user_sessions_overview_security_recommendations_inactive_info" = "Overweeg om u af te melden bij oude sessies (90 dagen of ouder) die u niet meer gebruikt.";
+"user_sessions_overview_security_recommendations_inactive_title" = "Inactieve sessies";
+"user_sessions_overview_security_recommendations_unverified_info" = "Verifieer of meld u af bij niet geverifieerde sessies.";
+"user_sessions_overview_security_recommendations_unverified_title" = "Niet geverifieerde sessies";
+"user_sessions_overview_security_recommendations_section_info" = "Verbeter uw accountbeveiliging door deze aanbevelingen op te volgen.";
+"user_sessions_overview_security_recommendations_section_title" = "Beveiligingsaanbevelingen";
+
+// MARK: User sessions management
+
+// Parameter is the application display name (e.g. "Element")
+"user_sessions_default_session_display_name" = "%@ iOS";
+"all_chats_user_menu_accessibility_label" = "Gebruikersmenu";
+"voice_broadcast_playback_loading_error" = "Kan deze spraakuitzending niet afspelen.";
+"voice_broadcast_already_in_progress_message" = "U neemt al een spraakuitzending op. Beëindig uw huidige spraakuitzending om een nieuwe te starten.";
+"voice_broadcast_blocked_by_someone_else_message" = "Iemand anders neemt al een spraakuitzending op. Wacht tot hun spraakuitzending is afgelopen om een nieuwe te starten.";
+"voice_broadcast_permission_denied_message" = "U heeft niet de vereiste rechten om een spraakuitzending in deze kamer te starten. Neem contact op met een kamer beheerder om uw machtigingen te upgraden.";
+
+// Mark: - Voice broadcast
+"voice_broadcast_unauthorized_title" = "Kan geen nieuwe spraakuitzending starten";
+"sign_out_confirmation_message" = "Weet u zeker dat u zich wilt afmelden?";
+
+// MARK: Sign out warning
+
+"sign_out" = "Afmelden";
+"manage_session_rename" = "Sessie hernoemen";
+"manage_session_name_info_link" = "Lees meer";
+/* The placeholder will be replaces with manage_session_name_info_link */
+"manage_session_name_info" = "Houd er rekening mee dat sessienamen ook zichtbaar zijn voor mensen met wie u communiceert. %@";
+"manage_session_name_hint" = "Met aangepaste sessienamen kunt u uw apparaten gemakkelijker herkennen.";
+"settings_labs_enable_voice_broadcast" = "Voice-uitzending (in actieve ontwikkeling)";
+"settings_labs_enable_wysiwyg_composer" = "Probeer de rich-text-editor (platte tekst-modus komt binnenkort)";
+"settings_labs_enable_new_session_manager" = "Nieuwe sessiemanager";
+"room_first_message_placeholder" = "Stuur uw eerste berichtâŠ";
+"authentication_qr_login_failure_retry" = "Probeer het nog eens";
+"authentication_qr_login_failure_request_timed_out" = "De koppeling is niet binnen de vereiste tijd voltooid.";
+"authentication_qr_login_failure_request_denied" = "Het verzoek is geweigerd op het andere apparaat.";
+"authentication_qr_login_failure_invalid_qr" = "QR-code is ongeldig.";
+"authentication_qr_login_failure_title" = "Koppelen mislukt";
+"authentication_qr_login_loading_signed_in" = "U bent nu aangemeld op uw andere apparaat.";
+"authentication_qr_login_loading_waiting_signin" = "Wachten tot het apparaat zich aanmeldt.";
+"authentication_qr_login_loading_connecting_device" = "Verbinden met apparaat";
+"authentication_qr_login_confirm_alert" = "Zorg ervoor dat u de herkomst van deze code kent. Door apparaten te koppelen, geeft u iemand volledige toegang tot uw account.";
+"authentication_qr_login_confirm_subtitle" = "Controleer of de onderstaande code overeenkomt met uw andere apparaat:";
+"authentication_qr_login_confirm_title" = "Beveiligde verbinding tot stand gebracht";
+"authentication_qr_login_scan_subtitle" = "Positioneer de QR-code in het vierkant hieronder";
+"authentication_qr_login_scan_title" = "Scan QR-code";
+"authentication_qr_login_display_step2" = "Selecteer 'Aanmelden met QR-code'";
+"authentication_qr_login_display_step1" = "Open Element op uw andere apparaat";
+"authentication_qr_login_display_subtitle" = "Scan de onderstaande QR-code met uw apparaat dat is uitgelogd.";
+"authentication_qr_login_display_title" = "Een apparaat koppelen";
+"authentication_qr_login_start_display_qr" = "QR-code weergeven op dit apparaat";
+"authentication_qr_login_start_need_alternative" = "Een alternatieve methode nodig?";
+"authentication_qr_login_start_step4" = "Selecteer 'Toon QR-code op dit apparaat'";
+"authentication_qr_login_start_step3" = "Selecteer 'Een apparaat koppelen'";
+"authentication_qr_login_start_step2" = "Ga naar Instellingen -> Beveiliging en privacy";
+"authentication_qr_login_start_step1" = "Open Element op uw andere apparaat";
+"authentication_qr_login_start_subtitle" = "Gebruik de camera op dit apparaat om de QR-code te scannen die op uw andere apparaat wordt weergegeven:";
+"authentication_qr_login_start_title" = "Scan QR-code";
+"authentication_login_with_qr" = "Log in met QR-code";
diff --git a/Riot/Assets/pl.lproj/Vector.strings b/Riot/Assets/pl.lproj/Vector.strings
index 3b82fe79e8..8532c4b4de 100644
--- a/Riot/Assets/pl.lproj/Vector.strings
+++ b/Riot/Assets/pl.lproj/Vector.strings
@@ -751,7 +751,7 @@
"group_participants_invited_section" = "ZAPROSZONY";
"receipt_status_read" = "Odczytano: ";
// Media picker
-"media_picker_title" = "Selektor mediĂłw";
+"media_picker_title" = "Biblioteka mediĂłw";
// Image picker
"image_picker_action_camera" = "ZrĂłb zdjÄcie";
"image_picker_action_library" = "Wybierz z biblioteki";
@@ -2569,7 +2569,7 @@
// Mark: - All Chats
-"all_chats_title" = "Wszystkie rozmowy";
+"all_chats_title" = "Rozmowy";
"spaces_subspace_creation_visibility_message" = "Utworzona przestrzeĆ zostanie dodana do %@.";
"spaces_subspace_creation_visibility_title" = "Jakiego rodzaju podprzestrzeĆ chcesz utworzyÄ?";
"spaces_explore_rooms_format" = "PrzeglÄ
daj %@";
diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings
index 3d6b5ee57c..3741d98419 100644
--- a/Riot/Assets/pt_BR.lproj/Vector.strings
+++ b/Riot/Assets/pt_BR.lproj/Vector.strings
@@ -1688,7 +1688,7 @@
"invite_user" = "Convidar UsuĂĄria(o) matrix";
"reset_to_default" = "Resettar para default";
"resend_message" = "Reenviar a mensagem";
-"select_all" = "Selecionar Todas";
+"select_all" = "Selecionar Todas(os)";
"cancel_upload" = "Cancelar Upload";
"cancel_download" = "Cancelar Download";
"show_details" = "Mostrar Detalhes";
@@ -2606,3 +2606,15 @@
"manage_session_name_info" = "Por favor esteja ciente que nomes de sessĂ”es tambĂ©m sĂŁo visĂveis a pessoas com quem vocĂȘ se comunica. %@";
"manage_session_name_hint" = "Nomes de sessĂ”es personalizados podem ajudar vocĂȘ a reconhecer seus dispositivos mais facilmente.";
"settings_labs_enable_wysiwyg_composer" = "Experimente o editor de texto rico (modo de texto puro vindo em breve)";
+"wysiwyg_composer_start_action_voice_broadcast" = "Broadcast de voz";
+"voice_broadcast_playback_loading_error" = "Incapaz de tocar este broadcast de voz.";
+"voice_broadcast_already_in_progress_message" = "VocĂȘ jĂĄ estĂĄ gravando um broadcast de voz. Por favor termine seu broadcast de voz atual para começar um novo.";
+"voice_broadcast_blocked_by_someone_else_message" = "Alguma outra pessoa jå estå gravando um broadcast de voz. Espere que o broadcast de voz dela termine para começar um novo.";
+"voice_broadcast_permission_denied_message" = "VocĂȘ nĂŁo tem as permissĂ”es requeridas para começar um broadcast de voz nesta sala. Contacte um(a) administrador(a) da sala para fazer upgrade de suas permissĂ”es.";
+
+// Mark: - Voice broadcast
+"voice_broadcast_unauthorized_title" = "Não då para começar um novo broadcast de voz";
+"settings_labs_enable_voice_broadcast" = "Broadcast de voz (sob desenvolvimento ativo)";
+"deselect_all" = "Desselecionar Todas(os)";
+"user_other_session_menu_select_sessions" = "Selecionar sessÔes";
+"user_other_session_selected_count" = "%@ selecionadas";
diff --git a/Riot/Assets/ru.lproj/Vector.strings b/Riot/Assets/ru.lproj/Vector.strings
index 5c3fb89eba..7680e9765a 100644
--- a/Riot/Assets/ru.lproj/Vector.strings
+++ b/Riot/Assets/ru.lproj/Vector.strings
@@ -603,7 +603,7 @@
"key_backup_recover_from_passphrase_passphrase_title" = "ĐĐČĐŸĐŽ";
"key_backup_recover_from_passphrase_passphrase_placeholder" = "ĐĐČДЎОŃĐ” ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń";
"key_backup_recover_from_passphrase_recover_action" = "Đ Đ°Đ·Đ±Đ»ĐŸĐșĐžŃĐŸĐČĐ°ŃŃ ĐžŃŃĐŸŃĐžŃ";
-"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "ĐĐ” Đ·ĐœĐ°Đ”ŃĐ” ĐČĐ°ŃŃ ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń ĐŽĐ»Ń ĐČĐŸŃŃŃĐ°ĐœĐŸĐČĐ»Đ”ĐœĐžŃ? ĐŃ ĐŒĐŸĐ¶Đ”ŃĐ” ";
+"key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "ĐĐ” ĐżĐŸĐŒĐœĐžŃĐ” ŃĐČĐŸŃ ĐŒĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșŃŃ ŃŃĐ°Đ·Ń? ĐŃ ĐŒĐŸĐ¶Đ”ŃĐ” ";
"key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "ĐžŃĐżĐŸĐ»ŃĐ·ĐŸĐČĐ°ŃŃ ĐșĐ»ŃŃ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž";
"key_backup_recover_from_passphrase_lost_passphrase_action_part3" = ".";
"key_backup_recover_from_recovery_key_info" = "ĐŃĐżĐŸĐ»ŃĐ·ŃĐčŃĐ” ĐșĐ»ŃŃ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž ĐŽĐ»Ń ŃĐ°Đ·Đ±Đ»ĐŸĐșĐžŃĐŸĐČĐșĐž ĐžŃŃĐŸŃОО Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœŃŃ
ŃĐŸĐŸĐ±ŃĐ”ĐœĐžĐč";
@@ -624,7 +624,7 @@
"key_backup_setup_success_from_recovery_key_recovery_key_title" = "ĐĐ»ŃŃ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž";
"key_backup_setup_success_from_recovery_key_make_copy_action" = "ХЎДлаŃŃ ĐșĐŸĐżĐžŃ";
"key_backup_setup_success_from_recovery_key_made_copy_action" = "ĐŻ ŃЎДлал ĐșĐŸĐżĐžŃ";
-"key_backup_recover_invalid_passphrase_title" = "ĐĐ”ĐČĐ”ŃĐœĐ°Ń ŃĐ”ĐșŃĐ”ŃĐœĐ°Ń ŃŃĐ°Đ·Đ° ĐŽĐ»Ń ĐČĐŸŃŃŃĐ°ĐœĐŸĐČĐ»Đ”ĐœĐžŃ";
+"key_backup_recover_invalid_passphrase_title" = "ĐĐ”ĐČĐ”ŃĐœĐ°Ń ĐŒĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșĐ°Ń ŃŃĐ°Đ·Đ°";
"key_backup_recover_invalid_recovery_key_title" = "ĐĐ”ŃĐŸĐŸŃĐČĐ”ŃŃŃĐČŃŃŃĐžĐč ĐșĐ»ŃŃ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž";
"key_backup_setup_banner_title" = "ĐĐ” ŃĐ”ŃŃĐčŃĐ” Đ·Đ°ŃĐžŃŃĐŸĐČĐ°ĐœĐœŃĐ” ŃĐŸĐŸĐ±ŃĐ”ĐœĐžŃ";
"key_backup_setup_banner_subtitle" = "ĐĐ°ŃĐ°ŃŃ ĐžŃĐżĐŸĐ»ŃĐ·ĐŸĐČĐ°ŃŃ ĐșĐ»ŃŃ ĐČĐŸŃŃŃĐ°ĐœĐŸĐČĐ»Đ”ĐœĐžŃ";
@@ -641,7 +641,7 @@
"key_backup_setup_intro_setup_action_with_existing_backup" = "ĐŃĐżĐŸĐ»ŃĐ·ĐŸĐČĐ°ŃŃ ĐșĐ»ŃŃ ĐČĐŸŃŃŃĐ°ĐœĐŸĐČĐ»Đ”ĐœĐžŃ";
"settings_key_backup_info" = "ĐĐ°ŃĐžŃŃĐŸĐČĐ°ĐœĐœŃĐ” ŃĐŸĐŸĐ±ŃĐ”ĐœĐžŃ Đ·Đ°ŃĐžŃĐ”ĐœŃ ŃĐșĐČĐŸĐ·ĐœŃĐŒ ŃĐžŃŃĐŸĐČĐ°ĐœĐžĐ”ĐŒ. ĐąĐŸĐ»ŃĐșĐŸ ĐČŃ Đž ĐżĐŸĐ»ŃŃĐ°ŃДлŃ(Đž) ĐžĐŒĐ”ŃŃ ĐșĐ»ŃŃĐž ĐŽĐ»Ń ŃŃĐ”ĐœĐžŃ ŃŃĐžŃ
ŃĐŸĐŸĐ±ŃĐ”ĐœĐžĐč.";
"settings_key_backup_info_signout_warning" = "ХЎДлаĐčŃĐ” ŃДзДŃĐČĐœŃŃ ĐșĐŸĐżĐžŃ ĐșĐ»ŃŃĐ”Đč пДŃДЎ ĐČŃŃ
ĐŸĐŽĐŸĐŒ, ŃŃĐŸĐ±Ń ĐœĐ” ĐżĐŸŃĐ”ŃŃŃŃ ĐžŃ
.";
-"key_backup_setup_passphrase_title" = "ĐĐ°ŃĐžŃĐžŃĐ” ŃДзДŃĐČĐœŃŃ ĐșĐŸĐżĐžŃ ŃĐ”ĐșŃĐ”ŃĐœĐŸĐč ŃŃĐ°Đ·ĐŸĐč";
+"key_backup_setup_passphrase_title" = "ĐĐ°ŃĐžŃĐžŃĐ” ŃДзДŃĐČĐœŃŃ ĐșĐŸĐżĐžŃ ĐŒĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșĐŸĐč ŃŃĐ°Đ·ĐŸĐč";
"key_backup_setup_passphrase_setup_recovery_key_info" = "ĐлО Đ·Đ°ŃĐžŃĐžŃĐ” ŃĐČĐŸŃ ŃДзДŃĐČĐœŃŃ ĐșĐŸĐżĐžŃ Ń ĐżĐŸĐŒĐŸŃŃŃ ĐșĐ»ŃŃĐ° Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž, ŃĐŸŃ
ŃĐ°ĐœĐžĐČ Đ”Đ” ĐČ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸĐŒ ĐŒĐ”ŃŃĐ”.";
"key_backup_setup_passphrase_setup_recovery_key_action" = "(Đ Đ°ŃŃĐžŃĐ”ĐœĐœŃĐč) ĐĐ°ŃŃŃĐŸĐčĐșĐ° Ń ĐșĐ»ŃŃĐŸĐŒ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž";
// Success from passphrase
@@ -654,7 +654,7 @@
"sign_out_non_existing_key_backup_sign_out_confirmation_alert_title" = "ĐĐ°ŃĐžŃŃĐŸĐČĐ°ĐœĐœŃĐ” ŃĐŸĐŸĐ±ŃĐ”ĐœĐžŃ Đ±ŃĐŽŃŃ ŃŃĐ”ŃŃĐœŃ";
"sign_out_non_existing_key_backup_alert_discard_key_backup_action" = "ĐĐœĐ” ĐœĐ” ĐœŃĐ¶ĐœŃ ĐŒĐŸĐž Đ·Đ°ŃĐžŃŃĐŸĐČĐ°ĐœĐœŃĐ” ŃĐŸĐŸĐ±ŃĐ”ĐœĐžŃ";
"sign_out_non_existing_key_backup_alert_title" = "ĐŃ ĐżĐŸŃĐ”ŃŃĐ”ŃĐ” ĐŽĐŸŃŃŃĐż Đș Đ·Đ°ŃĐžŃŃĐŸĐČĐ°ĐœĐœŃĐŒ ŃĐŸĐŸĐ±ŃĐ”ĐœĐžŃĐŒ Đ”ŃлО ĐČŃĐčĐŽĐ”ŃĐ” ŃĐ”ĐčŃĐ°Ń";
-"key_backup_recover_invalid_passphrase" = "ĐĐ”ĐČĐŸĐ·ĐŒĐŸĐ¶ĐœĐŸ ŃĐ°ŃŃĐžŃŃĐŸĐČĐ°ŃŃ ŃДзДŃĐČĐœŃŃ ĐșĐŸĐżĐžŃ Ń ĐżĐŸĐŒĐŸŃŃŃ ŃŃĐŸĐč ŃĐ”ĐșŃĐ”ŃĐœĐŸĐč ŃŃĐ°Đ·Ń: ŃбДЎОŃĐ”ŃŃ, ŃŃĐŸ ĐČŃ ĐČĐČДлО ĐČĐ”ŃĐœŃŃ ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń ĐŽĐ»Ń ĐČĐŸŃŃŃĐ°ĐœĐŸĐČĐ»Đ”ĐœĐžŃ.";
+"key_backup_recover_invalid_passphrase" = "ĐĐ”ĐČĐŸĐ·ĐŒĐŸĐ¶ĐœĐŸ ŃĐ°ŃŃĐžŃŃĐŸĐČĐ°ŃŃ ŃДзДŃĐČĐœŃŃ ĐșĐŸĐżĐžŃ Ń ĐżĐŸĐŒĐŸŃŃŃ ŃŃĐŸĐč ŃŃĐ°Đ·Ń: ŃбДЎОŃĐ”ŃŃ, ŃŃĐŸ ĐČŃ ĐČĐČДлО ĐČĐ”ŃĐœŃŃ ĐŒĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșŃŃ ŃŃĐ°Đ·Ń.";
"key_backup_recover_invalid_recovery_key" = "ĐĐ”ĐČĐŸĐ·ĐŒĐŸĐ¶ĐœĐŸ ŃĐ°ŃŃĐžŃŃĐŸĐČĐ°ŃŃ ŃДзДŃĐČĐœŃŃ ĐșĐŸĐżĐžŃ Ń ĐżĐŸĐŒĐŸŃŃŃ ŃŃĐŸĐłĐŸ ĐșĐ»ŃŃĐ°: ŃбДЎОŃĐ”ŃŃ, ŃŃĐŸ ĐČŃ ĐČĐČДлО ĐČĐ”ŃĐœŃĐč ĐșĐ»ŃŃ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž.";
"e2e_key_backup_wrong_version_button_settings" = "ĐĐ°ŃŃŃĐŸĐčĐșĐž";
"key_backup_setup_intro_manual_export_info" = "(Đ Đ°ŃŃĐžŃĐ”ĐœĐœŃĐč)";
@@ -986,7 +986,7 @@
"secure_key_backup_setup_intro_info" = "ĐĐ°ŃĐžŃĐžŃĐ” ŃĐ”Đ±Ń ĐŸŃ ĐżĐŸŃĐ”ŃĐž ĐŽĐŸŃŃŃпа Đș Đ·Đ°ŃĐžŃŃĐŸĐČĐ°ĐœĐœŃĐŒ ŃĐŸĐŸĐ±ŃĐ”ĐœĐžŃĐŒ Đž ĐŽĐ°ĐœĐœŃĐŒ, ŃĐŸĐ·ĐŽĐ°ĐČ ŃДзДŃĐČĐœŃŃ ĐșĐŸĐżĐžŃ ĐșĐ»ŃŃĐ”Đč ŃĐžŃŃĐŸĐČĐ°ĐœĐžŃ ĐœĐ° ŃĐČĐŸŃĐŒ ŃĐ”ŃĐČĐ”ŃĐ”.";
"secure_key_backup_setup_intro_use_security_key_title" = "ĐŃĐżĐŸĐ»ŃĐ·ŃĐčŃĐ” ĐșĐ»ŃŃ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž";
"secure_key_backup_setup_intro_use_security_key_info" = "ĐĄĐŸĐ·ĐŽĐ°ĐčŃĐ” ĐșĐ»ŃŃ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž ĐŽĐ»Ń Ń
ŃĐ°ĐœĐ”ĐœĐžŃ ĐČ ĐœĐ°ĐŽĐ”Đ¶ĐœĐŸĐŒ ĐŒĐ”ŃŃĐ”, ĐœĐ°ĐżŃĐžĐŒĐ”Ń ĐČ ĐŒĐ”ĐœĐ”ĐŽĐ¶Đ”ŃĐ” паŃĐŸĐ»Đ”Đč ОлО ŃĐ”ĐčŃĐ”.";
-"secure_key_backup_setup_intro_use_security_passphrase_title" = "ĐŃĐżĐŸĐ»ŃĐ·ĐŸĐČĐ°ŃŃ ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń";
+"secure_key_backup_setup_intro_use_security_passphrase_title" = "ĐŃĐżĐŸĐ»ŃĐ·ĐŸĐČĐ°ŃŃ ĐŒĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșŃŃ ŃŃĐ°Đ·Ń";
"secure_key_backup_setup_intro_use_security_passphrase_info" = "ĐĐČДЎОŃĐ” ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń, ОзĐČĐ”ŃŃĐœŃŃ ŃĐŸĐ»ŃĐșĐŸ ĐČĐ°ĐŒ, Đž ŃĐŸĐ·ĐŽĐ°ĐčŃĐ” ĐșĐ»ŃŃ ĐŽĐ»Ń ŃДзДŃĐČĐœĐŸĐłĐŸ ĐșĐŸĐżĐžŃĐŸĐČĐ°ĐœĐžŃ.";
"secure_key_backup_setup_existing_backup_error_title" = "РДзДŃĐČĐœĐ°Ń ĐșĐŸĐżĐžŃ ŃĐŸĐŸĐ±ŃĐ”ĐœĐžĐč ŃжД ŃŃŃĐ”ŃŃĐČŃĐ”Ń";
"secure_key_backup_setup_existing_backup_error_info" = "Đ Đ°Đ·Đ±Đ»ĐŸĐșĐžŃŃĐčŃĐ” Đ”ĐłĐŸ ĐŽĐ»Ń ĐżĐŸĐČŃĐŸŃĐœĐŸĐłĐŸ ĐžŃĐżĐŸĐ»ŃĐ·ĐŸĐČĐ°ĐœĐžŃ ĐČ Đ·Đ°ŃĐžŃĐ”ĐœĐœĐŸĐč ŃДзДŃĐČĐœĐŸĐč ĐșĐŸĐżĐžĐž ОлО ŃЎалОŃĐ” ĐŽĐ»Ń ŃĐŸĐ·ĐŽĐ°ĐœĐžŃ ĐœĐŸĐČĐŸĐč ŃДзДŃĐČĐœĐŸĐč ĐșĐŸĐżĐžĐž ŃĐŸĐŸĐ±ŃĐ”ĐœĐžĐč ĐČ Đ·Đ°ŃĐžŃĐ”ĐœĐœĐŸĐč ŃДзДŃĐČĐœĐŸĐč ĐșĐŸĐżĐžĐž.";
@@ -1024,7 +1024,7 @@
"device_verification_self_verify_wait_information" = "ĐĐŸĐŽŃĐČĐ”ŃĐŽĐžŃĐ” ŃŃĐŸŃ ŃĐ”Đ°ĐœŃ ĐœĐ° ĐŸĐŽĐœĐŸĐŒ Оз ĐŽŃŃгОŃ
ĐČĐ°ŃĐžŃ
ŃĐ”Đ°ĐœŃĐŸĐČ, ĐżŃĐ”ĐŽĐŸŃŃĐ°ĐČĐžĐČ Đ”ĐŒŃ ĐŽĐŸŃŃŃĐż Đș Đ·Đ°ŃĐžŃŃĐŸĐČĐ°ĐœĐœŃĐŒ ŃĐŸĐŸĐ±ŃĐ”ĐœĐžŃĐŒ.\n\nĐŃĐżĐŸĐ»ŃĐ·ŃĐčŃĐ” ĐżĐŸŃĐ»Đ”ĐŽĐœŃŃ ĐČĐ”ŃŃĐžŃ %@ ĐœĐ° ĐŽŃŃгОŃ
ĐČĐ°ŃĐžŃ
ŃŃŃŃĐŸĐčŃŃĐČĐ°Ń
:";
"device_verification_self_verify_wait_additional_information" = "ĐŃĐŸ ŃĐ°Đ±ĐŸŃĐ°Đ”Ń Ń %@ Đž ĐŽŃŃĐłĐžĐŒĐž ĐșĐ»ĐžĐ”ĐœŃĐ°ĐŒĐž Matrix Ń ĐżĐŸĐŽĐŽĐ”ŃжĐșĐŸĐč ĐșŃĐŸŃŃ-ĐżĐŸĐŽĐżĐžŃĐž.";
"device_verification_self_verify_wait_recover_secrets_without_passphrase" = "ĐŃĐżĐŸĐ»ŃĐ·ŃĐčŃĐ” ĐșĐ»ŃŃ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž";
-"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "ĐŃĐżĐŸĐ»ŃĐ·ŃĐčŃĐ” ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń ĐžĐ»Đž ĐșĐ»ŃŃ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž";
+"device_verification_self_verify_wait_recover_secrets_with_passphrase" = "ĐŃĐżĐŸĐ»ŃĐ·ŃĐčŃĐ” ĐŒĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșŃŃ ŃŃĐ°Đ·Ń ĐžĐ»Đž бŃĐŒĐ°Đ¶ĐœŃĐč ĐșĐ»ŃŃ";
"device_verification_self_verify_wait_recover_secrets_additional_information" = "ĐŃлО ĐČŃ ĐœĐ” ĐŒĐŸĐ¶Đ”ŃĐ” ĐżĐŸĐ»ŃŃĐžŃŃ ĐŽĐŸŃŃŃĐż Đș ŃŃŃĐ”ŃŃĐČŃŃŃĐ”ĐŒŃ ŃĐ”Đ°ĐœŃŃ";
"key_verification_verify_sas_title_emoji" = "ĐĄŃĐ°ĐČĐœĐžŃĐ” ŃĐŒĐ°ĐčĐ»Ń";
"key_verification_verify_sas_title_number" = "ĐĄŃĐ°ĐČĐœĐžŃĐ” ŃĐžŃла";
@@ -1102,17 +1102,17 @@
"user_verification_session_details_verify_action_current_user" = "ĐĐœŃĐ”ŃĐ°ĐșŃĐžĐČĐœĐ°Ń ĐżŃĐŸĐČĐ”ŃĐșĐ°";
"user_verification_session_details_verify_action_current_user_manually" = "Đ ŃŃĐœĐ°Ń ĐżŃĐŸĐČĐ”ŃĐșĐ° Ń ĐżĐŸĐŒĐŸŃŃŃ ŃĐ”ĐșŃŃĐ°";
"user_verification_session_details_verify_action_other_user" = "ĐĐŸĐŽŃĐČĐ”ŃĐ¶ĐŽĐ”ĐœĐžĐ” ĐČŃŃŃĐœŃŃ";
-"secrets_recovery_with_passphrase_title" = "ĐĄĐ”ĐșŃĐ”ŃĐœĐ°Ń ŃŃĐ°Đ·Đ°";
+"secrets_recovery_with_passphrase_title" = "ĐĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșĐ°Ń ŃŃĐ°Đ·Đ°";
"secrets_recovery_with_passphrase_information_default" = "ĐĐŸĐ»ŃŃĐžŃĐ” ĐŽĐŸŃŃŃĐż Đș ŃĐČĐŸĐ”Đč Đ·Đ°ŃĐžŃŃĐœĐœĐŸĐč ĐžŃŃĐŸŃОО ŃĐŸĐŸĐ±ŃĐ”ĐœĐžĐč Đž ĐČĐ°ŃĐ”Đč лОŃĐœĐŸŃŃĐž Ń ĐșŃĐŸŃŃ-ĐżĐŸĐŽĐżĐžŃŃŃ ĐŽĐ»Ń ĐżŃĐŸĐČĐ”ŃĐșĐž ĐŽŃŃгОŃ
ŃĐ”Đ°ĐœŃĐŸĐČ, ĐČĐČĐ”ĐŽŃ ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń.";
-"secrets_recovery_with_passphrase_information_verify_device" = "ĐŃĐżĐŸĐ»ŃĐ·ŃĐčŃĐ” ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń, ŃŃĐŸĐ±Ń ĐżŃĐŸĐČĐ”ŃĐžŃŃ ŃŃĐŸ ŃŃŃŃĐŸĐčŃŃĐČĐŸ.";
+"secrets_recovery_with_passphrase_information_verify_device" = "ĐŃĐżĐŸĐ»ŃĐ·ŃĐčŃĐ” ŃĐČĐŸŃ ĐŒĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșŃŃ ŃŃĐ°Đ·Ń, ŃŃĐŸĐ±Ń Đ·Đ°ĐČĐ”ŃĐžŃŃ ŃŃŃ ŃĐ”ŃŃĐžŃ.";
"secrets_recovery_with_passphrase_passphrase_title" = "ĐĐČĐŸĐŽ";
-"secrets_recovery_with_passphrase_passphrase_placeholder" = "ĐĐČДЎОŃĐ” ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń";
+"secrets_recovery_with_passphrase_passphrase_placeholder" = "ĐĐČДЎОŃĐ” ĐŒĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșŃŃ ŃŃĐ°Đ·Ń";
"secrets_recovery_with_passphrase_recover_action" = "ĐŃĐżĐŸĐ»ŃĐ·ĐŸĐČĐ°ŃŃ ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń";
-"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "ĐĐ” Đ·ĐœĐ°Đ”ŃĐ” ĐČĐ°ŃŃ ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń? ĐŃ ĐŒĐŸĐ¶Đ”ŃĐ” ";
-"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "ĐžŃĐżĐŸĐ»ŃĐ·ĐŸĐČĐ°ŃŃ ĐșĐ»ŃŃ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž";
+"secrets_recovery_with_passphrase_lost_passphrase_action_part1" = "ĐĐ” ĐżĐŸĐŒĐœĐžŃĐ” ŃĐČĐŸŃ ĐŒĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșŃŃ ŃŃĐ°Đ·Ń? ĐŃ ĐŒĐŸĐ¶Đ”ŃĐ” ";
+"secrets_recovery_with_passphrase_lost_passphrase_action_part2" = "ĐžŃĐżĐŸĐ»ŃĐ·ĐŸĐČĐ°ŃŃ Đ±ŃĐŒĐ°Đ¶ĐœŃĐč ĐșĐ»ŃŃ";
"secrets_recovery_with_passphrase_lost_passphrase_action_part3" = ".";
"secrets_recovery_with_passphrase_invalid_passphrase_title" = "ĐĐ”ĐČĐŸĐ·ĐŒĐŸĐ¶ĐœĐŸ ĐżĐŸĐ»ŃŃĐžŃŃ ĐŽĐŸŃŃŃĐż Đș ŃĐ”ĐșŃĐ”ŃĐœĐŸĐŒŃ Ń
ŃĐ°ĐœĐžĐ»ĐžŃŃ";
-"secrets_recovery_with_passphrase_invalid_passphrase_message" = "УбДЎОŃĐ”ŃŃ, ŃŃĐŸ ĐČŃ ĐČĐČДлО ĐżŃĐ°ĐČОлŃĐœŃŃ ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń.";
+"secrets_recovery_with_passphrase_invalid_passphrase_message" = "УбДЎОŃĐ”ŃŃ, ŃŃĐŸ ĐČŃ ĐČĐČДлО ĐČĐ”ŃĐœŃŃ ĐŒĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșŃŃ ŃŃĐ°Đ·Ń.";
"secrets_recovery_with_key_title" = "ĐĐ»ŃŃ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž";
"secrets_recovery_with_key_information_default" = "ĐĐŸĐ»ŃŃĐžŃĐ” ĐŽĐŸŃŃŃĐż Đș ŃĐČĐŸĐ”Đč Đ·Đ°ŃĐžŃŃĐœĐœĐŸĐč ĐžŃŃĐŸŃОО ŃĐŸĐŸĐ±ŃĐ”ĐœĐžĐč Đž ĐČĐ°ŃĐ”Đč лОŃĐœĐŸŃŃĐž Ń ĐșŃĐŸŃŃ-ĐżĐŸĐŽĐżĐžŃŃŃ ĐŽĐ»Ń ĐżŃĐŸĐČĐ”ŃĐșĐž ĐŽŃŃгОŃ
ŃĐ”Đ°ĐœŃĐŸĐČ, ĐČĐČĐ”ĐŽŃ ĐșĐ»ŃŃ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž.";
"secrets_recovery_with_key_information_verify_device" = "ĐŃĐżĐŸĐ»ŃĐ·ŃĐčŃĐ” ĐșĐ»ŃŃ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž, ŃŃĐŸĐ±Ń ĐżŃĐŸĐČĐ”ŃĐžŃŃ ŃŃĐŸ ŃŃŃŃĐŸĐčŃŃĐČĐŸ.";
@@ -1128,11 +1128,11 @@
"secrets_setup_recovery_key_done_action" = "ĐĐŸŃĐŸĐČĐŸ";
"secrets_setup_recovery_key_storage_alert_title" = "Đ„ŃĐ°ĐœĐžŃĐ” Đ”ĐłĐŸ ĐČ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž";
"secrets_setup_recovery_key_storage_alert_message" = "â Đ Đ°ŃпДŃĐ°ŃĐ°ĐčŃĐ” Đž Ń
ŃĐ°ĐœĐžŃĐ” ĐČ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸĐŒ ĐŒĐ”ŃŃĐ”\nâ ĐĄĐŸŃ
ŃĐ°ĐœĐžŃĐ” Đ”ĐłĐŸ ĐœĐ° USB-ĐœĐŸŃĐžŃДлД ОлО ŃДзДŃĐČĐœĐŸĐŒ ĐœĐŸŃĐžŃДлД\nâ ĐĄĐșĐŸĐżĐžŃŃĐčŃĐ” Đ”ĐłĐŸ ĐČ ŃĐČĐŸĐ” лОŃĐœĐŸĐ” ĐŸĐ±Đ»Đ°ŃĐœĐŸĐ” Ń
ŃĐ°ĐœĐžĐ»ĐžŃĐ”";
-"secrets_setup_recovery_passphrase_title" = "ĐĐ°ĐŽĐ°ĐčŃĐ” ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń";
+"secrets_setup_recovery_passphrase_title" = "ĐĐ°ĐŽĐ°ĐčŃĐ” ĐŒĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșŃŃ ŃŃĐ°Đ·Ń";
"secrets_setup_recovery_passphrase_information" = "ĐĐČДЎОŃĐ” ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń, ОзĐČĐ”ŃŃĐœŃŃ ŃĐŸĐ»ŃĐșĐŸ ĐČĐ°ĐŒ, ĐŽĐ»Ń Đ·Đ°ŃĐžŃŃ ĐŽĐ°ĐœĐœŃŃ
ĐœĐ° ĐČĐ°ŃĐ”ĐŒ ŃĐ”ŃĐČĐ”ŃĐ”.";
"secrets_setup_recovery_passphrase_additional_information" = "ĐĐ” ĐžŃĐżĐŸĐ»ŃĐ·ŃĐčŃĐ” паŃĐŸĐ»Ń ŃĐČĐŸĐ”Đč ŃŃĐ”ŃĐœĐŸĐč запОŃĐž.";
"secrets_setup_recovery_passphrase_validate_action" = "ĐĐŸŃĐŸĐČĐŸ";
-"secrets_setup_recovery_passphrase_confirm_information" = "ĐĐ»Ń ĐżĐŸĐŽŃĐČĐ”ŃĐ¶ĐŽĐ”ĐœĐžŃ ĐČĐČДЎОŃĐ” ĐČĐ°ŃŃ ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń Đ”ŃŃ ŃĐ°Đ·.";
+"secrets_setup_recovery_passphrase_confirm_information" = "ĐĐČДЎОŃĐ” ĐŒĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșŃŃ ŃŃĐ°Đ·Ń Đ”ŃŃ ŃĐ°Đ·, ŃŃĐŸĐ±Ń ĐżĐŸĐŽŃĐČĐ”ŃĐŽĐžŃŃ Đ”Ń.";
"secrets_setup_recovery_passphrase_confirm_passphrase_title" = "ĐĐŸĐŽŃĐČĐ”ŃĐŽĐžŃŃ";
"secrets_setup_recovery_passphrase_confirm_passphrase_placeholder" = "ĐĐŸĐŽŃĐČĐ”ŃĐŽĐžŃŃ ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń";
"cross_signing_setup_banner_title" = "ĐĐ°ŃŃŃĐŸĐčĐșĐ° ŃĐžŃŃĐŸĐČĐ°ĐœĐžŃ";
@@ -1238,8 +1238,8 @@
// MARK: - Home
"home_empty_view_title" = "ĐĐŸĐ±ŃĐŸ ĐżĐŸĐ¶Đ°Đ»ĐŸĐČĐ°ŃŃ ĐČ %@,\n%@";
-"secrets_setup_recovery_passphrase_summary_information" = "ĐĐ°ĐżĐŸĐŒĐœĐžŃĐ” ŃĐČĐŸŃ ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń. ĐŃ ĐŒĐŸĐ¶ĐœĐŸ ĐžŃĐżĐŸĐ»ŃĐ·ĐŸĐČĐ°ŃŃ ĐŽĐ»Ń ŃĐ°Đ·Đ±Đ»ĐŸĐșĐžŃĐŸĐČĐșĐž ĐČĐ°ŃĐžŃ
Đ·Đ°ŃĐžŃŃĐŸĐČĐ°ĐœĐœŃŃ
ŃĐŸĐŸĐ±ŃĐ”ĐœĐžĐč Đž ĐŽĐ°ĐœĐœŃŃ
.";
-"secrets_setup_recovery_passphrase_summary_title" = "ĐĄĐŸŃ
ŃĐ°ĐœĐžŃĐ” ĐČĐ°ŃŃ ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń";
+"secrets_setup_recovery_passphrase_summary_information" = "ĐĐ°ĐżĐŸĐŒĐœĐžŃĐ” ŃĐČĐŸŃ ĐŒĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșŃŃ ŃŃĐ°Đ·Ń. ĐŃ ĐŒĐŸĐ¶ĐœĐŸ ĐžŃĐżĐŸĐ»ŃĐ·ĐŸĐČĐ°ŃŃ ĐŽĐ»Ń ŃĐ°Đ·Đ±Đ»ĐŸĐșĐžŃĐŸĐČĐșĐž ĐČĐ°ŃĐžŃ
Đ·Đ°ŃĐžŃŃĐŸĐČĐ°ĐœĐœŃŃ
ŃĐŸĐŸĐ±ŃĐ”ĐœĐžĐč Đž ĐŽĐ°ĐœĐœŃŃ
.";
+"secrets_setup_recovery_passphrase_summary_title" = "ĐĄĐŸŃ
ŃĐ°ĐœĐžŃĐ” ŃĐČĐŸŃ ĐŒĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșŃŃ ŃŃĐ°Đ·Ń";
"favourites_empty_view_information" = "ĐŃ ĐŒĐŸĐ¶Đ”ŃĐ” ĐŽĐŸĐ±Đ°ĐČĐžŃŃ ĐČ ĐžĐ·Đ±ŃĐ°ĐœĐœĐŸĐ” ĐœĐ”ŃĐșĐŸĐ»ŃĐșĐžĐŒĐž ŃĐżĐŸŃĐŸĐ±Đ°ĐŒĐž - ŃĐ°ĐŒŃĐč бŃŃŃŃŃĐč - ĐżŃĐŸŃŃĐŸ ĐœĐ°Đ¶Đ°ŃŃ Đž ŃĐŽĐ”ŃжОĐČĐ°ŃŃ. ĐĐ°Đ¶ĐŒĐžŃĐ” ĐœĐ° Đ·ĐČŃĐ·ĐŽĐŸŃĐșŃ, Đž ĐŸĐœĐž Đ°ĐČŃĐŸĐŒĐ°ŃĐžŃĐ”ŃĐșĐž ĐżĐŸŃĐČŃŃŃŃ Đ·ĐŽĐ”ŃŃ, Đž ĐČŃ ĐžŃ
ĐœĐ°ĐČŃДгЎа ŃĐŸŃ
ŃĐ°ĐœĐžŃĐ”.";
// MARK: - Favourites
@@ -1355,7 +1355,7 @@
"space_feature_unavailable_title" = "ĐŃĐŸŃŃŃĐ°ĐœŃŃĐČ Đ”ŃŃ ĐœĐ”Ń";
"secrets_recovery_with_key_information_unlock_secure_backup_with_key" = "ĐĐČДЎОŃĐ” ŃĐČĐŸĐč ĐșĐ»ŃŃ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃĐž, ŃŃĐŸĐ±Ń ĐżŃĐŸĐŽĐŸĐ»Đ¶ĐžŃŃ.";
-"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "ĐĐČДЎОŃĐ” ŃĐ”ĐșŃĐ”ŃĐœŃŃ ŃŃĐ°Đ·Ń, ŃŃĐŸĐ±Ń ĐżŃĐŸĐŽĐŸĐ»Đ¶ĐžŃŃ.";
+"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "ĐĐČДЎОŃĐ” ĐŒĐœĐ”ĐŒĐŸĐœĐžŃĐ”ŃĐșŃŃ ŃŃĐ°Đ·Ń, ŃŃĐŸĐ±Ń ĐżŃĐŸĐŽĐŸĐ»Đ¶ĐžŃŃ.";
"key_verification_verify_qr_code_scan_code_other_device_action" = "ĐĄĐșĐ°ĐœĐžŃĐŸĐČĐ°ĐœĐžĐ” Ń ĐżĐŸĐŒĐŸŃŃŃ ŃŃĐŸĐłĐŸ ŃŃŃŃĐŸĐčŃŃĐČĐ°";
// Success from secure backup
diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings
index 78fe3194e1..ba65a69cb5 100644
--- a/Riot/Assets/sk.lproj/Vector.strings
+++ b/Riot/Assets/sk.lproj/Vector.strings
@@ -2828,3 +2828,15 @@
"manage_session_name_info" = "Uvedomte si, ĆŸe nĂĄzvy relĂĄciĂ sĂș viditeÄŸnĂ© aj pre ÄŸudĂ, s ktorĂœmi komunikujete. %@";
"manage_session_name_hint" = "VlastnĂ© nĂĄzvy relĂĄciĂ vĂĄm pomĂŽĆŸu ÄŸahĆĄie rozpoznaĆ„ vaĆĄe zariadenia.";
"settings_labs_enable_wysiwyg_composer" = "VyskĂșĆĄajte rozĆĄĂrenĂœ textovĂœ editor (ÄistĂœ textovĂœ reĆŸim sa objavĂ Äoskoro)";
+"wysiwyg_composer_start_action_voice_broadcast" = "Hlasové vysielanie";
+"voice_broadcast_already_in_progress_message" = "UĆŸ nahrĂĄvate hlasovĂ© vysielanie. UkonÄite aktuĂĄlne hlasovĂ© vysielanie a spustite novĂ©.";
+"voice_broadcast_blocked_by_someone_else_message" = "Niekto inĂœ uĆŸ nahrĂĄva hlasovĂ© vysielanie. PoÄkajte, kĂœm sa skonÄĂ jeho hlasovĂ© vysielanie, a potom spustite novĂ©.";
+"voice_broadcast_permission_denied_message" = "NemĂĄte poĆŸadovanĂ© oprĂĄvnenia na spustenie hlasovĂ©ho vysielania v tejto miestnosti. ObrĂĄĆ„te sa na sprĂĄvcu miestnosti, aby vĂĄm rozĆĄĂril oprĂĄvnenia.";
+
+// Mark: - Voice broadcast
+"voice_broadcast_unauthorized_title" = "Nie je moĆŸnĂ© spustiĆ„ novĂ© hlasovĂ© vysielanie";
+"settings_labs_enable_voice_broadcast" = "HlasovĂ© vysielanie (v ĆĄtĂĄdiu aktĂvneho vĂœvoja)";
+"voice_broadcast_playback_loading_error" = "Toto hlasovĂ© vysielanie nie je moĆŸnĂ© prehraĆ„.";
+"deselect_all" = "ZruĆĄiĆ„ vĂœber vĆĄetkĂœch";
+"user_other_session_selected_count" = "%@ vybratĂœch";
+"user_other_session_menu_select_sessions" = "Vyberte relĂĄcie";
diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings
index 95706cdf67..d1d1d7d7cc 100644
--- a/Riot/Assets/uk.lproj/Vector.strings
+++ b/Riot/Assets/uk.lproj/Vector.strings
@@ -2830,3 +2830,15 @@
"manage_session_name_info" = "ĐĐ°ŃĐČажŃĐ”, ŃĐŸ ĐœĐ°Đ·ĐČĐž ŃĐ”Đ°ĐœŃŃĐČ ŃĐ°ĐșĐŸĐ¶ ĐČĐžĐŽĐœĐŸ Đ»ŃĐŽŃĐŒ, Đ· ŃĐșĐžĐŒĐž ĐČĐž ŃĐżŃĐ»ĐșŃŃŃĐ”ŃŃ. %@";
"manage_session_name_hint" = "ĐлаŃĐœŃ ĐœĐ°Đ·ĐČĐž ŃĐ”Đ°ĐœŃŃĐČ ĐŽĐŸĐżĐŸĐŒĐŸĐ¶ŃŃŃ ĐČĐ°ĐŒ лДгŃĐ” ŃĐŸĐ·ĐżŃĐ·ĐœĐ°ĐČĐ°ŃĐž ĐČĐ°ŃŃ ĐżŃĐžŃŃŃĐŸŃ.";
"settings_labs_enable_wysiwyg_composer" = "ĐĄĐżŃĐŸĐ±ŃĐčŃĐ” ŃĐŸĐ·ŃĐžŃĐ”ĐœĐžĐč ŃĐ”ĐșŃŃĐŸĐČĐžĐč ŃДЎаĐșŃĐŸŃ (ĐœĐ”Đ·Đ°Đ±Đ°ŃĐŸĐŒ Đ·'ŃĐČĐžŃŃŃŃ ŃĐ”Đ¶ĐžĐŒ Đ·ĐČĐžŃĐ°ĐčĐœĐŸĐłĐŸ ŃĐ”ĐșŃŃŃ)";
+"wysiwyg_composer_start_action_voice_broadcast" = "ĐĐŸĐ»ĐŸŃĐŸĐČŃ ĐżĐŸĐČŃĐŽĐŸĐŒĐ»Đ”ĐœĐœŃ";
+"voice_broadcast_playback_loading_error" = "ĐĐ”ĐŒĐŸĐ¶Đ»ĐžĐČĐŸ ĐČŃĐŽŃĐČĐŸŃĐžŃĐž ŃĐ” ĐłĐŸĐ»ĐŸŃĐŸĐČĐ” ĐżĐŸĐČŃĐŽĐŸĐŒĐ»Đ”ĐœĐœŃ.";
+"voice_broadcast_already_in_progress_message" = "ĐĐž ĐČжД запОŃŃŃŃĐ” ĐłĐŸĐ»ĐŸŃĐŸĐČĐ” ĐżĐŸĐČŃĐŽĐŸĐŒĐ»Đ”ĐœĐœŃ. ĐĐ°ĐČĐ”ŃŃŃŃŃ ĐżĐŸŃĐŸŃĐœŃ ŃŃĐ°ĐœŃĐ»ŃŃŃŃ, ŃĐŸĐ± ŃĐŸĐ·ĐżĐŸŃĐ°ŃĐž ĐœĐŸĐČŃ.";
+"voice_broadcast_blocked_by_someone_else_message" = "Đ„ŃĐŸŃŃ ŃĐœŃĐžĐč ĐČжД запОŃŃŃ ĐłĐŸĐ»ĐŸŃĐŸĐČĐ” ĐżĐŸĐČŃĐŽĐŸĐŒĐ»Đ”ĐœĐœŃ. ĐĐ°ŃĐ”ĐșĐ°ĐčŃĐ”, ĐżĐŸĐșĐž Đ·Đ°ĐșŃĐœŃĐžŃŃŃŃ ŃŃĐ°ĐœŃĐ»ŃŃŃŃ, ŃĐŸĐ± ŃĐŸĐ·ĐżĐŸŃĐ°ŃĐž ĐœĐŸĐČŃ.";
+"voice_broadcast_permission_denied_message" = "ĐĐž ĐœĐ” ĐŒĐ°ŃŃĐ” ĐœĐ”ĐŸĐ±Ń
ŃĐŽĐœĐžŃ
ĐŽĐŸĐ·ĐČĐŸĐ»ŃĐČ ĐŽĐ»Ń ĐżĐŸŃĐ°ŃĐșŃ ŃŃĐ°ĐœŃĐ»ŃŃŃŃ ĐłĐŸĐ»ĐŸŃĐŸĐČĐŸĐłĐŸ ĐżĐŸĐČŃĐŽĐŸĐŒĐ»Đ”ĐœĐœŃ ĐČ ŃŃĐč ĐșŃĐŒĐœĐ°ŃŃ. ĐĐČĐ”ŃĐœŃŃŃŃŃ ĐŽĐŸ Đ°ĐŽĐŒŃĐœŃŃŃŃĐ°ŃĐŸŃĐ° ĐșŃĐŒĐœĐ°ŃĐž, ŃĐŸĐ± ĐŸĐœĐŸĐČĐžŃĐž ĐČĐ°ŃŃ ĐŽĐŸĐ·ĐČĐŸĐ»Đž.";
+
+// Mark: - Voice broadcast
+"voice_broadcast_unauthorized_title" = "ĐĐ” ĐČĐŽĐ°Đ»ĐŸŃŃ ŃĐŸĐ·ĐżĐŸŃĐ°ŃĐž ŃŃĐ°ĐœŃĐ»ŃŃŃŃ ĐœĐŸĐČĐŸĐłĐŸ ĐłĐŸĐ»ĐŸŃĐŸĐČĐŸĐłĐŸ ĐżĐŸĐČŃĐŽĐŸĐŒĐ»Đ”ĐœĐœŃ";
+"settings_labs_enable_voice_broadcast" = "ĐĐŸĐ»ĐŸŃĐŸĐČŃ ĐżĐŸĐČŃĐŽĐŸĐŒĐ»Đ”ĐœĐœŃ (ĐČ Đ°ĐșŃĐžĐČĐœŃĐč ŃĐŸĐ·ŃĐŸĐ±ŃŃ)";
+"deselect_all" = "ĐĄĐșĐ°ŃŃĐČĐ°ŃĐž ĐČОбŃŃ ŃŃŃŃ
";
+"user_other_session_menu_select_sessions" = "ĐОбŃĐ°ŃĐž ŃĐ”Đ°ĐœŃĐž";
+"user_other_session_selected_count" = "ĐОбŃĐ°ĐœĐŸ %@";
diff --git a/Riot/Categories/MXRoom+Riot.m b/Riot/Categories/MXRoom+Riot.m
index 8012215974..df47c16740 100644
--- a/Riot/Categories/MXRoom+Riot.m
+++ b/Riot/Categories/MXRoom+Riot.m
@@ -329,7 +329,7 @@ - (void)encryptionTrustLevelForUserId:(NSString*)userId onComplete:(void (^)(Use
{
if (self.mxSession.crypto)
{
- [self.mxSession.crypto trustLevelSummaryForUserIds:@[userId] onComplete:^(MXUsersTrustLevelSummary *usersTrustLevelSummary) {
+ [self.mxSession.crypto trustLevelSummaryForUserIds:@[userId] forceDownload:NO success:^(MXUsersTrustLevelSummary *usersTrustLevelSummary) {
UserEncryptionTrustLevel userEncryptionTrustLevel;
double trustedDevicesPercentage = usersTrustLevelSummary.trustedDevicesProgress.fractionCompleted;
@@ -341,7 +341,7 @@ - (void)encryptionTrustLevelForUserId:(NSString*)userId onComplete:(void (^)(Use
else if (trustedDevicesPercentage == 0.0)
{
// Verify if the user has the user has cross-signing enabled
- if ([self.mxSession.crypto crossSigningKeysForUser:userId])
+ if ([self.mxSession.crypto.crossSigning crossSigningKeysForUser:userId])
{
userEncryptionTrustLevel = UserEncryptionTrustLevelNotVerified;
}
@@ -357,6 +357,9 @@ - (void)encryptionTrustLevelForUserId:(NSString*)userId onComplete:(void (^)(Use
onComplete(userEncryptionTrustLevel);
+ } failure:^(NSError *error) {
+ MXLogErrorDetails(@"[MXRoom+Riot] Error fetching trust level summary", error);
+ onComplete(UserEncryptionTrustLevelUnknown);
}];
}
else
diff --git a/Riot/Categories/UITableViewCell.swift b/Riot/Categories/UITableViewCell.swift
index 86c4b7ee0d..6071a11ec0 100644
--- a/Riot/Categories/UITableViewCell.swift
+++ b/Riot/Categories/UITableViewCell.swift
@@ -51,5 +51,16 @@ extension UITableViewCell {
@objc func vc_setAccessoryDisclosureIndicatorWithCurrentTheme() {
self.vc_setAccessoryDisclosureIndicator(withTheme: ThemeService.shared().theme)
}
+
+ @objc var vc_parentViewController: UIViewController? {
+ var parent: UIResponder? = self
+ while parent != nil {
+ parent = parent?.next
+ if let viewController = parent as? UIViewController {
+ return viewController
+ }
+ }
+ return nil
+ }
}
diff --git a/Riot/Categories/UITextView.swift b/Riot/Categories/UITextView.swift
index 56b19047ab..1c989cc68c 100644
--- a/Riot/Categories/UITextView.swift
+++ b/Riot/Categories/UITextView.swift
@@ -22,7 +22,10 @@ extension UITextView {
self.attributedText.enumerateAttribute(
.attachment,
in: NSRange(location: 0, length: self.attributedText.length),
- options: []) { _, range, _ in
+ options: []) { value, range, _ in
+ guard value != nil else {
+ return
+ }
self.layoutManager.invalidateDisplay(forCharacterRange: range)
}
}
diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift
index 3cc10eb2e8..9221744276 100644
--- a/Riot/Generated/Images.swift
+++ b/Riot/Generated/Images.swift
@@ -127,6 +127,8 @@ internal class Asset: NSObject {
internal static let userOtherSessionsUnverified = ImageAsset(name: "user_other_sessions_unverified")
internal static let userOtherSessionsVerified = ImageAsset(name: "user_other_sessions_verified")
internal static let userSessionListItemInactiveSession = ImageAsset(name: "user_session_list_item_inactive_session")
+ internal static let userSessionListItemNotSelected = ImageAsset(name: "user_session_list_item_not_selected")
+ internal static let userSessionListItemSelected = ImageAsset(name: "user_session_list_item_selected")
internal static let userSessionUnverified = ImageAsset(name: "user_session_unverified")
internal static let userSessionVerificationUnknown = ImageAsset(name: "user_session_verification_unknown")
internal static let userSessionVerified = ImageAsset(name: "user_session_verified")
@@ -335,6 +337,12 @@ internal class Asset: NSObject {
internal static let tabHome = ImageAsset(name: "tab_home")
internal static let tabPeople = ImageAsset(name: "tab_people")
internal static let tabRooms = ImageAsset(name: "tab_rooms")
+ internal static let voiceBroadcastLive = ImageAsset(name: "voice_broadcast_live")
+ internal static let voiceBroadcastPause = ImageAsset(name: "voice_broadcast_pause")
+ internal static let voiceBroadcastPlay = ImageAsset(name: "voice_broadcast_play")
+ internal static let voiceBroadcastRecord = ImageAsset(name: "voice_broadcast_record")
+ internal static let voiceBroadcastRecordPause = ImageAsset(name: "voice_broadcast_record_pause")
+ internal static let voiceBroadcastStop = ImageAsset(name: "voice_broadcast_stop")
internal static let launchScreenLogo = ImageAsset(name: "launch_screen_logo")
}
@objcMembers
diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift
index 7671c5c732..6c82fd2666 100644
--- a/Riot/Generated/Strings.swift
+++ b/Riot/Generated/Strings.swift
@@ -1523,6 +1523,10 @@ public class VectorL10n: NSObject {
public static var delete: String {
return VectorL10n.tr("Vector", "delete")
}
+ /// Deselect All
+ public static var deselectAll: String {
+ return VectorL10n.tr("Vector", "deselect_all")
+ }
/// This operation requires additional authentication.\nTo continue, please enter your password.
public static var deviceDetailsDeletePromptMessage: String {
return VectorL10n.tr("Vector", "device_details_delete_prompt_message")
@@ -7535,7 +7539,7 @@ public class VectorL10n: NSObject {
public static var settingsLabsEnableThreads: String {
return VectorL10n.tr("Vector", "settings_labs_enable_threads")
}
- /// Voice broadcast (under active development). We currently only detect voice broadcast in the room timeline, this is not possible to send or listen an actual voice broadcast
+ /// Voice broadcast (under active development)
public static var settingsLabsEnableVoiceBroadcast: String {
return VectorL10n.tr("Vector", "settings_labs_enable_voice_broadcast")
}
@@ -8671,6 +8675,10 @@ public class VectorL10n: NSObject {
public static var userOtherSessionFilterMenuVerified: String {
return VectorL10n.tr("Vector", "user_other_session_filter_menu_verified")
}
+ /// Select sessions
+ public static var userOtherSessionMenuSelectSessions: String {
+ return VectorL10n.tr("Vector", "user_other_session_menu_select_sessions")
+ }
/// No inactive sessions found.
public static var userOtherSessionNoInactiveSessions: String {
return VectorL10n.tr("Vector", "user_other_session_no_inactive_sessions")
@@ -8687,6 +8695,10 @@ public class VectorL10n: NSObject {
public static var userOtherSessionSecurityRecommendationTitle: String {
return VectorL10n.tr("Vector", "user_other_session_security_recommendation_title")
}
+ /// %@ selected
+ public static func userOtherSessionSelectedCount(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "user_other_session_selected_count", p1)
+ }
/// Verify or sign out from this session for best security and reliability.
public static var userOtherSessionUnverifiedAdditionalInfo: String {
return VectorL10n.tr("Vector", "user_other_session_unverified_additional_info")
@@ -9051,6 +9063,26 @@ public class VectorL10n: NSObject {
public static var voice: String {
return VectorL10n.tr("Vector", "voice")
}
+ /// You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.
+ public static var voiceBroadcastAlreadyInProgressMessage: String {
+ return VectorL10n.tr("Vector", "voice_broadcast_already_in_progress_message")
+ }
+ /// Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.
+ public static var voiceBroadcastBlockedBySomeoneElseMessage: String {
+ return VectorL10n.tr("Vector", "voice_broadcast_blocked_by_someone_else_message")
+ }
+ /// You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.
+ public static var voiceBroadcastPermissionDeniedMessage: String {
+ return VectorL10n.tr("Vector", "voice_broadcast_permission_denied_message")
+ }
+ /// Unable to play this voice broadcast.
+ public static var voiceBroadcastPlaybackLoadingError: String {
+ return VectorL10n.tr("Vector", "voice_broadcast_playback_loading_error")
+ }
+ /// Can't start a new voice broadcast
+ public static var voiceBroadcastUnauthorizedTitle: String {
+ return VectorL10n.tr("Vector", "voice_broadcast_unauthorized_title")
+ }
/// Voice message
public static var voiceMessageLockScreenPlaceholder: String {
return VectorL10n.tr("Vector", "voice_message_lock_screen_placeholder")
@@ -9207,6 +9239,10 @@ public class VectorL10n: NSObject {
public static var wysiwygComposerStartActionTextFormatting: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_start_action_text_formatting")
}
+ /// Voice broadcast
+ public static var wysiwygComposerStartActionVoiceBroadcast: String {
+ return VectorL10n.tr("Vector", "wysiwyg_composer_start_action_voice_broadcast")
+ }
/// Yes
public static var yes: String {
return VectorL10n.tr("Vector", "yes")
diff --git a/Riot/Generated/UntranslatedStrings.swift b/Riot/Generated/UntranslatedStrings.swift
index f571ff96dc..1f417c7701 100644
--- a/Riot/Generated/UntranslatedStrings.swift
+++ b/Riot/Generated/UntranslatedStrings.swift
@@ -14,10 +14,6 @@ public extension VectorL10n {
static var imagePickerActionFiles: String {
return VectorL10n.tr("Untranslated", "image_picker_action_files")
}
- /// We currently only detect voice broadcast in the room timeline, this is not possible to send or listen an actual voice broadcast
- static var voiceBroadcastInTimelineBody: String {
- return VectorL10n.tr("Untranslated", "voice_broadcast_in_timeline_body")
- }
/// Voice broadcast detected (under active development)
static var voiceBroadcastInTimelineTitle: String {
return VectorL10n.tr("Untranslated", "voice_broadcast_in_timeline_title")
diff --git a/Riot/Modules/Application/LegacyAppDelegate.h b/Riot/Modules/Application/LegacyAppDelegate.h
index c84ff4809f..dff221ba30 100644
--- a/Riot/Modules/Application/LegacyAppDelegate.h
+++ b/Riot/Modules/Application/LegacyAppDelegate.h
@@ -195,7 +195,9 @@ UINavigationControllerDelegate
- (BOOL)presentIncomingKeyVerificationRequest:(id)incomingKeyVerificationRequest
inSession:(MXSession*)session;
-- (BOOL)presentUserVerificationForRoomMember:(MXRoomMember*)roomMember session:(MXSession*)mxSession;
+- (BOOL)presentUserVerificationForRoomMember:(MXRoomMember*)roomMember
+ session:(MXSession*)mxSession
+ completion:(void (^)(void))completion;
- (BOOL)presentCompleteSecurityForSession:(MXSession*)mxSession;
diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m
index 040e243f5d..63d2afe504 100644
--- a/Riot/Modules/Application/LegacyAppDelegate.m
+++ b/Riot/Modules/Application/LegacyAppDelegate.m
@@ -128,6 +128,11 @@ @interface LegacyAppDelegate () *> *pendingKeyRequests) {
+ [crypto pendingKeyRequests:^(MXUsersDevicesMap *> *pendingKeyRequests) {
MXStrongifyAndReturnIfNil(self);
MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: cross-signing state: %ld, pendingKeyRequests.count: %@. Already displayed: %@",
- mxSession.crypto.crossSigning.state,
+ crypto.crossSigning.state,
@(pendingKeyRequests.count),
self->roomKeyRequestViewController ? @"YES" : @"NO");
- if (!mxSession.crypto.crossSigning || mxSession.crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped)
+ if (!crypto.crossSigning || crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped)
{
if (self->roomKeyRequestViewController)
{
@@ -3515,13 +3536,13 @@ - (void)checkPendingRoomKeyRequestsInSession:(MXSession*)mxSession
// Give the client a chance to refresh the device list
MXWeakify(self);
- [mxSession.crypto downloadKeys:@[userId] forceDownload:NO success:^(MXUsersDevicesMap *usersDevicesInfoMap, NSDictionary *crossSigningKeysMap) {
+ [crypto downloadKeys:@[userId] forceDownload:NO success:^(MXUsersDevicesMap *usersDevicesInfoMap, NSDictionary *crossSigningKeysMap) {
MXStrongifyAndReturnIfNil(self);
MXDeviceInfo *deviceInfo = [usersDevicesInfoMap objectForDevice:deviceId forUser:userId];
if (deviceInfo)
{
- if (!mxSession.crypto.crossSigning || mxSession.crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped)
+ if (!crypto.crossSigning || crypto.crossSigning.state == MXCrossSigningStateNotBootstrapped)
{
BOOL wasNewDevice = (deviceInfo.trustLevel.localVerificationStatus == MXDeviceUnknown);
@@ -3529,7 +3550,7 @@ - (void)checkPendingRoomKeyRequestsInSession:(MXSession*)mxSession
{
MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: Open dialog for %@", deviceInfo);
- self->roomKeyRequestViewController = [[RoomKeyRequestViewController alloc] initWithDeviceInfo:deviceInfo wasNewDevice:wasNewDevice andMatrixSession:mxSession onComplete:^{
+ self->roomKeyRequestViewController = [[RoomKeyRequestViewController alloc] initWithDeviceInfo:deviceInfo wasNewDevice:wasNewDevice andMatrixSession:mxSession crypto:crypto onComplete:^{
self->roomKeyRequestViewController = nil;
@@ -3543,7 +3564,7 @@ - (void)checkPendingRoomKeyRequestsInSession:(MXSession*)mxSession
// If the device was new before, it's not any more.
if (wasNewDevice)
{
- [mxSession.crypto setDeviceVerification:MXDeviceUnverified forDevice:deviceId ofUser:userId success:openDialog failure:nil];
+ [crypto setDeviceVerification:MXDeviceUnverified forDevice:deviceId ofUser:userId success:openDialog failure:nil];
}
else
{
@@ -3552,13 +3573,13 @@ - (void)checkPendingRoomKeyRequestsInSession:(MXSession*)mxSession
}
else if (deviceInfo.trustLevel.isVerified)
{
- [mxSession.crypto acceptAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{
+ [crypto acceptAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{
[self checkPendingRoomKeyRequests];
}];
}
else
{
- [mxSession.crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{
+ [crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{
[self checkPendingRoomKeyRequests];
}];
}
@@ -3566,7 +3587,7 @@ - (void)checkPendingRoomKeyRequestsInSession:(MXSession*)mxSession
else
{
MXLogDebug(@"[AppDelegate] checkPendingRoomKeyRequestsInSession: No details found for device %@:%@", userId, deviceId);
- [mxSession.crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{
+ [crypto ignoreAllPendingKeyRequestsFromUser:userId andDevice:deviceId onComplete:^{
[self checkPendingRoomKeyRequests];
}];
}
@@ -3697,7 +3718,9 @@ - (BOOL)presentIncomingKeyVerification:(id)transaction inSessi
return presented;
}
-- (BOOL)presentUserVerificationForRoomMember:(MXRoomMember*)roomMember session:(MXSession*)mxSession
+- (BOOL)presentUserVerificationForRoomMember:(MXRoomMember*)roomMember
+ session:(MXSession*)mxSession
+ completion:(void (^)(void))completion;
{
MXLogDebug(@"[AppDelegate][MXKeyVerification] presentUserVerificationForRoomMember: %@", roomMember);
@@ -3710,6 +3733,8 @@ - (BOOL)presentUserVerificationForRoomMember:(MXRoomMember*)roomMember session:(
[keyVerificationCoordinatorBridgePresenter presentFrom:self.presentedViewController roomMember:roomMember animated:YES];
presented = YES;
+
+ keyVerificationCompletionBlock = completion;
}
else
{
@@ -3741,11 +3766,11 @@ - (BOOL)presentSelfVerificationForOtherDeviceId:(NSString*)deviceId inSession:(M
- (void)keyVerificationCoordinatorBridgePresenterDelegateDidComplete:(KeyVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter otherUserId:(NSString * _Nonnull)otherUserId otherDeviceId:(NSString * _Nonnull)otherDeviceId
{
- MXCrypto *crypto = coordinatorBridgePresenter.session.crypto;
- if (!crypto.backup.hasPrivateKeyInCryptoStore || !crypto.backup.enabled)
+ id crypto = coordinatorBridgePresenter.session.crypto;
+ if ([crypto isKindOfClass:[MXLegacyCrypto class]] && (!crypto.backup.hasPrivateKeyInCryptoStore || !crypto.backup.enabled))
{
MXLogDebug(@"[AppDelegate][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys");
- [crypto setOutgoingKeyRequestsEnabled:YES onComplete:nil];
+ [(MXLegacyCrypto *)crypto setOutgoingKeyRequestsEnabled:YES onComplete:nil];
}
[self dismissKeyVerificationCoordinatorBridgePresenter];
}
@@ -3762,6 +3787,11 @@ - (void)dismissKeyVerificationCoordinatorBridgePresenter
}];
keyVerificationCoordinatorBridgePresenter = nil;
+
+ if (keyVerificationCompletionBlock) {
+ keyVerificationCompletionBlock();
+ }
+ keyVerificationCompletionBlock = nil;
}
#pragma mark - New request
@@ -3981,7 +4011,7 @@ - (void)presentNewKeyVerificationRequestAlertForSession:(MXSession*)session
- (void)registerUserDidSignInOnNewDeviceNotificationForSession:(MXSession*)session
{
- MXCrossSigning *crossSigning = session.crypto.crossSigning;
+ id crossSigning = session.crypto.crossSigning;
if (!crossSigning)
{
@@ -4072,7 +4102,7 @@ - (void)presentNewSignInAlertForDevice:(MXDevice*)device inSession:(MXSession*)s
- (void)registerDidChangeCrossSigningKeysNotificationForSession:(MXSession*)session
{
- MXCrossSigning *crossSigning = session.crypto.crossSigning;
+ id crossSigning = session.crypto.crossSigning;
if (!crossSigning)
{
diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift
index 47ed8bfab6..5aa6b3731f 100644
--- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift
+++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift
@@ -758,8 +758,8 @@ extension AuthenticationCoordinator: AuthenticationServiceDelegate {
// MARK: - KeyVerificationCoordinatorDelegate
extension AuthenticationCoordinator: KeyVerificationCoordinatorDelegate {
func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) {
- if let crypto = session?.crypto,
- !crypto.backup.hasPrivateKeyInCryptoStore || !crypto.backup.enabled {
+ if let crypto = session?.crypto as? MXLegacyCrypto, let backup = crypto.backup,
+ !backup.hasPrivateKeyInCryptoStore || !backup.enabled {
MXLog.debug("[AuthenticationCoordinator][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys")
crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
}
@@ -810,5 +810,4 @@ extension AuthenticationCoordinator: AuthFallBackViewControllerDelegate {
func authFallBackViewControllerDidClose(_ authFallBackViewController: AuthFallBackViewController) {
dismissFallback()
}
-
}
diff --git a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift
index 13b776c4ee..e8ca770abf 100644
--- a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift
+++ b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift
@@ -219,8 +219,8 @@ extension LegacyAuthenticationCoordinator: AuthenticationViewControllerDelegate
// MARK: - KeyVerificationCoordinatorDelegate
extension LegacyAuthenticationCoordinator: KeyVerificationCoordinatorDelegate {
func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) {
- if let crypto = session?.crypto,
- !crypto.backup.hasPrivateKeyInCryptoStore || !crypto.backup.enabled {
+ if let crypto = session?.crypto as? MXLegacyCrypto, let backup = crypto.backup,
+ !backup.hasPrivateKeyInCryptoStore || !backup.enabled {
MXLog.debug("[LegacyAuthenticationCoordinator][MXKeyVerification] requestAllPrivateKeys: Request key backup private keys")
crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
}
diff --git a/Riot/Modules/Authentication/SessionVerificationListener.swift b/Riot/Modules/Authentication/SessionVerificationListener.swift
index 7d5d7f48b8..214c76695f 100644
--- a/Riot/Modules/Authentication/SessionVerificationListener.swift
+++ b/Riot/Modules/Authentication/SessionVerificationListener.swift
@@ -69,7 +69,7 @@ class SessionVerificationListener {
}
if session.state == .storeDataReady {
- if let crypto = session.crypto, crypto.crossSigning != nil {
+ if let crypto = session.crypto as? MXLegacyCrypto {
// Do not make key share requests while the "Complete security" is not complete.
// If the device is self-verified, the SDK will restore the existing key backup.
// Then, it will re-enable outgoing key share requests
@@ -78,7 +78,8 @@ class SessionVerificationListener {
} else if session.state == .running {
unregisterSessionStateChangeNotification()
- if let crypto = session.crypto, let crossSigning = crypto.crossSigning {
+ if let crypto = session.crypto {
+ let crossSigning = crypto.crossSigning
crossSigning.refreshState { [weak self] stateUpdated in
guard let self = self else { return }
@@ -100,7 +101,7 @@ class SessionVerificationListener {
self.completion?(.authenticationIsComplete)
} failure: { error in
MXLog.error("[SessionVerificationListener] sessionStateDidChange: Bootstrap failed", context: error)
- crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
+ (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
self.completion?(.authenticationIsComplete)
}
} else {
@@ -110,12 +111,12 @@ class SessionVerificationListener {
self.completion?(.authenticationIsComplete)
} failure: { error in
MXLog.error("[SessionVerificationListener] sessionStateDidChange: Do not know how to bootstrap cross-signing. Skip it.")
- crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
+ (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
self.completion?(.authenticationIsComplete)
}
}
} else {
- crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
+ (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
self.completion?(.authenticationIsComplete)
}
case .crossSigningExists:
@@ -124,12 +125,12 @@ class SessionVerificationListener {
default:
MXLog.debug("[SessionVerificationListener] sessionStateDidChange: Nothing to do")
- crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
+ (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
self.completion?(.authenticationIsComplete)
}
} failure: { [weak self] error in
MXLog.error("[SessionVerificationListener] sessionStateDidChange: Fail to refresh crypto state", context: error)
- crypto.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
+ (crypto as? MXLegacyCrypto)?.setOutgoingKeyRequestsEnabled(true, onComplete: nil)
self?.completion?(.authenticationIsComplete)
}
} else {
diff --git a/Riot/Modules/Call/CallViewController.m b/Riot/Modules/Call/CallViewController.m
index 71c52ba727..a8019c06c2 100644
--- a/Riot/Modules/Call/CallViewController.m
+++ b/Riot/Modules/Call/CallViewController.m
@@ -373,7 +373,12 @@ - (void)call:(MXCall *)call didEncounterError:(NSError *)error reason:(MXCallHan
// Acknowledge the existence of all devices
[self startActivityIndicator];
- [self.mainSession.crypto setDevicesKnown:unknownDevices complete:^{
+ if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]])
+ {
+ MXLogFailure(@"[CallViewController] call: Only legacy crypto supports manual setting of known devices");
+ return;
+ }
+ [(MXLegacyCrypto *)self.mainSession.crypto setDevicesKnown:unknownDevices complete:^{
[self stopActivityIndicator];
diff --git a/Riot/Modules/Common/Avatar/AvatarView.swift b/Riot/Modules/Common/Avatar/AvatarView.swift
index e222d5af94..a3f46a5aa7 100644
--- a/Riot/Modules/Common/Avatar/AvatarView.swift
+++ b/Riot/Modules/Common/Avatar/AvatarView.swift
@@ -106,19 +106,9 @@ class AvatarView: UIView, Themable {
return
}
- let defaultAvatarImage: UIImage?
- var defaultAvatarImageContentMode: UIView.ContentMode = .scaleAspectFill
+ let (defaultAvatarImage, defaultAvatarImageContentMode) = viewData.fallbackImageParameters() ?? (nil, .scaleAspectFill)
+ updateAvatarImageView(image: defaultAvatarImage, contentMode: defaultAvatarImageContentMode)
- switch viewData.fallbackImage {
- case .matrixItem(let matrixItemId, let matrixItemDisplayName):
- defaultAvatarImage = AvatarGenerator.generateAvatar(forMatrixItem: matrixItemId, withDisplayName: matrixItemDisplayName)
- case .image(let image, let contentMode):
- defaultAvatarImage = image
- defaultAvatarImageContentMode = contentMode ?? .scaleAspectFill
- case .none:
- defaultAvatarImage = nil
- }
-
if let avatarUrl = viewData.avatarUrl {
avatarImageView.setImageURI(avatarUrl,
withType: nil,
@@ -127,12 +117,9 @@ class AvatarView: UIView, Themable {
with: MXThumbnailingMethodScale,
previewImage: defaultAvatarImage,
mediaManager: viewData.mediaManager)
- avatarImageView.contentMode = .scaleAspectFill
- avatarImageView.imageView?.contentMode = .scaleAspectFill
+ updateAvatarContentMode(contentMode: .scaleAspectFill)
} else {
- avatarImageView.image = defaultAvatarImage
- avatarImageView.contentMode = defaultAvatarImageContentMode
- avatarImageView.imageView?.contentMode = defaultAvatarImageContentMode
+ updateAvatarImageView(image: defaultAvatarImage, contentMode: defaultAvatarImageContentMode)
}
}
@@ -148,6 +135,16 @@ class AvatarView: UIView, Themable {
gestureRecognizer.minimumPressDuration = 0
self.addGestureRecognizer(gestureRecognizer)
}
+
+ private func updateAvatarImageView(image: UIImage?, contentMode: UIView.ContentMode) {
+ avatarImageView?.image = image
+ updateAvatarContentMode(contentMode: contentMode)
+ }
+
+ private func updateAvatarContentMode(contentMode: UIView.ContentMode) {
+ avatarImageView?.contentMode = contentMode
+ avatarImageView?.imageView?.contentMode = contentMode
+ }
// MARK: - Actions
diff --git a/Riot/Modules/Common/Avatar/AvatarViewData.swift b/Riot/Modules/Common/Avatar/AvatarViewData.swift
index ef5cbb89c8..88eb47a078 100644
--- a/Riot/Modules/Common/Avatar/AvatarViewData.swift
+++ b/Riot/Modules/Common/Avatar/AvatarViewData.swift
@@ -29,6 +29,21 @@ struct AvatarViewData: AvatarViewDataProtocol {
/// Matrix media handler if exists
var mediaManager: MXMediaManager?
- /// Fallback image used when avatarUrl is nil
- var fallbackImage: AvatarFallbackImage?
+ /// Fallback images used when avatarUrl is nil
+ var fallbackImages: [AvatarFallbackImage]?
+}
+
+extension AvatarViewData {
+ init(matrixItemId: String,
+ displayName: String? = nil,
+ avatarUrl: String? = nil,
+ mediaManager: MXMediaManager? = nil,
+ fallbackImage: AvatarFallbackImage?) {
+
+ self.matrixItemId = matrixItemId
+ self.displayName = displayName
+ self.avatarUrl = avatarUrl
+ self.mediaManager = mediaManager
+ self.fallbackImages = fallbackImage.map { [$0] }
+ }
}
diff --git a/Riot/Modules/Common/Avatar/AvatarViewDataProtocol.swift b/Riot/Modules/Common/Avatar/AvatarViewDataProtocol.swift
index 9b677e5813..f3410783bf 100644
--- a/Riot/Modules/Common/Avatar/AvatarViewDataProtocol.swift
+++ b/Riot/Modules/Common/Avatar/AvatarViewDataProtocol.swift
@@ -41,6 +41,24 @@ protocol AvatarViewDataProtocol: AvatarProtocol {
/// Matrix media handler
var mediaManager: MXMediaManager? { get }
- /// Fallback image used when avatarUrl is nil
- var fallbackImage: AvatarFallbackImage? { get }
+ /// Fallback images used when avatarUrl is nil
+ var fallbackImages: [AvatarFallbackImage]? { get }
+}
+
+extension AvatarViewDataProtocol {
+ func fallbackImageParameters() -> (UIImage?, UIView.ContentMode)? {
+ fallbackImages?
+ .lazy
+ .map { fallbackImage in
+ switch fallbackImage {
+ case .matrixItem(let matrixItemId, let matrixItemDisplayName):
+ return (AvatarGenerator.generateAvatar(forMatrixItem: matrixItemId, withDisplayName: matrixItemDisplayName), .scaleAspectFill)
+ case .image(let image, let contentMode):
+ return (image, contentMode ?? .scaleAspectFill)
+ }
+ }
+ .first { (image, contentMode) in
+ image != nil
+ }
+ }
}
diff --git a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift
index 141c676e25..ef1c2a66d6 100644
--- a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift
+++ b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift
@@ -25,8 +25,7 @@ import Combine
class VectorHostingController: UIHostingController {
// MARK: Private
-
- private let forceZeroSafeAreaInsets: Bool
+
private var theme: Theme
private var heightSubject = CurrentValueSubject(0)
@@ -55,11 +54,8 @@ class VectorHostingController: UIHostingController {
}
/// Initializer
/// - Parameter rootView: Root view for the controller.
- /// - Parameter forceZeroSafeAreaInsets: Whether to force-set the hosting view's safe area insets to zero. Useful when the view is used as part of a table view.
- init(rootView: Content,
- forceZeroSafeAreaInsets: Bool = false) where Content: View {
+ init(rootView: Content) where Content: View {
self.theme = ThemeService.shared().theme
- self.forceZeroSafeAreaInsets = forceZeroSafeAreaInsets
super.init(rootView: AnyView(rootView.vectorContent()))
}
@@ -116,22 +112,6 @@ class VectorHostingController: UIHostingController {
heightSubject.send(height)
}
}
-
- override func viewSafeAreaInsetsDidChange() {
- super.viewSafeAreaInsetsDidChange()
-
- guard forceZeroSafeAreaInsets else {
- return
- }
-
- let counterSafeAreaInsets = UIEdgeInsets(top: -view.safeAreaInsets.top,
- left: -view.safeAreaInsets.left,
- bottom: -view.safeAreaInsets.bottom,
- right: -view.safeAreaInsets.right)
- if additionalSafeAreaInsets != counterSafeAreaInsets, counterSafeAreaInsets != .zero {
- additionalSafeAreaInsets = counterSafeAreaInsets
- }
- }
private func registerThemeServiceDidChangeThemeNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
diff --git a/Riot/Modules/CrossSigning/CrossSigningService.swift b/Riot/Modules/CrossSigning/CrossSigningService.swift
index ab40e63695..c4773581b3 100644
--- a/Riot/Modules/CrossSigning/CrossSigningService.swift
+++ b/Riot/Modules/CrossSigning/CrossSigningService.swift
@@ -85,7 +85,7 @@ final class CrossSigningService: NSObject {
@discardableResult
func setupCrossSigningWithoutAuthentication(for session: MXSession, success: @escaping (() -> Void), failure: @escaping ((Error) -> Void)) -> MXHTTPOperation? {
- guard let crossSigning = session.crypto.crossSigning else {
+ guard let crossSigning = session.crypto?.crossSigning else {
failure(CrossSigningServiceError.unknown)
return nil
}
diff --git a/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinator.swift b/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinator.swift
index f545b2e444..2877de09d7 100644
--- a/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinator.swift
+++ b/Riot/Modules/CrossSigning/Setup/CrossSigningSetupCoordinator.swift
@@ -72,7 +72,7 @@ final class CrossSigningSetupCoordinator: CrossSigningSetupCoordinatorType {
}
private func setupCrossSigning(with authenticationParameters: [String: Any]) {
- guard let crossSigning = self.parameters.session.crypto.crossSigning else {
+ guard let crossSigning = self.parameters.session.crypto?.crossSigning else {
return
}
diff --git a/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift b/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift
index 932a52333f..1070db4e57 100644
--- a/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift
+++ b/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift
@@ -332,7 +332,7 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol {
createAvatarButtonItem(for: viewController)
}
- private func createAvatarButtonItem(for viewController: UIViewController) {
+ private var avatarMenu: UIMenu {
var actions: [UIMenuElement] = []
actions.append(UIAction(title: VectorL10n.allChatsUserMenuSettings, image: UIImage(systemName: "gearshape")) { [weak self] action in
@@ -358,32 +358,30 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol {
}
]))
- let menu = UIMenu(options: .displayInline, children: actions)
-
+ return UIMenu(options: .displayInline, children: actions)
+ }
+
+ private func createAvatarButtonItem(for viewController: UIViewController) {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 36, height: 36))
view.backgroundColor = .clear
- let button: UIButton = UIButton(frame: view.bounds.inset(by: UIEdgeInsets(top: 7, left: 7, bottom: 7, right: 7)))
+ let avatarInsets: UIEdgeInsets = .init(top: 7, left: 7, bottom: 7, right: 7)
+ let button: UIButton = .init(frame: view.bounds.inset(by: avatarInsets))
button.setImage(Asset.Images.tabPeople.image, for: .normal)
- button.menu = menu
+ button.menu = avatarMenu
button.showsMenuAsPrimaryAction = true
button.autoresizingMask = [.flexibleHeight, .flexibleWidth]
button.accessibilityLabel = VectorL10n.allChatsUserMenuAccessibilityLabel
view.addSubview(button)
self.avatarMenuButton = button
- let avatarView = UserAvatarView(frame: view.bounds.inset(by: UIEdgeInsets(top: 7, left: 7, bottom: 7, right: 7)))
+ let avatarView = UserAvatarView(frame: view.bounds.inset(by: avatarInsets))
avatarView.isUserInteractionEnabled = false
avatarView.update(theme: ThemeService.shared().theme)
avatarView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
view.addSubview(avatarView)
self.avatarMenuView = avatarView
-
- if let avatar = userAvatarViewData(from: currentMatrixSession) {
- avatarView.fill(with: avatar)
- button.setImage(nil, for: .normal)
- }
-
+ updateAvatarButtonItem()
viewController.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: view)
}
diff --git a/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinator.swift b/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinator.swift
index d7d0a9a312..82404834e3 100644
--- a/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinator.swift
+++ b/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinator.swift
@@ -22,7 +22,7 @@ final class KeyBackupRecoverCoordinator: KeyBackupRecoverCoordinatorType {
// MARK: Private
- private let session: MXSession
+ private let keyBackup: MXKeyBackup
private let navigationRouter: NavigationRouterType
private let keyBackupVersion: MXKeyBackupVersion
@@ -34,8 +34,8 @@ final class KeyBackupRecoverCoordinator: KeyBackupRecoverCoordinatorType {
// MARK: - Setup
- init(session: MXSession, keyBackupVersion: MXKeyBackupVersion, navigationRouter: NavigationRouterType? = nil) {
- self.session = session
+ init(keyBackup: MXKeyBackup, keyBackupVersion: MXKeyBackupVersion, navigationRouter: NavigationRouterType? = nil) {
+ self.keyBackup = keyBackup
self.keyBackupVersion = keyBackupVersion
if let navigationRouter = navigationRouter {
@@ -52,7 +52,7 @@ final class KeyBackupRecoverCoordinator: KeyBackupRecoverCoordinatorType {
let rootCoordinator: Coordinator & Presentable
// Check if we have the private key locally
- if self.session.crypto.backup.hasPrivateKeyInCryptoStore {
+ if keyBackup.hasPrivateKeyInCryptoStore {
rootCoordinator = self.createRecoverFromPrivateKeyCoordinator()
} else {
rootCoordinator = self.createRecoverWithUserInteractionCoordinator()
@@ -93,19 +93,19 @@ final class KeyBackupRecoverCoordinator: KeyBackupRecoverCoordinatorType {
}
private func createRecoverFromPrivateKeyCoordinator() -> KeyBackupRecoverFromPrivateKeyCoordinator {
- let coordinator = KeyBackupRecoverFromPrivateKeyCoordinator(keyBackup: self.session.crypto.backup, keyBackupVersion: self.keyBackupVersion)
+ let coordinator = KeyBackupRecoverFromPrivateKeyCoordinator(keyBackup: keyBackup, keyBackupVersion: self.keyBackupVersion)
coordinator.delegate = self
return coordinator
}
private func createRecoverFromPassphraseCoordinator() -> KeyBackupRecoverFromPassphraseCoordinator {
- let coordinator = KeyBackupRecoverFromPassphraseCoordinator(keyBackup: self.session.crypto.backup, keyBackupVersion: self.keyBackupVersion)
+ let coordinator = KeyBackupRecoverFromPassphraseCoordinator(keyBackup: keyBackup, keyBackupVersion: self.keyBackupVersion)
coordinator.delegate = self
return coordinator
}
private func createRecoverFromRecoveryKeyCoordinator() -> KeyBackupRecoverFromRecoveryKeyCoordinator {
- let coordinator = KeyBackupRecoverFromRecoveryKeyCoordinator(keyBackup: self.session.crypto.backup, keyBackupVersion: self.keyBackupVersion)
+ let coordinator = KeyBackupRecoverFromRecoveryKeyCoordinator(keyBackup: keyBackup, keyBackupVersion: self.keyBackupVersion)
coordinator.delegate = self
return coordinator
}
diff --git a/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift b/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift
index a06e9befd5..2d5be45785 100644
--- a/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift
+++ b/Riot/Modules/KeyBackup/Recover/KeyBackupRecoverCoordinatorBridgePresenter.swift
@@ -49,7 +49,12 @@ final class KeyBackupRecoverCoordinatorBridgePresenter: NSObject {
// MARK: - Public
func present(from viewController: UIViewController, animated: Bool) {
- let keyBackupSetupCoordinator = KeyBackupRecoverCoordinator(session: self.session, keyBackupVersion: keyBackupVersion)
+ guard let keyBackup = session.crypto?.backup else {
+ MXLog.failure("[KeyBackupRecoverCoordinatorBridgePresenter] Cannot setup backups without backup module")
+ return
+ }
+
+ let keyBackupSetupCoordinator = KeyBackupRecoverCoordinator(keyBackup: keyBackup, keyBackupVersion: keyBackupVersion)
keyBackupSetupCoordinator.delegate = self
viewController.present(keyBackupSetupCoordinator.toPresentable(), animated: animated, completion: nil)
keyBackupSetupCoordinator.start()
@@ -58,12 +63,16 @@ final class KeyBackupRecoverCoordinatorBridgePresenter: NSObject {
}
func push(from navigationController: UINavigationController, animated: Bool) {
+ guard let keyBackup = session.crypto?.backup else {
+ MXLog.failure("[KeyBackupRecoverCoordinatorBridgePresenter] Cannot setup backups without backup module")
+ return
+ }
MXLog.debug("[KeyBackupRecoverCoordinatorBridgePresenter] Push complete security from \(navigationController)")
let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController)
- let keyBackupSetupCoordinator = KeyBackupRecoverCoordinator(session: self.session, keyBackupVersion: keyBackupVersion, navigationRouter: navigationRouter)
+ let keyBackupSetupCoordinator = KeyBackupRecoverCoordinator(keyBackup: keyBackup, keyBackupVersion: keyBackupVersion, navigationRouter: navigationRouter)
keyBackupSetupCoordinator.delegate = self
keyBackupSetupCoordinator.start() // Will trigger view controller push
diff --git a/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift b/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift
index 03171ebd2a..aab964c2fb 100644
--- a/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift
+++ b/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift
@@ -66,7 +66,7 @@ final class KeyBackupSetupCoordinator: KeyBackupSetupCoordinatorType {
private func createSetupIntroViewController() -> KeyBackupSetupIntroViewController {
- let backupState = self.session.crypto.backup?.state ?? MXKeyBackupStateUnknown
+ let backupState = self.session.crypto?.backup?.state ?? MXKeyBackupStateUnknown
let isABackupAlreadyExists: Bool
switch backupState {
@@ -99,7 +99,12 @@ final class KeyBackupSetupCoordinator: KeyBackupSetupCoordinatorType {
}
private func showSetupPassphrase(animated: Bool) {
- let keyBackupSetupPassphraseCoordinator = KeyBackupSetupPassphraseCoordinator(session: self.session)
+ guard let keyBackup = self.session.crypto?.backup else {
+ MXLog.failure("[KeyBackupSetupCoordinator] Cannot setup backups without backup module")
+ return
+ }
+
+ let keyBackupSetupPassphraseCoordinator = KeyBackupSetupPassphraseCoordinator(keyBackup: keyBackup)
keyBackupSetupPassphraseCoordinator.delegate = self
keyBackupSetupPassphraseCoordinator.start()
@@ -130,7 +135,7 @@ final class KeyBackupSetupCoordinator: KeyBackupSetupCoordinatorType {
}
private func createKeyBackupUsingSecureBackup(privateKey: Data, completion: @escaping (Result) -> Void) {
- guard let keyBackup = session.crypto.backup, let recoveryService = session.crypto.recoveryService else {
+ guard let keyBackup = session.crypto?.backup, let recoveryService = session.crypto?.recoveryService else {
return
}
diff --git a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinator.swift b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinator.swift
index ea0d2b549d..f9c0342bef 100644
--- a/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinator.swift
+++ b/Riot/Modules/KeyBackup/Setup/Passphrase/KeyBackupSetupPassphraseCoordinator.swift
@@ -23,7 +23,6 @@ final class KeyBackupSetupPassphraseCoordinator: KeyBackupSetupPassphraseCoordin
// MARK: Private
- private let session: MXSession
private var keyBackupSetupPassphraseViewModel: KeyBackupSetupPassphraseViewModelType
private let keyBackupSetupPassphraseViewController: KeyBackupSetupPassphraseViewController
@@ -35,10 +34,8 @@ final class KeyBackupSetupPassphraseCoordinator: KeyBackupSetupPassphraseCoordin
// MARK: - Setup
- init(session: MXSession) {
- self.session = session
-
- let keyBackupSetupPassphraseViewModel = KeyBackupSetupPassphraseViewModel(keyBackup: self.session.crypto.backup)
+ init(keyBackup: MXKeyBackup) {
+ let keyBackupSetupPassphraseViewModel = KeyBackupSetupPassphraseViewModel(keyBackup: keyBackup)
let keyBackupSetupPassphraseViewController = KeyBackupSetupPassphraseViewController.instantiate(with: keyBackupSetupPassphraseViewModel)
self.keyBackupSetupPassphraseViewModel = keyBackupSetupPassphraseViewModel
self.keyBackupSetupPassphraseViewController = keyBackupSetupPassphraseViewController
diff --git a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift
index 51dd86b696..4129c4f478 100644
--- a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift
+++ b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift
@@ -324,12 +324,8 @@ extension KeyVerificationCoordinator: KeyVerificationDataLoadingCoordinatorDeleg
// MARK: - DeviceVerificationStartCoordinatorDelegate
extension KeyVerificationCoordinator: DeviceVerificationStartCoordinatorDelegate {
- func deviceVerificationStartCoordinator(_ coordinator: DeviceVerificationStartCoordinatorType, didCompleteWithOutgoingTransaction transaction: MXSASTransaction) {
- self.showVerifyBySAS(transaction: transaction, animated: true)
- }
-
- func deviceVerificationStartCoordinator(_ coordinator: DeviceVerificationStartCoordinatorType, didTransactionCancelled transaction: MXSASTransaction) {
- self.didCancel()
+ func deviceVerificationStartCoordinator(_ coordinator: DeviceVerificationStartCoordinatorType, otherDidAcceptRequest request: MXKeyVerificationRequest) {
+ self.showVerifyByScanning(keyVerificationRequest: request, animated: true)
}
func deviceVerificationStartCoordinatorDidCancel(_ coordinator: DeviceVerificationStartCoordinatorType) {
diff --git a/Riot/Modules/KeyVerification/Common/Loading/KeyVerificationDataLoadingViewModel.swift b/Riot/Modules/KeyVerification/Common/Loading/KeyVerificationDataLoadingViewModel.swift
index b2874db8f9..7db1624c99 100644
--- a/Riot/Modules/KeyVerification/Common/Loading/KeyVerificationDataLoadingViewModel.swift
+++ b/Riot/Modules/KeyVerification/Common/Loading/KeyVerificationDataLoadingViewModel.swift
@@ -19,7 +19,6 @@
import Foundation
enum KeyVerificationDataLoadingViewModelError: Error {
- case unknown
case transactionCancelled
case transactionCancelledByMe(reason: MXTransactionCancelCode)
}
@@ -137,9 +136,7 @@ final class KeyVerificationDataLoadingViewModel: KeyVerificationDataLoadingViewM
return
}
- let finalError = error ?? KeyVerificationDataLoadingViewModelError.unknown
-
- sself.update(viewState: .error(finalError))
+ sself.update(viewState: .error(error))
})
} else {
diff --git a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift
index 29c312bfc7..b064d4f84b 100644
--- a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift
+++ b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewModel.swift
@@ -92,21 +92,29 @@ final class KeyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWai
// be sure that session has completed its first sync
if session.state >= .running {
- // Always send request instead of waiting for an incoming one as per recent EW changes
- MXLog.debug("[KeyVerificationSelfVerifyWaitViewModel] loadData: Send a verification request to all devices instead of waiting")
-
- let keyVerificationService = KeyVerificationService()
- self.verificationManager.requestVerificationByToDevice(withUserId: self.session.myUserId, deviceIds: nil, methods: keyVerificationService.supportedKeyVerificationMethods(), success: { [weak self] (keyVerificationRequest) in
- guard let self = self else {
- return
- }
+ if let existingRequest = verificationManager.pendingRequests.first(where: { $0.isFromMyUser && !$0.isFromMyDevice && $0.state == MXKeyVerificationRequestStatePending }) {
+ MXLog.debug("[KeyVerificationSelfVerifyWaitViewModel] loadData: Accepting an existing self-verification request instead of starting a new one")
+
+ registerTransactionDidStateChangeNotification()
+ acceptKeyVerificationRequest(existingRequest)
+ } else {
- self.keyVerificationRequest = keyVerificationRequest
+ // Always send request instead of waiting for an incoming one as per recent EW changes
+ MXLog.debug("[KeyVerificationSelfVerifyWaitViewModel] loadData: Send a verification request to all devices instead of waiting")
- }, failure: { [weak self] error in
- self?.update(viewState: .error(error))
- })
- continueLoadData()
+ let keyVerificationService = KeyVerificationService()
+ self.verificationManager.requestVerificationByToDevice(withUserId: self.session.myUserId, deviceIds: nil, methods: keyVerificationService.supportedKeyVerificationMethods(), success: { [weak self] (keyVerificationRequest) in
+ guard let self = self else {
+ return
+ }
+
+ self.keyVerificationRequest = keyVerificationRequest
+
+ }, failure: { [weak self] error in
+ self?.update(viewState: .error(error))
+ })
+ continueLoadData()
+ }
} else {
// show loader
self.update(viewState: .secretsRecoveryCheckingAvailability(VectorL10n.deviceVerificationSelfVerifyWaitRecoverSecretsCheckingAvailability))
diff --git a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinator.swift b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinator.swift
index f6ba2bec80..0c806ceddf 100644
--- a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinator.swift
+++ b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinator.swift
@@ -63,13 +63,9 @@ extension DeviceVerificationStartCoordinator: DeviceVerificationStartViewModelCo
func deviceVerificationStartViewModelDidUseLegacyVerification(_ viewModel: DeviceVerificationStartViewModelType) {
self.delegate?.deviceVerificationStartCoordinatorDidCancel(self)
}
-
- func deviceVerificationStartViewModel(_ viewModel: DeviceVerificationStartViewModelType, didCompleteWithOutgoingTransaction transaction: MXSASTransaction) {
- self.delegate?.deviceVerificationStartCoordinator(self, didCompleteWithOutgoingTransaction: transaction)
- }
-
- func deviceVerificationStartViewModel(_ viewModel: DeviceVerificationStartViewModelType, didTransactionCancelled transaction: MXSASTransaction) {
- self.delegate?.deviceVerificationStartCoordinator(self, didTransactionCancelled: transaction)
+
+ func deviceVerificationStartViewModel(_ viewModel: DeviceVerificationStartViewModelType, otherDidAcceptRequest request: MXKeyVerificationRequest) {
+ self.delegate?.deviceVerificationStartCoordinator(self, otherDidAcceptRequest: request)
}
func deviceVerificationStartViewModelDidCancel(_ viewModel: DeviceVerificationStartViewModelType) {
diff --git a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinatorType.swift b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinatorType.swift
index 16a79760cf..f26862e903 100644
--- a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinatorType.swift
+++ b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartCoordinatorType.swift
@@ -19,8 +19,7 @@
import Foundation
protocol DeviceVerificationStartCoordinatorDelegate: AnyObject {
- func deviceVerificationStartCoordinator(_ coordinator: DeviceVerificationStartCoordinatorType, didCompleteWithOutgoingTransaction transaction: MXSASTransaction)
- func deviceVerificationStartCoordinator(_ coordinator: DeviceVerificationStartCoordinatorType, didTransactionCancelled transaction: MXSASTransaction)
+ func deviceVerificationStartCoordinator(_ coordinator: DeviceVerificationStartCoordinatorType, otherDidAcceptRequest request: MXKeyVerificationRequest)
func deviceVerificationStartCoordinatorDidCancel(_ coordinator: DeviceVerificationStartCoordinatorType)
}
diff --git a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModel.swift b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModel.swift
index 4a8d0e66f4..8a64cc8815 100644
--- a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModel.swift
+++ b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModel.swift
@@ -29,7 +29,7 @@ final class DeviceVerificationStartViewModel: DeviceVerificationStartViewModelTy
private let otherUser: MXUser
private let otherDevice: MXDeviceInfo
- private var transaction: MXSASTransaction!
+ private var request: MXKeyVerificationRequest?
// MARK: Public
@@ -52,12 +52,12 @@ final class DeviceVerificationStartViewModel: DeviceVerificationStartViewModelTy
case .beginVerifying:
self.beginVerifying()
case .verifyUsingLegacy:
- self.cancelTransaction()
+ self.cancelRequest()
self.update(viewState: .verifyUsingLegacy(self.session, self.otherDevice))
case .verifiedUsingLegacy:
self.coordinatorDelegate?.deviceVerificationStartViewModelDidUseLegacyVerification(self)
case .cancel:
- self.cancelTransaction()
+ self.cancelRequest()
self.coordinatorDelegate?.deviceVerificationStartViewModelDidCancel(self)
}
}
@@ -67,30 +67,22 @@ final class DeviceVerificationStartViewModel: DeviceVerificationStartViewModelTy
private func beginVerifying() {
self.update(viewState: .loading)
- self.verificationManager.beginKeyVerification(withUserId: self.otherUser.userId, andDeviceId: self.otherDevice.deviceId, method: MXKeyVerificationMethodSAS, success: { [weak self] (transaction) in
-
- guard let sself = self else {
- return
- }
- guard let sasTransaction = transaction as? MXSASTransaction, !sasTransaction.isIncoming else {
+ self.verificationManager.requestVerificationByToDevice(withUserId: otherUser.userId, deviceIds: [otherDevice.deviceId], methods: [MXKeyVerificationMethodSAS], success: { [weak self] request in
+ guard let self = self else {
return
}
- sself.transaction = sasTransaction
+ self.request = request
- sself.update(viewState: .loaded)
- sself.registerTransactionDidStateChangeNotification(transaction: sasTransaction)
+ self.update(viewState: .loaded)
+ self.registerKeyVerificationRequestDidChangeNotification(for: request)
}, failure: {[weak self] error in
self?.update(viewState: .error(error))
})
}
- private func cancelTransaction() {
- guard let transaction = self.transaction else {
- return
- }
-
- transaction.cancel(with: MXTransactionCancelCode.user())
+ private func cancelRequest() {
+ request?.cancel(with: MXTransactionCancelCode.user(), success: nil)
}
private func update(viewState: DeviceVerificationStartViewState) {
@@ -98,37 +90,41 @@ final class DeviceVerificationStartViewModel: DeviceVerificationStartViewModelTy
}
- // MARK: - MXKeyVerificationTransactionDidChange
+ // MARK: - MXKeyVerificationRequestDidChange
- private func registerTransactionDidStateChangeNotification(transaction: MXSASTransaction) {
- NotificationCenter.default.addObserver(self, selector: #selector(transactionDidStateChange(notification:)), name: NSNotification.Name.MXKeyVerificationTransactionDidChange, object: transaction)
+ private func registerKeyVerificationRequestDidChangeNotification(for request: MXKeyVerificationRequest) {
+ NotificationCenter.default.addObserver(self, selector: #selector(requestDidStateChange(notification:)), name: .MXKeyVerificationRequestDidChange, object: request)
}
- private func unregisterTransactionDidStateChangeNotification() {
- NotificationCenter.default.removeObserver(self, name: .MXKeyVerificationTransactionDidChange, object: nil)
+ private func unregisterKeyVerificationRequestDidChangeNotification() {
+ NotificationCenter.default.removeObserver(self, name: .MXKeyVerificationRequestDidChange, object: nil)
}
-
- @objc private func transactionDidStateChange(notification: Notification) {
- guard let transaction = notification.object as? MXSASTransaction, !transaction.isIncoming else {
+
+ @objc private func requestDidStateChange(notification: Notification) {
+ guard let request = notification.object as? MXKeyVerificationRequest, request.requestId == self.request?.requestId else {
return
}
- switch transaction.state {
- case MXSASTransactionStateShowSAS:
- self.unregisterTransactionDidStateChangeNotification()
- self.coordinatorDelegate?.deviceVerificationStartViewModel(self, didCompleteWithOutgoingTransaction: transaction)
- case MXSASTransactionStateCancelled:
- guard let reason = transaction.reasonCancelCode else {
+ switch request.state {
+ case MXKeyVerificationRequestStateAccepted, MXKeyVerificationRequestStateReady:
+ self.unregisterKeyVerificationRequestDidChangeNotification()
+ self.coordinatorDelegate?.deviceVerificationStartViewModel(self, otherDidAcceptRequest: request)
+
+ case MXKeyVerificationRequestStateCancelled:
+ guard let reason = request.reasonCancelCode else {
return
}
- self.unregisterTransactionDidStateChangeNotification()
+ self.unregisterKeyVerificationRequestDidChangeNotification()
self.update(viewState: .cancelled(reason))
- case MXSASTransactionStateCancelledByMe:
- guard let reason = transaction.reasonCancelCode else {
+ case MXKeyVerificationRequestStateCancelledByMe:
+ guard let reason = request.reasonCancelCode else {
return
}
- self.unregisterTransactionDidStateChangeNotification()
+ self.unregisterKeyVerificationRequestDidChangeNotification()
self.update(viewState: .cancelledByMe(reason))
+ case MXKeyVerificationRequestStateExpired:
+ self.unregisterKeyVerificationRequestDidChangeNotification()
+ self.update(viewState: .error(UserVerificationStartViewModelError.keyVerificationRequestExpired))
default:
break
}
diff --git a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModelType.swift b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModelType.swift
index 015e80fafd..c4f04b2872 100644
--- a/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModelType.swift
+++ b/Riot/Modules/KeyVerification/Device/Start/DeviceVerificationStartViewModelType.swift
@@ -25,8 +25,7 @@ protocol DeviceVerificationStartViewModelViewDelegate: AnyObject {
protocol DeviceVerificationStartViewModelCoordinatorDelegate: AnyObject {
func deviceVerificationStartViewModelDidUseLegacyVerification(_ viewModel: DeviceVerificationStartViewModelType)
- func deviceVerificationStartViewModel(_ viewModel: DeviceVerificationStartViewModelType, didCompleteWithOutgoingTransaction transaction: MXSASTransaction)
- func deviceVerificationStartViewModel(_ viewModel: DeviceVerificationStartViewModelType, didTransactionCancelled transaction: MXSASTransaction)
+ func deviceVerificationStartViewModel(_ viewModel: DeviceVerificationStartViewModelType, otherDidAcceptRequest request: MXKeyVerificationRequest)
func deviceVerificationStartViewModelDidCancel(_ viewModel: DeviceVerificationStartViewModelType)
}
diff --git a/Riot/Modules/KeyVerification/User/SessionsStatus/UserVerificationSessionsStatusViewModel.swift b/Riot/Modules/KeyVerification/User/SessionsStatus/UserVerificationSessionsStatusViewModel.swift
index 104da3b32c..a46b305553 100644
--- a/Riot/Modules/KeyVerification/User/SessionsStatus/UserVerificationSessionsStatusViewModel.swift
+++ b/Riot/Modules/KeyVerification/User/SessionsStatus/UserVerificationSessionsStatusViewModel.swift
@@ -18,10 +18,6 @@
import Foundation
-enum UserVerificationSessionsStatusViewModelError: Error {
- case unknown
-}
-
final class UserVerificationSessionsStatusViewModel: UserVerificationSessionsStatusViewModelType {
// MARK: - Properties
@@ -103,7 +99,7 @@ final class UserVerificationSessionsStatusViewModel: UserVerificationSessionsSta
}
private func getDevicesFromCache(for userId: String) -> [MXDeviceInfo] {
- guard let deviceInfoMap = self.session.crypto.devices(forUser: self.userId) else {
+ guard let deviceInfoMap = self.session.crypto?.devices(forUser: self.userId) else {
return []
}
return Array(deviceInfoMap.values)
@@ -128,9 +124,7 @@ final class UserVerificationSessionsStatusViewModel: UserVerificationSessionsSta
completion(.success(sessionsViewData))
}, failure: { error in
-
- let finalError = error ?? UserVerificationSessionsStatusViewModelError.unknown
- completion(.failure(finalError))
+ completion(.failure(error))
})
return httpOperation
diff --git a/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift b/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift
index 45e8e378f6..604253c268 100644
--- a/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift
+++ b/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift
@@ -189,6 +189,7 @@ extension UserVerificationCoordinator: KeyVerificationCoordinatorDelegate {
func keyVerificationCoordinatorDidComplete(_ coordinator: KeyVerificationCoordinatorType, otherUserId: String, otherDeviceId: String) {
dismissPresenter(coordinator: coordinator)
+ delegate?.userVerificationCoordinatorDidComplete(self)
}
func keyVerificationCoordinatorDidCancel(_ coordinator: KeyVerificationCoordinatorType) {
diff --git a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m
index 128e9b1615..97cc2fb5c3 100644
--- a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m
+++ b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m
@@ -952,7 +952,10 @@ - (void)closeSession:(BOOL)clearStore
{
// Force a reload of device keys at the next session start.
// This will fix potential UISIs other peoples receive for our messages.
- [mxSession.crypto resetDeviceKeys];
+ if ([mxSession.crypto isKindOfClass:[MXLegacyCrypto class]])
+ {
+ [(MXLegacyCrypto *)mxSession.crypto resetDeviceKeys];
+ }
// Clean other stores
[mxSession.scanManager deleteAllAntivirusScans];
@@ -1743,8 +1746,18 @@ - (void)attemptDeviceDehydrationWithKeyData:(NSData *)keyData
return;
}
+ if (![mxSession.crypto.crossSigning isKindOfClass:[MXLegacyCrossSigning class]]) {
+ MXLogFailure(@"Device dehydratation is currently only supported by legacy cross signing, add support to all implementations");
+ if (failure)
+ {
+ failure(nil);
+ }
+ return;
+ }
+ MXLegacyCrossSigning *crossSigning = (MXLegacyCrossSigning *)mxSession.crypto.crossSigning;;
+
MXLogDebug(@"[MXKAccount] attemptDeviceDehydrationWithRetry: starting device dehydration");
- [[MXKAccountManager sharedManager].dehydrationService dehydrateDeviceWithMatrixRestClient:mxRestClient crypto:mxSession.crypto dehydrationKey:keyData success:^(NSString *deviceId) {
+ [[MXKAccountManager sharedManager].dehydrationService dehydrateDeviceWithMatrixRestClient:mxRestClient crossSigning:crossSigning dehydrationKey:keyData success:^(NSString *deviceId) {
MXLogDebug(@"[MXKAccount] attemptDeviceDehydrationWithRetry: device successfully dehydrated");
if (success)
diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m
index 6d231262c9..5202098609 100644
--- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m
+++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m
@@ -65,9 +65,12 @@ - (instancetype)initWithEvent:(MXEvent*)event
_event = event;
_displayFix = MXKRoomBubbleComponentDisplayFixNone;
- if ([event.content[@"format"] isEqualToString:kMXRoomMessageFormatHTML])
+
+ NSString *format = event.content[@"format"];
+ if ([format isKindOfClass:[NSString class]] && [format isEqualToString:kMXRoomMessageFormatHTML])
{
- if ([((NSString*)event.content[@"formatted_body"]) containsString:@" bubbleData = [roomDataSource cellDataOfEventWithEventId:voiceBroadcastInfo.eventId];
+ bubbleData.tag = RoomBubbleCellDataTagVoiceBroadcastPlayback;
+ }
+ }
self.collapsable = NO;
self.collapsed = NO;
- MXLogDebug(@"VB incoming initWithEvent")
break;
}
@@ -205,7 +235,7 @@ - (instancetype)initWithEvent:(MXEvent *)event andRoomState:(MXRoomState *)roomS
}
else if (event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType])
{
- self.tag = RoomBubbleCellDataTagVoiceBroadcast;
+ self.tag = RoomBubbleCellDataTagVoiceBroadcastNoDisplay;
self.collapsable = NO;
self.collapsed = NO;
}
@@ -315,13 +345,11 @@ - (BOOL)hasNoDisplay
}
break;
- case RoomBubbleCellDataTagVoiceBroadcast:
- if (RiotSettings.shared.enableVoiceBroadcast == YES &&
- [VoiceBroadcastInfo isStartedFor:[VoiceBroadcastInfo modelFromJSON:self.events.lastObject.content].state])
- {
- hasNoDisplay = NO;
- }
-
+ case RoomBubbleCellDataTagVoiceBroadcastRecord:
+ case RoomBubbleCellDataTagVoiceBroadcastPlayback:
+ hasNoDisplay = NO;
+ break;
+ case RoomBubbleCellDataTagVoiceBroadcastNoDisplay:
break;
default:
hasNoDisplay = [super hasNoDisplay];
@@ -1072,7 +1100,9 @@ - (BOOL)addEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState
case RoomBubbleCellDataTagLiveLocation:
shouldAddEvent = NO;
break;
- case RoomBubbleCellDataTagVoiceBroadcast:
+ case RoomBubbleCellDataTagVoiceBroadcastRecord:
+ case RoomBubbleCellDataTagVoiceBroadcastPlayback:
+ case RoomBubbleCellDataTagVoiceBroadcastNoDisplay:
shouldAddEvent = NO;
break;
default:
@@ -1143,7 +1173,7 @@ - (BOOL)addEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState
{
shouldAddEvent = NO;
}
- } else if ([event.type isEqualToString:VoiceBroadcastSettings.eventType]) {
+ } else if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) {
shouldAddEvent = NO;
}
break;
diff --git a/Riot/Modules/Room/MXKRoomViewController.h b/Riot/Modules/Room/MXKRoomViewController.h
index 0ff875fc37..dd3bfd2051 100644
--- a/Riot/Modules/Room/MXKRoomViewController.h
+++ b/Riot/Modules/Room/MXKRoomViewController.h
@@ -73,11 +73,6 @@ typedef NS_ENUM(NSUInteger, MXKRoomViewControllerJoinRoomResult) {
*/
MXKAttachment *currentSharedAttachment;
- /**
- The potential text input placeholder is saved when it is replaced temporarily
- */
- NSString *savedInputToolbarPlaceholder;
-
/**
Tell whether the input toolbar required to run an animation indicator.
*/
diff --git a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m
index 7e1ba5129d..aecfcf6652 100644
--- a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m
+++ b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m
@@ -440,7 +440,9 @@ - (BOOL)isRoomMemberCurrentUser
- (void)startUserVerification
{
- [[AppDelegate theDelegate] presentUserVerificationForRoomMember:self.mxRoomMember session:self.mainSession];
+ [[AppDelegate theDelegate] presentUserVerificationForRoomMember:self.mxRoomMember session:self.mainSession completion:^{
+ [self refreshUserEncryptionTrustLevel];
+ }];
}
- (void)presentUserVerification
@@ -1332,6 +1334,7 @@ - (void)roomMemberTitleViewDidLayoutSubview:(RoomMemberTitleView*)titleView
- (void)keyVerificationCoordinatorBridgePresenterDelegateDidComplete:(KeyVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter otherUserId:(NSString * _Nonnull)otherUserId otherDeviceId:(NSString * _Nonnull)otherDeviceId
{
+ [self refreshUserEncryptionTrustLevel];
[self dismissKeyVerificationCoordinatorBridgePresenter];
}
diff --git a/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationSettingsAvatarView.swift b/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationSettingsAvatarView.swift
index 744bfa2b64..caab08a869 100644
--- a/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationSettingsAvatarView.swift
+++ b/Riot/Modules/Room/NotificationSettings/UIKit/RoomNotificationSettingsAvatarView.swift
@@ -25,7 +25,7 @@ class RoomNotificationSettingsAvatarView: UIView {
func configure(viewData: AvatarViewDataProtocol) {
avatarView.fill(with: viewData)
- switch viewData.fallbackImage {
+ switch viewData.fallbackImages?.first {
case .matrixItem(_, let matrixItemDisplayName):
nameLabel.text = matrixItemDisplayName
default:
diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift
index 3ba9d87936..35caf9084b 100644
--- a/Riot/Modules/Room/RoomCoordinator.swift
+++ b/Riot/Modules/Room/RoomCoordinator.swift
@@ -92,7 +92,8 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol {
self.roomViewController.parentSpaceId = parameters.parentSpaceId
TimelinePollProvider.shared.session = parameters.session
- TimelineVoiceBroadcastProvider.shared.session = parameters.session
+ VoiceBroadcastPlaybackProvider.shared.session = parameters.session
+ VoiceBroadcastRecorderProvider.shared.session = parameters.session
super.init()
}
diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m
index a1c520302d..2149ecaa42 100644
--- a/Riot/Modules/Room/RoomViewController.m
+++ b/Riot/Modules/Room/RoomViewController.m
@@ -598,6 +598,7 @@ - (void)viewWillDisappear:(BOOL)animated
isAppeared = NO;
[VoiceMessageMediaServiceProvider.sharedProvider pauseAllServices];
+ [VoiceBroadcastRecorderProvider.shared pauseRecording];
// Stop the loading indicator even if the session is still in progress
[self stopLoadingUserIndicator];
@@ -1775,15 +1776,20 @@ - (BOOL)isCallActive
|| self.customizedRoomDataSource.jitsiWidget;
}
+- (BOOL)canSendStateEventWithType:(MXEventTypeString)eventTypeString
+{
+ MXRoomPowerLevels *powerLevels = [self.roomDataSource.roomState powerLevels];
+ NSInteger requiredPower = [powerLevels minimumPowerLevelForSendingEventAsStateEvent:eventTypeString];
+ NSInteger myPower = [powerLevels powerLevelOfUserWithUserID:self.roomDataSource.mxSession.myUserId];
+ return myPower >= requiredPower;
+}
+
/**
Returns a flag for the current user whether it's privileged to add/remove Jitsi widgets to this room.
*/
- (BOOL)canEditJitsiWidget
{
- MXRoomPowerLevels *powerLevels = [self.roomDataSource.roomState powerLevels];
- NSInteger requiredPower = [powerLevels minimumPowerLevelForSendingEventAsStateEvent:kWidgetModularEventTypeString];
- NSInteger myPower = [powerLevels powerLevelOfUserWithUserID:self.roomDataSource.mxSession.myUserId];
- return myPower >= requiredPower;
+ return [self canSendStateEventWithType:kWidgetModularEventTypeString];
}
- (void)registerURLPreviewNotifications
@@ -1993,9 +1999,9 @@ - (void)refreshRoomInputToolbar
[self updateInputToolBarVisibility];
// Check whether the input toolbar is ready before updating it.
- if (self.inputToolbarView && [self.inputToolbarView isKindOfClass:RoomInputToolbarView.class])
+ if (self.inputToolbarView && [self inputToolbarConformsToToolbarViewProtocol])
{
- RoomInputToolbarView *roomInputToolbarView = (RoomInputToolbarView*)self.inputToolbarView;
+ id roomInputToolbarView = (id) self.inputToolbarView;
// Update encryption decoration if needed
[self updateEncryptionDecorationForRoomInputToolbar:roomInputToolbarView];
@@ -2115,9 +2121,9 @@ - (UIImage*)roomEncryptionBadgeImage
- (void)updateInputToolbarEncryptionDecoration
{
- if (self.inputToolbarView && [self.inputToolbarView isKindOfClass:RoomInputToolbarView.class])
+ if (self.inputToolbarView && [self inputToolbarConformsToToolbarViewProtocol])
{
- RoomInputToolbarView *roomInputToolbarView = (RoomInputToolbarView*)self.inputToolbarView;
+ id roomInputToolbarView = (id)self.inputToolbarView;
[self updateEncryptionDecorationForRoomInputToolbar:roomInputToolbarView];
}
}
@@ -2133,7 +2139,7 @@ - (void)updateTitleViewEncryptionDecoration
roomTitleView.badgeImageView.image = self.roomEncryptionBadgeImage;
}
-- (void)updateEncryptionDecorationForRoomInputToolbar:(RoomInputToolbarView*)roomInputToolbarView
+- (void)updateEncryptionDecorationForRoomInputToolbar:(id)roomInputToolbarView
{
roomInputToolbarView.isEncryptionEnabled = self.isEncryptionEnabled;
}
@@ -2290,6 +2296,16 @@ - (void)setupActions {
[self roomInputToolbarViewDidTapFileUpload];
}]];
}
+ if (RiotSettings.shared.enableVoiceBroadcast && !self.isNewDirectChat)
+ {
+ [actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionLive.image andAction:^{
+ MXStrongifyAndReturnIfNil(self);
+ if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
+ ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
+ }
+ [self roomInputToolbarViewDidTapVoiceBroadcast];
+ }]];
+ }
if (BuildSettings.pollsEnabled && self.displayConfiguration.sendingPollsEnabled && !self.isNewDirectChat)
{
[actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionPoll.image andAction:^{
@@ -2320,35 +2336,6 @@ - (void)setupActions {
[self showCameraControllerAnimated:YES];
}]];
}
- if (RiotSettings.shared.enableVoiceBroadcast && !self.isNewDirectChat)
- {
- [actionItems addObject:[[RoomActionItem alloc] initWithImage:AssetImages.actionLive.image andAction:^{
- MXStrongifyAndReturnIfNil(self);
- if ([self.inputToolbarView isKindOfClass:RoomInputToolbarView.class]) {
- ((RoomInputToolbarView *) self.inputToolbarView).actionMenuOpened = NO;
- }
-
- // TODO: Init and start voice broadcast
- MXSession* session = self.roomDataSource.mxSession;
- [session getOrCreateVoiceBroadcastServiceFor:self.roomDataSource.room completion:^(VoiceBroadcastService *voiceBroadcastService) {
- if (voiceBroadcastService) {
- if ([VoiceBroadcastInfo isStoppedFor:[voiceBroadcastService getState]]) {
- [session.voiceBroadcastService startVoiceBroadcastWithSuccess:^(NSString * _Nullable success) {
-
- } failure:^(NSError * _Nonnull error) {
-
- }];
- } else {
- [session.voiceBroadcastService stopVoiceBroadcastWithSuccess:^(NSString * _Nullable success) {
-
- } failure:^(NSError * _Nonnull error) {
-
- }];
- }
- }
- }];
- }]];
- }
roomInputView.actionsBar.actionItems = actionItems;
}
@@ -2436,6 +2423,39 @@ - (void)roomInputToolbarViewDidTapFileUpload
self.documentPickerPresenter = documentPickerPresenter;
}
+- (void)roomInputToolbarViewDidTapVoiceBroadcast
+{
+ // Check first the room permission
+ if (![self canSendStateEventWithType:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType])
+ {
+ [self showAlertWithTitle:[VectorL10n voiceBroadcastUnauthorizedTitle] message:[VectorL10n voiceBroadcastPermissionDeniedMessage]];
+ return;
+ }
+
+ MXSession* session = self.roomDataSource.mxSession;
+ // Check whether the user is not already broadcasting here or in another room
+ if (session.voiceBroadcastService)
+ {
+ [self showAlertWithTitle:[VectorL10n voiceBroadcastUnauthorizedTitle] message:[VectorL10n voiceBroadcastAlreadyInProgressMessage]];
+ return;
+ }
+
+ // Request the voice broadcast service to start recording - No service is returned if someone else is already broadcasting in the room
+ [session getOrCreateVoiceBroadcastServiceFor:self.roomDataSource.room completion:^(VoiceBroadcastService *voiceBroadcastService) {
+ if (voiceBroadcastService) {
+ [voiceBroadcastService startVoiceBroadcastWithSuccess:^(NSString * _Nullable success) {
+
+ } failure:^(NSError * _Nonnull error) {
+
+ }];
+ }
+ else
+ {
+ [self showAlertWithTitle:[VectorL10n voiceBroadcastUnauthorizedTitle] message:[VectorL10n voiceBroadcastBlockedBySomeoneElseMessage]];
+ }
+ }];
+}
+
/**
Send a video asset via the room input toolbar prompting the user for the conversion preset to use
if the `showMediaCompressionPrompt` setting has been enabled.
@@ -3211,7 +3231,7 @@ - (RoomTimelineCellIdentifier)cellIdentifierForCellData:(MXKCellData*)cellData a
}
}
}
- else if (bubbleData.tag == RoomBubbleCellDataTagVoiceBroadcast)
+ else if (bubbleData.tag == RoomBubbleCellDataTagVoiceBroadcastPlayback)
{
if (bubbleData.isIncoming)
{
@@ -3244,6 +3264,22 @@ - (RoomTimelineCellIdentifier)cellIdentifierForCellData:(MXKCellData*)cellData a
}
}
}
+ else if (bubbleData.tag == RoomBubbleCellDataTagVoiceBroadcastRecord)
+ {
+ if (bubbleData.isPaginationFirstBubble)
+ {
+ cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithPaginationTitle;
+ }
+ else if (bubbleData.shouldHideSenderInformation)
+ {
+ cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithoutSenderInfo;
+ }
+ else
+ {
+ cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder;
+ }
+ }
+
else if (roomBubbleCellData.getFirstBubbleComponentWithDisplay.event.isEmote)
{
if (bubbleData.isIncoming)
@@ -4565,6 +4601,9 @@ - (BOOL)dataSource:(MXKDataSource *)dataSource shouldDoAction:(NSString *)action
// Do nothing for dummy links
shouldDoAction = NO;
break;
+ case RoomMessageURLTypeHttp:
+ shouldDoAction = YES;
+ break;
default:
{
MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey];
@@ -4590,16 +4629,20 @@ - (BOOL)dataSource:(MXKDataSource *)dataSource shouldDoAction:(NSString *)action
break;
case UITextItemInteractionPresentActions:
{
- // Retrieve the tapped event
- MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey];
-
- if (tappedEvent)
- {
- // Long press on link, present room contextual menu.
- [self showContextualMenuForEvent:tappedEvent fromSingleTapGesture:NO cell:cell animated:YES];
+ if (roomMessageURLType == RoomMessageURLTypeHttp) {
+ shouldDoAction = YES;
+ } else {
+ // Retrieve the tapped event
+ MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey];
+
+ if (tappedEvent)
+ {
+ // Long press on link, present room contextual menu.
+ [self showContextualMenuForEvent:tappedEvent fromSingleTapGesture:NO cell:cell animated:YES];
+ }
+
+ shouldDoAction = NO;
}
-
- shouldDoAction = NO;
}
break;
case UITextItemInteractionPreview:
@@ -4997,27 +5040,12 @@ - (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView heightDidChan
{
if (self.roomInputToolbarContainerHeightConstraint.constant != height)
{
- // Hide temporarily the placeholder to prevent its distortion during height animation
- if (!savedInputToolbarPlaceholder)
- {
- savedInputToolbarPlaceholder = toolbarView.placeholder.length ? toolbarView.placeholder : @"";
- }
- toolbarView.placeholder = nil;
-
[super roomInputToolbarView:toolbarView heightDidChanged:height completion:^(BOOL finished) {
if (completion)
{
completion (finished);
}
-
- // Consider here the saved placeholder only if no new placeholder has been defined during the height animation.
- if (!toolbarView.placeholder)
- {
- // Restore the placeholder if any
- toolbarView.placeholder = self->savedInputToolbarPlaceholder.length ? self->savedInputToolbarPlaceholder : nil;
- }
- self->savedInputToolbarPlaceholder = nil;
}];
}
}
@@ -5070,6 +5098,10 @@ - (void)roomInputToolbarViewShowSendMediaActions:(MXKRoomInputToolbarView *)tool
{
[actionItems addObject:@(ComposerCreateActionAttachments)];
}
+ if (RiotSettings.shared.enableVoiceBroadcast && !self.isNewDirectChat)
+ {
+ [actionItems addObject:@(ComposerCreateActionVoiceBroadcast)];
+ }
if (BuildSettings.pollsEnabled && self.displayConfiguration.sendingPollsEnabled && !self.isNewDirectChat)
{
[actionItems addObject:@(ComposerCreateActionPolls)];
@@ -6223,7 +6255,13 @@ - (void)eventDidChangeSentState:(NSNotification *)notif
// Acknowledge the existence of all devices
[self startActivityIndicator];
- [self.mainSession.crypto setDevicesKnown:self->unknownDevices complete:^{
+
+ if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]])
+ {
+ MXLogFailure(@"[RoomVC] eventDidChangeSentState: Only legacy crypto supports manual setting of known devices");
+ return;
+ }
+ [(MXLegacyCrypto *)self.mainSession.crypto setDevicesKnown:self->unknownDevices complete:^{
self->unknownDevices = nil;
[self stopActivityIndicator];
@@ -8007,6 +8045,9 @@ - (void)composerCreateActionListBridgePresenterDelegateDidComplete:(ComposerCrea
case ComposerCreateActionAttachments:
[self roomInputToolbarViewDidTapFileUpload];
break;
+ case ComposerCreateActionVoiceBroadcast:
+ [self roomInputToolbarViewDidTapVoiceBroadcast];
+ break;
case ComposerCreateActionPolls:
[self.delegate roomViewControllerDidRequestPollCreationFormPresentation:self];
break;
diff --git a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m
index 13eac3bcc7..d5d9f08eda 100644
--- a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m
+++ b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m
@@ -237,6 +237,7 @@ - (void)setupMessageTextView
[tapGesture setDelegate:self];
[self.messageTextView addGestureRecognizer:tapGesture];
self.messageTextView.userInteractionEnabled = YES;
+ self.messageTextView.clipsToBounds = NO;
// Recognise and make tappable phone numbers, address, etc.
self.messageTextView.dataDetectorTypes = UIDataDetectorTypeAll;
@@ -805,7 +806,7 @@ - (void)renderGif
mimetype = bubbleData.attachment.contentInfo[@"mimetype"];
}
- if ([mimetype isEqualToString:@"image/gif"])
+ if ([mimetype isKindOfClass:[NSString class]] && [mimetype isEqualToString:@"image/gif"])
{
if (_isAutoAnimatedGif)
{
diff --git a/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h b/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h
index 640a2e3bce..3348df0e66 100644
--- a/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h
+++ b/Riot/Modules/Room/TimelineCells/RoomTimelineCellIdentifier.h
@@ -178,6 +178,11 @@ typedef NS_ENUM(NSUInteger, RoomTimelineCellIdentifier) {
RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithoutSenderInfo,
RoomTimelineCellIdentifierOutgoingVoiceBroadcastWithPaginationTitle,
+ // - Voice broadcast recorder
+ RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder,
+ RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithoutSenderInfo,
+ RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithPaginationTitle,
+
// - Others
RoomTimelineCellIdentifierEmpty,
RoomTimelineCellIdentifierSelectedSticker,
diff --git a/Riot/Modules/Room/TimelineCells/SizableCell/SizableBaseRoomCell.swift b/Riot/Modules/Room/TimelineCells/SizableCell/SizableBaseRoomCell.swift
index 5aa5f10e52..f33762144e 100644
--- a/Riot/Modules/Room/TimelineCells/SizableCell/SizableBaseRoomCell.swift
+++ b/Riot/Modules/Room/TimelineCells/SizableCell/SizableBaseRoomCell.swift
@@ -16,6 +16,7 @@
import UIKit
import MatrixSDK
+import SwiftUI
@objc protocol SizableBaseRoomCellType: BaseRoomCellProtocol {
static func sizingViewHeightHashValue(from bubbleCellData: MXKRoomBubbleCellData) -> Int
@@ -35,6 +36,7 @@ class SizableBaseRoomCell: BaseRoomCell, SizableBaseRoomCellType {
private static let reactionsViewModelBuilder = RoomReactionsViewModelBuilder()
private static let urlPreviewViewSizer = URLPreviewViewSizer()
+ private var contentVC: UIViewController?
private class var sizingView: SizableBaseRoomCell {
let sizingView: SizableBaseRoomCell
@@ -115,6 +117,10 @@ class SizableBaseRoomCell: BaseRoomCell, SizableBaseRoomCellType {
sizingView.setNeedsLayout()
sizingView.layoutIfNeeded()
+ if let contentVC = sizingView.contentVC as? UIHostingController {
+ contentVC.view.invalidateIntrinsicContentSize()
+ }
+
let fittingSize = CGSize(width: width, height: UIView.layoutFittingCompressedSize.height)
var height = sizingView.systemLayoutSizeFitting(fittingSize).height
@@ -168,4 +174,24 @@ class SizableBaseRoomCell: BaseRoomCell, SizableBaseRoomCellType {
return height
}
+
+ func addContentViewController(_ controller: UIViewController, on contentView: UIView) {
+ controller.view.invalidateIntrinsicContentSize()
+
+ let parent = vc_parentViewController
+ parent?.addChild(controller)
+ contentView.vc_addSubViewMatchingParent(controller.view)
+ controller.didMove(toParent: parent)
+
+ contentVC = controller
+ }
+
+ override func prepareForReuse() {
+ contentVC?.removeFromParent()
+ contentVC?.view.removeFromSuperview()
+ contentVC?.didMove(toParent: nil)
+ contentVC = nil
+
+ super.prepareForReuse()
+ }
}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m b/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m
index 42bad501db..c747476eee 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m
@@ -143,6 +143,13 @@ - (void)registerVoiceBroadcastCellsForTableView:(UITableView*)tableView
[tableView registerClass:VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceBroadcastOutgoingWithPaginationTitleBubbleCell.defaultReuseIdentifier];
}
+- (void)registerVoiceBroadcastRecorderCellsForTableView:(UITableView*)tableView
+{
+ // Outgoing
+ [tableView registerClass:VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell.defaultReuseIdentifier];
+ [tableView registerClass:VoiceBroadcastRecorderOutgoingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceBroadcastRecorderOutgoingWithPaginationTitleBubbleCell.defaultReuseIdentifier];
+}
+
#pragma mark - Mapping
- (NSDictionary*)incomingTextMessageCellsMapping
@@ -318,4 +325,14 @@ - (void)registerVoiceBroadcastCellsForTableView:(UITableView*)tableView
};
}
+- (NSDictionary*)voiceBroadcastRecorderCellsMapping
+{
+ return @{
+ // Outgoing
+ @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder) : VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell.class,
+ @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithoutSenderInfo) : VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell.class,
+ @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithPaginationTitle) : VoiceBroadcastRecorderOutgoingWithPaginationTitleBubbleCell.class,
+ };
+}
+
@end
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/Poll/PollBaseBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/Poll/PollBaseBubbleCell.swift
index b69abdcd4b..993b606c5b 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/Poll/PollBaseBubbleCell.swift
+++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/Poll/PollBaseBubbleCell.swift
@@ -36,10 +36,10 @@ class PollBaseBubbleCell: PollPlainCell {
self.setupBubbleBackgroundView()
}
- override func addPollView(_ pollView: UIView, on contentView: UIView) {
- super.addPollView(pollView, on: contentView)
+ override func addContentViewController(_ controller: UIViewController, on contentView: UIView) {
+ super.addContentViewController(controller, on: contentView)
- self.addBubbleBackgroundViewIfNeeded(for: pollView)
+ self.addBubbleBackgroundViewIfNeeded(for: controller.view)
}
// MARK: - Private
diff --git a/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/TimelineVoiceBroadcastViewModelProtocol.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/Outgoing/VoiceBroadcastRecorderOutgoingWithPaginationTitleBubbleCell.swift
similarity index 70%
rename from RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/TimelineVoiceBroadcastViewModelProtocol.swift
rename to Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/Outgoing/VoiceBroadcastRecorderOutgoingWithPaginationTitleBubbleCell.swift
index 80d44c2111..c30badc8ec 100644
--- a/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/TimelineVoiceBroadcastViewModelProtocol.swift
+++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/Outgoing/VoiceBroadcastRecorderOutgoingWithPaginationTitleBubbleCell.swift
@@ -16,9 +16,12 @@
import Foundation
-protocol TimelineVoiceBroadcastViewModelProtocol {
- var context: TimelineVoiceBroadcastViewModelType.Context { get }
- var completion: (() -> Void)? { get set }
+class VoiceBroadcastRecorderOutgoingWithPaginationTitleBubbleCell: VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell {
+
+ override func setupViews() {
+ super.setupViews()
+
+ roomCellContentView?.showPaginationTitle = true
+ }
- func updateWithVoiceBroadcastDetails(_ voiceBroadcastDetails: TimelineVoiceBroadcastDetails)
}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/Outgoing/VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/Outgoing/VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell.swift
new file mode 100644
index 0000000000..4d56aee969
--- /dev/null
+++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/Outgoing/VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell.swift
@@ -0,0 +1,41 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+class VoiceBroadcastRecorderOutgoingWithoutSenderInfoBubbleCell: VoiceBroadcastRecorderBubbleCell, BubbleOutgoingRoomCellProtocol {
+
+ override func setupViews() {
+ super.setupViews()
+
+ roomCellContentView?.showSenderInfo = false
+
+ // TODO: VB update margins attributes
+ let leftMargin: CGFloat = BubbleRoomCellLayoutConstants.outgoingBubbleBackgroundMargins.left + BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.left
+ let rightMargin: CGFloat = BubbleRoomCellLayoutConstants.outgoingBubbleBackgroundMargins.right + BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.right
+
+ roomCellContentView?.innerContentViewTrailingConstraint.constant = rightMargin
+ roomCellContentView?.innerContentViewLeadingConstraint.constant = leftMargin
+
+ self.setupBubbleDecorations()
+ }
+
+ override func update(theme: Theme) {
+ super.update(theme: theme)
+
+ self.bubbleBackgroundColor = theme.roomCellOutgoingBubbleBackgroundColor
+ }
+}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderBubbleCell.swift
new file mode 100644
index 0000000000..5b7a92a2f7
--- /dev/null
+++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderBubbleCell.swift
@@ -0,0 +1,113 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import UIKit
+
+class VoiceBroadcastRecorderBubbleCell: VoiceBroadcastRecorderPlainCell {
+
+ // MARK: - Properties
+
+ var bubbleBackgroundColor: UIColor?
+
+ // MARK: - Overrides
+
+ override func render(_ cellData: MXKCellData!) {
+ super.render(cellData)
+
+ self.update(theme: ThemeService.shared().theme)
+ }
+
+ override func setupViews() {
+ super.setupViews()
+
+ self.setupBubbleBackgroundView()
+ }
+
+ override func addVoiceBroadcastView(_ voiceBroadcastView: UIView, on contentView: UIView) {
+ super.addVoiceBroadcastView(voiceBroadcastView, on: contentView)
+
+ self.addBubbleBackgroundViewIfNeeded(for: voiceBroadcastView)
+ }
+
+ // MARK: - Private
+
+ private func addBubbleBackgroundViewIfNeeded(for voiceBroadcastView: UIView) {
+
+ guard let messageBubbleBackgroundView = self.getBubbleBackgroundView() else {
+ return
+ }
+
+ self.addBubbleBackgroundView( messageBubbleBackgroundView, to: voiceBroadcastView)
+ messageBubbleBackgroundView.backgroundColor = self.bubbleBackgroundColor
+ }
+
+ private func addBubbleBackgroundView(_ bubbleBackgroundView: RoomMessageBubbleBackgroundView,
+ to voiceBroadcastView: UIView) {
+
+ // TODO: VB update margins attributes
+ let topMargin = BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.top
+ let leftMargin = BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.left
+ let rightMargin = BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.right
+ let bottomMargin = BubbleRoomCellLayoutConstants.pollBubbleBackgroundInsets.bottom
+
+ let topAnchor = voiceBroadcastView.topAnchor
+ let leadingAnchor = voiceBroadcastView.leadingAnchor
+ let trailingAnchor = voiceBroadcastView.trailingAnchor
+ let bottomAnchor = voiceBroadcastView.bottomAnchor
+
+ NSLayoutConstraint.activate([
+ bubbleBackgroundView.topAnchor.constraint(equalTo: topAnchor, constant: -topMargin),
+ bubbleBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -leftMargin),
+ bubbleBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: rightMargin),
+ bubbleBackgroundView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: bottomMargin)
+ ])
+ }
+
+ private func setupBubbleBackgroundView() {
+ let bubbleBackgroundView = RoomMessageBubbleBackgroundView()
+ self.roomCellContentView?.insertSubview(bubbleBackgroundView, at: 0)
+ }
+
+ // The extension property MXKRoomBubbleTableViewCell.messageBubbleBackgroundView is not working there even by doing recursion
+ private func getBubbleBackgroundView() -> RoomMessageBubbleBackgroundView? {
+ guard let contentView = self.roomCellContentView else {
+ return nil
+ }
+
+ let foundView = contentView.subviews.first { view in
+ return view is RoomMessageBubbleBackgroundView
+ }
+ return foundView as? RoomMessageBubbleBackgroundView
+ }
+}
+
+// MARK: - TimestampDisplayable
+extension VoiceBroadcastRecorderBubbleCell: TimestampDisplayable {
+
+ func addTimestampView(_ timestampView: UIView) {
+ guard let messageBubbleBackgroundView = self.getBubbleBackgroundView() else {
+ return
+ }
+ messageBubbleBackgroundView.addTimestampView(timestampView)
+ }
+
+ func removeTimestampView() {
+ guard let messageBubbleBackgroundView = self.getBubbleBackgroundView() else {
+ return
+ }
+ messageBubbleBackgroundView.removeTimestampView()
+ }
+}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/VoiceBroadcastBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/VoiceBroadcastBubbleCell.swift
index a05f002854..67db62e889 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/VoiceBroadcastBubbleCell.swift
+++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/VoiceBroadcast/VoiceBroadcastBubbleCell.swift
@@ -36,10 +36,10 @@ class VoiceBroadcastBubbleCell: VoiceBroadcastPlainCell {
self.setupBubbleBackgroundView()
}
- override func addVoiceBroadcastView(_ voiceBroadcastView: UIView, on contentView: UIView) {
- super.addVoiceBroadcastView(voiceBroadcastView, on: contentView)
-
- self.addBubbleBackgroundViewIfNeeded(for: voiceBroadcastView)
+ override func addContentViewController(_ controller: UIViewController, on contentView: UIView) {
+ super.addContentViewController(controller, on: contentView)
+
+ self.addBubbleBackgroundViewIfNeeded(for: controller.view)
}
// MARK: - Private
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Poll/PollPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Poll/PollPlainCell.swift
index 345f0de953..70cf4370f0 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Poll/PollPlainCell.swift
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Poll/PollPlainCell.swift
@@ -17,8 +17,7 @@
import Foundation
class PollPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCellReadMarkerDisplayable {
-
- private var pollView: UIView?
+
private var event: MXEvent?
override func render(_ cellData: MXKCellData!) {
@@ -28,12 +27,12 @@ class PollPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCell
let bubbleData = cellData as? RoomBubbleCellData,
let event = bubbleData.events.last,
event.eventType == __MXEventType.pollStart,
- let view = TimelinePollProvider.shared.buildTimelinePollViewForEvent(event) else {
+ let controller = TimelinePollProvider.shared.buildTimelinePollVCForEvent(event) else {
return
}
self.event = event
- self.addPollView(view, on: contentView)
+ self.addContentViewController(controller, on: contentView)
}
override func setupViews() {
@@ -52,13 +51,6 @@ class PollPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCell
delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellTapOnContentView, userInfo: [kMXKRoomBubbleCellEventKey: event])
}
-
- func addPollView(_ pollView: UIView, on contentView: UIView) {
-
- self.pollView?.removeFromSuperview()
- contentView.vc_addSubViewMatchingParent(pollView)
- self.pollView = pollView
- }
}
extension PollPlainCell: RoomCellThreadSummaryDisplayable {}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainCell.swift
new file mode 100644
index 0000000000..a65254be5d
--- /dev/null
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainCell.swift
@@ -0,0 +1,65 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+class VoiceBroadcastRecorderPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCellReadMarkerDisplayable {
+
+ private var voiceBroadcastView: UIView?
+ private var event: MXEvent?
+
+ override func render(_ cellData: MXKCellData!) {
+ super.render(cellData)
+
+ guard let contentView = roomCellContentView?.innerContentView,
+ let bubbleData = cellData as? RoomBubbleCellData,
+ let event = bubbleData.events.last,
+ let voiceBroadcastContent = VoiceBroadcastInfo(fromJSON: event.content),
+ voiceBroadcastContent.state == VoiceBroadcastInfo.State.started.rawValue,
+ let view = VoiceBroadcastRecorderProvider.shared.buildVoiceBroadcastRecorderViewForEvent(event, senderDisplayName: bubbleData.senderDisplayName) else {
+ return
+ }
+
+ self.event = event
+ self.addVoiceBroadcastView(view, on: contentView)
+ }
+
+ override func setupViews() {
+ super.setupViews()
+
+ roomCellContentView?.backgroundColor = .clear
+ roomCellContentView?.showSenderInfo = true
+ roomCellContentView?.showPaginationTitle = false
+ }
+
+ // The normal flow for tapping on cell content views doesn't work for bubbles without attributed strings
+ override func onContentViewTap(_ sender: UITapGestureRecognizer) {
+ guard let event = self.event else {
+ return
+ }
+
+ delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellTapOnContentView, userInfo: [kMXKRoomBubbleCellEventKey: event])
+ }
+
+ func addVoiceBroadcastView(_ voiceBroadcastView: UIView, on contentView: UIView) {
+
+ self.voiceBroadcastView?.removeFromSuperview()
+ contentView.vc_addSubViewMatchingParent(voiceBroadcastView)
+ self.voiceBroadcastView = voiceBroadcastView
+ }
+}
+
+extension VoiceBroadcastRecorderPlainCell: RoomCellThreadSummaryDisplayable {}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithPaginationTitlePlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithPaginationTitlePlainCell.swift
new file mode 100644
index 0000000000..4247f306c9
--- /dev/null
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithPaginationTitlePlainCell.swift
@@ -0,0 +1,27 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+class VoiceBroadcastRecorderWithPaginationTitlePlainCell: VoiceBroadcastRecorderPlainCell {
+
+ override func setupViews() {
+ super.setupViews()
+
+ roomCellContentView?.showPaginationTitle = true
+ }
+
+}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithoutSenderInfoPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithoutSenderInfoPlainCell.swift
new file mode 100644
index 0000000000..172b10aee8
--- /dev/null
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderWithoutSenderInfoPlainCell.swift
@@ -0,0 +1,27 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+class VoiceBroadcastRecorderWithoutSenderInfoPlainCell: VoiceBroadcastRecorderPlainCell {
+
+ override func setupViews() {
+ super.setupViews()
+
+ roomCellContentView?.showSenderInfo = false
+ }
+
+}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastPlainCell.swift
index 967f4cef83..14c602c4cd 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastPlainCell.swift
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/VoiceBroadcastPlainCell.swift
@@ -18,7 +18,6 @@ import Foundation
class VoiceBroadcastPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCellReadMarkerDisplayable {
- private var voiceBroadcastView: UIView?
private var event: MXEvent?
override func render(_ cellData: MXKCellData!) {
@@ -29,12 +28,12 @@ class VoiceBroadcastPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable
let event = bubbleData.events.last,
let voiceBroadcastContent = VoiceBroadcastInfo(fromJSON: event.content),
voiceBroadcastContent.state == VoiceBroadcastInfo.State.started.rawValue,
- let view = TimelineVoiceBroadcastProvider.shared.buildTimelineVoiceBroadcastViewForEvent(event) else {
+ let controller = VoiceBroadcastPlaybackProvider.shared.buildVoiceBroadcastPlaybackVCForEvent(event, senderDisplayName: bubbleData.senderDisplayName) else {
return
}
self.event = event
- self.addVoiceBroadcastView(view, on: contentView)
+ self.addContentViewController(controller, on: contentView)
}
override func setupViews() {
@@ -53,13 +52,6 @@ class VoiceBroadcastPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable
delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellTapOnContentView, userInfo: [kMXKRoomBubbleCellEventKey: event])
}
-
- func addVoiceBroadcastView(_ voiceBroadcastView: UIView, on contentView: UIView) {
-
- self.voiceBroadcastView?.removeFromSuperview()
- contentView.vc_addSubViewMatchingParent(voiceBroadcastView)
- self.voiceBroadcastView = voiceBroadcastView
- }
}
extension VoiceBroadcastPlainCell: RoomCellThreadSummaryDisplayable {}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h
index 9f18a71d9a..b1e85a6212 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.h
@@ -58,6 +58,8 @@ NS_ASSUME_NONNULL_BEGIN
- (NSDictionary*)voiceBroadcastCellsMapping;
+- (NSDictionary*)voiceBroadcastRecorderCellsMapping;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m
index db11457d78..4813b539d9 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/PlainRoomTimelineCellProvider.m
@@ -115,6 +115,8 @@ - (void)registerCellsForTableView:(UITableView*)tableView
[self registerVoiceBroadcastCellsForTableView:tableView];
+ [self registerVoiceBroadcastRecorderCellsForTableView:tableView];
+
[tableView registerClass:RoomEmptyBubbleCell.class forCellReuseIdentifier:RoomEmptyBubbleCell.defaultReuseIdentifier];
[tableView registerClass:RoomSelectedStickerBubbleCell.class forCellReuseIdentifier:RoomSelectedStickerBubbleCell.defaultReuseIdentifier];
@@ -279,6 +281,13 @@ - (void)registerVoiceBroadcastCellsForTableView:(UITableView*)tableView
[tableView registerClass:VoiceBroadcastWithPaginationTitlePlainCell.class forCellReuseIdentifier:VoiceBroadcastWithPaginationTitlePlainCell.defaultReuseIdentifier];
}
+- (void)registerVoiceBroadcastRecorderCellsForTableView:(UITableView*)tableView
+{
+ [tableView registerClass:VoiceBroadcastRecorderPlainCell.class forCellReuseIdentifier:VoiceBroadcastRecorderPlainCell.defaultReuseIdentifier];
+ [tableView registerClass:VoiceBroadcastRecorderWithoutSenderInfoPlainCell.class forCellReuseIdentifier:VoiceBroadcastRecorderWithoutSenderInfoPlainCell.defaultReuseIdentifier];
+ [tableView registerClass:VoiceBroadcastRecorderWithPaginationTitlePlainCell.class forCellReuseIdentifier:VoiceBroadcastRecorderWithPaginationTitlePlainCell.defaultReuseIdentifier];
+}
+
#pragma mark Cell class association
- (NSDictionary*)buildCellClasses
@@ -339,6 +348,9 @@ - (void)registerVoiceBroadcastCellsForTableView:(UITableView*)tableView
NSDictionary *voiceBroadcastCellsMapping = [self voiceBroadcastCellsMapping];
[cellClasses addEntriesFromDictionary:voiceBroadcastCellsMapping];
+
+ NSDictionary *voiceBroadcastRecorderCellsMapping = [self voiceBroadcastRecorderCellsMapping];
+ [cellClasses addEntriesFromDictionary:voiceBroadcastRecorderCellsMapping];
NSDictionary *othersCells = @{
@(RoomTimelineCellIdentifierEmpty) : RoomEmptyBubbleCell.class,
@@ -576,5 +588,14 @@ - (void)registerVoiceBroadcastCellsForTableView:(UITableView*)tableView
};
}
+- (NSDictionary*)voiceBroadcastRecorderCellsMapping
+{
+ return @{
+ // Outoing
+ @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorder) : VoiceBroadcastRecorderPlainCell.class,
+ @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithoutSenderInfo) : VoiceBroadcastRecorderWithoutSenderInfoPlainCell.class,
+ @(RoomTimelineCellIdentifierOutgoingVoiceBroadcastRecorderWithPaginationTitle) : VoiceBroadcastRecorderWithPaginationTitlePlainCell.class
+ };
+}
@end
diff --git a/Riot/Modules/Room/Views/Avatar/RoomAvatarViewData.swift b/Riot/Modules/Room/Views/Avatar/RoomAvatarViewData.swift
index 1af0a99aa3..7f8df4e18a 100644
--- a/Riot/Modules/Room/Views/Avatar/RoomAvatarViewData.swift
+++ b/Riot/Modules/Room/Views/Avatar/RoomAvatarViewData.swift
@@ -26,7 +26,7 @@ struct RoomAvatarViewData: AvatarViewDataProtocol {
return roomId
}
- var fallbackImage: AvatarFallbackImage? {
- return .matrixItem(matrixItemId, displayName)
+ var fallbackImages: [AvatarFallbackImage]? {
+ [.matrixItem(matrixItemId, displayName)]
}
}
diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift
index 389e057ead..47d981c866 100644
--- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift
+++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarTextView.swift
@@ -139,7 +139,7 @@ class RoomInputToolbarTextView: UITextView {
}
private func updateUI() {
- var height = sizeThatFits(CGSize(width: bounds.size.width, height: CGFloat.greatestFiniteMagnitude)).height
+ var height = contentSize.height
height = minHeight > 0 ? max(height, minHeight) : height
height = maxHeight > 0 ? min(height, maxHeight) : height
diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h
index 72341ff2a7..4bdea353b6 100644
--- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h
+++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h
@@ -37,6 +37,7 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode)
@property (nonatomic, strong) NSString *eventSenderDisplayName;
@property (nonatomic, assign) RoomInputToolbarViewSendMode sendMode;
+@property (nonatomic, assign) BOOL isEncryptionEnabled;
- (void)setVoiceMessageToolbarView:(UIView *)voiceMessageToolbarView;
- (CGFloat)toolbarHeight;
@@ -80,7 +81,7 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode)
`RoomInputToolbarView` instance is a view used to handle all kinds of available inputs
for a room (message composer, attachments selection...).
*/
-@interface RoomInputToolbarView : MXKRoomInputToolbarView
+@interface RoomInputToolbarView : MXKRoomInputToolbarView
/**
The delegate notified when inputs are ready.
diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m
index cd9195516b..9abfde4213 100644
--- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m
+++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m
@@ -19,7 +19,6 @@
#import "ThemeService.h"
#import "GeneratedInterface-Swift.h"
-#import "GBDeviceInfo_iOS.h"
static const CGFloat kContextBarHeight = 24;
static const CGFloat kActionMenuAttachButtonSpringVelocity = 7;
@@ -30,7 +29,7 @@
static const NSTimeInterval kActionMenuContentAlphaAnimationDuration = .2;
static const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
-@interface RoomInputToolbarView()
+@interface RoomInputToolbarView()
@property (nonatomic, weak) IBOutlet UIView *mainToolbarView;
@@ -281,69 +280,6 @@ - (void)updateToolbarButtonLabelWithPreviousMode:(RoomInputToolbarViewSendMode)p
}
}
-- (void)updatePlaceholder
-{
- // Consider the default placeholder
-
- NSString *placeholder;
-
- // Check the device screen size before using large placeholder
- BOOL shouldDisplayLargePlaceholder = [GBDeviceInfo deviceInfo].family == GBDeviceFamilyiPad || [GBDeviceInfo deviceInfo].displayInfo.display >= GBDeviceDisplay5p8Inch;
-
- if (!shouldDisplayLargePlaceholder)
- {
- switch (_sendMode)
- {
- case RoomInputToolbarViewSendModeReply:
- placeholder = [VectorL10n roomMessageReplyToShortPlaceholder];
- break;
-
- case RoomInputToolbarViewSendModeCreateDM:
- placeholder = [VectorL10n roomFirstMessagePlaceholder];
- break;
-
- default:
- placeholder = [VectorL10n roomMessageShortPlaceholder];
- break;
- }
- }
- else
- {
- if (_isEncryptionEnabled)
- {
- switch (_sendMode)
- {
- case RoomInputToolbarViewSendModeReply:
- placeholder = [VectorL10n encryptedRoomMessageReplyToPlaceholder];
- break;
-
- default:
- placeholder = [VectorL10n encryptedRoomMessagePlaceholder];
- break;
- }
- }
- else
- {
- switch (_sendMode)
- {
- case RoomInputToolbarViewSendModeReply:
- placeholder = [VectorL10n roomMessageReplyToPlaceholder];
- break;
-
- case RoomInputToolbarViewSendModeCreateDM:
- placeholder = [VectorL10n roomFirstMessagePlaceholder];
- break;
-
- default:
- placeholder = [VectorL10n roomMessagePlaceholder];
- break;
- }
- }
- }
-
- self.placeholder = placeholder;
-}
-
- (void)setPlaceholder:(NSString *)inPlaceholder
{
[super setPlaceholder:inPlaceholder];
diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.swift b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.swift
index 6a9de2f30d..045fcc9a49 100644
--- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.swift
+++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.swift
@@ -16,6 +16,7 @@
import Foundation
import UIKit
+import GBDeviceInfo
extension RoomInputToolbarView {
open override func sendCurrentMessage() {
@@ -28,15 +29,66 @@ extension RoomInputToolbarView {
self.becomeFirstResponder()
temp.removeFromSuperview()
}
-
+
// Send message if any.
if let messageToSend = self.attributedTextMessage, messageToSend.length > 0 {
self.delegate.roomInputToolbarView(self, sendAttributedTextMessage: messageToSend)
}
-
+
// Reset message, disable view animation during the update to prevent placeholder distorsion.
UIView.setAnimationsEnabled(false)
self.attributedTextMessage = nil
UIView.setAnimationsEnabled(true)
}
}
+
+@objc extension RoomInputToolbarView {
+ func updatePlaceholder() {
+ updatePlaceholderText()
+ }
+}
+
+extension RoomInputToolbarViewProtocol where Self: MXKRoomInputToolbarView {
+ func updatePlaceholderText() {
+ // Consider the default placeholder
+
+ let placeholder: String
+
+ // Check the device screen size before using large placeholder
+ let shouldDisplayLargePlaceholder = GBDeviceInfo.deviceInfo().family == .familyiPad || GBDeviceInfo.deviceInfo().displayInfo.display.rawValue >= GBDeviceDisplay.display5p8Inch.rawValue
+
+ if !shouldDisplayLargePlaceholder {
+ switch sendMode {
+ case .reply:
+ placeholder = VectorL10n.roomMessageReplyToShortPlaceholder
+ case .createDM:
+ placeholder = VectorL10n.roomFirstMessagePlaceholder
+
+ default:
+ placeholder = VectorL10n.roomMessageShortPlaceholder
+ }
+ } else {
+ if isEncryptionEnabled {
+ switch sendMode {
+ case .reply:
+ placeholder = VectorL10n.encryptedRoomMessageReplyToPlaceholder
+
+ default:
+ placeholder = VectorL10n.encryptedRoomMessagePlaceholder
+ }
+ } else {
+ switch sendMode {
+ case .reply:
+ placeholder = VectorL10n.roomMessageReplyToPlaceholder
+
+ case .createDM:
+ placeholder = VectorL10n.roomFirstMessagePlaceholder
+ default:
+ placeholder = VectorL10n.roomMessagePlaceholder
+ }
+ }
+ }
+
+ self.placeholder = placeholder
+ }
+}
diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib
index 16118e659b..ca3b0f5a61 100644
--- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib
+++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.xib
@@ -1,9 +1,9 @@
-
+
-
+
@@ -41,7 +41,7 @@
-
+
@@ -69,7 +69,7 @@
-
+
diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift
index 5f2c06cc70..f886276fd6 100644
--- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift
+++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift
@@ -29,11 +29,10 @@ import CoreGraphics
// The toolbar for editing with rich text
class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInputToolbarViewProtocol {
-
-
// MARK: - Properties
// MARK: Private
+ private var voiceMessageToolbarView: VoiceMessageToolbarView?
private var cancellables = Set()
private var heightConstraint: NSLayoutConstraint!
private var hostingViewController: VectorHostingController!
@@ -42,33 +41,12 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
// MARK: Public
- /// The current html content of the composer
- var htmlContent: String {
- get {
- wysiwygViewModel.content.html
- }
- set {
- wysiwygViewModel.setHtmlContent(newValue)
- }
- }
-
- /// The display name to show when in edit/reply
- var eventSenderDisplayName: String! {
- get {
- viewModel.eventSenderDisplayName
- }
- set {
- viewModel.eventSenderDisplayName = newValue
- }
- }
-
- /// Whether the composer is in send, reply or edit mode.
- var sendMode: RoomInputToolbarViewSendMode {
+ override var placeholder: String! {
get {
- viewModel.sendMode.legacySendMode
+ viewModel.placeholder
}
set {
- viewModel.sendMode = ComposerSendMode(from: newValue)
+ viewModel.placeholder = newValue
}
}
@@ -86,13 +64,11 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
super.awakeFromNib()
viewModel.callback = { [weak self] result in
- guard let self = self else { return }
- switch result {
- case .cancel:
- self.toolbarViewDelegate?.roomInputToolbarViewDidTapCancel(self)
- }
+ self?.handleViewModelResult(result)
}
+
inputAccessoryViewForKeyboard = UIView(frame: .zero)
+
let composer = Composer(viewModel: viewModel.context,
wysiwygViewModel: wysiwygViewModel,
sendMessageAction: { [weak self] content in
@@ -112,7 +88,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
let subView: UIView = hostingViewController.view
self.addSubview(subView)
- hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false
+ self.translatesAutoresizingMaskIntoConstraints = false
subView.translatesAutoresizingMaskIntoConstraints = false
heightConstraint = subView.heightAnchor.constraint(equalToConstant: height)
NSLayoutConstraint.activate([
@@ -127,7 +103,13 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
.sink(receiveValue: { [weak self] idealHeight in
guard let self = self else { return }
self.updateToolbarHeight(wysiwygHeight: idealHeight)
- })
+ }),
+ // Required to update the view constraints after minimise/maximise is tapped
+ wysiwygViewModel.$idealHeight
+ .removeDuplicates()
+ .sink { [weak hostingViewController] _ in
+ hostingViewController?.view.setNeedsLayout()
+ }
]
update(theme: ThemeService.shared().theme)
@@ -150,11 +132,30 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: content.html, withRawText: content.plainText)
}
-
private func showSendMediaActions() {
delegate?.roomInputToolbarViewShowSendMediaActions?(self)
}
+ private func handleViewModelResult(_ result: ComposerViewModelResult) {
+ switch result {
+ case .cancel:
+ self.toolbarViewDelegate?.roomInputToolbarViewDidTapCancel(self)
+ case let .contentDidChange(isEmpty):
+ setVoiceMessageToolbarIsHidden(!isEmpty)
+ }
+ }
+
+ private func setVoiceMessageToolbarIsHidden(_ isHidden: Bool) {
+ guard let voiceMessageToolbarView = voiceMessageToolbarView else { return }
+ UIView.transition(
+ with: voiceMessageToolbarView, duration: 0.15,
+ options: .transitionCrossDissolve,
+ animations: {
+ voiceMessageToolbarView.isHidden = isHidden
+ }
+ )
+ }
+
private func registerThemeServiceDidChangeThemeNotification() {
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
}
@@ -168,12 +169,64 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
wysiwygViewModel.textColor = theme.colors.primaryContent
}
- // MARK: - RoomInputToolbarViewProtocol
+ // MARK: - HtmlRoomInputToolbarViewProtocol
+ var isEncryptionEnabled = false {
+ didSet {
+ updatePlaceholderText()
+ }
+ }
+
+ /// The current html content of the composer
+ var htmlContent: String {
+ get {
+ wysiwygViewModel.content.html
+ }
+ set {
+ wysiwygViewModel.setHtmlContent(newValue)
+ }
+ }
+
+ /// The display name to show when in edit/reply
+ var eventSenderDisplayName: String! {
+ get {
+ viewModel.eventSenderDisplayName
+ }
+ set {
+ viewModel.eventSenderDisplayName = newValue
+ }
+ }
+
+ /// Whether the composer is in send, reply or edit mode.
+ var sendMode: RoomInputToolbarViewSendMode {
+ get {
+ viewModel.sendMode.legacySendMode
+ }
+ set {
+ viewModel.sendMode = ComposerSendMode(from: newValue)
+ updatePlaceholderText()
+ }
+ }
/// Add the voice message toolbar to the composer
/// - Parameter voiceMessageToolbarView: the voice message toolbar UIView
func setVoiceMessageToolbarView(_ voiceMessageToolbarView: UIView!) {
- // TODO embed the voice messages UI
+ if let voiceMessageToolbarView = voiceMessageToolbarView as? VoiceMessageToolbarView {
+ self.voiceMessageToolbarView = voiceMessageToolbarView
+ voiceMessageToolbarView.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.deactivate(voiceMessageToolbarView.containersTopConstraints)
+ addSubview(voiceMessageToolbarView)
+ NSLayoutConstraint.activate(
+ [
+ hostingViewController.view.topAnchor.constraint(equalTo: voiceMessageToolbarView.topAnchor),
+ hostingViewController.view.leftAnchor.constraint(equalTo: voiceMessageToolbarView.leftAnchor),
+ hostingViewController.view.bottomAnchor.constraint(equalTo: voiceMessageToolbarView.bottomAnchor, constant: 4),
+ hostingViewController.view.rightAnchor.constraint(equalTo: voiceMessageToolbarView.rightAnchor)
+ ]
+ )
+ } else {
+ self.voiceMessageToolbarView?.removeFromSuperview()
+ self.voiceMessageToolbarView = nil
+ }
}
func toolbarHeight() -> CGFloat {
diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift
index c7e52e89a2..dc46839e39 100644
--- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift
+++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift
@@ -208,7 +208,8 @@ class VoiceMessageAttachmentCacheManager {
return
}
- let newURL = temporaryFilesFolderURL.appendingPathComponent(identifier).appendingPathExtension("m4a")
+ let fileExtension = filePath.hasSuffix(".mp4") ? "mp4" : "m4a"
+ let newURL = temporaryFilesFolderURL.appendingPathComponent(identifier).appendingPathExtension(fileExtension)
let conversionCompletion: (Result) -> Void = { result in
self.workQueue.async {
diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift
index 7a21edcf64..996e33b4a9 100644
--- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift
+++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioConverter.swift
@@ -42,7 +42,12 @@ struct VoiceMessageAudioConverter {
static func convertToMPEG4AAC(sourceURL: URL, destinationURL: URL, completion: @escaping (Result) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
do {
- try OGGConverter.convertOpusOGGToM4aFile(src: sourceURL, dest: destinationURL)
+ if sourceURL.pathExtension == "mp4" {
+ try FileManager.default.copyItem(atPath: sourceURL.path, toPath: destinationURL.path)
+ } else {
+ try OGGConverter.convertOpusOGGToM4aFile(src: sourceURL, dest: destinationURL)
+ }
+
DispatchQueue.main.async {
completion(.success(()))
}
diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift
index ebe038c6d1..46e06cfeda 100644
--- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift
+++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift
@@ -35,12 +35,13 @@ enum VoiceMessageAudioPlayerError: Error {
class VoiceMessageAudioPlayer: NSObject {
private var playerItem: AVPlayerItem?
- private var audioPlayer: AVPlayer?
+ private var audioPlayer: AVQueuePlayer?
private var statusObserver: NSKeyValueObservation?
private var playbackBufferEmptyObserver: NSKeyValueObservation?
private var rateObserver: NSKeyValueObservation?
private var playToEndObserver: NSObjectProtocol?
+ private var appBackgroundObserver: NSObjectProtocol?
private let delegateContainer = DelegateContainer()
@@ -63,6 +64,14 @@ class VoiceMessageAudioPlayer: NSObject {
return abs(CMTimeGetSeconds(audioPlayer?.currentTime() ?? .zero))
}
+ var playerItems: [AVPlayerItem] {
+ guard let audioPlayer = audioPlayer else {
+ return []
+ }
+
+ return audioPlayer.items()
+ }
+
private(set) var isStopped = true
deinit {
@@ -84,11 +93,30 @@ class VoiceMessageAudioPlayer: NSObject {
}
playerItem = AVPlayerItem(url: url)
- audioPlayer = AVPlayer(playerItem: playerItem)
+ audioPlayer = AVQueuePlayer(playerItem: playerItem)
addObservers()
}
+ func addContentFromURL(_ url: URL) {
+ let playerItem = AVPlayerItem(url: url)
+ audioPlayer?.insert(playerItem, after: nil)
+
+ // audioPlayerDidFinishPlaying must be called on this last AVPlayerItem
+ NotificationCenter.default.removeObserver(playToEndObserver as Any)
+ playToEndObserver = NotificationCenter.default.addObserver(forName: Notification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem, queue: nil) { [weak self] notification in
+ guard let self = self else { return }
+
+ self.delegateContainer.notifyDelegatesWithBlock { delegate in
+ (delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidFinishPlaying(self)
+ }
+ }
+ }
+
+ func removeAllPlayerItems() {
+ audioPlayer?.removeAllItems()
+ }
+
func unloadContent() {
url = nil
audioPlayer?.replaceCurrentItem(with: nil)
@@ -121,7 +149,7 @@ class VoiceMessageAudioPlayer: NSObject {
audioPlayer?.seek(to: .zero)
}
- func seekToTime(_ time: TimeInterval, completionHandler:@escaping (Bool) -> Void = { _ in }) {
+ func seekToTime(_ time: TimeInterval, completionHandler: @escaping (Bool) -> Void = { _ in }) {
audioPlayer?.seek(to: CMTime(seconds: time, preferredTimescale: 60000), completionHandler: completionHandler)
}
@@ -198,6 +226,15 @@ class VoiceMessageAudioPlayer: NSObject {
(delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidFinishPlaying(self)
}
}
+
+ appBackgroundObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
+ guard let self = self, !BuildSettings.allowBackgroundAudioMessagePlayback else { return }
+
+ self.pause()
+ self.delegateContainer.notifyDelegatesWithBlock { delegate in
+ (delegate as? VoiceMessageAudioPlayerDelegate)?.audioPlayerDidPausePlaying(self)
+ }
+ }
}
private func removeObservers() {
@@ -205,6 +242,7 @@ class VoiceMessageAudioPlayer: NSObject {
playbackBufferEmptyObserver?.invalidate()
rateObserver?.invalidate()
NotificationCenter.default.removeObserver(playToEndObserver as Any)
+ NotificationCenter.default.removeObserver(appBackgroundObserver as Any)
}
}
diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift
index 3037c67d07..54262f8289 100644
--- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift
+++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift
@@ -183,6 +183,10 @@ import MediaPlayer
}
private func setUpRemoteCommandCenter() {
+ guard BuildSettings.allowBackgroundAudioMessagePlayback else {
+ return
+ }
+
displayLink.isPaused = false
UIApplication.shared.beginReceivingRemoteControlEvents()
@@ -252,14 +256,8 @@ import MediaPlayer
return
}
- let artwork = MPMediaItemArtwork(boundsSize: Constants.roomAvatarImageSize) { [weak self] size in
- return self?.roomAvatar ?? UIImage()
- }
-
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
- nowPlayingInfoCenter.nowPlayingInfo = [MPMediaItemPropertyTitle: audioPlayer.displayName ?? VectorL10n.voiceMessageLockScreenPlaceholder,
- MPMediaItemPropertyArtist: currentRoomSummary?.displayname as Any,
- MPMediaItemPropertyArtwork: artwork,
+ nowPlayingInfoCenter.nowPlayingInfo = [MPMediaItemPropertyTitle: VectorL10n.voiceMessageLockScreenPlaceholder,
MPMediaItemPropertyPlaybackDuration: audioPlayer.duration as Any,
MPNowPlayingInfoPropertyElapsedPlaybackTime: audioPlayer.currentTime as Any]
}
diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift
index a5e634dfd8..b78d1df768 100644
--- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift
+++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift
@@ -88,6 +88,8 @@ class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGesture
@IBOutlet private var toastNotificationContainerView: UIView!
@IBOutlet private var toastNotificationLabel: UILabel!
+ @IBOutlet var containersTopConstraints: [NSLayoutConstraint]!
+
private var playbackView: VoiceMessagePlaybackView!
private var cancelLabelToRecordButtonDistance: CGFloat = 0.0
diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib
index 52dcce870a..a5cb9a5b83 100644
--- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib
+++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib
@@ -1,16 +1,16 @@
-
+
-
+
-
+
@@ -19,7 +19,7 @@
-
+
@@ -71,7 +71,7 @@
-
+
@@ -267,12 +267,14 @@
+
+
-
+
diff --git a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h
index be19d7b71e..e9db3a583d 100644
--- a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h
+++ b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.h
@@ -39,10 +39,15 @@
@param deviceInfo the device to share keys to.
@param wasNewDevice flag indicating whether this is the first time we meet the device.
@param session the related matrix session.
+ @param crypto the related (legacy) crypto module
@param onComplete a block called when the the dialog is closed.
@return the newly created instance.
*/
-- (instancetype)initWithDeviceInfo:(MXDeviceInfo*)deviceInfo wasNewDevice:(BOOL)wasNewDevice andMatrixSession:(MXSession*)session onComplete:(void (^)(void))onComplete;
+- (instancetype)initWithDeviceInfo:(MXDeviceInfo*)deviceInfo
+ wasNewDevice:(BOOL)wasNewDevice
+ andMatrixSession:(MXSession*)session
+ crypto:(MXLegacyCrypto *)crypto
+ onComplete:(void (^)(void))onComplete;
/**
Show the dialog in a modal way.
diff --git a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m
index 91f62a8d67..6f638bd78f 100644
--- a/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m
+++ b/Riot/Modules/RoomKeyRequest/RoomKeyRequestViewController.m
@@ -26,16 +26,24 @@ @interface RoomKeyRequestViewController () _alertController = nil;
// Accept the received requests from this device
- [self.mxSession.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{
+ [self.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{
self->onComplete();
}];
@@ -108,7 +116,7 @@ - (void)show
self->_alertController = nil;
// Ignore all pending requests from this device
- [self.mxSession.crypto ignoreAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{
+ [self.crypto ignoreAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{
self->onComplete();
}];
@@ -160,14 +168,14 @@ - (void)dismissKeyVerificationCoordinatorBridgePresenter
keyVerificationCoordinatorBridgePresenter = nil;
// Check device new status
- [self.mxSession.crypto downloadKeys:@[self.device.userId] forceDownload:NO success:^(MXUsersDevicesMap *usersDevicesInfoMap, NSDictionary *crossSigningKeysMap) {
+ [self.crypto downloadKeys:@[self.device.userId] forceDownload:NO success:^(MXUsersDevicesMap *usersDevicesInfoMap, NSDictionary *crossSigningKeysMap) {
MXDeviceInfo *deviceInfo = [usersDevicesInfoMap objectForDevice:self.device.deviceId forUser:self.device.userId];
if (deviceInfo && deviceInfo.trustLevel.localVerificationStatus == MXDeviceVerified)
{
// Accept the received requests from this device
// As the device is now verified, all other key requests will be automatically accepted.
- [self.mxSession.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{
+ [self.crypto acceptAllPendingKeyRequestsFromUser:self.device.userId andDevice:self.device.deviceId onComplete:^{
self->onComplete();
}];
diff --git a/Riot/Modules/Rooms/ShowDirectory/Cells/Room/DirectoryRoomTableViewCellVM.swift b/Riot/Modules/Rooms/ShowDirectory/Cells/Room/DirectoryRoomTableViewCellVM.swift
index 099102014f..27659b9be9 100644
--- a/Riot/Modules/Rooms/ShowDirectory/Cells/Room/DirectoryRoomTableViewCellVM.swift
+++ b/Riot/Modules/Rooms/ShowDirectory/Cells/Room/DirectoryRoomTableViewCellVM.swift
@@ -27,19 +27,7 @@ struct DirectoryRoomTableViewCellVM {
// TODO: Use AvatarView subclass in the cell view
func setAvatar(in avatarImageView: MXKImageView) {
-
- let defaultAvatarImage: UIImage?
- var defaultAvatarImageContentMode: UIView.ContentMode = .scaleAspectFill
-
- switch self.avatarViewData.fallbackImage {
- case .matrixItem(let matrixItemId, let matrixItemDisplayName):
- defaultAvatarImage = AvatarGenerator.generateAvatar(forMatrixItem: matrixItemId, withDisplayName: matrixItemDisplayName)
- case .image(let image, let contentMode):
- defaultAvatarImage = image
- defaultAvatarImageContentMode = contentMode ?? .scaleAspectFill
- case .none:
- defaultAvatarImage = nil
- }
+ let (defaultAvatarImage, defaultAvatarImageContentMode) = avatarViewData.fallbackImageParameters() ?? (nil, .scaleAspectFill)
if let avatarUrl = self.avatarViewData.avatarUrl {
avatarImageView.enableInMemoryCache = true
diff --git a/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift b/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift
index 05b691f3a6..2e8e7604ce 100644
--- a/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift
+++ b/Riot/Modules/Secrets/Reset/SecretsResetViewModel.swift
@@ -63,7 +63,7 @@ final class SecretsResetViewModel: SecretsResetViewModelType {
}
private func resetSecrets(with authParameters: [String: Any]) {
- guard let crossSigning = self.session.crypto.crossSigning else {
+ guard let crossSigning = self.session.crypto?.crossSigning else {
return
}
MXLog.debug("[SecretsResetViewModel] resetSecrets")
diff --git a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift
index 3385063ce3..53a03e3595 100644
--- a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift
+++ b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift
@@ -149,11 +149,11 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType {
}
private func showKeyBackupRestore() {
- guard let keyBackupVersion = self.keyBackup?.keyBackupVersion else {
+ guard let backup = keyBackup, let keyBackupVersion = backup.keyBackupVersion else {
return
}
- let coordinator = KeyBackupRecoverCoordinator(session: self.session, keyBackupVersion: keyBackupVersion, navigationRouter: self.navigationRouter)
+ let coordinator = KeyBackupRecoverCoordinator(keyBackup: backup, keyBackupVersion: keyBackupVersion, navigationRouter: self.navigationRouter)
self.add(childCoordinator: coordinator)
coordinator.delegate = self
diff --git a/Riot/Modules/Settings/Security/SecurityViewController.m b/Riot/Modules/Settings/Security/SecurityViewController.m
index 2e7993c38b..d3c7c6c357 100644
--- a/Riot/Modules/Settings/Security/SecurityViewController.m
+++ b/Riot/Modules/Settings/Security/SecurityViewController.m
@@ -324,7 +324,7 @@ - (void)updateSections
// Crypto sessions section
- if (RiotSettings.shared.settingsSecurityScreenShowSessions)
+ if (RiotSettings.shared.settingsSecurityScreenShowSessions && !RiotSettings.shared.enableNewSessionManager)
{
Section *sessionsSection = [Section sectionWithTag:SECTION_CRYPTO_SESSIONS];
@@ -627,7 +627,7 @@ - (void)crossSigningInfoTrustLevelDidChangeNotification:(NSNotification*)notific
- (void)loadCrossSigning
{
- MXCrossSigning *crossSigning = self.mainSession.crypto.crossSigning;
+ id crossSigning = self.mainSession.crypto.crossSigning;
[crossSigning refreshStateWithSuccess:^(BOOL stateUpdated) {
if (stateUpdated)
@@ -643,7 +643,7 @@ - (NSInteger)numberOfRowsInCrossSigningSection
{
NSInteger numberOfRowsInCrossSigningSection;
- MXCrossSigning *crossSigning = self.mainSession.crypto.crossSigning;
+ id crossSigning = self.mainSession.crypto.crossSigning;
switch (crossSigning.state)
{
case MXCrossSigningStateNotBootstrapped: // Action: Bootstrap
@@ -661,7 +661,7 @@ - (NSInteger)numberOfRowsInCrossSigningSection
- (NSAttributedString*)crossSigningInformation
{
- MXCrossSigning *crossSigning = self.mainSession.crypto.crossSigning;
+ id crossSigning = self.mainSession.crypto.crossSigning;
NSString *crossSigningInformation;
switch (crossSigning.state)
@@ -708,7 +708,7 @@ - (UITableViewCell*)crossSigningButtonCellInTableView:(UITableView*)tableView fo
buttonCell.mxkButton.accessibilityIdentifier = nil;
// And customise it
- MXCrossSigning *crossSigning = self.mainSession.crypto.crossSigning;
+ id crossSigning = self.mainSession.crypto.crossSigning;
switch (crossSigning.state)
{
case MXCrossSigningStateNotBootstrapped: // Action: Bootstrap
diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m
index 915e99e123..54bb95d34e 100644
--- a/Riot/Modules/Settings/SettingsViewController.m
+++ b/Riot/Modules/Settings/SettingsViewController.m
@@ -1445,13 +1445,11 @@ - (NSString*)buildAboutSectionFooterTitleWithAccount:(MXKAccount*)account
NSString *sdkVersionInfo = [NSString stringWithFormat:@"Matrix SDK %@", MatrixSDKVersion];
- NSString *olmVersionInfo = [NSString stringWithFormat:@"OLM %@", [OLMKit versionString]];
-
[footerText appendFormat:@"%@\n", loggedUserInfo];
[footerText appendFormat:@"%@\n", homeserverInfo];
[footerText appendFormat:@"%@\n", appVersionInfo];
[footerText appendFormat:@"%@\n", sdkVersionInfo];
- [footerText appendFormat:@"%@", olmVersionInfo];
+ [footerText appendFormat:@"%@", self.mainSession.crypto.version];
return [footerText copy];
}
diff --git a/Riot/Modules/User/Avatar/UserAvatarViewData.swift b/Riot/Modules/User/Avatar/UserAvatarViewData.swift
index 2f9dea0f08..2dad83e98b 100644
--- a/Riot/Modules/User/Avatar/UserAvatarViewData.swift
+++ b/Riot/Modules/User/Avatar/UserAvatarViewData.swift
@@ -26,7 +26,7 @@ struct UserAvatarViewData: AvatarViewDataProtocol {
return userId
}
- var fallbackImage: AvatarFallbackImage? {
- return .matrixItem(matrixItemId, displayName)
+ var fallbackImages: [AvatarFallbackImage]? {
+ [.matrixItem(matrixItemId, displayName), .image(Asset.Images.tabPeople.image, .scaleAspectFill)]
}
}
diff --git a/Riot/Modules/UserDevices/UsersDevicesViewController.m b/Riot/Modules/UserDevices/UsersDevicesViewController.m
index 6e2145c4c4..3b5b8c9a86 100644
--- a/Riot/Modules/UserDevices/UsersDevicesViewController.m
+++ b/Riot/Modules/UserDevices/UsersDevicesViewController.m
@@ -274,7 +274,12 @@ - (IBAction)onDone:(id)sender
{
// Acknowledge the existence of all devices before leaving this screen
[self startActivityIndicator];
- [mxSession.crypto setDevicesKnown:usersDevices complete:^{
+ if (![self.mainSession.crypto isKindOfClass:[MXLegacyCrypto class]])
+ {
+ MXLogFailure(@"[UsersDevicesViewController] onDone: Only legacy crypto supports manual setting of known devices");
+ return;
+ }
+ [(MXLegacyCrypto *)mxSession.crypto setDevicesKnown:usersDevices complete:^{
[self stopActivityIndicator];
[self dismissViewControllerAnimated:YES completion:nil];
diff --git a/Riot/Modules/VoiceBroadcast/MXSession+VoiceBroadcast.swift b/Riot/Modules/VoiceBroadcast/MXSession+VoiceBroadcast.swift
index a20ef0d41b..b6ac2af219 100644
--- a/Riot/Modules/VoiceBroadcast/MXSession+VoiceBroadcast.swift
+++ b/Riot/Modules/VoiceBroadcast/MXSession+VoiceBroadcast.swift
@@ -28,4 +28,8 @@ extension MXSession {
@objc public func getOrCreateVoiceBroadcastService(for room: MXRoom, completion: @escaping (VoiceBroadcastService?) -> Void) {
VoiceBroadcastServiceProvider.shared.getOrCreateVoiceBroadcastService(for: room, completion: completion)
}
+
+ @objc public func tearDownVoiceBroadcastService() {
+ VoiceBroadcastServiceProvider.shared.tearDownVoiceBroadcastService()
+ }
}
diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift
index 1a10324d40..1de0229049 100644
--- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift
+++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift
@@ -25,6 +25,8 @@ public protocol VoiceBroadcastAggregatorDelegate: AnyObject {
func voiceBroadcastAggregatorDidStartLoading(_ aggregator: VoiceBroadcastAggregator)
func voiceBroadcastAggregatorDidEndLoading(_ aggregator: VoiceBroadcastAggregator)
func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didFailWithError: Error)
+ func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveChunk: VoiceBroadcastChunk)
+ func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfo.State)
func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator)
}
@@ -42,17 +44,20 @@ public class VoiceBroadcastAggregator {
private let voiceBroadcastBuilder: VoiceBroadcastBuilder
private var voiceBroadcastInfoStartEventContent: VoiceBroadcastInfo!
+ private var voiceBroadcastSenderId: String!
private var referenceEventsListener: Any?
private var events: [MXEvent] = []
- public private(set) var voiceBroadcast: VoiceBroadcastProtocol! {
+ public private(set) var voiceBroadcast: VoiceBroadcast! {
didSet {
delegate?.voiceBroadcastAggregatorDidUpdateData(self)
}
}
+ public private(set) var isStarted: Bool = false
+ public private(set) var voiceBroadcastState: VoiceBroadcastInfo.State
public var delegate: VoiceBroadcastAggregatorDelegate?
deinit {
@@ -61,31 +66,34 @@ public class VoiceBroadcastAggregator {
}
}
- public init(session: MXSession, room: MXRoom, voiceBroadcastStartEventId: String) throws {
+ public init(session: MXSession, room: MXRoom, voiceBroadcastStartEventId: String, voiceBroadcastState: VoiceBroadcastInfo.State) throws {
self.session = session
self.room = room
self.voiceBroadcastStartEventId = voiceBroadcastStartEventId
+ self.voiceBroadcastState = voiceBroadcastState
self.voiceBroadcastBuilder = VoiceBroadcastBuilder()
NotificationCenter.default.addObserver(self, selector: #selector(handleRoomDataFlush), name: NSNotification.Name.mxRoomDidFlushData, object: self.room)
-
+
try buildVoiceBroadcastStartContent()
}
private func buildVoiceBroadcastStartContent() throws {
guard let event = session.store.event(withEventId: voiceBroadcastStartEventId, inRoom: room.roomId),
- let eventContent = VoiceBroadcastInfo(fromJSON: event.content)
+ let eventContent = VoiceBroadcastInfo(fromJSON: event.content),
+ let senderId = event.stateKey
else {
throw VoiceBroadcastAggregatorError.invalidVoiceBroadcastStartEvent
}
voiceBroadcastInfoStartEventContent = eventContent
+ voiceBroadcastSenderId = senderId
- voiceBroadcast = voiceBroadcastBuilder.build(voiceBroadcastStartEventContent: eventContent,
- events: events,
- currentUserIdentifier: session.myUserId)
-
- reloadVoiceBroadcastData()
+ voiceBroadcast = voiceBroadcastBuilder.build(mediaManager: session.mediaManager,
+ voiceBroadcastStartEventId: voiceBroadcastStartEventId,
+ voiceBroadcastInvoiceBroadcastStartEventContent: eventContent,
+ events: events,
+ currentUserIdentifier: session.myUserId)
}
@objc private func handleRoomDataFlush(sender: Notification) {
@@ -93,10 +101,30 @@ public class VoiceBroadcastAggregator {
return
}
- reloadVoiceBroadcastData()
+ // TODO: What is the impact on room data flush on voice broadcast audio streaming?
+ MXLog.warning("[VoiceBroadcastAggregator] handleRoomDataFlush is not supported yet")
}
- private func reloadVoiceBroadcastData() {
+ private func updateState() {
+ self.room.state { roomState in
+ guard let event = roomState?.stateEvents(with: .custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType))?.last,
+ event.stateKey == self.voiceBroadcastSenderId,
+ let voiceBroadcastInfo = VoiceBroadcastInfo(fromJSON: event.content),
+ (event.eventId == self.voiceBroadcastStartEventId || voiceBroadcastInfo.eventId == self.voiceBroadcastStartEventId),
+ let state = VoiceBroadcastInfo.State(rawValue: voiceBroadcastInfo.state) else {
+ return
+ }
+
+ self.delegate?.voiceBroadcastAggregator(self, didReceiveState: state)
+ }
+ }
+
+ func start() {
+ if isStarted {
+ return
+ }
+ isStarted = true
+
delegate?.voiceBroadcastAggregatorDidStartLoading(self)
session.aggregations.referenceEvents(forEvent: voiceBroadcastStartEventId, inRoom: room.roomId, from: nil, limit: -1) { [weak self] response in
@@ -106,29 +134,66 @@ public class VoiceBroadcastAggregator {
self.events.removeAll()
- self.events.append(contentsOf: response.chunk)
+ let filteredChunk = response.chunk.filter { event in
+ event.sender == self.voiceBroadcastSenderId &&
+ event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil
+ }
+ self.events.append(contentsOf: filteredChunk)
- let eventTypes = [VoiceBroadcastSettings.eventType, kMXEventTypeStringRoomMessage]
+ let eventTypes = [VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType, kMXEventTypeStringRoomMessage]
self.referenceEventsListener = self.room.listen(toEventsOfTypes: eventTypes) { [weak self] event, direction, state in
- // TODO: check sender id to block fake voice broadcast chunk
- guard let self = self,
- let relatedEventId = event.relatesTo?.eventId,
- relatedEventId == self.voiceBroadcastStartEventId,
- event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil else {
+
+ guard let self = self else {
return
}
- self.events.append(event)
-
- self.voiceBroadcast = self.voiceBroadcastBuilder.build(voiceBroadcastStartEventContent: self.voiceBroadcastInfoStartEventContent,
- events: self.events,
- currentUserIdentifier: self.session.myUserId)
+ if event.eventType == .roomMessage {
+ guard event.sender == self.voiceBroadcastSenderId,
+ let relatedEventId = event.relatesTo?.eventId,
+ relatedEventId == self.voiceBroadcastStartEventId,
+ event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil else {
+ return
+ }
+
+ if let chunk = self.voiceBroadcastBuilder.buildChunk(event: event, mediaManager: self.session.mediaManager, voiceBroadcastStartEventId: self.voiceBroadcastStartEventId) {
+ self.delegate?.voiceBroadcastAggregator(self, didReceiveChunk: chunk)
+ }
+
+ if !self.events.contains(where: { newEvent in
+ newEvent.eventId == event.eventId
+ }) {
+ self.events.append(event)
+ MXLog.debug("[VoiceBroadcastAggregator] Got a new chunk for broadcast \(relatedEventId). Total: \(self.events.count)")
+
+ self.voiceBroadcast = self.voiceBroadcastBuilder.build(mediaManager: self.session.mediaManager,
+ voiceBroadcastStartEventId: self.voiceBroadcastStartEventId,
+ voiceBroadcastInvoiceBroadcastStartEventContent: self.voiceBroadcastInfoStartEventContent,
+ events: self.events,
+ currentUserIdentifier: self.session.myUserId)
+ }
+ } else {
+ self.updateState()
+ }
} as Any
- self.voiceBroadcast = self.voiceBroadcastBuilder.build(voiceBroadcastStartEventContent: self.voiceBroadcastInfoStartEventContent,
- events: self.events,
- currentUserIdentifier: self.session.myUserId)
+
+ self.events.forEach { event in
+ guard let chunk = self.voiceBroadcastBuilder.buildChunk(event: event, mediaManager: self.session.mediaManager, voiceBroadcastStartEventId: self.voiceBroadcastStartEventId) else {
+ return
+ }
+ self.delegate?.voiceBroadcastAggregator(self, didReceiveChunk: chunk)
+ }
+
+ self.updateState()
+
+ self.voiceBroadcast = self.voiceBroadcastBuilder.build(mediaManager: self.session.mediaManager,
+ voiceBroadcastStartEventId: self.voiceBroadcastStartEventId,
+ voiceBroadcastInvoiceBroadcastStartEventContent: self.voiceBroadcastInfoStartEventContent,
+ events: self.events,
+ currentUserIdentifier: self.session.myUserId)
+
+ MXLog.debug("[VoiceBroadcastAggregator] Start aggregation with \(self.voiceBroadcast.chunks.count) chunks for broadcast \(self.voiceBroadcastStartEventId)")
self.delegate?.voiceBroadcastAggregatorDidEndLoading(self)
@@ -137,6 +202,8 @@ public class VoiceBroadcastAggregator {
return
}
+ MXLog.error("[VoiceBroadcastAggregator] start failed", context: error)
+ self.isStarted = false
self.delegate?.voiceBroadcastAggregator(self, didFailWithError: error)
}
}
diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastBuilder.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastBuilder.swift
index df2f609078..e27f5258a8 100644
--- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastBuilder.swift
+++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastBuilder.swift
@@ -18,12 +18,29 @@ import Foundation
struct VoiceBroadcastBuilder {
- func build(voiceBroadcastStartEventContent: VoiceBroadcastInfo, events: [MXEvent], currentUserIdentifier: String, hasBeenEdited: Bool = false) -> VoiceBroadcastProtocol {
+ func build(mediaManager: MXMediaManager,
+ voiceBroadcastStartEventId: String,
+ voiceBroadcastInvoiceBroadcastStartEventContent: VoiceBroadcastInfo,
+ events: [MXEvent],
+ currentUserIdentifier: String,
+ hasBeenEdited: Bool = false) -> VoiceBroadcast {
- let voiceBroadcast = VoiceBroadcast()
+ var voiceBroadcast = VoiceBroadcast()
- // TODO: set voice broadcast object
+ voiceBroadcast.chunks = Set(events.compactMap { event in
+ buildChunk(event: event, mediaManager: mediaManager, voiceBroadcastStartEventId: voiceBroadcastStartEventId)
+ })
return voiceBroadcast
}
+
+ func buildChunk(event: MXEvent, mediaManager: MXMediaManager, voiceBroadcastStartEventId: String) -> VoiceBroadcastChunk? {
+ guard let attachment = MXKAttachment(event: event, andMediaManager: mediaManager),
+ let chunkInfo = event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] as? [String: UInt],
+ let sequence = chunkInfo[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkSequence] else {
+ return nil
+ }
+
+ return VoiceBroadcastChunk(voiceBroadcastInfoEventId: voiceBroadcastStartEventId, sequence: sequence, attachment: attachment)
+ }
}
diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h
index 2b759102ee..36b963e475 100644
--- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h
+++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h
@@ -22,21 +22,25 @@ NS_ASSUME_NONNULL_BEGIN
@interface VoiceBroadcastInfo : MXJSONModel
+/// The device id from which the broadcast has been started
+@property (nonatomic) NSString *deviceId;
+
/// The voice broadcast state (started - paused - resumed - stopped).
@property (nonatomic) NSString *state;
/// The length of the voice chunks in seconds. Only required on the started state event.
@property (nonatomic) NSInteger chunkLength;
-/// The event id of the started voice broadcast info state event.
+/// The event id of the started voice broadcast info state event.
@property (nonatomic, strong, nullable) NSString* eventId;
/// The event used to build the MXBeaconInfo.
@property (nonatomic, readonly, nullable) MXEvent *originalEvent;
-- (instancetype)initWithState:(NSString *)state
- chunkLength:(NSInteger)chunkLength
- eventId:(NSString *)eventId;
+- (instancetype)initWithDeviceId:(NSString *)deviceId
+ state:(NSString *)state
+ chunkLength:(NSInteger)chunkLength
+ eventId:(NSString *)eventId;
@end
diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m
index 14f3c80c33..51a50876c5 100644
--- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m
+++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m
@@ -19,12 +19,14 @@
@implementation VoiceBroadcastInfo
-- (instancetype)initWithState:(NSString *)state
- chunkLength:(NSInteger)chunkLength
- eventId:(NSString *)eventId
+- (instancetype)initWithDeviceId:(NSString *)deviceId
+ state:(NSString *)state
+ chunkLength:(NSInteger)chunkLength
+ eventId:(NSString *)eventId
{
if (self = [super init])
{
+ _deviceId = deviceId;
_state = state;
_chunkLength = chunkLength;
_eventId = eventId;
@@ -35,9 +37,18 @@ - (instancetype)initWithState:(NSString *)state
+ (id)modelFromJSON:(NSDictionary *)JSONDictionary
{
+ // Return nil for redacted state event
+ if (!JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyState])
+ {
+ return nil;
+ }
+
NSString *state;
MXJSONModelSetString(state, JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyState]);
+ NSString *deviceId;
+ MXJSONModelSetString(deviceId, JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyDeviceId]);
+
NSInteger chunkLength = BuildSettings.voiceBroadcastChunkLength;
if (JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkLength])
{
@@ -56,13 +67,15 @@ + (id)modelFromJSON:(NSDictionary *)JSONDictionary
}
}
- return [[VoiceBroadcastInfo alloc] initWithState:state chunkLength:chunkLength eventId:eventId];
+ return [[VoiceBroadcastInfo alloc] initWithDeviceId:deviceId state:state chunkLength:chunkLength eventId:eventId];
}
- (NSDictionary *)JSONDictionary
{
NSMutableDictionary *JSONDictionary = [NSMutableDictionary dictionary];
+ JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyDeviceId] = self.deviceId;
+
JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyState] = self.state;
if (_eventId) {
diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastModels.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastModels.swift
index 4b2bfc2581..138af9e32d 100644
--- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastModels.swift
+++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastModels.swift
@@ -16,19 +16,12 @@
import Foundation
-public protocol VoiceBroadcastProtocol {
- var chunks: Set { get }
- var isClosed: Bool { get }
- var kind: VoiceBroadcastKind { get }
-}
-
public enum VoiceBroadcastKind {
- case disclosed
- case undisclosed
+ case player
+ case recorder
}
-class VoiceBroadcast: VoiceBroadcastProtocol {
+public struct VoiceBroadcast {
var chunks: Set = []
- var isClosed: Bool = false
- var kind: VoiceBroadcastKind = .disclosed
+ var kind: VoiceBroadcastKind = .player
}
diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift
index 01dd4e80e6..81cbc51aff 100644
--- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift
+++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift
@@ -23,7 +23,7 @@ public class VoiceBroadcastService: NSObject {
// MARK: - Properties
- private var voiceBroadcastInfoEventId: String?
+ public private(set) var voiceBroadcastInfoEventId: String?
public let room: MXRoom
public private(set) var state: VoiceBroadcastInfo.State
@@ -98,12 +98,14 @@ public class VoiceBroadcastService: NSObject {
/// - mimeType: (optional) the mime type of the file. Defaults to `audio/ogg`
/// - duration: the length of the voice message in milliseconds
/// - samples: an array of floating point values normalized to [0, 1], boxed within NSNumbers
+ /// - sequence: value of the chunk sequence.
/// - success: A block object called when the operation succeeds. It returns the event id of the event generated on the homeserver
/// - failure: A block object called when the operation fails.
func sendChunkOfVoiceBroadcast(audioFileLocalURL: URL,
mimeType: String?,
duration: UInt,
samples: [Float]?,
+ sequence: UInt,
success: @escaping ((String?) -> Void),
failure: @escaping ((Error?) -> Void)) {
guard let voiceBroadcastInfoEventId = self.voiceBroadcastInfoEventId else {
@@ -115,6 +117,7 @@ public class VoiceBroadcastService: NSObject {
mimeType: mimeType,
duration: duration,
samples: samples,
+ sequence: sequence,
success: success,
failure: failure)
}
@@ -130,6 +133,9 @@ public class VoiceBroadcastService: NSObject {
let stateKey = userId
let voiceBroadcastInfo = VoiceBroadcastInfo()
+
+ voiceBroadcastInfo.deviceId = self.room.mxSession.myDeviceId
+
voiceBroadcastInfo.state = state.rawValue
if state != VoiceBroadcastInfo.State.started {
@@ -148,7 +154,7 @@ public class VoiceBroadcastService: NSObject {
return nil
}
- return self.room.sendStateEvent(.custom(VoiceBroadcastSettings.eventType),
+ return self.room.sendStateEvent(.custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType),
content: stateEventContent, stateKey: stateKey) { [weak self] response in
guard let self = self else { return }
@@ -246,6 +252,7 @@ extension MXRoom {
/// - duration: the length of the voice message in milliseconds
/// - samples: an array of floating point values normalized to [0, 1]
/// - threadId: the id of the thread to send the message. nil by default.
+ /// - sequence: value of the chunk sequence.
/// - success: A closure called when the operation is complete.
/// - failure: A closure called when the operation fails.
/// - Returns: a `MXHTTPOperation` instance.
@@ -255,6 +262,7 @@ extension MXRoom {
duration: UInt,
samples: [Float]?,
threadId: String? = nil,
+ sequence: UInt,
success: @escaping ((String?) -> Void),
failure: @escaping ((Error?) -> Void)) -> MXHTTPOperation? {
let boxedSamples = samples?.compactMap { NSNumber(value: $0) }
@@ -265,9 +273,12 @@ extension MXRoom {
failure(VoiceBroadcastServiceError.unknown)
return nil
}
+
+ let sequenceValue = [VoiceBroadcastSettings.voiceBroadcastContentKeyChunkSequence: sequence]
return __sendVoiceMessage(localURL,
- additionalContentParams: [kMXEventRelationRelatesToKey: relatesTo],
+ additionalContentParams: [kMXEventRelationRelatesToKey: relatesTo,
+ VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType: sequenceValue],
mimeType: mimeType,
duration: duration,
samples: boxedSamples,
diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastSettings.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastSettings.swift
index 9d17da35be..425cc03f4c 100644
--- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastSettings.swift
+++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastSettings.swift
@@ -19,8 +19,9 @@ import Foundation
/// Voice Broadcast settings.
@objcMembers
final class VoiceBroadcastSettings: NSObject {
- static let eventType = "io.element.voice_broadcast_info"
+ static let voiceBroadcastInfoContentKeyType = "io.element.voice_broadcast_info"
+ static let voiceBroadcastContentKeyDeviceId = "device_id"
static let voiceBroadcastContentKeyState = "state"
static let voiceBroadcastContentKeyChunkLength = "chunk_length"
static let voiceBroadcastContentKeyChunkType = "io.element.voice_broadcast_chunk"
diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastServiceProvider.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastServiceProvider.swift
index 579ef45d47..e39c838b73 100644
--- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastServiceProvider.swift
+++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastServiceProvider.swift
@@ -66,7 +66,7 @@ class VoiceBroadcastServiceProvider {
/// - completion: Completion block that will return the lastest voice broadcast info state event of the room.
private func getLastVoiceBroadcastInfo(for room: MXRoom, completion: @escaping (MXEvent?) -> Void) {
room.state { roomState in
- completion(roomState?.stateEvents(with: .custom(VoiceBroadcastSettings.eventType))?.last ?? nil)
+ completion(roomState?.stateEvents(with: .custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType))?.last ?? nil)
}
}
diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m
index 85bfbe000e..80efe2f996 100644
--- a/Riot/Utils/EventFormatter.m
+++ b/Riot/Utils/EventFormatter.m
@@ -272,7 +272,7 @@ - (NSAttributedString *)unsafeAttributedStringFromEvent:(MXEvent *)event
// Build the attributed string with the right font and color for the events
return [self renderString:displayText forEvent:event];
}
- } else if ([event.type isEqualToString:VoiceBroadcastSettings.eventType]) {
+ } else if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) {
MXLogDebug(@"VB incoming build string")
}
}
diff --git a/RiotShareExtension/Shared/ShareManager.m b/RiotShareExtension/Shared/ShareManager.m
index 2233b352d1..22d0063be2 100644
--- a/RiotShareExtension/Shared/ShareManager.m
+++ b/RiotShareExtension/Shared/ShareManager.m
@@ -102,7 +102,10 @@ - (void)shareViewController:(ShareViewController *)shareViewController didReques
[session setStore:self.fileStore success:^{
MXStrongifyAndReturnIfNil(session);
- session.crypto.warnOnUnknowDevices = NO; // Do not warn for unknown devices. We have cross-signing now
+ if ([session.crypto isKindOfClass:[MXLegacyCrypto class]])
+ {
+ ((MXLegacyCrypto *)session.crypto).warnOnUnknowDevices = NO; // Do not warn for unknown devices. We have cross-signing now
+ }
self.selectedRooms = [NSMutableArray array];
for (NSString *roomIdentifier in roomIdentifiers) {
diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift
index 0dc3f78d57..30059334e0 100644
--- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift
+++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift
@@ -106,7 +106,11 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
}
func stopScanning(destroy: Bool) {
- zxCapture.delegate = nil
+ if (zxCapture.delegate != nil) {
+ // Setting the zxCapture to nil without checking makes it start
+ // scanning and implicitly requesting camera access
+ zxCapture.delegate = nil
+ }
guard zxCapture.running else {
return
@@ -292,7 +296,7 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
MXLog.debug("[QRLoginService] Received cross-signing details \(responsePayload)")
if let masterKeyFromVerifyingDevice = responsePayload.masterKey,
- let localMasterKey = session.crypto.crossSigningKeys(forUser: session.myUserId).masterKeys?.keys {
+ let localMasterKey = session.crypto.crossSigning.crossSigningKeys(forUser: session.myUserId)?.masterKeys?.keys {
guard masterKeyFromVerifyingDevice == localMasterKey else {
MXLog.error("[QRLoginService] Received invalid master key from verifying device")
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
@@ -348,6 +352,7 @@ class QRLoginService: NSObject, QRLoginServiceProtocol {
await teardownRendezvous()
}
+ @MainActor
private func teardownRendezvous(state: QRLoginServiceState? = nil) async {
// Stop listening for changes, try deleting the resource
_ = await rendezvousService?.tearDown()
diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
index 8beba56a5e..e2b3ce30e5 100644
--- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
+++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
@@ -70,6 +70,7 @@ enum MockAppScreens {
MockTemplateRoomChatScreenState.self,
MockSpaceSelectorScreenState.self,
MockComposerScreenState.self,
- MockComposerCreateActionListScreenState.self
+ MockComposerCreateActionListScreenState.self,
+ MockVoiceBroadcastPlaybackScreenState.self
]
}
diff --git a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift
index 91cf8937f7..ee618b8f8d 100644
--- a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift
+++ b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift
@@ -36,6 +36,7 @@ struct ScreenList: View {
VStack {
TextField("Search", text: $searchQuery)
.textFieldStyle(.roundedBorder)
+ .autocorrectionDisabled()
.padding(.horizontal)
.accessibilityIdentifier("searchQueryTextField")
.onChange(of: searchQuery, perform: search)
diff --git a/RiotSwiftUI/Modules/Common/Test/UI/XCUIApplication+Riot.swift b/RiotSwiftUI/Modules/Common/Test/UI/XCUIApplication+Riot.swift
index a2e7dc2b5c..f0912c5bcd 100644
--- a/RiotSwiftUI/Modules/Common/Test/UI/XCUIApplication+Riot.swift
+++ b/RiotSwiftUI/Modules/Common/Test/UI/XCUIApplication+Riot.swift
@@ -20,16 +20,33 @@ import XCTest
extension XCUIApplication {
func goToScreenWithIdentifier(_ identifier: String) {
// Search for the screen identifier
- textFields["searchQueryTextField"].tap()
- typeText(identifier)
-
+ let textField = textFields["searchQueryTextField"]
let button = buttons[identifier]
- let footer = staticTexts["footerText"]
- while !button.isHittable, !footer.isHittable {
- tables.firstMatch.swipeUp()
+ // Sometimes the search gets stuck without showing any results. Try to nudge it along
+ for _ in 0...10 {
+ textField.clearAndTypeText(identifier)
+ if button.exists {
+ break
+ }
}
button.tap()
}
}
+
+private extension XCUIElement {
+ func clearAndTypeText(_ text: String) {
+ guard let stringValue = value as? String else {
+ XCTFail("Tried to clear and type text into a non string value")
+ return
+ }
+
+ tap()
+
+ let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count)
+
+ typeText(deleteString)
+ typeText(text)
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift
index 0b3a6080f1..457cc612af 100644
--- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift
@@ -42,6 +42,8 @@ struct ComposerCreateActionListViewState: BindableState {
case stickers
/// Upload an attachment
case attachments
+ /// Voice broadcast
+ case voiceBroadcast
/// Create a Poll
case polls
/// Add a location
@@ -63,6 +65,8 @@ extension ComposerCreateAction {
return VectorL10n.wysiwygComposerStartActionStickers
case .attachments:
return VectorL10n.wysiwygComposerStartActionAttachments
+ case .voiceBroadcast:
+ return VectorL10n.wysiwygComposerStartActionVoiceBroadcast
case .polls:
return VectorL10n.wysiwygComposerStartActionPolls
case .location:
@@ -80,6 +84,8 @@ extension ComposerCreateAction {
return "stickersAction"
case .attachments:
return "attachmentsAction"
+ case .voiceBroadcast:
+ return "voiceBroadcastAction"
case .polls:
return "pollsAction"
case .location:
@@ -97,6 +103,8 @@ extension ComposerCreateAction {
return Asset.Images.actionSticker.name
case .attachments:
return Asset.Images.actionFile.name
+ case .voiceBroadcast:
+ return Asset.Images.actionLive.name
case .polls:
return Asset.Images.actionPoll.name
case .location:
diff --git a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift
index 4aa4837856..48d7df0545 100644
--- a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift
@@ -40,11 +40,13 @@ enum MockComposerScreenState: MockScreenState, CaseIterable {
viewModel.callback = { [weak viewModel, weak wysiwygviewModel] result in
guard let viewModel = viewModel else { return }
- if viewModel.sendMode == .edit {
- wysiwygviewModel?.setHtmlContent("")
- }
switch result {
- case .cancel: viewModel.sendMode = .send
+ case .cancel:
+ if viewModel.sendMode == .edit {
+ wysiwygviewModel?.setHtmlContent("")
+ }
+ viewModel.sendMode = .send
+ default: break
}
}
diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift
index 00470aa534..badcd2b20c 100644
--- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift
@@ -127,12 +127,14 @@ enum ComposerSendMode: Equatable {
case createDM
}
-enum ComposerViewAction {
+enum ComposerViewAction: Equatable {
case cancel
+ case contentDidChange(isEmpty: Bool)
}
-enum ComposerViewModelResult {
+enum ComposerViewModelResult: Equatable {
case cancel
+ case contentDidChange(isEmpty: Bool)
}
diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift
index 6cba03bed5..0f8ad1fdc7 100644
--- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift
@@ -19,6 +19,7 @@ import Foundation
struct ComposerViewState: BindableState {
var eventSenderDisplayName: String?
var sendMode: ComposerSendMode = .send
+ var placeholder: String?
}
extension ComposerViewState {
diff --git a/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift b/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift
index 1a37c020b9..c80bea8191 100644
--- a/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/Test/UI/ComposerUITests.swift
@@ -25,12 +25,24 @@ final class ComposerUITests: MockScreenTestCase {
let wysiwygTextView = app.textViews.allElementsBoundByIndex[0]
XCTAssertTrue(wysiwygTextView.exists)
let sendButton = app.buttons["sendButton"]
- XCTAssertTrue(sendButton.exists)
- XCTAssertFalse(sendButton.isEnabled)
+ XCTAssertFalse(sendButton.exists)
wysiwygTextView.tap()
wysiwygTextView.typeText("test")
- XCTAssertTrue(sendButton.isEnabled)
+ XCTAssertTrue(sendButton.exists)
XCTAssertFalse(app.buttons["editButton"].exists)
+
+ let maximiseButton = app.buttons["maximiseButton"]
+ let minimiseButton = app.buttons["minimiseButton"]
+ XCTAssertFalse(minimiseButton.exists)
+ XCTAssertTrue(maximiseButton.exists)
+
+ maximiseButton.tap()
+ XCTAssertTrue(minimiseButton.exists)
+ XCTAssertFalse(maximiseButton.exists)
+
+ minimiseButton.tap()
+ XCTAssertFalse(minimiseButton.exists)
+ XCTAssertTrue(maximiseButton.exists)
}
func testReplyMode() throws {
@@ -39,8 +51,7 @@ final class ComposerUITests: MockScreenTestCase {
let wysiwygTextView = app.textViews.allElementsBoundByIndex[0]
XCTAssertTrue(wysiwygTextView.exists)
let sendButton = app.buttons["sendButton"]
- XCTAssertTrue(sendButton.exists)
- XCTAssertFalse(sendButton.isEnabled)
+ XCTAssertFalse(sendButton.exists)
let cancelButton = app.buttons["cancelButton"]
XCTAssertTrue(cancelButton.exists)
@@ -51,13 +62,26 @@ final class ComposerUITests: MockScreenTestCase {
wysiwygTextView.tap()
wysiwygTextView.typeText("test")
- XCTAssertTrue(sendButton.isEnabled)
+ XCTAssertTrue(sendButton.exists)
XCTAssertFalse(app.buttons["editButton"].exists)
cancelButton.tap()
let textViewContent = wysiwygTextView.value as! String
XCTAssertFalse(textViewContent.isEmpty)
XCTAssertFalse(cancelButton.exists)
+
+ let maximiseButton = app.buttons["maximiseButton"]
+ let minimiseButton = app.buttons["minimiseButton"]
+ XCTAssertFalse(minimiseButton.exists)
+ XCTAssertTrue(maximiseButton.exists)
+
+ maximiseButton.tap()
+ XCTAssertTrue(minimiseButton.exists)
+ XCTAssertFalse(maximiseButton.exists)
+
+ minimiseButton.tap()
+ XCTAssertFalse(minimiseButton.exists)
+ XCTAssertTrue(maximiseButton.exists)
}
func testEditMode() throws {
@@ -66,8 +90,7 @@ final class ComposerUITests: MockScreenTestCase {
let wysiwygTextView = app.textViews.allElementsBoundByIndex[0]
XCTAssertTrue(wysiwygTextView.exists)
let editButton = app.buttons["editButton"]
- XCTAssertTrue(editButton.exists)
- XCTAssertFalse(editButton.isEnabled)
+ XCTAssertFalse(editButton.exists)
let cancelButton = app.buttons["cancelButton"]
XCTAssertTrue(cancelButton.exists)
@@ -78,12 +101,25 @@ final class ComposerUITests: MockScreenTestCase {
wysiwygTextView.tap()
wysiwygTextView.typeText("test")
- XCTAssertTrue(editButton.isEnabled)
+ XCTAssertTrue(editButton.exists)
XCTAssertFalse(app.buttons["sendButton"].exists)
cancelButton.tap()
let textViewContent = wysiwygTextView.value as! String
XCTAssertTrue(textViewContent.isEmpty)
XCTAssertFalse(cancelButton.exists)
+
+ let maximiseButton = app.buttons["maximiseButton"]
+ let minimiseButton = app.buttons["minimiseButton"]
+ XCTAssertFalse(minimiseButton.exists)
+ XCTAssertTrue(maximiseButton.exists)
+
+ maximiseButton.tap()
+ XCTAssertTrue(minimiseButton.exists)
+ XCTAssertFalse(maximiseButton.exists)
+
+ minimiseButton.tap()
+ XCTAssertFalse(minimiseButton.exists)
+ XCTAssertTrue(maximiseButton.exists)
}
}
diff --git a/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift b/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift
index f125b638a4..5f16cfa42f 100644
--- a/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift
@@ -63,4 +63,10 @@ final class ComposerViewModelTests: XCTestCase {
context.send(viewAction: .cancel)
XCTAssert(result == .cancel)
}
+
+ func testPlaceholder() {
+ XCTAssert(context.viewState.placeholder == nil)
+ viewModel.placeholder = "Placeholder Test"
+ XCTAssert(context.viewState.placeholder == "Placeholder Test")
+ }
}
diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift
index 64069e5c33..624c846386 100644
--- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift
@@ -26,7 +26,7 @@ struct Composer: View {
@Environment(\.theme) private var theme: ThemeSwiftUI
@State private var focused = false
- @State private var isActionButtonEnabled = false
+ @State private var isActionButtonShowing = false
private let horizontalPadding: CGFloat = 12
private let borderHeight: CGFloat = 40
@@ -51,6 +51,14 @@ struct Composer: View {
viewModel.viewState.sendMode == .edit ? "editButton" : "sendButton"
}
+ private var toggleButtonAcccessibilityIdentifier: String {
+ wysiwygViewModel.maximised ? "minimiseButton" : "maximiseButton"
+ }
+
+ private var toggleButtonImageName: String {
+ wysiwygViewModel.maximised ? Asset.Images.minimiseComposer.name : Asset.Images.maximiseComposer.name
+ }
+
private var borderColor: Color {
focused ? theme.colors.quarterlyContent : theme.colors.quinaryContent
}
@@ -76,8 +84,6 @@ struct Composer: View {
var body: some View {
VStack(spacing: 8) {
let rect = RoundedRectangle(cornerRadius: cornerRadius)
- // TODO: Fix maximise animation bugs before re-enabling
- // ZStack(alignment: .topTrailing) {
VStack(spacing: 12) {
if viewModel.viewState.shouldDisplayContext {
HStack {
@@ -103,35 +109,39 @@ struct Composer: View {
.padding(.top, 8)
.padding(.horizontal, horizontalPadding)
}
- WysiwygComposerView(
- focused: $focused,
- content: wysiwygViewModel.content,
- replaceText: wysiwygViewModel.replaceText,
- select: wysiwygViewModel.select,
- didUpdateText: wysiwygViewModel.didUpdateText
- )
- .tintColor(theme.colors.accent)
- .frame(height: wysiwygViewModel.idealHeight)
- .padding(.horizontal, horizontalPadding)
- .onAppear {
- wysiwygViewModel.setup()
+ HStack(alignment: .top, spacing: 0) {
+ WysiwygComposerView(
+ focused: $focused,
+ content: wysiwygViewModel.content,
+ replaceText: wysiwygViewModel.replaceText,
+ select: wysiwygViewModel.select,
+ didUpdateText: wysiwygViewModel.didUpdateText
+ )
+ .tintColor(theme.colors.accent)
+ .placeholder(viewModel.viewState.placeholder, color: theme.colors.tertiaryContent)
+ .frame(height: wysiwygViewModel.idealHeight)
+ .onAppear {
+ wysiwygViewModel.setup()
+ }
+ Button {
+ wysiwygViewModel.maximised.toggle()
+ } label: {
+ Image(toggleButtonImageName)
+ .resizable()
+ .foregroundColor(theme.colors.tertiaryContent)
+ .frame(width: 16, height: 16)
+ }
+ .accessibilityIdentifier(toggleButtonAcccessibilityIdentifier)
+ .padding(.leading, 12)
+ .padding(.trailing, 4)
}
- // Button {
- // withAnimation(.easeInOut(duration: 0.25)) {
- // viewModel.maximised.toggle()
- // }
- // } label: {
- // Image(viewModel.maximised ? Asset.Images.minimiseComposer.name : Asset.Images.maximiseComposer.name)
- // .foregroundColor(theme.colors.tertiaryContent)
- // }
- // .padding(.top, 4)
- // .padding(.trailing, 12)
- // }
+ .padding(.horizontal, horizontalPadding)
.padding(.top, topPadding)
.padding(.bottom, verticalPadding)
}
.clipShape(rect)
.overlay(rect.stroke(borderColor, lineWidth: 1))
+ .animation(.easeInOut(duration: 0.1), value: wysiwygViewModel.idealHeight)
.padding(.horizontal, horizontalPadding)
.padding(.top, 8)
.onTapGesture {
@@ -147,7 +157,6 @@ struct Composer: View {
.resizable()
.foregroundColor(theme.colors.tertiaryContent)
.frame(width: 14, height: 14)
-
}
.frame(width: 36, height: 36)
.background(Circle().fill(theme.colors.system))
@@ -158,16 +167,6 @@ struct Composer: View {
}
.frame(height: 44)
Spacer()
- // ZStack {
- // TODO: Add support for voice messages
- // Button {
- //
- // } label: {
- // Image(Asset.Images.voiceMessageRecordButtonDefault.name)
- // .foregroundColor(theme.colors.tertiaryContent)
- // }
- // .isHidden(showSendButton)
- // .isHidden(true)
Button {
sendMessageAction(wysiwygViewModel.content)
wysiwygViewModel.clearContent()
@@ -180,18 +179,18 @@ struct Composer: View {
}
.frame(width: 36, height: 36)
.padding(.leading, 8)
- .disabled(!isActionButtonEnabled)
- .opacity(isActionButtonEnabled ? 1 : 0.3)
- .animation(.easeInOut(duration: 0.15), value: isActionButtonEnabled)
+ .isHidden(!isActionButtonShowing)
.accessibilityIdentifier(actionButtonAccessibilityIdentifier)
.accessibilityLabel(VectorL10n.send)
- .onChange(of: wysiwygViewModel.isContentEmpty) { empty in
- isActionButtonEnabled = !empty
+ .onChange(of: wysiwygViewModel.isContentEmpty) { isEmpty in
+ viewModel.send(viewAction: .contentDidChange(isEmpty: isEmpty))
+ withAnimation(.easeInOut(duration: 0.15)) {
+ isActionButtonShowing = !isEmpty
+ }
}
}
.padding(.horizontal, 12)
.padding(.bottom, 4)
- .animation(.none)
}
}
}
diff --git a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift
index dcb1ec6fed..1e44ed0491 100644
--- a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift
@@ -45,12 +45,23 @@ final class ComposerViewModel: ComposerViewModelType, ComposerViewModelProtocol
}
}
+ var placeholder: String? {
+ get {
+ state.placeholder
+ }
+ set {
+ state.placeholder = newValue
+ }
+ }
+
// MARK: - Public
override func process(viewAction: ComposerViewAction) {
switch viewAction {
case .cancel:
callback?(.cancel)
+ case let .contentDidChange(isEmpty):
+ callback?(.contentDidChange(isEmpty: isEmpty))
}
}
}
diff --git a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift
index 1448f2d1b5..70d943dc76 100644
--- a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift
@@ -21,4 +21,5 @@ protocol ComposerViewModelProtocol {
var callback: ((ComposerViewModelResult) -> Void)? { get set }
var sendMode: ComposerSendMode { get set }
var eventSenderDisplayName: String? { get set }
+ var placeholder: String? { get set }
}
diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift
index a587b23d82..1acd907a41 100644
--- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift
+++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift
@@ -84,8 +84,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
func start() { }
func toPresentable() -> UIViewController {
- VectorHostingController(rootView: TimelinePollView(viewModel: viewModel.context),
- forceZeroSafeAreaInsets: true)
+ VectorHostingController(rootView: TimelinePollView(viewModel: viewModel.context))
}
func canEndPoll() -> Bool {
diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift
index 78b1d8ab75..31fb638490 100644
--- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift
+++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift
@@ -26,13 +26,13 @@ class TimelinePollProvider {
/// Create or retrieve the poll timeline coordinator for this event and return
/// a view to be displayed in the timeline
- func buildTimelinePollViewForEvent(_ event: MXEvent) -> UIView? {
+ func buildTimelinePollVCForEvent(_ event: MXEvent) -> UIViewController? {
guard let session = session, let room = session.room(withRoomId: event.roomId) else {
return nil
}
if let coordinator = coordinatorsForEventIdentifiers[event.eventId] {
- return coordinator.toPresentable().view
+ return coordinator.toPresentable()
}
let parameters = TimelinePollCoordinatorParameters(session: session, room: room, pollStartEvent: event)
@@ -42,7 +42,7 @@ class TimelinePollProvider {
coordinatorsForEventIdentifiers[event.eventId] = coordinator
- return coordinator.toPresentable().view
+ return coordinator.toPresentable()
}
/// Retrieve the poll timeline coordinator for the given event or nil if it hasn't been created yet
diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift
index 5e7eaceefe..9b363d3675 100644
--- a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift
+++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift
@@ -24,36 +24,45 @@ class TimelinePollUITests: MockScreenTestCase {
XCTAssert(app.staticTexts["Question"].exists)
XCTAssert(app.staticTexts["20 votes cast"].exists)
- XCTAssert(app.buttons["First, 10 votes"].exists)
- XCTAssertEqual(app.buttons["First, 10 votes"].value as! String, "50%")
-
- XCTAssert(app.buttons["Second, 5 votes"].exists)
- XCTAssertEqual(app.buttons["Second, 5 votes"].value as! String, "25%")
-
- XCTAssert(app.buttons["Third, 15 votes"].exists)
- XCTAssertEqual(app.buttons["Third, 15 votes"].value as! String, "75%")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption0Count"].label, "10 votes")
+ XCTAssertEqual(app.progressIndicators["PollAnswerOption0Progress"].value as? String, "50%")
+
+ XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption1Count"].label, "5 votes")
+ XCTAssertEqual(app.progressIndicators["PollAnswerOption1Progress"].value as? String, "25%")
+
+ XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption2Count"].label, "15 votes")
+ XCTAssertEqual(app.progressIndicators["PollAnswerOption2Progress"].value as? String, "75%")
- app.buttons["First, 10 votes"].tap()
+ app.buttons["PollAnswerOption0"].tap()
- XCTAssert(app.buttons["First, 11 votes"].exists)
- XCTAssertEqual(app.buttons["First, 11 votes"].value as! String, "55%")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption0Count"].label, "11 votes")
+ XCTAssertEqual(app.progressIndicators["PollAnswerOption0Progress"].value as? String, "55%")
- XCTAssert(app.buttons["Second, 4 votes"].exists)
- XCTAssertEqual(app.buttons["Second, 4 votes"].value as! String, "20%")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption1Count"].label, "4 votes")
+ XCTAssertEqual(app.progressIndicators["PollAnswerOption1Progress"].value as? String, "20%")
- XCTAssert(app.buttons["Third, 15 votes"].exists)
- XCTAssertEqual(app.buttons["Third, 15 votes"].value as! String, "75%")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption2Count"].label, "15 votes")
+ XCTAssertEqual(app.progressIndicators["PollAnswerOption2Progress"].value as? String, "75%")
- app.buttons["Third, 15 votes"].tap()
+ app.buttons["PollAnswerOption2"].tap()
- XCTAssert(app.buttons["First, 10 votes"].exists)
- XCTAssertEqual(app.buttons["First, 10 votes"].value as! String, "50%")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption0Count"].label, "10 votes")
+ XCTAssertEqual(app.progressIndicators["PollAnswerOption0Progress"].value as? String, "50%")
- XCTAssert(app.buttons["Second, 4 votes"].exists)
- XCTAssertEqual(app.buttons["Second, 4 votes"].value as! String, "20%")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption1Count"].label, "4 votes")
+ XCTAssertEqual(app.progressIndicators["PollAnswerOption1Progress"].value as? String, "20%")
- XCTAssert(app.buttons["Third, 16 votes"].exists)
- XCTAssertEqual(app.buttons["Third, 16 votes"].value as! String, "80%")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption2Count"].label, "16 votes")
+ XCTAssertEqual(app.progressIndicators["PollAnswerOption2Progress"].value as? String, "80%")
}
func testOpenUndisclosedPoll() {
@@ -62,29 +71,29 @@ class TimelinePollUITests: MockScreenTestCase {
XCTAssert(app.staticTexts["Question"].exists)
XCTAssert(app.staticTexts["20 votes cast"].exists)
- XCTAssert(!app.buttons["First, 10 votes"].exists)
- XCTAssert(app.buttons["First"].exists)
- XCTAssertTrue((app.buttons["First"].value as! String).isEmpty)
-
- XCTAssert(!app.buttons["Second, 5 votes"].exists)
- XCTAssert(app.buttons["Second"].exists)
- XCTAssertTrue((app.buttons["Second"].value as! String).isEmpty)
+ XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First")
+ XCTAssert(!app.staticTexts["PollAnswerOption0Count"].exists)
+ XCTAssert(!app.progressIndicators["PollAnswerOption0Progress"].exists)
+
+ XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second")
+ XCTAssert(!app.staticTexts["PollAnswerOption1Count"].exists)
+ XCTAssert(!app.progressIndicators["PollAnswerOption1Progress"].exists)
- XCTAssert(!app.buttons["Third, 15 votes"].exists)
- XCTAssert(app.buttons["Third"].exists)
- XCTAssertTrue((app.buttons["Third"].value as! String).isEmpty)
+ XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third")
+ XCTAssert(!app.staticTexts["PollAnswerOption2Count"].exists)
+ XCTAssert(!app.progressIndicators["PollAnswerOption2Progress"].exists)
- app.buttons["First"].tap()
+ app.buttons["PollAnswerOption0"].tap()
- XCTAssert(app.buttons["First"].exists)
- XCTAssert(app.buttons["Second"].exists)
- XCTAssert(app.buttons["Third"].exists)
+ XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third")
- app.buttons["Third"].tap()
+ app.buttons["PollAnswerOption2"].tap()
- XCTAssert(app.buttons["First"].exists)
- XCTAssert(app.buttons["Second"].exists)
- XCTAssert(app.buttons["Third"].exists)
+ XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third")
}
func testClosedDisclosedPoll() {
@@ -100,25 +109,31 @@ class TimelinePollUITests: MockScreenTestCase {
private func checkClosedPoll() {
XCTAssert(app.staticTexts["Question"].exists)
XCTAssert(app.staticTexts["Final results based on 20 votes"].exists)
+
+ XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption0Count"].label, "10 votes")
+ XCTAssertEqual(app.progressIndicators["PollAnswerOption0Progress"].value as? String, "50%")
- XCTAssert(app.buttons["First, 10 votes"].exists)
- XCTAssertEqual(app.buttons["First, 10 votes"].value as! String, "50%")
-
- XCTAssert(app.buttons["Second, 5 votes"].exists)
- XCTAssertEqual(app.buttons["Second, 5 votes"].value as! String, "25%")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption1Count"].label, "5 votes")
+ XCTAssertEqual(app.progressIndicators["PollAnswerOption1Progress"].value as? String, "25%")
- XCTAssert(app.buttons["Third, 15 votes"].exists)
- XCTAssertEqual(app.buttons["Third, 15 votes"].value as! String, "75%")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption2Count"].label, "15 votes")
+ XCTAssertEqual(app.progressIndicators["PollAnswerOption2Progress"].value as? String, "75%")
- app.buttons["First, 10 votes"].tap()
+ app.buttons["PollAnswerOption0"].tap()
- XCTAssert(app.buttons["First, 10 votes"].exists)
- XCTAssertEqual(app.buttons["First, 10 votes"].value as! String, "50%")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption0Label"].label, "First")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption0Count"].label, "10 votes")
+ XCTAssertEqual(app.progressIndicators["PollAnswerOption0Progress"].value as? String, "50%")
- XCTAssert(app.buttons["Second, 5 votes"].exists)
- XCTAssertEqual(app.buttons["Second, 5 votes"].value as! String, "25%")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption1Label"].label, "Second")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption1Count"].label, "5 votes")
+ XCTAssertEqual(app.progressIndicators["PollAnswerOption1Progress"].value as? String, "25%")
- XCTAssert(app.buttons["Third, 15 votes"].exists)
- XCTAssertEqual(app.buttons["Third, 15 votes"].value as! String, "75%")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption2Label"].label, "Third")
+ XCTAssertEqual(app.staticTexts["PollAnswerOption2Count"].label, "15 votes")
+ XCTAssertEqual(app.progressIndicators["PollAnswerOption2Progress"].value as? String, "75%")
}
}
diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift
index aaaba7c37a..2ffa68be97 100644
--- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift
+++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift
@@ -41,6 +41,7 @@ struct TimelinePollAnswerOptionButton: View {
.overlay(rect.stroke(borderAccentColor, lineWidth: 1.0))
.accentColor(progressViewAccentColor)
}
+ .accessibilityIdentifier("PollAnswerOption\(optionIndex)")
}
var answerOptionLabel: some View {
@@ -53,6 +54,7 @@ struct TimelinePollAnswerOptionButton: View {
Text(answerOption.text)
.font(theme.fonts.body)
.foregroundColor(theme.colors.primaryContent)
+ .accessibilityIdentifier("PollAnswerOption\(optionIndex)Label")
if poll.closed, answerOption.winner {
Spacer()
@@ -66,11 +68,13 @@ struct TimelinePollAnswerOptionButton: View {
total: Double(poll.totalAnswerCount))
.progressViewStyle(LinearProgressViewStyle())
.scaleEffect(x: 1.0, y: 1.2, anchor: .center)
+ .accessibilityIdentifier("PollAnswerOption\(optionIndex)Progress")
if poll.shouldDiscloseResults {
Text(answerOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(answerOption.count)))
.font(theme.fonts.footnote)
.foregroundColor(poll.closed && answerOption.winner ? theme.colors.accent : theme.colors.secondaryContent)
+ .accessibilityIdentifier("PollAnswerOption\(optionIndex)Count")
}
}
}
@@ -92,6 +96,10 @@ struct TimelinePollAnswerOptionButton: View {
return answerOption.selected ? theme.colors.accent : theme.colors.quarterlyContent
}
+
+ var optionIndex: Int {
+ poll.answerOptions.firstIndex { $0.id == answerOption.id } ?? Int.max
+ }
}
struct TimelinePollAnswerOptionButton_Previews: PreviewProvider {
diff --git a/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/Coordinator/TimelineVoiceBroadcastCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/Coordinator/TimelineVoiceBroadcastCoordinator.swift
deleted file mode 100644
index 65c6188609..0000000000
--- a/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/Coordinator/TimelineVoiceBroadcastCoordinator.swift
+++ /dev/null
@@ -1,107 +0,0 @@
-//
-// Copyright 2021 New Vector Ltd
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-import Combine
-import MatrixSDK
-import SwiftUI
-
-struct TimelineVoiceBroadcastCoordinatorParameters {
- let session: MXSession
- let room: MXRoom
- let voiceBroadcastStartEvent: MXEvent
-}
-
-final class TimelineVoiceBroadcastCoordinator: Coordinator, Presentable, VoiceBroadcastAggregatorDelegate {
- // MARK: - Properties
-
- // MARK: Private
-
- private let parameters: TimelineVoiceBroadcastCoordinatorParameters
- private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>()
-
- private var voiceBroadcastAggregator: VoiceBroadcastAggregator
- private var viewModel: TimelineVoiceBroadcastViewModelProtocol!
- private var cancellables = Set()
-
- // MARK: Public
-
- // Must be used only internally
- var childCoordinators: [Coordinator] = []
-
- // MARK: - Setup
-
- init(parameters: TimelineVoiceBroadcastCoordinatorParameters) throws {
- self.parameters = parameters
-
- try voiceBroadcastAggregator = VoiceBroadcastAggregator(session: parameters.session, room: parameters.room, voiceBroadcastStartEventId: parameters.voiceBroadcastStartEvent.eventId)
- voiceBroadcastAggregator.delegate = self
-
- viewModel = TimelineVoiceBroadcastViewModel(timelineVoiceBroadcastDetails: buildTimelineVoiceBroadcastFrom(voiceBroadcastAggregator.voiceBroadcast))
-
- // TODO: manage voicebroacast chunks
- viewModel.completion = { }
-
- }
-
- // MARK: - Public
-
- func start() { }
-
- func toPresentable() -> UIViewController {
- VectorHostingController(rootView: TimelineVoiceBroadcastView(viewModel: viewModel.context),
- forceZeroSafeAreaInsets: true)
- }
-
- func canEndVoiceBroadcast() -> Bool {
- // TODO: check is voicebroadcast stopped
- return false
- }
-
- func canEditVoiceBroadcast() -> Bool {
- return false
- }
-
- func endVoiceBroadcast() {}
-
- // MARK: - VoiceBroadcastAggregatorDelegate
-
- func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) {
- viewModel.updateWithVoiceBroadcastDetails(buildTimelineVoiceBroadcastFrom(aggregator.voiceBroadcast))
- }
-
- func voiceBroadcastAggregatorDidStartLoading(_ aggregator: VoiceBroadcastAggregator) { }
-
- func voiceBroadcastAggregatorDidEndLoading(_ aggregator: VoiceBroadcastAggregator) { }
-
- func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didFailWithError: Error) { }
-
- // MARK: - Private
-
- // VoiceBroadcastProtocol is intentionally not available in the SwiftUI target as we don't want
- // to add the SDK as a dependency to it. We need to translate from one to the other on this level.
- func buildTimelineVoiceBroadcastFrom(_ voiceBroadcast: VoiceBroadcastProtocol) -> TimelineVoiceBroadcastDetails {
-
- return TimelineVoiceBroadcastDetails(closed: voiceBroadcast.isClosed,
- type: voiceBroadcastKindToTimelineVoiceBroadcastType(voiceBroadcast.kind))
- }
-
- private func voiceBroadcastKindToTimelineVoiceBroadcastType(_ kind: VoiceBroadcastKind) -> TimelineVoiceBroadcastType {
- let mapping = [VoiceBroadcastKind.disclosed: TimelineVoiceBroadcastType.disclosed,
- VoiceBroadcastKind.undisclosed: TimelineVoiceBroadcastType.undisclosed]
-
- return mapping[kind] ?? .disclosed
- }
-}
diff --git a/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/Coordinator/TimelineVoiceBroadcastProvider.swift b/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/Coordinator/TimelineVoiceBroadcastProvider.swift
deleted file mode 100644
index 327da466df..0000000000
--- a/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/Coordinator/TimelineVoiceBroadcastProvider.swift
+++ /dev/null
@@ -1,52 +0,0 @@
-//
-// Copyright 2022 New Vector Ltd
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-import Foundation
-
-class TimelineVoiceBroadcastProvider {
- static let shared = TimelineVoiceBroadcastProvider()
-
- var session: MXSession?
- var coordinatorsForEventIdentifiers = [String: TimelineVoiceBroadcastCoordinator]()
-
- private init() { }
-
- /// Create or retrieve the voiceBroadcast timeline coordinator for this event and return
- /// a view to be displayed in the timeline
- func buildTimelineVoiceBroadcastViewForEvent(_ event: MXEvent) -> UIView? {
- guard let session = session, let room = session.room(withRoomId: event.roomId) else {
- return nil
- }
-
- if let coordinator = coordinatorsForEventIdentifiers[event.eventId] {
- return coordinator.toPresentable().view
- }
-
- let parameters = TimelineVoiceBroadcastCoordinatorParameters(session: session, room: room, voiceBroadcastStartEvent: event)
- guard let coordinator = try? TimelineVoiceBroadcastCoordinator(parameters: parameters) else {
- return nil
- }
-
- coordinatorsForEventIdentifiers[event.eventId] = coordinator
-
- return coordinator.toPresentable().view
- }
-
- /// Retrieve the voiceBroadcast timeline coordinator for the given event or nil if it hasn't been created yet
- func timelineVoiceBroadcastCoordinatorForEventIdentifier(_ eventIdentifier: String) -> TimelineVoiceBroadcastCoordinator? {
- coordinatorsForEventIdentifiers[eventIdentifier]
- }
-}
diff --git a/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/TimelineVoiceBroadcastModels.swift b/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/TimelineVoiceBroadcastModels.swift
deleted file mode 100644
index f11cca32b0..0000000000
--- a/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/TimelineVoiceBroadcastModels.swift
+++ /dev/null
@@ -1,53 +0,0 @@
-//
-// Copyright 2022 New Vector Ltd
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-import Foundation
-import SwiftUI
-
-typealias TimelineVoiceBroadcastViewModelCallback = () -> Void
-
-// TODO: add play pause cases
-enum TimelineVoiceBroadcastViewAction { }
-
-enum TimelineVoiceBroadcastType {
- case disclosed
- case undisclosed
-}
-
-struct TimelineVoiceBroadcastDetails {
- var closed: Bool
- var type: TimelineVoiceBroadcastType
-
- init(closed: Bool,
- type: TimelineVoiceBroadcastType) {
- self.closed = closed
- self.type = type
- }
-}
-
-struct TimelineVoiceBroadcastViewState: BindableState {
- var voiceBroadcast: TimelineVoiceBroadcastDetails
- var bindings: TimelineVoiceBroadcastViewStateBindings
-}
-
-struct TimelineVoiceBroadcastViewStateBindings {
- var alertInfo: AlertInfo?
-}
-
-enum TimelineVoiceBroadcastAlertType {
- case failedClosingVoiceBroadcast
-}
-
diff --git a/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/TimelineVoiceBroadcastViewModel.swift b/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/TimelineVoiceBroadcastViewModel.swift
deleted file mode 100644
index dd546cfcc7..0000000000
--- a/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/TimelineVoiceBroadcastViewModel.swift
+++ /dev/null
@@ -1,48 +0,0 @@
-//
-// Copyright 2022 New Vector Ltd
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-import Combine
-import SwiftUI
-
-typealias TimelineVoiceBroadcastViewModelType = StateStoreViewModel
-
-class TimelineVoiceBroadcastViewModel: TimelineVoiceBroadcastViewModelType, TimelineVoiceBroadcastViewModelProtocol {
- // MARK: - Properties
-
- // MARK: Private
-
- // MARK: Public
-
- var completion: TimelineVoiceBroadcastViewModelCallback?
-
- // MARK: - Setup
-
- init(timelineVoiceBroadcastDetails: TimelineVoiceBroadcastDetails) {
- super.init(initialViewState: TimelineVoiceBroadcastViewState(voiceBroadcast: timelineVoiceBroadcastDetails, bindings: TimelineVoiceBroadcastViewStateBindings()))
- }
-
- // MARK: - Public
-
- override func process(viewAction: TimelineVoiceBroadcastViewAction) {
- // TODO: add some actions as play pause
- }
-
- // MARK: - TimelineVoiceBroadcastViewModelProtocol
-
- func updateWithVoiceBroadcastDetails(_ voiceBroadcastDetails: TimelineVoiceBroadcastDetails) {
- state.voiceBroadcast = voiceBroadcastDetails
- }
-}
diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift
index 23b204083f..f44744a9c1 100644
--- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift
+++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift
@@ -21,10 +21,7 @@ class UserSuggestionUITests: MockScreenTestCase {
func testUserSuggestionScreen() throws {
app.goToScreenWithIdentifier(MockUserSuggestionScreenState.multipleResults.title)
- XCTAssert(app.tables.firstMatch.waitForExistence(timeout: 1))
-
- let firstButton = app.tables.firstMatch.buttons.firstMatch
- _ = firstButton.waitForExistence(timeout: 10)
- XCTAssert(firstButton.identifier == "displayNameText-userIdText")
+ let firstButton = app.buttons["displayNameText-userIdText"].firstMatch
+ XCTAssert(firstButton.waitForExistence(timeout: 10))
}
}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift
new file mode 100644
index 0000000000..4184f0d63e
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift
@@ -0,0 +1,77 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Combine
+import MatrixSDK
+import SwiftUI
+
+struct VoiceBroadcastPlaybackCoordinatorParameters {
+ let session: MXSession
+ let room: MXRoom
+ let voiceBroadcastStartEvent: MXEvent
+ let voiceBroadcastState: VoiceBroadcastInfo.State
+ let senderDisplayName: String?
+}
+
+final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable {
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let parameters: VoiceBroadcastPlaybackCoordinatorParameters
+
+ private var viewModel: VoiceBroadcastPlaybackViewModelProtocol!
+ private var cancellables = Set()
+
+ // MARK: Public
+
+ // Must be used only internally
+ var childCoordinators: [Coordinator] = []
+
+ // MARK: - Setup
+
+ init(parameters: VoiceBroadcastPlaybackCoordinatorParameters) throws {
+ self.parameters = parameters
+
+ let voiceBroadcastAggregator = try VoiceBroadcastAggregator(session: parameters.session, room: parameters.room, voiceBroadcastStartEventId: parameters.voiceBroadcastStartEvent.eventId, voiceBroadcastState: parameters.voiceBroadcastState)
+
+ let details = VoiceBroadcastPlaybackDetails(senderDisplayName: parameters.senderDisplayName)
+ viewModel = VoiceBroadcastPlaybackViewModel(details: details,
+ mediaServiceProvider: VoiceMessageMediaServiceProvider.sharedProvider,
+ cacheManager: VoiceMessageAttachmentCacheManager.sharedManager,
+ voiceBroadcastAggregator: voiceBroadcastAggregator)
+
+ }
+
+ // MARK: - Public
+
+ func start() { }
+
+ func toPresentable() -> UIViewController {
+ VectorHostingController(rootView: VoiceBroadcastPlaybackView(viewModel: viewModel.context))
+ }
+
+ func canEndVoiceBroadcast() -> Bool {
+ // TODO: VB check is voicebroadcast stopped
+ return false
+ }
+
+ func canEditVoiceBroadcast() -> Bool {
+ return false
+ }
+
+ func endVoiceBroadcast() {}
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift
new file mode 100644
index 0000000000..5167a23647
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift
@@ -0,0 +1,73 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+class VoiceBroadcastPlaybackProvider {
+ static let shared = VoiceBroadcastPlaybackProvider()
+
+ var session: MXSession?
+ var coordinatorsForEventIdentifiers = [String: VoiceBroadcastPlaybackCoordinator]()
+
+ private init() { }
+
+ /// Create or retrieve the voiceBroadcast timeline coordinator for this event and return
+ /// a view to be displayed in the timeline
+ func buildVoiceBroadcastPlaybackVCForEvent(_ event: MXEvent, senderDisplayName: String?) -> UIViewController? {
+ guard let session = session, let room = session.room(withRoomId: event.roomId) else {
+ return nil
+ }
+
+ if let coordinator = coordinatorsForEventIdentifiers[event.eventId] {
+ return coordinator.toPresentable()
+ }
+
+ let dispatchGroup = DispatchGroup()
+ dispatchGroup.enter()
+ var voiceBroadcastState = VoiceBroadcastInfo.State.stopped
+
+ room.state { roomState in
+ if let stateEvent = roomState?.stateEvents(with: .custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType))?.last,
+ stateEvent.stateKey == event.stateKey,
+ let voiceBroadcastInfo = VoiceBroadcastInfo(fromJSON: stateEvent.content),
+ (stateEvent.eventId == event.eventId || voiceBroadcastInfo.eventId == event.eventId),
+ let state = VoiceBroadcastInfo.State(rawValue: voiceBroadcastInfo.state) {
+ voiceBroadcastState = state
+ }
+
+ dispatchGroup.leave()
+ }
+
+ let parameters = VoiceBroadcastPlaybackCoordinatorParameters(session: session,
+ room: room,
+ voiceBroadcastStartEvent: event,
+ voiceBroadcastState: voiceBroadcastState,
+ senderDisplayName: senderDisplayName)
+ guard let coordinator = try? VoiceBroadcastPlaybackCoordinator(parameters: parameters) else {
+ return nil
+ }
+
+ coordinatorsForEventIdentifiers[event.eventId] = coordinator
+
+ return coordinator.toPresentable()
+
+ }
+
+ /// Retrieve the voiceBroadcast timeline coordinator for the given event or nil if it hasn't been created yet
+ func voiceBroadcastPlaybackCoordinatorForEventIdentifier(_ eventIdentifier: String) -> VoiceBroadcastPlaybackCoordinator? {
+ coordinatorsForEventIdentifiers[eventIdentifier]
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift
new file mode 100644
index 0000000000..c27da240e6
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift
@@ -0,0 +1,334 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Combine
+import SwiftUI
+
+// TODO: VoiceBroadcastPlaybackViewModel must be revisited in order to not depend on MatrixSDK
+// We need a VoiceBroadcastPlaybackServiceProtocol and VoiceBroadcastAggregatorProtocol
+import MatrixSDK
+
+class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, VoiceBroadcastPlaybackViewModelProtocol {
+
+ // MARK: - Properties
+
+ // MARK: Private
+ private var voiceBroadcastAggregator: VoiceBroadcastAggregator
+ private let mediaServiceProvider: VoiceMessageMediaServiceProvider
+ private let cacheManager: VoiceMessageAttachmentCacheManager
+ private var audioPlayer: VoiceMessageAudioPlayer?
+
+ private var voiceBroadcastChunkQueue: [VoiceBroadcastChunk] = []
+
+ private var isLivePlayback = false
+
+ // MARK: Public
+
+ // MARK: - Setup
+
+ init(details: VoiceBroadcastPlaybackDetails,
+ mediaServiceProvider: VoiceMessageMediaServiceProvider,
+ cacheManager: VoiceMessageAttachmentCacheManager,
+ voiceBroadcastAggregator: VoiceBroadcastAggregator) {
+ self.mediaServiceProvider = mediaServiceProvider
+ self.cacheManager = cacheManager
+ self.voiceBroadcastAggregator = voiceBroadcastAggregator
+
+ let viewState = VoiceBroadcastPlaybackViewState(details: details,
+ broadcastState: VoiceBroadcastPlaybackViewModel.getBroadcastState(from: voiceBroadcastAggregator.voiceBroadcastState),
+ playbackState: .stopped,
+ bindings: VoiceBroadcastPlaybackViewStateBindings())
+ super.init(initialViewState: viewState)
+
+ self.voiceBroadcastAggregator.delegate = self
+ }
+
+ private func release() {
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] release")
+ if let audioPlayer = audioPlayer {
+ audioPlayer.deregisterDelegate(self)
+ self.audioPlayer = nil
+ }
+ }
+
+ // MARK: - Public
+
+ override func process(viewAction: VoiceBroadcastPlaybackViewAction) {
+ switch viewAction {
+ case .play:
+ play()
+ case .playLive:
+ playLive()
+ case .pause:
+ pause()
+ }
+ }
+
+
+ // MARK: - Private
+
+ /// Listen voice broadcast
+ private func play() {
+ isLivePlayback = false
+
+ if voiceBroadcastAggregator.isStarted == false {
+ // Start the streaming by fetching broadcast chunks
+ // The audio player will automatically start the playback on incoming chunks
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: Start streaming")
+ state.playbackState = .buffering
+ voiceBroadcastAggregator.start()
+ }
+ else if let audioPlayer = audioPlayer {
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: resume")
+ audioPlayer.play()
+ }
+ else {
+ let chunks = voiceBroadcastAggregator.voiceBroadcast.chunks
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: restart from the beginning: \(chunks.count) chunks")
+
+ // Reinject all the chuncks we already have and play them
+ voiceBroadcastChunkQueue.append(contentsOf: chunks)
+ processPendingVoiceBroadcastChunks()
+ }
+ }
+
+ private func playLive() {
+ guard isLivePlayback == false else {
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] playLive: Already playing live")
+ return
+ }
+
+ isLivePlayback = true
+
+ // Flush the current audio player playlist
+ audioPlayer?.removeAllPlayerItems()
+
+ if voiceBroadcastAggregator.isStarted == false {
+ // Start the streaming by fetching broadcast chunks
+ // The audio player will automatically start the playback on incoming chunks
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] playLive: Start streaming")
+ state.playbackState = .buffering
+ voiceBroadcastAggregator.start()
+ }
+ else {
+ let chunks = voiceBroadcastAggregator.voiceBroadcast.chunks
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] playLive: restart from the last chunk: \(chunks.count) chunks")
+
+ // Reinject all the chuncks we already have and play the last one
+ voiceBroadcastChunkQueue.append(contentsOf: chunks)
+ processPendingVoiceBroadcastChunksForLivePlayback()
+ }
+ }
+
+ /// Stop voice broadcast
+ private func pause() {
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] pause")
+
+ isLivePlayback = false
+
+ if let audioPlayer = audioPlayer, audioPlayer.isPlaying {
+ audioPlayer.pause()
+ }
+ }
+
+ private func stopIfVoiceBroadcastOver() {
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] stopIfVoiceBroadcastOver")
+
+ // TODO: Check if the broadcast is over before stopping everything
+ // If not, the player should not stopped. The view state must be move to buffering
+ stop()
+ }
+
+ private func stop() {
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] stop")
+
+ isLivePlayback = false
+
+ // Objects will be released on audioPlayerDidStopPlaying
+ audioPlayer?.stop()
+ }
+
+
+ // MARK: - Voice broadcast chunks playback
+
+ /// Start the playback from the beginning or push more chunks to it
+ private func processPendingVoiceBroadcastChunks() {
+ reorderPendingVoiceBroadcastChunks()
+ processNextVoiceBroadcastChunk()
+ }
+
+ /// Start the playback from the last known chunk
+ private func processPendingVoiceBroadcastChunksForLivePlayback() {
+ let chunks = reorderVoiceBroadcastChunks(chunks: Array(voiceBroadcastAggregator.voiceBroadcast.chunks))
+ if let lastChunk = chunks.last {
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] processPendingVoiceBroadcastChunksForLivePlayback. Use the last chunk: sequence: \(lastChunk.sequence) out of the \(voiceBroadcastChunkQueue.count) chunks")
+ voiceBroadcastChunkQueue = [lastChunk]
+ }
+ processNextVoiceBroadcastChunk()
+ }
+
+ private func reorderPendingVoiceBroadcastChunks() {
+ // Make sure we download and process chunks in the right order
+ voiceBroadcastChunkQueue = reorderVoiceBroadcastChunks(chunks: voiceBroadcastChunkQueue)
+ }
+ private func reorderVoiceBroadcastChunks(chunks: [VoiceBroadcastChunk]) -> [VoiceBroadcastChunk] {
+ chunks.sorted(by: {$0.sequence < $1.sequence})
+ }
+
+ private func processNextVoiceBroadcastChunk() {
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] processNextVoiceBroadcastChunk: \(voiceBroadcastChunkQueue.count) chunks remaining")
+
+ guard voiceBroadcastChunkQueue.count > 0 else {
+ // We cached all chunks. Nothing more to do
+ return
+ }
+
+ // TODO: Control the download rate to avoid to download all chunk in mass
+ // We could synchronise it with the number of chunks in the player playlist (audioPlayer.playerItems)
+
+ let chunk = voiceBroadcastChunkQueue.removeFirst()
+
+ // numberOfSamples is for the equalizer view we do not support yet
+ cacheManager.loadAttachment(chunk.attachment, numberOfSamples: 1) { [weak self] result in
+ guard let self = self else {
+ return
+ }
+
+ // TODO: Make sure there has no new incoming chunk that should be before this attachment
+ // Be careful that this new chunk is not older than the chunk being played by the audio player. Else
+ // we will get an unexecpted rewind.
+
+ switch result {
+ case .success(let result):
+ guard result.eventIdentifier == chunk.attachment.eventId else {
+ return
+ }
+
+ if let audioPlayer = self.audioPlayer {
+ // Append the chunk to the current playlist
+ audioPlayer.addContentFromURL(result.url)
+
+ // Resume the player. Needed after a pause
+ if audioPlayer.isPlaying == false {
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] processNextVoiceBroadcastChunk: Resume the player")
+ audioPlayer.play()
+ }
+ }
+ else {
+ // Init and start the player on the first chunk
+ let audioPlayer = self.mediaServiceProvider.audioPlayerForIdentifier(result.eventIdentifier)
+ audioPlayer.registerDelegate(self)
+
+ audioPlayer.loadContentFromURL(result.url, displayName: chunk.attachment.originalFileName)
+ audioPlayer.play()
+ self.audioPlayer = audioPlayer
+ }
+
+ case .failure (let error):
+ MXLog.error("[VoiceBroadcastPlaybackViewModel] processVoiceBroadcastChunkQueue: loadAttachment error", context: error)
+ if self.voiceBroadcastChunkQueue.count == 0 {
+ // No more chunk to try. Go to error
+ self.state.playbackState = .error
+ }
+ }
+
+ self.processNextVoiceBroadcastChunk()
+ }
+ }
+
+ private static func getBroadcastState(from state: VoiceBroadcastInfo.State) -> VoiceBroadcastState {
+ var broadcastState: VoiceBroadcastState
+ switch state {
+ case .started:
+ broadcastState = VoiceBroadcastState.live
+ case .paused:
+ broadcastState = VoiceBroadcastState.paused
+ case .resumed:
+ broadcastState = VoiceBroadcastState.live
+ case .stopped:
+ broadcastState = VoiceBroadcastState.stopped
+ }
+
+ return broadcastState
+ }
+}
+
+// MARK: VoiceBroadcastAggregatorDelegate
+extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate {
+ func voiceBroadcastAggregatorDidStartLoading(_ aggregator: VoiceBroadcastAggregator) {
+ }
+
+ func voiceBroadcastAggregatorDidEndLoading(_ aggregator: VoiceBroadcastAggregator) {
+ }
+
+ func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didFailWithError: Error) {
+ MXLog.error("[VoiceBroadcastPlaybackViewModel] voiceBroadcastAggregator didFailWithError:", context: didFailWithError)
+ }
+
+ func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveChunk: VoiceBroadcastChunk) {
+ voiceBroadcastChunkQueue.append(didReceiveChunk)
+ }
+
+ func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfo.State) {
+ state.broadcastState = VoiceBroadcastPlaybackViewModel.getBroadcastState(from: didReceiveState)
+ }
+
+ func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) {
+ if isLivePlayback && state.playbackState == .buffering {
+ // We started directly with a live playback but there was no known chuncks at that time
+ // These are the first chunks we get. Start the playback on the latest one
+ processPendingVoiceBroadcastChunksForLivePlayback()
+ }
+ else {
+ processPendingVoiceBroadcastChunks()
+ }
+ }
+}
+
+
+// MARK: - VoiceMessageAudioPlayerDelegate
+extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate {
+ func audioPlayerDidFinishLoading(_ audioPlayer: VoiceMessageAudioPlayer) {
+ }
+
+ func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
+ if isLivePlayback {
+ state.playbackState = .playingLive
+ }
+ else {
+ state.playbackState = .playing
+ }
+ }
+
+ func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
+ state.playbackState = .paused
+ }
+
+ func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] audioPlayerDidStopPlaying")
+ state.playbackState = .stopped
+ release()
+ }
+
+ func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError error: Error) {
+ state.playbackState = .error
+ }
+
+ func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] audioPlayerDidFinishPlaying: \(audioPlayer.playerItems.count)")
+ stopIfVoiceBroadcastOver()
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/View/TimelineVoiceBroadcastView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackErrorView.swift
similarity index 50%
rename from RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/View/TimelineVoiceBroadcastView.swift
rename to RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackErrorView.swift
index 5235677dcc..0ac7822c64 100644
--- a/RiotSwiftUI/Modules/Room/TimelineVoiceBroadcast/View/TimelineVoiceBroadcastView.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackErrorView.swift
@@ -16,7 +16,7 @@
import SwiftUI
-struct TimelineVoiceBroadcastView: View {
+struct VoiceBroadcastPlaybackErrorView: View {
// MARK: - Properties
// MARK: Private
@@ -25,27 +25,27 @@ struct TimelineVoiceBroadcastView: View {
// MARK: Public
- @ObservedObject var viewModel: TimelineVoiceBroadcastViewModel.Context
+ var action: (() -> Void)?
var body: some View {
- let voiceBroadcast = viewModel.viewState.voiceBroadcast
-
- VStack(alignment: .leading, spacing: 16.0) {
- Text(VectorL10n.voiceBroadcastInTimelineTitle)
- .font(theme.fonts.bodySB)
- .foregroundColor(theme.colors.primaryContent)
- Text(VectorL10n.voiceBroadcastInTimelineBody)
- .font(theme.fonts.body)
- .foregroundColor(theme.colors.primaryContent)
- }
- .padding([.horizontal, .top], 2.0)
- .padding([.bottom])
- .alert(item: $viewModel.alertInfo) { info in
- info.alert
+ VStack {
+ VStack {
+ Image(uiImage: Asset.Images.errorIcon.image)
+ .frame(width: 40, height: 40)
+ Text(VectorL10n.voiceBroadcastPlaybackLoadingError)
+ .multilineTextAlignment(.center)
+ .font(theme.fonts.caption1)
+ .foregroundColor(theme.colors.primaryContent)
+ }
+ .padding()
}
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(theme.colors.system.ignoresSafeArea())
}
}
-// MARK: - Previews
-
-// TODO: Add Voice broadcast preview
+struct VoiceBroadcastPlaybackErrorView_Previews: PreviewProvider {
+ static var previews: some View {
+ VoiceBroadcastPlaybackErrorView()
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift
new file mode 100644
index 0000000000..04ade8a77d
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift
@@ -0,0 +1,121 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import SwiftUI
+
+// TODO: To remove
+// VoiceBroadcastPlaybackViewModel must be revisited in order to not depend on MatrixSDK
+#if canImport(MatrixSDK)
+typealias VoiceBroadcastPlaybackViewModelImpl = VoiceBroadcastPlaybackViewModel
+#else
+typealias VoiceBroadcastPlaybackViewModelImpl = MockVoiceBroadcastPlaybackViewModel
+#endif
+
+struct VoiceBroadcastPlaybackView: View {
+ // MARK: - Properties
+
+ // MARK: Private
+
+ @Environment(\.theme) private var theme: ThemeSwiftUI
+
+ private var backgroundColor: Color {
+ if viewModel.viewState.playbackState == .playingLive {
+ return theme.colors.alert
+ }
+ return theme.colors.quarterlyContent
+ }
+
+ // MARK: Public
+
+ @ObservedObject var viewModel: VoiceBroadcastPlaybackViewModelImpl.Context
+
+ var body: some View {
+ let details = viewModel.viewState.details
+
+ VStack(alignment: .center, spacing: 16.0) {
+
+ HStack {
+ Text(details.senderDisplayName ?? "")
+ //Text(VectorL10n.voiceBroadcastInTimelineTitle)
+ .font(theme.fonts.bodySB)
+ .foregroundColor(theme.colors.primaryContent)
+
+ if viewModel.viewState.broadcastState == .live {
+ Button { viewModel.send(viewAction: .playLive) } label:
+ {
+ HStack {
+ Image(uiImage: Asset.Images.voiceBroadcastLive.image)
+ .renderingMode(.original)
+ Text("Live")
+ .font(theme.fonts.bodySB)
+ .foregroundColor(Color.white)
+ }
+
+ }
+ .padding(5.0)
+ .background(RoundedRectangle(cornerRadius: 4, style: .continuous)
+ .fill(backgroundColor))
+ .accessibilityIdentifier("liveButton")
+ }
+ }
+
+ if viewModel.viewState.playbackState == .error {
+ VoiceBroadcastPlaybackErrorView()
+ } else {
+ ZStack {
+ if viewModel.viewState.playbackState == .playing ||
+ viewModel.viewState.playbackState == .playingLive {
+ Button { viewModel.send(viewAction: .pause) } label: {
+ Image(uiImage: Asset.Images.voiceBroadcastPause.image)
+ .renderingMode(.original)
+ }
+ .accessibilityIdentifier("pauseButton")
+ } else {
+ Button {
+ if viewModel.viewState.broadcastState == .live &&
+ viewModel.viewState.playbackState == .stopped {
+ viewModel.send(viewAction: .playLive)
+ } else {
+ viewModel.send(viewAction: .play)
+ }
+ } label: {
+ Image(uiImage: Asset.Images.voiceBroadcastPlay.image)
+ .renderingMode(.original)
+ }
+ .disabled(viewModel.viewState.playbackState == .buffering)
+ .accessibilityIdentifier("playButton")
+ }
+ }
+ .activityIndicator(show: viewModel.viewState.playbackState == .buffering)
+ }
+
+ }
+ .padding([.horizontal, .top], 2.0)
+ .padding([.bottom])
+ .alert(item: $viewModel.alertInfo) { info in
+ info.alert
+ }
+ }
+}
+
+// MARK: - Previews
+
+struct VoiceBroadcastPlaybackView_Previews: PreviewProvider {
+ static let stateRenderer = MockVoiceBroadcastPlaybackScreenState.stateRenderer
+ static var previews: some View {
+ stateRenderer.screenGroup()
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift
new file mode 100644
index 0000000000..09a12b87d1
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift
@@ -0,0 +1,62 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import SwiftUI
+
+enum VoiceBroadcastPlaybackViewAction {
+ case play
+ case playLive
+ case pause
+}
+
+enum VoiceBroadcastPlaybackState {
+ case stopped
+ case buffering
+ case playing
+ case playingLive
+ case paused
+ case error
+}
+
+struct VoiceBroadcastPlaybackDetails {
+ let senderDisplayName: String?
+}
+
+enum VoiceBroadcastState {
+ case unknown
+ case stopped
+ case live
+ case paused
+}
+
+struct VoiceBroadcastPlaybackViewState: BindableState {
+ var details: VoiceBroadcastPlaybackDetails
+ var broadcastState: VoiceBroadcastState
+ var playbackState: VoiceBroadcastPlaybackState
+ var bindings: VoiceBroadcastPlaybackViewStateBindings
+}
+
+struct VoiceBroadcastPlaybackViewStateBindings {
+ // TODO: Neeeded?
+ var alertInfo: AlertInfo?
+}
+
+enum VoiceBroadcastPlaybackAlertType {
+ // TODO: What is it?
+ case failedClosingVoiceBroadcast
+}
+
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift
new file mode 100644
index 0000000000..72a15185f3
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift
@@ -0,0 +1,53 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import SwiftUI
+
+typealias MockVoiceBroadcastPlaybackViewModelType = StateStoreViewModel
+class MockVoiceBroadcastPlaybackViewModel: MockVoiceBroadcastPlaybackViewModelType, VoiceBroadcastPlaybackViewModelProtocol {
+}
+
+/// Using an enum for the screen allows you define the different state cases with
+/// the relevant associated data for each case.
+enum MockVoiceBroadcastPlaybackScreenState: MockScreenState, CaseIterable {
+ // A case for each state you want to represent
+ // with specific, minimal associated data that will allow you
+ // mock that screen.
+ case animated
+
+ /// The associated screen
+ var screenType: Any.Type {
+ VoiceBroadcastPlaybackView.self
+ }
+
+ /// A list of screen state definitions
+ static var allCases: [MockVoiceBroadcastPlaybackScreenState] {
+ [.animated]
+ }
+
+ /// Generate the view struct for the screen state.
+ var screenView: ([Any], AnyView) {
+
+ let details = VoiceBroadcastPlaybackDetails(senderDisplayName: "Alice")
+ let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .live, playbackState: .stopped, bindings: VoiceBroadcastPlaybackViewStateBindings()))
+
+ return (
+ [false, viewModel],
+ AnyView(VoiceBroadcastPlaybackView(viewModel: viewModel.context))
+ )
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackViewModelProtocol.swift
new file mode 100644
index 0000000000..1ad8d64c5d
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackViewModelProtocol.swift
@@ -0,0 +1,23 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+typealias VoiceBroadcastPlaybackViewModelType = StateStoreViewModel
+
+protocol VoiceBroadcastPlaybackViewModelProtocol {
+ var context: VoiceBroadcastPlaybackViewModelType.Context { get }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift
new file mode 100644
index 0000000000..c13524e13c
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift
@@ -0,0 +1,67 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+struct VoiceBroadcastRecorderCoordinatorParameters {
+ let session: MXSession
+ let room: MXRoom
+ let voiceBroadcastStartEvent: MXEvent
+ let senderDisplayName: String?
+}
+
+final class VoiceBroadcastRecorderCoordinator: Coordinator, Presentable {
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let parameters: VoiceBroadcastRecorderCoordinatorParameters
+
+ private var voiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol
+ private var voiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelProtocol
+
+ // MARK: Public
+
+ // Must be used only internally
+ var childCoordinators: [Coordinator] = []
+
+ // MARK: - Setup
+
+ init(parameters: VoiceBroadcastRecorderCoordinatorParameters) {
+ self.parameters = parameters
+
+ voiceBroadcastRecorderService = VoiceBroadcastRecorderService(session: parameters.session, roomId: parameters.room.matrixItemId)
+
+ let details = VoiceBroadcastRecorderDetails(senderDisplayName: parameters.senderDisplayName)
+ let viewModel = VoiceBroadcastRecorderViewModel(details: details,
+ recorderService: voiceBroadcastRecorderService)
+ voiceBroadcastRecorderViewModel = viewModel
+ }
+
+ // MARK: - Public
+
+ func start() { }
+
+ func toPresentable() -> UIViewController {
+ VectorHostingController(rootView: VoiceBroadcastRecorderView(viewModel: voiceBroadcastRecorderViewModel.context))
+ }
+
+ func pauseRecording() {
+ voiceBroadcastRecorderViewModel.context.send(viewAction: .pause)
+ }
+
+ // MARK: - Private
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift
new file mode 100644
index 0000000000..c7bc2b1a06
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift
@@ -0,0 +1,77 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+@objc public class VoiceBroadcastRecorderProvider: NSObject {
+
+ // MARK: - Constants
+ @objc public static let shared = VoiceBroadcastRecorderProvider()
+
+ // MARK: - Properties
+ // MARK: Public
+ var session: MXSession?
+ var coordinatorsForEventIdentifiers = [String: VoiceBroadcastRecorderCoordinator]()
+
+ // MARK: Private
+ private var currentEventIdentifier: String?
+
+ // MARK: - Setup
+ private override init() { }
+
+ // MARK: - Public
+
+ /// Create or retrieve the voiceBroadcast timeline coordinator for this event and return
+ /// a view to be displayed in the timeline
+ func buildVoiceBroadcastRecorderViewForEvent(_ event: MXEvent, senderDisplayName: String?) -> UIView? {
+ guard let session = session,
+ let room = session.room(withRoomId: event.roomId) else {
+ return nil
+ }
+
+ self.currentEventIdentifier = event.eventId
+
+ if let coordinator = coordinatorsForEventIdentifiers[event.eventId] {
+ return coordinator.toPresentable().view
+ }
+
+ let parameters = VoiceBroadcastRecorderCoordinatorParameters(session: session,
+ room: room,
+ voiceBroadcastStartEvent: event,
+ senderDisplayName: senderDisplayName)
+ let coordinator = VoiceBroadcastRecorderCoordinator(parameters: parameters)
+
+ coordinatorsForEventIdentifiers[event.eventId] = coordinator
+
+ return coordinator.toPresentable().view
+ }
+
+ /// Pause current voice broadcast recording.
+ @objc public func pauseRecording() {
+ voiceBroadcastRecorderCoordinatorForCurrentEvent()?.pauseRecording()
+ }
+
+ // MARK: - Private
+
+ /// Retrieve the voiceBroadcast recorder coordinator for the current event or nil if it hasn't been created yet
+ private func voiceBroadcastRecorderCoordinatorForCurrentEvent() -> VoiceBroadcastRecorderCoordinator? {
+ guard let currentEventIdentifier = currentEventIdentifier else {
+ return nil
+ }
+
+ return coordinatorsForEventIdentifiers[currentEventIdentifier]
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift
new file mode 100644
index 0000000000..d75f698306
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift
@@ -0,0 +1,274 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private let roomId: String
+ private let session: MXSession
+ private var voiceBroadcastService: VoiceBroadcastService? {
+ session.voiceBroadcastService
+ }
+
+ private let audioEngine = AVAudioEngine()
+ private let audioNodeBus = AVAudioNodeBus(0)
+
+ private var chunkFile: AVAudioFile! = nil
+ private var chunkFrames: AVAudioFrameCount = 0
+ private var chunkFileNumber: Int = 1
+
+ // MARK: Public
+
+ weak var serviceDelegate: VoiceBroadcastRecorderServiceDelegate?
+
+ // MARK: - Setup
+
+ init(session: MXSession, roomId: String) {
+ self.session = session
+ self.roomId = roomId
+ }
+
+ // MARK: - VoiceBroadcastRecorderServiceProtocol
+
+ func startRecordingVoiceBroadcast() {
+ let inputNode = audioEngine.inputNode
+
+ let inputFormat = inputNode.inputFormat(forBus: audioNodeBus)
+ MXLog.debug("[VoiceBroadcastRecorderService] Start recording voice broadcast for bus name : \(String(describing: inputNode.name(forInputBus: audioNodeBus)))")
+
+ inputNode.installTap(onBus: audioNodeBus,
+ bufferSize: 512,
+ format: inputFormat) { (buffer, time) -> Void in
+ DispatchQueue.main.async {
+ self.writeBuffer(buffer)
+ }
+ }
+
+ try? audioEngine.start()
+ }
+
+ func stopRecordingVoiceBroadcast() {
+ MXLog.debug("[VoiceBroadcastRecorderService] Stop recording voice broadcast")
+ audioEngine.stop()
+ audioEngine.inputNode.removeTap(onBus: audioNodeBus)
+
+ resetValues()
+
+ voiceBroadcastService?.stopVoiceBroadcast(success: { [weak self] _ in
+ MXLog.debug("[VoiceBroadcastRecorderService] Stopped")
+
+ guard let self = self else { return }
+
+ // Update state
+ self.serviceDelegate?.voiceBroadcastRecorderService(self, didUpdateState: .stopped)
+
+ // Send current chunk
+ if self.chunkFile != nil {
+ self.sendChunkFile(at: self.chunkFile.url, sequence: self.chunkFileNumber)
+ }
+
+ self.session.tearDownVoiceBroadcastService()
+ }, failure: { error in
+ MXLog.error("[VoiceBroadcastRecorderService] Failed to stop voice broadcast", context: error)
+ })
+ }
+
+ func pauseRecordingVoiceBroadcast() {
+ audioEngine.pause()
+
+ voiceBroadcastService?.pauseVoiceBroadcast(success: { [weak self] _ in
+ guard let self = self else { return }
+
+ // Send current chunk
+ self.sendChunkFile(at: self.chunkFile.url, sequence: self.chunkFileNumber)
+ self.chunkFile = nil
+
+ }, failure: { error in
+ MXLog.error("[VoiceBroadcastRecorderService] Failed to pause voice broadcast", context: error)
+ })
+ }
+
+ func resumeRecordingVoiceBroadcast() {
+ try? audioEngine.start()
+
+ voiceBroadcastService?.resumeVoiceBroadcast(success: { [weak self] _ in
+ guard let self = self else { return }
+
+ // Update state
+ self.serviceDelegate?.voiceBroadcastRecorderService(self, didUpdateState: .started)
+ }, failure: { error in
+ MXLog.error("[VoiceBroadcastRecorderService] Failed to resume voice broadcast", context: error)
+ })
+ }
+
+ // MARK: - Private
+ /// Reset chunk values.
+ private func resetValues() {
+ chunkFrames = 0
+ chunkFileNumber = 1
+ }
+
+ /// Write audio buffer to chunk file.
+ private func writeBuffer(_ buffer: AVAudioPCMBuffer) {
+ let sampleRate = buffer.format.sampleRate
+
+ if chunkFile == nil {
+ createNewChunkFile(channelsCount: buffer.format.channelCount, sampleRate: sampleRate)
+ }
+ try? chunkFile.write(from: buffer)
+
+ chunkFrames += buffer.frameLength
+
+ if chunkFrames > AVAudioFrameCount(Double(BuildSettings.voiceBroadcastChunkLength) * sampleRate) {
+ sendChunkFile(at: chunkFile.url, sequence: self.chunkFileNumber)
+ // Reset chunkFile
+ chunkFile = nil
+ }
+ }
+
+ /// Create new chunk file with sample rate.
+ private func createNewChunkFile(channelsCount: AVAudioChannelCount, sampleRate: Float64) {
+ guard let directory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
+ // FIXME: Manage error
+ return
+ }
+ let temporaryFileName = "VoiceBroadcastChunk-\(roomId)-\(chunkFileNumber)"
+ let fileUrl = directory
+ .appendingPathComponent(temporaryFileName)
+ .appendingPathExtension("aac")
+ MXLog.debug("[VoiceBroadcastRecorderService] Create chunk file to \(fileUrl)")
+
+ let settings: [String: Any] = [AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
+ AVSampleRateKey: sampleRate,
+ AVEncoderBitRateKey: 128000,
+ AVNumberOfChannelsKey: channelsCount,
+ AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue]
+
+ chunkFile = try? AVAudioFile(forWriting: fileUrl, settings: settings)
+
+ if chunkFile != nil {
+ chunkFileNumber += 1
+ chunkFrames = 0
+ } else {
+ stopRecordingVoiceBroadcast()
+ // FIXME: Manage error ?
+ }
+ }
+
+ /// Send chunk file to the server.
+ private func sendChunkFile(at url: URL, sequence: Int) {
+ guard let voiceBroadcastService = voiceBroadcastService else {
+ // FIXME: Manage error
+ return
+ }
+
+ let dispatchGroup = DispatchGroup()
+ var duration = 0.0
+
+ dispatchGroup.enter()
+ VoiceMessageAudioConverter.mediaDurationAt(url) { result in
+ switch result {
+ case .success:
+ if let someDuration = try? result.get() {
+ duration = someDuration
+ } else {
+ MXLog.error("[VoiceBroadcastRecorderService] Failed to retrieve media duration")
+ }
+ case .failure(let error):
+ MXLog.error("[VoiceBroadcastRecorderService] Failed to get audio duration", context: error)
+ }
+
+ dispatchGroup.leave()
+ }
+
+ convertAACToM4A(at: url) { [weak self] convertedUrl in
+ guard let self = self else { return }
+
+ if let convertedUrl = convertedUrl {
+ dispatchGroup.notify(queue: .main) {
+ self.voiceBroadcastService?.sendChunkOfVoiceBroadcast(audioFileLocalURL: convertedUrl,
+ mimeType: "audio/mp4",
+ duration: UInt(duration * 1000),
+ samples: nil,
+ sequence: UInt(sequence)) { eventId in
+ MXLog.debug("[VoiceBroadcastRecorderService] Send voice broadcast chunk with success.")
+ if eventId != nil {
+ self.deleteRecording(at: url)
+ }
+ } failure: { error in
+ MXLog.error("[VoiceBroadcastRecorderService] Failed to send voice broadcast chunk.", context: error)
+ }
+ }
+ }
+ }
+ }
+
+ /// Delete voice broadcast chunk at URL.
+ private func deleteRecording(at url: URL?) {
+ guard let url = url else {
+ return
+ }
+
+ do {
+ try FileManager.default.removeItem(at: url)
+ } catch {
+ MXLog.error("[VoiceBroadcastRecorderService] Delete chunk file error.", context: error)
+ }
+ }
+
+ /// Convert AAC file into m4a one.
+ private func convertAACToM4A(at url: URL, completion: @escaping (URL?) -> Void) {
+ // FIXME: Manage errors at completion
+ let asset = AVURLAsset(url: url)
+ let updatedPath = url.path.replacingOccurrences(of: ".aac", with: ".m4a")
+ let outputUrl = URL(string: "file://" + updatedPath)
+ MXLog.debug("[VoiceBroadcastRecorderService] convertAACToM4A updatedPath : \(updatedPath).")
+
+ if FileManager.default.fileExists(atPath: updatedPath) {
+ try? FileManager.default.removeItem(atPath: updatedPath)
+ }
+
+ guard let exportSession = AVAssetExportSession(asset: asset,
+ presetName: AVAssetExportPresetPassthrough) else {
+ completion(nil)
+ return
+ }
+
+ exportSession.outputURL = outputUrl
+ exportSession.outputFileType = AVFileType.m4a
+ let start = CMTimeMakeWithSeconds(0.0, preferredTimescale: 0)
+ let range = CMTimeRangeMake(start: start, duration: asset.duration)
+ exportSession.timeRange = range
+ exportSession.exportAsynchronously() {
+ switch exportSession.status {
+ case .failed:
+ MXLog.error("[VoiceBroadcastRecorderService] convertAACToM4A error", context: exportSession.error)
+ completion(nil)
+ case .completed:
+ MXLog.debug("[VoiceBroadcastRecorderService] convertAACToM4A success.")
+ completion(outputUrl)
+ default:
+ MXLog.debug("[VoiceBroadcastRecorderService] convertAACToM4A other cases.")
+ completion(nil)
+ }
+ }
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift
new file mode 100644
index 0000000000..7b97eb83a2
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift
@@ -0,0 +1,38 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+protocol VoiceBroadcastRecorderServiceDelegate: AnyObject {
+ func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateState state: VoiceBroadcastRecorderState)
+}
+
+protocol VoiceBroadcastRecorderServiceProtocol {
+ /// Service delegate
+ var serviceDelegate: VoiceBroadcastRecorderServiceDelegate? { get set }
+
+ /// Start voice broadcast recording.
+ func startRecordingVoiceBroadcast()
+
+ /// Stop voice broadcast recording.
+ func stopRecordingVoiceBroadcast()
+
+ /// Pause voice broadcast recording.
+ func pauseRecordingVoiceBroadcast()
+
+ /// Resume voice broadcast recording after paused it.
+ func resumeRecordingVoiceBroadcast()
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift
new file mode 100644
index 0000000000..71fb41cc11
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift
@@ -0,0 +1,83 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import SwiftUI
+
+struct VoiceBroadcastRecorderView: View {
+ // MARK: - Properties
+
+ // MARK: Private
+
+ @Environment(\.theme) private var theme: ThemeSwiftUI
+
+ // MARK: Public
+
+ @ObservedObject var viewModel: VoiceBroadcastRecorderViewModel.Context
+
+ var body: some View {
+ let details = viewModel.viewState.details
+
+ VStack(alignment: .leading, spacing: 16.0) {
+ Text(details.senderDisplayName ?? "")
+ .font(theme.fonts.bodySB)
+ .foregroundColor(theme.colors.primaryContent)
+
+ HStack(alignment: .top, spacing: 16.0) {
+ Button {
+ switch viewModel.viewState.recordingState {
+ case .started, .resumed:
+ viewModel.send(viewAction: .pause)
+ case .stopped:
+ viewModel.send(viewAction: .start)
+ case .paused:
+ viewModel.send(viewAction: .resume)
+ }
+ } label: {
+ if viewModel.viewState.recordingState == .started || viewModel.viewState.recordingState == .resumed {
+ Image("voice_broadcast_record_pause")
+ .renderingMode(.original)
+ } else {
+ Image("voice_broadcast_record")
+ .renderingMode(.original)
+ }
+ }
+ .accessibilityIdentifier("recordButton")
+
+ Button {
+ viewModel.send(viewAction: .stop)
+ } label: {
+ Image("voice_broadcast_stop")
+ .renderingMode(.original)
+ }
+ .accessibilityIdentifier("stopButton")
+ .disabled(viewModel.viewState.recordingState == .stopped)
+ .mask(Color.black.opacity(viewModel.viewState.recordingState == .stopped ? 0.3 : 1.0))
+ }
+ }
+ .padding([.horizontal, .top], 2.0)
+ .padding([.bottom])
+ }
+}
+
+
+// MARK: - Previews
+
+struct VoiceBroadcastRecorderView_Previews: PreviewProvider {
+ static let stateRenderer = MockVoiceBroadcastRecorderScreenState.stateRenderer
+ static var previews: some View {
+ stateRenderer.screenGroup()
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift
new file mode 100644
index 0000000000..b88021bfea
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift
@@ -0,0 +1,44 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+enum VoiceBroadcastRecorderViewAction {
+ case start
+ case stop
+ case pause
+ case resume
+}
+
+enum VoiceBroadcastRecorderState {
+ case started
+ case stopped
+ case paused
+ case resumed
+}
+
+struct VoiceBroadcastRecorderDetails {
+ let senderDisplayName: String?
+}
+
+struct VoiceBroadcastRecorderViewState: BindableState {
+ var details: VoiceBroadcastRecorderDetails
+ var recordingState: VoiceBroadcastRecorderState
+ var bindings: VoiceBroadcastRecorderViewStateBindings
+}
+
+struct VoiceBroadcastRecorderViewStateBindings {
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift
new file mode 100644
index 0000000000..baa9488f42
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift
@@ -0,0 +1,42 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import SwiftUI
+
+typealias MockVoiceBroadcastRecorderViewModelType = StateStoreViewModel
+class MockVoiceBroadcastRecorderViewModel: MockVoiceBroadcastRecorderViewModelType, VoiceBroadcastRecorderViewModelProtocol {
+
+}
+
+/// Using an enum for the screen allows you define the different state cases with
+/// the relevant associated data for each case.
+enum MockVoiceBroadcastRecorderScreenState: MockScreenState, CaseIterable {
+
+ var screenType: Any.Type {
+ VoiceBroadcastRecorderView.self
+ }
+
+ var screenView: ([Any], AnyView) {
+ let details = VoiceBroadcastRecorderDetails(senderDisplayName: "")
+ let viewModel = MockVoiceBroadcastRecorderViewModel(initialViewState: VoiceBroadcastRecorderViewState(details: details, recordingState: .started, bindings: VoiceBroadcastRecorderViewStateBindings()))
+
+ return (
+ [false, viewModel],
+ AnyView(VoiceBroadcastRecorderView(viewModel: viewModel.context))
+ )
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift
new file mode 100644
index 0000000000..6e14441620
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift
@@ -0,0 +1,86 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Combine
+import SwiftUI
+
+typealias VoiceBroadcastRecorderViewModelType = StateStoreViewModel
+
+class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, VoiceBroadcastRecorderViewModelProtocol {
+
+ // MARK: - Properties
+
+ // MARK: Private
+
+ private var voiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol
+
+ // MARK: Public
+
+ // MARK: - Setup
+
+ init(details: VoiceBroadcastRecorderDetails,
+ recorderService: VoiceBroadcastRecorderServiceProtocol) {
+ self.voiceBroadcastRecorderService = recorderService
+ super.init(initialViewState: VoiceBroadcastRecorderViewState(details: details,
+ recordingState: .stopped,
+ bindings: VoiceBroadcastRecorderViewStateBindings()))
+
+ self.voiceBroadcastRecorderService.serviceDelegate = self
+ process(viewAction: .start)
+ }
+
+ // MARK: - Public
+
+ override func process(viewAction: VoiceBroadcastRecorderViewAction) {
+ switch viewAction {
+ case .start:
+ start()
+ case .stop:
+ stop()
+ case .pause:
+ pause()
+ case .resume:
+ resume()
+ }
+ }
+
+ // MARK: - Private
+ private func start() {
+ self.state.recordingState = .started
+ voiceBroadcastRecorderService.startRecordingVoiceBroadcast()
+ }
+
+ private func stop() {
+ self.state.recordingState = .stopped
+ voiceBroadcastRecorderService.stopRecordingVoiceBroadcast()
+ }
+
+ private func pause() {
+ self.state.recordingState = .paused
+ voiceBroadcastRecorderService.pauseRecordingVoiceBroadcast()
+ }
+
+ private func resume() {
+ self.state.recordingState = .resumed
+ voiceBroadcastRecorderService.resumeRecordingVoiceBroadcast()
+ }
+}
+
+extension VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderServiceDelegate {
+ func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateState state: VoiceBroadcastRecorderState) {
+ self.state.recordingState = state
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModelProtocol.swift
new file mode 100644
index 0000000000..ab1e74c89b
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModelProtocol.swift
@@ -0,0 +1,21 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+protocol VoiceBroadcastRecorderViewModelProtocol {
+ var context: VoiceBroadcastRecorderViewModelType.Context { get }
+}
diff --git a/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarView.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarView.swift
index 053d585fdf..0e894961da 100644
--- a/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarView.swift
+++ b/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarView.swift
@@ -22,7 +22,8 @@ struct DeviceAvatarView: View {
@Environment(\.theme) var theme: ThemeSwiftUI
var viewData: DeviceAvatarViewData
-
+ var isSelected: Bool
+
var avatarSize: CGFloat = 40
var badgeSize: CGFloat = 24
@@ -31,10 +32,12 @@ struct DeviceAvatarView: View {
// Device image
VStack(alignment: .center) {
viewData.deviceType.image
+ .renderingMode(isSelected ? .template : .original)
+ .foregroundColor(isSelected ? theme.colors.background : nil)
}
.padding()
.frame(maxWidth: CGFloat(avatarSize), maxHeight: CGFloat(avatarSize))
- .background(theme.colors.system)
+ .background(isSelected ? theme.colors.primaryContent : theme.colors.system)
.clipShape(Circle())
// Verification badge
@@ -62,10 +65,10 @@ struct DeviceAvatarViewListPreview: View {
var body: some View {
HStack {
VStack(alignment: .center, spacing: 20) {
- DeviceAvatarView(viewData: DeviceAvatarViewData(deviceType: .web, verificationState: .verified))
- DeviceAvatarView(viewData: DeviceAvatarViewData(deviceType: .desktop, verificationState: .unverified))
- DeviceAvatarView(viewData: DeviceAvatarViewData(deviceType: .mobile, verificationState: .verified))
- DeviceAvatarView(viewData: DeviceAvatarViewData(deviceType: .unknown, verificationState: .unverified))
+ DeviceAvatarView(viewData: DeviceAvatarViewData(deviceType: .web, verificationState: .verified), isSelected: false)
+ DeviceAvatarView(viewData: DeviceAvatarViewData(deviceType: .desktop, verificationState: .unverified), isSelected: false)
+ DeviceAvatarView(viewData: DeviceAvatarViewData(deviceType: .mobile, verificationState: .verified), isSelected: false)
+ DeviceAvatarView(viewData: DeviceAvatarViewData(deviceType: .unknown, verificationState: .unverified), isSelected: false)
}
}
}
diff --git a/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardView.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardView.swift
index 172e0d834f..864c727d99 100644
--- a/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardView.swift
+++ b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardView.swift
@@ -36,7 +36,7 @@ struct UserSessionCardView: View {
var body: some View {
VStack(alignment: .center, spacing: 12) {
- DeviceAvatarView(viewData: viewData.deviceAvatarViewData)
+ DeviceAvatarView(viewData: viewData.deviceAvatarViewData, isSelected: false)
.accessibilityHidden(true)
Text(viewData.sessionName)
diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift
index 2fb7d89107..7b9a6d4fbf 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift
@@ -207,7 +207,7 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
isCurrent: false)]
}
- private func allSessions() -> [UserSessionInfo] {
+ func allSessions() -> [UserSessionInfo] {
[UserSessionInfo(id: "0",
name: "iOS",
deviceType: .mobile,
diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift
index 1273f32d56..45d43f3b37 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift
@@ -56,4 +56,40 @@ class UserOtherSessionsUITests: MockScreenTestCase {
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionVerifiedShort].exists)
XCTAssertTrue(app.staticTexts[VectorL10n.userOtherSessionVerifiedSessionsHeaderSubtitle].exists)
}
+
+ func test_whenOtherSessionsMoreMenuButtonSelected_selectSessionsButtonExists() {
+ app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.all.title)
+
+ app.buttons["More"].tap()
+ XCTAssertTrue(app.buttons["Select sessions"].exists)
+ }
+
+ func test_whenOtherSessionsSelectSessionsSelected_navBarContainsCorrectButtons() {
+ app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.all.title)
+
+ app.buttons["More"].tap()
+ app.buttons["Select sessions"].tap()
+ XCTAssertTrue(app.buttons["Select All"].exists)
+ XCTAssertTrue(app.buttons["Cancel"].exists)
+ }
+
+ func test_whenOtherSessionsSelectAllSelected_navBarContainsCorrectButtons() {
+ app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.all.title)
+
+ app.buttons["More"].tap()
+ app.buttons["Select sessions"].tap()
+ app.buttons["Select All"].tap()
+ XCTAssertTrue(app.buttons["Deselect All"].exists)
+ XCTAssertTrue(app.buttons["Cancel"].exists)
+ }
+
+ func test_whenAllOtherSessionsAreSelected_navBarContainsCorrectButtons() {
+ app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.all.title)
+ app.buttons["More"].tap()
+ app.buttons["Select sessions"].tap()
+ for i in 0...MockUserOtherSessionsScreenState.all.allSessions().count - 1 {
+ app.buttons["UserSessionListItem_\(i)"].tap()
+ }
+ XCTAssertTrue(app.buttons["Deselect All"].exists)
+ }
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift
index 05a25b5f54..782bdac4f3 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift
@@ -55,9 +55,13 @@ class UserOtherSessionsViewModelTests: XCTestCase {
let sut = createSUT(sessionInfos: sessionInfos, filter: .inactive)
let expectedItems = sessionInfos.filter { !$0.isActive }.asViewData()
- let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .inactive),
+ let bindings = UserOtherSessionsBindings(filter: .inactive, isEditModeEnabled: false)
+ let expectedState = UserOtherSessionsViewState(bindings: bindings,
title: "Title",
- sections: [.sessionItems(header: inactiveSectionHeader, items: expectedItems)])
+ sessionItems: expectedItems,
+ header: inactiveSectionHeader,
+ emptyItemsTitle: VectorL10n.userOtherSessionNoInactiveSessions,
+ allItemsSelected: false)
XCTAssertEqual(sut.state, expectedState)
}
@@ -67,9 +71,13 @@ class UserOtherSessionsViewModelTests: XCTestCase {
let sut = createSUT(sessionInfos: sessionInfos, filter: .all)
let expectedItems = sessionInfos.filter { !$0.isCurrent }.asViewData()
- let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .all),
+ let bindings = UserOtherSessionsBindings(filter: .all, isEditModeEnabled: false)
+ let expectedState = UserOtherSessionsViewState(bindings: bindings,
title: "Title",
- sections: [.sessionItems(header: allSectionHeader, items: expectedItems)])
+ sessionItems: expectedItems,
+ header: allSectionHeader,
+ emptyItemsTitle: "",
+ allItemsSelected: false)
XCTAssertEqual(sut.state, expectedState)
}
@@ -79,9 +87,13 @@ class UserOtherSessionsViewModelTests: XCTestCase {
let sut = createSUT(sessionInfos: sessionInfos, filter: .unverified)
let expectedItems = sessionInfos.filter { !$0.isCurrent }.asViewData()
- let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .unverified),
+ let bindings = UserOtherSessionsBindings(filter: .unverified, isEditModeEnabled: false)
+ let expectedState = UserOtherSessionsViewState(bindings: bindings,
title: "Title",
- sections: [.sessionItems(header: unverifiedSectionHeader, items: expectedItems)])
+ sessionItems: expectedItems,
+ header: unverifiedSectionHeader,
+ emptyItemsTitle: VectorL10n.userOtherSessionNoUnverifiedSessions,
+ allItemsSelected: false)
XCTAssertEqual(sut.state, expectedState)
}
@@ -91,9 +103,13 @@ class UserOtherSessionsViewModelTests: XCTestCase {
let sut = createSUT(sessionInfos: sessionInfos, filter: .verified)
let expectedItems = sessionInfos.filter { !$0.isCurrent }.asViewData()
- let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .verified),
+ let bindings = UserOtherSessionsBindings(filter: .verified, isEditModeEnabled: false)
+ let expectedState = UserOtherSessionsViewState(bindings: bindings,
title: "Title",
- sections: [.sessionItems(header: verifiedSectionHeader, items: expectedItems)])
+ sessionItems: expectedItems,
+ header: verifiedSectionHeader,
+ emptyItemsTitle: VectorL10n.userOtherSessionNoVerifiedSessions,
+ allItemsSelected: false)
XCTAssertEqual(sut.state, expectedState)
}
@@ -101,10 +117,13 @@ class UserOtherSessionsViewModelTests: XCTestCase {
let sessionInfos = [createUserSessionInfo(sessionId: "session 1", isVerified: false),
createUserSessionInfo(sessionId: "session 2", isVerified: false)]
let sut = createSUT(sessionInfos: sessionInfos, filter: .verified)
-
- let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .verified),
+ let bindings = UserOtherSessionsBindings(filter: .verified, isEditModeEnabled: false)
+ let expectedState = UserOtherSessionsViewState(bindings: bindings,
title: "Title",
- sections: [.emptySessionItems(header: verifiedSectionHeader, title: VectorL10n.userOtherSessionNoVerifiedSessions)])
+ sessionItems: [],
+ header: verifiedSectionHeader,
+ emptyItemsTitle: VectorL10n.userOtherSessionNoVerifiedSessions,
+ allItemsSelected: false)
XCTAssertEqual(sut.state, expectedState)
}
@@ -112,10 +131,13 @@ class UserOtherSessionsViewModelTests: XCTestCase {
let sessionInfos = [createUserSessionInfo(sessionId: "session 1", isVerified: true),
createUserSessionInfo(sessionId: "session 2", isVerified: true)]
let sut = createSUT(sessionInfos: sessionInfos, filter: .unverified)
-
- let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .unverified),
+ let bindings = UserOtherSessionsBindings(filter: .unverified, isEditModeEnabled: false)
+ let expectedState = UserOtherSessionsViewState(bindings: bindings,
title: "Title",
- sections: [.emptySessionItems(header: unverifiedSectionHeader, title: VectorL10n.userOtherSessionNoUnverifiedSessions)])
+ sessionItems: [],
+ header: unverifiedSectionHeader,
+ emptyItemsTitle: VectorL10n.userOtherSessionNoUnverifiedSessions,
+ allItemsSelected: false)
XCTAssertEqual(sut.state, expectedState)
}
@@ -123,13 +145,134 @@ class UserOtherSessionsViewModelTests: XCTestCase {
let sessionInfos = [createUserSessionInfo(sessionId: "session 1", isActive: true),
createUserSessionInfo(sessionId: "session 2", isActive: true)]
let sut = createSUT(sessionInfos: sessionInfos, filter: .inactive)
-
- let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .inactive),
+ let bindings = UserOtherSessionsBindings(filter: .inactive, isEditModeEnabled: false)
+ let expectedState = UserOtherSessionsViewState(bindings: bindings,
title: "Title",
- sections: [.emptySessionItems(header: inactiveSectionHeader, title: VectorL10n.userOtherSessionNoInactiveSessions)])
+ sessionItems: [],
+ header: inactiveSectionHeader,
+ emptyItemsTitle: VectorL10n.userOtherSessionNoInactiveSessions,
+ allItemsSelected: false)
+ XCTAssertEqual(sut.state, expectedState)
+ }
+
+ func test_whenEditModeEnabledAndAllItemsSelected_viewStateIsCorrect() {
+ let sessionInfos = [createUserSessionInfo(sessionId: "session 1"),
+ createUserSessionInfo(sessionId: "session 2")]
+ let sut = createSUT(sessionInfos: sessionInfos, filter: .all)
+ toggleEditMode(for: sut, value: true)
+ sut.process(viewAction: .userOtherSessionSelected(sessionId: "session 1"))
+ sut.process(viewAction: .userOtherSessionSelected(sessionId: "session 2"))
+
+ let expectedItems = sessionInfos.map { UserSessionListItemViewDataFactory().create(from: $0, isSelected: true) }
+ let bindings = UserOtherSessionsBindings(filter: .all, isEditModeEnabled: true)
+ let expectedState = UserOtherSessionsViewState(bindings: bindings,
+ title: VectorL10n.userOtherSessionSelectedCount("2"),
+ sessionItems: expectedItems,
+ header: allSectionHeader,
+ emptyItemsTitle: "",
+ allItemsSelected: true)
XCTAssertEqual(sut.state, expectedState)
}
+ func test_whenEditModeEnabledAndItemSelectedAndDeselected_viewStateIsCorrect() {
+ let sessionInfos = [createUserSessionInfo(sessionId: "session 1"),
+ createUserSessionInfo(sessionId: "session 2")]
+ let sut = createSUT(sessionInfos: sessionInfos, filter: .all)
+ toggleEditMode(for: sut, value: true)
+ sut.process(viewAction: .userOtherSessionSelected(sessionId: "session 1"))
+ sut.process(viewAction: .userOtherSessionSelected(sessionId: "session 1"))
+
+ let expectedItems = sessionInfos.map { UserSessionListItemViewDataFactory().create(from: $0, isSelected: false) }
+ let bindings = UserOtherSessionsBindings(filter: .all, isEditModeEnabled: true)
+ let expectedState = UserOtherSessionsViewState(bindings: bindings,
+ title: VectorL10n.userOtherSessionSelectedCount("0"),
+ sessionItems: expectedItems,
+ header: allSectionHeader,
+ emptyItemsTitle: "",
+ allItemsSelected: false)
+ XCTAssertEqual(sut.state, expectedState)
+ }
+
+ func test_whenEditModeEnabledAndNotAllItemsSelected_viewStateIsCorrect() {
+ let sessionInfos = [createUserSessionInfo(sessionId: "session 1"),
+ createUserSessionInfo(sessionId: "session 2")]
+ let sut = createSUT(sessionInfos: sessionInfos, filter: .all)
+ toggleEditMode(for: sut, value: true)
+ sut.process(viewAction: .userOtherSessionSelected(sessionId: "session 2"))
+
+ let expectedItems = sessionInfos.map { UserSessionListItemViewDataFactory().create(from: $0, isSelected: $0.id == "session 2") }
+ let bindings = UserOtherSessionsBindings(filter: .all, isEditModeEnabled: true)
+ let expectedState = UserOtherSessionsViewState(bindings: bindings,
+ title: VectorL10n.userOtherSessionSelectedCount("1"),
+ sessionItems: expectedItems,
+ header: allSectionHeader,
+ emptyItemsTitle: "",
+ allItemsSelected: false)
+ XCTAssertEqual(sut.state, expectedState)
+ }
+
+ func test_whenEditModeEnabledAndAllItemsSelectedByButton_viewStateIsCorrect() {
+ let sessionInfos = [createUserSessionInfo(sessionId: "session 1"),
+ createUserSessionInfo(sessionId: "session 2")]
+ let sut = createSUT(sessionInfos: sessionInfos, filter: .all)
+ toggleEditMode(for: sut, value: true)
+ sut.process(viewAction: .toggleAllSelection)
+
+ let expectedItems = sessionInfos.map { UserSessionListItemViewDataFactory().create(from: $0, isSelected: true) }
+ let bindings = UserOtherSessionsBindings(filter: .all, isEditModeEnabled: true)
+ let expectedState = UserOtherSessionsViewState(bindings: bindings,
+ title: VectorL10n.userOtherSessionSelectedCount("2"),
+ sessionItems: expectedItems,
+ header: allSectionHeader,
+ emptyItemsTitle: "",
+ allItemsSelected: true)
+ XCTAssertEqual(sut.state, expectedState)
+ }
+
+ func test_whenEditModeEnabledAndAllItemsDeselectedByButton_viewStateIsCorrect() {
+ let sessionInfos = [createUserSessionInfo(sessionId: "session 1"),
+ createUserSessionInfo(sessionId: "session 2")]
+ let sut = createSUT(sessionInfos: sessionInfos, filter: .all)
+ toggleEditMode(for: sut, value: true)
+ sut.process(viewAction: .toggleAllSelection)
+ sut.process(viewAction: .toggleAllSelection)
+ let expectedItems = sessionInfos.map { UserSessionListItemViewDataFactory().create(from: $0, isSelected: false) }
+ let bindings = UserOtherSessionsBindings(filter: .all, isEditModeEnabled: true)
+ let expectedState = UserOtherSessionsViewState(bindings: bindings,
+ title: VectorL10n.userOtherSessionSelectedCount("0"),
+ sessionItems: expectedItems,
+ header: allSectionHeader,
+ emptyItemsTitle: "",
+ allItemsSelected: false)
+ XCTAssertEqual(sut.state, expectedState)
+ }
+
+ func test_whenEditModeEnabledDisabledAndEnabled_viewStateIsCorrect() {
+ let sessionInfos = [createUserSessionInfo(sessionId: "session 1"),
+ createUserSessionInfo(sessionId: "session 2")]
+ let sut = createSUT(sessionInfos: sessionInfos, filter: .all)
+ toggleEditMode(for: sut, value: true)
+ sut.process(viewAction: .editModeWasToggled)
+ sut.process(viewAction: .userOtherSessionSelected(sessionId: "session 1"))
+ sut.process(viewAction: .userOtherSessionSelected(sessionId: "session 2"))
+ toggleEditMode(for: sut, value: false)
+ toggleEditMode(for: sut, value: true)
+ let expectedItems = sessionInfos.map { UserSessionListItemViewDataFactory().create(from: $0, isSelected: false) }
+ let bindings = UserOtherSessionsBindings(filter: .all, isEditModeEnabled: true)
+ let expectedState = UserOtherSessionsViewState(bindings: bindings,
+ title: VectorL10n.userOtherSessionSelectedCount("0"),
+ sessionItems: expectedItems,
+ header: allSectionHeader,
+ emptyItemsTitle: "",
+ allItemsSelected: false)
+ XCTAssertEqual(sut.state, expectedState)
+ }
+
+ private func toggleEditMode(for model: UserOtherSessionsViewModel, value: Bool) {
+ model.context.isEditModeEnabled = value
+ model.process(viewAction: .editModeWasToggled)
+ }
+
private func createSUT(sessionInfos: [UserSessionInfo],
filter: UserOtherSessionsFilter,
title: String = "Title") -> UserOtherSessionsViewModel {
diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift
index c09ff774c3..8aefc40b96 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift
@@ -32,25 +32,22 @@ enum UserOtherSessionsViewModelResult: Equatable {
struct UserOtherSessionsViewState: BindableState, Equatable {
var bindings: UserOtherSessionsBindings
- let title: String
- var sections: [UserOtherSessionsSection]
+ var title: String
+ var sessionItems: [UserSessionListItemViewData]
+ var header: UserOtherSessionsHeaderViewData
+ var emptyItemsTitle: String
+ var allItemsSelected: Bool
}
struct UserOtherSessionsBindings: Equatable {
var filter: UserOtherSessionsFilter
-}
-
-enum UserOtherSessionsSection: Hashable, Identifiable {
- var id: Self {
- self
- }
-
- case sessionItems(header: UserOtherSessionsHeaderViewData, items: [UserSessionListItemViewData])
- case emptySessionItems(header: UserOtherSessionsHeaderViewData, title: String)
+ var isEditModeEnabled: Bool
}
enum UserOtherSessionsViewAction {
case userOtherSessionSelected(sessionId: String)
case filterWasChanged
case clearFilter
+ case editModeWasToggled
+ case toggleAllSelection
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift
index 9bad552d8b..b0cac51850 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift
@@ -21,15 +21,22 @@ typealias UserOtherSessionsViewModelType = StateStoreViewModel Void)?
private let sessionInfos: [UserSessionInfo]
+ private var selectedSessions: Set = []
+ private let defaultTitle: String
init(sessionInfos: [UserSessionInfo],
filter: UserOtherSessionsFilter,
title: String) {
self.sessionInfos = sessionInfos
- super.init(initialViewState: UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: filter),
+ defaultTitle = title
+ let bindings = UserOtherSessionsBindings(filter: filter, isEditModeEnabled: false)
+ let sessionItems = filter.filterSessionInfos(sessionInfos: sessionInfos, selectedSessions: selectedSessions)
+ super.init(initialViewState: UserOtherSessionsViewState(bindings: bindings,
title: title,
- sections: []))
- updateViewState()
+ sessionItems: sessionItems,
+ header: filter.userOtherSessionsViewHeader,
+ emptyItemsTitle: filter.userOtherSessionsViewEmptyResultsTitle,
+ allItemsSelected: false))
}
// MARK: - Public
@@ -37,56 +44,75 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi
override func process(viewAction: UserOtherSessionsViewAction) {
switch viewAction {
case let .userOtherSessionSelected(sessionId: sessionId):
- guard let session = sessionInfos.first(where: { $0.id == sessionId }) else {
- assertionFailure("Session should exist in the array.")
- return
+ if state.bindings.isEditModeEnabled {
+ updateSelectionForSession(sessionId: sessionId)
+ updateViewState()
+ } else {
+ showUserSessionOverview(sessionId: sessionId)
}
- completion?(.showUserSessionOverview(sessionInfo: session))
case .filterWasChanged:
updateViewState()
case .clearFilter:
state.bindings.filter = .all
updateViewState()
+ case .editModeWasToggled:
+ selectedSessions.removeAll()
+ updateViewState()
+ case .toggleAllSelection:
+ toggleAllSelection()
+ updateViewState()
}
}
-
+
// MARK: - Private
- private func updateViewState() {
- let sectionItems = createSectionItems(sessionInfos: sessionInfos, filter: state.bindings.filter)
- let sectionHeader = createHeaderData(filter: state.bindings.filter)
- if sectionItems.isEmpty {
- state.sections = [.emptySessionItems(header: sectionHeader,
- title: noSessionsTitle(filter: state.bindings.filter))]
- } else {
- state.sections = [.sessionItems(header: sectionHeader,
- items: sectionItems)]
+ private func showUserSessionOverview(sessionId: String) {
+ guard let session = sessionInfos.first(where: { $0.id == sessionId }) else {
+ assertionFailure("Session should exist in the array.")
+ return
}
+ completion?(.showUserSessionOverview(sessionInfo: session))
}
- private func createSectionItems(sessionInfos: [UserSessionInfo], filter: UserOtherSessionsFilter) -> [UserSessionListItemViewData] {
- filterSessions(sessionInfos: sessionInfos, by: filter)
- .map {
- UserSessionListItemViewDataFactory().create(from: $0,
- highlightSessionDetails: filter == .unverified && $0.isCurrent)
- }
+ private func updateSelectionForSession(sessionId: String) {
+ if selectedSessions.contains(sessionId) {
+ selectedSessions.remove(sessionId)
+ } else {
+ selectedSessions.insert(sessionId)
+ }
}
- private func filterSessions(sessionInfos: [UserSessionInfo], by filter: UserOtherSessionsFilter) -> [UserSessionInfo] {
- switch filter {
- case .all:
- return sessionInfos.filter { !$0.isCurrent }
- case .inactive:
- return sessionInfos.filter { !$0.isActive }
- case .unverified:
- return sessionInfos.filter { $0.verificationState != .verified }
- case .verified:
- return sessionInfos.filter { $0.verificationState == .verified }
+ private func updateViewState() {
+ let currentFilter = state.bindings.filter
+
+ state.sessionItems = currentFilter.filterSessionInfos(sessionInfos: sessionInfos, selectedSessions: selectedSessions)
+ state.header = currentFilter.userOtherSessionsViewHeader
+
+ if state.bindings.isEditModeEnabled {
+ state.title = VectorL10n.userOtherSessionSelectedCount(String(selectedSessions.count))
+ } else {
+ state.title = defaultTitle
}
+
+ state.emptyItemsTitle = currentFilter.userOtherSessionsViewEmptyResultsTitle
+
+ state.allItemsSelected = sessionInfos.count == selectedSessions.count
}
- private func createHeaderData(filter: UserOtherSessionsFilter) -> UserOtherSessionsHeaderViewData {
- switch filter {
+ private func toggleAllSelection() {
+ if state.allItemsSelected {
+ selectedSessions.removeAll()
+ } else {
+ sessionInfos.forEach { sessionInfo in
+ selectedSessions.insert(sessionInfo.id)
+ }
+ }
+ }
+}
+
+private extension UserOtherSessionsFilter {
+ var userOtherSessionsViewHeader: UserOtherSessionsHeaderViewData {
+ switch self {
case .all:
return UserOtherSessionsHeaderViewData(title: nil,
subtitle: VectorL10n.userSessionsOverviewOtherSessionsSectionInfo,
@@ -106,10 +132,9 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi
}
}
- private func noSessionsTitle(filter: UserOtherSessionsFilter) -> String {
- switch filter {
+ var userOtherSessionsViewEmptyResultsTitle: String {
+ switch self {
case .all:
- assertionFailure("The view is not intended to be displayed without any session")
return ""
case .verified:
return VectorL10n.userOtherSessionNoVerifiedSessions
@@ -119,4 +144,26 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi
return VectorL10n.userOtherSessionNoInactiveSessions
}
}
+
+ func filterSessionsInfos(_ sessionInfos: [UserSessionInfo]) -> [UserSessionInfo] {
+ switch self {
+ case .all:
+ return sessionInfos.filter { !$0.isCurrent }
+ case .inactive:
+ return sessionInfos.filter { !$0.isActive }
+ case .unverified:
+ return sessionInfos.filter { $0.verificationState != .verified }
+ case .verified:
+ return sessionInfos.filter { $0.verificationState == .verified }
+ }
+ }
+
+ func filterSessionInfos(sessionInfos: [UserSessionInfo], selectedSessions: Set) -> [UserSessionListItemViewData] {
+ filterSessionsInfos(sessionInfos)
+ .map {
+ UserSessionListItemViewDataFactory().create(from: $0,
+ highlightSessionDetails: self == .unverified && $0.isCurrent,
+ isSelected: selectedSessions.contains($0.id))
+ }
+ }
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift
index 9b64b201bd..b8f390a056 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift
@@ -23,85 +23,71 @@ struct UserOtherSessions: View {
var body: some View {
ScrollView {
- ForEach(viewModel.viewState.sections) { section in
- switch section {
- case let .sessionItems(header: header, items: items):
- createSessionItemsSection(header: header, items: items)
- case let .emptySessionItems(header: header, title: title):
- createEmptySessionsItemsSection(header: header, title: title)
+ SwiftUI.Section {
+ if viewModel.viewState.sessionItems.isEmpty {
+ noItemsView()
+ } else {
+ itemsView()
}
+ } header: {
+ UserOtherSessionsHeaderView(viewData: viewModel.viewState.header)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.top, 24.0)
}
}
+ .onChange(of: viewModel.isEditModeEnabled) { _ in
+ viewModel.send(viewAction: .editModeWasToggled)
+ }
+ .onChange(of: viewModel.filter) { _ in
+ viewModel.send(viewAction: .filterWasChanged)
+ }
.background(theme.colors.system.ignoresSafeArea())
.frame(maxHeight: .infinity)
.navigationTitle(viewModel.viewState.title)
.toolbar {
- ToolbarItem(placement: .navigationBarTrailing) {
- Menu {
- Picker("", selection: $viewModel.filter) {
- ForEach(UserOtherSessionsFilter.allCases) { filter in
- Text(filter.menuLocalizedName).tag(filter)
- }
- }
- .labelsHidden()
- .onChange(of: viewModel.filter) { _ in
- viewModel.send(viewAction: .filterWasChanged)
- }
- } label: {
- Image(viewModel.filter == .all ? Asset.Images.userOtherSessionsFilter.name : Asset.Images.userOtherSessionsFilterSelected.name)
- }
- .accessibilityLabel(VectorL10n.userOtherSessionFilter)
+ UserOtherSessionsToolbar(isEditModeEnabled: $viewModel.isEditModeEnabled,
+ filter: $viewModel.filter,
+ allItemsSelected: viewModel.viewState.allItemsSelected) {
+ viewModel.send(viewAction: .toggleAllSelection)
}
}
+ .navigationBarBackButtonHidden(viewModel.isEditModeEnabled)
+ .accentColor(theme.colors.accent)
}
- private func createSessionItemsSection(header: UserOtherSessionsHeaderViewData, items: [UserSessionListItemViewData]) -> some View {
- SwiftUI.Section {
- LazyVStack(spacing: 0) {
- ForEach(items) { viewData in
- UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in
- viewModel.send(viewAction: .userOtherSessionSelected(sessionId: sessionId))
- })
+ private func noItemsView() -> some View {
+ VStack {
+ Text(viewModel.viewState.emptyItemsTitle)
+ .font(theme.fonts.footnote)
+ .foregroundColor(theme.colors.primaryContent)
+ .padding(.bottom, 20)
+ Button {
+ viewModel.send(viewAction: .clearFilter)
+ } label: {
+ VStack(spacing: 0) {
+ SeparatorLine()
+ Text(VectorL10n.userOtherSessionClearFilter)
+ .font(theme.fonts.body)
+ .foregroundColor(theme.colors.accent)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding(.vertical, 11)
+ SeparatorLine()
}
+ .background(theme.colors.background)
}
- .background(theme.colors.background)
- } header: {
- headerView(header: header)
}
}
- private func createEmptySessionsItemsSection(header: UserOtherSessionsHeaderViewData, title: String) -> some View {
- SwiftUI.Section {
- VStack {
- Text(title)
- .font(theme.fonts.footnote)
- .foregroundColor(theme.colors.primaryContent)
- .padding(.bottom, 20)
- Button {
- viewModel.send(viewAction: .clearFilter)
- } label: {
- VStack(spacing: 0) {
- SeparatorLine()
- Text(VectorL10n.userOtherSessionClearFilter)
- .font(theme.fonts.body)
- .foregroundColor(theme.colors.accent)
- .frame(maxWidth: .infinity, alignment: .center)
- .padding(.vertical, 11)
- SeparatorLine()
- }
- .background(theme.colors.background)
- }
+ private func itemsView() -> some View {
+ LazyVStack(spacing: 0) {
+ ForEach(viewModel.viewState.sessionItems) { viewData in
+ UserSessionListItem(viewData: viewData,
+ isEditModeEnabled: viewModel.isEditModeEnabled,
+ onBackgroundTap: { sessionId in viewModel.send(viewAction: .userOtherSessionSelected(sessionId: sessionId)) },
+ onBackgroundLongPress: { _ in viewModel.isEditModeEnabled = true })
}
-
- } header: {
- headerView(header: header)
}
- }
-
- private func headerView(header: UserOtherSessionsHeaderViewData) -> some View {
- UserOtherSessionsHeaderView(viewData: header)
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding(.top, 24.0)
+ .background(theme.colors.background)
}
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift
new file mode 100644
index 0000000000..244e1473e9
--- /dev/null
+++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift
@@ -0,0 +1,94 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import SwiftUI
+
+struct UserOtherSessionsToolbar: ToolbarContent {
+ @Environment(\.theme) private var theme
+
+ @Binding var isEditModeEnabled: Bool
+ @Binding var filter: UserOtherSessionsFilter
+ var allItemsSelected: Bool
+ let onToggleSelection: () -> Void
+
+ var body: some ToolbarContent {
+ navigationBarLeading()
+ navigationBarTrailing()
+ }
+
+ private func navigationBarLeading() -> some ToolbarContent {
+ ToolbarItemGroup(placement: .navigationBarLeading) {
+ if isEditModeEnabled {
+ Button(allItemsSelected ? VectorL10n.deselectAll : VectorL10n.selectAll, action: {
+ onToggleSelection()
+ })
+ }
+ }
+ }
+
+ private func navigationBarTrailing() -> some ToolbarContent {
+ ToolbarItemGroup(placement: .navigationBarTrailing) {
+ if isEditModeEnabled {
+ cancelButton()
+ } else {
+ filterMenuButton()
+ .offset(x: 12)
+ optionsMenu()
+ }
+ }
+ }
+
+ private func cancelButton() -> some View {
+ Button(VectorL10n.cancel) {
+ isEditModeEnabled = false
+ }
+ .font(theme.fonts.bodySB)
+ .foregroundColor(theme.colors.accent)
+ }
+
+ private func filterMenuButton() -> some View {
+ Button { } label: {
+ Menu {
+ Picker("", selection: $filter) {
+ ForEach(UserOtherSessionsFilter.allCases) { filter in
+ Text(filter.menuLocalizedName).tag(filter)
+ }
+ }
+ .labelsHidden()
+ } label: {
+ Image(filter == .all ? Asset.Images.userOtherSessionsFilter.name : Asset.Images.userOtherSessionsFilterSelected.name)
+ }
+ .accessibilityLabel(VectorL10n.userOtherSessionFilter)
+ }
+ }
+
+ private func optionsMenu() -> some View {
+ Button { } label: {
+ Menu {
+ Button {
+ isEditModeEnabled = true
+ } label: {
+ Label(VectorL10n.userOtherSessionMenuSelectSessions, systemImage: "checkmark.circle")
+ }
+
+ } label: {
+ Image(systemName: "ellipsis")
+ .padding(.horizontal, 4)
+ .padding(.vertical, 12)
+ }
+ }
+ }
+}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/UI/UserSessionDetailsUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/UI/UserSessionDetailsUITests.swift
index 6eb573fe96..ea2133dd8a 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/UI/UserSessionDetailsUITests.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/UI/UserSessionDetailsUITests.swift
@@ -18,7 +18,7 @@ import RiotSwiftUI
import XCTest
class UserSessionDetailsUITests: MockScreenTestCase {
- func test_longPressDetailsCell_CopiesValueToClipboard() throws {
+ func disabled_broken_xcode14_test_longPressDetailsCell_CopiesValueToClipboard() throws {
app.goToScreenWithIdentifier(MockUserSessionDetailsScreenState.allSections.title)
UIPasteboard.general.string = ""
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProvider.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProvider.swift
index 84c75b7eab..1028dd3cbf 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProvider.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProvider.swift
@@ -47,7 +47,7 @@ class UserSessionsDataProvider: UserSessionsDataProviderProtocol {
func verificationState(for deviceInfo: MXDeviceInfo?) -> UserSessionInfo.VerificationState {
guard let deviceInfo = deviceInfo else { return .unknown }
- guard session.crypto?.crossSigning?.canCrossSign == true else {
+ guard session.crypto?.crossSigning.canCrossSign == true else {
return deviceInfo.deviceId == session.myDeviceId ? .unverified : .unknown
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift
index 9c25ad3afe..0705c8c54a 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift
@@ -25,59 +25,76 @@ struct UserSessionListItem: View {
}
@Environment(\.theme) private var theme: ThemeSwiftUI
-
+
let viewData: UserSessionListItemViewData
+ var isEditModeEnabled = false
+
var onBackgroundTap: ((String) -> Void)?
+ var onBackgroundLongPress: ((String) -> Void)?
var body: some View {
- Button {
- onBackgroundTap?(viewData.sessionId)
- } label: {
- VStack(alignment: .leading, spacing: LayoutConstants.verticalPadding) {
- HStack(spacing: LayoutConstants.avatarRightMargin) {
- DeviceAvatarView(viewData: viewData.deviceAvatarViewData)
- VStack(alignment: .leading, spacing: 2) {
- Text(viewData.sessionName)
- .font(theme.fonts.bodySB)
- .foregroundColor(theme.colors.primaryContent)
- .multilineTextAlignment(.leading)
- HStack {
- if let sessionDetailsIcon = viewData.sessionDetailsIcon {
- Image(sessionDetailsIcon)
- .padding(.leading, 2)
- }
- Text(viewData.sessionDetails)
- .font(theme.fonts.caption1)
- .foregroundColor(viewData.highlightSessionDetails ? theme.colors.alert : theme.colors.secondaryContent)
+ Button { } label: {
+ ZStack {
+ if viewData.isSelected {
+ RoundedRectangle(cornerRadius: 8, style: .continuous)
+ .fill(theme.colors.system)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(4)
+ }
+ VStack(alignment: .leading, spacing: LayoutConstants.verticalPadding) {
+ HStack(spacing: LayoutConstants.avatarRightMargin) {
+ if isEditModeEnabled {
+ Image(viewData.isSelected ? Asset.Images.userSessionListItemSelected.name : Asset.Images.userSessionListItemNotSelected.name)
+ }
+ DeviceAvatarView(viewData: viewData.deviceAvatarViewData, isSelected: viewData.isSelected)
+ VStack(alignment: .leading, spacing: 2) {
+ Text(viewData.sessionName)
+ .font(theme.fonts.bodySB)
+ .foregroundColor(theme.colors.primaryContent)
.multilineTextAlignment(.leading)
+ HStack {
+ if let sessionDetailsIcon = viewData.sessionDetailsIcon {
+ Image(sessionDetailsIcon)
+ .padding(.leading, 2)
+ }
+ Text(viewData.sessionDetails)
+ .font(theme.fonts.caption1)
+ .foregroundColor(viewData.highlightSessionDetails ? theme.colors.alert : theme.colors.secondaryContent)
+ .multilineTextAlignment(.leading)
+ }
}
}
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.horizontal, LayoutConstants.horizontalPadding)
+
+ // Separator
+ // Note: Separator leading is matching the text leading, we could use alignment guide in the future
+ SeparatorLine()
+ .padding(.leading, LayoutConstants.horizontalPadding + LayoutConstants.avatarRightMargin + LayoutConstants.avatarWidth)
}
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding(.horizontal, LayoutConstants.horizontalPadding)
-
- // Separator
- // Note: Separator leading is matching the text leading, we could use alignment guide in the future
- SeparatorLine()
- .padding(.leading, LayoutConstants.horizontalPadding + LayoutConstants.avatarRightMargin + LayoutConstants.avatarWidth)
+ .padding(.top, LayoutConstants.verticalPadding)
+ }.onTapGesture {
+ onBackgroundTap?(viewData.sessionId)
+ }
+ .onLongPressGesture {
+ onBackgroundLongPress?(viewData.sessionId)
}
- .padding(.top, LayoutConstants.verticalPadding)
}
.frame(maxWidth: .infinity, alignment: .leading)
+ .accessibilityIdentifier("UserSessionListItem_\(viewData.sessionId)")
}
}
struct UserSessionListPreview: View {
let userSessionsOverviewService: UserSessionsOverviewServiceProtocol = MockUserSessionsOverviewService()
+ var isEditModeEnabled = false
var body: some View {
VStack(alignment: .leading, spacing: 0) {
ForEach(userSessionsOverviewService.otherSessions) { userSessionInfo in
let viewData = UserSessionListItemViewDataFactory().create(from: userSessionInfo)
-
- UserSessionListItem(viewData: viewData, onBackgroundTap: { _ in
-
+ UserSessionListItem(viewData: viewData, isEditModeEnabled: isEditModeEnabled, onBackgroundTap: { _ in
})
}
}
@@ -89,6 +106,8 @@ struct UserSessionListItem_Previews: PreviewProvider {
Group {
UserSessionListPreview().theme(.light).preferredColorScheme(.light)
UserSessionListPreview().theme(.dark).preferredColorScheme(.dark)
+ UserSessionListPreview(isEditModeEnabled: true).theme(.light).preferredColorScheme(.light)
+ UserSessionListPreview(isEditModeEnabled: true).theme(.dark).preferredColorScheme(.dark)
}
}
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift
index 6cddefda27..5122e0895f 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift
@@ -16,13 +16,15 @@
import Foundation
+typealias SessionId = String
+
/// View data for UserSessionListItem
struct UserSessionListItemViewData: Identifiable, Hashable {
var id: String {
sessionId
}
- let sessionId: String
+ let sessionId: SessionId
let sessionName: String
@@ -33,4 +35,6 @@ struct UserSessionListItemViewData: Identifiable, Hashable {
let deviceAvatarViewData: DeviceAvatarViewData
let sessionDetailsIcon: String?
+
+ let isSelected: Bool
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift
index 4fb030de8d..5486073a73 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift
@@ -17,7 +17,9 @@
import Foundation
struct UserSessionListItemViewDataFactory {
- func create(from sessionInfo: UserSessionInfo, highlightSessionDetails: Bool = false) -> UserSessionListItemViewData {
+ func create(from sessionInfo: UserSessionInfo,
+ highlightSessionDetails: Bool = false,
+ isSelected: Bool = false) -> UserSessionListItemViewData {
let sessionName = UserSessionNameFormatter.sessionName(deviceType: sessionInfo.deviceType,
sessionDisplayName: sessionInfo.name)
let sessionDetails = buildSessionDetails(sessionInfo: sessionInfo)
@@ -28,7 +30,8 @@ struct UserSessionListItemViewDataFactory {
sessionDetails: sessionDetails,
highlightSessionDetails: highlightSessionDetails,
deviceAvatarViewData: deviceAvatarViewData,
- sessionDetailsIcon: getSessionDetailsIcon(isActive: sessionInfo.isActive))
+ sessionDetailsIcon: getSessionDetailsIcon(isActive: sessionInfo.isActive),
+ isSelected: isSelected)
}
private func buildSessionDetails(sessionInfo: UserSessionInfo) -> String {
diff --git a/RiotTests/UserSessionsDataProviderTests.swift b/RiotTests/UserSessionsDataProviderTests.swift
index 85459347e3..3780dcd653 100644
--- a/RiotTests/UserSessionsDataProviderTests.swift
+++ b/RiotTests/UserSessionsDataProviderTests.swift
@@ -111,9 +111,9 @@ private class MockSession: MXSession {
}
/// A mock `MXCrypto` that can override the `canCrossSign` state.
-private class MockCrypto: MXCrypto {
+private class MockCrypto: MXLegacyCrypto {
let canCrossSign: Bool
- override var crossSigning: MXCrossSigning! { MockCrossSigning(canCrossSign: canCrossSign) }
+ override var crossSigning: MXCrossSigning { MockCrossSigning(canCrossSign: canCrossSign) }
init(canCrossSign: Bool) {
self.canCrossSign = canCrossSign
@@ -123,7 +123,7 @@ private class MockCrypto: MXCrypto {
}
/// A mock `MXCrossSigning` with an overridden `canCrossSign` property.
-private class MockCrossSigning: MXCrossSigning {
+private class MockCrossSigning: MXLegacyCrossSigning {
let canCrossSignMock: Bool
override var canCrossSign: Bool { canCrossSignMock }
diff --git a/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m b/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m
index 84d9dee63b..34ebb66e9b 100644
--- a/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m
+++ b/SiriIntents/IntentHandlers/SendMessage/SendMessageIntentHandler.m
@@ -118,7 +118,10 @@ - (void)handleSendMessage:(INSendMessageIntent *)intent completion:(void (^)(INS
self.selectedRoom = [MXRoom loadRoomFromStore:fileStore withRoomId:roomID matrixSession:session];
// Do not warn for unknown devices. We have cross-signing now
- session.crypto.warnOnUnknowDevices = NO;
+ if ([session.crypto isKindOfClass:[MXLegacyCrypto class]])
+ {
+ ((MXLegacyCrypto *)session.crypto).warnOnUnknowDevices = NO;
+ }
MXWeakify(self);
[self.selectedRoom sendTextMessage:intent.content
diff --git a/Tools/Templates/README.md b/Tools/Templates/README.md
index dc1bb15b68..3f83b42c45 100644
--- a/Tools/Templates/README.md
+++ b/Tools/Templates/README.md
@@ -33,6 +33,39 @@ To use it (before it becomes an Xcode template):
- Import created files in the Xcode project
+# SwiftUISimpleScreenTemplate
+This is the boilerplate to create a simple SwiftUI screen including view model, screen coordinator, unit and UI tests.
+
+To create a screen from this template (before it becomes an Xcode template):
+
+- `./createSwiftUISimpleScreen.sh ScreenFolder MyScreenName`
+- Import created files in the Xcode project
+
+This will create `ScreenFolder` within the `RiotSwiftUI/Modules`. Files inside will be named `MyScreenNameXxx`.
+
+
+# SwiftUISingleScreenTempalte
+This is the boilerplate to create a simple SwiftUI screen including view model, screen coordinator, service, unit and UI tests.
+
+To create a screen from this template (before it becomes an Xcode template):
+
+- `./createSwiftUISingleScreen.sh ScreenFolder MyScreenName`
+- Import created files in the Xcode project
+
+This will create `ScreenFolder` within the `RiotSwiftUI/Modules`. Files inside will be named `MyScreenNameXxx`.
+
+
+# SwiftUITwoScreenTemplate
+This is the boilerplate to create two single SwiftUI screens (including view models, screen coordinators, services, unit and UI tests) and a flow coordinator.
+
+To create screens from this template (before it becomes an Xcode template):
+
+- `./createSwiftUITwoScreen.sh TwoScreenFolder MyRootCoordinator FirstScreenName SecondScreenName`
+- Import created files in the Xcode project
+
+This will create `TwoScreenFolder` within the `RiotSwiftUI/Modules`.
+
+
# Usage example
Following commands:
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 222bd7b9f1..913e18172d 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -21,7 +21,7 @@ platform :ios do
before_all do
# Ensure used Xcode version
- xcversion(version: "~> 13.4")
+ xcversion(version: "~> 14.0.1")
end
#### Public ####
@@ -196,7 +196,7 @@ platform :ios do
run_tests(
workspace: "Riot.xcworkspace",
scheme: "RiotSwiftUITests",
- device: "iPhone 13",
+ device: "iPhone 14",
code_coverage: true,
# Test result configuration
result_bundle: true,
diff --git a/project.yml b/project.yml
index 722cce9725..391e91acc5 100644
--- a/project.yml
+++ b/project.yml
@@ -53,7 +53,7 @@ packages:
branch: main
WysiwygComposer:
url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift
- revision: 11dad16e3e589dba423f6cc5707e9df8aace89b0
+ revision: d5ef7054fb43924d5b92d5d627347ca2bc333717
DeviceKit:
url: https://github.com/devicekit/DeviceKit
majorVersion: 4.7.0