diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml
index f8d03f08b..e4f4671de 100644
--- a/.github/workflows/release-alpha.yml
+++ b/.github/workflows/release-alpha.yml
@@ -13,22 +13,10 @@ env:
MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }}
jobs:
- check-secret:
- runs-on: macos-12
- outputs:
- out-key: ${{ steps.out-key.outputs.defined }}
- steps:
- - id: out-key
- env:
- P12_KEY: ${{ secrets.ALPHA_CERTIFICATES_P12 }}
- P12_PASSWORD_KEY: ${{ secrets.ALPHA_CERTIFICATES_P12 }}
- if: "${{ env.P12_KEY != '' || env.P12_PASSWORD_KEY != '' }}"
- run: echo "::set-output name=defined::true"
build:
- # Run job if secrets are available (not available for forks).
- needs: [check-secret]
+ # Don't run for forks as secrets are unavailable.
if: |
- needs.check-secret.outputs.out-key == 'true' &&
+ github.event.pull_request.head.repo.full_name == github.repository &&
(github.event_name == 'push' ||
(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'Trigger-PR-Build')))
diff --git a/CHANGES.md b/CHANGES.md
index f62115f7c..e4d8ef4d8 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,43 @@
+## Changes in 1.9.16 (2023-01-24)
+
+✨ Features
+
+- Rich Text Composer: Enable bulleted/numbered lists support ([#7238](https://github.com/vector-im/element-ios/issues/7238))
+- Rich Text Composer: Enable quote & code blocks support ([#7271](https://github.com/vector-im/element-ios/issues/7271))
+- Voice Broadcast: When deleting a voice broadcast, all data is now deleted on server side (MSC3912 implementation). ([#7283](https://github.com/vector-im/element-ios/issues/7283))
+
+🙌 Improvements
+
+- Labs: VoiceBroadcast: Handle VoIP buttons when VB is used ([#7225](https://github.com/vector-im/element-ios/pull/7225))
+- Polls: add UI for active poll history. ([#7267](https://github.com/vector-im/element-ios/pull/7267))
+- CryptoSDK: Add labs settings to enable Crypto SDK ([#7272](https://github.com/vector-im/element-ios/pull/7272))
+- Voice Broadcast: Improved detection of voice broadcast completion during playback. ([#7273](https://github.com/vector-im/element-ios/pull/7273))
+- Remove "Leave" button on Room details screen ([#7275](https://github.com/vector-im/element-ios/pull/7275))
+- Polls: poll history UI for past polls. ([#7278](https://github.com/vector-im/element-ios/pull/7278))
+- Polls: render replies to poll events better. ([#7284](https://github.com/vector-im/element-ios/pull/7284))
+- CryptoV2: Display migration progress during startup ([#7286](https://github.com/vector-im/element-ios/pull/7286))
+- Upgrade MatrixSDK version ([v0.24.8](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.8)).
+- Voice broadcast connection error handling while recording. ([#7229](https://github.com/vector-im/element-ios/issues/7229))
+- Handle a connection issue when we try to start a new voice broadcast. ([#7234](https://github.com/vector-im/element-ios/issues/7234))
+- Rich Text Editor: https:// or mailto: scheme is automatically added when creating a link if no scheme is specified. ([#7279](https://github.com/vector-im/element-ios/issues/7279))
+- Rich Text Editor: Adding a link over a blank selection, prompts the user to create a new link with new text to replace such selection. ([#7280](https://github.com/vector-im/element-ios/issues/7280))
+- Voice Broadcast: handle the lost of connectivity with the homeserver while recording. ([#7285](https://github.com/vector-im/element-ios/issues/7285))
+
+🐛 Bugfixes
+
+- Voice Broadcast: The Now Playing Info Center now displays a voice broadcast instead of a voice message when a user is listening to a voice broadcast. ([#7257](https://github.com/vector-im/element-ios/pull/7257))
+- Fix a crash caused by the missing Avatar Service dependency. ([#7268](https://github.com/vector-im/element-ios/pull/7268))
+- The (edited) tag for messages is now light grey like on web and Android. ([#5148](https://github.com/vector-im/element-ios/issues/5148))
+- Live Location Sharing does not work on first selection after granting "Allow always" location permission. ([#7222](https://github.com/vector-im/element-ios/issues/7222))
+- Voice Broadcast: Fixed an issue where the voice broadcast audio player progress bar behaved unexpectedly. ([#7252](https://github.com/vector-im/element-ios/issues/7252))
+- Voice Broadcast: VoiceBroadcast chunks are no longer resent as voice messages ([#7261](https://github.com/vector-im/element-ios/issues/7261))
+- Timeline's links and hyperlinks match now the blue colour of Android and Web. ([#7263](https://github.com/vector-im/element-ios/issues/7263))
+
+🧱 Build
+
+- Fix Element Alpha workflow not being able to run. ([#7256](https://github.com/vector-im/element-ios/pull/7256))
+
+
## Changes in 1.9.15 (2023-01-10)
✨ Features
diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig
index ea1f94f28..7271fabb8 100644
--- a/Config/AppVersion.xcconfig
+++ b/Config/AppVersion.xcconfig
@@ -15,5 +15,5 @@
//
// Version
-MARKETING_VERSION = 1.9.15
-CURRENT_PROJECT_VERSION = 1.9.15
+MARKETING_VERSION = 1.9.16
+CURRENT_PROJECT_VERSION = 1.9.16
diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift
index 2f85f3c13..d4b57871a 100644
--- a/Config/BuildSettings.swift
+++ b/Config/BuildSettings.swift
@@ -399,6 +399,7 @@ final class BuildSettings: NSObject {
// MARK: - Polls
static let pollsEnabled = true
+ static var pollsHistoryEnabled: Bool = false
// MARK: - Location Sharing
diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift
index fee3796ff..f3172a710 100644
--- a/Config/CommonConfiguration.swift
+++ b/Config/CommonConfiguration.swift
@@ -91,6 +91,12 @@ class CommonConfiguration: NSObject, Configurable {
MXKeyProvider.sharedInstance().delegate = EncryptionKeyManager.shared
sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature
+
+ #if DEBUG
+ if sdkOptions.isCryptoSDKAvailable {
+ sdkOptions.enableCryptoSDK = RiotSettings.shared.enableCryptoSDK
+ }
+ #endif
}
private func makeASCIIUserAgent() -> String? {
diff --git a/DesignKit/Source/ColorValues.swift b/DesignKit/Source/ColorValues.swift
index 338d1cfe8..4e967ab05 100644
--- a/DesignKit/Source/ColorValues.swift
+++ b/DesignKit/Source/ColorValues.swift
@@ -48,5 +48,7 @@ public struct ColorValues: Colors {
public let ems: UIColor
+ public let links: UIColor
+
public let namesAndAvatars: [UIColor]
}
diff --git a/DesignKit/Source/Colors.swift b/DesignKit/Source/Colors.swift
index bf3e9abd3..bea9b0706 100644
--- a/DesignKit/Source/Colors.swift
+++ b/DesignKit/Source/Colors.swift
@@ -67,6 +67,10 @@ public protocol Colors {
/// Global color: The EMS brand's purple colour.
var ems: ColorType { get }
+ /// - Links
+ /// - Hyperlinks
+ var links: ColorType { get }
+
/// - Names in chat timeline
/// - Avatars default states that include first name letter
var namesAndAvatars: [ColorType] { get }
diff --git a/DesignKit/Source/ColorsSwiftUI.swift b/DesignKit/Source/ColorsSwiftUI.swift
index ea3ca6779..bb25d025f 100644
--- a/DesignKit/Source/ColorsSwiftUI.swift
+++ b/DesignKit/Source/ColorsSwiftUI.swift
@@ -21,7 +21,7 @@ import SwiftUI
Struct for holding colors for use in SwiftUI.
*/
public struct ColorSwiftUI: Colors {
-
+
public let accent: Color
public let alert: Color
@@ -48,8 +48,10 @@ public struct ColorSwiftUI: Colors {
public var ems: Color
- public let namesAndAvatars: [Color]
+ public let links: Color
+ public let namesAndAvatars: [Color]
+
init(values: ColorValues) {
accent = Color(values.accent)
alert = Color(values.alert)
@@ -64,6 +66,7 @@ public struct ColorSwiftUI: Colors {
navigation = Color(values.navigation)
background = Color(values.background)
ems = Color(values.ems)
+ links = Color(values.links)
namesAndAvatars = values.namesAndAvatars.map({ Color($0) })
}
}
diff --git a/DesignKit/Source/ColorsUIkit.swift b/DesignKit/Source/ColorsUIkit.swift
index 3add385c3..5ca20ab0b 100644
--- a/DesignKit/Source/ColorsUIkit.swift
+++ b/DesignKit/Source/ColorsUIkit.swift
@@ -45,6 +45,8 @@ import UIKit
public let navigation: UIColor
public let background: UIColor
+
+ public let links: UIColor
public let namesAndAvatars: [UIColor]
@@ -61,6 +63,7 @@ import UIKit
tile = values.tile
navigation = values.navigation
background = values.background
+ links = values.links
namesAndAvatars = values.namesAndAvatars
}
}
diff --git a/DesignKit/Variants/Colors/Dark/DarkColors.swift b/DesignKit/Variants/Colors/Dark/DarkColors.swift
index 88bd12ff3..21394475c 100644
--- a/DesignKit/Variants/Colors/Dark/DarkColors.swift
+++ b/DesignKit/Variants/Colors/Dark/DarkColors.swift
@@ -34,6 +34,7 @@ public class DarkColors {
navigation: UIColor(rgb:0x21262C),
background: UIColor(rgb:0x15191E),
ems: UIColor(rgb: 0x7E69FF),
+ links: UIColor(rgb: 0x0086E6),
namesAndAvatars: [
UIColor(rgb:0x368BD6),
UIColor(rgb:0xAC3BA8),
diff --git a/DesignKit/Variants/Colors/Light/LightColors.swift b/DesignKit/Variants/Colors/Light/LightColors.swift
index 93cb3eadb..f8fa0e8e3 100644
--- a/DesignKit/Variants/Colors/Light/LightColors.swift
+++ b/DesignKit/Variants/Colors/Light/LightColors.swift
@@ -35,6 +35,7 @@ public class LightColors {
navigation: UIColor(rgb:0xF4F6FA),
background: UIColor(rgb:0xFFFFFF),
ems: UIColor(rgb: 0x7E69FF),
+ links: UIColor(rgb: 0x0086E6),
namesAndAvatars: [
UIColor(rgb:0x368BD6),
UIColor(rgb:0xAC3BA8),
diff --git a/Podfile b/Podfile
index 02f9e605f..35ba935b2 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.7'
+$matrixSDKVersion = '= 0.24.8'
# $matrixSDKVersion = :local
# $matrixSDKVersion = { :branch => 'develop'}
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }
diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 102f87715..34484b0a7 100644
--- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -23,7 +23,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift",
"state" : {
- "revision" : "534ee5bae5e8de69ed398937b5edb7b5f21551d2"
+ "revision" : "6927cb878376136c4a03d919b689af8dfbdad080",
+ "version" : "0.19.0"
}
},
{
diff --git a/Riot/Assets/Images.xcassets/Composer/code_block.imageset/Contents.json b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/Contents.json
new file mode 100644
index 000000000..bcb234ccc
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "code_block.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "code_block@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "code_block@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block.png b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block.png
new file mode 100644
index 000000000..a70342a59
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block.png differ
diff --git a/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@2x.png b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@2x.png
new file mode 100644
index 000000000..8b69b41cb
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@2x.png differ
diff --git a/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@3x.png b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@3x.png
new file mode 100644
index 000000000..21bd83678
Binary files /dev/null and b/Riot/Assets/Images.xcassets/Composer/code_block.imageset/code_block@3x.png differ
diff --git a/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/Contents.json
new file mode 100644
index 000000000..fcc0b5765
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "pollHistory.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/pollHistory.svg b/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/pollHistory.svg
new file mode 100644
index 000000000..a0243252c
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/Room/pollHistory.imageset/pollHistory.svg
@@ -0,0 +1,3 @@
+
diff --git a/Riot/Assets/ar.lproj/Vector.strings b/Riot/Assets/ar.lproj/Vector.strings
index a9a7b2cb6..7810fc3cc 100644
--- a/Riot/Assets/ar.lproj/Vector.strings
+++ b/Riot/Assets/ar.lproj/Vector.strings
@@ -455,7 +455,7 @@
"sign_up" = "الاِشتِراك";
"dismiss" = "إبعَاد";
"discard" = "اِستِبعاد";
-"abort" = "إِجهَاض";
+"abort" = "إنهاء";
"yes" = "نَعَم";
// Action
@@ -1078,3 +1078,8 @@
/* The placeholder will show the email address that was entered. */
"authentication_verify_email_waiting_message" = "اتبع التعليمات المرسلة إلى %@";
"invite_to" = "الدعوة إلى %@";
+"password_policy_pwd_in_dict_error" = "تم العثور على كلمة المرور هذه في القاموس لدينا، وهي كلمة مرور غير مسموح في استخدامها.";
+
+// Others
+"or" = "أو";
+"accessibility_selected" = "تم تحديده";
diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings
index f1886a04c..bb52fc5ea 100644
--- a/Riot/Assets/de.lproj/Vector.strings
+++ b/Riot/Assets/de.lproj/Vector.strings
@@ -2700,5 +2700,34 @@
"notice_voice_broadcast_live" = "Echtzeitübertragung";
"user_other_session_security_recommendation_title" = "Andere Sitzungen";
"voice_message_broadcast_in_progress_title" = "Kann Sprachnachricht nicht beginnen";
-"poll_timeline_decryption_error" = "Aufgrund von Entschlüsselungsfehlern könnten einige Stimmen nicht gezählt werden";
-"voice_message_broadcast_in_progress_message" = "Du kannst kein Gespräch beginnen, da du im Moment eine Sprachübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen";
+"poll_timeline_decryption_error" = "Evtl. werden infolge von Entschlüsselungsfehlern einige Stimmen nicht gezählt";
+"voice_message_broadcast_in_progress_message" = "Du kannst keine Sprachnachricht beginnen, da du im Moment eine Echtzeitübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen";
+"poll_timeline_ended_text" = "Umfrage beendet";
+"voice_broadcast_voip_cannot_start_description" = "Du kannst keinen Anruf beginnen, da du im Moment eine Sprachübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen.";
+"voice_broadcast_voip_cannot_start_title" = "Kann keinen Anruf beginnen";
+"poll_history_no_past_poll_text" = "In diesem Raum gibt es keine abgeschlossenen Umfragen";
+"poll_history_no_active_poll_text" = "In diesem Raum gibt es keine aktiven Umfragen";
+"poll_history_past_segment_title" = "Vergangene Umfragen";
+"poll_history_active_segment_title" = "Aktive Umfragen";
+
+// MARK: - Polls history
+
+"poll_history_title" = "Umfrageverlauf";
+"room_details_polls" = "Umfrageverlauf";
+"accessibility_selected" = "ausgewählt";
+"voice_broadcast_playback_lock_screen_placeholder" = "Sprachübertragung";
+"voice_broadcast_connection_error_message" = "Leider ist es aktuell nicht möglich, eine Aufnahme zu beginnen. Bitte versuche es später erneut.";
+"voice_broadcast_connection_error_title" = "Verbindungsfehler";
+"wysiwyg_composer_format_action_code_block" = "Quelltextblock umschalten";
+"wysiwyg_composer_format_action_quote" = "Zitat umschalten";
+"wysiwyg_composer_format_action_ordered_list" = "Nummerierte Liste umschalten";
+"wysiwyg_composer_format_action_unordered_list" = "Unsortierte Liste umschalten";
+"voice_broadcast_recorder_connection_error" = "Verbindungsfehler – Aufzeichnung pausiert";
+"poll_timeline_reply_ended_poll" = "Beendete Umfrage";
+
+// MARK: - Launch loading
+
+"launch_loading_migrating_data" = "Migriere Daten\n%@ %%";
+"settings_labs_disable_crypto_sdk" = "Krypto-SDK ist aktiviert. Zum Deaktivieren, bitte die App neu installieren";
+"settings_labs_confirm_crypto_sdk" = "Dies kann nicht rückgängig gemacht werden";
+"settings_labs_enable_crypto_sdk" = "Rust-basiertes Krypto-SDK aktivieren";
diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings
index 04d0cd693..5d7ac9e4a 100644
--- a/Riot/Assets/en.lproj/Vector.strings
+++ b/Riot/Assets/en.lproj/Vector.strings
@@ -96,6 +96,7 @@
// Accessibility
"accessibility_checkbox_label" = "checkbox";
"accessibility_button_label" = "button";
+"accessibility_selected" = "selected";
// MARK: Onboarding
"onboarding_splash_register_button_title" = "Create account";
@@ -803,6 +804,9 @@ Tap the + to start adding people.";
"settings_labs_enable_new_app_layout" = "New Application Layout";
"settings_labs_enable_wysiwyg_composer" = "Try out the rich text editor";
"settings_labs_enable_voice_broadcast" = "Voice broadcast";
+"settings_labs_enable_crypto_sdk" = "Enable new rust-based Crypto SDK";
+"settings_labs_confirm_crypto_sdk" = "This action cannot be undone";
+"settings_labs_disable_crypto_sdk" = "Crypto SDK is enabled. To disable please reinstall the app";
"settings_version" = "Version %@";
"settings_olm_version" = "Olm Version %@";
@@ -982,6 +986,7 @@ Tap the + to start adding people.";
"room_details_title_for_dm" = "Details";
"room_details_people" = "Members";
"room_details_files" = "Uploads";
+"room_details_polls" = "Poll history";
"room_details_search" = "Search room";
"room_details_integrations" = "Integrations";
"room_details_settings" = "Settings";
@@ -1522,7 +1527,7 @@ Tap the + to start adding people.";
"device_verification_cancelled_by_me" = "The verification has been cancelled. Reason: %@";
"device_verification_error_cannot_load_device" = "Cannot load session information.";
-// Mark: Incoming
+// MARK: Incoming
"device_verification_incoming_title" = "Incoming Verification Request";
"device_verification_incoming_description_1" = "Verify this session to mark it as trusted. Trusting sessions of partners gives you extra peace of mind when using end-to-end encrypted messages.";
"device_verification_incoming_description_2" = "Verifying this session will mark it as trusted, and also mark your session as trusted to the partner.";
@@ -1974,6 +1979,7 @@ Tap the + to start adding people.";
// MARK: - Launch loading
+"launch_loading_migrating_data" = "Migrating data\n%@ %%";
"launch_loading_server_syncing" = "Syncing with the server";
"launch_loading_server_syncing_nth_attempt" = "Syncing with the server\n(%@ attempt)";
"launch_loading_processing_response" = "Processing data\n%@ %%";
@@ -2011,12 +2017,12 @@ Tap the + to start adding people.";
"share_invite_link_room_text" = "Hey, join this room on %@";
"share_invite_link_space_text" = "Hey, join this space on %@";
-// Mark: - Room avatar view
+// MARK: - Room avatar view
"room_avatar_view_accessibility_label" = "avatar";
"room_avatar_view_accessibility_hint" = "Change room avatar";
-// Mark: - Room creation introduction cell
+// MARK: - Room creation introduction cell
"room_intro_cell_add_participants_action" = "Add people";
@@ -2033,7 +2039,7 @@ Tap the + to start adding people.";
"room_intro_cell_information_dm_sentence2" = "Only the two of you are in this conversation, no one else can join.";
"room_intro_cell_information_multiple_dm_sentence2" = "Only you are in this conversation, unless any of you invites someone to join.";
-// Mark: - Room invite
+// MARK: - Room invite
"room_invite_to_space_option_title" = "To %@";
"room_invite_to_space_option_detail" = "They can explore %@, but won’t be a member of %@.";
@@ -2042,7 +2048,7 @@ Tap the + to start adding people.";
"room_invite_not_enough_permission" = "You do not have permission to invite people to this room";
"space_invite_not_enough_permission" = "You do not have permission to invite people to this space";
-// Mark: - Spaces
+// MARK: - Spaces
"space_feature_unavailable_title" = "Spaces aren’t here yet";
"space_feature_unavailable_subtitle" = "Spaces aren't on iOS yet, but you can use them now on Web and Desktop";
@@ -2099,7 +2105,7 @@ Tap the + to start adding people.";
"spaces_feature_not_available" = "This feature isn't available here. For now, you can do this with %@ on your computer.";
-// Mark: - Space Creation
+// MARK: - Space Creation
"spaces_creation_hint" = "Spaces are a new way to group rooms and people.";
"spaces_creation_visibility_title" = "What type of space do you want to create?";
@@ -2158,7 +2164,7 @@ Tap the + to start adding people.";
"spaces_add_room_missing_permission_message" = "You do not have permissions to add rooms to this space.";
-// Mark: Leave space
+// MARK: Leave space
"leave_space_action" = "Leave space";
"leave_space_and_one_room" = "Leave space and 1 room";
@@ -2171,17 +2177,17 @@ Tap the + to start adding people.";
"room_event_action_reaction_more" = "%@ more";
-// Mark: Avatar
+// MARK: Avatar
"space_avatar_view_accessibility_label" = "avatar";
"space_avatar_view_accessibility_hint" = "Change space avatar";
-// Mark: - User avatar view
+// MARK: - User avatar view
"user_avatar_view_accessibility_label" = "avatar";
"user_avatar_view_accessibility_hint" = "Change user avatar";
-// Mark: - Side menu
+// MARK: - Side menu
"side_menu_reveal_action_accessibility_label" = "Left panel";
"side_menu_action_invite_friends" = "Invite friends";
@@ -2191,7 +2197,7 @@ Tap the + to start adding people.";
"side_menu_app_version" = "Version %@";
"side_menu_coach_message" = "Swipe right or tap to see all rooms";
-// Mark: - Voice Messages
+// MARK: - Voice Messages
"voice_message_release_to_send" = "Hold to record, release to send";
"voice_message_remaining_recording_time" = "%@s left";
@@ -2200,12 +2206,13 @@ Tap the + to start adding people.";
"voice_message_broadcast_in_progress_title" = "Can't start voice message";
"voice_message_broadcast_in_progress_message" = "You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message";
-// Mark: - Voice broadcast
+// 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.";
+"voice_broadcast_playback_lock_screen_placeholder" = "Voice broadcast";
"voice_broadcast_live" = "Live";
"voice_broadcast_tile" = "Voice broadcast";
"voice_broadcast_time_left" = "%@ left";
@@ -2213,8 +2220,13 @@ Tap the + to start adding people.";
"voice_broadcast_stop_alert_title" = "Stop live broadcasting?";
"voice_broadcast_stop_alert_description" = "Are you sure you want to stop your live broadcast? This will end the broadcast, and the full recording will be available in the room.";
"voice_broadcast_stop_alert_agree_button" = "Yes, stop";
+"voice_broadcast_voip_cannot_start_title" = "Can’t start a call";
+"voice_broadcast_voip_cannot_start_description" = "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.";
+"voice_broadcast_connection_error_title" = "Connection error";
+"voice_broadcast_connection_error_message" = "Unfortunately we’re unable to start a recording right now. Please try again later.";
+"voice_broadcast_recorder_connection_error" = "Connection error - Recording paused";
-// Mark: - Version check
+// MARK: - Version check
"version_check_banner_title_supported" = "We’re ending support for iOS %@";
"version_check_banner_subtitle_supported" = "We will soon be ending support for %@ on iOS %@. To continue using %@ to its full potential, we advise you to upgrade your version of iOS.";
@@ -2230,7 +2242,7 @@ Tap the + to start adding people.";
"version_check_modal_subtitle_deprecated" = "We've been working on enhancing %@ for a faster and more polished experience. Unfortunately your current version of iOS is not compatible with some of those fixes and is no longer supported.\nWe're advising you to upgrade your operating system to use %@ to its full potential.";
"version_check_modal_action_title_deprecated" = "Find out how";
-// Mark: - All Chats
+// MARK: - All Chats
"all_chats_title" = "All chats";
"all_chats_section_title" = "Chats";
@@ -2274,12 +2286,12 @@ Tap the + to start adding people.";
"all_chats_onboarding_title" = "What's new";
"all_chats_onboarding_try_it" = "Try it out";
-// Mark: - Room invites
+// MARK: - Room invites
"room_invites_empty_view_title" = "Nothing new.";
"room_invites_empty_view_information" = "This is where your invites appear.";
-// Mark: - Space Selector
+// MARK: - Space Selector
"space_selector_title" = "My spaces";
"space_selector_empty_view_title" = "No spaces yet.";
@@ -2289,7 +2301,15 @@ Tap the + to start adding people.";
"space_detail_nav_title" = "Space detail";
"space_invite_nav_title" = "Space invite";
-// Mark: - Polls
+// MARK: - Polls history
+
+"poll_history_title" = "Poll history";
+"poll_history_active_segment_title" = "Active polls";
+"poll_history_past_segment_title" = "Past polls";
+"poll_history_no_active_poll_text" = "There are no active polls in this room";
+"poll_history_no_past_poll_text" = "There are no past polls in this room";
+
+// MARK: - Polls
"poll_edit_form_create_poll" = "Create poll";
@@ -2353,6 +2373,8 @@ Tap the + to start adding people.";
"poll_timeline_ended_text" = "Ended the poll";
+"poll_timeline_reply_ended_poll" = "Ended poll";
+
// MARK: - Location sharing
"location_sharing_title" = "Location";
@@ -2538,7 +2560,7 @@ To enable access, tap Settings> Location and select Always";
"user_session_overview_session_details_button_title" = "Session details";
-// Mark: - WYSIWYG Composer
+// MARK: - WYSIWYG Composer
// Send Media Actions
"wysiwyg_composer_start_action_media_picker" = "Photo Library";
@@ -2557,6 +2579,12 @@ To enable access, tap Settings> Location and select Always";
"wysiwyg_composer_format_action_strikethrough" = "Apply underline format";
"wysiwyg_composer_format_action_link" = "Apply link format";
"wysiwyg_composer_format_action_inline_code" = "Apply inline code format";
+"wysiwyg_composer_format_action_unordered_list" = "Toggle bulleted list";
+"wysiwyg_composer_format_action_ordered_list" = "Toggle numbered list";
+"wysiwyg_composer_format_action_code_block" = "Toggle code block";
+"wysiwyg_composer_format_action_quote" = "Toggle quote";
+
+
// Links
"wysiwyg_composer_link_action_text" = "Text";
diff --git a/Riot/Assets/et.lproj/Localizable.strings b/Riot/Assets/et.lproj/Localizable.strings
index 9f1002800..b3a015217 100644
--- a/Riot/Assets/et.lproj/Localizable.strings
+++ b/Riot/Assets/et.lproj/Localizable.strings
@@ -118,3 +118,6 @@
/* New file message from a specific person, not referencing a room. */
"LOCATION_FROM_USER" = "%@ jagas oma asukohta";
+
+/* New voice broadcast from a specific person, not referencing a room. */
+"VOICE_BROADCAST_FROM_USER" = "%@ alustas ringhäälingukõnet";
diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings
index d84b26c76..71ea69ebf 100644
--- a/Riot/Assets/et.lproj/Vector.strings
+++ b/Riot/Assets/et.lproj/Vector.strings
@@ -2640,3 +2640,32 @@
"poll_timeline_decryption_error" = "Krüptimisvigade tõttu jääb osa hääli lugemata";
"voice_message_broadcast_in_progress_title" = "Häälsõnumi salvestamine või esitamine ei õnnestu";
"voice_message_broadcast_in_progress_message" = "Kuna sa hetkel salvestad ringhäälingukõnet, siis häälsõnumi salvestamine või esitamine ei õnnestu. Selleks palun lõpeta ringhäälingukõne";
+"poll_timeline_ended_text" = "Küsitlus on lõppenud";
+"voice_broadcast_voip_cannot_start_description" = "Kuna sa hetkel salvestad ringhäälingukõnet, siis tavakõne algatamine ei õnnestu. Kõne alustamiseks palun lõpeta ringhäälingukõne.";
+"voice_broadcast_voip_cannot_start_title" = "Kõne algatamine ei õnnestu";
+"poll_history_no_past_poll_text" = "Selles jututoas pole varasemaid küsitlusi";
+"poll_history_no_active_poll_text" = "Selles jututoas pole käimasolevaid küsitlusi";
+"poll_history_past_segment_title" = "Varasemad küsitlused";
+"poll_history_active_segment_title" = "Käimasolevad küsitlused";
+
+// MARK: - Polls history
+
+"poll_history_title" = "Küsitluste ajalugu";
+"room_details_polls" = "Küsitluste ajalugu";
+"accessibility_selected" = "valitud";
+"voice_broadcast_playback_lock_screen_placeholder" = "Ringhäälingukõne";
+"voice_broadcast_connection_error_message" = "Kahjuks me ei saa hetkel salvestamist alustada. Palun proovi hiljem uuesti.";
+"voice_broadcast_connection_error_title" = "Ühenduse viga";
+"wysiwyg_composer_format_action_quote" = "Lülita tsiteerimine sisse/välja";
+"wysiwyg_composer_format_action_code_block" = "Lülita koodiblokk sisse/välja";
+"wysiwyg_composer_format_action_ordered_list" = "Lülita nummerdatud loend sisse/välja";
+"wysiwyg_composer_format_action_unordered_list" = "Lülita täpploend sisse/välja";
+"voice_broadcast_recorder_connection_error" = "Viga võrguühenduses - salvestamine on peatatud";
+"poll_timeline_reply_ended_poll" = "Lõppenud küsitlus";
+
+// MARK: - Launch loading
+
+"launch_loading_migrating_data" = "Tõstame andmeid ümber\n%@ %%";
+"settings_labs_disable_crypto_sdk" = "Uus Crypto SDK on kasutusel. Tema väljalülitamiseks palun paigalda rakendus uuesti";
+"settings_labs_confirm_crypto_sdk" = "Seda toimingut ei saa tagasi pöörata";
+"settings_labs_enable_crypto_sdk" = "Võta kasutusele uus Rust-keelel põhinev Crypto SDK";
diff --git a/Riot/Assets/hu.lproj/Localizable.strings b/Riot/Assets/hu.lproj/Localizable.strings
index 82161237e..fee155510 100644
--- a/Riot/Assets/hu.lproj/Localizable.strings
+++ b/Riot/Assets/hu.lproj/Localizable.strings
@@ -120,3 +120,6 @@
/* New video message from a specific person, not referencing a room. */
"VIDEO_FROM_USER" = "%@ videót küldött";
+
+/* New voice broadcast from a specific person, not referencing a room. */
+"VOICE_BROADCAST_FROM_USER" = "%@ hang közvetítést indított";
diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings
index 53debbedb..6dd053e54 100644
--- a/Riot/Assets/hu.lproj/Vector.strings
+++ b/Riot/Assets/hu.lproj/Vector.strings
@@ -2688,3 +2688,25 @@
"poll_timeline_decryption_error" = "Visszafejtési hibák miatt néhány szavazat nem kerül beszámításra";
"voice_message_broadcast_in_progress_message" = "Nem lehet hang üzenetet indítani élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hang üzenet indításához";
"voice_message_broadcast_in_progress_title" = "Hang üzenetet nem lehet elindítani";
+"poll_timeline_ended_text" = "Szavazás vége";
+"voice_broadcast_voip_cannot_start_description" = "Nem lehet hívást kezdeményezni élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hívás indításához.";
+"voice_broadcast_voip_cannot_start_title" = "Nem sikerült hívást indítani";
+"poll_history_no_past_poll_text" = "Nincsenek régi szavazások ebben a szobában";
+"poll_history_no_active_poll_text" = "Nincsenek aktív szavazások ebben a szobában";
+"poll_history_past_segment_title" = "Régi szavazások";
+"poll_history_active_segment_title" = "Aktív szavazások";
+
+// MARK: - Polls history
+
+"poll_history_title" = "Szavazás alakulása";
+"room_details_polls" = "Szavazás alakulása";
+"accessibility_selected" = "kiválasztva";
+"wysiwyg_composer_format_action_quote" = "Idézet be/ki";
+"wysiwyg_composer_format_action_code_block" = "Kód blokk be/ki";
+"wysiwyg_composer_format_action_ordered_list" = "Számozott lista ki-,bekapcsolása";
+"wysiwyg_composer_format_action_unordered_list" = "Lista ki-,bekapcsolása";
+"poll_timeline_reply_ended_poll" = "Lezárt szavazások";
+"voice_broadcast_recorder_connection_error" = "Kapcsolódási hiba – Felvétel szüneteltetve";
+"voice_broadcast_connection_error_message" = "Sajnos most nem lehet elindítani a felvételt. Próbálja meg később.";
+"voice_broadcast_connection_error_title" = "Kapcsolat hiba";
+"voice_broadcast_playback_lock_screen_placeholder" = "Hang közvetítés";
diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings
index d6449680c..508d3eb7d 100644
--- a/Riot/Assets/id.lproj/Vector.strings
+++ b/Riot/Assets/id.lproj/Vector.strings
@@ -2893,5 +2893,34 @@
"notice_voice_broadcast_live" = "Siaran langsung";
"user_other_session_security_recommendation_title" = "Sesi lainnya";
"poll_timeline_decryption_error" = "Karena kesalahan enkripsi, beberapa suara mungkin tidak terhitung";
-"voice_message_broadcast_in_progress_message" = "Anda tidak dapat memulai sebuah pesan suara selagi Anda merekam sebuah siaran langsung. Silakan mengakhiri siaran langsung Anda untuk memulai merekam sebuah pesan suara";
+"voice_message_broadcast_in_progress_message" = "Anda tidak dapat memulai sebuah pesan suara karena Anda saat ini merekam sebuah siaran langsung. Silakan mengakhiri siaran langsung Anda untuk memulai merekam sebuah pesan suara";
"voice_message_broadcast_in_progress_title" = "Tidak dapat memulai pesan suara";
+"poll_timeline_ended_text" = "Mengakhiri pemungutan suara";
+"voice_broadcast_voip_cannot_start_description" = "Anda tidak dapat memulai sebuah panggilan karena Anda saat ini merekam sebuah siaran langsung. Mohon akhiri siaran langsung Anda untuk memulai sebuah panggilan.";
+"voice_broadcast_voip_cannot_start_title" = "Tidak dapat memulai sebuah panggilan";
+"poll_history_no_past_poll_text" = "Tidak ada pemungutan suara masa lalu di ruangan ini";
+"poll_history_no_active_poll_text" = "Tidak ada pemungutan suara yang aktifk di ruangan ini";
+"poll_history_past_segment_title" = "Pemungutan suara sebelumnya";
+"poll_history_active_segment_title" = "Pemungutan suara aktif";
+
+// MARK: - Polls history
+
+"poll_history_title" = "Riwayat pemungutan suara";
+"room_details_polls" = "Riwayat pemungutan suara";
+"accessibility_selected" = "dipilih";
+"voice_broadcast_playback_lock_screen_placeholder" = "Siaran suara";
+"wysiwyg_composer_format_action_quote" = "Saklar kutipan";
+"wysiwyg_composer_format_action_code_block" = "Saklar blok kode";
+"wysiwyg_composer_format_action_ordered_list" = "Saklar daftar bernomor";
+"wysiwyg_composer_format_action_unordered_list" = "Saklar daftar bulat";
+"voice_broadcast_connection_error_message" = "Sayangnya kami tidak dapat memulai sebuah rekaman saat ini. Silakan coba lagi nanti.";
+"voice_broadcast_connection_error_title" = "Kesalahan koneksi";
+"voice_broadcast_recorder_connection_error" = "Kesalahan koneksi - Perekaman dijeda";
+"poll_timeline_reply_ended_poll" = "Pemungutan suara berakhir";
+
+// MARK: - Launch loading
+
+"launch_loading_migrating_data" = "Memigrasikan data\n%@ %%";
+"settings_labs_disable_crypto_sdk" = "SDK Kripto diaktifkan. Untuk menonaktifkan, mohon memasang ulang aplikasi";
+"settings_labs_confirm_crypto_sdk" = "Tindakan ini tidak dapat diurungkan";
+"settings_labs_enable_crypto_sdk" = "Aktifkan SDK Kripto baru berbasis Rust";
diff --git a/Riot/Assets/is.lproj/Localizable.strings b/Riot/Assets/is.lproj/Localizable.strings
index ec3fdaace..d47171514 100644
--- a/Riot/Assets/is.lproj/Localizable.strings
+++ b/Riot/Assets/is.lproj/Localizable.strings
@@ -170,3 +170,6 @@
/* Look, stuff's happened, alright? Just open the app. */
"MSGS_IN_TWO_PLUS_ROOMS" = "%@ ný skilaboð í %@, %@ og fleirum";
+
+/* New voice broadcast from a specific person, not referencing a room. */
+"VOICE_BROADCAST_FROM_USER" = "%@ byrjaði talútsendingu";
diff --git a/Riot/Assets/is.lproj/Vector.strings b/Riot/Assets/is.lproj/Vector.strings
index aba02e2d6..10722ec1b 100644
--- a/Riot/Assets/is.lproj/Vector.strings
+++ b/Riot/Assets/is.lproj/Vector.strings
@@ -1283,7 +1283,7 @@
"settings_add_3pid_invalid_password_message" = "Ógild auðkenni";
"settings_add_3pid_password_title_msidsn" = "Bæta við símanúmeri";
"settings_add_3pid_password_title_email" = "Bæta við tölvupóstfangi";
-"settings_labs_enable_threads" = "Skilaboð í spjallþráðum";
+"settings_labs_enable_threads" = "Spjallþræðir skilaboða";
"settings_labs_enabled_polls" = "Kannanir";
"settings_integrations_allow_button" = "Sýsla með samþættingar";
"settings_new_keyword" = "Bæta við nýju stikkorði";
@@ -2317,7 +2317,7 @@
"device_name_mobile" = "%@ fyrir farsíma";
"device_name_web" = "%@ á vefnum";
"device_name_desktop" = "%@ fyrir einkatölvur";
-"user_session_item_details" = "%@ · Síðasta virkni %@";
+"user_session_item_details" = "%1$@ · %2$@";
"location_sharing_live_loading" = "Hleð inn rauntímastaðsetningu...";
"location_sharing_live_list_item_time_left" = "%@ fór";
"location_sharing_map_credits_title" = "© Höfundarréttur";
@@ -2368,3 +2368,119 @@
// MARK: Authentication
"authentication_registration_title" = "Búðu til aðganginn þinn";
+"notice_voice_broadcast_ended_by_you" = "Þú endaðir talútsendingu.";
+"notice_voice_broadcast_ended" = "%@ endaði talútsendingu.";
+"notice_voice_broadcast_live" = "Bein útsending";
+"deselect_all" = "Afvelja allt";
+"wysiwyg_composer_link_action_edit_title" = "Breyta tengli";
+"wysiwyg_composer_link_action_create_title" = "Búa til tengil";
+"wysiwyg_composer_link_action_link" = "Tengill";
+
+
+
+// Links
+"wysiwyg_composer_link_action_text" = "Texti";
+"wysiwyg_composer_start_action_voice_broadcast" = "Útvörpun tals";
+"wysiwyg_composer_start_action_text_formatting" = "Sníðing texta";
+"wysiwyg_composer_start_action_camera" = "Myndavél";
+"wysiwyg_composer_start_action_location" = "Staðsetning";
+"wysiwyg_composer_start_action_polls" = "Kannanir";
+"wysiwyg_composer_start_action_attachments" = "Viðhengi";
+"wysiwyg_composer_start_action_stickers" = "Límmerki";
+
+
+// MARK: - WYSIWYG Composer
+
+// Send Media Actions
+"wysiwyg_composer_start_action_media_picker" = "Ljósmyndasafn";
+"user_session_overview_session_details_button_title" = "Nánar um setuna";
+"user_session_overview_session_title" = "Seta";
+"user_session_overview_current_session_title" = "Núverandi seta";
+"user_session_details_application_url" = "Slóð (URL)";
+"user_session_details_application_version" = "Útgáfa";
+"user_session_details_application_name" = "Heiti";
+"user_session_details_device_os" = "Stýrikerfi";
+"user_session_details_device_browser" = "Vafri";
+"user_session_details_device_model" = "Gerð";
+"user_session_details_device_ip_location" = "Staðsetning IP-vistfangs";
+"user_session_details_device_ip_address" = "IP-vistfang";
+"user_session_details_last_activity" = "Síðasta virkni";
+"user_session_details_session_id" = "Auðkenni setu";
+"user_session_details_session_name" = "Nafn á setu";
+"user_session_details_device_section_header" = "Tæki";
+"user_session_details_application_section_header" = "Forrit";
+"user_session_details_session_section_header" = "Seta";
+"user_session_details_title" = "Nánar um setuna";
+"device_type_name_unknown" = "Óþekkt";
+"device_type_name_mobile" = "Farsími";
+"device_type_name_web" = "Vefur";
+"device_type_name_desktop" = "Borðtölva";
+"user_other_session_selected_count" = "%@ valið";
+"user_other_session_clear_filter" = "Hreinsa síu";
+"user_other_session_no_unverified_sessions" = "Engar óstaðfestar setur fundust.";
+"user_other_session_no_verified_sessions" = "Engar staðfestar setur fundust.";
+"user_other_session_no_inactive_sessions" = "Engar óvirkar setur fundust.";
+"user_other_session_filter_menu_inactive" = "Óvirkt";
+"user_other_session_filter_menu_unverified" = "Óstaðfestar";
+"user_other_session_filter_menu_verified" = "Staðfestar";
+"user_other_session_filter_menu_all" = "Allar setur";
+"user_other_session_filter" = "Sía";
+"user_other_session_security_recommendation_title" = "Aðrar setur";
+"user_session_inactive_session_title" = "Óvirkar setur";
+"user_session_unverified_session_title" = "Óstaðfest seta";
+"user_session_verified_session_title" = "Sannreyndar setur";
+"user_session_got_it" = "Náði því";
+"user_session_push_notifications" = "Ýti-tilkynningar";
+"user_session_verification_unknown_short" = "Óþekkt";
+"user_session_verification_unknown" = "Óþekkt staða sannvottunar";
+"user_sessions_view_all_action" = "Skoða öll (%d)";
+"user_sessions_overview_link_device" = "Tengja tæki";
+"user_sessions_overview_current_session_section_title" = "Núverandi seta";
+"user_sessions_hide_location_info" = "Fela IP-vistfang";
+"user_sessions_show_location_info" = "Birta IP-vistfang";
+"user_sessions_overview_other_sessions_section_title" = "Aðrar setur";
+"user_sessions_overview_security_recommendations_inactive_title" = "Óvirkar setur";
+"user_sessions_overview_security_recommendations_unverified_title" = "Óstaðfestar setur";
+"user_sessions_overview_security_recommendations_section_title" = "Ráðleggingar varðandi öryggi";
+
+// MARK: User sessions management
+
+// Parameter is the application display name (e.g. "Element")
+"user_sessions_default_session_display_name" = "%@ iOS";
+"location_sharing_live_lab_promotion_activation" = "Virkja deilingu rauntímastaðsetninga";
+"location_sharing_live_timer_incoming" = "Í beinni til %@";
+"poll_timeline_reply_ended_poll" = "Lauk könnun";
+"poll_timeline_ended_text" = "Lauk könnuninni";
+"poll_history_past_segment_title" = "Fyrri kannanir";
+"poll_history_active_segment_title" = "Virkar kannanir";
+
+// MARK: - Polls history
+
+"poll_history_title" = "Breytingaskrá könnunar";
+"all_chats_user_menu_accessibility_label" = "Valmynd notandans";
+"voice_broadcast_connection_error_title" = "Villa í tengingu";
+"voice_broadcast_voip_cannot_start_title" = "Get ekki hafið símtal";
+"voice_broadcast_stop_alert_agree_button" = "Já, stöðva";
+"voice_broadcast_buffering" = "Hleð í biðminni...";
+"voice_broadcast_time_left" = "%@ eftir";
+"voice_broadcast_tile" = "Útvörpun tals";
+"voice_broadcast_live" = "Beint";
+"voice_broadcast_playback_lock_screen_placeholder" = "Útvörpun tals";
+
+// Unverified sessions
+"key_verification_alert_title" = "Þú ert með óstaðfestar setur";
+"sign_out_confirmation_message" = "Ertu viss um að þú viljir skrá þig út?";
+
+// MARK: Sign out warning
+
+"sign_out" = "Skrá út";
+"secure_key_backup_setup_cancel_alert_message" = "Ef þú hættir við núna, geturðu tapað dulrituðum skilaboðum og gögnum ef þú missir aðgang að innskráningum þínum.\n\nÞú getur víka sett upp örugga afritun og sýslað með dulritunarlyklana þína í stillingunum.";
+"room_details_polls" = "Breytingaskrá könnunar";
+"manage_session_sign_out_other_sessions" = "Skrá út úr öllum öðrum setum";
+"manage_session_rename" = "Endurnefna setu";
+"settings_labs_enable_voice_broadcast" = "Útvörpun tals";
+"authentication_qr_login_failure_retry" = "Reyna aftur";
+"authentication_qr_login_loading_connecting_device" = "Tengist við tæki";
+"authentication_qr_login_scan_title" = "Skanna QR-kóða";
+"authentication_qr_login_start_title" = "Skanna QR-kóða";
+"authentication_login_with_qr" = "Skrá inn með QR-kóða";
diff --git a/Riot/Assets/it.lproj/Localizable.strings b/Riot/Assets/it.lproj/Localizable.strings
index bb8b3e707..3232ff2a4 100644
--- a/Riot/Assets/it.lproj/Localizable.strings
+++ b/Riot/Assets/it.lproj/Localizable.strings
@@ -118,3 +118,6 @@
/* New file message from a specific person, not referencing a room. */
"LOCATION_FROM_USER" = "%@ ha condiviso la sua posizione";
+
+/* New voice broadcast from a specific person, not referencing a room. */
+"VOICE_BROADCAST_FROM_USER" = "%@ ha iniziato una trasmissione vocale";
diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings
index 35bd94c35..8dbc7f07a 100644
--- a/Riot/Assets/it.lproj/Vector.strings
+++ b/Riot/Assets/it.lproj/Vector.strings
@@ -2665,3 +2665,28 @@
"notice_voice_broadcast_live" = "Trasmissione in diretta";
"wysiwyg_composer_format_action_inline_code" = "Applica formato codice interlinea";
"user_other_session_security_recommendation_title" = "Altre sessioni";
+"poll_timeline_ended_text" = "Sondaggio terminato";
+"poll_timeline_decryption_error" = "A causa di errori di decifrazione, alcuni voti potrebbero non venire contati";
+"voice_broadcast_voip_cannot_start_description" = "Non puoi avviare una chiamata perché stai registrando una trasmissione in diretta. Termina la trasmissione per potere iniziare una chiamata.";
+"voice_broadcast_voip_cannot_start_title" = "Impossibile avviare una chiamata";
+"voice_message_broadcast_in_progress_title" = "Impossibile iniziare il messaggio vocale";
+"voice_message_broadcast_in_progress_message" = "Non puoi iniziare un messaggio vocale perché stai registrando una trasmissione in diretta. Termina la trasmissione per potere iniziare un messaggio vocale";
+"poll_history_no_past_poll_text" = "In questa stanza non ci sono sondaggi passati";
+"poll_history_no_active_poll_text" = "In questa stanza non ci sono sondaggi attivi";
+"poll_history_past_segment_title" = "Sondaggi passati";
+"poll_history_active_segment_title" = "Sondaggi attivi";
+
+// MARK: - Polls history
+
+"poll_history_title" = "Cronologia sondaggi";
+"voice_broadcast_playback_lock_screen_placeholder" = "Trasmissione vocale";
+"room_details_polls" = "Cronologia sondaggi";
+"accessibility_selected" = "selezionato";
+"wysiwyg_composer_format_action_quote" = "Attiva/disattiva citazione";
+"wysiwyg_composer_format_action_code_block" = "Attiva/disattiva blocco di codice";
+"wysiwyg_composer_format_action_ordered_list" = "Attiva/disattiva elenco numerato";
+"wysiwyg_composer_format_action_unordered_list" = "Attiva/disattiva elenco puntato";
+"poll_timeline_reply_ended_poll" = "Sondaggio terminato";
+"voice_broadcast_recorder_connection_error" = "Errore di connessione - Registrazione in pausa";
+"voice_broadcast_connection_error_message" = "Sfortunatamente non riusciamo ad iniziare una registrazione al momento. Riprova più tardi.";
+"voice_broadcast_connection_error_title" = "Errore di connessione";
diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings
index 88f0760ee..9c76e1920 100644
--- a/Riot/Assets/ja.lproj/Vector.strings
+++ b/Riot/Assets/ja.lproj/Vector.strings
@@ -1720,7 +1720,7 @@
"pin_protection_reset_alert_action_reset" = "リセット";
"authentication_recaptcha_title" = "あなたは人間ですか?";
"authentication_verify_msisdn_waiting_button" = "コードを再送信";
-"authentication_choose_password_submit_button" = "パスワードをリセット";
+"authentication_choose_password_submit_button" = "パスワードを再設定";
"authentication_choose_password_signout_all_devices" = "全ての端末からサインアウト";
"authentication_choose_password_text_field_placeholder" = "新しいパスワード";
"authentication_terms_title" = "プライバシーポリシー";
@@ -1763,13 +1763,13 @@
"home_context_menu_favourite" = "お気に入り";
"all_chats_user_menu_settings" = "ユーザー設定";
"all_chats_edit_layout_show_filters" = "フィルターを表示";
-"all_chats_edit_layout_show_recents" = "最近使用したものを表示";
+"all_chats_edit_layout_show_recents" = "最近の履歴を表示";
"all_chats_edit_layout_alphabetical_order" = "アルファベット順で並び替え";
"all_chats_edit_layout_activity_order" = "アクティビティで並び替え";
"space_selector_create_space" = "スペースを作成";
-"space_selector_empty_view_information" = "スペースは、ルームや連絡先をグループ化する方法です。以下からスペースを作成できます。";
+"space_selector_empty_view_information" = "スペースは、ルームと連絡先をまとめる方法です。はじめに、スペースを作成しましょう。";
"all_chats_all_filter" = "全て";
-"all_chats_edit_layout_recents" = "最近";
+"all_chats_edit_layout_recents" = "履歴";
"all_chats_edit_layout_unreads" = "未読";
"all_chats_section_title" = "チャット";
@@ -1799,3 +1799,5 @@
"service_terms_modal_information_title_identity_server" = "IDサーバー";
"location_sharing_invalid_power_level_message" = "位置情報(ライブ)の共有には適切な権限が必要です。";
"location_sharing_live_error" = "位置情報(ライブ)のエラー";
+"all_chats_onboarding_page_title3" = "フィードバックを送信";
+"all_chats_edit_layout" = "レイアウトの設定";
diff --git a/Riot/Assets/sk.lproj/Localizable.strings b/Riot/Assets/sk.lproj/Localizable.strings
index 612c4f691..02c086065 100644
--- a/Riot/Assets/sk.lproj/Localizable.strings
+++ b/Riot/Assets/sk.lproj/Localizable.strings
@@ -168,3 +168,6 @@
/* New file message from a specific person, not referencing a room. */
"LOCATION_FROM_USER" = "%@ zdieľal/a svoju polohu";
+
+/* New voice broadcast from a specific person, not referencing a room. */
+"VOICE_BROADCAST_FROM_USER" = "%@ začal/a hlasové vysielanie";
diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings
index 87edea36c..83b316cf8 100644
--- a/Riot/Assets/sk.lproj/Vector.strings
+++ b/Riot/Assets/sk.lproj/Vector.strings
@@ -2891,3 +2891,32 @@
"poll_timeline_decryption_error" = "Z dôvodu chýb v dešifrovaní sa niektoré hlasy nemusia započítať";
"voice_message_broadcast_in_progress_message" = "Nemôžete spustiť hlasovú správu, pretože práve nahrávate živé vysielanie. Ukončite prosím živé vysielanie, aby ste mohli začať nahrávať hlasovú správu";
"voice_message_broadcast_in_progress_title" = "Nemožno spustiť hlasovú správu";
+"poll_timeline_ended_text" = "Ukončil anketu";
+"voice_broadcast_voip_cannot_start_description" = "Nemôžete spustiť hovor, pretože práve nahrávate živé vysielanie. Ukončite živé vysielanie, aby ste mohli začať hovor.";
+"voice_broadcast_voip_cannot_start_title" = "Nie je možné začať hovor";
+"poll_history_no_past_poll_text" = "V tejto miestnosti nie sú žiadne predchádzajúce ankety";
+"poll_history_no_active_poll_text" = "V tejto miestnosti nie sú žiadne aktívne ankety";
+"poll_history_past_segment_title" = "Predchádzajúce ankety";
+"poll_history_active_segment_title" = "Aktívne ankety";
+
+// MARK: - Polls history
+
+"poll_history_title" = "História ankety";
+"room_details_polls" = "História ankety";
+"accessibility_selected" = "vybrané";
+"voice_broadcast_connection_error_message" = "Bohužiaľ teraz nemôžeme spustiť nahrávanie. Skúste to prosím neskôr.";
+"voice_broadcast_connection_error_title" = "Chyba pripojenia";
+"voice_broadcast_playback_lock_screen_placeholder" = "Hlasové vysielanie";
+"wysiwyg_composer_format_action_quote" = "Prepínanie citácie";
+"wysiwyg_composer_format_action_code_block" = "Prepnutie bloku kódu";
+"wysiwyg_composer_format_action_ordered_list" = "Prepínanie číslovaného zoznamu";
+"wysiwyg_composer_format_action_unordered_list" = "Prepnúť zoznam s odrážkami";
+"voice_broadcast_recorder_connection_error" = "Chyba pripojenia - nahrávanie pozastavené";
+"poll_timeline_reply_ended_poll" = "Ukončená anketa";
+
+// MARK: - Launch loading
+
+"launch_loading_migrating_data" = "Migrácia údajov\n%@ %%";
+"settings_labs_disable_crypto_sdk" = "Crypto SDK je povolené. Ak to chcete vypnúť, preinštalujte prosím aplikáciu";
+"settings_labs_confirm_crypto_sdk" = "Túto akciu nemožno vrátiť späť";
+"settings_labs_enable_crypto_sdk" = "Zapnúť nové Crypto SDK využívajúce Rust";
diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings
index f484f7275..b2a297d07 100644
--- a/Riot/Assets/sq.lproj/Vector.strings
+++ b/Riot/Assets/sq.lproj/Vector.strings
@@ -2676,3 +2676,14 @@
"notice_voice_broadcast_ended" = "%@ përfundoi një transmetim zanor.";
"notice_voice_broadcast_live" = "Transmetim i drejtëpërdrejtë";
"user_other_session_security_recommendation_title" = "Sesione të tjerë";
+"poll_timeline_ended_text" = "Përfundoi pyetësori";
+"poll_timeline_decryption_error" = "Për shkak gabimesh shfshehtëzimi, mund të mos jenë numëruar disa vota";
+"poll_history_no_past_poll_text" = "Në këtë dhomë s’ka pyetësorë të dikurshëm";
+"poll_history_no_active_poll_text" = "Në këtë dhomë s’ka pyetësorë aktivë";
+"poll_history_past_segment_title" = "Pyetësorë të dikurshëm";
+"poll_history_active_segment_title" = "Pyetësorë aktivë";
+"voice_broadcast_playback_lock_screen_placeholder" = "Transmetim zanor";
+"voice_broadcast_voip_cannot_start_description" = "S’mund të niset thirrje, ngaqë aktualisht po regjistroni një transmetim të drejtpërdrejtë. Ju lutemi, përfundoni transmetimin e drejtpërdrejtë, që të mund të nisni një thirrje.";
+"voice_broadcast_voip_cannot_start_title" = "S’niset dot një thirrje";
+"voice_message_broadcast_in_progress_message" = "S’mund të niset mesazh zanor, ngaqë aktualisht po regjistroni një transmetim të drejtpërdrejtë. Ju lutemi, përfundoni transmetimin e drejtpërdrejtë, që të mund të nisni regjistrimin e një mesazhi zanor";
+"voice_message_broadcast_in_progress_title" = "S’niset dot mesazh zanor";
diff --git a/Riot/Assets/sv.lproj/Localizable.strings b/Riot/Assets/sv.lproj/Localizable.strings
index 5b3e918dd..fc7c18b9e 100644
--- a/Riot/Assets/sv.lproj/Localizable.strings
+++ b/Riot/Assets/sv.lproj/Localizable.strings
@@ -118,3 +118,6 @@
/* New file message from a specific person, not referencing a room. */
"LOCATION_FROM_USER" = "%@ delade sin plats";
+
+/* New voice broadcast from a specific person, not referencing a room. */
+"VOICE_BROADCAST_FROM_USER" = "%@ påbörjade en röstsändning";
diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings
index 53b8d822f..872645f66 100644
--- a/Riot/Assets/sv.lproj/Vector.strings
+++ b/Riot/Assets/sv.lproj/Vector.strings
@@ -2310,7 +2310,7 @@
"authentication_terms_policy_url_error" = "Kan inte hitta den valda policyn. Vänligen pröva igen senare.";
/* The placeholder will show the homeserver's domain */
"authentication_terms_message" = "Vänligen läs villkor och policyer för %@";
-"authentication_terms_title" = "Serverpolicyer";
+"authentication_terms_title" = "Sekretesspolicyer";
"authentication_verify_msisdn_invalid_phone_number" = "Ogiltigt telefonnummer";
"authentication_verify_msisdn_waiting_button" = "Skicka kod igen";
/* The placeholder will show the phone number that was entered. */
@@ -2363,3 +2363,112 @@
// MARK: Authentication
"authentication_registration_title" = "Skapa ditt konto";
+"voice_broadcast_time_left" = "%@ kvar";
+"all_chats_empty_list_placeholder_title" = "Du är ikapp.";
+"all_chats_empty_view_information" = "Den säkra allt-i-ett-chattappen för lag, vänner och organisationer. Skapa en chatt, eller gå med i ett existerande rum, för att komma igång.";
+"all_chats_empty_space_information" = "Utrymmen är ett nytt sätt att gruppera rum och personer. Lägg till ett existerande rum, eller skapa ett nytt, med knappen nere till höger.";
+"all_chats_empty_view_title" = "%@\nser lite tom ut.";
+"all_chats_all_filter" = "Alla";
+"all_chats_edit_layout_alphabetical_order" = "Sortera A-Ö";
+"all_chats_edit_layout_activity_order" = "Sortera efter aktivitet";
+"all_chats_edit_layout_show_filters" = "Visa filter";
+"all_chats_edit_layout_show_recents" = "Visa nyliga";
+"all_chats_edit_layout_sorting_options_title" = "Sortera meddelanden efter";
+"all_chats_edit_layout_pin_spaces_title" = "Fäst dina utrymmen";
+"all_chats_edit_layout_add_filters_message" = "Filtrera automatiskt dina meddelanden i valfria kategorier";
+"all_chats_edit_layout_add_filters_title" = "Filtrera dina meddelanden";
+"all_chats_edit_layout_add_section_message" = "Fäst sektioner till hem för enkel åtkomst";
+"all_chats_edit_layout_add_section_title" = "Lägg till sektion i hem";
+"all_chats_edit_layout_unreads" = "Olästa";
+"all_chats_edit_layout_recents" = "Nyliga";
+"all_chats_edit_layout" = "Layoutalternativ";
+"all_chats_section_title" = "Chattar";
+
+// MARK: - All Chats
+
+"all_chats_title" = "Alla chattar";
+"voice_broadcast_voip_cannot_start_description" = "Du kan inte starta ett samtal eftersom att du för närvarande spelar in en direktsändning. Vänligen avsluta din direktsändning för att starta ett samtal.";
+"voice_broadcast_voip_cannot_start_title" = "Kan inte starta ett samtal";
+"voice_broadcast_stop_alert_agree_button" = "Ja, avsluta";
+"voice_broadcast_stop_alert_description" = "Är du säker på att du vill avsluta din direktsändning? Det här kommer att avsluta sändningen, och den fulla inspelningen kommer att bli tillgänglig i rummet.";
+"voice_broadcast_stop_alert_title" = "Avsluta direktsändning?";
+"voice_broadcast_buffering" = "Buffrar…";
+"voice_broadcast_tile" = "Röstsändning";
+"voice_broadcast_live" = "Live";
+"voice_broadcast_playback_loading_error" = "Kunde inte spela den här röstsändningen.";
+"voice_broadcast_already_in_progress_message" = "Du spelar redan in en röstsändning. Vänligen avsluta din nuvarande röstsändning för att starta en ny.";
+"voice_broadcast_blocked_by_someone_else_message" = "Någon annan spelar redan in en röstsändning. Vänta på att deras röstsändning avslutas för att starta en ny.";
+"voice_broadcast_permission_denied_message" = "Du har inte behörigheten som krävs för att starta en röstsändning i det här rummet. Kontakta en rumsadministratör för att uppgradera din behörighet.";
+
+// MARK: - Voice Broadcast
+"voice_broadcast_unauthorized_title" = "Du kan inte starta en ny röstsändning";
+"voice_message_broadcast_in_progress_message" = "Du kan inte starta ett röstmeddelande eftersom att du för närvarande spelar in en direktsändning. Vänligen avsluta din direktsändning för att börja spela in ett röstmeddelande";
+"voice_message_broadcast_in_progress_title" = "Kan inte starta röstmeddelande";
+"spaces_subspace_creation_visibility_message" = "Det skapade utrymmet kommer att läggas till i %@.";
+"spaces_subspace_creation_visibility_title" = "Vad för sorts utrymme vill du skapa?";
+"spaces_explore_rooms_format" = "Utforska %@";
+"spaces_create_subspace_title" = "Skapa ett underutrymme";
+"spaces_add_subspace_title" = "Skapa utrymme inuti %@";
+"launch_loading_processing_response" = "Hanterar data\n%@ %%";
+"launch_loading_server_syncing_nth_attempt" = "Synkar med servern\n(%@ försök)";
+
+// MARK: - Launch loading
+
+"launch_loading_server_syncing" = "Synkar med servern";
+"key_verification_alert_body" = "Granska för att försäkra att ditt konto är säkert.";
+
+// Unverified sessions
+"key_verification_alert_title" = "Du har overifierade sessioner";
+"sign_out_confirmation_message" = "Är du säker på att du vill logga ut?";
+
+// MARK: Sign out warning
+
+"sign_out" = "Logga ut";
+// User sessions management
+"user_sessions_settings" = "Hantera sessioner";
+"manage_session_sign_out_other_sessions" = "Logga ut ur alla andra sessioner";
+"manage_session_rename" = "Döp om session";
+"manage_session_name_info_link" = "Läs mer";
+/* The placeholder will be replaces with manage_session_name_info_link */
+"manage_session_name_info" = "Observera att sessionsnamn också är synliga för personer du pratar med. %@";
+"manage_session_name_hint" = "Anpassade sessionsnamn kan hjälpa dig att känna igen dina enheter lättare.";
+"settings_labs_enable_voice_broadcast" = "Röstsändning";
+"settings_labs_enable_wysiwyg_composer" = "Pröva den nya riktextredigeraren";
+"settings_labs_enable_new_app_layout" = "Ny applikationslayout";
+"settings_labs_enable_new_client_info_feature" = "Spara klientens namn, version och URL för att lättare känna igen sessioner i sessionshanteraren";
+"settings_labs_enable_new_session_manager" = "My sessionshanterare";
+"room_first_message_placeholder" = "Skicka ditt första meddelande…";
+"password_policy_pwd_in_dict_error" = "Det här lösenordet har hittats i en ordlista, och tillåts inte.";
+"password_policy_weak_pwd_error" = "Det här lösenordet är för svagt. Det måste innehålla minst 8 tecken, och minst ett tecken av varje typ: stor bokstav, liten bokstav, siffra och specialtecken.";
+
+// MARK: Password policy errors
+"password_policy_too_short_pwd_error" = "För kort lösenord";
+"authentication_qr_login_failure_retry" = "Pröva igen";
+"authentication_qr_login_failure_request_timed_out" = "Länkningen slutfördes inte inom den krävda tiden.";
+"authentication_qr_login_failure_request_denied" = "Förfrågan nekades på en andra enheten.";
+"authentication_qr_login_failure_invalid_qr" = "QR-kod är ogiltig.";
+"authentication_qr_login_failure_title" = "Länkning misslyckades";
+"authentication_qr_login_loading_signed_in" = "Du är nu inloggad på din andra enhet.";
+"authentication_qr_login_loading_waiting_signin" = "Väntar på att enheten loggar in.";
+"authentication_qr_login_loading_connecting_device" = "Ansluter till enhet";
+"authentication_qr_login_confirm_alert" = "Vänligen försäkra att du känner till källan till den här koden. Genom att länka enheter så ger du någon full åtkomst till ditt konto.";
+"authentication_qr_login_confirm_subtitle" = "Bekräfta att koden nedan matchar den andra enheten:";
+"authentication_qr_login_confirm_title" = "Säker kommunikation etablerad";
+"authentication_qr_login_scan_subtitle" = "Placera QR-koden i rutan nedan";
+"authentication_qr_login_scan_title" = "Skanna QR-kod";
+"authentication_qr_login_display_step2" = "Välj 'Logga in med QR-kod'";
+"authentication_qr_login_display_step1" = "Öppna Element på din andra enhet";
+"authentication_qr_login_display_subtitle" = "Skanna QR-koden nedan med din enhet som är utloggad.";
+"authentication_qr_login_display_title" = "Länka en enhet";
+"authentication_qr_login_start_display_qr" = "Visa QR-kod på den här enheten";
+"authentication_qr_login_start_need_alternative" = "Behöver du en alternativ metod?";
+"authentication_qr_login_start_step4" = "Välj 'Visa QR-kod på den här enheten'";
+"authentication_qr_login_start_step3" = "Välj 'Länka en enhet'";
+"authentication_qr_login_start_step2" = "Gå till Inställningar -> Säkerhet & sekretess";
+"authentication_qr_login_start_step1" = "Öppna Element på den andra enheten";
+"authentication_qr_login_start_subtitle" = "Använd kameran på den här enheten för att skanna QR-koden som visas på den andra enheten:";
+"authentication_qr_login_start_title" = "Skanna QR-kod";
+"authentication_choose_password_not_verified_message" = "Kolla din inkorg";
+"authentication_choose_password_not_verified_title" = "E-post inte verifierad";
+"authentication_login_with_qr" = "Logga in med QR-kod";
+"invite_to" = "Bjud in till %@";
diff --git a/Riot/Assets/uk.lproj/Localizable.strings b/Riot/Assets/uk.lproj/Localizable.strings
index 276df4c3e..90e0de28e 100644
--- a/Riot/Assets/uk.lproj/Localizable.strings
+++ b/Riot/Assets/uk.lproj/Localizable.strings
@@ -118,3 +118,6 @@
/* New file message from a specific person, not referencing a room. */
"LOCATION_FROM_USER" = "%@ надсилає дані про своє місцеперебування";
+
+/* New voice broadcast from a specific person, not referencing a room. */
+"VOICE_BROADCAST_FROM_USER" = "%@ розпочинає голосову трансляцію";
diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings
index 96e8166d7..95c51d890 100644
--- a/Riot/Assets/uk.lproj/Vector.strings
+++ b/Riot/Assets/uk.lproj/Vector.strings
@@ -2893,3 +2893,32 @@
"poll_timeline_decryption_error" = "Через помилки під час розшифрування деякі голоси можуть бути не враховані";
"voice_message_broadcast_in_progress_title" = "Неможливо розпочати запис голосового повідомлення";
"voice_message_broadcast_in_progress_message" = "Ви не можете розпочати запис голосового повідомлення, оскільки зараз триває запис трансляції наживо. Будь ласка, завершіть трансляцію, щоб розпочати запис голосового повідомлення";
+"poll_timeline_ended_text" = "Опитування завершено";
+"voice_broadcast_voip_cannot_start_description" = "Ви не можете розпочати виклик, оскільки зараз відбувається запис трансляції наживо. Завершіть трансляцію, щоб розпочати виклик.";
+"voice_broadcast_voip_cannot_start_title" = "Неможливо розпочати виклик";
+"poll_history_no_past_poll_text" = "У цій кімнаті немає минулих опитувань";
+"poll_history_no_active_poll_text" = "У цій кімнаті немає активних опитувань";
+"poll_history_past_segment_title" = "Минулі опитування";
+"poll_history_active_segment_title" = "Активні опитування";
+
+// MARK: - Polls history
+
+"poll_history_title" = "Історія опитувань";
+"room_details_polls" = "Історія опитувань";
+"accessibility_selected" = "вибрано";
+"voice_broadcast_playback_lock_screen_placeholder" = "Голосові трансляції";
+"voice_broadcast_connection_error_message" = "На жаль, ми не можемо розпочати запис прямо зараз. Повторіть спробу пізніше.";
+"voice_broadcast_connection_error_title" = "Помилка з'єднання";
+"wysiwyg_composer_format_action_quote" = "Перемкнути цитування";
+"wysiwyg_composer_format_action_code_block" = "Перемкнути блок коду";
+"wysiwyg_composer_format_action_ordered_list" = "Перемкнути на нумерований список";
+"wysiwyg_composer_format_action_unordered_list" = "Перемкнути на маркований список";
+"voice_broadcast_recorder_connection_error" = "Помилка з'єднання - Запис призупинено";
+"poll_timeline_reply_ended_poll" = "Завершене опитування";
+
+// MARK: - Launch loading
+
+"launch_loading_migrating_data" = "Перенесення даних\n%@ %%";
+"settings_labs_disable_crypto_sdk" = "Crypto SDK увімкнено. Щоб вимкнути, перевстановіть застосунок";
+"settings_labs_confirm_crypto_sdk" = "Дію не можна скасувати";
+"settings_labs_enable_crypto_sdk" = "Увімкнути новий заснований на rust Crypto SDK";
diff --git a/Riot/Assets/zh_Hans.lproj/Localizable.strings b/Riot/Assets/zh_Hans.lproj/Localizable.strings
index 1e13dc6f6..2b4f707e2 100644
--- a/Riot/Assets/zh_Hans.lproj/Localizable.strings
+++ b/Riot/Assets/zh_Hans.lproj/Localizable.strings
@@ -123,3 +123,6 @@
/* New file message from a specific person, not referencing a room. */
"LOCATION_FROM_USER" = "%@ 分享了他们的位置";
+
+/* New voice broadcast from a specific person, not referencing a room. */
+"VOICE_BROADCAST_FROM_USER" = "%@开始语音广播";
diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings
index d7a775d4b..f7fd8166c 100644
--- a/Riot/Assets/zh_Hans.lproj/Vector.strings
+++ b/Riot/Assets/zh_Hans.lproj/Vector.strings
@@ -196,7 +196,7 @@
"room_event_action_copy" = "复制";
"room_event_action_quote" = "引用";
"room_event_action_redact" = "移除";
-"room_event_action_more" = "移动";
+"room_event_action_more" = "更多";
"room_event_action_share" = "分享";
"room_event_action_permalink" = "复制消息的链接";
"room_event_action_view_source" = "查看源数据";
@@ -2231,3 +2231,47 @@
"onboarding_congratulations_home_button" = "带我到主页";
"onboarding_use_case_message" = "我们将帮助你连接";
"invite_to" = "邀请到%@";
+"threads_empty_title" = "保持讨论的有条理性";
+"threads_action_my_threads" = "我的消息列";
+"threads_action_all_threads" = "所有消息列";
+"threads_title" = "消息列";
+"thread_copy_link_to_thread" = "将链接复制到消息列";
+
+// MARK: Threads
+"room_thread_title" = "消息列";
+"room_accessibility_record_voice_message_hint" = "双击并保持录音。";
+"room_accessibility_record_voice_message" = "录制语音消息";
+"room_accessibility_thread_more" = "更多";
+"room_accessibility_threads" = "消息列";
+"room_event_copy_link_info" = "链接已复制到剪贴板。";
+"room_event_action_reply_in_thread" = "消息列";
+"room_event_action_view_in_room" = "在房间浏览";
+"room_first_message_placeholder" = "发送您的第一条消息……";
+"room_participants_invite_prompt_to_msg" = "您确定要邀请%@ 到 %@吗?";
+"room_participants_leave_success" = "离开房间";
+"room_participants_leave_processing" = "离开";
+"search_filter_placeholder" = "过滤";
+"password_policy_pwd_in_dict_error" = "此密码已在字典中找到,不允许使用。";
+"password_policy_weak_pwd_error" = "此密码太弱了。它必须包含至少8个字符,每种类型至少有一个字符: 大写、小写、数字和特殊字符。";
+
+// MARK: Password policy errors
+"password_policy_too_short_pwd_error" = "密码过短";
+"authentication_qr_login_failure_retry" = "再试一次";
+"authentication_qr_login_failure_request_timed_out" = "连接没有在规定的时间内完成。";
+"authentication_qr_login_failure_request_denied" = "请求在另一个设备上被拒绝。";
+"authentication_qr_login_failure_invalid_qr" = "二维码无效。";
+"authentication_qr_login_failure_title" = "连接失败";
+"authentication_qr_login_loading_signed_in" = "您现在已经登录到另一个设备上。";
+"authentication_qr_login_loading_waiting_signin" = "等待设备登录。";
+"authentication_qr_login_loading_connecting_device" = "连接到设备";
+"authentication_qr_login_confirm_alert" = "请确保您知道此代码的来源。通过连接设备,您将为某人提供对您帐户的完全访问权限。";
+"authentication_qr_login_confirm_subtitle" = "确认下面的代码与您的其他设备匹配:";
+"authentication_qr_login_confirm_title" = "建立安全连接";
+"authentication_qr_login_scan_subtitle" = "将二维码放置在下面的方框中";
+"authentication_qr_login_scan_title" = "扫描二维码";
+"authentication_qr_login_display_step2" = "选择“以二维码登入”";
+"authentication_qr_login_display_step1" = "在您的其它设备中打开Element";
+"onboarding_splash_page_4_title_no_pun" = "为您的团队发送消息。";
+"user_session_learn_more" = "了解更多";
+"manage_session_name_info_link" = "了解更多";
+"threads_beta_information_link" = "了解更多";
diff --git a/Riot/Categories/MXKTableViewCellWithTextView.swift b/Riot/Categories/MXKTableViewCellWithTextView.swift
index 9331b5fd6..03288d377 100644
--- a/Riot/Categories/MXKTableViewCellWithTextView.swift
+++ b/Riot/Categories/MXKTableViewCellWithTextView.swift
@@ -24,7 +24,6 @@ extension MXKTableViewCellWithTextView: Themable {
func update(theme: Theme) {
mxkTextView.backgroundColor = .clear
mxkTextView.textColor = theme.textPrimaryColor
- mxkTextView.tintColor = theme.tintColor
backgroundColor = theme.backgroundColor
contentView.backgroundColor = .clear
}
diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift
index ed763a171..5cbcab2e2 100644
--- a/Riot/Generated/Images.swift
+++ b/Riot/Generated/Images.swift
@@ -110,6 +110,7 @@ internal class Asset: NSObject {
internal static let strikethrough = ImageAsset(name: "Strikethrough")
internal static let underlined = ImageAsset(name: "Underlined")
internal static let bulletList = ImageAsset(name: "bullet_list")
+ internal static let codeBlock = ImageAsset(name: "code_block")
internal static let indentDecrease = ImageAsset(name: "indent_decrease")
internal static let maximiseComposer = ImageAsset(name: "maximise_composer")
internal static let minimiseComposer = ImageAsset(name: "minimise_composer")
@@ -285,6 +286,7 @@ internal class Asset: NSObject {
internal static let modIcon = ImageAsset(name: "mod_icon")
internal static let moreReactions = ImageAsset(name: "more_reactions")
internal static let notifications = ImageAsset(name: "notifications")
+ internal static let pollHistory = ImageAsset(name: "pollHistory")
internal static let reactionsMoreAction = ImageAsset(name: "reactions_more_action")
internal static let roomAccessInfoHeaderIcon = ImageAsset(name: "room_access_info_header_icon")
internal static let scrollup = ImageAsset(name: "scrollup")
diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift
index 1dde36ec4..d42977875 100644
--- a/Riot/Generated/Strings.swift
+++ b/Riot/Generated/Strings.swift
@@ -27,6 +27,10 @@ public class VectorL10n: NSObject {
public static var accessibilityCheckboxLabel: String {
return VectorL10n.tr("Vector", "accessibility_checkbox_label")
}
+ /// selected
+ public static var accessibilitySelected: String {
+ return VectorL10n.tr("Vector", "accessibility_selected")
+ }
/// Unable to verify email address. Please check your email and click on the link it contains. Once this is done, click continue
public static var accountEmailValidationError: String {
return VectorL10n.tr("Vector", "account_email_validation_error")
@@ -3179,6 +3183,10 @@ public class VectorL10n: NSObject {
public static var later: String {
return VectorL10n.tr("Vector", "later")
}
+ /// Migrating data\n%@ %%
+ public static func launchLoadingMigratingData(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "launch_loading_migrating_data", p1)
+ }
/// Processing data\n%@ %%
public static func launchLoadingProcessingResponse(_ p1: String) -> String {
return VectorL10n.tr("Vector", "launch_loading_processing_response", p1)
@@ -4839,6 +4847,26 @@ public class VectorL10n: NSObject {
public static var pollEditFormUpdateFailureTitle: String {
return VectorL10n.tr("Vector", "poll_edit_form_update_failure_title")
}
+ /// Active polls
+ public static var pollHistoryActiveSegmentTitle: String {
+ return VectorL10n.tr("Vector", "poll_history_active_segment_title")
+ }
+ /// There are no active polls in this room
+ public static var pollHistoryNoActivePollText: String {
+ return VectorL10n.tr("Vector", "poll_history_no_active_poll_text")
+ }
+ /// There are no past polls in this room
+ public static var pollHistoryNoPastPollText: String {
+ return VectorL10n.tr("Vector", "poll_history_no_past_poll_text")
+ }
+ /// Past polls
+ public static var pollHistoryPastSegmentTitle: String {
+ return VectorL10n.tr("Vector", "poll_history_past_segment_title")
+ }
+ /// Poll history
+ public static var pollHistoryTitle: String {
+ return VectorL10n.tr("Vector", "poll_history_title")
+ }
/// Due to decryption errors, some votes may not be counted
public static var pollTimelineDecryptionError: String {
return VectorL10n.tr("Vector", "poll_timeline_decryption_error")
@@ -4859,6 +4887,10 @@ public class VectorL10n: NSObject {
public static var pollTimelineOneVote: String {
return VectorL10n.tr("Vector", "poll_timeline_one_vote")
}
+ /// Ended poll
+ public static var pollTimelineReplyEndedPoll: String {
+ return VectorL10n.tr("Vector", "poll_timeline_reply_ended_poll")
+ }
/// Final results based on %lu votes
public static func pollTimelineTotalFinalResults(_ p1: Int) -> String {
return VectorL10n.tr("Vector", "poll_timeline_total_final_results", p1)
@@ -5503,6 +5535,10 @@ public class VectorL10n: NSObject {
public static var roomDetailsPhotoForDm: String {
return VectorL10n.tr("Vector", "room_details_photo_for_dm")
}
+ /// Poll history
+ public static var roomDetailsPolls: String {
+ return VectorL10n.tr("Vector", "room_details_polls")
+ }
/// Suggest to space members
public static var roomDetailsPromoteRoomSuggestTitle: String {
return VectorL10n.tr("Vector", "room_details_promote_room_suggest_title")
@@ -7547,10 +7583,18 @@ public class VectorL10n: NSObject {
public static var settingsLabs: String {
return VectorL10n.tr("Vector", "settings_labs")
}
+ /// This action cannot be undone
+ public static var settingsLabsConfirmCryptoSdk: String {
+ return VectorL10n.tr("Vector", "settings_labs_confirm_crypto_sdk")
+ }
/// Create conference calls with jitsi
public static var settingsLabsCreateConferenceWithJitsi: String {
return VectorL10n.tr("Vector", "settings_labs_create_conference_with_jitsi")
}
+ /// Crypto SDK is enabled. To disable please reinstall the app
+ public static var settingsLabsDisableCryptoSdk: String {
+ return VectorL10n.tr("Vector", "settings_labs_disable_crypto_sdk")
+ }
/// End-to-End Encryption
public static var settingsLabsE2eEncryption: String {
return VectorL10n.tr("Vector", "settings_labs_e2e_encryption")
@@ -7563,6 +7607,10 @@ public class VectorL10n: NSObject {
public static var settingsLabsEnableAutoReportDecryptionErrors: String {
return VectorL10n.tr("Vector", "settings_labs_enable_auto_report_decryption_errors")
}
+ /// Enable new rust-based Crypto SDK
+ public static var settingsLabsEnableCryptoSdk: String {
+ return VectorL10n.tr("Vector", "settings_labs_enable_crypto_sdk")
+ }
/// Live location sharing - share current location (active development, and temporarily, locations persist in room history)
public static var settingsLabsEnableLiveLocationSharing: String {
return VectorL10n.tr("Vector", "settings_labs_enable_live_location_sharing")
@@ -9179,6 +9227,14 @@ public class VectorL10n: NSObject {
public static var voiceBroadcastBuffering: String {
return VectorL10n.tr("Vector", "voice_broadcast_buffering")
}
+ /// Unfortunately we’re unable to start a recording right now. Please try again later.
+ public static var voiceBroadcastConnectionErrorMessage: String {
+ return VectorL10n.tr("Vector", "voice_broadcast_connection_error_message")
+ }
+ /// Connection error
+ public static var voiceBroadcastConnectionErrorTitle: String {
+ return VectorL10n.tr("Vector", "voice_broadcast_connection_error_title")
+ }
/// Live
public static var voiceBroadcastLive: String {
return VectorL10n.tr("Vector", "voice_broadcast_live")
@@ -9191,6 +9247,14 @@ public class VectorL10n: NSObject {
public static var voiceBroadcastPlaybackLoadingError: String {
return VectorL10n.tr("Vector", "voice_broadcast_playback_loading_error")
}
+ /// Voice broadcast
+ public static var voiceBroadcastPlaybackLockScreenPlaceholder: String {
+ return VectorL10n.tr("Vector", "voice_broadcast_playback_lock_screen_placeholder")
+ }
+ /// Connection error - Recording paused
+ public static var voiceBroadcastRecorderConnectionError: String {
+ return VectorL10n.tr("Vector", "voice_broadcast_recorder_connection_error")
+ }
/// Yes, stop
public static var voiceBroadcastStopAlertAgreeButton: String {
return VectorL10n.tr("Vector", "voice_broadcast_stop_alert_agree_button")
@@ -9215,6 +9279,14 @@ public class VectorL10n: NSObject {
public static var voiceBroadcastUnauthorizedTitle: String {
return VectorL10n.tr("Vector", "voice_broadcast_unauthorized_title")
}
+ /// You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.
+ public static var voiceBroadcastVoipCannotStartDescription: String {
+ return VectorL10n.tr("Vector", "voice_broadcast_voip_cannot_start_description")
+ }
+ /// Can’t start a call
+ public static var voiceBroadcastVoipCannotStartTitle: String {
+ return VectorL10n.tr("Vector", "voice_broadcast_voip_cannot_start_title")
+ }
/// You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message
public static var voiceMessageBroadcastInProgressMessage: String {
return VectorL10n.tr("Vector", "voice_message_broadcast_in_progress_message")
@@ -9339,6 +9411,10 @@ public class VectorL10n: NSObject {
public static var wysiwygComposerFormatActionBold: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_bold")
}
+ /// Toggle code block
+ public static var wysiwygComposerFormatActionCodeBlock: String {
+ return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_code_block")
+ }
/// Apply inline code format
public static var wysiwygComposerFormatActionInlineCode: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_inline_code")
@@ -9351,6 +9427,14 @@ public class VectorL10n: NSObject {
public static var wysiwygComposerFormatActionLink: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_link")
}
+ /// Toggle numbered list
+ public static var wysiwygComposerFormatActionOrderedList: String {
+ return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_ordered_list")
+ }
+ /// Toggle quote
+ public static var wysiwygComposerFormatActionQuote: String {
+ return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_quote")
+ }
/// Apply underline format
public static var wysiwygComposerFormatActionStrikethrough: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_strikethrough")
@@ -9359,6 +9443,10 @@ public class VectorL10n: NSObject {
public static var wysiwygComposerFormatActionUnderline: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_underline")
}
+ /// Toggle bulleted list
+ public static var wysiwygComposerFormatActionUnorderedList: String {
+ return VectorL10n.tr("Vector", "wysiwyg_composer_format_action_unordered_list")
+ }
/// Create a link
public static var wysiwygComposerLinkActionCreateTitle: String {
return VectorL10n.tr("Vector", "wysiwyg_composer_link_action_create_title")
diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift
index 591f3968a..d9e64a1af 100644
--- a/Riot/Managers/Settings/RiotSettings.swift
+++ b/Riot/Managers/Settings/RiotSettings.swift
@@ -191,6 +191,12 @@ final class RiotSettings: NSObject {
/// Flag indicating if the voice broadcast feature is enabled
@UserDefault(key: "enableVoiceBroadcast", defaultValue: false, storage: defaults)
var enableVoiceBroadcast
+
+ #if DEBUG
+ /// Flag indicating if we are using rust-based `MatrixCryptoSDK` instead of `MatrixSDK`'s internal crypto module
+ @UserDefault(key: "enableCryptoSDK", defaultValue: false, storage: defaults)
+ var enableCryptoSDK
+ #endif
// MARK: Calls
diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift
index 9dbba85c7..269b387fa 100644
--- a/Riot/Managers/Theme/Themes/DarkTheme.swift
+++ b/Riot/Managers/Theme/Themes/DarkTheme.swift
@@ -168,14 +168,8 @@ class DarkTheme: NSObject, Theme {
searchBar.backgroundImage = UIImage() // Remove top and bottom shadow
searchBar.tintColor = self.tintColor
- if #available(iOS 13.0, *) {
- searchBar.searchTextField.backgroundColor = self.searchBackgroundColor
- searchBar.searchTextField.textColor = self.searchPlaceholderColor
- } else {
- if let searchBarTextField = searchBar.vc_searchTextField {
- searchBarTextField.textColor = self.searchPlaceholderColor
- }
- }
+ searchBar.searchTextField.backgroundColor = self.searchBackgroundColor
+ searchBar.searchTextField.textColor = self.searchPlaceholderColor
}
func applyStyle(onTextField texField: UITextField) {
diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift
index d5d309bf3..97c701975 100644
--- a/Riot/Managers/Theme/Themes/DefaultTheme.swift
+++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift
@@ -177,14 +177,8 @@ class DefaultTheme: NSObject, Theme {
return
}
- if #available(iOS 13.0, *) {
- searchBar.searchTextField.backgroundColor = self.searchBackgroundColor
- searchBar.searchTextField.textColor = self.searchPlaceholderColor
- } else {
- if let searchBarTextField = searchBar.vc_searchTextField {
- searchBarTextField.textColor = self.searchPlaceholderColor
- }
- }
+ searchBar.searchTextField.backgroundColor = self.searchBackgroundColor
+ searchBar.searchTextField.textColor = self.searchPlaceholderColor
}
func applyStyle(onTextField texField: UITextField) {
diff --git a/Riot/Modules/Application/AppCoordinator.swift b/Riot/Modules/Application/AppCoordinator.swift
index 202e698a0..c356d0b86 100755
--- a/Riot/Modules/Application/AppCoordinator.swift
+++ b/Riot/Modules/Application/AppCoordinator.swift
@@ -101,7 +101,7 @@ final class AppCoordinator: NSObject, AppCoordinatorType {
if AppDelegate.theDelegate().isOffline {
self.splitViewCoordinator?.showAppStateIndicator(with: VectorL10n.networkOfflineTitle, icon: UIImage(systemName: "wifi.slash"))
} else {
- self.splitViewCoordinator?.hideAppStateIndicator()
+ self.splitViewCoordinator?.hideAppStateIndicator()
}
}
diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m
index 15cdb8be1..7bf51dd46 100644
--- a/Riot/Modules/Application/LegacyAppDelegate.m
+++ b/Riot/Modules/Application/LegacyAppDelegate.m
@@ -618,6 +618,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// Pause Voice Broadcast recording if needed
[VoiceBroadcastRecorderProvider.shared pauseRecording];
+
+ // Pause Voice Broadcast playing if needed
+ [VoiceBroadcastPlaybackProvider.shared pausePlayingInProgressVoiceBroadcast];
}
- (void)applicationWillEnterForeground:(UIApplication *)application
@@ -2395,14 +2398,14 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
MXLogDebug(@"[AppDelegate] showLaunchAnimation");
LaunchLoadingView *launchLoadingView;
- if (MXSDKOptions.sharedInstance.enableSyncProgress)
+ if (MXSDKOptions.sharedInstance.enableStartupProgress)
{
MXSession *mainSession = self.mxSessions.firstObject;
- launchLoadingView = [LaunchLoadingView instantiateWithSyncProgress:mainSession.syncProgress];
+ launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:mainSession.startupProgress];
}
else
{
- launchLoadingView = [LaunchLoadingView instantiateWithSyncProgress:nil];
+ launchLoadingView = [LaunchLoadingView instantiateWithStartupProgress:nil];
}
launchLoadingView.frame = window.bounds;
diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift
index 8b9898992..9f2e7083b 100644
--- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift
+++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift
@@ -613,8 +613,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
/// Replace the contents of the navigation router with a loading animation.
private func showLoadingAnimation() {
- let syncProgress: MXSessionSyncProgress? = MXSDKOptions.sharedInstance().enableSyncProgress ? session?.syncProgress : nil
- let loadingViewController = LaunchLoadingViewController(syncProgress: syncProgress)
+ let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil
+ let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress)
loadingViewController.modalPresentationStyle = .fullScreen
// Replace the navigation stack with the loading animation
diff --git a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift
index 583419075..d6270edae 100644
--- a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift
+++ b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift
@@ -106,8 +106,8 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
// MARK: - Private
private func showLoadingAnimation() {
- let syncProgress: MXSessionSyncProgress? = MXSDKOptions.sharedInstance().enableSyncProgress ? session?.syncProgress : nil
- let loadingViewController = LaunchLoadingViewController(syncProgress: syncProgress)
+ let startupProgress: MXSessionStartupProgress? = MXSDKOptions.sharedInstance().enableStartupProgress ? session?.startupProgress : nil
+ let loadingViewController = LaunchLoadingViewController(startupProgress: startupProgress)
loadingViewController.modalPresentationStyle = .fullScreen
// Replace the navigation stack with the loading animation
diff --git a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m
index 561b0aac5..32e8edcd9 100644
--- a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m
+++ b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultAttachmentBubbleCell.m
@@ -28,9 +28,7 @@
[super customizeTableViewCellRendering];
self.roomNameLabel.textColor = ThemeService.shared.theme.textSecondaryColor;
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
-
+
[self updateUserNameColor];
}
diff --git a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m
index 40818f8a0..780d11efe 100644
--- a/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m
+++ b/Riot/Modules/GlobalSearch/Messages/Views/MessagesSearchResultTextMsgBubbleCell.m
@@ -30,8 +30,6 @@
[self updateUserNameColor];
self.roomNameLabel.textColor = ThemeService.shared.theme.textSecondaryColor;
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
}
- (void)render:(MXKCellData *)cellData
diff --git a/Riot/Modules/LaunchLoading/LaunchLoadingView.swift b/Riot/Modules/LaunchLoading/LaunchLoadingView.swift
index 55f3aff05..18d6add9d 100644
--- a/Riot/Modules/LaunchLoading/LaunchLoadingView.swift
+++ b/Riot/Modules/LaunchLoading/LaunchLoadingView.swift
@@ -41,9 +41,9 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
// MARK: - Setup
- static func instantiate(syncProgress: MXSessionSyncProgress?) -> LaunchLoadingView {
+ static func instantiate(startupProgress: MXSessionStartupProgress?) -> LaunchLoadingView {
let view = LaunchLoadingView.loadFromNib()
- syncProgress?.delegate = view
+ startupProgress?.delegate = view
return view
}
@@ -54,7 +54,7 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
animationTimeline.play()
self.animationTimeline = animationTimeline
- self.statusLabel.isHidden = !MXSDKOptions.sharedInstance().enableSyncProgress
+ self.statusLabel.isHidden = !MXSDKOptions.sharedInstance().enableStartupProgress
}
// MARK: - Public
@@ -65,21 +65,35 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
}
}
-extension LaunchLoadingView: MXSessionSyncProgressDelegate {
- func sessionDidUpdateSyncState(_ state: MXSessionSyncState) {
- guard MXSDKOptions.sharedInstance().enableSyncProgress else {
+extension LaunchLoadingView: MXSessionStartupProgressDelegate {
+ func sessionDidUpdateStartupStage(_ stage: MXSessionStartupStage) {
+ guard MXSDKOptions.sharedInstance().enableStartupProgress else {
+ return
+ }
+ updateStatusText(for: stage)
+
+ }
+
+ private func updateStatusText(for stage: MXSessionStartupStage) {
+ guard Thread.isMainThread else {
+ DispatchQueue.main.async { [weak self] in
+ self?.updateStatusText(for: stage)
+ }
return
}
// Sync may be doing a lot of heavy work on the main thread and the status text
// does not update reliably enough without explicitly refreshing
CATransaction.begin()
- statusLabel.text = statusText(for: state)
+ statusLabel.text = statusText(for: stage)
CATransaction.commit()
}
- private func statusText(for state: MXSessionSyncState) -> String {
- switch state {
+ private func statusText(for stage: MXSessionStartupStage) -> String {
+ switch stage {
+ case .migratingData(let progress):
+ let percent = Int(floor(progress * 100))
+ return VectorL10n.launchLoadingMigratingData("\(percent)")
case .serverSyncing(let attempts):
if attempts > 1, let nth = numberFormatter.string(from: NSNumber(value: attempts)) {
return VectorL10n.launchLoadingServerSyncingNthAttempt(nth)
diff --git a/Riot/Modules/LaunchLoading/LaunchLoadingViewController.swift b/Riot/Modules/LaunchLoading/LaunchLoadingViewController.swift
index 1da229b79..ef9630dda 100644
--- a/Riot/Modules/LaunchLoading/LaunchLoadingViewController.swift
+++ b/Riot/Modules/LaunchLoading/LaunchLoadingViewController.swift
@@ -21,10 +21,10 @@ class LaunchLoadingViewController: UIViewController, Reusable {
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
- init(syncProgress: MXSessionSyncProgress?) {
+ init(startupProgress: MXSessionStartupProgress?) {
super.init(nibName: "LaunchLoadingViewController", bundle: nil)
- let launchLoadingView = LaunchLoadingView.instantiate(syncProgress: syncProgress)
+ let launchLoadingView = LaunchLoadingView.instantiate(startupProgress: startupProgress)
launchLoadingView.update(theme: ThemeService.shared().theme)
view.vc_addSubViewMatchingParent(launchLoadingView)
diff --git a/Riot/Modules/LocationSharing/LocationManager.swift b/Riot/Modules/LocationSharing/LocationManager.swift
index 5fb222493..857a59597 100644
--- a/Riot/Modules/LocationSharing/LocationManager.swift
+++ b/Riot/Modules/LocationSharing/LocationManager.swift
@@ -43,6 +43,7 @@ class LocationManager: NSObject {
private let locationManager: CLLocationManager
private var authorizationHandler: LocationAuthorizationHandler?
+ private var authorizationReturnedSinceRequestingAlways = false
// MARK: Public
@@ -144,14 +145,16 @@ class LocationManager: NSObject {
// See https://developer.apple.com/documentation/corelocation/cllocationmanager/1620551-requestalwaysauthorization?changes=_6_6
private func tryToRequestAlwaysAuthorization(handler: @escaping LocationAuthorizationHandler) {
self.authorizationHandler = handler
+ self.authorizationReturnedSinceRequestingAlways = false
+ self.locationManager.delegate = self
self.locationManager.requestAlwaysAuthorization()
Timer.scheduledTimer(withTimeInterval: Constants.waitForAuthorizationStatusDelay, repeats: false) { [weak self] _ in
- guard let self = self else {
+ guard let self = self, !self.authorizationReturnedSinceRequestingAlways else {
return
}
- self.authorizationRequestDidComplete(with: self.locationManager.authorizationStatus)
+ self.authorizationAlwaysRequestDidComplete(with: self.locationManager.authorizationStatus)
}
}
@@ -174,8 +177,7 @@ class LocationManager: NSObject {
return status
}
-
- private func authorizationRequestDidComplete(with status: CLAuthorizationStatus) {
+ private func authorizationAlwaysRequestDidComplete(with status: CLAuthorizationStatus) {
guard let authorizationHandler = self.authorizationHandler else {
return
}
@@ -191,7 +193,14 @@ extension LocationManager: CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
let status = self.locationManager.authorizationStatus
- self.authorizationRequestDidComplete(with: status)
+ authorizationReturnedSinceRequestingAlways = true
+ if status == .authorizedAlways {
+ // LocationManager can call locationManagerDidChangeAuthorization multiple times.
+ // For example it calls it at initialisation of LocationManager manager and we are also seeing it called
+ // after requestAlwaysAuthorization but before the user has actually selected on option on the prompt.
+ // Therefore we should only call `authorizationAlwaysRequestDidComplete` once on the success of authorizedAlways being granted.
+ self.authorizationAlwaysRequestDidComplete(with: status)
+ }
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h
index 9928892f2..87aabe50b 100644
--- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h
+++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h
@@ -572,6 +572,7 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey;
Once complete, this local echo will be replaced by the event saved by the homeserver.
@param audioFileLocalURL the local filesystem path of the audio file to send.
+ @param additionalContentParams (optional) the additional parameters to the content.
@param mimeType (optional) the mime type of the file. Defaults to `audio/ogg`
@param duration the length of the voice message in milliseconds
@param samples an array of floating point values normalized to [0, 1], boxed within NSNumbers
@@ -580,6 +581,7 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey;
@param failure A block object called when the operation fails.
*/
- (void)sendVoiceMessage:(NSURL *)audioFileLocalURL
+ additionalContentParams:(NSDictionary*)additionalContentParams
mimeType:mimeType
duration:(NSUInteger)duration
samples:(NSArray *)samples
diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m
index d8f55bf14..5f33af838 100644
--- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m
+++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m
@@ -1998,6 +1998,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
}
- (void)sendVoiceMessage:(NSURL *)audioFileLocalURL
+ additionalContentParams:(NSDictionary *)additionalContentParams
mimeType:mimeType
duration:(NSUInteger)duration
samples:(NSArray *)samples
@@ -2006,7 +2007,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
{
__block MXEvent *localEchoEvent = nil;
- [_room sendVoiceMessage:audioFileLocalURL mimeType:mimeType duration:duration samples:samples threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure keepActualFilename:YES];
+ [_room sendVoiceMessage:audioFileLocalURL additionalContentParams:additionalContentParams mimeType:mimeType duration:duration samples:samples threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure keepActualFilename:YES];
if (localEchoEvent)
{
@@ -2185,10 +2186,20 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
[self removeEventWithEventId:eventId];
if (event.isVoiceMessage) {
+ // Voice message
NSNumber *duration = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioDuration];
NSArray *samples = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioWaveform];
-
- [self sendVoiceMessage:localFileURL mimeType:mimetype duration:duration.doubleValue samples:samples success:success failure:failure];
+
+ // Additional content params in case it is a voicebroacast chunk
+ NSDictionary* additionalContentParams = nil;
+ if (event.content[kMXEventRelationRelatesToKey] != nil && event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil) {
+ additionalContentParams = @{
+ kMXEventRelationRelatesToKey: event.content[kMXEventRelationRelatesToKey],
+ VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType: event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType]
+ };
+ }
+
+ [self sendVoiceMessage:localFileURL additionalContentParams:additionalContentParams mimeType:mimetype duration:duration.doubleValue samples:samples success:success failure:failure];
} else {
[self sendAudioFile:localFileURL mimeType:mimetype success:success failure:failure];
}
diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift
index f22c779fd..4c942bccd 100644
--- a/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift
+++ b/Riot/Modules/MatrixKit/Models/Room/MXKSendReplyEventStringLocalizer.swift
@@ -18,34 +18,38 @@ import Foundation
class MXKSendReplyEventStringLocalizer: NSObject, MXSendReplyEventStringLocalizerProtocol {
func senderSentAnImage() -> String {
- return VectorL10n.messageReplyToSenderSentAnImage
+ VectorL10n.messageReplyToSenderSentAnImage
}
func senderSentAVideo() -> String {
- return VectorL10n.messageReplyToSenderSentAVideo
+ VectorL10n.messageReplyToSenderSentAVideo
}
func senderSentAnAudioFile() -> String {
- return VectorL10n.messageReplyToSenderSentAnAudioFile
+ VectorL10n.messageReplyToSenderSentAnAudioFile
}
func senderSentAVoiceMessage() -> String {
- return VectorL10n.messageReplyToSenderSentAVoiceMessage
+ VectorL10n.messageReplyToSenderSentAVoiceMessage
}
func senderSentAFile() -> String {
- return VectorL10n.messageReplyToSenderSentAFile
+ VectorL10n.messageReplyToSenderSentAFile
}
func senderSentTheirLocation() -> String {
- return VectorL10n.messageReplyToSenderSentTheirLocation
+ VectorL10n.messageReplyToSenderSentTheirLocation
}
func senderSentTheirLiveLocation() -> String {
- return VectorL10n.messageReplyToSenderSentTheirLiveLocation
+ VectorL10n.messageReplyToSenderSentTheirLiveLocation
}
func messageToReplyToPrefix() -> String {
- return VectorL10n.messageReplyToMessageToReplyToPrefix
+ VectorL10n.messageReplyToMessageToReplyToPrefix
+ }
+
+ func replyToEndedPoll() -> String {
+ VectorL10n.pollTimelineReplyEndedPoll
}
}
diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift b/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift
index de1b19821..6c7e43a90 100644
--- a/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift
+++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/HTMLFormatter.swift
@@ -51,6 +51,7 @@ class HTMLFormatter: NSObject {
DTDefaultFontName: font.fontName,
DTDefaultFontSize: font.pointSize,
DTDefaultLinkDecoration: false,
+ DTDefaultLinkColor: ThemeService.shared().theme.colors.links,
DTWillFlushBlockCallBack: sanitizeCallback
]
options.merge(extraOptions) { (_, new) in new }
diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h
index 776b79927..999da7034 100644
--- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h
+++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.h
@@ -366,6 +366,12 @@ typedef enum : NSUInteger {
*/
@property (nonatomic) UIColor *sendingTextColor;
+/**
+ Color used to display links and hyperlinks contentt.
+ Default is [UIColor linkColor].
+ */
+@property (nonatomic) UIColor *linksColor;
+
/**
Color used to display error text.
Default is red.
diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m
index 21be58eb1..422e08990 100644
--- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m
+++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m
@@ -31,6 +31,7 @@
#import "GeneratedInterface-Swift.h"
static NSString *const kHTMLATagRegexPattern = @"([^<]*)";
+static NSString *const kRepliedTextPattern = @".*.*
(.*)
";
@interface MXKEventFormatter ()
{
@@ -89,6 +90,7 @@ static NSString *const kHTMLATagRegexPattern = @"(
_encryptingTextColor = [UIColor lightGrayColor];
_sendingTextColor = [UIColor lightGrayColor];
_errorTextColor = [UIColor redColor];
+ _linksColor = [UIColor linkColor];
_htmlBlockquoteBorderColor = [MXKTools colorWithRGBValue:0xDDDDDD];
_defaultTextFont = [UIFont systemFontOfSize:14];
@@ -1749,6 +1751,7 @@ static NSString *const kHTMLATagRegexPattern = @"(
if (url.URL)
{
[str addAttribute:NSLinkAttributeName value:url.URL range:matchRange];
+ [str addAttribute:NSForegroundColorAttributeName value:self.linksColor range:matchRange];
}
}
}
@@ -1806,6 +1809,7 @@ static NSString *const kHTMLATagRegexPattern = @"(
}
html = [self renderReplyTo:html withRoomState:roomState];
+ html = [self renderPollEndedReplyTo:html repliedEvent:repliedEvent];
}
// Apply the css style that corresponds to the event state
@@ -1878,6 +1882,12 @@ static NSString *const kHTMLATagRegexPattern = @"(
{
MXJSONModelSetString(repliedEventContent, repliedEvent.content[kMXMessageBodyKey]);
}
+ if (!repliedEventContent && repliedEvent.eventType == MXEventTypePollStart) {
+ repliedEventContent = [MXEventContentPollStart modelFromJSON:repliedEvent.content].question;
+ }
+ if (!repliedEventContent && repliedEvent.eventType == MXEventTypePollEnd) {
+ repliedEventContent = MXSendReplyEventDefaultStringLocalizer.new.replyToEndedPoll;
+ }
}
// No message content in a non-redacted event. Formatter should use fallback.
@@ -2012,6 +2022,44 @@ static NSString *const kHTMLATagRegexPattern = @"(
return html;
}
+- (NSString*)renderPollEndedReplyTo:(NSString*)htmlString repliedEvent:(MXEvent*)repliedEvent {
+ static NSRegularExpression *endedPollRegex;
+ static dispatch_once_t onceToken;
+
+ dispatch_once(&onceToken, ^{
+ endedPollRegex = [NSRegularExpression regularExpressionWithPattern:kRepliedTextPattern options:NSRegularExpressionCaseInsensitive error:nil];
+ });
+
+ NSString* finalString = htmlString;
+
+ if (repliedEvent.eventType != MXEventTypePollEnd) {
+ return finalString;
+ }
+
+ NSTextCheckingResult* match = [endedPollRegex firstMatchInString:htmlString options:0 range:NSMakeRange(0, htmlString.length)];
+
+ if (!(match && match.numberOfRanges > 1)) {
+ // no useful match found
+ return finalString;
+ }
+
+ NSRange groupRange = [match rangeAtIndex:1];
+ NSString* replacementText;
+
+ if (repliedEvent) {
+ MXEvent* pollStartedEvent = [mxSession.store eventWithEventId:repliedEvent.relatesTo.eventId inRoom:repliedEvent.roomId];
+ replacementText = [MXEventContentPollStart modelFromJSON:pollStartedEvent.content].question;
+ }
+
+ if (replacementText == nil) {
+ replacementText = VectorL10n.pollTimelineReplyEndedPoll;
+ }
+
+ finalString = [htmlString stringByReplacingCharactersInRange:groupRange withString:replacementText];
+
+ return finalString;
+}
+
- (void)postFormatMutableAttributedString:(NSMutableAttributedString*)mutableAttributedString
forEvent:(MXEvent*)event
andRepliedEvent:(MXEvent*)repliedEvent
diff --git a/Riot/Modules/MatrixKit/Utils/MXKTools.m b/Riot/Modules/MatrixKit/Utils/MXKTools.m
index 10cf491a6..9ba006a89 100644
--- a/Riot/Modules/MatrixKit/Utils/MXKTools.m
+++ b/Riot/Modules/MatrixKit/Utils/MXKTools.m
@@ -46,6 +46,7 @@ static NSRegularExpression *eventIdRegex;
static NSRegularExpression *httpLinksRegex;
// A regex to find all HTML tags
static NSRegularExpression *htmlTagsRegex;
+static NSDataDetector *linkDetector;
@implementation MXKTools
@@ -60,7 +61,8 @@ static NSRegularExpression *htmlTagsRegex;
eventIdRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixEventIdentifier options:NSRegularExpressionCaseInsensitive error:nil];
httpLinksRegex = [NSRegularExpression regularExpressionWithPattern:@"(?i)\\b(https?://\\S*)\\b" options:NSRegularExpressionCaseInsensitive error:nil];
- htmlTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil];
+ htmlTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil];
+ linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
});
}
@@ -1037,6 +1039,23 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
{
[MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:eventIdRegex];
}
+
+ // This allows to check for normal url based links (like https://element.io)
+ // And set back the default link color
+ NSArray *matches = [linkDetector matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)];
+ if (matches)
+ {
+ for (NSTextCheckingResult *match in matches)
+ {
+ NSRange matchRange = [match range];
+ NSURL *matchUrl = [match URL];
+ NSURLComponents *url = [[NSURLComponents new] initWithURL:matchUrl resolvingAgainstBaseURL:NO];
+ if (url.URL)
+ {
+ [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.colors.links range:matchRange];
+ }
+ }
+ }
}
+ (void)createLinksInMutableAttributedString:(NSMutableAttributedString*)mutableAttributedString matchingRegex:(NSRegularExpression*)regex
@@ -1083,6 +1102,8 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
// If the match is fully in the link, skip it
if (NSIntersectionRange(match.range, linkMatch.range).length == match.range.length)
{
+ // but before we set the right color
+ [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.colors.links range:linkMatch.range];
hasAlreadyLink = YES;
break;
}
@@ -1097,6 +1118,7 @@ manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
NSString *link = [mutableAttributedString.string substringWithRange:match.range];
link = [link stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
[mutableAttributedString addAttribute:NSLinkAttributeName value:link range:match.range];
+ [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.colors.links range:match.range];
}
}];
}
diff --git a/Riot/Modules/MatrixKit/Views/MXKMessageTextView.m b/Riot/Modules/MatrixKit/Views/MXKMessageTextView.m
index 74132c23f..ca572733a 100644
--- a/Riot/Modules/MatrixKit/Views/MXKMessageTextView.m
+++ b/Riot/Modules/MatrixKit/Views/MXKMessageTextView.m
@@ -67,6 +67,7 @@
- (void)setAttributedText:(NSAttributedString *)attributedText
{
+ self.linkTextAttributes = @{};
if (@available(iOS 15.0, *)) {
[self flushPills];
}
diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift
index 44ddb735b..046c20c79 100644
--- a/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift
+++ b/Riot/Modules/Room/RoomInfo/RoomInfoCoordinator.swift
@@ -174,8 +174,11 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
case .notifications:
let coordinator = createRoomNotificationSettingsCoordinator()
coordinator.start()
- self.add(childCoordinator: coordinator)
- self.navigationRouter.push(coordinator, animated: true, popCompletion: nil)
+ push(coordinator: coordinator)
+ case .pollHistory:
+ let coordinator: PollHistoryCoordinator = .init(parameters: .init(mode: .active))
+ coordinator.start()
+ push(coordinator: coordinator)
default:
guard let tabIndex = target.tabIndex else {
fatalError("No settings tab index for this target.")
@@ -189,6 +192,13 @@ final class RoomInfoCoordinator: NSObject, RoomInfoCoordinatorType {
navigationRouter.push(segmentedViewController, animated: animated, popCompletion: nil)
}
}
+
+ private func push(coordinator: Coordinator & Presentable, animated: Bool = true) {
+ self.add(childCoordinator: coordinator)
+ navigationRouter.push(coordinator, animated: animated) {
+ self.remove(childCoordinator: coordinator)
+ }
+ }
}
// MARK: - RoomInfoListCoordinatorDelegate
diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift
index c6e0c8887..6383d4fb5 100644
--- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift
+++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift
@@ -25,6 +25,7 @@ enum RoomInfoListTarget: Equatable {
case integrations
case search
case notifications
+ case pollHistory
var tabIndex: UInt? {
switch self {
diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift
index d55f674fe..fdb34304d 100644
--- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift
+++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift
@@ -174,6 +174,11 @@ final class RoomInfoListViewController: UIViewController {
let rowMembers = Row(type: .default, icon: Asset.Images.userIcon.image, text: text, accessoryType: .disclosureIndicator) {
self.viewModel.process(viewAction: .navigate(target: .members))
}
+
+ let rowPollHistory = Row(type: .default, icon: Asset.Images.pollHistory.image, text: VectorL10n.roomDetailsPolls, accessoryType: .disclosureIndicator) {
+ self.viewModel.process(viewAction: .navigate(target: .pollHistory))
+ }
+
let rowUploads = Row(type: .default, icon: Asset.Images.scrollup.image, text: VectorL10n.roomDetailsFiles, accessoryType: .disclosureIndicator) {
self.viewModel.process(viewAction: .navigate(target: .uploads))
}
@@ -193,6 +198,11 @@ final class RoomInfoListViewController: UIViewController {
rows.append(rowIntegrations)
}
rows.append(rowMembers)
+
+ if BuildSettings.pollsHistoryEnabled {
+ rows.append(rowPollHistory)
+ }
+
rows.append(rowUploads)
if !viewData.isEncrypted {
rows.append(rowSearch)
diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m
index ca6daf5e0..3792fee64 100644
--- a/Riot/Modules/Room/RoomViewController.m
+++ b/Riot/Modules/Room/RoomViewController.m
@@ -2454,13 +2454,19 @@ static CGSize kThreadListBarButtonItemImageSize;
// Prevents listening a VB when recording a new one
[VoiceBroadcastPlaybackProvider.shared pausePlaying];
+ // Check connectivity
+ if ([AppDelegate theDelegate].isOffline)
+ {
+ [self showAlertWithTitle:[VectorL10n voiceBroadcastConnectionErrorTitle] message:[VectorL10n voiceBroadcastConnectionErrorMessage]];
+ 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) {
-
+ [voiceBroadcastService startVoiceBroadcastWithSuccess:^(NSString * _Nullable success) { } failure:^(NSError * _Nonnull error) {
+ [self showAlertWithTitle:[VectorL10n voiceBroadcastConnectionErrorTitle] message:[VectorL10n voiceBroadcastConnectionErrorMessage]];
+ [session tearDownVoiceBroadcastService];
}];
}
else
@@ -4296,8 +4302,14 @@ static CGSize kThreadListBarButtonItemImageSize;
[self startActivityIndicator];
+ NSArray* relationTypes = nil;
+ // If it's a voice broadcast, delete the selected event and all related events.
+ if (selectedEvent.eventType == MXEventTypeCustom && [selectedEvent.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) {
+ relationTypes = @[MXEventRelationTypeReference];
+ }
+
MXWeakify(self);
- [self.roomDataSource.room redactEvent:selectedEvent.eventId reason:nil success:^{
+ [self.roomDataSource.room redactEvent:selectedEvent.eventId withRelations:relationTypes reason:nil success:^{
MXStrongifyAndReturnIfNil(self);
[self stopActivityIndicator];
} failure:^(NSError *error) {
@@ -5187,7 +5199,14 @@ static CGSize kThreadListBarButtonItemImageSize;
- (IBAction)onVoiceCallPressed:(id)sender
{
- if (self.isCallActive)
+ // Manage case of a Voice broadcast listening -> Pause Voice broadcast playback
+ [VoiceBroadcastPlaybackProvider.shared pausePlaying];
+
+ if (VoiceBroadcastRecorderProvider.shared.isVoiceBroadcastRecording) {
+ [[AppDelegate theDelegate] showAlertWithTitle:VectorL10n.voiceBroadcastVoipCannotStartTitle
+ message:VectorL10n.voiceBroadcastVoipCannotStartDescription];
+ }
+ else if (self.isCallActive)
{
[self hangupCall];
}
@@ -5199,7 +5218,15 @@ static CGSize kThreadListBarButtonItemImageSize;
- (IBAction)onVideoCallPressed:(id)sender
{
- [self placeCallWithVideo:YES];
+ // Manage case of a Voice broadcast listening -> Pause Voice broadcast playback
+ [VoiceBroadcastPlaybackProvider.shared pausePlaying];
+
+ if (VoiceBroadcastRecorderProvider.shared.isVoiceBroadcastRecording) {
+ [[AppDelegate theDelegate] showAlertWithTitle:VectorL10n.voiceBroadcastVoipCannotStartTitle
+ message:VectorL10n.voiceBroadcastVoipCannotStartDescription];
+ } else {
+ [self placeCallWithVideo:YES];
+ }
}
- (IBAction)onThreadListTapped:(id)sender
@@ -7911,7 +7938,7 @@ static CGSize kThreadListBarButtonItemImageSize;
samples:(NSArray *)samples
completion:(void (^)(BOOL))completion
{
- [self.roomDataSource sendVoiceMessage:url mimeType:nil duration:duration samples:samples success:^(NSString *eventId) {
+ [self.roomDataSource sendVoiceMessage:url additionalContentParams:nil mimeType:nil duration:duration samples:samples success:^(NSString *eventId) {
MXLogDebug(@"Success with event id %@", eventId);
completion(YES);
} failure:^(NSError *error) {
diff --git a/Riot/Modules/Room/Settings/RoomSettingsViewController.m b/Riot/Modules/Room/Settings/RoomSettingsViewController.m
index d042a5f18..d958bd2e8 100644
--- a/Riot/Modules/Room/Settings/RoomSettingsViewController.m
+++ b/Riot/Modules/Room/Settings/RoomSettingsViewController.m
@@ -52,8 +52,7 @@ enum
ROOM_SETTINGS_MAIN_SECTION_ROW_TOPIC,
ROOM_SETTINGS_MAIN_SECTION_ROW_TAG,
ROOM_SETTINGS_MAIN_SECTION_ROW_DIRECT_CHAT,
- ROOM_SETTINGS_MAIN_SECTION_ROW_MUTE_NOTIFICATIONS,
- ROOM_SETTINGS_MAIN_SECTION_ROW_LEAVE
+ ROOM_SETTINGS_MAIN_SECTION_ROW_MUTE_NOTIFICATIONS
};
enum
@@ -515,7 +514,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
{
[sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_MUTE_NOTIFICATIONS];
}
- [sectionMain addRowWithTag:ROOM_SETTINGS_MAIN_SECTION_ROW_LEAVE];
[tmpSections addObject:sectionMain];
if (RiotSettings.shared.roomSettingsScreenAllowChangingAccessSettings)
@@ -2325,22 +2323,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
cell = favoriteCell;
}
}
- else if (row == ROOM_SETTINGS_MAIN_SECTION_ROW_LEAVE)
- {
- MXKTableViewCellWithButton *leaveCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCellWithButton defaultReuseIdentifier] forIndexPath:indexPath];
-
- NSString* title = [VectorL10n leave];
-
- [leaveCell.mxkButton setTitle:title forState:UIControlStateNormal];
- [leaveCell.mxkButton setTitle:title forState:UIControlStateHighlighted];
- [leaveCell.mxkButton setTintColor:ThemeService.shared.theme.tintColor];
- leaveCell.mxkButton.titleLabel.font = [UIFont systemFontOfSize:17];
-
- [leaveCell.mxkButton removeTarget:self action:nil forControlEvents:UIControlEventTouchUpInside];
- [leaveCell.mxkButton addTarget:self action:@selector(onLeave:) forControlEvents:UIControlEventTouchUpInside];
-
- cell = leaveCell;
- }
}
else if (section == SECTION_TAG_ACCESS)
{
@@ -3196,76 +3178,6 @@ NSString *const kRoomSettingsAdvancedE2eEnabledCellViewIdentifier = @"kRoomSetti
#pragma mark - actions
-- (void)onLeave:(id)sender
-{
- // Prompt user before leaving the room
- __weak typeof(self) weakSelf = self;
-
- [currentAlert dismissViewControllerAnimated:NO completion:nil];
-
- NSString *title, *message;
- if ([self.mainSession roomWithRoomId:self.roomId].isDirect)
- {
- title = [VectorL10n roomParticipantsLeavePromptTitleForDm];
- message = [VectorL10n roomParticipantsLeavePromptMsgForDm];
- }
- else
- {
- title = [VectorL10n roomParticipantsLeavePromptTitle];
- message = [VectorL10n roomParticipantsLeavePromptMsg];
- }
-
- currentAlert = [UIAlertController alertControllerWithTitle:title
- message:message
- preferredStyle:UIAlertControllerStyleAlert];
-
- [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
- style:UIAlertActionStyleCancel
- handler:^(UIAlertAction * action) {
-
- if (weakSelf)
- {
- typeof(self) self = weakSelf;
- self->currentAlert = nil;
- }
-
- }]];
-
- [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n leave]
- style:UIAlertActionStyleDefault
- handler:^(UIAlertAction * action) {
-
- if (weakSelf)
- {
- typeof(self) self = weakSelf;
- self->currentAlert = nil;
-
- [self startActivityIndicator];
- [self->mxRoom leave:^{
-
- if (self.delegate) {
- [self.delegate roomSettingsViewControllerDidLeaveRoom:self];
- } else {
- [[LegacyAppDelegate theDelegate] restoreInitialDisplay:nil];
- }
-
- } failure:^(NSError *error) {
-
- [self stopActivityIndicator];
-
- MXLogDebug(@"[RoomSettingsViewController] Leave room failed");
- // Alert user
- [[AppDelegate theDelegate] showErrorAsAlert:error];
-
- }];
- }
-
- }]];
-
- [currentAlert mxk_setAccessibilityIdentifier:@"RoomSettingsVCLeaveAlert"];
- [self presentViewController:currentAlert animated:YES completion:nil];
-}
-
- (void)onRoomAvatarTap:(UITapGestureRecognizer *)recognizer
{
SingleImagePickerPresenter *singleImagePickerPresenter = [[SingleImagePickerPresenter alloc] initWithSession:self.mainSession];
diff --git a/Riot/Modules/Room/TimelineCells/RoomCreation/RoomCreationCollapsedBubbleCell.m b/Riot/Modules/Room/TimelineCells/RoomCreation/RoomCreationCollapsedBubbleCell.m
index c06b34016..34e292dc3 100644
--- a/Riot/Modules/Room/TimelineCells/RoomCreation/RoomCreationCollapsedBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/RoomCreation/RoomCreationCollapsedBubbleCell.m
@@ -26,8 +26,6 @@
- (void)customizeTableViewCellRendering
{
[super customizeTableViewCellRendering];
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
}
@end
diff --git a/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipBubbleCell.m b/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipBubbleCell.m
index 2fe69ff5f..21ae1b2ad 100644
--- a/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipBubbleCell.m
@@ -37,13 +37,6 @@
xibPictureViewTopConstraintConstant = self.pictureViewTopConstraint.constant;
}
-- (void)customizeTableViewCellRendering
-{
- [super customizeTableViewCellRendering];
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
-}
-
- (void)prepareForReuse
{
[super prepareForReuse];
diff --git a/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipCollapsedBubbleCell.m b/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipCollapsedBubbleCell.m
index eac1df923..558ef7050 100644
--- a/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipCollapsedBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/RoomMembership/RoomMembershipCollapsedBubbleCell.m
@@ -26,13 +26,6 @@
@implementation RoomMembershipCollapsedBubbleCell
-- (void)customizeTableViewCellRendering
-{
- [super customizeTableViewCellRendering];
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
-}
-
- (void)layoutSubviews
{
[super layoutSubviews];
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Common/TextMessageBaseBubbleCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Common/TextMessageBaseBubbleCell.swift
index 5a7d8984b..d38b5cb3c 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Common/TextMessageBaseBubbleCell.swift
+++ b/Riot/Modules/Room/TimelineCells/Styles/Bubble/Cells/TextMessage/Common/TextMessageBaseBubbleCell.swift
@@ -51,14 +51,6 @@ class TextMessageBaseBubbleCell: SizableBaseRoomCell, RoomCellURLPreviewDisplaya
override func setupMessageTextViewLongPressGesture() {
// Do nothing, otherwise default setup prevent link tap
}
-
- override func update(theme: Theme) {
- super.update(theme: theme)
-
- if let messageTextView = self.messageTextView {
- messageTextView.tintColor = theme.tintColor
- }
- }
}
// MARK: - RoomCellTimestampDisplayable
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentBubbleCell.m
index 3dd06a1d8..6da4b9db1 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentBubbleCell.m
@@ -28,8 +28,6 @@
[super customizeTableViewCellRendering];
[self updateUserNameColor];
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
}
- (void)render:(MXKCellData *)cellData
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m
index 2a7a96788..d40aea21c 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithPaginationTitleBubbleCell.m
@@ -30,7 +30,6 @@
[self updateUserNameColor];
self.paginationLabel.textColor = ThemeService.shared.theme.tintColor;
self.paginationSeparatorView.backgroundColor = ThemeService.shared.theme.tintColor;
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
}
- (void)render:(MXKCellData *)cellData
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m
index 30b096e91..2ca481673 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Incoming/Clear/RoomIncomingAttachmentWithoutSenderInfoBubbleCell.m
@@ -23,13 +23,6 @@
@implementation RoomIncomingAttachmentWithoutSenderInfoBubbleCell
-- (void)customizeTableViewCellRendering
-{
- [super customizeTableViewCellRendering];
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
-}
-
+ (CGFloat)heightForCellData:(MXKCellData*)cellData withMaximumWidth:(CGFloat)maxWidth
{
CGFloat rowHeight = [self attachmentBubbleCellHeightForCellData:cellData withMaximumWidth:maxWidth];
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentBubbleCell.m
index 456305e9c..716e15c31 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentBubbleCell.m
@@ -28,8 +28,6 @@
[super customizeTableViewCellRendering];
[self updateUserNameColor];
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
}
- (void)render:(MXKCellData *)cellData
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m
index d78362a65..4cb4f3761 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m
@@ -30,7 +30,6 @@
[self updateUserNameColor];
self.paginationLabel.textColor = ThemeService.shared.theme.tintColor;
self.paginationSeparatorView.backgroundColor = ThemeService.shared.theme.tintColor;
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
}
- (void)render:(MXKCellData *)cellData
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleWithoutSenderNameBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleWithoutSenderNameBubbleCell.m
index 46ea41861..57cf00691 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleWithoutSenderNameBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithPaginationTitleWithoutSenderNameBubbleCell.m
@@ -22,11 +22,5 @@
@implementation RoomOutgoingAttachmentWithPaginationTitleWithoutSenderNameBubbleCell
-- (void)customizeTableViewCellRendering
-{
- [super customizeTableViewCellRendering];
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
-}
@end
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m
index 7021df5f6..ec91ee33d 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/FileAttachment/Outgoing/Clear/RoomOutgoingAttachmentWithoutSenderInfoBubbleCell.m
@@ -24,13 +24,6 @@
@implementation RoomOutgoingAttachmentWithoutSenderInfoBubbleCell
-- (void)customizeTableViewCellRendering
-{
- [super customizeTableViewCellRendering];
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
-}
-
- (void)render:(MXKCellData *)cellData
{
[super render:cellData];
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgBubbleCell.m
index 28bbdb19c..288e54d0a 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgBubbleCell.m
@@ -28,8 +28,6 @@
[super customizeTableViewCellRendering];
[self updateUserNameColor];
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
}
- (void)render:(MXKCellData *)cellData
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m
index c66531dce..9e7bc37c3 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleBubbleCell.m
@@ -30,7 +30,6 @@
[self updateUserNameColor];
self.paginationLabel.textColor = ThemeService.shared.theme.tintColor;
self.paginationSeparatorView.backgroundColor = ThemeService.shared.theme.tintColor;
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
}
- (void)render:(MXKCellData *)cellData
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m
index ae4126049..86862a0d0 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m
@@ -22,11 +22,4 @@
@implementation RoomIncomingTextMsgWithPaginationTitleWithoutSenderNameBubbleCell
-- (void)customizeTableViewCellRendering
-{
- [super customizeTableViewCellRendering];
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
-}
-
@end
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m
index db4370dc5..245b17e77 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderInfoBubbleCell.m
@@ -25,8 +25,6 @@
- (void)customizeTableViewCellRendering
{
[super customizeTableViewCellRendering];
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
}
@end
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderNameBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderNameBubbleCell.m
index 8678dd894..3f5d73475 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderNameBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Incoming/Clear/RoomIncomingTextMsgWithoutSenderNameBubbleCell.m
@@ -25,8 +25,6 @@
- (void)customizeTableViewCellRendering
{
[super customizeTableViewCellRendering];
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
}
@end
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgBubbleCell.m
index a31a52029..fa50aa35f 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgBubbleCell.m
@@ -28,8 +28,6 @@
[super customizeTableViewCellRendering];
[self updateUserNameColor];
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m
index 0bc9f1de3..dfafa3df6 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/TextMessage/Outgoing/Clear/RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.m
@@ -25,8 +25,6 @@
- (void)customizeTableViewCellRendering
{
[super customizeTableViewCellRendering];
-
- self.messageTextView.tintColor = ThemeService.shared.theme.tintColor;
}
@end
diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift
index 7956ad107..013602843 100644
--- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift
+++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift
@@ -44,7 +44,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
private var hostingViewController: VectorHostingController!
private var wysiwygViewModel = WysiwygComposerViewModel(
textColor: ThemeService.shared().theme.colors.primaryContent,
- linkColor: ThemeService.shared().theme.colors.accent,
+ linkColor: ThemeService.shared().theme.colors.links,
codeBackgroundColor: ThemeService.shared().theme.selectedBackgroundColor
)
private var viewModel: ComposerViewModelProtocol!
@@ -299,7 +299,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
private func update(theme: Theme) {
hostingViewController.view.backgroundColor = theme.colors.background
wysiwygViewModel.textColor = theme.colors.primaryContent
- wysiwygViewModel.linkColor = theme.colors.accent
+ wysiwygViewModel.linkColor = theme.colors.links
wysiwygViewModel.codeBackgroundColor = theme.selectedBackgroundColor
}
diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift
index 54262f828..c6dc8b8b4 100644
--- a/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift
+++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageMediaServiceProvider.swift
@@ -28,6 +28,7 @@ import MediaPlayer
private var roomAvatarLoader: MXMediaLoader?
private let audioPlayers: NSMapTable
private let audioRecorders: NSHashTable
+ private let nowPlayingInfoDelegates: NSMapTable
private var displayLink: CADisplayLink!
@@ -93,6 +94,7 @@ import MediaPlayer
private override init() {
audioPlayers = NSMapTable(valueOptions: .weakMemory)
audioRecorders = NSHashTable(options: .weakMemory)
+ nowPlayingInfoDelegates = NSMapTable(keyOptions: .weakMemory, valueOptions: .weakMemory)
activeAudioPlayers = Set()
super.init()
@@ -123,27 +125,54 @@ import MediaPlayer
pauseAllServicesExcept(nil)
}
+ func registerNowPlayingInfoDelegate(_ delegate: VoiceMessageNowPlayingInfoDelegate, forPlayer player: VoiceMessageAudioPlayer) {
+ nowPlayingInfoDelegates.setObject(delegate, forKey: player)
+ }
+
+ func deregisterNowPlayingInfoDelegate(forPlayer player: VoiceMessageAudioPlayer) {
+ nowPlayingInfoDelegates.removeObject(forKey: player)
+ }
+
// MARK: - VoiceMessageAudioPlayerDelegate
func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
currentlyPlayingAudioPlayer = audioPlayer
activeAudioPlayers.insert(audioPlayer)
- setUpRemoteCommandCenter()
+
+ let shouldSetupRemoteCommandCenter = nowPlayingInfoDelegates.object(forKey: audioPlayer)?.shouldSetupRemoteCommandCenter(audioPlayer: audioPlayer) ?? true
+ if shouldSetupRemoteCommandCenter {
+ setUpRemoteCommandCenter()
+ } else {
+ // clean up the remote command center
+ tearDownRemoteCommandCenter()
+ }
pauseAllServicesExcept(audioPlayer)
}
func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
if currentlyPlayingAudioPlayer == audioPlayer {
- currentlyPlayingAudioPlayer = nil
- tearDownRemoteCommandCenter()
+ // If we have a NowPlayingInfoDelegate for this player
+ let nowPlayingInfoDelegate = nowPlayingInfoDelegates.object(forKey: audioPlayer)
+
+ // ask the delegate if we should disconnect from NowPlayingInfoCenter (if there's no delegate, we consider it safe to disconnect it)
+ if nowPlayingInfoDelegate?.shouldDisconnectFromNowPlayingInfoCenter(audioPlayer: audioPlayer) ?? true {
+ currentlyPlayingAudioPlayer = nil
+ tearDownRemoteCommandCenter()
+ }
}
activeAudioPlayers.remove(audioPlayer)
}
func audioPlayerDidFinishPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
if currentlyPlayingAudioPlayer == audioPlayer {
- currentlyPlayingAudioPlayer = nil
- tearDownRemoteCommandCenter()
+ // If we have a NowPlayingInfoDelegate for this player
+ let nowPlayingInfoDelegate = nowPlayingInfoDelegates.object(forKey: audioPlayer)
+
+ // ask the delegate if we should disconnect from NowPlayingInfoCenter (if there's no delegate, we consider it safe to disconnect it)
+ if nowPlayingInfoDelegate?.shouldDisconnectFromNowPlayingInfoCenter(audioPlayer: audioPlayer) ?? true {
+ currentlyPlayingAudioPlayer = nil
+ tearDownRemoteCommandCenter()
+ }
}
activeAudioPlayers.remove(audioPlayer)
}
@@ -249,6 +278,17 @@ import MediaPlayer
let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
nowPlayingInfoCenter.nowPlayingInfo = nil
+ nowPlayingInfoCenter.playbackState = .stopped
+
+ let commandCenter = MPRemoteCommandCenter.shared()
+ commandCenter.playCommand.isEnabled = false
+ commandCenter.playCommand.removeTarget(nil)
+ commandCenter.pauseCommand.isEnabled = false
+ commandCenter.pauseCommand.removeTarget(nil)
+ commandCenter.skipForwardCommand.isEnabled = false
+ commandCenter.skipForwardCommand.removeTarget(nil)
+ commandCenter.skipBackwardCommand.isEnabled = false
+ commandCenter.skipBackwardCommand.removeTarget(nil)
}
private func updateNowPlayingInfoCenter() {
@@ -256,9 +296,14 @@ import MediaPlayer
return
}
- let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
- nowPlayingInfoCenter.nowPlayingInfo = [MPMediaItemPropertyTitle: VectorL10n.voiceMessageLockScreenPlaceholder,
- MPMediaItemPropertyPlaybackDuration: audioPlayer.duration as Any,
- MPNowPlayingInfoPropertyElapsedPlaybackTime: audioPlayer.currentTime as Any]
+ // Checks if we have a delegate for this player, or if we should update the NowPlayingInfoCenter ourselves
+ if let nowPlayingInfoDelegate = nowPlayingInfoDelegates.object(forKey: audioPlayer) {
+ nowPlayingInfoDelegate.updateNowPlayingInfoCenter(forPlayer: audioPlayer)
+ } else {
+ let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
+ nowPlayingInfoCenter.nowPlayingInfo = [MPMediaItemPropertyTitle: VectorL10n.voiceMessageLockScreenPlaceholder,
+ MPMediaItemPropertyPlaybackDuration: audioPlayer.duration as Any,
+ MPNowPlayingInfoPropertyElapsedPlaybackTime: audioPlayer.currentTime as Any]
+ }
}
}
diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoDelegate.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoDelegate.swift
new file mode 100644
index 000000000..959f8ca91
--- /dev/null
+++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageNowPlayingInfoDelegate.swift
@@ -0,0 +1,26 @@
+//
+// Copyright 2023 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 protocol VoiceMessageNowPlayingInfoDelegate {
+
+ func updateNowPlayingInfoCenter(forPlayer player: VoiceMessageAudioPlayer)
+
+ func shouldSetupRemoteCommandCenter(audioPlayer player: VoiceMessageAudioPlayer) -> Bool
+
+ func shouldDisconnectFromNowPlayingInfoCenter(audioPlayer: VoiceMessageAudioPlayer) -> Bool
+}
diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m
index 54bb95d34..90533cfd1 100644
--- a/Riot/Modules/Settings/SettingsViewController.m
+++ b/Riot/Modules/Settings/SettingsViewController.m
@@ -176,7 +176,8 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE)
LABS_ENABLE_NEW_SESSION_MANAGER,
LABS_ENABLE_NEW_CLIENT_INFO_FEATURE,
LABS_ENABLE_WYSIWYG_COMPOSER,
- LABS_ENABLE_VOICE_BROADCAST
+ LABS_ENABLE_VOICE_BROADCAST,
+ LABS_ENABLE_CRYPTO_SDK
};
typedef NS_ENUM(NSUInteger, SECURITY)
@@ -587,6 +588,13 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
if (BuildSettings.settingsScreenShowLabSettings)
{
Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS];
+ #if DEBUG
+ if (MXSDKOptions.sharedInstance.isCryptoSDKAvailable)
+ {
+ [sectionLabs addRowWithTag:LABS_ENABLE_CRYPTO_SDK];
+ }
+ #endif
+
[sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX];
[sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX];
[sectionLabs addRowWithTag:LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS];
@@ -2583,6 +2591,23 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
cell = labelAndSwitchCell;
}
+ else
+ {
+ #if DEBUG
+ if (row == LABS_ENABLE_CRYPTO_SDK)
+ {
+ MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
+ BOOL isEnabled = MXSDKOptions.sharedInstance.enableCryptoSDK;
+ labelAndSwitchCell.mxkLabel.text = isEnabled ? VectorL10n.settingsLabsDisableCryptoSdk : VectorL10n.settingsLabsEnableCryptoSdk;
+ labelAndSwitchCell.mxkSwitch.on = isEnabled;
+ [labelAndSwitchCell.mxkSwitch setEnabled:!isEnabled];
+ labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor;
+ [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableCryptoSDKFeature:) forControlEvents:UIControlEventTouchUpInside];
+
+ cell = labelAndSwitchCell;
+ }
+ #endif
+ }
}
else if (section == SECTION_TAG_SECURITY)
{
@@ -3354,6 +3379,37 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
RiotSettings.shared.enableVoiceBroadcast = sender.isOn;
}
+#if DEBUG
+- (void)toggleEnableCryptoSDKFeature:(UISwitch *)sender
+{
+ BOOL isEnabled = sender.isOn;
+ MXWeakify(self);
+
+ [currentAlert dismissViewControllerAnimated:NO completion:nil];
+ UIAlertController *confirmationAlert = [UIAlertController alertControllerWithTitle:nil
+ message:VectorL10n.settingsLabsConfirmCryptoSdk
+ preferredStyle:UIAlertControllerStyleAlert];
+
+ [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
+ MXStrongifyAndReturnIfNil(self);
+ self->currentAlert = nil;
+
+ [sender setOn:NO animated:YES];
+ }]];
+
+ [confirmationAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n continue] style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
+ MXStrongifyAndReturnIfNil(self);
+
+ RiotSettings.shared.enableCryptoSDK = isEnabled;
+ MXSDKOptions.sharedInstance.enableCryptoSDK = isEnabled;
+ [[AppDelegate theDelegate] reloadMatrixSessions:YES];
+ }]];
+
+ [self presentViewController:confirmationAlert animated:YES completion:nil];
+ currentAlert = confirmationAlert;
+}
+#endif
+
- (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender
{
RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn;
diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift
index f9afbadc1..39264b42c 100644
--- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift
+++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift
@@ -53,6 +53,8 @@ public class VoiceBroadcastAggregator {
private var voiceBroadcastInfoStartEventContent: VoiceBroadcastInfo!
private var voiceBroadcastSenderId: String!
+ public private(set) var voiceBroadcastLastChunkSequence: Int = 0
+
private var referenceEventsListener: Any?
private var events: [MXEvent] = []
@@ -168,7 +170,10 @@ public class VoiceBroadcastAggregator {
let state = VoiceBroadcastInfoState(rawValue: voiceBroadcastInfo.state) else {
return
}
-
+ // For .pause and .stopped, update the last chunk sequence
+ if [.stopped, .paused].contains(state) {
+ self.voiceBroadcastLastChunkSequence = voiceBroadcastInfo.lastChunkSequence
+ }
self.delegate?.voiceBroadcastAggregator(self, didReceiveState: state)
}
}
@@ -187,6 +192,7 @@ public class VoiceBroadcastAggregator {
}
self.events.removeAll()
+ self.voiceBroadcastLastChunkSequence = 0
let filteredChunk = response.chunk.filter { event in
event.sender == self.voiceBroadcastSenderId &&
diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift
index 8c707eab2..66bd6dd57 100644
--- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift
+++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift
@@ -186,7 +186,7 @@ public class VoiceBroadcastService: NSObject {
return
}
- self.room.sendStateEvent(.custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType),
+ let httpOperation = self.room.sendStateEvent(.custom(VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType),
content: stateEventContent, stateKey: stateKey) { [weak self] response in
guard let self = self else { return }
@@ -199,6 +199,9 @@ public class VoiceBroadcastService: NSObject {
}
taskCompleted()
}
+
+ // No retry to send the request
+ httpOperation.maxNumberOfTries = 0
}
}
}
@@ -295,7 +298,7 @@ extension MXRoom {
threadId: String? = nil,
sequence: UInt,
success: @escaping ((String?) -> Void),
- failure: @escaping ((Error?) -> Void)) -> MXHTTPOperation? {
+ failure: @escaping ((Swift.Error?) -> Void)) -> MXHTTPOperation? {
guard let relatesTo = MXEventContentRelatesTo(relationType: MXEventRelationTypeReference,
eventId: voiceBroadcastId).jsonDictionary() as? [String: Any] else {
failure(VoiceBroadcastServiceError.unknown)
diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m
index 654fab329..21cd496dd 100644
--- a/Riot/Utils/EventFormatter.m
+++ b/Riot/Utils/EventFormatter.m
@@ -359,7 +359,8 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm";
attributes:@{
NSLinkAttributeName: linkActionString,
NSForegroundColorAttributeName: self.sendingTextColor,
- NSFontAttributeName: self.encryptedMessagesTextFont
+ NSFontAttributeName: self.encryptedMessagesTextFont,
+ NSUnderlineStyleAttributeName: [NSNumber numberWithInt:NSUnderlineStyleSingle]
}]];
[attributedStringWithRerequestMessage appendAttributedString:
@@ -384,8 +385,6 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm";
[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@" %@", [VectorL10n eventFormatterMessageEditedMention]]
attributes:@{
NSLinkAttributeName: linkActionString,
- // NOTE: Color is curretly overidden by UIText.tintColor as we use `NSLinkAttributeName`.
- // If we use UITextView.linkTextAttributes to set link color we will also have the issue that color will be the same for all kind of links.
NSForegroundColorAttributeName: self.editionMentionTextColor,
NSFontAttributeName: self.editionMentionTextFont
}]];
@@ -487,6 +486,7 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm";
self.bingTextColor = ThemeService.shared.theme.noticeColor;
self.encryptingTextColor = ThemeService.shared.theme.textPrimaryColor;
self.sendingTextColor = ThemeService.shared.theme.textPrimaryColor;
+ self.linksColor = ThemeService.shared.theme.colors.links;
self.errorTextColor = ThemeService.shared.theme.textPrimaryColor;
self.showEditionMention = YES;
self.editionMentionTextColor = ThemeService.shared.theme.textSecondaryColor;
@@ -546,8 +546,8 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm";
}
#pragma mark - MXRoomSummaryUpdating
-- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withLastEvent:(MXEvent *)event eventState:(MXRoomState *)eventState roomState:(MXRoomState *)roomState {
-
+- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withLastEvent:(MXEvent *)event eventState:(MXRoomState *)eventState roomState:(MXRoomState *)roomState
+{
// Do not display voice broadcast chunk in last message.
if (event.eventType == MXEventTypeRoomMessage && event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType])
{
@@ -555,31 +555,94 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm";
}
// Update last message if we have a voice broadcast in the room.
- if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType])
+ MXEvent *lastVoiceBroadcastInfoEvent = [self lastVoiceBroadcastInfoEventWithEvent:event roomState:roomState];
+ if (lastVoiceBroadcastInfoEvent != nil)
{
- return [self session:session updateRoomSummary:summary withVoiceBroadcastInfoStateEvent:event roomState:roomState];
- }
- else
- {
- MXEvent *stateEvent = [roomState stateEventsWithType:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType].lastObject;
- if (stateEvent && ![VoiceBroadcastInfo isStoppedFor:[VoiceBroadcastInfo modelFromJSON: stateEvent.content].state])
+ MXEvent *voiceBroadcastInfoStartedEvent = [self voiceBroadcastInfoStartedEventWithEvent:lastVoiceBroadcastInfoEvent
+ roomId:summary.roomId
+ session:session];
+ if (voiceBroadcastInfoStartedEvent != nil
+ && !(voiceBroadcastInfoStartedEvent.isRedactedEvent || [voiceBroadcastInfoStartedEvent.eventId isEqualToString:event.redacts]))
{
- return [self session:session updateRoomSummary:summary withVoiceBroadcastInfoStateEvent:stateEvent roomState:roomState];
+ return [self session:session
+ updateRoomSummary:summary
+withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent
+ voiceBroadcastInfoStartedEvent:voiceBroadcastInfoStartedEvent roomState:roomState];
}
}
BOOL updated = [super session:session updateRoomSummary:summary withLastEvent:event eventState:eventState roomState:roomState];
- if (updated) {
+ if (updated)
+ {
// Force the default text color for the last message (cancel highlighted message color)
NSMutableAttributedString *lastEventDescription = [[NSMutableAttributedString alloc] initWithAttributedString:summary.lastMessage.attributedText];
- [lastEventDescription addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.textSecondaryColor range:NSMakeRange(0, lastEventDescription.length)];
+ [lastEventDescription addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.textSecondaryColor
+ range:NSMakeRange(0, lastEventDescription.length)];
summary.lastMessage.attributedText = lastEventDescription;
}
return updated;
}
+
+- (MXEvent *)lastVoiceBroadcastInfoEventWithEvent:(MXEvent *)event roomState:(MXRoomState *)roomState
+{
+ MXEvent *voiceBroadcastInfoEvent = nil;
+ VoiceBroadcastInfo *info = nil;
+ if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType])
+ {
+ info = [VoiceBroadcastInfo modelFromJSON: event.content];
+
+ if (info != nil)
+ {
+ voiceBroadcastInfoEvent = event;
+ }
+ }
+ else
+ {
+ MXEvent *stateEvent = [roomState stateEventsWithType:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType].lastObject;
+ if (stateEvent != nil)
+ {
+ info = [VoiceBroadcastInfo modelFromJSON: stateEvent.content];
+ if (info != nil && ![VoiceBroadcastInfo isStoppedFor:info.state])
+ {
+ voiceBroadcastInfoEvent = stateEvent;
+ }
+ }
+ }
+
+ return voiceBroadcastInfoEvent;
+}
+
+- (MXEvent *)voiceBroadcastInfoStartedEventWithEvent:(MXEvent *)voiceBroadcastInfoEvent roomId:(NSString *)roomId session:(MXSession *)session
+{
+ VoiceBroadcastInfo *voiceBroadcastInfo = [VoiceBroadcastInfo modelFromJSON: voiceBroadcastInfoEvent.content];
+ if ([VoiceBroadcastInfo isStartedFor:voiceBroadcastInfo.state])
+ {
+ return voiceBroadcastInfoEvent;
+ }
+ else
+ {
+ dispatch_group_t group = dispatch_group_create();
+ dispatch_group_enter(group);
+
+ __block MXEvent *voiceBroadcastInfoStartedEvent;
+
+ [session eventWithEventId:voiceBroadcastInfo.voiceBroadcastId inRoom:roomId success:^(MXEvent *resultEvent) {
+ voiceBroadcastInfoStartedEvent = resultEvent;
+ dispatch_group_leave(group);
+ } failure:^(NSError *error) {
+ MXLogErrorDetails(@"[EventFormatter] Fetch eventWithEventId with error = %@", error.description);
+ dispatch_group_leave(group);
+ }];
+
+ dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
+
+ return voiceBroadcastInfoStartedEvent;
+ }
+}
+
- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withStateEvents:(NSArray *)stateEvents roomState:(MXRoomState *)roomState
{
BOOL updated = [super session:session updateRoomSummary:summary withStateEvents:stateEvents roomState:roomState];
@@ -603,18 +666,29 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm";
return updated;
}
-- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withVoiceBroadcastInfoStateEvent:(MXEvent *)stateEvent roomState:(MXRoomState *)roomState
+- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withVoiceBroadcastInfoStateEvent:(MXEvent *)stateEvent voiceBroadcastInfoStartedEvent:(MXEvent *)voiceBroadcastInfoStartedEvent roomState:(MXRoomState *)roomState
{
- [summary updateLastMessage:[[MXRoomLastMessage alloc] initWithEvent:stateEvent]];
- if (summary.lastMessage.others == nil)
+ BOOL isStoppedVoiceBroadcast = [VoiceBroadcastInfo isStoppedFor:[VoiceBroadcastInfo modelFromJSON: stateEvent.content].state];
+
+ if ([summary.lastMessage.eventId isEqualToString:voiceBroadcastInfoStartedEvent.eventId])
{
- summary.lastMessage.others = [NSMutableDictionary dictionary];
+ if (!isStoppedVoiceBroadcast)
+ {
+ return NO;
+ }
+ }
+ else
+ {
+ [summary updateLastMessage:[[MXRoomLastMessage alloc] initWithEvent:voiceBroadcastInfoStartedEvent]];
+ if (summary.lastMessage.others == nil)
+ {
+ summary.lastMessage.others = [NSMutableDictionary dictionary];
+ }
}
- summary.lastMessage.others[@"lastEventDate"] = [self dateStringFromEvent:stateEvent withTime:YES];
NSAttributedString *attachmentString = nil;
UIColor *textColor;
- if ([VoiceBroadcastInfo isStoppedFor:[VoiceBroadcastInfo modelFromJSON: stateEvent.content].state])
+ if (isStoppedVoiceBroadcast)
{
textColor = ThemeService.shared.theme.textSecondaryColor;
NSString *senderDisplayName;
@@ -627,6 +701,7 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm";
senderDisplayName = [self senderDisplayNameForEvent:stateEvent withRoomState:roomState];
summary.lastMessage.text = [VectorL10n noticeVoiceBroadcastEnded:senderDisplayName];
}
+ summary.lastMessage.others[@"lastEventDate"] = [self dateStringFromEvent:stateEvent withTime:YES];
}
else
{
@@ -638,6 +713,7 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm";
attachmentString = [NSAttributedString attributedStringWithAttachment:attachment];
summary.lastMessage.text = VectorL10n.noticeVoiceBroadcastLive;
+ summary.lastMessage.others[@"lastEventDate"] = [self dateStringFromEvent:voiceBroadcastInfoStartedEvent withTime:YES];
}
// Compute the attribute text message
diff --git a/RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h b/RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h
index 6409af92f..5db20c745 100644
--- a/RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h
+++ b/RiotNSE/SupportingFiles/RiotNSE-Bridging-Header.h
@@ -23,4 +23,6 @@
#import "BuildInfo.h"
+#import "ThemeService.h"
+
#endif /* RiotNSE_Bridging_Header_h */
diff --git a/RiotNSE/target.yml b/RiotNSE/target.yml
index 415f8447a..ae27022c3 100644
--- a/RiotNSE/target.yml
+++ b/RiotNSE/target.yml
@@ -78,3 +78,5 @@ targets:
- "**/*.md" # excludes all files with the .md extension
- path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift
- path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/MatrixSDK
+ - path: ../Riot/Managers/Theme
+ - path: ../Riot/Categories/UIColor.swift
diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift
index 7d222aa31..559c67d17 100644
--- a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift
+++ b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift
@@ -47,7 +47,7 @@ enum MockAnalyticsPromptScreenState: MockScreenState, CaseIterable {
return (
[promptType, viewModel],
AnyView(AnalyticsPrompt(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift b/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift
index 02369980c..ea4488244 100644
--- a/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift
+++ b/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift
@@ -32,10 +32,6 @@ class AvatarService: AvatarServiceProtocol {
private let mediaManager: MXMediaManager
- static func instantiate(mediaManager: MXMediaManager) -> AvatarServiceProtocol {
- AvatarService(mediaManager: mediaManager)
- }
-
init(mediaManager: MXMediaManager) {
self.mediaManager = mediaManager
}
diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift
index 4f51f574a..b143d4d30 100644
--- a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift
+++ b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift
@@ -19,8 +19,7 @@ import SwiftUI
struct AvatarImage: View {
@Environment(\.theme) var theme: ThemeSwiftUI
- @Environment(\.dependencies) var dependencies: DependencyContainer
- @StateObject var viewModel = AvatarViewModel()
+ @EnvironmentObject var viewModel: AvatarViewModel
var mxContentUri: String?
var matrixItemId: String
@@ -43,7 +42,6 @@ struct AvatarImage: View {
.frame(maxWidth: CGFloat(size.rawValue), maxHeight: CGFloat(size.rawValue))
.clipShape(Circle())
.onAppear {
- viewModel.inject(dependencies: dependencies)
viewModel.loadAvatar(
mxContentUri: mxContentUri,
matrixItemId: matrixItemId,
@@ -95,7 +93,7 @@ struct AvatarImage_Previews: PreviewProvider {
AvatarImage(mxContentUri: nil, matrixItemId: name, displayName: name, size: .xLarge)
}
}
- .addDependency(MockAvatarService.example)
+ .environmentObject(AvatarViewModel.withMockedServices())
}
}
}
diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift
index 8e967fe18..2662831e1 100644
--- a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift
+++ b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift
@@ -19,8 +19,7 @@ import SwiftUI
struct SpaceAvatarImage: View {
@Environment(\.theme) var theme: ThemeSwiftUI
- @Environment(\.dependencies) var dependencies: DependencyContainer
- @StateObject var viewModel = AvatarViewModel()
+ @EnvironmentObject var viewModel: AvatarViewModel
var mxContentUri: String?
var matrixItemId: String
@@ -59,7 +58,6 @@ struct SpaceAvatarImage: View {
)
})
.onAppear {
- viewModel.inject(dependencies: dependencies)
viewModel.loadAvatar(
mxContentUri: mxContentUri,
matrixItemId: matrixItemId,
@@ -99,7 +97,7 @@ struct LiveAvatarImage_Previews: PreviewProvider {
SpaceAvatarImage(mxContentUri: nil, matrixItemId: name, displayName: name, size: .xLarge)
}
}
- .addDependency(MockAvatarService.example)
+ .environmentObject(AvatarViewModel.withMockedServices())
}
}
}
diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift
index 433fb9cba..10055738d 100644
--- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift
+++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift
@@ -19,11 +19,15 @@ import DesignKit
import Foundation
/// Simple ViewModel that supports loading an avatar image
-class AvatarViewModel: InjectableObject, ObservableObject {
- @Inject var avatarService: AvatarServiceProtocol
+final class AvatarViewModel: ObservableObject {
+ private let avatarService: AvatarServiceProtocol
@Published private(set) var viewState = AvatarViewState.empty
+ init(avatarService: AvatarServiceProtocol) {
+ self.avatarService = avatarService
+ }
+
private var cancellables = Set()
/// Load an avatar
@@ -58,3 +62,9 @@ class AvatarViewModel: InjectableObject, ObservableObject {
.store(in: &cancellables)
}
}
+
+extension AvatarViewModel {
+ static func withMockedServices() -> AvatarViewModel {
+ .init(avatarService: MockAvatarService.example)
+ }
+}
diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift
deleted file mode 100644
index d09fa87f4..000000000
--- a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift
+++ /dev/null
@@ -1,46 +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 Foundation
-
-/// Used for storing and resolving dependencies at runtime.
-struct DependencyContainer {
- // Stores the dependencies with type information removed.
- private var dependencyStore: [String: Any] = [:]
-
- /// Resolve a dependency by type.
- ///
- /// Given a particular `Type` (Inferred from return type),
- /// generate a key and retrieve from storage.
- ///
- /// - Returns: The resolved dependency.
- func resolve() -> T {
- let key = String(describing: T.self)
- guard let t = dependencyStore[key] as? T else {
- fatalError("No provider registered for type \(T.self)")
- }
- return t
- }
-
- /// Register a dependency.
- ///
- /// Given a dependency, generate a key from it's `Type` and save in storage.
- /// - Parameter dependency: The dependency to register.
- mutating func register(dependency: T) {
- let key = String(describing: T.self)
- dependencyStore[key] = dependency
- }
-}
diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift
deleted file mode 100644
index 4bde8956e..000000000
--- a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift
+++ /dev/null
@@ -1,48 +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 Foundation
-import SwiftUI
-
-/// An Environment Key for retrieving runtime dependencies.
-///
-/// Dependencies are to be injected into `ObservableObjects`
-/// that are owned by a View (i.e. `@StateObject`'s, such as ViewModels owned by the View).
-private struct DependencyContainerKey: EnvironmentKey {
- static let defaultValue = DependencyContainer()
-}
-
-extension EnvironmentValues {
- var dependencies: DependencyContainer {
- get { self[DependencyContainerKey.self] }
- set { self[DependencyContainerKey.self] = newValue }
- }
-}
-
-extension View {
- /// A modifier for adding a dependency to the SwiftUI view hierarchy's dependency container.
- ///
- /// Important: When adding a dependency to cast it to the type in which it will be injected.
- /// So if adding `MockDependency` but type at injection is `Dependency` remember to cast
- /// to `Dependency` first.
- /// - Parameter dependency: The dependency to add.
- /// - Returns: The wrapped view that now includes the dependency.
- func addDependency(_ dependency: T) -> some View {
- transformEnvironment(\.dependencies) { container in
- container.register(dependency: dependency)
- }
- }
-}
diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift
deleted file mode 100644
index d45907eeb..000000000
--- a/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift
+++ /dev/null
@@ -1,43 +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 Foundation
-
-/// A property wrapped used to inject from the dependency container on the instance, to instance properties.
-///
-/// ```
-/// @Inject var someClass: SomeClass
-/// ```
-@propertyWrapper struct Inject {
- static subscript(_enclosingInstance instance: T,
- wrapped wrappedKeyPath: ReferenceWritableKeyPath,
- storage storageKeyPath: ReferenceWritableKeyPath) -> Value {
- get {
- // Resolve dependencies from enclosing instance's `dependencies` property
- let v: Value = instance.dependencies.resolve()
- return v
- }
- set {
- fatalError("Only subscript get is supported for injection")
- }
- }
-
- @available(*, unavailable, message: "This property wrapper can only be applied to classes")
- var wrappedValue: Value {
- get { fatalError("wrappedValue get not used") }
- set { fatalError("wrappedValue set not used. \(newValue)") }
- }
-}
diff --git a/RiotSwiftUI/Modules/Common/InfoSheet/MockInfoSheetScreenState.swift b/RiotSwiftUI/Modules/Common/InfoSheet/MockInfoSheetScreenState.swift
index 62d86a681..b34c744c3 100644
--- a/RiotSwiftUI/Modules/Common/InfoSheet/MockInfoSheetScreenState.swift
+++ b/RiotSwiftUI/Modules/Common/InfoSheet/MockInfoSheetScreenState.swift
@@ -51,7 +51,7 @@ enum MockInfoSheetScreenState: MockScreenState, CaseIterable {
return (
[model, viewModel],
AnyView(InfoSheet(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
index eaee22e73..ba1d91e52 100644
--- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
+++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift
@@ -72,6 +72,7 @@ enum MockAppScreens {
MockComposerScreenState.self,
MockComposerCreateActionListScreenState.self,
MockComposerLinkActionScreenState.self,
- MockVoiceBroadcastPlaybackScreenState.self
+ MockVoiceBroadcastPlaybackScreenState.self,
+ MockPollHistoryScreenState.self
]
}
diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift
index fc5c6b87a..4bd1f7c52 100644
--- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift
+++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift
@@ -53,7 +53,7 @@ final class LiveLocationSharingViewerCoordinator: Coordinator, Presentable {
service: service
)
let view = LiveLocationSharingViewer(viewModel: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager)))
liveLocationSharingViewerViewModel = viewModel
liveLocationSharingViewerHostingController = VectorHostingController(rootView: view)
diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift
index ea3e1b908..9cd5853c7 100644
--- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift
+++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift
@@ -86,7 +86,7 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
)
let view = LocationSharingView(context: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager)))
locationSharingViewModel = viewModel
locationSharingHostingController = VectorHostingController(rootView: view)
diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift
index 3b86f4e89..c59e0bf4b 100644
--- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift
+++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift
@@ -34,6 +34,6 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable {
isLiveLocationSharingEnabled: true, service: locationSharingService)
return ([viewModel],
AnyView(LocationSharingView(context: viewModel.context)
- .addDependency(MockAvatarService.example)))
+ .environmentObject(AvatarViewModel.withMockedServices())))
}
}
diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift
index b125fbcd6..bf938dac8 100644
--- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift
+++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift
@@ -56,7 +56,8 @@ final class StaticLocationViewingCoordinator: Coordinator, Presentable {
coordinateType: parameters.coordinateType
)
let view = StaticLocationView(viewModel: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager)))
+
staticLocationViewingViewModel = viewModel
staticLocationViewingHostingController = VectorHostingController(rootView: view)
}
diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift
index 2ea0f0aec..4430c36c2 100644
--- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift
+++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift
@@ -50,6 +50,6 @@ enum MockStaticLocationViewingScreenState: MockScreenState, CaseIterable {
return ([viewModel],
AnyView(StaticLocationView(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example)))
+ .environmentObject(AvatarViewModel.withMockedServices())))
}
}
diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift
index 3c972b602..73fb8e668 100644
--- a/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift
+++ b/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift
@@ -57,7 +57,7 @@ enum MockOnboardingAvatarScreenState: MockScreenState, CaseIterable {
return (
[self, viewModel],
AnyView(OnboardingAvatarScreen(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift
index d911f8249..e0bca18c1 100644
--- a/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift
+++ b/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift
@@ -39,7 +39,7 @@ enum MockOnboardingCelebrationScreenState: MockScreenState, CaseIterable {
return (
[self, viewModel],
AnyView(OnboardingCelebrationScreen(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift
index ed580b834..c1ac46265 100644
--- a/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift
+++ b/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift
@@ -45,7 +45,7 @@ enum MockOnboardingUseCaseSelectionScreenState: MockScreenState, CaseIterable {
return (
[self, viewModel],
AnyView(OnboardingUseCaseSelectionScreen(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift
index b47c5bcd5..fdf92cab5 100644
--- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Model/ComposerLinkActionModel.swift
@@ -59,20 +59,31 @@ extension ComposerLinkActionViewState {
}
var isSaveButtonDisabled: Bool {
- guard isValidLink else { return true }
+ guard !bindings.linkUrl.isEmpty else { return true }
switch linkAction {
case .createWithText: return bindings.text.isEmpty
- default: return false
+ case .create: return false
+ case .edit: return !bindings.hasEditedUrl
}
}
-
- private var isValidLink: Bool {
- guard let url = URL(string: bindings.linkUrl) else { return false }
- return UIApplication.shared.canOpenURL(url)
- }
}
struct ComposerLinkActionBindings {
var text: String
- var linkUrl: String
+
+ private let initialLinkUrl: String
+ fileprivate var hasEditedUrl = false
+ var linkUrl: String {
+ didSet {
+ if !hasEditedUrl && linkUrl != initialLinkUrl {
+ hasEditedUrl = true
+ }
+ }
+ }
+
+ init(text: String, linkUrl: String) {
+ self.text = text
+ self.linkUrl = linkUrl
+ self.initialLinkUrl = linkUrl
+ }
}
diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/UI/ComposerLinkActionUITests.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/UI/ComposerLinkActionUITests.swift
index f30dacf30..c18405951 100644
--- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/UI/ComposerLinkActionUITests.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/UI/ComposerLinkActionUITests.swift
@@ -29,9 +29,7 @@ final class ComposerLinkActionUITests: MockScreenTestCase {
let linkTextField = app.textFields["linkTextField"]
XCTAssertTrue(linkTextField.exists)
linkTextField.tap()
- linkTextField.typeText("invalid url")
- XCTAssertFalse(saveButton.isEnabled)
- linkTextField.clearAndTypeText("https://element.io")
+ linkTextField.clearAndTypeText("element.io")
XCTAssertTrue(saveButton.isEnabled)
}
@@ -47,7 +45,7 @@ final class ComposerLinkActionUITests: MockScreenTestCase {
let linkTextField = app.textFields["linkTextField"]
XCTAssertTrue(linkTextField.exists)
linkTextField.tap()
- linkTextField.typeText("https://element.io")
+ linkTextField.typeText("element.io")
XCTAssertFalse(saveButton.isEnabled)
textTextField.tap()
textTextField.typeText("test")
@@ -60,13 +58,15 @@ final class ComposerLinkActionUITests: MockScreenTestCase {
XCTAssertTrue(app.buttons[VectorL10n.cancel].exists)
let saveButton = app.buttons[VectorL10n.save]
XCTAssertTrue(saveButton.exists)
- XCTAssertTrue(saveButton.isEnabled)
+ XCTAssertFalse(saveButton.isEnabled)
XCTAssertFalse(app.textFields["textTextField"].exists)
let linkTextField = app.textFields["linkTextField"]
XCTAssertTrue(linkTextField.exists)
let value = linkTextField.value as? String
XCTAssertEqual(value, "https://element.io")
- linkTextField.clearAndTypeText("invalid url")
+ linkTextField.clearAndTypeText("")
XCTAssertFalse(saveButton.isEnabled)
+ linkTextField.clearAndTypeText("matrix.org")
+ XCTAssertTrue(saveButton.isEnabled)
}
}
diff --git a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift
index 40ad27358..2407eccc4 100644
--- a/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/LinkAction/Test/Unit/ComposerLinkActionViewModelTests.swift
@@ -53,29 +53,20 @@ final class ComposerLinkActionViewModelTests: XCTestCase {
}
func testEditDefaultState() {
- let link = "https://element.io"
+ let link = "element.io"
setUp(with: .edit(link: link))
XCTAssertEqual(context.viewState.bindings.text, "")
XCTAssertEqual(context.viewState.bindings.linkUrl, link)
- XCTAssertFalse(context.viewState.isSaveButtonDisabled)
+ XCTAssertTrue(context.viewState.isSaveButtonDisabled)
XCTAssertTrue(context.viewState.shouldDisplayRemoveButton)
XCTAssertFalse(context.viewState.shouldDisplayTextField)
XCTAssertEqual(context.viewState.title, VectorL10n.wysiwygComposerLinkActionEditTitle)
}
- func testUrlValidityCheck() {
- setUp(with: .create)
- XCTAssertTrue(context.viewState.isSaveButtonDisabled)
- context.linkUrl = "invalid url"
- XCTAssertTrue(context.viewState.isSaveButtonDisabled)
- context.linkUrl = "https://element.io"
- XCTAssertFalse(context.viewState.isSaveButtonDisabled)
- }
-
func testTextNotEmptyCheck() {
setUp(with: .createWithText)
XCTAssertTrue(context.viewState.isSaveButtonDisabled)
- context.linkUrl = "https://element.io"
+ context.linkUrl = "element.io"
XCTAssertTrue(context.viewState.isSaveButtonDisabled)
context.text = "text"
XCTAssertFalse(context.viewState.isSaveButtonDisabled)
@@ -92,7 +83,7 @@ final class ComposerLinkActionViewModelTests: XCTestCase {
}
func testRemoveAction() {
- setUp(with: .edit(link: "https://element.io"))
+ setUp(with: .edit(link: "element.io"))
var result: ComposerLinkActionViewModelResult!
viewModel.callback = { value in
result = value
@@ -107,7 +98,7 @@ final class ComposerLinkActionViewModelTests: XCTestCase {
viewModel.callback = { value in
result = value
}
- let link = "https://element.io"
+ let link = "element.io"
context.linkUrl = link
context.send(viewAction: .save)
XCTAssertEqual(result, .performOperation(.setLink(urlString: link)))
@@ -119,7 +110,7 @@ final class ComposerLinkActionViewModelTests: XCTestCase {
viewModel.callback = { value in
result = value
}
- let link = "https://element.io"
+ let link = "element.io"
context.linkUrl = link
let text = "test"
context.text = text
@@ -128,13 +119,15 @@ final class ComposerLinkActionViewModelTests: XCTestCase {
}
func testSaveActionForEdit() {
- setUp(with: .edit(link: "https://element.io"))
+ setUp(with: .edit(link: "element.io"))
var result: ComposerLinkActionViewModelResult!
viewModel.callback = { value in
result = value
}
- let link = "https://matrix.org"
+ XCTAssertTrue(context.viewState.isSaveButtonDisabled)
+ let link = "matrix.org"
context.linkUrl = link
+ XCTAssertFalse(context.viewState.isSaveButtonDisabled)
context.send(viewAction: .save)
XCTAssertEqual(result, .performOperation(.setLink(urlString: link)))
}
diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift
index bc2e8771d..5525e9940 100644
--- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift
@@ -34,7 +34,11 @@ enum FormatType {
case italic
case underline
case strikethrough
+ case unorderedList
+ case orderedList
case inlineCode
+ case codeBlock
+ case quote
case link
}
@@ -54,14 +58,22 @@ extension FormatItem {
return Asset.Images.bold.name
case .italic:
return Asset.Images.italic.name
- case .strikethrough:
- return Asset.Images.strikethrough.name
case .underline:
return Asset.Images.underlined.name
- case .link:
- return Asset.Images.link.name
+ case .strikethrough:
+ return Asset.Images.strikethrough.name
+ case .unorderedList:
+ return Asset.Images.bulletList.name
+ case .orderedList:
+ return Asset.Images.numberedList.name
case .inlineCode:
return Asset.Images.code.name
+ case .codeBlock:
+ return Asset.Images.codeBlock.name
+ case .quote:
+ return Asset.Images.quote.name
+ case .link:
+ return Asset.Images.link.name
}
}
@@ -71,14 +83,22 @@ extension FormatItem {
return "boldButton"
case .italic:
return "italicButton"
- case .strikethrough:
- return "strikethroughButton"
case .underline:
return "underlineButton"
- case .link:
- return "linkButton"
+ case .strikethrough:
+ return "strikethroughButton"
+ case .unorderedList:
+ return "unorderedListButton"
+ case .orderedList:
+ return "orderedListButton"
case .inlineCode:
return "inlineCodeButton"
+ case .codeBlock:
+ return "codeBlockButton"
+ case .quote:
+ return "quoteButton"
+ case .link:
+ return "linkButton"
}
}
@@ -88,14 +108,22 @@ extension FormatItem {
return VectorL10n.wysiwygComposerFormatActionBold
case .italic:
return VectorL10n.wysiwygComposerFormatActionItalic
- case .strikethrough:
- return VectorL10n.wysiwygComposerFormatActionStrikethrough
case .underline:
return VectorL10n.wysiwygComposerFormatActionUnderline
- case .link:
- return VectorL10n.wysiwygComposerFormatActionLink
+ case .strikethrough:
+ return VectorL10n.wysiwygComposerFormatActionStrikethrough
+ case .unorderedList:
+ return VectorL10n.wysiwygComposerFormatActionUnorderedList
+ case .orderedList:
+ return VectorL10n.wysiwygComposerFormatActionOrderedList
case .inlineCode:
return VectorL10n.wysiwygComposerFormatActionInlineCode
+ case .codeBlock:
+ return VectorL10n.wysiwygComposerFormatActionCodeBlock
+ case .quote:
+ return VectorL10n.wysiwygComposerFormatActionQuote
+ case .link:
+ return VectorL10n.wysiwygComposerFormatActionLink
}
}
}
@@ -108,14 +136,22 @@ extension FormatType {
return .bold
case .italic:
return .italic
- case .strikethrough:
- return .strikeThrough
case .underline:
return .underline
- case .link:
- return .link
+ case .strikethrough:
+ return .strikeThrough
+ case .unorderedList:
+ return .unorderedList
+ case .orderedList:
+ return .orderedList
case .inlineCode:
return .inlineCode
+ case .codeBlock:
+ return .codeBlock
+ case .quote:
+ return .quote
+ case .link:
+ return .link
}
}
@@ -127,14 +163,22 @@ extension FormatType {
return .bold
case .italic:
return .italic
- case .strikethrough:
- return .strikeThrough
case .underline:
return .underline
- case .link:
- return .link
+ case .strikethrough:
+ return .strikeThrough
+ case .unorderedList:
+ return .unorderedList
+ case .orderedList:
+ return .orderedList
case .inlineCode:
return .inlineCode
+ case .codeBlock:
+ return .codeBlock
+ case .quote:
+ return .quote
+ case .link:
+ return .link
}
}
}
@@ -167,5 +211,3 @@ final class LinkActionWrapper: NSObject {
super.init()
}
}
-
-
diff --git a/RiotSwiftUI/Modules/Room/Composer/View/FormattingToolbar.swift b/RiotSwiftUI/Modules/Room/Composer/View/FormattingToolbar.swift
index d8670ee0c..e7d59a989 100644
--- a/RiotSwiftUI/Modules/Room/Composer/View/FormattingToolbar.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/View/FormattingToolbar.swift
@@ -32,21 +32,23 @@ struct FormattingToolbar: View {
var formatAction: (FormatType) -> Void
var body: some View {
- HStack(spacing: 4) {
- ForEach(formatItems) { item in
- Button {
- formatAction(item.type)
- } label: {
- Image(item.icon)
- .renderingMode(.template)
- .foregroundColor(getForegroundColor(for: item))
+ ScrollView(.horizontal) {
+ HStack(spacing: 4) {
+ ForEach(formatItems) { item in
+ Button {
+ formatAction(item.type)
+ } label: {
+ Image(item.icon)
+ .renderingMode(.template)
+ .foregroundColor(getForegroundColor(for: item))
+ }
+ .disabled(item.state == .disabled)
+ .frame(width: 44, height: 44)
+ .background(getBackgroundColor(for: item))
+ .cornerRadius(8)
+ .accessibilityIdentifier(item.accessibilityIdentifier)
+ .accessibilityLabel(item.accessibilityLabel)
}
- .disabled(item.state == .disabled)
- .frame(width: 44, height: 44)
- .background(getBackgroundColor(for: item))
- .cornerRadius(8)
- .accessibilityIdentifier(item.accessibilityIdentifier)
- .accessibilityLabel(item.accessibilityLabel)
}
}
}
diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift
index e0d909898..9a25f8689 100644
--- a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift
+++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift
@@ -51,7 +51,7 @@ final class RoomNotificationSettingsCoordinator: RoomNotificationSettingsCoordin
)
let avatarService: AvatarServiceProtocol = AvatarService(mediaManager: room.mxSession.mediaManager)
let view = RoomNotificationSettings(viewModel: viewModel, presentedModally: presentedModally)
- .addDependency(avatarService)
+ .environmentObject(AvatarViewModel(avatarService: avatarService))
let viewController = VectorHostingController(rootView: view)
roomNotificationSettingsViewModel = viewModel
roomNotificationSettingsViewController = viewController
diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift
index 3034f50db..20a6406e3 100644
--- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift
+++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift
@@ -85,13 +85,13 @@ struct RoomNotificationSettings_Previews: PreviewProvider {
NavigationView {
RoomNotificationSettings(viewModel: mockViewModel, presentedModally: true)
.navigationBarTitleDisplayMode(.inline)
- .addDependency(MockAvatarService.example)
+ .environmentObject(AvatarViewModel.withMockedServices())
}
NavigationView {
RoomNotificationSettings(viewModel: mockViewModel, presentedModally: true)
.navigationBarTitleDisplayMode(.inline)
.theme(ThemeIdentifier.dark)
- .addDependency(MockAvatarService.example)
+ .environmentObject(AvatarViewModel.withMockedServices())
}
}
}
diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift
index 20066b961..835c9bdd7 100644
--- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift
+++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift
@@ -43,6 +43,6 @@ struct RoomNotificationSettingsHeader_Previews: PreviewProvider {
static let name = "Element"
static var previews: some View {
RoomNotificationSettingsHeader(avatarData: MockAvatarInput.example, displayName: name)
- .addDependency(MockAvatarService.example)
+ .environmentObject(AvatarViewModel.withMockedServices())
}
}
diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift
new file mode 100644
index 000000000..b9129a6e9
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/PollHistory/Coordinator/PollHistoryCoordinator.swift
@@ -0,0 +1,55 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import CommonKit
+import SwiftUI
+
+struct PollHistoryCoordinatorParameters {
+ let mode: PollHistoryMode
+}
+
+final class PollHistoryCoordinator: Coordinator, Presentable {
+ private let parameters: PollHistoryCoordinatorParameters
+ private let pollHistoryHostingController: UIViewController
+ private var pollHistoryViewModel: PollHistoryViewModelProtocol
+
+ // Must be used only internally
+ var childCoordinators: [Coordinator] = []
+ var completion: (() -> Void)?
+
+ init(parameters: PollHistoryCoordinatorParameters) {
+ self.parameters = parameters
+
+ #warning("replace with the real service after that it's done")
+ let viewModel = PollHistoryViewModel(mode: parameters.mode, pollService: MockPollHistoryService())
+ let view = PollHistory(viewModel: viewModel.context)
+ pollHistoryViewModel = viewModel
+ pollHistoryHostingController = VectorHostingController(rootView: view)
+ }
+
+ // MARK: - Public
+
+ func start() {
+ MXLog.debug("[PollHistoryCoordinator] did start.")
+ pollHistoryViewModel.completion = { [weak self] result in
+ self?.completion?()
+ }
+ }
+
+ func toPresentable() -> UIViewController {
+ pollHistoryHostingController
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift
new file mode 100644
index 000000000..65d393957
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/PollHistory/MockPollHistoryScreenState.swift
@@ -0,0 +1,64 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import SwiftUI
+
+/// Using an enum for the screen allows you define the different state cases with
+/// the relevant associated data for each case.
+enum MockPollHistoryScreenState: MockScreenState, CaseIterable {
+ // A case for each state you want to represent
+ // with specific, minimal associated data that will allow you
+ // mock that screen.
+ case active
+ case past
+ case activeEmpty
+ case pastEmpty
+
+ /// The associated screen
+ var screenType: Any.Type {
+ PollHistory.self
+ }
+
+ /// Generate the view struct for the screen state.
+ var screenView: ([Any], AnyView) {
+ let pollHistoryMode: PollHistoryMode
+ let pollService = MockPollHistoryService()
+
+ switch self {
+ case .active:
+ pollHistoryMode = .active
+ case .past:
+ pollHistoryMode = .past
+ case .activeEmpty:
+ pollHistoryMode = .active
+ pollService.activePollsData = []
+ case .pastEmpty:
+ pollHistoryMode = .past
+ pollService.pastPollsData = []
+ }
+
+ let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: pollService)
+
+ // can simulate service and viewModel actions here if needs be.
+
+ return (
+ [pollHistoryMode, viewModel],
+ AnyView(PollHistory(viewModel: viewModel.context)
+ .environmentObject(AvatarViewModel.withMockedServices()))
+ )
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift
new file mode 100644
index 000000000..93ef30819
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryModels.swift
@@ -0,0 +1,46 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+// MARK: View model
+
+enum PollHistoryViewModelResult: Equatable {
+ #warning("e.g. show poll detail")
+}
+
+// MARK: View
+
+enum PollHistoryMode: CaseIterable {
+ case active
+ case past
+}
+
+struct PollHistoryViewBindings {
+ var mode: PollHistoryMode
+}
+
+struct PollHistoryViewState: BindableState {
+ init(mode: PollHistoryMode) {
+ bindings = .init(mode: mode)
+ }
+
+ var bindings: PollHistoryViewBindings
+ var polls: [PollListData] = []
+}
+
+enum PollHistoryViewAction {
+ case viewAppeared
+ case segmentDidChange
+}
diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.swift
new file mode 100644
index 000000000..4199251da
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModel.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 SwiftUI
+
+typealias PollHistoryViewModelType = StateStoreViewModel
+
+final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModelProtocol {
+ private let pollService: PollHistoryServiceProtocol
+ private var polls: [PollListData] = []
+ private var fetchingTask: Task? {
+ didSet {
+ oldValue?.cancel()
+ }
+ }
+
+ var completion: ((PollHistoryViewModelResult) -> Void)?
+
+ init(mode: PollHistoryMode, pollService: PollHistoryServiceProtocol) {
+ self.pollService = pollService
+ super.init(initialViewState: PollHistoryViewState(mode: mode))
+ }
+
+ // MARK: - Public
+
+ override func process(viewAction: PollHistoryViewAction) {
+ switch viewAction {
+ case .viewAppeared:
+ fetchingTask = fetchPolls()
+ case .segmentDidChange:
+ updatePolls()
+ }
+ }
+}
+
+private extension PollHistoryViewModel {
+ func fetchPolls() -> Task {
+ Task {
+ let polls = try await pollService.fetchHistory()
+
+ guard Task.isCancelled == false else {
+ return
+ }
+
+ await MainActor.run {
+ self.polls = polls
+ updatePolls()
+ }
+ }
+ }
+
+ func updatePolls() {
+ let renderedPolls: [PollListData]
+
+ switch context.mode {
+ case .active:
+ renderedPolls = polls.filter { $0.winningOption == nil }
+ case .past:
+ renderedPolls = polls.filter { $0.winningOption != nil }
+ }
+
+ state.polls = renderedPolls
+ }
+}
diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModelProtocol.swift
similarity index 77%
rename from RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift
rename to RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModelProtocol.swift
index eab3cdcdf..d116c0254 100644
--- a/RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift
+++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryViewModelProtocol.swift
@@ -16,7 +16,7 @@
import Foundation
-/// Class that can be extended that supports injection and the `@Inject` property wrapper.
-open class InjectableObject: Injectable {
- var dependencies: DependencyContainer!
+protocol PollHistoryViewModelProtocol {
+ var completion: ((PollHistoryViewModelResult) -> Void)? { get set }
+ var context: PollHistoryViewModelType.Context { get }
}
diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift
similarity index 56%
rename from RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift
rename to RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift
index b05b966e4..a2dcf256a 100644
--- a/RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift
+++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/MatrixSDK/PollHistoryService.swift
@@ -1,5 +1,5 @@
-//
-// Copyright 2021 New Vector Ltd
+//
+// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,17 +14,11 @@
// limitations under the License.
//
+import MatrixSDK
import Foundation
-/// A protocol for classes that can be injected with a dependency container
-protocol Injectable: AnyObject {
- var dependencies: DependencyContainer! { get set }
-}
-
-extension Injectable {
- /// Used to inject the dependency container into an Injectable.
- /// - Parameter dependencies: The `DependencyContainer` to inject.
- func inject(dependencies: DependencyContainer) {
- self.dependencies = dependencies
+final class PollHistoryService: PollHistoryServiceProtocol {
+ func fetchHistory() async throws -> [PollListData] {
+ []
}
}
diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift
new file mode 100644
index 000000000..62796963d
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift
@@ -0,0 +1,44 @@
+//
+// Copyright 2023 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.
+//
+
+final class MockPollHistoryService: PollHistoryServiceProtocol {
+ var activePollsData: [PollListData] = (1..<10)
+ .map { index in
+ PollListData(
+ startDate: .init().addingTimeInterval(-CGFloat(index) * 3600),
+ question: "Do you like the active poll number \(index)?",
+ numberOfVotes: 30,
+ winningOption: nil
+ )
+ }
+
+ var pastPollsData: [PollListData] = (1..<10)
+ .map { index in
+ PollListData(
+ startDate: .init().addingTimeInterval(-CGFloat(index) * 3600),
+ question: "Do you like the past poll number \(index)?",
+ numberOfVotes: 30,
+ winningOption: .init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true)
+ )
+ }
+
+ func fetchHistory() async throws -> [PollListData] {
+ (activePollsData + pastPollsData)
+ .sorted { poll1, poll2 in
+ poll1.startDate > poll2.startDate
+ }
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift
new file mode 100644
index 000000000..4bb9b43b5
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/PollHistoryServiceProtocol.swift
@@ -0,0 +1,19 @@
+//
+// Copyright 2023 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.
+//
+
+protocol PollHistoryServiceProtocol {
+ func fetchHistory() async throws -> [PollListData]
+}
diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift
new file mode 100644
index 000000000..5867b88e8
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/PollHistory/Test/UI/PollHistoryUITests.swift
@@ -0,0 +1,68 @@
+//
+// 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 RiotSwiftUI
+import XCTest
+
+class PollHistoryUITests: MockScreenTestCase {
+ func testActivePollHistoryHasContent() {
+ app.goToScreenWithIdentifier(MockPollHistoryScreenState.active.title)
+ let title = app.navigationBars.firstMatch.identifier
+ let emptyText = app.staticTexts["PollHistory.emptyText"]
+ let items = app.staticTexts["PollListItem.title"]
+ let selectedSegment = app.buttons[VectorL10n.pollHistoryActiveSegmentTitle]
+ let winningOption = app.staticTexts["PollListData.winningOption"]
+
+ XCTAssertEqual(title, VectorL10n.pollHistoryTitle)
+ XCTAssertTrue(items.exists)
+ XCTAssertFalse(emptyText.exists)
+ XCTAssertTrue(selectedSegment.exists)
+ XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected)
+ XCTAssertFalse(winningOption.exists)
+ }
+
+ func testPastPollHistoryHasContent() {
+ app.goToScreenWithIdentifier(MockPollHistoryScreenState.past.title)
+ let title = app.navigationBars.firstMatch.identifier
+ let emptyText = app.staticTexts["PollHistory.emptyText"]
+ let items = app.staticTexts["PollListItem.title"]
+ let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle]
+ let winningOption = app.staticTexts["PollListData.winningOption"]
+
+ XCTAssertEqual(title, VectorL10n.pollHistoryTitle)
+ XCTAssertTrue(items.exists)
+ XCTAssertFalse(emptyText.exists)
+ XCTAssertTrue(selectedSegment.exists)
+ XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected)
+ XCTAssertTrue(winningOption.exists)
+ }
+
+ func testPastPollHistoryIsEmpty() {
+ app.goToScreenWithIdentifier(MockPollHistoryScreenState.pastEmpty.title)
+ let title = app.navigationBars.firstMatch.identifier
+ let emptyText = app.staticTexts["PollHistory.emptyText"]
+ let items = app.staticTexts["PollListItem.title"]
+ let selectedSegment = app.buttons[VectorL10n.pollHistoryPastSegmentTitle]
+ let winningOption = app.staticTexts["PollListData.winningOption"]
+
+ XCTAssertEqual(title, VectorL10n.pollHistoryTitle)
+ XCTAssertFalse(items.exists)
+ XCTAssertTrue(emptyText.exists)
+ XCTAssertTrue(selectedSegment.exists)
+ XCTAssertEqual(selectedSegment.value as? String, VectorL10n.accessibilitySelected)
+ XCTAssertFalse(winningOption.exists)
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift
new file mode 100644
index 000000000..7cf9bb45a
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollHistory.swift
@@ -0,0 +1,104 @@
+//
+// Copyright 2021 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import SwiftUI
+
+struct PollHistory: View {
+ @Environment(\.theme) private var theme
+
+ @ObservedObject var viewModel: PollHistoryViewModel.Context
+
+ var body: some View {
+ VStack {
+ SegmentedPicker(
+ segments: PollHistoryMode.allCases,
+ selection: $viewModel.mode,
+ interSegmentSpacing: 14
+ )
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.horizontal, 16)
+
+ if viewModel.viewState.polls.isEmpty {
+ noPollsView
+ } else {
+ pollListView
+ }
+ }
+ .padding(.top, 32)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(theme.colors.background.ignoresSafeArea())
+ .accentColor(theme.colors.accent)
+ .navigationTitle(VectorL10n.pollHistoryTitle)
+ .onAppear {
+ viewModel.send(viewAction: .viewAppeared)
+ }
+ .onChange(of: viewModel.mode) { _ in
+ viewModel.send(viewAction: .segmentDidChange)
+ }
+ }
+
+ private var pollListView: some View {
+ ScrollView {
+ LazyVStack(spacing: 32) {
+ let enumeratedPolls = Array(viewModel.viewState.polls.enumerated())
+
+ ForEach(enumeratedPolls, id: \.offset) { _, pollData in
+ PollListItem(pollData: pollData)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+
+ Button {
+ #warning("handle action")
+ } label: {
+ Text("Load more polls")
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ .padding(.top, 32)
+ .padding(.horizontal, 16)
+ }
+ }
+
+ private var noPollsView: some View {
+ Text(viewModel.mode == .active ? VectorL10n.pollHistoryNoActivePollText : VectorL10n.pollHistoryNoPastPollText)
+ .font(theme.fonts.body)
+ .foregroundColor(theme.colors.secondaryContent)
+ .frame(maxHeight: .infinity)
+ .padding(.horizontal, 16)
+ .accessibilityLabel("PollHistory.emptyText")
+ }
+}
+
+extension PollHistoryMode: CustomStringConvertible {
+ var description: String {
+ switch self {
+ case .active:
+ return VectorL10n.pollHistoryActiveSegmentTitle
+ case .past:
+ return VectorL10n.pollHistoryPastSegmentTitle
+ }
+ }
+}
+
+// MARK: - Previews
+
+struct PollHistory_Previews: PreviewProvider {
+ static let stateRenderer = MockPollHistoryScreenState.stateRenderer
+
+ static var previews: some View {
+ stateRenderer.screenGroup()
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift
new file mode 100644
index 000000000..335d62c39
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift
@@ -0,0 +1,153 @@
+//
+// Copyright 2023 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 PollListData {
+ let startDate: Date
+ let question: String
+ let numberOfVotes: UInt
+ let winningOption: TimelinePollAnswerOption?
+}
+
+struct PollListItem: View {
+ @Environment(\.theme) private var theme
+
+ private let pollData: PollListData
+ @ScaledMetric private var imageSize = 16
+
+ init(pollData: PollListData) {
+ self.pollData = pollData
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ Text(pollData.formattedDate)
+ .foregroundColor(theme.colors.tertiaryContent)
+ .font(theme.fonts.caption1)
+
+ HStack(alignment: .firstTextBaseline, spacing: 8) {
+ Image(uiImage: Asset.Images.pollHistory.image)
+ .resizable()
+ .frame(width: imageSize, height: imageSize)
+
+ Text(pollData.question)
+ .foregroundColor(theme.colors.primaryContent)
+ .font(theme.fonts.body)
+ .lineLimit(2)
+ .accessibilityLabel("PollListItem.title")
+ }
+
+ if pollData.winningOption != nil {
+ VStack(alignment: .leading, spacing: 12) {
+ optionView(winningOption: pollData.winningOption!)
+ resultView
+ }
+ }
+ }
+ }
+
+ private var clipShape: some Shape {
+ RoundedRectangle(cornerRadius: 4.0)
+ }
+
+ private func optionView(winningOption: TimelinePollAnswerOption) -> some View {
+ VStack(alignment: .leading, spacing: 12.0) {
+ HStack(alignment: .top, spacing: 8.0) {
+ Text(pollData.winningOption!.text)
+ .font(theme.fonts.body)
+ .foregroundColor(theme.colors.primaryContent)
+ .accessibilityIdentifier("PollListData.winningOption")
+
+ Spacer()
+
+ votesText(winningOption: winningOption)
+ }
+
+ ProgressView(value: Double(winningOption.count),
+ total: Double(pollData.numberOfVotes))
+ .progressViewStyle(LinearProgressViewStyle())
+ .scaleEffect(x: 1.0, y: 1.2, anchor: .center)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.horizontal, 8.0)
+ .padding(.top, 12.0)
+ .padding(.bottom, 12.0)
+ .clipShape(clipShape)
+ .overlay(clipShape.stroke(theme.colors.accent, lineWidth: 1.0))
+ .accentColor(theme.colors.accent)
+ }
+
+ private func votesText(winningOption: TimelinePollAnswerOption) -> some View {
+ Label {
+ Text(winningOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(winningOption.count)))
+ .font(theme.fonts.footnote)
+ .foregroundColor(theme.colors.accent)
+ } icon: {
+ Image(uiImage: Asset.Images.pollWinnerIcon.image)
+ }
+ }
+
+ private var resultView: some View {
+ let text = pollData.numberOfVotes == 1 ? VectorL10n.pollTimelineTotalFinalResultsOneVote : VectorL10n.pollTimelineTotalFinalResults(Int(pollData.numberOfVotes))
+
+ return Text(text)
+ .font(theme.fonts.footnote)
+ .foregroundColor(theme.colors.tertiaryContent)
+ }
+}
+
+private extension PollListData {
+ var formattedDate: String {
+ DateFormatter.shortDateFormatter.string(from: startDate)
+ }
+}
+
+private extension DateFormatter {
+ static let shortDateFormatter: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.timeStyle = .none
+ formatter.dateStyle = .short
+ formatter.timeZone = .init(identifier: "UTC")
+ return formatter
+ }()
+}
+
+// MARK: - Previews
+
+struct PollListItem_Previews: PreviewProvider {
+ static var previews: some View {
+ Group {
+ let pollData1 = PollListData(
+ startDate: .init(),
+ question: "Do you like polls?",
+ numberOfVotes: 30,
+ winningOption: .init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true)
+ )
+
+ PollListItem(pollData: pollData1)
+
+ let pollData2 = PollListData(
+ startDate: .init(),
+ question: "Do you like polls?",
+ numberOfVotes: 30,
+ winningOption: nil)
+
+ PollListItem(pollData: pollData2)
+ }
+ .padding()
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift
new file mode 100644
index 000000000..520a649c7
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/PollHistory/View/SegmentedPicker.swift
@@ -0,0 +1,72 @@
+//
+// Copyright 2023 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 SegmentedPicker: View {
+ private let segments: [Segment]
+ private let selection: Binding
+ private let interSegmentSpacing: CGFloat
+
+ @Environment(\.theme) private var theme
+
+ init(segments: [Segment], selection: Binding, interSegmentSpacing: CGFloat) {
+ self.segments = segments
+ self.selection = selection
+ self.interSegmentSpacing = interSegmentSpacing
+ }
+
+ var body: some View {
+ HStack(spacing: interSegmentSpacing) {
+ ForEach(segments, id: \.hashValue) { segment in
+ let isSelectedSegment = segment == selection.wrappedValue
+
+ Button {
+ selection.wrappedValue = segment
+ } label: {
+ Text(segment.description)
+ .font(isSelectedSegment ? theme.fonts.headline : theme.fonts.body)
+ .underline(isSelectedSegment)
+ }
+ .accentColor(isSelectedSegment ? theme.colors.accent : theme.colors.primaryContent)
+ .accessibilityLabel(segment.description)
+ .accessibilityValue(isSelectedSegment ? VectorL10n.accessibilitySelected : "")
+ }
+ }
+ }
+}
+
+struct SegmentedPicker_Previews: PreviewProvider {
+ static var previews: some View {
+ SegmentedPicker(
+ segments: [
+ "Segment 1",
+ "Segment 2"
+ ],
+ selection: .constant("Segment 1"),
+ interSegmentSpacing: 14
+ )
+
+ SegmentedPicker(
+ segments: [
+ "Segment 1",
+ "Segment 2"
+ ],
+ selection: .constant("Segment 2"),
+ interSegmentSpacing: 14
+ )
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift
index fd8e74103..9f937d435 100644
--- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift
+++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift
@@ -51,7 +51,7 @@ enum MockRoomAccessTypeChooserScreenState: MockScreenState, CaseIterable {
return (
[service, viewModel],
AnyView(RoomAccessTypeChooser(viewModel: viewModel.context, roomName: "Room Name")
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift
index bc89e53cb..32564e69d 100644
--- a/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift
+++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift
@@ -45,7 +45,7 @@ final class RoomUpgradeCoordinator: Coordinator, Presentable {
self.parameters = parameters
let viewModel = RoomUpgradeViewModel.makeRoomUpgradeViewModel(roomUpgradeService: RoomUpgradeService(session: parameters.session, roomId: parameters.roomId, parentSpaceId: parameters.parentSpaceId, versionOverride: parameters.versionOverride))
let view = RoomUpgrade(viewModel: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager)))
roomUpgradeViewModel = viewModel
roomUpgradeHostingController = VectorHostingController(rootView: view)
roomUpgradeHostingController.view.backgroundColor = .clear
diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift
index bca1f0ea7..4529c0eff 100644
--- a/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift
+++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift
@@ -49,7 +49,7 @@ enum MockRoomUpgradeScreenState: MockScreenState, CaseIterable {
return (
[service, viewModel],
AnyView(RoomUpgrade(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift
index c3812b5e8..c6d86a655 100644
--- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift
+++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift
@@ -61,7 +61,7 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService)
let view = UserSuggestionList(viewModel: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager)))
userSuggestionViewModel = viewModel
userSuggestionHostingController = VectorHostingController(rootView: view)
@@ -105,7 +105,7 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
private func calculateViewHeight() -> CGFloat {
let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService)
let view = UserSuggestionList(viewModel: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.mediaManager)))
let controller = VectorHostingController(rootView: view)
guard let view = controller.view else {
diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift
index f8a8acade..a0ed20268 100644
--- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift
+++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift
@@ -37,7 +37,7 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable {
return (
[service, listViewModel],
AnyView(UserSuggestionListWithInput(viewModel: viewModel)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift
index 0d3328b33..862e7573d 100644
--- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift
+++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift
@@ -55,6 +55,6 @@ struct UserSuggestionListItem: View {
struct UserSuggestionHeader_Previews: PreviewProvider {
static var previews: some View {
UserSuggestionListItem(avatar: MockAvatarInput.example, displayName: "Alice", userId: "@alice:matrix.org")
- .addDependency(MockAvatarService.example)
+ .environmentObject(AvatarViewModel.withMockedServices())
}
}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift
index 3f5e55c6e..7df3eaa04 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift
@@ -57,7 +57,8 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable {
}
deinit {
- viewModel.context.send(viewAction: .redact)
+ // If init has failed, our viewmodel will be nil.
+ viewModel?.context.send(viewAction: .redact)
}
// MARK: - Public
@@ -66,7 +67,7 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable {
func toPresentable() -> UIViewController {
let view = VoiceBroadcastPlaybackView(viewModel: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager)))
return VectorHostingController(rootView: view)
}
@@ -80,8 +81,15 @@ final class VoiceBroadcastPlaybackCoordinator: Coordinator, Presentable {
}
func endVoiceBroadcast() {}
-
+
func pausePlaying() {
viewModel.context.send(viewAction: .pause)
}
+
+ func pausePlayingInProgressVoiceBroadcast() {
+ // Pause the playback if we are playing a live voice broadcast (or waiting for more chunks)
+ if [.playing, .buffering].contains(viewModel.context.viewState.playbackState), viewModel.context.viewState.broadcastState != .stopped {
+ viewModel.context.send(viewAction: .pause)
+ }
+ }
}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift
index 3f28ab081..94c10eea4 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift
@@ -78,6 +78,12 @@ import Foundation
}
}
+ @objc public func pausePlayingInProgressVoiceBroadcast() {
+ coordinatorsForEventIdentifiers.forEach { _, coordinator in
+ coordinator.pausePlayingInProgressVoiceBroadcast()
+ }
+ }
+
private func handleEvent(event: MXEvent, direction: MXTimelineDirection, customObject: Any?) {
if direction == .backwards {
// ignore backwards events
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift
index 1f5ac9872..ffe713e73 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift
@@ -16,6 +16,7 @@
import Combine
import SwiftUI
+import MediaPlayer
// TODO: VoiceBroadcastPlaybackViewModel must be revisited in order to not depend on MatrixSDK
// We need a VoiceBroadcastPlaybackServiceProtocol and VoiceBroadcastAggregatorProtocol
@@ -43,7 +44,14 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
private var reloadVoiceBroadcastChunkQueue: Bool = false
private var seekToChunkTime: TimeInterval?
+ private var lastChunkAddedToPlayer: UInt = 0
+
private var isPlayingLastChunk: Bool {
+ // We can't play the last chunk if the brodcast is not stopped
+ guard state.broadcastState == .stopped else {
+ return false
+ }
+
let chunks = reorderVoiceBroadcastChunks(chunks: Array(voiceBroadcastAggregator.voiceBroadcast.chunks))
guard let chunkDuration = chunks.last?.duration else {
return false
@@ -52,6 +60,20 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
return state.bindings.progress + 1000 >= state.playingState.duration - Float(chunkDuration)
}
+ private var playingChunk: VoiceBroadcastChunk? {
+ guard let currentAudioPlayerUrl = audioPlayer?.currentUrl,
+ let playingEventId = voiceBroadcastAttachmentCacheManagerLoadResults.first(where: { result in
+ result.url == currentAudioPlayerUrl
+ })?.eventIdentifier else {
+ return nil
+ }
+
+ let playingChunk = voiceBroadcastAggregator.voiceBroadcast.chunks.first(where: { chunk in
+ chunk.attachment.eventId == playingEventId
+ })
+ return playingChunk
+ }
+
private var isLivePlayback: Bool {
return (!isPlaybackInitialized || isPlayingLastChunk) && (state.broadcastState == .started || state.broadcastState == .resumed)
}
@@ -168,11 +190,24 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
private func stopIfVoiceBroadcastOver() {
MXLog.debug("[VoiceBroadcastPlaybackViewModel] stopIfVoiceBroadcastOver")
+ var shouldStop = false
+
// Check if the broadcast is over before stopping everything
- // If not, the player should not stopped. The view state must be move to buffering
- if state.broadcastState == .stopped, isPlayingLastChunk {
+ if state.broadcastState == .stopped {
+ // If we known the last chunk sequence, use it to check if we need to stop
+ // Note: it's possible to be in .stopped state and to still have a last chunk sequence at 0 (old versions or a crash during recording). In this case, we use isPlayingLastChunk as a fallback solution
+ if voiceBroadcastAggregator.voiceBroadcastLastChunkSequence > 0 {
+ // we should stop only if we have already added the last chunk to the player
+ shouldStop = (lastChunkAddedToPlayer == voiceBroadcastAggregator.voiceBroadcastLastChunkSequence)
+ } else {
+ shouldStop = isPlayingLastChunk
+ }
+ }
+
+ if shouldStop {
stop()
} else {
+ // If not, the player should not stopped. The view state must be move to buffering
state.playbackState = .buffering
}
}
@@ -200,6 +235,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
private func seek(to seekTime: Float) {
// Flush the chunks queue and the current audio player playlist
+ lastChunkAddedToPlayer = 0
voiceBroadcastChunkQueue = []
reloadVoiceBroadcastChunkQueue = isProcessingVoiceBroadcastChunk
audioPlayer?.removeAllPlayerItems()
@@ -294,7 +330,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
guard result.eventIdentifier == chunk.attachment.eventId else {
return
}
-
+ self.lastChunkAddedToPlayer = max(self.lastChunkAddedToPlayer, chunk.sequence)
self.voiceBroadcastAttachmentCacheManagerLoadResults.append(result)
// Instanciate audioPlayer if needed.
@@ -302,6 +338,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
// Init and start the player on the first chunk
let audioPlayer = self.mediaServiceProvider.audioPlayerForIdentifier(result.eventIdentifier)
audioPlayer.registerDelegate(self)
+ self.mediaServiceProvider.registerNowPlayingInfoDelegate(self, forPlayer: audioPlayer)
audioPlayer.loadContentFromURL(result.url, displayName: chunk.attachment.originalFileName)
self.audioPlayer = audioPlayer
@@ -369,20 +406,19 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
}
@objc private func handleDisplayLinkTick() {
- guard let playingEventId = voiceBroadcastAttachmentCacheManagerLoadResults.first(where: { result in
- result.url == audioPlayer?.currentUrl
- })?.eventIdentifier,
- let playingSequence = voiceBroadcastAggregator.voiceBroadcast.chunks.first(where: { chunk in
- chunk.attachment.eventId == playingEventId
- })?.sequence else {
+ guard let playingSequence = self.playingChunk?.sequence else {
return
}
-
- let progress = Double(voiceBroadcastAggregator.voiceBroadcast.chunks.filter { chunk in
- chunk.sequence < playingSequence
- }.reduce(0) { $0 + $1.duration}) + (audioPlayer?.currentTime.rounded() ?? 0) * 1000
-
- state.bindings.progress = Float(progress)
+
+ // Get the audioPlayer current time, which is the elapsed time in the currently playing media item.
+ // Note: if the audioPlayer is not ready (eg. after a seek), its currentTime will be 0 and we shouldn't update the progress to avoid visual glitches.
+ let currentTime = audioPlayer?.currentTime ?? .zero
+ if currentTime > 0 {
+ let progress = Double(voiceBroadcastAggregator.voiceBroadcast.chunks.filter { chunk in
+ chunk.sequence < playingSequence
+ }.reduce(0) { $0 + $1.duration}) + currentTime * 1000
+ state.bindings.progress = Float(progress)
+ }
updateUI()
}
@@ -436,10 +472,14 @@ extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate {
// Handle the live icon appearance
state.playingState.isLive = isLivePlayback
+
+ // Handle the case where the playback state is .buffering and the new broadcast state is .stopped
+ if didReceiveState == .stopped, self.state.playbackState == .buffering {
+ stopIfVoiceBroadcastOver()
+ }
}
func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) {
-
updateDuration()
if state.playbackState != .stopped, !isActuallyPaused {
@@ -458,11 +498,13 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate {
state.playbackState = .playing
state.playingState.isLive = isLivePlayback
isPlaybackInitialized = true
+ displayLink.isPaused = false
}
func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
state.playbackState = .paused
state.playingState.isLive = false
+ displayLink.isPaused = true
}
func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
@@ -470,7 +512,9 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate {
state.playbackState = .stopped
state.playingState.isLive = false
audioPlayer.deregisterDelegate(self)
+ self.mediaServiceProvider.deregisterNowPlayingInfoDelegate(forPlayer: audioPlayer)
self.audioPlayer = nil
+ displayLink.isPaused = true
}
func audioPlayer(_ audioPlayer: VoiceMessageAudioPlayer, didFailWithError error: Error) {
@@ -482,3 +526,48 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate {
stopIfVoiceBroadcastOver()
}
}
+
+// MARK: - VoiceMessageNowPlayingInfoDelegate
+
+extension VoiceBroadcastPlaybackViewModel: VoiceMessageNowPlayingInfoDelegate {
+
+ func shouldSetupRemoteCommandCenter(audioPlayer player: VoiceMessageAudioPlayer) -> Bool {
+ guard BuildSettings.allowBackgroundAudioMessagePlayback, audioPlayer != nil, audioPlayer === player else {
+ return false
+ }
+
+ // we should setup the remote command center only for ended voice broadcast because we won't get new chunk if the app is in background.
+ return state.broadcastState == .stopped
+ }
+
+ func shouldDisconnectFromNowPlayingInfoCenter(audioPlayer player: VoiceMessageAudioPlayer) -> Bool {
+ guard BuildSettings.allowBackgroundAudioMessagePlayback, audioPlayer != nil, audioPlayer === player else {
+ return true
+ }
+
+ // we should disconnect from the now playing info center if the playback is stopped or if the broadcast is in progress
+ return state.playbackState == .stopped || state.broadcastState != .stopped
+ }
+
+ func updateNowPlayingInfoCenter(forPlayer player: VoiceMessageAudioPlayer) {
+ guard audioPlayer != nil, audioPlayer === player else {
+ return
+ }
+
+ // Don't update the NowPlayingInfoCenter for live broadcasts
+ guard state.broadcastState == .stopped else {
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
+ return
+ }
+
+ let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
+ nowPlayingInfoCenter.nowPlayingInfo = [
+ // Title
+ MPMediaItemPropertyTitle: VectorL10n.voiceBroadcastPlaybackLockScreenPlaceholder,
+ // Duration
+ MPMediaItemPropertyPlaybackDuration: (state.playingState.duration / 1000.0) as Any,
+ // Elapsed time
+ MPNowPlayingInfoPropertyElapsedPlaybackTime: (state.bindings.progress / 1000.0) as Any,
+ ]
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackErrorView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackErrorView.swift
index 0ac7822c6..0836bc661 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackErrorView.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackErrorView.swift
@@ -28,19 +28,17 @@ struct VoiceBroadcastPlaybackErrorView: View {
var action: (() -> Void)?
var body: some View {
- VStack {
- VStack {
+ ZStack {
+ HStack {
Image(uiImage: Asset.Images.errorIcon.image)
.frame(width: 40, height: 40)
Text(VectorL10n.voiceBroadcastPlaybackLoadingError)
.multilineTextAlignment(.center)
.font(theme.fonts.caption1)
- .foregroundColor(theme.colors.primaryContent)
+ .foregroundColor(theme.colors.alert)
}
- .padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
- .background(theme.colors.system.ignoresSafeArea())
}
}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift
index 2a9fe90b8..2b33f4121 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift
@@ -61,13 +61,22 @@ final class VoiceBroadcastRecorderCoordinator: Coordinator, Presentable {
func toPresentable() -> UIViewController {
let view = VoiceBroadcastRecorderView(viewModel: voiceBroadcastRecorderViewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager)))
+
return VectorHostingController(rootView: view)
}
func pauseRecording() {
voiceBroadcastRecorderViewModel.context.send(viewAction: .pause)
}
+
+ func pauseRecordingOnError() {
+ voiceBroadcastRecorderViewModel.context.send(viewAction: .pauseOnError)
+ }
+
+ func isVoiceBroadcastRecording() -> Bool {
+ return voiceBroadcastRecorderService.isRecording
+ }
// MARK: - Private
}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift
index e7f998716..b69476593 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderProvider.swift
@@ -1,4 +1,4 @@
-//
+//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -32,13 +32,16 @@ import Foundation
coordinatorsForEventIdentifiers.removeAll()
}
}
+ didSet {
+ sessionState = session?.state
+ }
}
private var coordinatorsForEventIdentifiers = [String: VoiceBroadcastRecorderCoordinator]() {
didSet {
if !self.coordinatorsForEventIdentifiers.isEmpty && self.redactionsListener == nil {
redactionsListener = session?.listenToEvents([MXEventType(identifier: kMXEventTypeStringRoomRedaction)], self.handleRedactedEvent)
}
-
+
if self.coordinatorsForEventIdentifiers.isEmpty && self.redactionsListener != nil {
session?.removeListener(self.redactionsListener)
self.redactionsListener = nil
@@ -49,9 +52,19 @@ import Foundation
// MARK: Private
private var currentEventIdentifier: String?
-
+ private var sessionState: MXSessionState?
+
+ private var sessionStateDidChangeObserver: Any?
+
// MARK: - Setup
- private override init() { }
+ private override init() {
+ super.init()
+ self.registerNotificationObservers()
+ }
+
+ deinit {
+ unregisterNotificationObservers()
+ }
// MARK: - Public
@@ -85,6 +98,19 @@ import Foundation
voiceBroadcastRecorderCoordinatorForCurrentEvent()?.pauseRecording()
}
+ /// Pause current voice broadcast recording without sending pending events.
+ @objc public func pauseRecordingOnError() {
+ voiceBroadcastRecorderCoordinatorForCurrentEvent()?.pauseRecordingOnError()
+ }
+
+ @objc public func isVoiceBroadcastRecording() -> Bool {
+ guard let coordinator = voiceBroadcastRecorderCoordinatorForCurrentEvent() else {
+ return false
+ }
+
+ return coordinator.isVoiceBroadcastRecording()
+ }
+
// MARK: - Private
/// Retrieve the voiceBroadcast recorder coordinator for the current event or nil if it hasn't been created yet
@@ -92,7 +118,7 @@ import Foundation
guard let currentEventIdentifier = currentEventIdentifier else {
return nil
}
-
+
return coordinatorsForEventIdentifiers[currentEventIdentifier]
}
@@ -101,11 +127,43 @@ import Foundation
// ignore backwards events
return
}
-
+
var coordinator = coordinatorsForEventIdentifiers.removeValue(forKey: event.redacts)
-
+
coordinator?.toPresentable().dismiss(animated: false) {
- coordinator = nil
+ coordinator = nil
+ }
+ }
+
+ // MARK: - Notification handling
+
+ private func registerNotificationObservers() {
+ self.sessionStateDidChangeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.mxSessionStateDidChange, object: session, queue: nil) { [weak self] notification in
+ guard let self else { return }
+ guard let concernedSession = notification.object as? MXSession, self.session === concernedSession else { return }
+
+ self.update(sessionState: concernedSession.state)
+ }
+ }
+
+ private func unregisterNotificationObservers() {
+ if let observer = self.sessionStateDidChangeObserver {
+ NotificationCenter.default.removeObserver(observer)
+ }
+ }
+
+ // MARK: - Session state
+ private func update(sessionState: MXSessionState) {
+ let oldState = self.sessionState
+ self.sessionState = sessionState
+
+ switch (oldState, sessionState) {
+ case (_, .homeserverNotReachable):
+ pauseRecordingOnError()
+ case (_, .running):
+ pauseRecording()
+ default:
+ break
}
}
}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift
index 437abbe3c..c4851e779 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift
@@ -44,6 +44,9 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
// MARK: Public
weak var serviceDelegate: VoiceBroadcastRecorderServiceDelegate?
+ var isRecording: Bool {
+ return audioEngine.isRunning
+ }
// MARK: - Setup
@@ -113,6 +116,8 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
// Discard the service on VoiceBroadcastService error. We keep the service in case of other error type
if error as? VoiceBroadcastServiceError != nil {
self.tearDownVoiceBroadcastService()
+ } else {
+ AppDelegate.theDelegate().showError(asAlert: error)
}
})
}
@@ -133,6 +138,10 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
}
}, failure: { error in
MXLog.error("[VoiceBroadcastRecorderService] Failed to pause voice broadcast", context: error)
+ // Pause voice broadcast recording without sending pending events.
+ if error is VoiceBroadcastServiceError == false {
+ AppDelegate.theDelegate().showError(asAlert: error)
+ }
})
}
@@ -148,6 +157,9 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
UIApplication.shared.isIdleTimerDisabled = true
}, failure: { error in
MXLog.error("[VoiceBroadcastRecorderService] Failed to resume voice broadcast", context: error)
+ if error is VoiceBroadcastServiceError == false {
+ AppDelegate.theDelegate().showError(asAlert: error)
+ }
})
}
@@ -166,6 +178,15 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
self.tearDownVoiceBroadcastService()
}
+ func pauseOnErrorRecordingVoiceBroadcast() {
+ audioEngine.pause()
+ UIApplication.shared.isIdleTimerDisabled = false
+ invalidateTimer()
+
+ // Update state
+ self.serviceDelegate?.voiceBroadcastRecorderService(self, didUpdateState: .error)
+ }
+
// MARK: - Private
/// Reset chunk values.
private func resetValues() {
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift
index 9e48e2e9a..78492fe15 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift
@@ -25,6 +25,9 @@ protocol VoiceBroadcastRecorderServiceProtocol {
/// Service delegate
var serviceDelegate: VoiceBroadcastRecorderServiceDelegate? { get set }
+ /// Returns if a voice broadcast is currently recording.
+ var isRecording: Bool { get }
+
/// Start voice broadcast recording.
func startRecordingVoiceBroadcast()
@@ -39,4 +42,7 @@ protocol VoiceBroadcastRecorderServiceProtocol {
/// Cancel voice broadcast recording after redacted it.
func cancelRecordingVoiceBroadcast()
+
+ /// Pause voice broadcast recording without sending pending events.
+ func pauseOnErrorRecordingVoiceBroadcast()
}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderConnectionErrorView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderConnectionErrorView.swift
new file mode 100644
index 000000000..051a6477b
--- /dev/null
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderConnectionErrorView.swift
@@ -0,0 +1,49 @@
+//
+// Copyright 2023 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 VoiceBroadcastRecorderConnectionErrorView: View {
+ // MARK: - Properties
+
+ // MARK: Private
+
+ @Environment(\.theme) private var theme: ThemeSwiftUI
+
+ // MARK: Public
+
+ var action: (() -> Void)?
+
+ var body: some View {
+ ZStack {
+ HStack(spacing: 0) {
+ Image(uiImage: Asset.Images.errorIcon.image)
+ .frame(width: 40, height: 40)
+ Text(VectorL10n.voiceBroadcastRecorderConnectionError)
+ .multilineTextAlignment(.center)
+ .font(theme.fonts.caption1)
+ .foregroundColor(theme.colors.alert)
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+}
+
+struct VoiceBroadcastRecorderConnectionErrorView_Previews: PreviewProvider {
+ static var previews: some View {
+ VoiceBroadcastRecorderConnectionErrorView()
+ }
+}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift
index c0cafed9b..c8e6532f6 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift
@@ -26,7 +26,7 @@ struct VoiceBroadcastRecorderView: View {
@State private var showingStopAlert = false
private var backgroundColor: Color {
- if viewModel.viewState.recordingState != .paused {
+ if viewModel.viewState.recordingState != .paused, viewModel.viewState.recordingState != .error {
return theme.colors.alert
}
return theme.colors.quarterlyContent
@@ -78,47 +78,53 @@ struct VoiceBroadcastRecorderView: View {
.accessibilityIdentifier("liveButton")
}
- HStack(alignment: .top, spacing: 34.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)
+ if viewModel.viewState.recordingState == .error {
+ VoiceBroadcastRecorderConnectionErrorView()
+ } else {
+ HStack(alignment: .top, spacing: 34.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)
+ case .error:
+ break
+ }
+ } label: {
+ if viewModel.viewState.recordingState == .started || viewModel.viewState.recordingState == .resumed {
+ Image("voice_broadcast_record_pause")
+ .renderingMode(.original)
+ } else {
+ Image("voice_broadcast_record")
+ .renderingMode(.original)
+ }
}
- } label: {
- if viewModel.viewState.recordingState == .started || viewModel.viewState.recordingState == .resumed {
- Image("voice_broadcast_record_pause")
- .renderingMode(.original)
- } else {
- Image("voice_broadcast_record")
+ .accessibilityIdentifier("recordButton")
+
+ Button {
+ showingStopAlert = true
+ } label: {
+ Image("voice_broadcast_stop")
.renderingMode(.original)
}
+ .alert(isPresented:$showingStopAlert) {
+ Alert(title: Text(VectorL10n.voiceBroadcastStopAlertTitle),
+ message: Text(VectorL10n.voiceBroadcastStopAlertDescription),
+ primaryButton: .cancel(),
+ secondaryButton: .default(Text(VectorL10n.voiceBroadcastStopAlertAgreeButton),
+ action: {
+ viewModel.send(viewAction: .stop)
+ }))
+ }
+ .accessibilityIdentifier("stopButton")
+ .disabled(viewModel.viewState.recordingState == .stopped)
+ .mask(Color.black.opacity(viewModel.viewState.recordingState == .stopped ? 0.3 : 1.0))
}
- .accessibilityIdentifier("recordButton")
-
- Button {
- showingStopAlert = true
- } label: {
- Image("voice_broadcast_stop")
- .renderingMode(.original)
- }
- .alert(isPresented:$showingStopAlert) {
- Alert(title: Text(VectorL10n.voiceBroadcastStopAlertTitle),
- message: Text(VectorL10n.voiceBroadcastStopAlertDescription),
- primaryButton: .cancel(),
- secondaryButton: .default(Text(VectorL10n.voiceBroadcastStopAlertAgreeButton),
- action: {
- viewModel.send(viewAction: .stop)
- }))
- }
- .accessibilityIdentifier("stopButton")
- .disabled(viewModel.viewState.recordingState == .stopped)
- .mask(Color.black.opacity(viewModel.viewState.recordingState == .stopped ? 0.3 : 1.0))
+ .padding(EdgeInsets(top: 10.0, leading: 0.0, bottom: 10.0, trailing: 0.0))
}
- .padding(EdgeInsets(top: 10.0, leading: 0.0, bottom: 10.0, trailing: 0.0))
}
.padding(EdgeInsets(top: 12.0, leading: 4.0, bottom: 12.0, trailing: 4.0))
}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift
index cb807a430..f992cd2f4 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift
@@ -21,6 +21,7 @@ enum VoiceBroadcastRecorderViewAction {
case stop
case pause
case resume
+ case pauseOnError
}
enum VoiceBroadcastRecorderState {
@@ -28,6 +29,7 @@ enum VoiceBroadcastRecorderState {
case stopped
case paused
case resumed
+ case error
}
struct VoiceBroadcastRecorderDetails {
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift
index ba9690bfb..ff486c5df 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift
@@ -56,6 +56,8 @@ class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, Voic
pause()
case .resume:
resume()
+ case .pauseOnError:
+ pauseOnError()
}
}
@@ -80,6 +82,10 @@ class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, Voic
voiceBroadcastRecorderService.resumeRecordingVoiceBroadcast()
}
+ private func pauseOnError() {
+ voiceBroadcastRecorderService.pauseOnErrorRecordingVoiceBroadcast()
+ }
+
private func updateRemainingTime(_ remainingTime: UInt) {
state.currentRecordingState = VoiceBroadcastRecorderViewModel.currentRecordingState(from: remainingTime)
}
diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift
index 467c69eb9..0f3155c9f 100644
--- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift
+++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift
@@ -70,11 +70,12 @@ final class MatrixItemChooserCoordinator: Coordinator, Presentable {
let viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserService(session: parameters.session, selectedItemIds: parameters.selectedItemsIds, itemsProcessor: parameters.itemsProcessor), title: parameters.title, detail: parameters.detail, selectionHeader: parameters.selectionHeader)
matrixItemChooserViewModel = viewModel
if let viewProvider = parameters.viewProvider {
- let view = viewProvider.view(with: viewModel.context).addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ let view = viewProvider.view(with: viewModel.context)
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager)))
matrixItemChooserHostingController = VectorHostingController(rootView: view)
} else {
let view = MatrixItemChooser(viewModel: viewModel.context, listBottomPadding: nil)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager)))
matrixItemChooserHostingController = VectorHostingController(rootView: view)
}
}
diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift
index e18476c24..b2db1b340 100644
--- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift
+++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift
@@ -61,7 +61,7 @@ enum MockMatrixItemChooserScreenState: MockScreenState, CaseIterable {
return (
[service, viewModel],
AnyView(MatrixItemChooser(viewModel: viewModel.context, listBottomPadding: nil)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift
index 1d67f2a72..883fd6e8e 100644
--- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift
+++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift
@@ -70,6 +70,6 @@ struct MatrixItemChooserListRow: View {
struct MatrixItemChooserListRow_Previews: PreviewProvider {
static var previews: some View {
TemplateRoomListRow(avatar: MockAvatarInput.example, displayName: "Alice")
- .addDependency(MockAvatarService.example)
+ .environmentObject(AvatarViewModel.withMockedServices())
}
}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift
index 28792056f..d7a31ede5 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift
@@ -42,7 +42,7 @@ final class SpaceCreationEmailInvitesCoordinator: Coordinator, Presentable {
let service = SpaceCreationEmailInvitesService(session: parameters.session)
let viewModel = SpaceCreationEmailInvitesViewModel(creationParameters: parameters.creationParams, service: service)
let view = SpaceCreationEmailInvites(viewModel: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager)))
spaceCreationEmailInvitesViewModel = viewModel
let hostingController = VectorHostingController(rootView: view)
hostingController.isNavigationBarHidden = true
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift
index 0b70a8746..add3eb6ed 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift
@@ -64,7 +64,7 @@ enum MockSpaceCreationEmailInvitesScreenState: MockScreenState, CaseIterable {
return (
[viewModel],
AnyView(SpaceCreationEmailInvites(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift
index 8d590936d..e3bcd6c9c 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift
@@ -41,7 +41,7 @@ final class SpaceCreationMenuCoordinator: Coordinator, Presentable {
self.parameters = parameters
let viewModel = SpaceCreationMenuViewModel(navTitle: parameters.navTitle, creationParams: parameters.creationParams, title: parameters.title, detail: parameters.detail, options: parameters.options)
let view = SpaceCreationMenu(viewModel: viewModel.context, showBackButton: parameters.showBackButton)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager)))
spaceCreationMenuViewModel = viewModel
let hostingController = VectorHostingController(rootView: view)
hostingController.isNavigationBarHidden = true
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift
index a1838458d..e385e30cb 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift
@@ -41,7 +41,7 @@ final class SpaceCreationPostProcessCoordinator: Coordinator, Presentable {
self.parameters = parameters
let viewModel = SpaceCreationPostProcessViewModel.makeSpaceCreationPostProcessViewModel(spaceCreationPostProcessService: SpaceCreationPostProcessService(session: parameters.session, parentSpaceId: parameters.parentSpaceId, creationParams: parameters.creationParams))
let view = SpaceCreationPostProcess(viewModel: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager)))
spaceCreationPostProcessViewModel = viewModel
let hostingController = VectorHostingController(rootView: view)
hostingController.isNavigationBarHidden = true
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift
index 27c003b71..f3c18f543 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift
@@ -54,7 +54,7 @@ enum MockSpaceCreationPostProcessScreenState: MockScreenState {
return (
[service, viewModel],
AnyView(SpaceCreationPostProcess(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift
index 2e94feec2..3e7ef4b4e 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift
@@ -41,7 +41,7 @@ final class SpaceCreationRoomsCoordinator: Coordinator, Presentable {
self.parameters = parameters
let viewModel = SpaceCreationRoomsViewModel(creationParameters: parameters.creationParams)
let view = SpaceCreationRooms(viewModel: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager)))
spaceCreationRoomsViewModel = viewModel
let hostingController = VectorHostingController(rootView: view)
hostingController.isNavigationBarHidden = true
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift
index 3ef06a64f..f23e463e2 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift
@@ -55,7 +55,7 @@ enum MockSpaceCreationRoomsScreenState: MockScreenState, CaseIterable {
return (
[viewModel],
AnyView(SpaceCreationRooms(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift
index 5077e4657..a1ca35a42 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift
@@ -48,7 +48,7 @@ final class SpaceCreationSettingsCoordinator: Coordinator, Presentable {
let service = SpaceCreationSettingsService(roomName: parameters.creationParameters.name ?? "", userDefinedAddress: parameters.creationParameters.userDefinedAddress, session: parameters.session)
let viewModel = SpaceCreationSettingsViewModel(spaceCreationSettingsService: service, creationParameters: parameters.creationParameters)
let view = SpaceCreationSettings(viewModel: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager)))
spaceCreationSettingsViewModel = viewModel
let hostingController = VectorHostingController(rootView: view)
hostingController.isNavigationBarHidden = true
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift
index 8738289a9..2942a43b4 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift
@@ -59,7 +59,7 @@ enum MockSpaceCreationSettingsScreenState: MockScreenState, CaseIterable {
return (
[service, viewModel],
AnyView(SpaceCreationSettings(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift
index 771ad0b8f..530f33eb9 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift
@@ -62,7 +62,7 @@ final class SpaceSelectorCoordinator: Coordinator, Presentable {
let service = SpaceSelectorService(session: parameters.session, parentSpaceId: parameters.parentSpaceId, showHomeSpace: parameters.showHomeSpace, selectedSpaceId: parameters.selectedSpaceId)
let viewModel = SpaceSelectorViewModel.makeViewModel(service: service, showCancel: parameters.showCancel)
let view = SpaceSelector(viewModel: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager)))
self.viewModel = viewModel
let hostingViewController = VectorHostingController(rootView: view)
hostingViewController.hidesBackTitleWhenPushed = true
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift
index 0224ae5cf..6748ee5a7 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift
@@ -48,7 +48,8 @@ final class SpaceSettingsCoordinator: Coordinator, Presentable {
self.parameters = parameters
let viewModel = SpaceSettingsViewModel.makeSpaceSettingsViewModel(service: SpaceSettingsService(session: parameters.session, spaceId: parameters.spaceId))
let view = SpaceSettings(viewModel: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager)))
+
spaceSettingsViewModel = viewModel
let controller = VectorHostingController(rootView: view)
controller.enableNavigationBarScrollEdgeAppearance = true
diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift
index f18482638..342cf520f 100644
--- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift
+++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift
@@ -79,7 +79,7 @@ enum MockSpaceSettingsScreenState: MockScreenState, CaseIterable {
return (
[service, viewModel],
AnyView(SpaceSettings(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift b/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift
index 5db4451ec..ffb4af8ac 100644
--- a/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift
+++ b/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift
@@ -50,7 +50,7 @@ enum MockTemplateSimpleScreenScreenState: MockScreenState, CaseIterable {
return (
[promptType, viewModel],
AnyView(TemplateSimpleScreen(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift
index b3098629b..73dc2d987 100644
--- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift
+++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift
@@ -37,7 +37,7 @@ final class TemplateUserProfileCoordinator: Coordinator, Presentable {
self.parameters = parameters
let viewModel = TemplateUserProfileViewModel.makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileService(session: parameters.session))
let view = TemplateUserProfile(viewModel: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager)))
templateUserProfileViewModel = viewModel
templateUserProfileHostingController = VectorHostingController(rootView: view)
diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift
index 08082aec5..27e9d215b 100644
--- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift
+++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift
@@ -55,7 +55,7 @@ enum MockTemplateUserProfileScreenState: MockScreenState, CaseIterable {
return (
[service, viewModel],
AnyView(TemplateUserProfile(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift
index a87e06605..f8e9bfe28 100644
--- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift
+++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift
@@ -46,6 +46,6 @@ struct TemplateUserProfileHeader: View {
struct TemplateUserProfileHeader_Previews: PreviewProvider {
static var previews: some View {
TemplateUserProfileHeader(avatar: MockAvatarInput.example, displayName: "Alice", presence: .online)
- .addDependency(MockAvatarService.example)
+ .environmentObject(AvatarViewModel.withMockedServices())
}
}
diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift
index bca98511f..afe669ec8 100644
--- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift
+++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift
@@ -33,7 +33,8 @@ final class TemplateRoomChatCoordinator: Coordinator, Presentable {
self.parameters = parameters
let viewModel = TemplateRoomChatViewModel(templateRoomChatService: TemplateRoomChatService(room: parameters.room))
let view = TemplateRoomChat(viewModel: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.room.mxSession.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.room.mxSession.mediaManager)))
+
templateRoomChatViewModel = viewModel
templateRoomChatHostingController = VectorHostingController(rootView: view)
}
diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift
index 8ec8bced0..f9a74abae 100644
--- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift
+++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift
@@ -56,7 +56,7 @@ enum MockTemplateRoomChatScreenState: MockScreenState, CaseIterable {
return (
[service, viewModel],
AnyView(TemplateRoomChat(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift
index 70495bd25..24df8b7e0 100644
--- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift
+++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift
@@ -58,6 +58,6 @@ struct TemplateRoomChatBubbleView_Previews: PreviewProvider {
)
static var previews: some View {
TemplateRoomChatBubbleView(bubble: bubble)
- .addDependency(MockAvatarService.example)
+ .environmentObject(AvatarViewModel.withMockedServices())
}
}
diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift
index 26bbbbb99..263e878c0 100644
--- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift
+++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift
@@ -33,7 +33,7 @@ final class TemplateRoomListCoordinator: Coordinator, Presentable {
self.parameters = parameters
let viewModel = TemplateRoomListViewModel(templateRoomListService: TemplateRoomListService(session: parameters.session))
let view = TemplateRoomList(viewModel: viewModel.context)
- .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
+ .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.session.mediaManager)))
templateRoomListViewModel = viewModel
templateRoomListHostingController = VectorHostingController(rootView: view)
}
diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift
index 8b8c0e2a3..59ad3fc8e 100644
--- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift
+++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift
@@ -47,7 +47,7 @@ enum MockTemplateRoomListScreenState: MockScreenState, CaseIterable {
return (
[service, viewModel],
AnyView(TemplateRoomList(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift
index a8db7b8ba..06841bbb2 100644
--- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift
+++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift
@@ -42,6 +42,6 @@ struct TemplateRoomListRow: View {
struct TemplateRoomListRow_Previews: PreviewProvider {
static var previews: some View {
TemplateRoomListRow(avatar: MockAvatarInput.example, displayName: "Alice")
- .addDependency(MockAvatarService.example)
+ .environmentObject(AvatarViewModel.withMockedServices())
}
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift
index 175a7d9a0..e09586b83 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift
@@ -56,7 +56,7 @@ enum MockUserSessionsOverviewScreenState: MockScreenState, CaseIterable {
return (
[service, viewModel],
AnyView(UserSessionsOverview(viewModel: viewModel.context)
- .addDependency(MockAvatarService.example))
+ .environmentObject(AvatarViewModel.withMockedServices()))
)
}
}
diff --git a/RiotTests/MatrixKitTests/MXKEventFormatterTests.swift b/RiotTests/MatrixKitTests/MXKEventFormatterSwiftTests.swift
similarity index 73%
rename from RiotTests/MatrixKitTests/MXKEventFormatterTests.swift
rename to RiotTests/MatrixKitTests/MXKEventFormatterSwiftTests.swift
index 31db30954..457f10853 100644
--- a/RiotTests/MatrixKitTests/MXKEventFormatterTests.swift
+++ b/RiotTests/MatrixKitTests/MXKEventFormatterSwiftTests.swift
@@ -29,22 +29,16 @@ private enum Constants {
static let expectedEditedHTML = "In reply to alice
Edited message
Reply"
static let expectedEditedHTMLWithNewContent = "In reply to alice
New content
Reply"
static let expectedEditedHTMLWithParsedItalic = "In reply to alice
New content
Reply"
+ static let expectedReplyToPollEndedEvent = "In reply to alice
Ended poll
Reply"
}
-class MXKEventFormatterTests: XCTestCase {
+class MXKEventFormatterSwiftTests: XCTestCase {
func testBuildHTMLString() {
let formatter = MXKEventFormatter()
- let repliedEvent = MXEvent()
+ let repliedEvent: MXEvent = .mockEvent(eventType: kMXEventTypeStringRoomMessage)
let event = MXEvent()
func buildHTML() -> String? { return formatter.buildHTMLString(for: event, inReplyTo: repliedEvent) }
- // Initial setup.
- repliedEvent.sender = "alice"
- repliedEvent.roomId = Constants.roomId
- repliedEvent.eventId = Constants.repliedEventId
- repliedEvent.wireType = kMXEventTypeStringRoomMessage
- repliedEvent.wireContent = [kMXMessageTypeKey: kMXMessageTypeText,
- kMXMessageBodyKey: Constants.repliedEventBody]
event.sender = "bob"
event.wireType = kMXEventTypeStringRoomMessage
event.wireContent = [
@@ -73,4 +67,39 @@ class MXKEventFormatterTests: XCTestCase {
repliedEvent.wireContent[kMXMessageContentKeyNewContent] = nil
XCTAssertNil(buildHTML())
}
+
+ func testBuildHTMLStringWithPollEndedReply() {
+ let formatter = MXKEventFormatter()
+ let repliedEvent: MXEvent = .mockEvent(eventType: kMXEventTypeStringPollEnd, body: nil)
+
+ let event = MXEvent()
+ event.sender = "bob"
+ event.wireType = kMXEventTypeStringRoomMessage
+ event.wireContent = [
+ kMXMessageTypeKey: kMXMessageTypeText,
+ kMXMessageBodyKey: Constants.replyBody,
+ kMXEventRelationRelatesToKey: [kMXEventContentRelatesToKeyInReplyTo: ["event_id": Constants.repliedEventId]]
+ ]
+
+ let formattedText = formatter.buildHTMLString(for: event, inReplyTo: repliedEvent)
+
+ XCTAssertEqual(formattedText, Constants.expectedReplyToPollEndedEvent)
+ }
+}
+
+private extension MXEvent {
+ static func mockEvent(eventType: String, body: String? = Constants.repliedEventBody) -> MXEvent {
+ let repliedEvent = MXEvent()
+ repliedEvent.sender = "alice"
+ repliedEvent.roomId = Constants.roomId
+ repliedEvent.eventId = Constants.repliedEventId
+ repliedEvent.wireType = eventType
+ repliedEvent.wireContent = [kMXMessageTypeKey: kMXMessageTypeText]
+
+ if let body = body {
+ repliedEvent.wireContent[kMXMessageBodyKey] = body
+ }
+
+ return repliedEvent
+ }
}
diff --git a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m
index fbe801665..b21f57715 100644
--- a/RiotTests/MatrixKitTests/MXKEventFormatterTests.m
+++ b/RiotTests/MatrixKitTests/MXKEventFormatterTests.m
@@ -414,14 +414,16 @@
NSString *s = @"Matrix HQ room is at https://matrix.to/#/room/#matrix:matrix.org.";
NSAttributedString *as = [eventFormatter renderString:s forEvent:anEvent];
- __block NSUInteger ranges = 0;
+ __block BOOL hasLink = false;
[as enumerateAttributesInRange:NSMakeRange(0, as.length) options:(0) usingBlock:^(NSDictionary * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
-
- ranges++;
+ if (attrs[NSLinkAttributeName]) {
+ hasLink = true;
+ *stop = true;
+ }
}];
- XCTAssertEqual(ranges, 1, @"There should be no link in this case. We let the UI manage the link");
+ XCTAssertEqual(hasLink, false, @"There should be no link in this case. We let the UI manage the link");
}
#pragma mark - Event sender/target info
diff --git a/SiriIntents/SupportingFiles/SiriIntents-Bridging-Header.h b/SiriIntents/SupportingFiles/SiriIntents-Bridging-Header.h
index ca6d81962..01aeb20d1 100644
--- a/SiriIntents/SupportingFiles/SiriIntents-Bridging-Header.h
+++ b/SiriIntents/SupportingFiles/SiriIntents-Bridging-Header.h
@@ -16,3 +16,4 @@
#import "MatrixKit-Bridging-Header.h"
#import "BuildInfo.h"
+#import "ThemeService.h"
diff --git a/SiriIntents/target.yml b/SiriIntents/target.yml
index 5b63a95b3..324497cf3 100644
--- a/SiriIntents/target.yml
+++ b/SiriIntents/target.yml
@@ -66,3 +66,5 @@ targets:
- "**/*.md" # excludes all files with the .md extension
- path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift
- path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/MatrixSDK
+ - path: ../Riot/Managers/Theme
+ - path: ../Riot/Categories/UIColor.swift
diff --git a/project.yml b/project.yml
index f2c4dbc23..fe9af33a8 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: 534ee5bae5e8de69ed398937b5edb7b5f21551d2
+ version: 0.19.0
DeviceKit:
url: https://github.com/devicekit/DeviceKit
majorVersion: 4.7.0