diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml
index 2c932c52fe..f8d03f08b9 100644
--- a/.github/workflows/release-alpha.yml
+++ b/.github/workflows/release-alpha.yml
@@ -4,7 +4,6 @@ on:
# Triggers the workflow on any pull request
pull_request:
- types: [ labeled, synchronized, opened, reopened ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
diff --git a/CHANGES.md b/CHANGES.md
index 15c1ca2e63..65b409b528 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,82 @@
+## Changes in 1.9.13 (2022-11-29)
+
+✨ Features
+
+- Add the left time in the Voice Broadcast tile recorder. ([#7103](https://github.com/vector-im/element-ios/pull/7103))
+
+🙌 Improvements
+
+- CryptoV2: Import progress for room keys ([#7078](https://github.com/vector-im/element-ios/pull/7078))
+- Add support in the new Device Manager to sessions without crypto support. ([#7083](https://github.com/vector-im/element-ios/pull/7083))
+- Loading: Display sync progress on the loading screen ([#7101](https://github.com/vector-im/element-ios/pull/7101))
+- Refactor bottom sheet presentation in the device manager. ([#7107](https://github.com/vector-im/element-ios/pull/7107))
+- Upgrade MatrixSDK version ([v0.24.5](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.5)).
+- Rich Text Composer: Fullscreen mode now is matching the design requirements. ([#7058](https://github.com/vector-im/element-ios/issues/7058))
+- Rich Text Editor: on iPhones when in landscape mode the fullscreen mode is disabled. ([#7096](https://github.com/vector-im/element-ios/issues/7096))
+
+🐛 Bugfixes
+
+- Fix scroll issues with VoiceBroadcast and Poll cells ([#7105](https://github.com/vector-im/element-ios/pull/7105))
+- VoiceBroadcast: Display the playback duration in the default state ([#7110](https://github.com/vector-im/element-ios/pull/7110))
+- Polls: mitigate flickering on vote. ([#5329](https://github.com/vector-im/element-ios/issues/5329))
+- Labs: Rich text editor: Fix smart punctuation (e.g. double space transforms into dot) ([#6930](https://github.com/vector-im/element-ios/issues/6930))
+- Labs: Rich text editor: Fix input for keyboards that use symbols composition and replacement (e.g. Japanese Romaji, Korean) ([#6983](https://github.com/vector-im/element-ios/issues/6983))
+- Labs: Rich text editor: Fix keyboard suggestions for non-latin keyboards (e.g. Chinese Pinyin) ([#7042](https://github.com/vector-im/element-ios/issues/7042))
+- Voice Messages: Fix crash when voice message finishes playing. ([#7074](https://github.com/vector-im/element-ios/issues/7074))
+- Rich Text Composer: Bottom Sheet is sized to always show all the elements inside, and in case it reaches the top, is also scrollable. ([#7082](https://github.com/vector-im/element-ios/issues/7082))
+- Labs: Rich text editor: Fix broken backspace around some type of whitespaces ([#7086](https://github.com/vector-im/element-ios/issues/7086))
+- Support voice broadcast live playback ([#7094](https://github.com/vector-im/element-ios/issues/7094))
+- Rich Text Editor: Fixed a bug that prevented the drag gesture to dismiss the fullscreen mode when there is a lot of text. ([#7116](https://github.com/vector-im/element-ios/issues/7116))
+
+🚧 In development 🚧
+
+- Labs: VoiceBroadcast - Add the Voice Broadcast option in the room functionalities ([#6721](https://github.com/vector-im/element-ios/issues/6721))
+
+
+## Changes in 1.9.12 (2022-11-15)
+
+✨ Features
+
+- Threads: added support to read receipts (MSC3771) ([#6663](https://github.com/vector-im/element-ios/issues/6663))
+- Threads: added support to notifications count (MSC3773) ([#6664](https://github.com/vector-im/element-ios/issues/6664))
+- Threads: added support to labs flag for read receipts ([#7029](https://github.com/vector-im/element-ios/issues/7029))
+- Threads: notification count in main timeline including un participated threads ([#7038](https://github.com/vector-im/element-ios/issues/7038))
+- Unverified sessions alert. ([#7056](https://github.com/vector-im/element-ios/issues/7056))
+- Labs: Rich-text editor: enable translations between Markdown and HTML when toggling text formatting ([#7061](https://github.com/vector-im/element-ios/issues/7061))
+
+🙌 Improvements
+
+- Add informational sheets for user's session states. ([#6992](https://github.com/vector-im/element-ios/pull/6992))
+- Add the sign out option in the menu in the session overview. ([#7001](https://github.com/vector-im/element-ios/pull/7001))
+- Add show/hide sessions' ip address in the new session manager. ([#7028](https://github.com/vector-im/element-ios/pull/7028))
+- Updated GBDeviceInfo pod. ([#7051](https://github.com/vector-im/element-ios/pull/7051))
+- Improve device manager code coverage. ([#7065](https://github.com/vector-im/element-ios/pull/7065))
+- Initial sync: Remove 10s wait on failed initial sync ([#7068](https://github.com/vector-im/element-ios/pull/7068))
+- Labs: Rich text-editor - Add support for plain text mode ([#6980](https://github.com/vector-im/element-ios/issues/6980))
+
+🐛 Bugfixes
+
+- Prevent autolayout crashes when showing toast notifications ([#7046](https://github.com/vector-im/element-ios/pull/7046))
+- Fixed timeline layout issues for reactions and attachments ([#7064](https://github.com/vector-im/element-ios/pull/7064))
+- Rich Text Composer: Voice Dictation is supported (only plain text can be dictated). ([#6945](https://github.com/vector-im/element-ios/issues/6945))
+- Rich Text Composer dismisses the keyboard when sending custom iOS emojis as images, like the normal composer. ([#6946](https://github.com/vector-im/element-ios/issues/6946))
+- Fixed IRC-style message and commands support in Rich text editor ([#6962](https://github.com/vector-im/element-ios/issues/6962))
+- Fixed the missing keystrokes issue on the Rich Text Editor ([#7005](https://github.com/vector-im/element-ios/issues/7005))
+- Fixed the long press deleting issue skipping some text on the Rich Text Editor ([#7006](https://github.com/vector-im/element-ios/issues/7006))
+- Hide push toggles for http pushers when there is no server support. ([#7022](https://github.com/vector-im/element-ios/issues/7022))
+- Synchronise composer and toolbar resizing animation duration for smoother height updates. ([#7025](https://github.com/vector-im/element-ios/issues/7025))
+- Device Manager: Session list item is not tappable everywhere. ([#7035](https://github.com/vector-im/element-ios/issues/7035))
+- Labs: Rich-text editor - Fix text formatting enabled inconsistent state ([#7052](https://github.com/vector-im/element-ios/issues/7052))
+- Labs: Rich-text editor - Fix text formatting switch losing the current content of the composer ([#7054](https://github.com/vector-im/element-ios/issues/7054))
+- Threads: removed "unread_thread_notifications" from sync filters for server that doesn't support MSC3773 ([#7066](https://github.com/vector-im/element-ios/issues/7066))
+- Poll not usable after logging out and back in. ([#7070](https://github.com/vector-im/element-ios/issues/7070))
+- Threads: Display number of unread messages above threads button ([#7076](https://github.com/vector-im/element-ios/issues/7076))
+
+🚧 In development 🚧
+
+- Device Manager: Multi-session sign out. ([#6963](https://github.com/vector-im/element-ios/issues/6963))
+
+
## Changes in 1.9.12 (2022-11-15)
✨ Features
diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig
index ffb7d901a1..c5f0aa65da 100644
--- a/Config/AppVersion.xcconfig
+++ b/Config/AppVersion.xcconfig
@@ -15,5 +15,5 @@
//
// Version
-MARKETING_VERSION = 1.9.12
-CURRENT_PROJECT_VERSION = 1.9.12
+MARKETING_VERSION = 1.9.13
+CURRENT_PROJECT_VERSION = 1.9.13
diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift
index d6b022948b..2f85f3c131 100644
--- a/Config/BuildSettings.swift
+++ b/Config/BuildSettings.swift
@@ -409,7 +409,7 @@ final class BuildSettings: NSObject {
// MARK: - Voice Broadcast
static let voiceBroadcastChunkLength: Int = 120
- static let voiceBroadcastMaxLength: UInt64 = 144000
+ static let voiceBroadcastMaxLength: UInt = 14400 // 240min.
// MARK: - MXKAppSettings
static let enableBotCreation: Bool = false
diff --git a/Podfile b/Podfile
index 154c119d42..aaf64e8acf 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.3'
+$matrixSDKVersion = '= 0.24.5'
# $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 816ccb018f..a3f2f6fc96 100644
--- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -23,7 +23,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift",
"state" : {
- "revision" : "2469f27b7e1e51aaa135e09f9005eb10fda686e6"
+ "revision" : "1fbffd0321eb47abcd664ad19c6c943b60abf399"
}
},
{
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/Contents.json
new file mode 100644
index 0000000000..6dbed56480
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "voice_broadcast_time_left.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/voice_broadcast_time_left.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/voice_broadcast_time_left.svg
new file mode 100644
index 0000000000..82b9eb425a
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/voice_broadcast_time_left.svg
@@ -0,0 +1,3 @@
+
diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings
index fa68764c3e..e9295cc9e2 100644
--- a/Riot/Assets/en.lproj/Vector.strings
+++ b/Riot/Assets/en.lproj/Vector.strings
@@ -1968,6 +1968,12 @@ Tap the + to start adding people.";
"call_transfer_error_title" = "Error";
"call_transfer_error_message" = "Call transfer failed";
+// MARK: - Launch loading
+
+"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%@ %%";
+
// MARK: - Home
"home_empty_view_title" = "Welcome to %@,\n%@";
@@ -2196,6 +2202,7 @@ Tap the + to start adding people.";
"voice_broadcast_playback_loading_error" = "Unable to play this voice broadcast.";
"voice_broadcast_live" = "Live";
"voice_broadcast_tile" = "Voice broadcast";
+"voice_broadcast_time_left" = "%@ left";
// Mark: - Version check
@@ -2444,6 +2451,7 @@ To enable access, tap Settings> Location and select Always";
"user_session_unverified_additional_info" = "Verify your current session for enhanced secure messaging.";
"user_session_verification_unknown_additional_info" = "Verify your current session to reveal this session's verification status.";
"user_other_session_unverified_additional_info" = "Verify or sign out from this session for best security and reliability.";
+"user_other_session_permanently_unverified_additional_info" = "This session cannot be verified because it does not support encryption.";
"user_other_session_verified_additional_info" = "This session is ready for secure messaging.";
"user_session_push_notifications" = "Push notifications";
"user_session_push_notifications_message" = "When turned on, this session will receive push notifications.";
diff --git a/Riot/Assets/es.lproj/InfoPlist.strings b/Riot/Assets/es.lproj/InfoPlist.strings
index 6b8c191124..da6fdb6c67 100644
--- a/Riot/Assets/es.lproj/InfoPlist.strings
+++ b/Riot/Assets/es.lproj/InfoPlist.strings
@@ -1,8 +1,8 @@
// Permissions usage explanations
"NSCameraUsageDescription" = "La cámara se usa para sacar fotos, vídeos y hacer videollamadas.";
-"NSPhotoLibraryUsageDescription" = "La biblioteca de fotos se usa para enviar fotos y vídeos.";
+"NSPhotoLibraryUsageDescription" = "Permite el acceso a fotos para subir fotos y vídeos desde tu galería.";
"NSMicrophoneUsageDescription" = "Element necesita usar tu micrófono para hacer y recibir llamadas y grabar vídeos y mensajes de voz.";
-"NSContactsUsageDescription" = "Element te mostrará tus contactos para que les puedas invitar a una conversación.";
+"NSContactsUsageDescription" = "Se compartirán con tu servidor de identidad para ayudarte a encontrar tus contactos en Matrix.";
"NSFaceIDUsageDescription" = "Face ID se usa para acceder a tu aplicación.";
"NSCalendarsUsageDescription" = "Mostrar tus reuniones en la aplicación.";
"NSLocationWhenInUseUsageDescription" = "Cuando compartes tu ubicación con otras personas, Element necesita acceso para que puedan verla en el mapa.";
diff --git a/Riot/Assets/nl.lproj/InfoPlist.strings b/Riot/Assets/nl.lproj/InfoPlist.strings
index 2ef529d616..f31554ac87 100644
--- a/Riot/Assets/nl.lproj/InfoPlist.strings
+++ b/Riot/Assets/nl.lproj/InfoPlist.strings
@@ -16,10 +16,10 @@
// Permissions usage explanations
"NSCameraUsageDescription" = "De camera wordt gebruikt om te videobellen, of om foto's en video's te maken en te uploaden.";
-"NSPhotoLibraryUsageDescription" = "Geef toegang tot foto's om foto's en video's uit uw bibliotheek te uploaden.";
-"NSMicrophoneUsageDescription" = "Element heeft toegang nodig tot uw microfoon nodig voor oproepen, maken van video's en spraakberichten opnemen.";
-"NSContactsUsageDescription" = "Ze worden gedeeld met uw identiteitsserver om uw contacten op Matrix te vinden.";
-"NSCalendarsUsageDescription" = "Bekijk uw geplande afspraken in de app.";
-"NSFaceIDUsageDescription" = "Face ID wordt gebruikt om toegang te krijgen tot uw app.";
-"NSLocationWhenInUseUsageDescription" = "Wanneer u uw locatie deelt met mensen heeft Element toegang nodig om dit op een kaart te tonen.";
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "Wanneer u locatie met mensen deelt, heeft Element toegang nodig om ze een kaart te laten zien.";
+"NSPhotoLibraryUsageDescription" = "Geef toegang tot foto's om foto's en video's uit je bibliotheek te uploaden.";
+"NSMicrophoneUsageDescription" = "Element heeft toegang nodig tot je microfoon nodig voor oproepen, maken van video's en spraakberichten opnemen.";
+"NSContactsUsageDescription" = "Ze worden gedeeld met je identiteitsserver om je contacten op Matrix te vinden.";
+"NSCalendarsUsageDescription" = "Bekijk jouw geplande afspraken in de app.";
+"NSFaceIDUsageDescription" = "Face ID wordt gebruikt om toegang te krijgen tot je app.";
+"NSLocationWhenInUseUsageDescription" = "Wanneer je jouw locatie deelt met mensen heeft Element toegang nodig om dit op een kaart te tonen.";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "Wanneer je jouw locatie met mensen deelt, heeft Element toegang nodig om ze een kaart te laten zien.";
diff --git a/Riot/Assets/nl.lproj/Localizable.strings b/Riot/Assets/nl.lproj/Localizable.strings
index 0176086d14..eeab4c8072 100644
--- a/Riot/Assets/nl.lproj/Localizable.strings
+++ b/Riot/Assets/nl.lproj/Localizable.strings
@@ -54,11 +54,11 @@
/** Invites **/
/* A user has invited you to a chat */
-"USER_INVITE_TO_CHAT" = "%@ heeft u uitgenodigd om te chatten";
+"USER_INVITE_TO_CHAT" = "%@ heeft je uitgenodigd om te chatten";
/* A user has invited you to an (unamed) group chat */
-"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ heeft u uitgenodigd in een groepschat";
+"USER_INVITE_TO_CHAT_GROUP_CHAT" = "%@ heeft je uitgenodigd in een groepschat";
/* A user has invited you to a named room */
-"USER_INVITE_TO_NAMED_ROOM" = "%@ heeft u in %@-kamer uitgenodigd";
+"USER_INVITE_TO_NAMED_ROOM" = "%@ heeft je in %@-kamer uitgenodigd";
/** Calls **/
/* Incoming one-to-one voice call */
@@ -74,9 +74,9 @@
/* Incoming named video conference invite from a specific person */
"VIDEO_CONF_NAMED_FROM_USER" = "Video-groepsoproep van %@: ‘%@’";
/* A single unread message in a room */
-"SINGLE_UNREAD_IN_ROOM" = "U heeft een bericht ontvangen in %@";
+"SINGLE_UNREAD_IN_ROOM" = "Je hebt een bericht ontvangen in %@";
/* A single unread message */
-"SINGLE_UNREAD" = "U heeft een bericht ontvangen";
+"SINGLE_UNREAD" = "Je hebt een bericht ontvangen";
/* Message title for a specific person in a named room */
"MSG_FROM_USER_IN_ROOM_TITLE" = "%@ in %@";
/* Sticker from a specific person, not referencing a room. */
diff --git a/Riot/Assets/ru.lproj/InfoPlist.strings b/Riot/Assets/ru.lproj/InfoPlist.strings
index c3bb844cb8..b1ddc44b64 100644
--- a/Riot/Assets/ru.lproj/InfoPlist.strings
+++ b/Riot/Assets/ru.lproj/InfoPlist.strings
@@ -1,8 +1,8 @@
// Permissions usage explanations
-"NSCameraUsageDescription" = "Камера используется для съемки фото и видео, совершения видеозвонков.";
-"NSPhotoLibraryUsageDescription" = "Галерея используется для отправки фото и видео.";
+"NSCameraUsageDescription" = "Камера используется для совершения видеозвонков, съёмки и загрузки фотографий и видео.";
+"NSPhotoLibraryUsageDescription" = "Разрешите доступ к фото для отправки фото и видео из вашей библиотеки.";
"NSMicrophoneUsageDescription" = "Element необходим доступ к вашему микрофону, чтобы совершать и принимать звонки, снимать видео и записывать голосовые сообщения.";
-"NSContactsUsageDescription" = "Element покажет ваши контакты, чтобы вы могли пригласить их в чат.";
+"NSContactsUsageDescription" = "Они будут переданы вашему серверу идентификации, чтобы помочь найти ваши контакты в Matrix.";
"NSCalendarsUsageDescription" = "Просматривайте запланированные встречи в приложении.";
"NSFaceIDUsageDescription" = "Face ID используется для доступа к вашему приложению.";
"NSLocationWhenInUseUsageDescription" = "Когда вы делитесь с людьми своим местоположением, Element необходим доступ, чтобы показать им карту.";
diff --git a/Riot/Assets/sq.lproj/InfoPlist.strings b/Riot/Assets/sq.lproj/InfoPlist.strings
index 740651215f..a4d354d560 100644
--- a/Riot/Assets/sq.lproj/InfoPlist.strings
+++ b/Riot/Assets/sq.lproj/InfoPlist.strings
@@ -1,8 +1,8 @@
// Permissions usage explanations
-"NSCameraUsageDescription" = "Kamera përdoret për të bërë foto dhe regjistruar video, dhe për të bërë thirrje video.";
-"NSPhotoLibraryUsageDescription" = "Fototeka përdoret për të dërguar foto dhe video.";
+"NSCameraUsageDescription" = "Kamera përdoret për të bërë thirrje video, ose për të bërë dhe ngarkuar diku foto dhe video.";
+"NSPhotoLibraryUsageDescription" = "Që të ngarkohen foto dhe video që nga mediateka juaj, lejoni hyrje te fotot.";
"NSMicrophoneUsageDescription" = "Element-it i duhet të përdorë mikrofonin tuaj për të bërë dhe marrë thirrje, për të regjistruar video, dhe për të regjistruar mesazhe zanorë.";
-"NSContactsUsageDescription" = "Element-i do të shfaqë kontaktet tuaja, që kështu të mund t’i ftoni për të biseduar.";
+"NSContactsUsageDescription" = "Do t’i jepen shërbyesit tuaj të identiteteve, për ta ndihmuar të gjejë kontakte tuajt në Matrix.";
"NSCalendarsUsageDescription" = "Shihini te aplikacioni takimet tuaja të planifikuara.";
"NSFaceIDUsageDescription" = "Face ID përdoret që të hyni në aplikacionin tuaj.";
"NSLocationWhenInUseUsageDescription" = "Kur ndani vendndodhjen tuaj me persona, Element-i ka nevojë për hyrje në të, që t’u trgojë atyre një hartë.";
diff --git a/Riot/Assets/sq.lproj/Vector.strings b/Riot/Assets/sq.lproj/Vector.strings
index 847de8e424..8d6e3cbfd4 100644
--- a/Riot/Assets/sq.lproj/Vector.strings
+++ b/Riot/Assets/sq.lproj/Vector.strings
@@ -2552,7 +2552,7 @@
"all_chats_empty_unreads_placeholder_message" = "Ky është vendi ku do të shfaqen mesazhet tuaj të palexuar, kur të ketë të tillë.";
"all_chats_empty_view_information" = "Aplikacioni “all-in-one” i fjalosjeve të siguruara, për ekipe, shokë dhe ente. Që t’ia filloni, krijoni një fjalosje, ose hyni në një dhomë ekzistuese.";
"all_chats_empty_space_information" = "Hapësirat janë një mënyrë e re për të grupuar dhoma dhe persona. Shtoni një dhomë ekzistuese, ose krijoni një të re, duke përdorur butonin poshtë djathtas.";
-"all_chats_empty_view_title" = "%s\nduket paksa si i zbrazët.";
+"all_chats_empty_view_title" = "%@\nduket paksa si i zbrazët.";
"all_chats_all_filter" = "Krejt";
"all_chats_edit_layout_alphabetical_order" = "Renditi si A-Z";
"all_chats_edit_layout_activity_order" = "Renditi sipas veprimtarish";
diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift
index 94352b0be0..dcf78a2e13 100644
--- a/Riot/Generated/Images.swift
+++ b/Riot/Generated/Images.swift
@@ -347,6 +347,7 @@ internal class Asset: NSObject {
internal static let voiceBroadcastStop = ImageAsset(name: "voice_broadcast_stop")
internal static let voiceBroadcastTileLive = ImageAsset(name: "voice_broadcast_tile_live")
internal static let voiceBroadcastTileMic = ImageAsset(name: "voice_broadcast_tile_mic")
+ internal static let voiceBroadcastTimeLeft = ImageAsset(name: "voice_broadcast_time_left")
internal static let launchScreenLogo = ImageAsset(name: "launch_screen_logo")
}
@objcMembers
diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift
index 36091cc12f..f773146b2d 100644
--- a/Riot/Generated/Strings.swift
+++ b/Riot/Generated/Strings.swift
@@ -3179,6 +3179,18 @@ public class VectorL10n: NSObject {
public static var later: String {
return VectorL10n.tr("Vector", "later")
}
+ /// Processing data\n%@ %%
+ public static func launchLoadingProcessingResponse(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "launch_loading_processing_response", p1)
+ }
+ /// Syncing with the server
+ public static var launchLoadingServerSyncing: String {
+ return VectorL10n.tr("Vector", "launch_loading_server_syncing")
+ }
+ /// Syncing with the server\n(%@ attempt)
+ public static func launchLoadingServerSyncingNthAttempt(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "launch_loading_server_syncing_nth_attempt", p1)
+ }
/// Leave
public static var leave: String {
return VectorL10n.tr("Vector", "leave")
@@ -8699,6 +8711,10 @@ public class VectorL10n: NSObject {
public static var userOtherSessionNoVerifiedSessions: String {
return VectorL10n.tr("Vector", "user_other_session_no_verified_sessions")
}
+ /// This session cannot be verified because it does not support encryption.
+ public static var userOtherSessionPermanentlyUnverifiedAdditionalInfo: String {
+ return VectorL10n.tr("Vector", "user_other_session_permanently_unverified_additional_info")
+ }
/// Security recommendation
public static var userOtherSessionSecurityRecommendationTitle: String {
return VectorL10n.tr("Vector", "user_other_session_security_recommendation_title")
@@ -9139,6 +9155,10 @@ public class VectorL10n: NSObject {
public static var voiceBroadcastTile: String {
return VectorL10n.tr("Vector", "voice_broadcast_tile")
}
+ /// %@ left
+ public static func voiceBroadcastTimeLeft(_ p1: String) -> String {
+ return VectorL10n.tr("Vector", "voice_broadcast_time_left", p1)
+ }
/// Can't start a new voice broadcast
public static var voiceBroadcastUnauthorizedTitle: String {
return VectorL10n.tr("Vector", "voice_broadcast_unauthorized_title")
diff --git a/Riot/Modules/Analytics/SentryMonitoringClient.swift b/Riot/Modules/Analytics/SentryMonitoringClient.swift
index d15cf80393..54933a7ab3 100644
--- a/Riot/Modules/Analytics/SentryMonitoringClient.swift
+++ b/Riot/Modules/Analytics/SentryMonitoringClient.swift
@@ -42,6 +42,10 @@ struct SentryMonitoringClient {
options.enableNetworkTracking = false
options.beforeSend = { event in
+ // Use the actual error message as issue fingerprint
+ if let message = event.message?.formatted {
+ event.fingerprint = [message]
+ }
MXLog.debug("[SentryMonitoringClient] Issue detected: \(event)")
return event
}
diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m
index 3558a4a4d5..7c2975522f 100644
--- a/Riot/Modules/Application/LegacyAppDelegate.m
+++ b/Riot/Modules/Application/LegacyAppDelegate.m
@@ -2394,7 +2394,17 @@ - (void)showLaunchAnimation
{
MXLogDebug(@"[AppDelegate] showLaunchAnimation");
- LaunchLoadingView *launchLoadingView = [LaunchLoadingView instantiate];
+ LaunchLoadingView *launchLoadingView;
+ if (MXSDKOptions.sharedInstance.enableSyncProgress)
+ {
+ MXSession *mainSession = self.mxSessions.firstObject;
+ launchLoadingView = [LaunchLoadingView instantiateWithSyncProgress:mainSession.syncProgress];
+ }
+ else
+ {
+ launchLoadingView = [LaunchLoadingView instantiateWithSyncProgress:nil];
+ }
+
launchLoadingView.frame = window.bounds;
[launchLoadingView updateWithTheme:ThemeService.shared.theme];
launchLoadingView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift
index 5aa6b3731f..8b98989929 100644
--- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift
+++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift
@@ -613,7 +613,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
/// Replace the contents of the navigation router with a loading animation.
private func showLoadingAnimation() {
- let loadingViewController = LaunchLoadingViewController()
+ let syncProgress: MXSessionSyncProgress? = MXSDKOptions.sharedInstance().enableSyncProgress ? session?.syncProgress : nil
+ let loadingViewController = LaunchLoadingViewController(syncProgress: syncProgress)
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 e8ca770abf..583419075f 100644
--- a/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift
+++ b/Riot/Modules/Authentication/Legacy/LegacyAuthenticationCoordinator.swift
@@ -106,7 +106,8 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
// MARK: - Private
private func showLoadingAnimation() {
- let loadingViewController = LaunchLoadingViewController()
+ let syncProgress: MXSessionSyncProgress? = MXSDKOptions.sharedInstance().enableSyncProgress ? session?.syncProgress : nil
+ let loadingViewController = LaunchLoadingViewController(syncProgress: syncProgress)
loadingViewController.modalPresentationStyle = .fullScreen
// Replace the navigation stack with the loading animation
diff --git a/Riot/Modules/Common/SwiftUI/VectorHostingBottomSheetPreferences.swift b/Riot/Modules/Common/SwiftUI/VectorHostingBottomSheetPreferences.swift
index cb5afa51ae..d7f94c6260 100644
--- a/Riot/Modules/Common/SwiftUI/VectorHostingBottomSheetPreferences.swift
+++ b/Riot/Modules/Common/SwiftUI/VectorHostingBottomSheetPreferences.swift
@@ -25,11 +25,26 @@ class VectorHostingBottomSheetPreferences {
case medium
case large
+ /// only available on iOS16, medium behaviour will be used instead
+ /// - Parameters:
+ /// - height: The height of the custom detent, if the height is bigger than the maximum possible height for a detent the latter will be returned
+ /// - identifier: The identifier used to identify the custom detent during detent transitions, by default the value is set to "custom", however if you are supporting multiple custom detents in a bottom sheet, you should specify a different identifier for each
+ case custom(height: CGFloat, identifier: String = "custom")
+
@available(iOS 15, *)
fileprivate func uiSheetDetent() -> UISheetPresentationController.Detent {
switch self {
case .medium: return .medium()
case .large: return .large()
+ case let .custom(height, identifier):
+ if #available(iOS 16, *) {
+ let identifier = UISheetPresentationController.Detent.Identifier(identifier)
+ return .custom(identifier: identifier) { context in
+ return min(height, context.maximumDetentValue)
+ }
+ } else {
+ return .medium()
+ }
}
}
@@ -38,6 +53,12 @@ class VectorHostingBottomSheetPreferences {
switch self {
case .medium: return .medium
case .large: return .large
+ case let .custom(_, identifier):
+ if #available(iOS 16, *) {
+ return UISheetPresentationController.Detent.Identifier(identifier)
+ } else {
+ return .medium
+ }
}
}
}
diff --git a/Riot/Modules/LaunchLoading/LaunchLoadingView.swift b/Riot/Modules/LaunchLoading/LaunchLoadingView.swift
index f2843db621..55f3aff054 100644
--- a/Riot/Modules/LaunchLoading/LaunchLoadingView.swift
+++ b/Riot/Modules/LaunchLoading/LaunchLoadingView.swift
@@ -30,12 +30,20 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
// MARK: - Properties
@IBOutlet private weak var animationView: ElementView!
+ @IBOutlet private weak var statusLabel: UILabel!
+
private var animationTimeline: Timeline_1!
+ private let numberFormatter: NumberFormatter = {
+ let formatter = NumberFormatter()
+ formatter.numberStyle = .ordinal
+ return formatter
+ }()
// MARK: - Setup
- static func instantiate() -> LaunchLoadingView {
+ static func instantiate(syncProgress: MXSessionSyncProgress?) -> LaunchLoadingView {
let view = LaunchLoadingView.loadFromNib()
+ syncProgress?.delegate = view
return view
}
@@ -45,6 +53,8 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
let animationTimeline = Timeline_1(view: self.animationView, duration: LaunchAnimation.duration, repeatCount: LaunchAnimation.repeatCount)
animationTimeline.play()
self.animationTimeline = animationTimeline
+
+ self.statusLabel.isHidden = !MXSDKOptions.sharedInstance().enableSyncProgress
}
// MARK: - Public
@@ -54,3 +64,31 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
self.animationView.backgroundColor = theme.backgroundColor
}
}
+
+extension LaunchLoadingView: MXSessionSyncProgressDelegate {
+ func sessionDidUpdateSyncState(_ state: MXSessionSyncState) {
+ guard MXSDKOptions.sharedInstance().enableSyncProgress else {
+ 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)
+ CATransaction.commit()
+ }
+
+ private func statusText(for state: MXSessionSyncState) -> String {
+ switch state {
+ case .serverSyncing(let attempts):
+ if attempts > 1, let nth = numberFormatter.string(from: NSNumber(value: attempts)) {
+ return VectorL10n.launchLoadingServerSyncingNthAttempt(nth)
+ } else {
+ return VectorL10n.launchLoadingServerSyncing
+ }
+ case .processingResponse(let progress):
+ let percent = Int(floor(progress * 100))
+ return VectorL10n.launchLoadingProcessingResponse("\(percent)")
+ }
+ }
+}
diff --git a/Riot/Modules/LaunchLoading/LaunchLoadingView.xib b/Riot/Modules/LaunchLoading/LaunchLoadingView.xib
index c933d1e00c..81a6b64f9b 100644
--- a/Riot/Modules/LaunchLoading/LaunchLoadingView.xib
+++ b/Riot/Modules/LaunchLoading/LaunchLoadingView.xib
@@ -1,32 +1,51 @@
-
+
-
+
+
-
+
-
+
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Riot/Modules/LaunchLoading/LaunchLoadingViewController.swift b/Riot/Modules/LaunchLoading/LaunchLoadingViewController.swift
index bd7dba4097..1da229b79a 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() {
+ init(syncProgress: MXSessionSyncProgress?) {
super.init(nibName: "LaunchLoadingViewController", bundle: nil)
- let launchLoadingView = LaunchLoadingView.instantiate()
+ let launchLoadingView = LaunchLoadingView.instantiate(syncProgress: syncProgress)
launchLoadingView.update(theme: ThemeService.shared().theme)
view.vc_addSubViewMatchingParent(launchLoadingView)
diff --git a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h
index 2e71969ea0..d7bf9d8fca 100644
--- a/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h
+++ b/Riot/Modules/MatrixKit/Views/RoomInputToolbar/MXKRoomInputToolbarView.h
@@ -47,6 +47,7 @@ typedef enum : NSUInteger
@class MXKRoomInputToolbarView;
+@class MXKImageView;
@protocol MXKRoomInputToolbarViewDelegate
/**
@@ -381,4 +382,6 @@ typedef enum : NSUInteger
*/
@property (nonatomic) NSAttributedString *attributedTextMessage;
+- (void)dismissValidationView:(MXKImageView*)validationView;
+
@end
diff --git a/Riot/Modules/Room/MXKRoomViewController.h b/Riot/Modules/Room/MXKRoomViewController.h
index dd3bfd2051..7c1eaa5772 100644
--- a/Riot/Modules/Room/MXKRoomViewController.h
+++ b/Riot/Modules/Room/MXKRoomViewController.h
@@ -214,14 +214,14 @@ typedef NS_ENUM(NSUInteger, MXKRoomViewControllerJoinRoomResult) {
@property (weak, nonatomic) IBOutlet UITableView *bubblesTableView;
@property (weak, nonatomic) IBOutlet UIView *roomTitleViewContainer;
-@property (weak, nonatomic) IBOutlet UIView *roomInputToolbarContainer;
+@property (strong, nonatomic) IBOutlet UIView *roomInputToolbarContainer;
@property (weak, nonatomic) IBOutlet UIView *roomActivitiesContainer;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bubblesTableViewTopConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bubblesTableViewBottomConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *roomActivitiesContainerHeightConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *roomInputToolbarContainerHeightConstraint;
-@property (weak, nonatomic) IBOutlet NSLayoutConstraint *roomInputToolbarContainerBottomConstraint;
+@property (strong, nonatomic) IBOutlet NSLayoutConstraint *roomInputToolbarContainerBottomConstraint;
#pragma mark - Class methods
diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h
index af49e8f6ef..5b162b58c8 100644
--- a/Riot/Modules/Room/RoomViewController.h
+++ b/Riot/Modules/Room/RoomViewController.h
@@ -72,6 +72,7 @@ extern NSTimeInterval const kResizeComposerAnimationDuration;
@property (weak, nonatomic, nullable) IBOutlet UIView *inputBackgroundView;
@property (weak, nonatomic, nullable) IBOutlet UIButton *scrollToBottomButton;
@property (weak, nonatomic, nullable) IBOutlet BadgeLabel *scrollToBottomBadgeLabel;
+@property (nonatomic, strong) IBOutlet UIView *overlayContainerView;
// Remove Jitsi widget container
@property (weak, nonatomic, nullable) IBOutlet UIView *removeJitsiWidgetContainer;
@@ -115,6 +116,13 @@ extern NSTimeInterval const kResizeComposerAnimationDuration;
// The voice broadcast service
@property (nonatomic, nullable) VoiceBroadcastService *voiceBroadcastService;
+@property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *toolbarContainerConstraints;
+
+@property (strong, nonatomic, nullable) UIView* maximisedToolbarDimmingView;
+
+@property (nonatomic) CGFloat wysiwygTranslation;
+
+
/**
Retrieve the live data source in cases where the timeline is not live.
diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m
index a0834940a7..25170d023b 100644
--- a/Riot/Modules/Room/RoomViewController.m
+++ b/Riot/Modules/Room/RoomViewController.m
@@ -187,7 +187,6 @@ @interface RoomViewController () inputToolbar = (id)self.inputToolbarView;
[inputToolbar setVoiceMessageToolbarView:self.voiceMessageController.voiceMessageToolbarView];
diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift
index 7ac56f0d23..c3857db81d 100644
--- a/Riot/Modules/Room/RoomViewController.swift
+++ b/Riot/Modules/Room/RoomViewController.swift
@@ -154,6 +154,87 @@ extension RoomViewController {
RiotSettings.shared.enableWysiwygTextFormatting.toggle()
wysiwygInputToolbar?.textFormattingEnabled.toggle()
}
+
+ @objc func didChangeMaximisedState(_ isMaximised: Bool) {
+ guard let wysiwygInputToolbar = wysiwygInputToolbar else { return }
+ if isMaximised {
+ var view: UIView!
+ // iPhone
+ if let navView = self.navigationController?.navigationController?.view {
+ view = navView
+ // iPad
+ } else if let navView = self.navigationController?.view {
+ view = navView
+ } else {
+ return
+ }
+ var originalRect = roomInputToolbarContainer.convert(roomInputToolbarContainer.frame, to: view)
+ var optionalTextView: UITextView?
+ if wysiwygInputToolbar.isFocused {
+ let textView = UITextView()
+ optionalTextView = textView
+ self.view.window?.addSubview(textView)
+ optionalTextView?.becomeFirstResponder()
+ originalRect = wysiwygInputToolbar.convert(wysiwygInputToolbar.frame, to: view)
+ }
+ wysiwygInputToolbar.showKeyboard()
+ roomInputToolbarContainer.removeFromSuperview()
+ let dimmingView = UIView()
+ dimmingView.translatesAutoresizingMaskIntoConstraints = false
+ // Same as the system dimming background color
+ dimmingView.backgroundColor = .black.withAlphaComponent(ThemeService.shared().isCurrentThemeDark() ? 0.29 : 0.12)
+ maximisedToolbarDimmingView = dimmingView
+ view.addSubview(dimmingView)
+ dimmingView.frame = view.bounds
+ NSLayoutConstraint.activate(
+ [
+ dimmingView.topAnchor.constraint(equalTo: view.topAnchor),
+ dimmingView.leftAnchor.constraint(equalTo: view.leftAnchor),
+ dimmingView.rightAnchor.constraint(equalTo: view.rightAnchor),
+ dimmingView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
+ ]
+ )
+ dimmingView.addSubview(self.roomInputToolbarContainer)
+ roomInputToolbarContainer.frame = originalRect
+ roomInputToolbarContainer.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
+ roomInputToolbarContainer.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
+ roomInputToolbarContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
+ UIView.animate(withDuration: kResizeComposerAnimationDuration, delay: 0, options: [.curveEaseInOut]) {
+ view.layoutIfNeeded()
+ }
+ let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPanRoomToolbarContainer(_ :)))
+ roomInputToolbarContainer.addGestureRecognizer(panGesture)
+ optionalTextView?.removeFromSuperview()
+ } else {
+ let originalRect = wysiwygInputToolbar.convert(wysiwygInputToolbar.frame, to: view)
+ var optionalTextView: UITextView?
+ if wysiwygInputToolbar.isFocused {
+ let textView = UITextView()
+ optionalTextView = textView
+ self.view.window?.addSubview(textView)
+ optionalTextView?.becomeFirstResponder()
+ wysiwygInputToolbar.showKeyboard()
+ }
+ self.roomInputToolbarContainer.removeFromSuperview()
+ maximisedToolbarDimmingView?.removeFromSuperview()
+ maximisedToolbarDimmingView = nil
+ self.view.insertSubview(self.roomInputToolbarContainer, belowSubview: self.overlayContainerView)
+ roomInputToolbarContainer.frame = originalRect
+ NSLayoutConstraint.activate(self.toolbarContainerConstraints)
+ self.roomInputToolbarContainerBottomConstraint.isActive = true
+ UIView.animate(withDuration: kResizeComposerAnimationDuration, delay: 0, options: [.curveEaseInOut]) {
+ self.view.layoutIfNeeded()
+ }
+ roomInputToolbarContainer.gestureRecognizers?.removeAll()
+ optionalTextView?.removeFromSuperview()
+ }
+ }
+
+ @objc func setMaximisedToolbarIsHiddenIfNeeded(_ isHidden: Bool) {
+ if wysiwygInputToolbar?.isMaximised == true {
+ roomInputToolbarContainer.superview?.isHidden = isHidden
+ }
+ }
}
// MARK: - Private Helpers
@@ -165,4 +246,30 @@ private extension RoomViewController {
var wysiwygInputToolbar: WysiwygInputToolbarView? {
return self.inputToolbarView as? WysiwygInputToolbarView
}
+
+ @objc private func didPanRoomToolbarContainer(_ sender: UIPanGestureRecognizer) {
+ guard let wysiwygInputToolbar = wysiwygInputToolbar else { return }
+ switch sender.state {
+ case .began:
+ wysiwygTranslation = wysiwygInputToolbar.maxExpandedHeight
+ case .changed:
+ let translation = sender.translation(in: view.window)
+ let translatedValue = wysiwygInputToolbar.maxExpandedHeight - translation.y
+ wysiwygTranslation = translatedValue
+ guard translatedValue <= wysiwygInputToolbar.maxExpandedHeight, translatedValue >= wysiwygInputToolbar.compressedHeight else { return }
+ wysiwygInputToolbar.idealHeight = translatedValue
+ case .ended:
+ if wysiwygTranslation <= wysiwygInputToolbar.maxCompressedHeight {
+ wysiwygInputToolbar.minimise()
+ } else {
+ wysiwygTranslation = wysiwygInputToolbar.maxExpandedHeight
+ wysiwygInputToolbar.idealHeight = wysiwygInputToolbar.maxExpandedHeight
+ }
+ case .cancelled:
+ wysiwygTranslation = wysiwygInputToolbar.maxExpandedHeight
+ wysiwygInputToolbar.idealHeight = wysiwygInputToolbar.maxExpandedHeight
+ default:
+ break
+ }
+ }
}
diff --git a/Riot/Modules/Room/RoomViewController.xib b/Riot/Modules/Room/RoomViewController.xib
index 0732766042..f33d661bd4 100644
--- a/Riot/Modules/Room/RoomViewController.xib
+++ b/Riot/Modules/Room/RoomViewController.xib
@@ -1,9 +1,9 @@
-
+
-
+
@@ -35,6 +35,10 @@
+
+
+
+
diff --git a/Riot/Modules/Room/TimelineCells/SizableCell/SizableBaseRoomCell.swift b/Riot/Modules/Room/TimelineCells/SizableCell/SizableBaseRoomCell.swift
index f33762144e..b8ba675b2c 100644
--- a/Riot/Modules/Room/TimelineCells/SizableCell/SizableBaseRoomCell.swift
+++ b/Riot/Modules/Room/TimelineCells/SizableCell/SizableBaseRoomCell.swift
@@ -65,6 +65,12 @@ class SizableBaseRoomCell: BaseRoomCell, SizableBaseRoomCellType {
return self.height(for: roomBubbleCellData, fitting: maxWidth)
}
+
+ override func prepareForReuse() {
+ cleanContentVC()
+
+ super.prepareForReuse()
+ }
// MARK - SizableBaseRoomCellType
@@ -173,10 +179,21 @@ class SizableBaseRoomCell: BaseRoomCell, SizableBaseRoomCellType {
}
return height
- }
-
+ }
+
+ private func cleanContentVC() {
+ contentVC?.removeFromParent()
+ contentVC?.view.removeFromSuperview()
+ contentVC?.didMove(toParent: nil)
+ contentVC = nil
+ }
+
+ // MARK: - Public
+
func addContentViewController(_ controller: UIViewController, on contentView: UIView) {
controller.view.invalidateIntrinsicContentSize()
+
+ cleanContentVC()
let parent = vc_parentViewController
parent?.addChild(controller)
@@ -185,13 +202,4 @@ class SizableBaseRoomCell: BaseRoomCell, SizableBaseRoomCellType {
contentVC = controller
}
-
- override func prepareForReuse() {
- contentVC?.removeFromParent()
- contentVC?.view.removeFromSuperview()
- contentVC?.didMove(toParent: nil)
- contentVC = nil
-
- super.prepareForReuse()
- }
}
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainCell.swift
index 8987cb1de5..f673bebeee 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainCell.swift
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Playback/VoiceBroadcastPlaybackPlainCell.swift
@@ -27,7 +27,7 @@ class VoiceBroadcastPlaybackPlainCell: SizableBaseRoomCell, RoomCellReactionsDis
let bubbleData = cellData as? RoomBubbleCellData,
let event = bubbleData.events.last,
let voiceBroadcastContent = VoiceBroadcastInfo(fromJSON: event.content),
- voiceBroadcastContent.state == VoiceBroadcastInfo.State.started.rawValue,
+ voiceBroadcastContent.state == VoiceBroadcastInfoState.started.rawValue,
let controller = VoiceBroadcastPlaybackProvider.shared.buildVoiceBroadcastPlaybackVCForEvent(event,
senderDisplayName: bubbleData.senderDisplayName,
voiceBroadcastState: bubbleData.voiceBroadcastState)
diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainCell.swift
index a65254be5d..43047cfbab 100644
--- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainCell.swift
+++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/VoiceBroadcast/Recorder/VoiceBroadcastRecorderPlainCell.swift
@@ -28,7 +28,7 @@ class VoiceBroadcastRecorderPlainCell: SizableBaseRoomCell, RoomCellReactionsDis
let bubbleData = cellData as? RoomBubbleCellData,
let event = bubbleData.events.last,
let voiceBroadcastContent = VoiceBroadcastInfo(fromJSON: event.content),
- voiceBroadcastContent.state == VoiceBroadcastInfo.State.started.rawValue,
+ voiceBroadcastContent.state == VoiceBroadcastInfoState.started.rawValue,
let view = VoiceBroadcastRecorderProvider.shared.buildVoiceBroadcastRecorderViewForEvent(event, senderDisplayName: bubbleData.senderDisplayName) else {
return
}
diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h
index 4bdea353b6..4e351806ce 100644
--- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h
+++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.h
@@ -75,6 +75,8 @@ typedef NS_ENUM(NSUInteger, RoomInputToolbarViewSendMode)
*/
- (void)roomInputToolbarView:(RoomInputToolbarView *)toolbarView sendAttributedTextMessage:(NSAttributedString *)attributedTextMessage;
+- (void)didChangeMaximisedState: (BOOL) isMaximised;
+
@end
/**
diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift
index e6fad8e094..cee4a9c119 100644
--- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift
+++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift
@@ -32,14 +32,23 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
// MARK: - Properties
// MARK: Private
+ private var keyboardHeight: CGFloat = .zero {
+ didSet {
+ updateTextViewHeight()
+ }
+ }
private var voiceMessageToolbarView: VoiceMessageToolbarView?
private var cancellables = Set()
private var heightConstraint: NSLayoutConstraint!
+ private var voiceMessageBottomConstraint: NSLayoutConstraint?
private var hostingViewController: VectorHostingController!
private var wysiwygViewModel = WysiwygComposerViewModel(textColor: ThemeService.shared().theme.colors.primaryContent)
- private var viewModel: ComposerViewModelProtocol = ComposerViewModel(
- initialViewState: ComposerViewState(textFormattingEnabled: RiotSettings.shared.enableWysiwygTextFormatting,
- bindings: ComposerBindings(focused: false)))
+ private var viewModel: ComposerViewModelProtocol!
+
+ private var isLandscapePhone: Bool {
+ let device = UIDevice.current
+ return device.isPhone && device.orientation.isLandscape
+ }
// MARK: Public
@@ -52,6 +61,35 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
}
}
+ override var isFocused: Bool {
+ viewModel.isFocused
+ }
+
+ var isMaximised: Bool {
+ wysiwygViewModel.maximised
+ }
+
+ var idealHeight: CGFloat {
+ get {
+ wysiwygViewModel.idealHeight
+ }
+ set {
+ wysiwygViewModel.idealHeight = newValue
+ }
+ }
+
+ var compressedHeight: CGFloat {
+ wysiwygViewModel.compressedHeight
+ }
+
+ var maxExpandedHeight: CGFloat {
+ wysiwygViewModel.maxExpandedHeight
+ }
+
+ var maxCompressedHeight: CGFloat {
+ wysiwygViewModel.maxCompressedHeight
+ }
+
// MARK: - Setup
override class func instantiate() -> MXKRoomInputToolbarView! {
@@ -64,6 +102,9 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
override func awakeFromNib() {
super.awakeFromNib()
+ viewModel = ComposerViewModel(
+ initialViewState: ComposerViewState(textFormattingEnabled: RiotSettings.shared.enableWysiwygTextFormatting,
+ isLandscapePhone: isLandscapePhone, bindings: ComposerBindings(focused: false)))
viewModel.callback = { [weak self] result in
self?.handleViewModelResult(result)
@@ -115,11 +156,33 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
.removeDuplicates()
.sink { [weak hostingViewController] _ in
hostingViewController?.view.setNeedsLayout()
+ },
+
+ wysiwygViewModel.$maximised
+ .dropFirst()
+ .removeDuplicates()
+ .sink { [weak self] value in
+ guard let self = self else { return }
+ self.toolbarViewDelegate?.didChangeMaximisedState(value)
+ self.hostingViewController.view.layer.cornerRadius = value ? 20 : 0
}
]
update(theme: ThemeService.shared().theme)
registerThemeServiceDidChangeThemeNotification()
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(keyboardWillShow),
+ name: UIResponder.keyboardWillShowNotification,
+ object: nil
+ )
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(keyboardWillHide),
+ name: UIResponder.keyboardWillHideNotification,
+ object: nil
+ )
+ NotificationCenter.default.addObserver(self, selector: #selector(deviceDidRotate), name: UIDevice.orientationDidChangeNotification, object: nil)
}
override func customizeRendering() {
@@ -131,8 +194,54 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
self.viewModel.dismissKeyboard()
}
+ override func dismissValidationView(_ validationView: MXKImageView!) {
+ super.dismissValidationView(validationView)
+ if isMaximised {
+ showKeyboard()
+ }
+ }
+
+ func showKeyboard() {
+ self.viewModel.showKeyboard()
+ }
+
+ func minimise() {
+ wysiwygViewModel.maximised = false
+ }
+
// MARK: - Private
+ @objc private func keyboardWillShow(_ notification: Notification) {
+ if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
+ let keyboardRectangle = keyboardFrame.cgRectValue
+ keyboardHeight = keyboardRectangle.height
+ UIView.performWithoutAnimation {
+ if self.isMaximised {
+ self.voiceMessageBottomConstraint?.constant = keyboardHeight - (window?.safeAreaInsets.bottom ?? 0) + 4
+ } else {
+ self.voiceMessageBottomConstraint?.constant = 4
+ }
+ self.layoutIfNeeded()
+ }
+ }
+ }
+
+ @objc private func keyboardWillHide(_ notification: Notification) {
+ if self.isMaximised {
+ UIView.performWithoutAnimation {
+ self.voiceMessageBottomConstraint?.constant = 4
+ self.layoutIfNeeded()
+ }
+ }
+ }
+
+ @objc private func deviceDidRotate(_ notification: Notification) {
+ viewModel.isLandscapePhone = isLandscapePhone
+ DispatchQueue.main.async {
+ self.updateTextViewHeight()
+ }
+ }
+
private func updateToolbarHeight(wysiwygHeight: CGFloat) {
self.heightConstraint.constant = wysiwygHeight
toolbarViewDelegate?.roomInputToolbarView?(self, heightDidChanged: wysiwygHeight, completion: nil)
@@ -140,6 +249,9 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
private func sendWysiwygMessage(content: WysiwygComposerContent) {
delegate?.roomInputToolbarView?(self, sendFormattedTextMessage: content.html, withRawText: content.markdown)
+ if isMaximised {
+ minimise()
+ }
}
private func showSendMediaActions() {
@@ -179,6 +291,19 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
wysiwygViewModel.textColor = theme.colors.primaryContent
}
+ private func updateTextViewHeight() {
+ let height = UIScreen.main.bounds.height
+ let barOffset: CGFloat = 68
+ let toolbarHeight: CGFloat = 96
+ let finalHeight = height - keyboardHeight - toolbarHeight - barOffset
+ wysiwygViewModel.maxExpandedHeight = finalHeight
+ if finalHeight < 200 {
+ wysiwygViewModel.maxCompressedHeight = finalHeight > wysiwygViewModel.minHeight ? finalHeight : wysiwygViewModel.minHeight
+ } else {
+ wysiwygViewModel.maxCompressedHeight = 200
+ }
+ }
+
// MARK: - HtmlRoomInputToolbarViewProtocol
var isEncryptionEnabled = false {
didSet {
@@ -225,9 +350,6 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
set {
self.viewModel.textFormattingEnabled = newValue
self.wysiwygViewModel.plainTextMode = !newValue
- if !newValue {
- self.wysiwygViewModel.maximised = false
- }
}
}
@@ -239,17 +361,21 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
voiceMessageToolbarView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.deactivate(voiceMessageToolbarView.containersTopConstraints)
addSubview(voiceMessageToolbarView)
+ let bottomConstraint = hostingViewController.view.bottomAnchor.constraint(equalTo: voiceMessageToolbarView.bottomAnchor, constant: 4)
+ voiceMessageBottomConstraint = bottomConstraint
NSLayoutConstraint.activate(
[
- hostingViewController.view.topAnchor.constraint(equalTo: voiceMessageToolbarView.topAnchor),
- hostingViewController.view.leftAnchor.constraint(equalTo: voiceMessageToolbarView.leftAnchor),
- hostingViewController.view.bottomAnchor.constraint(equalTo: voiceMessageToolbarView.bottomAnchor, constant: 4),
- hostingViewController.view.rightAnchor.constraint(equalTo: voiceMessageToolbarView.rightAnchor)
+ hostingViewController.view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: voiceMessageToolbarView.topAnchor),
+ hostingViewController.view.safeAreaLayoutGuide.leftAnchor.constraint(equalTo: voiceMessageToolbarView.leftAnchor),
+ bottomConstraint,
+ hostingViewController.view.safeAreaLayoutGuide.rightAnchor.constraint(equalTo: voiceMessageToolbarView.rightAnchor)
]
)
} else {
self.voiceMessageToolbarView?.removeFromSuperview()
self.voiceMessageToolbarView = nil
+ self.voiceMessageBottomConstraint?.isActive = false
+ self.voiceMessageBottomConstraint = nil
}
}
diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift
index 231773f2b5..4a242a91ce 100644
--- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift
+++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioPlayer.swift
@@ -61,7 +61,8 @@ class VoiceMessageAudioPlayer: NSObject {
}
var currentTime: TimeInterval {
- return abs(CMTimeGetSeconds(audioPlayer?.currentTime() ?? .zero))
+ let currentTime = abs(CMTimeGetSeconds(audioPlayer?.currentTime() ?? .zero))
+ return currentTime.isFinite ? currentTime : .zero
}
var playerItems: [AVPlayerItem] {
diff --git a/Riot/Modules/Settings/Security/SecureBackup/SettingsSecureBackupTableViewSection.swift b/Riot/Modules/Settings/Security/SecureBackup/SettingsSecureBackupTableViewSection.swift
index ffeed21214..58982bb2a9 100644
--- a/Riot/Modules/Settings/Security/SecureBackup/SettingsSecureBackupTableViewSection.swift
+++ b/Riot/Modules/Settings/Security/SecureBackup/SettingsSecureBackupTableViewSection.swift
@@ -140,8 +140,20 @@ private enum BackupRows {
.info(text: infoText),
.createSecureBackupAction
]
- case .keyBackup(let keyBackupVersion, _, _),
- .keyBackupNotTrusted(let keyBackupVersion, _): // Manage the key backup in the same way for the moment
+ case .keyBackup(let keyBackupVersion, _, let progress):
+ if let progress = progress {
+ backupRows = [
+ .info(text: importProgressText(for: progress)),
+ .deleteKeyBackupAction(keyBackupVersion: keyBackupVersion)
+ ]
+ } else {
+ backupRows = [
+ .info(text: VectorL10n.securitySettingsSecureBackupInfoValid),
+ .restoreFromKeyBackupAction(keyBackupVersion: keyBackupVersion, title: VectorL10n.securitySettingsSecureBackupRestore),
+ .deleteKeyBackupAction(keyBackupVersion: keyBackupVersion)
+ ]
+ }
+ case .keyBackupNotTrusted(let keyBackupVersion, _):
backupRows = [
.info(text: VectorL10n.securitySettingsSecureBackupInfoValid),
.restoreFromKeyBackupAction(keyBackupVersion: keyBackupVersion, title: VectorL10n.securitySettingsSecureBackupRestore),
@@ -160,8 +172,22 @@ private enum BackupRows {
.createKeyBackupAction,
.resetSecureBackupAction
]
- case .keyBackup(let keyBackupVersion, _, _),
- .keyBackupNotTrusted(let keyBackupVersion, _): // Manage the key backup in the same way for the moment
+ case .keyBackup(let keyBackupVersion, _, let progress):
+ if let progress = progress {
+ backupRows = [
+ .info(text: importProgressText(for: progress)),
+ .deleteKeyBackupAction(keyBackupVersion: keyBackupVersion),
+ .resetSecureBackupAction
+ ]
+ } else {
+ backupRows = [
+ .info(text: VectorL10n.securitySettingsSecureBackupInfoValid),
+ .restoreFromKeyBackupAction(keyBackupVersion: keyBackupVersion, title: VectorL10n.securitySettingsSecureBackupRestore),
+ .deleteKeyBackupAction(keyBackupVersion: keyBackupVersion),
+ .resetSecureBackupAction
+ ]
+ }
+ case .keyBackupNotTrusted(let keyBackupVersion, _):
backupRows = [
.info(text: VectorL10n.securitySettingsSecureBackupInfoValid),
.restoreFromKeyBackupAction(keyBackupVersion: keyBackupVersion, title: VectorL10n.securitySettingsSecureBackupRestore),
@@ -172,6 +198,11 @@ private enum BackupRows {
}
self.backupRows = backupRows
}
+
+ private func importProgressText(for progress: Progress) -> String {
+ let percentage = Int(round(progress.fractionCompleted * 100))
+ return VectorL10n.keyBackupRecoverFromPrivateKeyInfo + " \(percentage)%"
+ }
// MARK: - Cells -
diff --git a/Riot/Modules/Settings/Security/SecureBackup/SettingsSecureBackupViewModel.swift b/Riot/Modules/Settings/Security/SecureBackup/SettingsSecureBackupViewModel.swift
index 66b7208615..5039bef6f6 100644
--- a/Riot/Modules/Settings/Security/SecureBackup/SettingsSecureBackupViewModel.swift
+++ b/Riot/Modules/Settings/Security/SecureBackup/SettingsSecureBackupViewModel.swift
@@ -24,6 +24,7 @@ final class SettingsSecureBackupViewModel: SettingsSecureBackupViewModelType {
// MARK: Private
private let recoveryService: MXRecoveryService
private let keyBackup: MXKeyBackup
+ private var progressUpdateTimer: Timer?
init(recoveryService: MXRecoveryService, keyBackup: MXKeyBackup) {
self.recoveryService = recoveryService
@@ -106,17 +107,13 @@ final class SettingsSecureBackupViewModel: SettingsSecureBackupViewModelType {
guard let keyBackupVersion = self.keyBackup.keyBackupVersion, let keyBackupVersionTrust = keyBackupVersionTrust else {
return
}
-
- // Get the backup progress before updating the state
- self.keyBackup.backupProgress { [weak self] (progress) in
- guard let self = self else {
- return
- }
-
- let keyBackupState: SettingsSecureBackupViewState.KeyBackupState = .keyBackup(keyBackupVersion, keyBackupVersionTrust, progress)
- let viewState: SettingsSecureBackupViewState = self.recoveryService.hasRecovery() ? .secureBackup(keyBackupState) : .noSecureBackup(keyBackupState)
- self.viewDelegate?.settingsSecureBackupViewModel(self, didUpdateViewState: viewState)
- }
+
+ let importProgress = keyBackup.importProgress
+ let keyBackupState: SettingsSecureBackupViewState.KeyBackupState = .keyBackup(keyBackupVersion, keyBackupVersionTrust, importProgress)
+ let viewState: SettingsSecureBackupViewState = self.recoveryService.hasRecovery() ? .secureBackup(keyBackupState) : .noSecureBackup(keyBackupState)
+ self.viewDelegate?.settingsSecureBackupViewModel(self, didUpdateViewState: viewState)
+ scheduleProgressUpdateIfNecessary(keyBackupVersionTrust: keyBackupVersionTrust, progress: importProgress)
+
default:
break
}
@@ -130,6 +127,17 @@ final class SettingsSecureBackupViewModel: SettingsSecureBackupViewModelType {
self.viewDelegate?.settingsSecureBackupViewModel(self, didUpdateViewState: viewState)
}
}
+
+ private func scheduleProgressUpdateIfNecessary(keyBackupVersionTrust: MXKeyBackupVersionTrust, progress: Progress?) {
+ if progress != nil {
+ progressUpdateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in
+ self?.computeState(withBackupVersionTrust: keyBackupVersionTrust)
+ }
+ } else {
+ progressUpdateTimer?.invalidate()
+ progressUpdateTimer = nil
+ }
+ }
private func deleteKeyBackupVersion(_ keyBackupVersion: MXKeyBackupVersion) {
guard let keyBackupVersionVersion = keyBackupVersion.version else {
diff --git a/Riot/Modules/Settings/Security/SecureBackup/SettingsSecureBackupViewState.swift b/Riot/Modules/Settings/Security/SecureBackup/SettingsSecureBackupViewState.swift
index 1390f56ba7..96e698a423 100644
--- a/Riot/Modules/Settings/Security/SecureBackup/SettingsSecureBackupViewState.swift
+++ b/Riot/Modules/Settings/Security/SecureBackup/SettingsSecureBackupViewState.swift
@@ -35,7 +35,7 @@ enum SettingsSecureBackupViewState {
/// - keyBackupNotTrusted: There is a backup on the homeserver but it is not trusted
enum KeyBackupState {
case noKeyBackup
- case keyBackup(MXKeyBackupVersion, MXKeyBackupVersionTrust, Progress)
+ case keyBackup(MXKeyBackupVersion, MXKeyBackupVersionTrust, Progress?)
case keyBackupNotTrusted(MXKeyBackupVersion, MXKeyBackupVersionTrust)
}
}
diff --git a/Riot/Modules/Settings/Security/SecurityViewController.m b/Riot/Modules/Settings/Security/SecurityViewController.m
index d3c7c6c357..cb72f38df1 100644
--- a/Riot/Modules/Settings/Security/SecurityViewController.m
+++ b/Riot/Modules/Settings/Security/SecurityViewController.m
@@ -1218,9 +1218,9 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N
cell = [secureBackupSection cellForRowAtRow:rowTag];
}
#ifdef CROSS_SIGNING_AND_BACKUP_DEV
- else if (section == SECTION_KEYBACKUP)
+ else if (sectionTag == SECTION_KEYBACKUP)
{
- cell = [keyBackupSection cellForRowAtRow:row];
+ cell = [keyBackupSection cellForRowAtRow:rowTag];
}
#endif
else if (sectionTag == SECTION_CROSSSIGNING)
diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift
index fb90d834de..31dd0045bf 100644
--- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift
+++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift
@@ -21,12 +21,19 @@ public enum VoiceBroadcastAggregatorError: Error {
case invalidVoiceBroadcastStartEvent
}
+public enum VoiceBroadcastAggregatorLaunchState {
+ case idle
+ case starting
+ case loaded
+ case error
+}
+
public protocol VoiceBroadcastAggregatorDelegate: AnyObject {
func voiceBroadcastAggregatorDidStartLoading(_ aggregator: VoiceBroadcastAggregator)
func voiceBroadcastAggregatorDidEndLoading(_ aggregator: VoiceBroadcastAggregator)
func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didFailWithError: Error)
func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveChunk: VoiceBroadcastChunk)
- func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfo.State)
+ func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfoState)
func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator)
}
@@ -56,8 +63,8 @@ public class VoiceBroadcastAggregator {
}
}
- public private(set) var isStarted: Bool = false
- public private(set) var voiceBroadcastState: VoiceBroadcastInfo.State
+ private(set) var launchState: VoiceBroadcastAggregatorLaunchState = .idle
+ public private(set) var voiceBroadcastState: VoiceBroadcastInfoState
public var delegate: VoiceBroadcastAggregatorDelegate?
deinit {
@@ -66,7 +73,7 @@ public class VoiceBroadcastAggregator {
}
}
- public init(session: MXSession, room: MXRoom, voiceBroadcastStartEventId: String, voiceBroadcastState: VoiceBroadcastInfo.State) throws {
+ public init(session: MXSession, room: MXRoom, voiceBroadcastStartEventId: String, voiceBroadcastState: VoiceBroadcastInfoState) throws {
self.session = session
self.room = room
self.voiceBroadcastStartEventId = voiceBroadcastStartEventId
@@ -111,7 +118,7 @@ public class VoiceBroadcastAggregator {
event.stateKey == self.voiceBroadcastSenderId,
let voiceBroadcastInfo = VoiceBroadcastInfo(fromJSON: event.content),
(event.eventId == self.voiceBroadcastStartEventId || voiceBroadcastInfo.voiceBroadcastId == self.voiceBroadcastStartEventId),
- let state = VoiceBroadcastInfo.State(rawValue: voiceBroadcastInfo.state) else {
+ let state = VoiceBroadcastInfoState(rawValue: voiceBroadcastInfo.state) else {
return
}
@@ -120,10 +127,10 @@ public class VoiceBroadcastAggregator {
}
func start() {
- if isStarted {
+ guard launchState == .idle else {
return
}
- isStarted = true
+ launchState = .starting
delegate?.voiceBroadcastAggregatorDidStartLoading(self)
@@ -156,16 +163,14 @@ public class VoiceBroadcastAggregator {
return
}
- if let chunk = self.voiceBroadcastBuilder.buildChunk(event: event, mediaManager: self.session.mediaManager, voiceBroadcastStartEventId: self.voiceBroadcastStartEventId) {
- self.delegate?.voiceBroadcastAggregator(self, didReceiveChunk: chunk)
- }
-
- if !self.events.contains(where: { newEvent in
- newEvent.eventId == event.eventId
- }) {
+ if !self.events.contains(where: { $0.eventId == event.eventId }) {
self.events.append(event)
MXLog.debug("[VoiceBroadcastAggregator] Got a new chunk for broadcast \(relatedEventId). Total: \(self.events.count)")
+ if let chunk = self.voiceBroadcastBuilder.buildChunk(event: event, mediaManager: self.session.mediaManager, voiceBroadcastStartEventId: self.voiceBroadcastStartEventId) {
+ self.delegate?.voiceBroadcastAggregator(self, didReceiveChunk: chunk)
+ }
+
self.voiceBroadcast = self.voiceBroadcastBuilder.build(mediaManager: self.session.mediaManager,
voiceBroadcastStartEventId: self.voiceBroadcastStartEventId,
voiceBroadcastInvoiceBroadcastStartEventContent: self.voiceBroadcastInfoStartEventContent,
@@ -177,7 +182,6 @@ public class VoiceBroadcastAggregator {
}
} as Any
-
self.events.forEach { event in
guard let chunk = self.voiceBroadcastBuilder.buildChunk(event: event, mediaManager: self.session.mediaManager, voiceBroadcastStartEventId: self.voiceBroadcastStartEventId) else {
return
@@ -195,6 +199,7 @@ public class VoiceBroadcastAggregator {
MXLog.debug("[VoiceBroadcastAggregator] Start aggregation with \(self.voiceBroadcast.chunks.count) chunks for broadcast \(self.voiceBroadcastStartEventId)")
+ self.launchState = .loaded
self.delegate?.voiceBroadcastAggregatorDidEndLoading(self)
} failure: { [weak self] error in
@@ -203,7 +208,7 @@ public class VoiceBroadcastAggregator {
}
MXLog.error("[VoiceBroadcastAggregator] start failed", context: error)
- self.isStarted = false
+ self.launchState = .error
self.delegate?.voiceBroadcastAggregator(self, didFailWithError: error)
}
}
diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift
index b2bc1afe42..5e6218f298 100644
--- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift
+++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.swift
@@ -19,36 +19,29 @@ import Foundation
extension VoiceBroadcastInfo {
// MARK: - Constants
- public enum State: String {
- case started
- case paused
- case resumed
- case stopped
- }
-
// MARK: - Public
@objc static func isStarted(for name: String) -> Bool {
- return name == State.started.rawValue
+ return name == VoiceBroadcastInfoState.started.rawValue
}
@objc static func isStopped(for name: String) -> Bool {
- return name == State.stopped.rawValue
+ return name == VoiceBroadcastInfoState.stopped.rawValue
}
@objc static func startedValue() -> String {
- return State.started.rawValue
+ return VoiceBroadcastInfoState.started.rawValue
}
@objc static func pausedValue() -> String {
- return State.paused.rawValue
+ return VoiceBroadcastInfoState.paused.rawValue
}
@objc static func resumedValue() -> String {
- return State.resumed.rawValue
+ return VoiceBroadcastInfoState.resumed.rawValue
}
@objc static func stoppedValue() -> String {
- return State.stopped.rawValue
+ return VoiceBroadcastInfoState.stopped.rawValue
}
}
diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfoState.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfoState.swift
new file mode 100644
index 0000000000..e808ddeb38
--- /dev/null
+++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfoState.swift
@@ -0,0 +1,22 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+public enum VoiceBroadcastInfoState: String {
+ case started
+ case paused
+ case resumed
+ case stopped
+}
diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift
index e6d6171a8a..6a3072ec3e 100644
--- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift
+++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift
@@ -25,13 +25,13 @@ public class VoiceBroadcastService: NSObject {
public let room: MXRoom
public private(set) var voiceBroadcastId: String?
- public private(set) var state: VoiceBroadcastInfo.State
+ public private(set) var state: VoiceBroadcastInfoState
// Mechanism to process one call of sendVoiceBroadcastInfo() at a time
private let asyncTaskQueue: MXAsyncTaskQueue
// MARK: - Setup
- public init(room: MXRoom, state: VoiceBroadcastInfo.State) {
+ public init(room: MXRoom, state: VoiceBroadcastInfoState) {
self.room = room
self.state = state
self.asyncTaskQueue = MXAsyncTaskQueue(label: "VoiceBroadcastServiceQueueEventSerialQueue-" + MXTools.generateSecret())
@@ -47,7 +47,7 @@ public class VoiceBroadcastService: NSObject {
/// - Parameters:
/// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success.
func startVoiceBroadcast(completion: @escaping (MXResponse) -> Void) {
- sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.started) { [weak self] response in
+ sendVoiceBroadcastInfo(state: VoiceBroadcastInfoState.started) { [weak self] response in
guard let self = self else { return }
switch response {
@@ -64,21 +64,21 @@ public class VoiceBroadcastService: NSObject {
/// - Parameters:
/// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success.
func pauseVoiceBroadcast(completion: @escaping (MXResponse) -> Void) {
- sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.paused, completion: completion)
+ sendVoiceBroadcastInfo(state: VoiceBroadcastInfoState.paused, completion: completion)
}
/// resume a voice broadcast.
/// - Parameters:
/// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success.
func resumeVoiceBroadcast(completion: @escaping (MXResponse) -> Void) {
- sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.resumed, completion: completion)
+ sendVoiceBroadcastInfo(state: VoiceBroadcastInfoState.resumed, completion: completion)
}
/// stop a voice broadcast info.
/// - Parameters:
/// - completion: A closure called when the operation completes. Provides the event id of the event generated on the home server on success.
func stopVoiceBroadcast(completion: @escaping (MXResponse) -> Void) {
- sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State.stopped, completion: completion)
+ sendVoiceBroadcastInfo(state: VoiceBroadcastInfoState.stopped, completion: completion)
}
func getState() -> String {
@@ -121,7 +121,7 @@ public class VoiceBroadcastService: NSObject {
// MARK: - Private
- private func allowedStates(from state: VoiceBroadcastInfo.State) -> [VoiceBroadcastInfo.State] {
+ private func allowedStates(from state: VoiceBroadcastInfoState) -> [VoiceBroadcastInfoState] {
switch state {
case .started:
return [.paused, .stopped]
@@ -134,7 +134,7 @@ public class VoiceBroadcastService: NSObject {
}
}
- private func sendVoiceBroadcastInfo(state: VoiceBroadcastInfo.State, completion: @escaping (MXResponse) -> Void) {
+ private func sendVoiceBroadcastInfo(state: VoiceBroadcastInfoState, completion: @escaping (MXResponse) -> Void) {
guard let userId = self.room.mxSession.myUserId else {
completion(.failure(VoiceBroadcastServiceError.missingUserId))
return
@@ -156,7 +156,7 @@ public class VoiceBroadcastService: NSObject {
voiceBroadcastInfo.state = state.rawValue
- if state != VoiceBroadcastInfo.State.started {
+ if state != VoiceBroadcastInfoState.started {
guard let voiceBroadcastId = self.voiceBroadcastId else {
completion(.failure(VoiceBroadcastServiceError.notStarted))
taskCompleted()
diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastServiceProvider.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastServiceProvider.swift
index e39c838b73..83051e1d5f 100644
--- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastServiceProvider.swift
+++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastServiceProvider.swift
@@ -70,9 +70,9 @@ class VoiceBroadcastServiceProvider {
}
}
- private func createVoiceBroadcastService(for room: MXRoom, state: VoiceBroadcastInfo.State) {
+ private func createVoiceBroadcastService(for room: MXRoom, state: VoiceBroadcastInfoState) {
- let voiceBroadcastService = VoiceBroadcastService(room: room, state: VoiceBroadcastInfo.State.stopped)
+ let voiceBroadcastService = VoiceBroadcastService(room: room, state: VoiceBroadcastInfoState.stopped)
self.currentVoiceBroadcastService = voiceBroadcastService
@@ -95,22 +95,22 @@ class VoiceBroadcastServiceProvider {
private func setupVoiceBroadcastService(for room: MXRoom, completion: @escaping (VoiceBroadcastService?) -> Void) {
self.getLastVoiceBroadcastInfo(for: room) { event in
guard let voiceBroadcastInfoEvent = event else {
- self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfo.State.stopped)
+ self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfoState.stopped)
completion(self.currentVoiceBroadcastService)
return
}
guard let voiceBroadcastInfo = VoiceBroadcastInfo(fromJSON: voiceBroadcastInfoEvent.content) else {
- self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfo.State.stopped)
+ self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfoState.stopped)
completion(self.currentVoiceBroadcastService)
return
}
- if voiceBroadcastInfo.state == VoiceBroadcastInfo.State.stopped.rawValue {
- self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfo.State.stopped)
+ if voiceBroadcastInfo.state == VoiceBroadcastInfoState.stopped.rawValue {
+ self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfoState.stopped)
completion(self.currentVoiceBroadcastService)
} else if voiceBroadcastInfoEvent.stateKey == room.mxSession.myUserId {
- self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfo.State(rawValue: voiceBroadcastInfo.state) ?? VoiceBroadcastInfo.State.stopped)
+ self.createVoiceBroadcastService(for: room, state: VoiceBroadcastInfoState(rawValue: voiceBroadcastInfo.state) ?? VoiceBroadcastInfoState.stopped)
completion(self.currentVoiceBroadcastService)
} else {
completion(nil)
diff --git a/RiotSwiftUI/Modules/Common/InfoSheet/Coordinator/InfoSheetCoordinator.swift b/RiotSwiftUI/Modules/Common/InfoSheet/Coordinator/InfoSheetCoordinator.swift
index 6fb7fc1103..54a9719b21 100644
--- a/RiotSwiftUI/Modules/Common/InfoSheet/Coordinator/InfoSheetCoordinator.swift
+++ b/RiotSwiftUI/Modules/Common/InfoSheet/Coordinator/InfoSheetCoordinator.swift
@@ -64,28 +64,22 @@ private extension InfoSheetCoordinator {
// The bottom sheet should be presented with the content intrinsic height as for design requirement
// We can do it easily just on iOS 16+
func setupPresentation(of viewController: VectorHostingController) {
- let cornerRadius: CGFloat = 24
+ let detents: [VectorHostingBottomSheetPreferences.Detent]
- guard
+ if
#available(iOS 16, *),
- let parentSize = parameters.parentSize,
- let presentationController = viewController.sheetPresentationController
- else {
- viewController.bottomSheetPreferences = .init(cornerRadius: cornerRadius)
- return
+ let parentSize = parameters.parentSize {
+
+ let intrisincSize = viewController.view.systemLayoutSizeFitting(.init(width: parentSize.width, height: UIView.layoutFittingCompressedSize.height),
+ withHorizontalFittingPriority: .defaultHigh,
+ verticalFittingPriority: .defaultLow)
+
+ detents = [.custom(height: intrisincSize.height), .large]
+ } else {
+ detents = [.medium, .large]
}
- let intrisincSize = viewController.view.systemLayoutSizeFitting(.init(width: parentSize.width, height: 0),
- withHorizontalFittingPriority: .defaultHigh,
- verticalFittingPriority: .defaultLow)
-
- presentationController.preferredCornerRadius = cornerRadius
- presentationController.prefersGrabberVisible = true
- presentationController.detents = [
- .custom { context in
- min(context.maximumDetentValue, intrisincSize.height)
- },
- .large()
- ]
+ viewController.bottomSheetPreferences = .init(detents: detents, cornerRadius: 24)
+ viewController.bottomSheetPreferences?.setup(viewController: viewController)
}
}
diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift
index cb52281eb8..f5adcc75ac 100644
--- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift
@@ -48,9 +48,10 @@ final class ComposerCreateActionListCoordinator: NSObject, Coordinator, Presenta
view = ComposerCreateActionList(viewModel: viewModel.context)
let hostingVC = VectorHostingController(rootView: view)
hostingVC.bottomSheetPreferences = VectorHostingBottomSheetPreferences(
- detents: [.medium],
+ detents: [.custom(height: 470)],
prefersGrabberVisible: true,
- cornerRadius: 20
+ cornerRadius: 20,
+ prefersScrollingExpandsWhenScrolledToEdge: false
)
hostingController = hostingVC
super.init()
diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift
index 7f2733e2b2..5da2b1d119 100644
--- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift
@@ -34,7 +34,7 @@ struct ComposerCreateActionList: View {
@ObservedObject var viewModel: ComposerCreateActionListViewModel.Context
var body: some View {
- VStack {
+ ScrollView {
VStack(alignment: .leading) {
ForEach(viewModel.viewState.actions) { action in
HStack(spacing: 16) {
@@ -78,9 +78,10 @@ struct ComposerCreateActionList: View {
}
}
- .padding(.top, 8)
Spacer()
- }.background(theme.colors.background.ignoresSafeArea())
+ }
+ .padding(.top, 23)
+ .background(theme.colors.background.ignoresSafeArea())
}
}
diff --git a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift
index c0602ab035..35a628d020 100644
--- a/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/MockComposerScreenState.swift
@@ -32,9 +32,9 @@ enum MockComposerScreenState: MockScreenState, CaseIterable {
let bindings = ComposerBindings(focused: false)
switch self {
- case .send: viewModel = ComposerViewModel(initialViewState: ComposerViewState(textFormattingEnabled: true, bindings: bindings))
- case .edit: viewModel = ComposerViewModel(initialViewState: ComposerViewState(sendMode: .edit, textFormattingEnabled: true, bindings: bindings))
- case .reply: viewModel = ComposerViewModel(initialViewState: ComposerViewState(eventSenderDisplayName: "TestUser", sendMode: .reply, textFormattingEnabled: true, bindings: bindings))
+ case .send: viewModel = ComposerViewModel(initialViewState: ComposerViewState(textFormattingEnabled: true, isLandscapePhone: false, bindings: bindings))
+ case .edit: viewModel = ComposerViewModel(initialViewState: ComposerViewState(sendMode: .edit, textFormattingEnabled: true, isLandscapePhone: false, bindings: bindings))
+ case .reply: viewModel = ComposerViewModel(initialViewState: ComposerViewState(eventSenderDisplayName: "TestUser", sendMode: .reply, textFormattingEnabled: true, isLandscapePhone: false, bindings: bindings))
}
let wysiwygviewModel = WysiwygComposerViewModel(minHeight: 20, maxCompressedHeight: 360)
diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift
index badcd2b20c..84b5f8f955 100644
--- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerModels.swift
@@ -34,8 +34,8 @@ struct FormatItem {
enum FormatType {
case bold
case italic
- case strikethrough
case underline
+ case strikethrough
}
extension FormatType: CaseIterable, Identifiable {
diff --git a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift
index c4293eafcd..66724cdceb 100644
--- a/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/Model/ComposerViewState.swift
@@ -20,6 +20,7 @@ struct ComposerViewState: BindableState {
var eventSenderDisplayName: String?
var sendMode: ComposerSendMode = .send
var textFormattingEnabled: Bool
+ var isLandscapePhone: Bool
var placeholder: String?
var bindings: ComposerBindings
@@ -47,6 +48,10 @@ extension ComposerViewState {
default: return nil
}
}
+
+ var isMinimiseForced: Bool {
+ isLandscapePhone || !textFormattingEnabled
+ }
}
struct ComposerBindings {
diff --git a/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift b/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift
index 073c6f357e..a49314062f 100644
--- a/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/Test/Unit/ComposerViewModelTests.swift
@@ -23,8 +23,13 @@ final class ComposerViewModelTests: XCTestCase {
var context: ComposerViewModel.Context!
override func setUpWithError() throws {
- viewModel = ComposerViewModel(initialViewState: ComposerViewState(textFormattingEnabled: true,
- bindings: ComposerBindings(focused: false)))
+ viewModel = ComposerViewModel(
+ initialViewState: ComposerViewState(
+ textFormattingEnabled: true,
+ isLandscapePhone: false,
+ bindings: ComposerBindings(focused: false)
+ )
+ )
context = viewModel.context
}
diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift
index f255c25a54..87c7cbe7a7 100644
--- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift
@@ -14,7 +14,6 @@
// limitations under the License.
//
-import DSBottomSheet
import SwiftUI
import WysiwygComposer
@@ -22,7 +21,6 @@ struct Composer: View {
// MARK: - Properties
// MARK: Private
-
@ObservedObject private var viewModel: ComposerViewModelType.Context
@ObservedObject private var wysiwygViewModel: WysiwygComposerViewModel
private let resizeAnimationDuration: Double
@@ -36,9 +34,8 @@ struct Composer: View {
private let horizontalPadding: CGFloat = 12
private let borderHeight: CGFloat = 40
- private var minTextViewHeight: CGFloat = 22
private var verticalPadding: CGFloat {
- (borderHeight - minTextViewHeight) / 2
+ (borderHeight - wysiwygViewModel.minHeight) / 2
}
private var topPadding: CGFloat {
@@ -46,7 +43,7 @@ struct Composer: View {
}
private var cornerRadius: CGFloat {
- if viewModel.viewState.shouldDisplayContext || wysiwygViewModel.idealHeight > minTextViewHeight {
+ if viewModel.viewState.shouldDisplayContext || wysiwygViewModel.idealHeight > wysiwygViewModel.minHeight {
return 14
} else {
return borderHeight / 2
@@ -78,7 +75,7 @@ struct Composer: View {
)
}
}
-
+
private var composerContainer: some View {
let rect = RoundedRectangle(cornerRadius: cornerRadius)
return VStack(spacing: 12) {
@@ -119,7 +116,7 @@ struct Composer: View {
wysiwygViewModel.setup()
}
}
- if viewModel.viewState.textFormattingEnabled {
+ if !viewModel.viewState.isMinimiseForced {
Button {
wysiwygViewModel.maximised.toggle()
} label: {
@@ -147,7 +144,7 @@ struct Composer: View {
}
}
}
-
+
private var sendMediaButton: some View {
return Button {
showSendMediaActions()
@@ -162,7 +159,7 @@ struct Composer: View {
.padding(.trailing, 8)
.accessibilityLabel(VectorL10n.create)
}
-
+
private var sendButton: some View {
return Button {
sendMessageAction(wysiwygViewModel.content)
@@ -204,6 +201,12 @@ struct Composer: View {
var body: some View {
VStack(spacing: 8) {
+ if wysiwygViewModel.maximised {
+ RoundedRectangle(cornerRadius: 4)
+ .fill(theme.colors.quinaryContent)
+ .frame(width: 36, height: 5)
+ .padding(.top, 10)
+ }
HStack(alignment: .bottom, spacing: 0) {
if !viewModel.viewState.textFormattingEnabled {
sendMediaButton
@@ -227,6 +230,11 @@ struct Composer: View {
}
.padding(.horizontal, horizontalPadding)
.padding(.bottom, 4)
+ .onChange(of: viewModel.viewState.isMinimiseForced) { newValue in
+ if wysiwygViewModel.maximised && newValue {
+ wysiwygViewModel.maximised = false
+ }
+ }
}
}
diff --git a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift
index 8ad3ebd272..4e64423034 100644
--- a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModel.swift
@@ -63,6 +63,19 @@ final class ComposerViewModel: ComposerViewModelType, ComposerViewModelProtocol
}
}
+ var isLandscapePhone: Bool {
+ get {
+ state.isLandscapePhone
+ }
+ set {
+ state.isLandscapePhone = newValue
+ }
+ }
+
+ var isFocused: Bool {
+ state.bindings.focused
+ }
+
// MARK: - Public
override func process(viewAction: ComposerViewAction) {
@@ -77,4 +90,8 @@ final class ComposerViewModel: ComposerViewModelType, ComposerViewModelProtocol
func dismissKeyboard() {
state.bindings.focused = false
}
+
+ func showKeyboard() {
+ state.bindings.focused = true
+ }
}
diff --git a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift
index a1674ff4d5..0d23be3cc7 100644
--- a/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift
+++ b/RiotSwiftUI/Modules/Room/Composer/ViewModel/ComposerViewModelProtocol.swift
@@ -23,6 +23,9 @@ protocol ComposerViewModelProtocol {
var textFormattingEnabled: Bool { get set }
var eventSenderDisplayName: String? { get set }
var placeholder: String? { get set }
+ var isFocused: Bool { get }
+ var isLandscapePhone: Bool { get set }
func dismissKeyboard()
+ func showKeyboard()
}
diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift
index 1acd907a41..74cfaf65b7 100644
--- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift
+++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift
@@ -60,7 +60,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
}
selectedAnswerIdentifiersSubject
- .debounce(for: 1.0, scheduler: RunLoop.main)
+ .debounce(for: 2.0, scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] identifiers in
guard let self = self else { return }
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift
index d353e2f557..652d6f7b2f 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackCoordinator.swift
@@ -22,7 +22,7 @@ struct VoiceBroadcastPlaybackCoordinatorParameters {
let session: MXSession
let room: MXRoom
let voiceBroadcastStartEvent: MXEvent
- let voiceBroadcastState: VoiceBroadcastInfo.State
+ let voiceBroadcastState: VoiceBroadcastInfoState
let senderDisplayName: String?
}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift
index 29b6252dfc..eaae1b11f1 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/Coordinator/VoiceBroadcastPlaybackProvider.swift
@@ -47,7 +47,7 @@ import Foundation
let parameters = VoiceBroadcastPlaybackCoordinatorParameters(session: session,
room: room,
voiceBroadcastStartEvent: event,
- voiceBroadcastState: VoiceBroadcastInfo.State(rawValue: voiceBroadcastState) ?? VoiceBroadcastInfo.State.stopped,
+ voiceBroadcastState: VoiceBroadcastInfoState(rawValue: voiceBroadcastState) ?? VoiceBroadcastInfoState.stopped,
senderDisplayName: senderDisplayName)
guard let coordinator = try? VoiceBroadcastPlaybackCoordinator(parameters: parameters) else {
return nil
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift
index ff237a320c..0d17cc35fb 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift
@@ -36,10 +36,25 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
private var audioPlayer: VoiceMessageAudioPlayer?
private var displayLink: CADisplayLink!
- private var isLivePlayback = false
- private var acceptProgressUpdates = true
-
+ private var isPlaybackInitialized: Bool = false
+ private var acceptProgressUpdates: Bool = true
private var isActuallyPaused: Bool = false
+ private var isProcessingVoiceBroadcastChunk: Bool = false
+ private var reloadVoiceBroadcastChunkQueue: Bool = false
+ private var seekToChunkTime: TimeInterval?
+
+ private var isPlayingLastChunk: Bool {
+ let chunks = reorderVoiceBroadcastChunks(chunks: Array(voiceBroadcastAggregator.voiceBroadcast.chunks))
+ guard let chunkDuration = chunks.last?.duration else {
+ return false
+ }
+
+ return state.bindings.progress + 1000 >= state.playingState.duration - Float(chunkDuration)
+ }
+
+ private var isLivePlayback: Bool {
+ return (!isPlaybackInitialized || isPlayingLastChunk) && (state.broadcastState == .started || state.broadcastState == .resumed)
+ }
// MARK: Public
@@ -54,9 +69,9 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
self.voiceBroadcastAggregator = voiceBroadcastAggregator
let viewState = VoiceBroadcastPlaybackViewState(details: details,
- broadcastState: VoiceBroadcastPlaybackViewModel.getBroadcastState(from: voiceBroadcastAggregator.voiceBroadcastState),
+ broadcastState: voiceBroadcastAggregator.voiceBroadcastState,
playbackState: .stopped,
- playingState: VoiceBroadcastPlayingState(duration: Float(voiceBroadcastAggregator.voiceBroadcast.duration)),
+ playingState: VoiceBroadcastPlayingState(duration: Float(voiceBroadcastAggregator.voiceBroadcast.duration), isLive: false),
bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0))
super.init(initialViewState: viewState)
@@ -65,6 +80,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
displayLink.add(to: .current, forMode: .common)
self.voiceBroadcastAggregator.delegate = self
+ self.voiceBroadcastAggregator.start()
}
private func release() {
@@ -81,8 +97,6 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
switch viewAction {
case .play:
play()
- case .playLive:
- playLive()
case .pause:
pause()
case .sliderChange(let didChange):
@@ -95,59 +109,22 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
/// Listen voice broadcast
private func play() {
- isLivePlayback = false
displayLink.isPaused = false
isActuallyPaused = false
- if voiceBroadcastAggregator.isStarted == false {
- // Start the streaming by fetching broadcast chunks
- // The audio player will automatically start the playback on incoming chunks
- MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: Start streaming")
- state.playbackState = .buffering
- voiceBroadcastAggregator.start()
-
- updateDuration()
- } else if let audioPlayer = audioPlayer {
+ if let audioPlayer = audioPlayer {
MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: resume")
audioPlayer.play()
} else {
- let chunks = voiceBroadcastAggregator.voiceBroadcast.chunks
- MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: restart from the beginning: \(chunks.count) chunks")
-
- // Reinject all the chunks we already have and play them
- voiceBroadcastChunkQueue.append(contentsOf: chunks)
- processPendingVoiceBroadcastChunks()
- }
- }
-
- private func playLive() {
- guard isLivePlayback == false else {
- MXLog.debug("[VoiceBroadcastPlaybackViewModel] playLive: Already playing live")
- return
- }
-
- isLivePlayback = true
- displayLink.isPaused = false
- isActuallyPaused = false
-
- // Flush the current audio player playlist
- audioPlayer?.removeAllPlayerItems()
-
- if voiceBroadcastAggregator.isStarted == false {
- // Start the streaming by fetching broadcast chunks
- // The audio player will automatically start the playback on incoming chunks
- MXLog.debug("[VoiceBroadcastPlaybackViewModel] playLive: Start streaming")
state.playbackState = .buffering
- voiceBroadcastAggregator.start()
-
- state.playingState.duration = Float(voiceBroadcastAggregator.voiceBroadcast.duration)
- } else {
- let chunks = voiceBroadcastAggregator.voiceBroadcast.chunks
- MXLog.debug("[VoiceBroadcastPlaybackViewModel] playLive: restart from the last chunk: \(chunks.count) chunks")
-
- // Reinject all the chunks we already have and play the last one
- voiceBroadcastChunkQueue.append(contentsOf: chunks)
- processPendingVoiceBroadcastChunksForLivePlayback()
+ if voiceBroadcastAggregator.launchState == .loaded {
+ let chunks = voiceBroadcastAggregator.voiceBroadcast.chunks
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] play: restart from the beginning: \(chunks.count) chunks")
+
+ // Reinject all the chunks we already have and play them
+ voiceBroadcastChunkQueue = Array(chunks)
+ handleVoiceBroadcastChunksProcessing()
+ }
}
}
@@ -155,7 +132,6 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
private func pause() {
MXLog.debug("[VoiceBroadcastPlaybackViewModel] pause")
- isLivePlayback = false
displayLink.isPaused = true
isActuallyPaused = true
@@ -169,9 +145,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
// Check if the broadcast is over before stopping everything
// If not, the player should not stopped. The view state must be move to buffering
- // TODO: Define with more accuracy the threshold to detect the end of the playback
- let remainingTime = state.playingState.duration - state.bindings.progress
- if remainingTime < 500 {
+ if state.broadcastState == .stopped, isPlayingLastChunk {
stop()
} else {
state.playbackState = .buffering
@@ -181,7 +155,6 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
private func stop() {
MXLog.debug("[VoiceBroadcastPlaybackViewModel] stop")
- isLivePlayback = false
displayLink.isPaused = true
// Objects will be released on audioPlayerDidStopPlaying
@@ -192,9 +165,9 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
// MARK: - Voice broadcast chunks playback
/// Start the playback from the beginning or push more chunks to it
- private func processPendingVoiceBroadcastChunks(_ time: TimeInterval? = nil) {
+ private func processPendingVoiceBroadcastChunks() {
reorderPendingVoiceBroadcastChunks()
- processNextVoiceBroadcastChunk(time)
+ processNextVoiceBroadcastChunk()
}
/// Start the playback from the last known chunk
@@ -215,7 +188,7 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
chunks.sorted(by: {$0.sequence < $1.sequence})
}
- private func processNextVoiceBroadcastChunk(_ time: TimeInterval? = nil) {
+ private func processNextVoiceBroadcastChunk() {
MXLog.debug("[VoiceBroadcastPlaybackViewModel] processNextVoiceBroadcastChunk: \(voiceBroadcastChunkQueue.count) chunks remaining")
guard voiceBroadcastChunkQueue.count > 0 else {
@@ -223,10 +196,17 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
return
}
- if (isActuallyPaused == false && state.playbackState == .paused) || state.playbackState == .stopped {
+ if (isActuallyPaused == false && state.playbackState == .paused) {
state.playbackState = .buffering
}
+ guard !isProcessingVoiceBroadcastChunk else {
+ // Chunks caching is already in progress
+ return
+ }
+
+ isProcessingVoiceBroadcastChunk = true
+
// TODO: Control the download rate to avoid to download all chunk in mass
// We could synchronise it with the number of chunks in the player playlist (audioPlayer.playerItems)
@@ -238,9 +218,12 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
return
}
- // TODO: Make sure there has no new incoming chunk that should be before this attachment
- // Be careful that this new chunk is not older than the chunk being played by the audio player. Else
- // we will get an unexecpted rewind.
+ self.isProcessingVoiceBroadcastChunk = false
+ if self.reloadVoiceBroadcastChunkQueue {
+ self.reloadVoiceBroadcastChunkQueue = false
+ self.processNextVoiceBroadcastChunk()
+ return
+ }
switch result {
case .success(let result):
@@ -254,13 +237,20 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
// Append the chunk to the current playlist
audioPlayer.addContentFromURL(result.url)
+ if let time = self.seekToChunkTime {
+ audioPlayer.seekToTime(time)
+ self.seekToChunkTime = nil
+ }
+
// Resume the player. Needed after a buffering
- if audioPlayer.isPlaying == false && self.state.playbackState == .buffering {
- MXLog.debug("[VoiceBroadcastPlaybackViewModel] processNextVoiceBroadcastChunk: Resume the player")
- self.displayLink.isPaused = false
- audioPlayer.play()
- if let time = time {
- audioPlayer.seekToTime(time)
+ if self.state.playbackState == .buffering {
+ if audioPlayer.isPlaying == false {
+ MXLog.debug("[VoiceBroadcastPlaybackViewModel] processNextVoiceBroadcastChunk: Resume the player")
+ self.displayLink.isPaused = false
+ audioPlayer.play()
+ } else {
+ self.state.playbackState = .playing
+ self.state.playingState.isLive = self.isLivePlayback
}
}
} else {
@@ -271,8 +261,9 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
audioPlayer.loadContentFromURL(result.url, displayName: chunk.attachment.originalFileName)
self.displayLink.isPaused = false
audioPlayer.play()
- if let time = time {
+ if let time = self.seekToChunkTime {
audioPlayer.seekToTime(time)
+ self.seekToChunkTime = nil
}
self.audioPlayer = audioPlayer
}
@@ -305,7 +296,9 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
audioPlayer?.pause()
displayLink.isPaused = true
} else {
- // Flush the current audio player playlist
+ // Flush the chunks queue and the current audio player playlist
+ voiceBroadcastChunkQueue = []
+ reloadVoiceBroadcastChunkQueue = isProcessingVoiceBroadcastChunk
audioPlayer?.removeAllPlayerItems()
let chunks = reorderVoiceBroadcastChunks(chunks: Array(voiceBroadcastAggregator.voiceBroadcast.chunks))
@@ -323,7 +316,8 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
MXLog.debug("[VoiceBroadcastPlaybackViewModel] didSliderChanged: restart to time: \(state.bindings.progress) milliseconds")
let time = state.bindings.progress - state.playingState.duration + Float(chunksDuration)
- processPendingVoiceBroadcastChunks(TimeInterval(time / 1000))
+ seekToChunkTime = TimeInterval(time / 1000)
+ processPendingVoiceBroadcastChunks()
}
}
@@ -348,20 +342,14 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic
state.bindings.progress = Float(progress)
}
- private static func getBroadcastState(from state: VoiceBroadcastInfo.State) -> VoiceBroadcastState {
- var broadcastState: VoiceBroadcastState
- switch state {
- case .started:
- broadcastState = VoiceBroadcastState.live
- case .paused:
- broadcastState = VoiceBroadcastState.paused
- case .resumed:
- broadcastState = VoiceBroadcastState.live
- case .stopped:
- broadcastState = VoiceBroadcastState.stopped
+ private func handleVoiceBroadcastChunksProcessing() {
+ // Handle specifically the case where we were waiting data to start playing a live playback
+ if isLivePlayback, state.playbackState == .buffering {
+ // Start the playback on the latest one
+ processPendingVoiceBroadcastChunksForLivePlayback()
+ } else {
+ processPendingVoiceBroadcastChunks()
}
-
- return broadcastState
}
}
@@ -381,17 +369,19 @@ extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate {
voiceBroadcastChunkQueue.append(didReceiveChunk)
}
- func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfo.State) {
- state.broadcastState = VoiceBroadcastPlaybackViewModel.getBroadcastState(from: didReceiveState)
+ func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfoState) {
+ state.broadcastState = didReceiveState
+
+ // Handle the live icon appearance
+ state.playingState.isLive = isLivePlayback
}
func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) {
- if isLivePlayback && state.playbackState == .buffering {
- // We started directly with a live playback but there was no known chunks at that time
- // These are the first chunks we get. Start the playback on the latest one
- processPendingVoiceBroadcastChunksForLivePlayback()
- } else {
- processPendingVoiceBroadcastChunks()
+
+ updateDuration()
+
+ if state.playbackState != .stopped {
+ handleVoiceBroadcastChunksProcessing()
}
}
}
@@ -403,20 +393,20 @@ extension VoiceBroadcastPlaybackViewModel: VoiceMessageAudioPlayerDelegate {
}
func audioPlayerDidStartPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
- if isLivePlayback {
- state.playbackState = .playingLive
- } else {
- state.playbackState = .playing
- }
+ state.playbackState = .playing
+ state.playingState.isLive = isLivePlayback
+ isPlaybackInitialized = true
}
func audioPlayerDidPausePlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
state.playbackState = .paused
+ state.playingState.isLive = false
}
func audioPlayerDidStopPlaying(_ audioPlayer: VoiceMessageAudioPlayer) {
MXLog.debug("[VoiceBroadcastPlaybackViewModel] audioPlayerDidStopPlaying")
state.playbackState = .stopped
+ state.playingState.isLive = false
release()
}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift
index fb2da1ddf4..c06d749765 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift
@@ -32,7 +32,7 @@ struct VoiceBroadcastPlaybackView: View {
@Environment(\.theme) private var theme: ThemeSwiftUI
private var backgroundColor: Color {
- if viewModel.viewState.playbackState == .playingLive {
+ if viewModel.viewState.playingState.isLive {
return theme.colors.alert
}
return theme.colors.quarterlyContent
@@ -70,20 +70,17 @@ struct VoiceBroadcastPlaybackView: View {
}
}.frame(maxWidth: .infinity, alignment: .leading)
- if viewModel.viewState.broadcastState == .live {
- Button { viewModel.send(viewAction: .playLive) } label:
- {
- Label {
- Text(VectorL10n.voiceBroadcastLive)
- .font(theme.fonts.caption1SB)
- .foregroundColor(Color.white)
- } icon: {
- Image(uiImage: Asset.Images.voiceBroadcastLive.image)
- }
+ if viewModel.viewState.broadcastState != .stopped {
+ Label {
+ Text(VectorL10n.voiceBroadcastLive)
+ .font(theme.fonts.caption1SB)
+ .foregroundColor(Color.white)
+ } icon: {
+ Image(uiImage: Asset.Images.voiceBroadcastLive.image)
}
.padding(.horizontal, 5)
.background(RoundedRectangle(cornerRadius: 4, style: .continuous).fill(backgroundColor))
- .accessibilityIdentifier("liveButton")
+ .accessibilityIdentifier("liveLabel")
}
}
.frame(maxWidth: .infinity, alignment: .leading)
@@ -92,22 +89,14 @@ struct VoiceBroadcastPlaybackView: View {
VoiceBroadcastPlaybackErrorView()
} else {
ZStack {
- if viewModel.viewState.playbackState == .playing ||
- viewModel.viewState.playbackState == .playingLive {
+ if viewModel.viewState.playbackState == .playing {
Button { viewModel.send(viewAction: .pause) } label: {
Image(uiImage: Asset.Images.voiceBroadcastPause.image)
.renderingMode(.original)
}
.accessibilityIdentifier("pauseButton")
} else {
- Button {
- if viewModel.viewState.broadcastState == .live &&
- viewModel.viewState.playbackState == .stopped {
- viewModel.send(viewAction: .playLive)
- } else {
- viewModel.send(viewAction: .play)
- }
- } label: {
+ Button { viewModel.send(viewAction: .play) } label: {
Image(uiImage: Asset.Images.voiceBroadcastPlay.image)
.renderingMode(.original)
}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift
index c9133f68eb..18d80d3af1 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift
@@ -19,7 +19,6 @@ import SwiftUI
enum VoiceBroadcastPlaybackViewAction {
case play
- case playLive
case pause
case sliderChange(didChange: Bool)
}
@@ -28,7 +27,6 @@ enum VoiceBroadcastPlaybackState {
case stopped
case buffering
case playing
- case playingLive
case paused
case error
}
@@ -38,21 +36,15 @@ struct VoiceBroadcastPlaybackDetails {
let avatarData: AvatarInputProtocol
}
-enum VoiceBroadcastState {
- case unknown
- case stopped
- case live
- case paused
-}
-
struct VoiceBroadcastPlayingState {
var duration: Float
var durationLabel: String?
+ var isLive: Bool
}
struct VoiceBroadcastPlaybackViewState: BindableState {
var details: VoiceBroadcastPlaybackDetails
- var broadcastState: VoiceBroadcastState
+ var broadcastState: VoiceBroadcastInfoState
var playbackState: VoiceBroadcastPlaybackState
var playingState: VoiceBroadcastPlayingState
var bindings: VoiceBroadcastPlaybackViewStateBindings
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift
index 4159d9aa7a..d88f7dfa8d 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift
@@ -43,7 +43,7 @@ enum MockVoiceBroadcastPlaybackScreenState: MockScreenState, CaseIterable {
var screenView: ([Any], AnyView) {
let details = VoiceBroadcastPlaybackDetails(senderDisplayName: "Alice", avatarData: AvatarInput(mxContentUri: "", matrixItemId: "!fakeroomid:matrix.org", displayName: "The name of the room"))
- let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .live, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: 10.0), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0)))
+ let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .started, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: 10.0, isLive: true), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0)))
return (
[false, viewModel],
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift
index f2e28e5da9..10538095d2 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift
@@ -34,7 +34,13 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
private var chunkFile: AVAudioFile! = nil
private var chunkFrames: AVAudioFrameCount = 0
private var chunkFileNumber: Int = 0
-
+
+ private var currentElapsedTime: UInt = 0 // Time in seconds.
+ private var currentRemainingTime: UInt { // Time in seconds.
+ BuildSettings.voiceBroadcastMaxLength - currentElapsedTime
+ }
+ private var elapsedTimeTimer: Timer?
+
// MARK: Public
weak var serviceDelegate: VoiceBroadcastRecorderServiceDelegate?
@@ -67,12 +73,14 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
}
try audioEngine.start()
+ startTimer()
// Disable the sleep mode during the recording until we are able to handle it
UIApplication.shared.isIdleTimerDisabled = true
} catch {
MXLog.debug("[VoiceBroadcastRecorderService] startRecordingVoiceBroadcast error", context: error)
stopRecordingVoiceBroadcast()
+ invalidateTimer()
}
}
@@ -81,6 +89,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
audioEngine.stop()
audioEngine.inputNode.removeTap(onBus: audioNodeBus)
UIApplication.shared.isIdleTimerDisabled = false
+ invalidateTimer()
voiceBroadcastService?.stopVoiceBroadcast(success: { [weak self] _ in
MXLog.debug("[VoiceBroadcastRecorderService] Stopped")
@@ -110,6 +119,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
func pauseRecordingVoiceBroadcast() {
audioEngine.pause()
UIApplication.shared.isIdleTimerDisabled = false
+ invalidateTimer()
voiceBroadcastService?.pauseVoiceBroadcast(success: { [weak self] _ in
guard let self = self else { return }
@@ -126,6 +136,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
func resumeRecordingVoiceBroadcast() {
try? audioEngine.start()
+ startTimer()
voiceBroadcastService?.resumeVoiceBroadcast(success: { [weak self] _ in
guard let self = self else { return }
@@ -143,12 +154,14 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
private func resetValues() {
chunkFrames = 0
chunkFileNumber = 0
+ currentElapsedTime = 0
}
/// Release the service
private func tearDownVoiceBroadcastService() {
resetValues()
session.tearDownVoiceBroadcastService()
+ invalidateTimer()
do {
try AVAudioSession.sharedInstance().setActive(false)
@@ -157,6 +170,31 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
}
}
+ /// Start ElapsedTimeTimer.
+ private func startTimer() {
+ elapsedTimeTimer = Timer.scheduledTimer(timeInterval: 1.0,
+ target: self,
+ selector: #selector(updateCurrentElapsedTimeValue),
+ userInfo: nil,
+ repeats: true)
+ }
+
+ /// Invalidate ElapsedTimeTimer.
+ private func invalidateTimer() {
+ elapsedTimeTimer?.invalidate()
+ elapsedTimeTimer = nil
+ }
+
+ /// Update currentElapsedTime value.
+ @objc private func updateCurrentElapsedTimeValue() {
+ guard currentRemainingTime > 0 else {
+ stopRecordingVoiceBroadcast()
+ return
+ }
+ currentElapsedTime += 1
+ serviceDelegate?.voiceBroadcastRecorderService(self, didUpdateRemainingTime: self.currentRemainingTime)
+ }
+
/// Write audio buffer to chunk file.
private func writeBuffer(_ buffer: AVAudioPCMBuffer) {
let sampleRate = buffer.format.sampleRate
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift
index 7b97eb83a2..e457eb843c 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift
@@ -18,6 +18,7 @@ import Foundation
protocol VoiceBroadcastRecorderServiceDelegate: AnyObject {
func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateState state: VoiceBroadcastRecorderState)
+ func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateRemainingTime remainingTime: UInt)
}
protocol VoiceBroadcastRecorderServiceProtocol {
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift
index 411ce0333b..6c2c21e3cb 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift
@@ -53,6 +53,14 @@ struct VoiceBroadcastRecorderView: View {
} icon: {
Image(uiImage: Asset.Images.voiceBroadcastTileLive.image)
}
+
+ Label {
+ Text(viewModel.viewState.currentRecordingState.remainingTimeLabel)
+ .foregroundColor(theme.colors.secondaryContent)
+ .font(theme.fonts.caption1)
+ } icon: {
+ Image(uiImage: Asset.Images.voiceBroadcastTimeLeft.image)
+ }
}.frame(maxWidth: .infinity, alignment: .leading)
Label {
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift
index 7a2566aad7..cb807a430c 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift
@@ -35,9 +35,15 @@ struct VoiceBroadcastRecorderDetails {
let avatarData: AvatarInputProtocol
}
+struct VoiceBroadcastRecordingState {
+ var remainingTime: UInt
+ var remainingTimeLabel: String
+}
+
struct VoiceBroadcastRecorderViewState: BindableState {
var details: VoiceBroadcastRecorderDetails
var recordingState: VoiceBroadcastRecorderState
+ var currentRecordingState: VoiceBroadcastRecordingState
var bindings: VoiceBroadcastRecorderViewStateBindings
}
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift
index bc915d36a3..c2b57dc5ce 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift
@@ -32,7 +32,8 @@ enum MockVoiceBroadcastRecorderScreenState: MockScreenState, CaseIterable {
var screenView: ([Any], AnyView) {
let details = VoiceBroadcastRecorderDetails(senderDisplayName: "", avatarData: AvatarInput(mxContentUri: "", matrixItemId: "!fakeroomid:matrix.org", displayName: "The name of the room"))
- let viewModel = MockVoiceBroadcastRecorderViewModel(initialViewState: VoiceBroadcastRecorderViewState(details: details, recordingState: .started, bindings: VoiceBroadcastRecorderViewStateBindings()))
+ let recordingState = VoiceBroadcastRecordingState(remainingTime: BuildSettings.voiceBroadcastMaxLength, remainingTimeLabel: "1h 20m 47s left")
+ let viewModel = MockVoiceBroadcastRecorderViewModel(initialViewState: VoiceBroadcastRecorderViewState(details: details, recordingState: .started, currentRecordingState: recordingState, bindings: VoiceBroadcastRecorderViewStateBindings()))
return (
[false, viewModel],
diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift
index 6e14441620..ba9690bfb1 100644
--- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift
+++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift
@@ -34,8 +34,10 @@ class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, Voic
init(details: VoiceBroadcastRecorderDetails,
recorderService: VoiceBroadcastRecorderServiceProtocol) {
self.voiceBroadcastRecorderService = recorderService
+ let currentRecordingState = VoiceBroadcastRecorderViewModel.currentRecordingState(from: BuildSettings.voiceBroadcastMaxLength)
super.init(initialViewState: VoiceBroadcastRecorderViewState(details: details,
recordingState: .stopped,
+ currentRecordingState: currentRecordingState,
bindings: VoiceBroadcastRecorderViewStateBindings()))
self.voiceBroadcastRecorderService.serviceDelegate = self
@@ -77,10 +79,27 @@ class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, Voic
self.state.recordingState = .resumed
voiceBroadcastRecorderService.resumeRecordingVoiceBroadcast()
}
+
+ private func updateRemainingTime(_ remainingTime: UInt) {
+ state.currentRecordingState = VoiceBroadcastRecorderViewModel.currentRecordingState(from: remainingTime)
+ }
+
+ private static func currentRecordingState(from remainingTime: UInt) -> VoiceBroadcastRecordingState {
+ let time = TimeInterval(Double(remainingTime))
+ let formatter = DateComponentsFormatter()
+ formatter.unitsStyle = .abbreviated
+
+ return VoiceBroadcastRecordingState(remainingTime: remainingTime,
+ remainingTimeLabel: VectorL10n.voiceBroadcastTimeLeft(formatter.string(from: time) ?? "0s"))
+ }
}
extension VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderServiceDelegate {
func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateState state: VoiceBroadcastRecorderState) {
self.state.recordingState = state
}
+
+ func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateRemainingTime remainingTime: UInt) {
+ self.updateRemainingTime(remainingTime)
+ }
}
diff --git a/RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift b/RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift
index d3e7690eaa..bee2862946 100644
--- a/RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift
+++ b/RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift
@@ -79,6 +79,12 @@ struct UserSessionInfo: Identifiable {
case unverified
/// The session has been verified.
case verified
+ /// A session which cannot be never verified due to lack of crypto support
+ case permanentlyUnverified
+
+ var isUnverified: Bool {
+ self == .unverified || self == .permanentlyUnverified
+ }
}
}
diff --git a/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarViewData.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarViewData.swift
index 1fcf65cf13..ce820eedcc 100644
--- a/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarViewData.swift
+++ b/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarViewData.swift
@@ -28,7 +28,7 @@ struct DeviceAvatarViewData: Hashable {
switch verificationState {
case .verified:
return Asset.Images.userSessionVerified.name
- case .unverified:
+ case .unverified, .permanentlyUnverified:
return Asset.Images.userSessionUnverified.name
case .unknown:
return Asset.Images.userSessionVerificationUnknown.name
diff --git a/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardViewData.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardViewData.swift
index 60adcb4c8a..d997567f3b 100644
--- a/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardViewData.swift
+++ b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardViewData.swift
@@ -47,7 +47,7 @@ struct UserSessionCardViewData {
switch verificationState {
case .verified:
return Asset.Images.userSessionVerified.name
- case .unverified:
+ case .unverified, .permanentlyUnverified:
return Asset.Images.userSessionUnverified.name
case .unknown:
return Asset.Images.userSessionVerificationUnknown.name
@@ -59,7 +59,7 @@ struct UserSessionCardViewData {
switch verificationState {
case .verified:
return VectorL10n.userSessionVerified
- case .unverified:
+ case .unverified, .permanentlyUnverified:
return VectorL10n.userSessionUnverified
case .unknown:
return VectorL10n.userSessionVerificationUnknown
@@ -71,7 +71,7 @@ struct UserSessionCardViewData {
switch verificationState {
case .verified:
return \.accent
- case .unverified:
+ case .unverified, .permanentlyUnverified:
return \.alert
case .unknown:
return \.secondaryContent
@@ -85,6 +85,8 @@ struct UserSessionCardViewData {
return isCurrentSessionDisplayMode ? VectorL10n.userSessionVerifiedAdditionalInfo : VectorL10n.userOtherSessionVerifiedAdditionalInfo + " %@"
case .unverified:
return isCurrentSessionDisplayMode ? VectorL10n.userSessionUnverifiedAdditionalInfo : VectorL10n.userOtherSessionUnverifiedAdditionalInfo + " %@"
+ case .permanentlyUnverified:
+ return VectorL10n.userOtherSessionPermanentlyUnverifiedAdditionalInfo
case .unknown:
return VectorL10n.userSessionVerificationUnknownAdditionalInfo
}
diff --git a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift
index 78df04c0b5..76c1be95b3 100644
--- a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift
+++ b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift
@@ -465,7 +465,7 @@ private extension InfoSheetCoordinatorParameters {
private extension UserSessionInfo {
var bottomSheetTitle: String {
switch verificationState {
- case .unverified:
+ case .unverified, .permanentlyUnverified:
return VectorL10n.userSessionUnverifiedSessionTitle
case .verified:
return VectorL10n.userSessionVerifiedSessionTitle
@@ -476,7 +476,7 @@ private extension UserSessionInfo {
var bottomSheetDescription: String {
switch verificationState {
- case .unverified:
+ case .unverified, .permanentlyUnverified:
return VectorL10n.userSessionUnverifiedSessionDescription
case .verified:
return VectorL10n.userSessionVerifiedSessionDescription
diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift
index bce987575c..270891d912 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift
@@ -87,10 +87,15 @@ class UserOtherSessionsViewModelTests: XCTestCase {
func test_whenModelCreated_withUnverifiedFilter_viewStateIsCorrect() {
let sessionInfos = [createUserSessionInfo(sessionId: "session 1"),
- createUserSessionInfo(sessionId: "session 2")]
+ createUserSessionInfo(sessionId: "session 2", verificationState: .permanentlyUnverified),
+ createUserSessionInfo(sessionId: "session 3", verificationState: .unknown)]
let sut = createSUT(sessionInfos: sessionInfos, filter: .unverified)
- let expectedItems = sessionInfos.filter { !$0.isCurrent }.asViewData()
+ let expectedItems = sessionInfos
+ .filter {
+ !$0.isCurrent && $0.verificationState.isUnverified
+ }
+ .asViewData()
let bindings = UserOtherSessionsBindings(filter: .unverified, isEditModeEnabled: false)
let expectedState = UserOtherSessionsViewState(bindings: bindings,
title: "Title",
@@ -100,12 +105,13 @@ class UserOtherSessionsViewModelTests: XCTestCase {
allItemsSelected: false,
enableSignOutButton: false,
showLocationInfo: false)
+ XCTAssertEqual(expectedItems.count, 2)
XCTAssertEqual(sut.state, expectedState)
}
func test_whenModelCreated_withVerifiedFilter_viewStateIsCorrect() {
- let sessionInfos = [createUserSessionInfo(sessionId: "session 1", isVerified: true),
- createUserSessionInfo(sessionId: "session 2", isVerified: true)]
+ let sessionInfos = [createUserSessionInfo(sessionId: "session 1", verificationState: .verified),
+ createUserSessionInfo(sessionId: "session 2", verificationState: .verified)]
let sut = createSUT(sessionInfos: sessionInfos, filter: .verified)
let expectedItems = sessionInfos.filter { !$0.isCurrent }.asViewData()
@@ -122,8 +128,8 @@ class UserOtherSessionsViewModelTests: XCTestCase {
}
func test_whenModelCreated_withVerifiedFilterWithNoVerifiedSessions_viewStateIsCorrect() {
- let sessionInfos = [createUserSessionInfo(sessionId: "session 1", isVerified: false),
- createUserSessionInfo(sessionId: "session 2", isVerified: false)]
+ let sessionInfos = [createUserSessionInfo(sessionId: "session 1"),
+ createUserSessionInfo(sessionId: "session 2")]
let sut = createSUT(sessionInfos: sessionInfos, filter: .verified)
let bindings = UserOtherSessionsBindings(filter: .verified, isEditModeEnabled: false)
let expectedState = UserOtherSessionsViewState(bindings: bindings,
@@ -138,8 +144,8 @@ class UserOtherSessionsViewModelTests: XCTestCase {
}
func test_whenModelCreated_withUnverifiedFilterWithNoUnverifiedSessions_viewStateIsCorrect() {
- let sessionInfos = [createUserSessionInfo(sessionId: "session 1", isVerified: true),
- createUserSessionInfo(sessionId: "session 2", isVerified: true)]
+ let sessionInfos = [createUserSessionInfo(sessionId: "session 1", verificationState: .verified),
+ createUserSessionInfo(sessionId: "session 2", verificationState: .verified)]
let sut = createSUT(sessionInfos: sessionInfos, filter: .unverified)
let bindings = UserOtherSessionsBindings(filter: .unverified, isEditModeEnabled: false)
let expectedState = UserOtherSessionsViewState(bindings: bindings,
@@ -350,13 +356,13 @@ class UserOtherSessionsViewModelTests: XCTestCase {
}
private func createUserSessionInfo(sessionId: String,
- isVerified: Bool = false,
+ verificationState: UserSessionInfo.VerificationState = .unverified,
isActive: Bool = true,
isCurrent: Bool = false) -> UserSessionInfo {
UserSessionInfo(id: sessionId,
name: "iOS",
deviceType: .mobile,
- verificationState: isVerified ? .verified : .unverified,
+ verificationState: verificationState,
lastSeenIP: "10.0.0.10",
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
applicationName: nil,
diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift
index c82532dd21..84ea6f9ad1 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift
@@ -172,7 +172,7 @@ private extension UserOtherSessionsFilter {
case .inactive:
return sessionInfos.filter { !$0.isActive }
case .unverified:
- return sessionInfos.filter { $0.verificationState != .verified }
+ return sessionInfos.filter { $0.verificationState.isUnverified }
case .verified:
return sessionInfos.filter { $0.verificationState == .verified }
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/MockUserSessionOverviewScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/MockUserSessionOverviewScreenState.swift
index 6b7040a2c9..adb01d9ec2 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/MockUserSessionOverviewScreenState.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/MockUserSessionOverviewScreenState.swift
@@ -39,6 +39,7 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable {
.currentSession(sessionState: .verified),
.otherSession(sessionState: .verified),
.otherSession(sessionState: .unverified),
+ .otherSession(sessionState: .permanentlyUnverified),
.sessionWithPushNotifications(enabled: true),
.sessionWithPushNotifications(enabled: false),
.remotelyTogglingPushersNotAvailable]
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift
index af71412477..746fa38ba9 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift
@@ -93,4 +93,9 @@ class UserSessionOverviewUITests: MockScreenTestCase {
let button = app.buttons[buttonId]
XCTAssertTrue(button.exists)
}
+
+ func test_whenPermanentlySessionSelected_copyIsCorrect() {
+ app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.otherSession(sessionState: .permanentlyUnverified).title)
+ XCTAssertTrue(app.buttons[VectorL10n.userOtherSessionPermanentlyUnverifiedAdditionalInfo].exists)
+ }
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProvider.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProvider.swift
index 1028dd3cbf..a5c90d65dd 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProvider.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProvider.swift
@@ -45,7 +45,9 @@ class UserSessionsDataProvider: UserSessionsDataProviderProtocol {
}
func verificationState(for deviceInfo: MXDeviceInfo?) -> UserSessionInfo.VerificationState {
- guard let deviceInfo = deviceInfo else { return .unknown }
+ guard let deviceInfo = deviceInfo else {
+ return .permanentlyUnverified
+ }
guard session.crypto?.crossSigning.canCrossSign == true else {
return deviceInfo.deviceId == session.myDeviceId ? .unverified : .unknown
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift
index 666232a0cc..9869ecd1ba 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift
@@ -117,7 +117,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
private func sessionsOverviewData(from allSessions: [UserSessionInfo],
linkDeviceEnabled: Bool) -> UserSessionsOverviewData {
UserSessionsOverviewData(currentSession: allSessions.filter(\.isCurrent).first,
- unverifiedSessions: allSessions.filter { $0.verificationState == .unverified && !$0.isCurrent },
+ unverifiedSessions: allSessions.filter { $0.verificationState.isUnverified && !$0.isCurrent },
inactiveSessions: allSessions.filter { !$0.isActive },
otherSessions: allSessions.filter { !$0.isCurrent },
linkDeviceEnabled: linkDeviceEnabled)
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift
index 250283410c..227ed5d012 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift
@@ -67,7 +67,7 @@ struct UserSessionListItemViewDataFactory {
switch sessionInfo.verificationState {
case .verified:
sessionStatusText = VectorL10n.userSessionVerifiedShort
- case .unverified:
+ case .unverified, .permanentlyUnverified:
sessionStatusText = VectorL10n.userSessionUnverifiedShort
case .unknown:
sessionStatusText = nil
diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml
index 99c555cc87..dbcf4854a1 100644
--- a/RiotSwiftUI/target.yml
+++ b/RiotSwiftUI/target.yml
@@ -63,6 +63,7 @@ targets:
- path: ../Riot/Modules/Analytics/AnalyticsScreen.swift
- path: ../Riot/Modules/LocationSharing/LocationAuthorizationStatus.swift
- path: ../Riot/Modules/QRCode/QRCodeGenerator.swift
+ - path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfoState.swift
- path: ../Riot/Assets/en.lproj/Untranslated.strings
buildPhase: resources
- path: ../Riot/Assets/Images.xcassets
diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml
index e2db2be617..533efab5f0 100644
--- a/RiotSwiftUI/targetUITests.yml
+++ b/RiotSwiftUI/targetUITests.yml
@@ -72,6 +72,7 @@ targets:
- path: ../Riot/Modules/Analytics/AnalyticsScreen.swift
- path: ../Riot/Modules/LocationSharing/LocationAuthorizationStatus.swift
- path: ../Riot/Modules/QRCode/QRCodeGenerator.swift
+ - path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfoState.swift
- path: ../Riot/Assets/en.lproj/Untranslated.strings
buildPhase: resources
- path: ../Riot/Assets/Images.xcassets
diff --git a/RiotTests/UserSessionsDataProviderTests.swift b/RiotTests/UserSessionsDataProviderTests.swift
index 3780dcd653..4695d9fc31 100644
--- a/RiotTests/UserSessionsDataProviderTests.swift
+++ b/RiotTests/UserSessionsDataProviderTests.swift
@@ -82,6 +82,24 @@ class UserSessionCardViewDataTests: XCTestCase {
XCTAssertEqual(verificationStateVerified, .unverified)
XCTAssertEqual(verificationStateUnverified, .unverified)
}
+
+ func testDeviceNotHavingCryptoSupportOnVerifiedDevice() {
+ let mxSession = MockSession(canCrossSign: true)
+ let dataProvider = UserSessionsDataProvider(session: mxSession)
+
+ let verificationState = dataProvider.verificationState(for: nil)
+
+ XCTAssertEqual(verificationState, .permanentlyUnverified)
+ }
+
+ func testDeviceNotHavingCryptoSupportOnUnverifiedDevice() {
+ let mxSession = MockSession(canCrossSign: false)
+ let dataProvider = UserSessionsDataProvider(session: mxSession)
+
+ let verificationState = dataProvider.verificationState(for: nil)
+
+ XCTAssertEqual(verificationState, .permanentlyUnverified)
+ }
}
// MARK: Mocks
diff --git a/project.yml b/project.yml
index 0fe95db190..6a08562d4d 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: 2469f27b7e1e51aaa135e09f9005eb10fda686e6
+ revision: 1fbffd0321eb47abcd664ad19c6c943b60abf399
DeviceKit:
url: https://github.com/devicekit/DeviceKit
majorVersion: 4.7.0