diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml index a5cd493289..122543abc3 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-move-labelled.yml @@ -68,7 +68,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc0sUA" + PROJECT_ID: "PVT_kwDOAM0swc0sUA" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} add_product_issues_to_project: @@ -92,7 +92,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc4AAg6N" + PROJECT_ID: "PVT_kwDOAM0swc4AAg6N" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} Delight_issues_to_board: @@ -116,7 +116,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc1HvQ" + PROJECT_ID: "PVT_kwDOAM0swc1HvQ" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_voice-message_issues: @@ -139,7 +139,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc2KCw" + PROJECT_ID: "PVT_kwDOAM0swc2KCw" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_message_bubble_issues: name: A-Message-Bubbles to Message bubble board @@ -161,7 +161,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc3m-g" + PROJECT_ID: "PVT_kwDOAM0swc3m-g" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_FTUE_issues: @@ -184,7 +184,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc4AAqVx" + PROJECT_ID: "PVT_kwDOAM0swc4AAqVx" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} move_WTF_issues: @@ -207,7 +207,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.issue.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc4AArk0" + PROJECT_ID: "PVT_kwDOAM0swc4AArk0" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ps_features1: diff --git a/.github/workflows/triage-review-requests.yml b/.github/workflows/triage-review-requests.yml index 295a18fdf6..0b13eb8733 100644 --- a/.github/workflows/triage-review-requests.yml +++ b/.github/workflows/triage-review-requests.yml @@ -67,7 +67,7 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.pull_request.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc0sUA" + PROJECT_ID: "PVT_kwDOAM0swc0sUA" TEAM: "design" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} @@ -134,6 +134,6 @@ jobs: projectid: ${{ env.PROJECT_ID }} contentid: ${{ github.event.pull_request.node_id }} env: - PROJECT_ID: "PN_kwDOAM0swc4AAg6N" + PROJECT_ID: "PVT_kwDOAM0swc4AAg6N" TEAM: "product" GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} diff --git a/CHANGES.md b/CHANGES.md index 65b409b528..fa9d863d35 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,35 @@ +## Changes in 1.9.14 (2022-12-13) + +🙌 Improvements + +- Add badge for messages in spaces button. ([#7088](https://github.com/vector-im/element-ios/pull/7088)) +- Session: Do not retry initial sync on fatal errors ([#7115](https://github.com/vector-im/element-ios/pull/7115)) +- Labs: VoiceBroadcast: Be able to pause the playback when it is buffering ([#7125](https://github.com/vector-im/element-ios/pull/7125)) +- Rich Text Editor: Design Improvements. ([#7127](https://github.com/vector-im/element-ios/pull/7127)) +- Add localization for authentication errors. ([#7131](https://github.com/vector-im/element-ios/pull/7131)) +- Labs: VoiceBroadcast: Prompt the user before ending a voice broadcast ([#7132](https://github.com/vector-im/element-ios/pull/7132)) +- Update unverifiable sessions copies in the Device Manager. ([#7138](https://github.com/vector-im/element-ios/pull/7138)) +- Refine badge for messages logic on spaces button. ([#7140](https://github.com/vector-im/element-ios/pull/7140)) +- Add message id for to-device events ([#7141](https://github.com/vector-im/element-ios/pull/7141)) +- Upgrade MatrixSDK version ([v0.24.6](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.6)). +- Turn on Threads for all users ([#7156](https://github.com/vector-im/element-ios/issues/7156)) + +🐛 Bugfixes + +- Labs: VoiceBroadcast: Remove the voice broadcast chunks from the attachments list ([#7133](https://github.com/vector-im/element-ios/pull/7133)) +- Labs: VoiceBroadcast: Add the last sequence number in the paused/stopped state event ([#7136](https://github.com/vector-im/element-ios/pull/7136)) +- Fix E2EE set up failure whilst signing in using QR code ([#7142](https://github.com/vector-im/element-ios/pull/7142)) +- Rich Text Editor: Fixed a bug that prevented fullscreen mode to work on iOS 15. ([#7118](https://github.com/vector-im/element-ios/issues/7118)) +- Rich Text Editor: Fixed a bug that did not resize the composer after a change of orientation. ([#7124](https://github.com/vector-im/element-ios/issues/7124)) +- Rich Text Composer: Fix for fullscreen mode breaking sometimes when opening it when keyboard is not showing. ([#7130](https://github.com/vector-im/element-ios/issues/7130)) +- Threads: Use cross-platform consistent naming for threads in labs ([#7147](https://github.com/vector-im/element-ios/issues/7147)) +- Threads: Thread preview doesn't update in main timeline ([#7151](https://github.com/vector-im/element-ios/issues/7151)) + +🧱 Build + +- Update Ruby gems. ([#7148](https://github.com/vector-im/element-ios/pull/7148)) + + ## Changes in 1.9.13 (2022-11-29) ✨ Features @@ -77,6 +109,50 @@ - 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 + +- 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 c5f0aa65da..7cf911f2b9 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.9.13 -CURRENT_PROJECT_VERSION = 1.9.13 +MARKETING_VERSION = 1.9.14 +CURRENT_PROJECT_VERSION = 1.9.14 diff --git a/Gemfile.lock b/Gemfile.lock index 53de8296b9..3af56bda65 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,16 +17,16 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.657.0) - aws-sdk-core (3.166.0) + aws-partitions (1.674.0) + aws-sdk-core (3.168.4) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.59.0) + aws-sdk-kms (1.61.0) aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.117.1) + aws-sdk-s3 (1.117.2) aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) @@ -87,7 +87,7 @@ GEM escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - excon (0.93.1) + excon (0.94.0) faraday (1.10.2) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -117,7 +117,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.210.1) + fastlane (2.211.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -213,14 +213,14 @@ GEM httpclient (2.8.3) i18n (1.12.0) concurrent-ruby (~> 1.0) - jmespath (1.6.1) - json (2.6.2) + jmespath (1.6.2) + json (2.6.3) jwt (2.5.0) memoist (0.16.2) mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) - mini_magick (4.11.0) + mini_magick (4.12.0) mini_mime (1.1.2) mini_portile2 (2.8.0) minitest (5.16.3) @@ -231,14 +231,14 @@ GEM nap (1.1.0) naturally (2.2.1) netrc (0.11.0) - nokogiri (1.13.9) + nokogiri (1.13.10) mini_portile2 (~> 2.8.0) racc (~> 1.4) optparse (0.1.1) os (1.1.4) plist (3.6.0) public_suffix (4.0.7) - racc (1.6.0) + racc (1.6.1) rake (13.0.6) representable (3.2.0) declarative (< 0.1.0) @@ -264,11 +264,11 @@ GEM simctl (1.6.8) CFPropertyList naturally - slather (2.7.2) + slather (2.7.3) CFPropertyList (>= 2.2, < 4) activesupport clamp (~> 1.3) - nokogiri (~> 1.12) + nokogiri (>= 1.13.9) xcodeproj (~> 1.21) terminal-notifier (2.0.0) terminal-table (1.8.0) diff --git a/Podfile b/Podfile index aaf64e8acf..14443360b2 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.5' +$matrixSDKVersion = '= 0.24.6' # $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 a3f2f6fc96..912bdf970b 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" : "1fbffd0321eb47abcd664ad19c6c943b60abf399" + "revision" : "38ad28bedbe63b3587126158245659b6c989ec2c" } }, { diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Contents.json index dbee5479fb..3451f0d6cc 100644 --- a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "action_formatting_disabled.png", + "filename" : "Frame 143.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "action_formatting_disabled@2x.png", + "filename" : "Frame 143@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "action_formatting_disabled@3x.png", + "filename" : "Frame 143@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Frame 143.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Frame 143.png new file mode 100644 index 0000000000..8cc5f79e86 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Frame 143.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Frame 143@2x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Frame 143@2x.png new file mode 100644 index 0000000000..1e4244a85c Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Frame 143@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Frame 143@3x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Frame 143@3x.png new file mode 100644 index 0000000000..88aaee3bac Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/Frame 143@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled.png deleted file mode 100644 index f7ef2b190a..0000000000 Binary files a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@2x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@2x.png deleted file mode 100644 index 270dd75d22..0000000000 Binary files a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@2x.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@3x.png b/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@3x.png deleted file mode 100644 index 9394656ff4..0000000000 Binary files a/Riot/Assets/Images.xcassets/Room/Actions/action_formatting_disabled.imageset/action_formatting_disabled@3x.png and /dev/null differ diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_spinner.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_spinner.imageset/Contents.json new file mode 100644 index 0000000000..f00919cff6 --- /dev/null +++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_spinner.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "voice_broadcast_spinner.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_spinner.imageset/voice_broadcast_spinner.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_spinner.imageset/voice_broadcast_spinner.svg new file mode 100644 index 0000000000..83abf08057 --- /dev/null +++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_spinner.imageset/voice_broadcast_spinner.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index e9295cc9e2..8ae662459c 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -255,6 +255,10 @@ "password_validation_error_contain_number" = "Contain a number."; "password_validation_error_contain_symbol" = "Contain a symbol."; +// MARK: Password policy errors +"password_policy_too_short_pwd_error" = "Too short password"; +"password_policy_weak_pwd_error" = "This password is too weak. It must contain at least 8 characters, with at least one character of each type: uppercase, lowercase, digit and special character."; +"password_policy_pwd_in_dict_error" = "This password has been found in a dictionary, and is not allowed."; // MARK: Legacy Authentication "auth_login" = "Log in"; @@ -790,7 +794,7 @@ Tap the + to start adding people."; "settings_labs_message_reaction" = "React to messages with emoji"; "settings_labs_enable_ringing_for_group_calls" = "Ring for group calls"; "settings_labs_enabled_polls" = "Polls"; -"settings_labs_enable_threads" = "Threaded messaging"; +"settings_labs_enable_threads" = "Threaded messages"; "settings_labs_enable_auto_report_decryption_errors" = "Auto Report Decryption Errors"; "settings_labs_use_only_latest_user_avatar_and_name" = "Show latest avatar and name for users in message history"; "settings_labs_enable_live_location_sharing" = "Live location sharing - share current location (active development, and temporarily, locations persist in room history)"; @@ -2203,6 +2207,10 @@ Tap the + to start adding people."; "voice_broadcast_live" = "Live"; "voice_broadcast_tile" = "Voice broadcast"; "voice_broadcast_time_left" = "%@ left"; +"voice_broadcast_buffering" = "Buffering..."; +"voice_broadcast_stop_alert_title" = "Stop live broadcasting?"; +"voice_broadcast_stop_alert_description" = "Are you sure you want to stop your live broadcast? This will end the broadcast, and the full recording will be available in the room."; +"voice_broadcast_stop_alert_agree_button" = "Yes, stop"; // Mark: - Version check @@ -2451,7 +2459,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_permanently_unverified_additional_info" = "This session doesn’t support encryption and thus can't be verified."; "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."; @@ -2460,6 +2468,7 @@ To enable access, tap Settings> Location and select Always"; "user_session_verified_session_description" = "Verified sessions are anywhere you are using Element after entering your passphrase or confirming your identity with another verified session.\n\nThis means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session."; "user_session_unverified_session_title" = "Unverified session"; "user_session_unverified_session_description" = "Unverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account."; +"user_session_permanently_unverified_session_description" = "This session doesn't support encryption, so it can't be verified.\n\nYou won't be able to participate in rooms where encryption is enabled when using this session.\n\nFor best security and privacy, it is recommended to use Matrix clients that support encryption."; "user_session_inactive_session_title" = "Inactive sessions"; "user_session_inactive_session_description" = "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.\n\nRemoving inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious."; "user_session_rename_session_title" = "Renaming sessions"; diff --git a/Riot/Categories/MXError.swift b/Riot/Categories/MXError.swift new file mode 100644 index 0000000000..2c237b93a3 --- /dev/null +++ b/Riot/Categories/MXError.swift @@ -0,0 +1,60 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import MatrixSDK + +extension MXError { + /// Returns custom localized message from errcode parameter of MXError. + func authenticationErrorMessage() -> String { + let message: String + switch self.errcode { + case kMXErrCodeStringForbidden: + message = VectorL10n.loginErrorForbidden + case kMXErrCodeStringUnknownToken: + message = VectorL10n.loginErrorUnknownToken + case kMXErrCodeStringBadJSON: + message = VectorL10n.loginErrorBadJson + case kMXErrCodeStringNotJSON: + message = VectorL10n.loginErrorBadJson + case kMXErrCodeStringLimitExceeded: + message = VectorL10n.loginErrorLimitExceeded + case kMXErrCodeStringUserInUse: + message = VectorL10n.loginErrorUserInUse + case kMXErrCodeStringLoginEmailURLNotYet: + message = VectorL10n.loginErrorLoginEmailNotYet + case kMXErrCodeStringThreePIDInUse: + message = VectorL10n.authEmailInUse + case kMXErrCodeStringPasswordTooShort: + message = VectorL10n.passwordPolicyTooShortPwdError + case kMXErrCodeStringPasswordNoDigit: + message = VectorL10n.passwordPolicyWeakPwdError + case kMXErrCodeStringPasswordNoLowercase: + message = VectorL10n.passwordPolicyWeakPwdError + case kMXErrCodeStringPasswordNoUppercase: + message = VectorL10n.passwordPolicyWeakPwdError + case kMXErrCodeStringPasswordNoSymbol: + message = VectorL10n.passwordPolicyWeakPwdError + case kMXErrCodeStringWeakPassword: + message = VectorL10n.passwordPolicyWeakPwdError + case kMXErrCodeStringPasswordInDictionary: + message = VectorL10n.passwordPolicyPwdInDictError + default: + message = self.error + } + + return message + } +} diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index dcf78a2e13..11839848cf 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -344,6 +344,7 @@ internal class Asset: NSObject { internal static let voiceBroadcastPlay = ImageAsset(name: "voice_broadcast_play") internal static let voiceBroadcastRecord = ImageAsset(name: "voice_broadcast_record") internal static let voiceBroadcastRecordPause = ImageAsset(name: "voice_broadcast_record_pause") + internal static let voiceBroadcastSpinner = ImageAsset(name: "voice_broadcast_spinner") 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") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index f773146b2d..122ebaae53 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4603,6 +4603,18 @@ public class VectorL10n: NSObject { public static var or: String { return VectorL10n.tr("Vector", "or") } + /// This password has been found in a dictionary, and is not allowed. + public static var passwordPolicyPwdInDictError: String { + return VectorL10n.tr("Vector", "password_policy_pwd_in_dict_error") + } + /// Too short password + public static var passwordPolicyTooShortPwdError: String { + return VectorL10n.tr("Vector", "password_policy_too_short_pwd_error") + } + /// This password is too weak. It must contain at least 8 characters, with at least one character of each type: uppercase, lowercase, digit and special character. + public static var passwordPolicyWeakPwdError: String { + return VectorL10n.tr("Vector", "password_policy_weak_pwd_error") + } /// Contain a lower-case letter. public static var passwordValidationErrorContainLowercaseLetter: String { return VectorL10n.tr("Vector", "password_validation_error_contain_lowercase_letter") @@ -7551,7 +7563,7 @@ public class VectorL10n: NSObject { public static var settingsLabsEnableRingingForGroupCalls: String { return VectorL10n.tr("Vector", "settings_labs_enable_ringing_for_group_calls") } - /// Threaded messaging + /// Threaded messages public static var settingsLabsEnableThreads: String { return VectorL10n.tr("Vector", "settings_labs_enable_threads") } @@ -8711,7 +8723,7 @@ 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. + /// This session doesn’t support encryption and thus can't be verified. public static var userOtherSessionPermanentlyUnverifiedAdditionalInfo: String { return VectorL10n.tr("Vector", "user_other_session_permanently_unverified_additional_info") } @@ -8843,6 +8855,10 @@ public class VectorL10n: NSObject { public static var userSessionOverviewSessionTitle: String { return VectorL10n.tr("Vector", "user_session_overview_session_title") } + /// This session doesn't support encryption, so it can't be verified.\n\nYou won't be able to participate in rooms where encryption is enabled when using this session.\n\nFor best security and privacy, it is recommended to use Matrix clients that support encryption. + public static var userSessionPermanentlyUnverifiedSessionDescription: String { + return VectorL10n.tr("Vector", "user_session_permanently_unverified_session_description") + } /// Push notifications public static var userSessionPushNotifications: String { return VectorL10n.tr("Vector", "user_session_push_notifications") @@ -9139,6 +9155,10 @@ public class VectorL10n: NSObject { public static var voiceBroadcastBlockedBySomeoneElseMessage: String { return VectorL10n.tr("Vector", "voice_broadcast_blocked_by_someone_else_message") } + /// Buffering... + public static var voiceBroadcastBuffering: String { + return VectorL10n.tr("Vector", "voice_broadcast_buffering") + } /// Live public static var voiceBroadcastLive: String { return VectorL10n.tr("Vector", "voice_broadcast_live") @@ -9151,6 +9171,18 @@ public class VectorL10n: NSObject { public static var voiceBroadcastPlaybackLoadingError: String { return VectorL10n.tr("Vector", "voice_broadcast_playback_loading_error") } + /// Yes, stop + public static var voiceBroadcastStopAlertAgreeButton: String { + return VectorL10n.tr("Vector", "voice_broadcast_stop_alert_agree_button") + } + /// Are you sure you want to stop your live broadcast? This will end the broadcast, and the full recording will be available in the room. + public static var voiceBroadcastStopAlertDescription: String { + return VectorL10n.tr("Vector", "voice_broadcast_stop_alert_description") + } + /// Stop live broadcasting? + public static var voiceBroadcastStopAlertTitle: String { + return VectorL10n.tr("Vector", "voice_broadcast_stop_alert_title") + } /// Voice broadcast public static var voiceBroadcastTile: String { return VectorL10n.tr("Vector", "voice_broadcast_tile") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 8e96eb1a76..591f3968a3 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -153,6 +153,10 @@ final class RiotSettings: NSObject { @UserDefault(key: "enableThreads", defaultValue: false, storage: defaults) var enableThreads + /// Indicates if threads should be forced enabled in the timeline. + @UserDefault(key: "forceThreadsEnabled", defaultValue: true, storage: defaults) + var forceThreadsEnabled + /// Indicates if auto reporting of decryption errors is enabled @UserDefault(key: UserDefaultsKeys.enableUISIAutoReporting, defaultValue: BuildSettings.cryptoUISIAutoReportingEnabled, storage: defaults) var enableUISIAutoReporting diff --git a/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift b/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift index e6a02982bf..f8cb8ddaf4 100644 --- a/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift +++ b/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift @@ -175,9 +175,10 @@ extension UISIAutoReportData: Codable { ] contentMap.setObject(content as NSDictionary, forUser: source.senderUserId, andDevice: source.senderDeviceId) session.matrixRestClient.sendDirectToDevice( - eventType: Self.autoRsRequest, - contentMap: contentMap, - txnId: nil + payload: .init( + eventType: Self.autoRsRequest, + contentMap: contentMap + ) ) { response in if response.isFailure { MXLog.warning("failed to send auto-uisi to device") diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 7c2975522f..15cdb8be1c 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -4328,6 +4328,12 @@ - (void)setupUserDefaults { RiotSettings.shared.showAllRoomsInHomeSpace = YES; } + + if (RiotSettings.shared.forceThreadsEnabled) + { + RiotSettings.shared.enableThreads = YES; + RiotSettings.shared.forceThreadsEnabled = NO; + } } #pragma mark - App version management diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index fed2030d9c..4b7ff566c9 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -27,7 +27,6 @@ protocol AllChatsViewControllerDelegate: AnyObject { } class AllChatsViewController: HomeViewController { - // MARK: - Class methods static override func nib() -> UINib! { @@ -72,6 +71,10 @@ class AllChatsViewController: HomeViewController { private var isOnboardingCoordinatorPreparing: Bool = false private var allChatsOnboardingCoordinatorBridgePresenter: AllChatsOnboardingCoordinatorBridgePresenter? + + private var theme: Theme { + ThemeService.shared().theme + } @IBOutlet private var toolbar: UIToolbar! private var isToolbarHidden: Bool = false { @@ -137,12 +140,13 @@ class AllChatsViewController: HomeViewController { searchController.delegate = self NotificationCenter.default.addObserver(self, selector: #selector(self.setupEditOptions), name: AllChatsLayoutSettingsManager.didUpdateSettings, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.updateBadgeButton), name: MXSpaceNotificationCounter.didUpdateNotificationCount, object: nil) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - self.toolbar.tintColor = ThemeService.shared().theme.colors.accent + self.toolbar.tintColor = theme.colors.accent if self.navigationItem.searchController == nil { self.navigationItem.searchController = searchController } @@ -460,7 +464,7 @@ class AllChatsViewController: HomeViewController { return } - self.update(with: ThemeService.shared().theme) + self.update(with: theme) } private func update(with theme: Theme) { @@ -500,22 +504,51 @@ class AllChatsViewController: HomeViewController { self?.updateToolbar(with: menu) })) updateEmptyView() + updateBadgeButton() } private func updateRightNavigationItem(with menu: UIMenu) { self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), menu: menu) } + private lazy var spacesButton: BadgedBarButtonItem = { + let innerButton = UIButton(type: .system) + innerButton.accessibilityLabel = VectorL10n.spaceSelectorTitle + innerButton.addTarget(self, action: #selector(self.showSpaceSelectorAction(sender:)), for: .touchUpInside) + innerButton.setImage(Asset.Images.allChatsSpacesIcon.image, for: .normal) + return BadgedBarButtonItem(withBaseButton: innerButton, theme: theme) + }() + + @objc private func updateBadgeButton() { + guard isViewLoaded, let session = mainSession else { + return + } + + let notificationCount = session.spaceService.missedNotificationsCount + let hasSpaceInvite = session.spaceService.hasSpaceInvite + let isBadgeHighlighed = session.spaceService.hasHighlightNotification || hasSpaceInvite + let badgeValue: String + + switch notificationCount { + case 0: + badgeValue = hasSpaceInvite ? "!" : "0" + case (1 ... Constants.spacesButtonMaxCount): + badgeValue = "\(notificationCount)" + default: + badgeValue = "\(Constants.spacesButtonMaxCount)+" + } + + spacesButton.badgeText = badgeValue + spacesButton.badgeBackgroundColor = isBadgeHighlighed ? theme.noticeColor : theme.noticeSecondaryColor + } + private func updateToolbar(with menu: UIMenu) { guard isViewLoaded else { return } self.isToolbarHidden = false - self.update(with: ThemeService.shared().theme) - - let spacesButton = UIBarButtonItem(image: Asset.Images.allChatsSpacesIcon.image, style: .done, target: self, action: #selector(self.showSpaceSelectorAction(sender: ))) - spacesButton.accessibilityLabel = VectorL10n.spaceSelectorTitle + self.update(with: theme) self.toolbar.items = [ spacesButton, @@ -657,6 +690,12 @@ class AllChatsViewController: HomeViewController { } } +private extension AllChatsViewController { + enum Constants { + static let spacesButtonMaxCount: UInt = 999 + } +} + // MARK: - SpaceSelectorBottomSheetCoordinatorBridgePresenterDelegate extension AllChatsViewController: SpaceSelectorBottomSheetCoordinatorBridgePresenterDelegate { @@ -693,7 +732,6 @@ extension AllChatsViewController: SpaceSelectorBottomSheetCoordinatorBridgePrese // MARK: - UISearchResultsUpdating extension AllChatsViewController: UISearchResultsUpdating { - func updateSearchResults(for searchController: UISearchController) { guard let searchText = searchController.searchBar.text, !searchText.isEmpty else { self.dataSource.search(withPatterns: nil) @@ -702,7 +740,6 @@ extension AllChatsViewController: UISearchResultsUpdating { self.dataSource.search(withPatterns: [searchText]) } - } // MARK: - UISearchControllerDelegate @@ -1076,3 +1113,22 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { self.refreshCurrentSelectedCell(false) } } + +private extension MXSpaceService { + var hasSpaceInvite: Bool { + spaceSummaries.contains(where: { $0.isJoined == false }) + } + + var missedNotificationsCount: UInt { + let notificationState = notificationCounter.homeNotificationState + let groupNotifications = notificationState.groupMissedDiscussionsCount + let directNotifications = notificationState.directMissedDiscussionsCount + + // `notificationState.allCount` returns twice the messages for favourite rooms. Fixing it here. + return groupNotifications + directNotifications + } + + var hasHighlightNotification: Bool { + notificationCounter.homeNotificationState.allHighlightCount > 0 + } +} diff --git a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m index 8b60e2fbe1..e75526a13e 100644 --- a/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m +++ b/Riot/Modules/MatrixKit/Models/Account/MXKAccount.m @@ -890,6 +890,9 @@ -(void)openSessionWithStore:(id)store MXStrongifyAndReturnIfNil(self); self->mxSession = nil; + NSString *myUserId = self.mxSession.myUser.userId; + [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self->sessionStateObserver]; self->sessionStateObserver = nil; @@ -1677,6 +1680,14 @@ - (void)launchInitialServerSync NSString *myUserId = self.mxSession.myUser.userId; [[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil]; } + + // If we cannot resolve this error by retrying, exit early + BOOL isRetryableError = [error.domain isEqualToString:NSURLErrorDomain] || [MXHTTPOperation urlResponseFromError:error] != nil; + if (!isRetryableError) + { + MXLogDebug(@"[MXKAccount] Initial sync will not be retried"); + return; + } // Check if it is a network connectivity issue AFNetworkReachabilityManager *networkReachabilityManager = [AFNetworkReachabilityManager sharedManager]; diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index d315f7afc6..ed649ebf74 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -985,8 +985,19 @@ - (void)refreshEventListeners:(NSArray *)liveEventTypesFilterForMessages else if (nil == localEcho) { // Process here incoming events, and outgoing events sent from another device. - [self queueEventForProcessing:event withRoomState:roomState direction:MXTimelineDirectionForwards]; - [self processQueuedEvents:nil]; + if (self.threadId == nil && event.isInThread) + { + NSInteger index = [self indexOfCellDataWithEventId:event.relatesTo.eventId]; + if (index != NSNotFound) + { + [self reloadNotifying:NO]; + } + } + else + { + [self queueEventForProcessing:event withRoomState:roomState direction:MXTimelineDirectionForwards]; + [self processQueuedEvents:nil]; + } } } }]; @@ -1373,6 +1384,11 @@ - (BOOL)shouldQueueEventForProcessing:(MXEvent*)event roomState:(MXRoomState*)ro // ignore the event return NO; } + + // Ignore voice message related to an actual voice broadcast. + if (event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil) { + return NO; + } } // Check for undecryptable messages that were sent while the user was not in the room and hide them diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 5d76bd5f68..16e05c4231 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -119,7 +119,7 @@ - (void)finalizeInitialization // Sadly, we need to make sure we have fetched all room members from the HS // to be able to display read receipts - if (![self.mxSession.store hasLoadedAllRoomMembersForRoom:self.roomId]) + if (!self.isPeeking && ![self.mxSession.store hasLoadedAllRoomMembersForRoom:self.roomId]) { [self.room members:^(MXRoomMembers *roomMembers) { MXLogDebug(@"[MXKRoomDataSource] finalizeRoomDataSource: All room members have been retrieved"); diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index c3857db81d..9204eeae9c 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -177,7 +177,15 @@ extension RoomViewController { optionalTextView?.becomeFirstResponder() originalRect = wysiwygInputToolbar.convert(wysiwygInputToolbar.frame, to: view) } - wysiwygInputToolbar.showKeyboard() + // This tirggers a SwiftUI update that is handled correctly on iOS 16, but needs to be dispatchted async on older versions + // Dispatching on iOS 16 instead causes some weird SwiftUI update behaviours + if #available(iOS 16, *) { + wysiwygInputToolbar.showKeyboard() + } else { + DispatchQueue.main.async { + wysiwygInputToolbar.showKeyboard() + } + } roomInputToolbarContainer.removeFromSuperview() let dimmingView = UIView() dimmingView.translatesAutoresizingMaskIntoConstraints = false diff --git a/Riot/Modules/Room/TimelineDecorations/Threads/Summary/ThreadSummaryView.swift b/Riot/Modules/Room/TimelineDecorations/Threads/Summary/ThreadSummaryView.swift index 97613e05c0..d7fc1b8899 100644 --- a/Riot/Modules/Room/TimelineDecorations/Threads/Summary/ThreadSummaryView.swift +++ b/Riot/Modules/Room/TimelineDecorations/Threads/Summary/ThreadSummaryView.swift @@ -114,11 +114,18 @@ class ThreadSummaryView: UIView { room.state { [weak self] roomState in guard let self = self else { return } - let formatterError = UnsafeMutablePointer.allocate(capacity: 1) - let lastMessageText = eventFormatter.attributedString(from: lastMessage.replyStrippedVersion, + let lastMessageText: NSAttributedString + + // Check if the last message is in the thread. If not, this means that it has been deleted + if lastMessage.isInThread() { + let formatterError = UnsafeMutablePointer.allocate(capacity: 1) + lastMessageText = eventFormatter.attributedString(from: lastMessage.replyStrippedVersion, with: roomState, andLatestRoomState: nil, error: formatterError) + } else { + lastMessageText = eventFormatter.redactedMessageReplacementAttributedString() + } let model = ThreadSummaryModel(numberOfReplies: thread.numberOfReplies, lastMessageSenderAvatar: avatarViewData, diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index cee4a9c119..5b9c7d4a71 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -165,6 +165,9 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp guard let self = self else { return } self.toolbarViewDelegate?.didChangeMaximisedState(value) self.hostingViewController.view.layer.cornerRadius = value ? 20 : 0 + if !value { + self.voiceMessageBottomConstraint?.constant = 2 + } } ] @@ -215,23 +218,17 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp 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() + if self.isMaximised { + self.voiceMessageBottomConstraint?.constant = keyboardHeight - (window?.safeAreaInsets.bottom ?? 0) + 2 + } else { + self.voiceMessageBottomConstraint?.constant = 2 } } } @objc private func keyboardWillHide(_ notification: Notification) { if self.isMaximised { - UIView.performWithoutAnimation { - self.voiceMessageBottomConstraint?.constant = 4 - self.layoutIfNeeded() - } + self.voiceMessageBottomConstraint?.constant = 2 } } @@ -277,7 +274,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } ) } - + private func registerThemeServiceDidChangeThemeNotification() { NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) } @@ -294,7 +291,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp private func updateTextViewHeight() { let height = UIScreen.main.bounds.height let barOffset: CGFloat = 68 - let toolbarHeight: CGFloat = 96 + let toolbarHeight: CGFloat = sendMode == .send ? 96 : 110 let finalHeight = height - keyboardHeight - toolbarHeight - barOffset wysiwygViewModel.maxExpandedHeight = finalHeight if finalHeight < 200 { @@ -339,9 +336,10 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp set { viewModel.sendMode = ComposerSendMode(from: newValue) updatePlaceholderText() + updateTextViewHeight() } } - + /// Whether text formatting is currently enabled in the composer. var textFormattingEnabled: Bool { get { @@ -361,7 +359,7 @@ 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) + let bottomConstraint = hostingViewController.view.bottomAnchor.constraint(equalTo: voiceMessageToolbarView.bottomAnchor, constant: 2) voiceMessageBottomConstraint = bottomConstraint NSLayoutConstraint.activate( [ diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h index 71781d9276..3c4193e3dc 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.h @@ -34,10 +34,14 @@ NS_ASSUME_NONNULL_BEGIN /// The event id of the started voice broadcast info state event. @property (nonatomic, strong, nullable) NSString* voiceBroadcastId; +/// The voice broadcast last chunk sequence number. +@property (nonatomic) NSInteger lastChunkSequence; + - (instancetype)initWithDeviceId:(NSString *)deviceId state:(NSString *)state chunkLength:(NSInteger)chunkLength - voiceBroadcastId:(NSString *)voiceBroadcastId; + voiceBroadcastId:(NSString *)voiceBroadcastId + lastChunkSequence:(NSInteger)lastChunkSequence; @end diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m index eaaaa9047c..0c26ad1850 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfo.m @@ -23,6 +23,7 @@ - (instancetype)initWithDeviceId:(NSString *)deviceId state:(NSString *)state chunkLength:(NSInteger)chunkLength voiceBroadcastId:(NSString *)voiceBroadcastId + lastChunkSequence:(NSInteger)lastChunkSequence { if (self = [super init]) { @@ -30,6 +31,7 @@ - (instancetype)initWithDeviceId:(NSString *)deviceId _state = state; _chunkLength = chunkLength; _voiceBroadcastId = voiceBroadcastId; + _lastChunkSequence = lastChunkSequence; } return self; @@ -66,8 +68,13 @@ + (id)modelFromJSON:(NSDictionary *)JSONDictionary voiceBroadcastId = relatesTo.eventId; } } + + NSInteger lastChunkSequence = 0; + if (JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkLastSequence]) { + MXJSONModelSetInteger(lastChunkSequence, JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkLastSequence]); + } - return [[VoiceBroadcastInfo alloc] initWithDeviceId:deviceId state:state chunkLength:chunkLength voiceBroadcastId:voiceBroadcastId]; + return [[VoiceBroadcastInfo alloc] initWithDeviceId:deviceId state:state chunkLength:chunkLength voiceBroadcastId:voiceBroadcastId lastChunkSequence:lastChunkSequence]; } - (NSDictionary *)JSONDictionary @@ -86,6 +93,10 @@ - (NSDictionary *)JSONDictionary JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkLength] = @(self.chunkLength); } + if (self.lastChunkSequence != 0) { + JSONDictionary[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkLastSequence] = @(self.lastChunkSequence); + } + return JSONDictionary; } diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift index 6a3072ec3e..b386f0d4d2 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastService.swift @@ -62,9 +62,10 @@ public class VoiceBroadcastService: NSObject { /// Pause a voice broadcast. /// - Parameters: + /// - lastChunkSequence: The last sent chunk number. /// - 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: VoiceBroadcastInfoState.paused, completion: completion) + func pauseVoiceBroadcast(lastChunkSequence: Int, completion: @escaping (MXResponse) -> Void) { + sendVoiceBroadcastInfo(lastChunkSequence: lastChunkSequence, state: VoiceBroadcastInfoState.paused, completion: completion) } /// resume a voice broadcast. @@ -76,9 +77,10 @@ public class VoiceBroadcastService: NSObject { /// stop a voice broadcast info. /// - Parameters: + /// - lastChunkSequence: The last sent chunk number. /// - 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: VoiceBroadcastInfoState.stopped, completion: completion) + func stopVoiceBroadcast(lastChunkSequence: Int, completion: @escaping (MXResponse) -> Void) { + sendVoiceBroadcastInfo(lastChunkSequence: lastChunkSequence, state: VoiceBroadcastInfoState.stopped, completion: completion) } func getState() -> String { @@ -134,7 +136,9 @@ public class VoiceBroadcastService: NSObject { } } - private func sendVoiceBroadcastInfo(state: VoiceBroadcastInfoState, completion: @escaping (MXResponse) -> Void) { + private func sendVoiceBroadcastInfo(lastChunkSequence: Int = 0, + state: VoiceBroadcastInfoState, + completion: @escaping (MXResponse) -> Void) { guard let userId = self.room.mxSession.myUserId else { completion(.failure(VoiceBroadcastServiceError.missingUserId)) return @@ -156,6 +160,8 @@ public class VoiceBroadcastService: NSObject { voiceBroadcastInfo.state = state.rawValue + voiceBroadcastInfo.lastChunkSequence = lastChunkSequence + if state != VoiceBroadcastInfoState.started { guard let voiceBroadcastId = self.voiceBroadcastId else { completion(.failure(VoiceBroadcastServiceError.notStarted)) @@ -211,10 +217,13 @@ extension VoiceBroadcastService { /// Pause a voice broadcast. /// - Parameters: + /// - lastChunkSequence: The last sent chunk number. /// - success: A closure called when the operation is complete. /// - failure: A closure called when the operation fails. - @objc public func pauseVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) { - self.pauseVoiceBroadcast { response in + @objc public func pauseVoiceBroadcast(lastChunkSequence: Int, + success: @escaping (String?) -> Void, + failure: @escaping (Error) -> Void) { + self.pauseVoiceBroadcast(lastChunkSequence: lastChunkSequence) { response in switch response { case .success(let object): success(object) @@ -241,10 +250,13 @@ extension VoiceBroadcastService { /// Stop a voice broadcast. /// - Parameters: + /// - lastChunkSequence: The last sent chunk number. /// - success: A closure called when the operation is complete. /// - failure: A closure called when the operation fails. - @objc public func stopVoiceBroadcast(success: @escaping (String?) -> Void, failure: @escaping (Error) -> Void) { - self.stopVoiceBroadcast { response in + @objc public func stopVoiceBroadcast(lastChunkSequence: Int, + success: @escaping (String?) -> Void, + failure: @escaping (Error) -> Void) { + self.stopVoiceBroadcast(lastChunkSequence: lastChunkSequence) { response in switch response { case .success(let object): success(object) diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastSettings.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastSettings.swift index 425cc03f4c..bafc767529 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastSettings.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastSettings.swift @@ -26,4 +26,5 @@ final class VoiceBroadcastSettings: NSObject { static let voiceBroadcastContentKeyChunkLength = "chunk_length" static let voiceBroadcastContentKeyChunkType = "io.element.voice_broadcast_chunk" static let voiceBroadcastContentKeyChunkSequence = "sequence" + static let voiceBroadcastContentKeyChunkLastSequence = "last_chunk_sequence" } diff --git a/RiotNSE/target.yml b/RiotNSE/target.yml index 22b415154f..c7578dffe4 100644 --- a/RiotNSE/target.yml +++ b/RiotNSE/target.yml @@ -76,3 +76,4 @@ targets: excludes: - "**/*.md" # excludes all files with the .md extension - path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift + - path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastSettings.swift diff --git a/RiotShareExtension/target.yml b/RiotShareExtension/target.yml index a44714f335..666a6e80a7 100644 --- a/RiotShareExtension/target.yml +++ b/RiotShareExtension/target.yml @@ -82,3 +82,4 @@ targets: excludes: - "**/*.md" # excludes all files with the .md extension - path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift + - path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastSettings.swift diff --git a/RiotSwiftUI/Modules/Authentication/ChoosePassword/Coordinator/AuthenticationChoosePasswordCoordinator.swift b/RiotSwiftUI/Modules/Authentication/ChoosePassword/Coordinator/AuthenticationChoosePasswordCoordinator.swift index df3f7d7937..b151f9f039 100644 --- a/RiotSwiftUI/Modules/Authentication/ChoosePassword/Coordinator/AuthenticationChoosePasswordCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/ChoosePassword/Coordinator/AuthenticationChoosePasswordCoordinator.swift @@ -138,7 +138,8 @@ final class AuthenticationChoosePasswordCoordinator: Coordinator, Presentable { if mxError.errcode == kMXErrCodeStringUnauthorized { authenticationChoosePasswordViewModel.displayError(.emailNotVerified) } else { - authenticationChoosePasswordViewModel.displayError(.mxError(mxError.error)) + let message = mxError.authenticationErrorMessage() + authenticationChoosePasswordViewModel.displayError(.mxError(message)) } return diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Coordinator/AuthenticationForgotPasswordCoordinator.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Coordinator/AuthenticationForgotPasswordCoordinator.swift index deea1e8034..0ac3c1b73a 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Coordinator/AuthenticationForgotPasswordCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Coordinator/AuthenticationForgotPasswordCoordinator.swift @@ -164,7 +164,8 @@ final class AuthenticationForgotPasswordCoordinator: Coordinator, Presentable { /// Processes an error to either update the flow or display it to the user. @MainActor private func handleError(_ error: Error) { if let mxError = MXError(nsError: error as NSError) { - authenticationForgotPasswordViewModel.displayError(.mxError(mxError.error)) + let message = mxError.authenticationErrorMessage() + authenticationForgotPasswordViewModel.displayError(.mxError(message)) return } diff --git a/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift b/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift index 2c6a7e3f97..a1b899ad67 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift @@ -180,7 +180,8 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable { /// Processes an error to either update the flow or display it to the user. @MainActor private func handleError(_ error: Error) { if let mxError = MXError(nsError: error as NSError) { - authenticationLoginViewModel.displayError(.mxError(mxError.error)) + let message = mxError.authenticationErrorMessage() + authenticationLoginViewModel.displayError(.mxError(message)) return } diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift index 30059334e0..fa743fd2b0 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift @@ -282,6 +282,18 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { return } + // explicitly download keys for ourself rather than racing with initial sync which might not complete in time + MXLog.debug("[QRLoginService] Downloading device list for self") + await withCheckedContinuation { (continuation: CheckedContinuation) in + session.crypto.downloadKeys([session.myUserId], forceDownload: false) { _, _ in + MXLog.debug("[QRLoginService] Device list downloaded for self") + continuation.resume(returning: ()) + } failure: { _ in + MXLog.error("[QRLoginService] Failed to download the device list for self") + continuation.resume(returning: ()) + } + } + MXLog.debug("[QRLoginService] Wait for cross-signing details") guard case let .success(data) = await rendezvousService.receive(), let responsePayload = try? JSONDecoder().decode(QRLoginRendezvousPayload.self, from: data), diff --git a/RiotSwiftUI/Modules/Authentication/ReCaptcha/Coordinator/AuthenticationReCaptchaCoordinator.swift b/RiotSwiftUI/Modules/Authentication/ReCaptcha/Coordinator/AuthenticationReCaptchaCoordinator.swift index efeafb1844..3f3df2be11 100644 --- a/RiotSwiftUI/Modules/Authentication/ReCaptcha/Coordinator/AuthenticationReCaptchaCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/ReCaptcha/Coordinator/AuthenticationReCaptchaCoordinator.swift @@ -130,7 +130,8 @@ final class AuthenticationReCaptchaCoordinator: Coordinator, Presentable { /// Processes an error to either update the flow or display it to the user. @MainActor private func handleError(_ error: Error) { if let mxError = MXError(nsError: error as NSError) { - authenticationReCaptchaViewModel.displayError(.mxError(mxError.error)) + let message = mxError.authenticationErrorMessage() + authenticationReCaptchaViewModel.displayError(.mxError(message)) return } diff --git a/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift b/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift index 72fa8f4410..6845def008 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift @@ -230,7 +230,8 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable { /// Processes an error to either update the flow or display it to the user. @MainActor private func handleError(_ error: Error) { if let mxError = MXError(nsError: error as NSError) { - authenticationRegistrationViewModel.displayError(.mxError(mxError.error)) + let message = mxError.authenticationErrorMessage() + authenticationRegistrationViewModel.displayError(.mxError(message)) return } diff --git a/RiotSwiftUI/Modules/Authentication/SoftLogout/Coordinator/AuthenticationSoftLogoutCoordinator.swift b/RiotSwiftUI/Modules/Authentication/SoftLogout/Coordinator/AuthenticationSoftLogoutCoordinator.swift index a76ce9a0b5..1d4d294b50 100644 --- a/RiotSwiftUI/Modules/Authentication/SoftLogout/Coordinator/AuthenticationSoftLogoutCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/SoftLogout/Coordinator/AuthenticationSoftLogoutCoordinator.swift @@ -210,7 +210,8 @@ final class AuthenticationSoftLogoutCoordinator: Coordinator, Presentable { /// Processes an error to either update the flow or display it to the user. @MainActor private func handleError(_ error: Error) { if let mxError = MXError(nsError: error as NSError) { - authenticationSoftLogoutViewModel.displayError(.mxError(mxError.error)) + let message = mxError.authenticationErrorMessage() + authenticationSoftLogoutViewModel.displayError(.mxError(message)) return } diff --git a/RiotSwiftUI/Modules/Authentication/Terms/Coordinator/AuthenticationTermsCoordinator.swift b/RiotSwiftUI/Modules/Authentication/Terms/Coordinator/AuthenticationTermsCoordinator.swift index 5baeb2d3f7..d212bd9653 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/Coordinator/AuthenticationTermsCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/Coordinator/AuthenticationTermsCoordinator.swift @@ -149,7 +149,8 @@ final class AuthenticationTermsCoordinator: Coordinator, Presentable { /// Processes an error to either update the flow or display it to the user. @MainActor private func handleError(_ error: Error) { if let mxError = MXError(nsError: error as NSError) { - authenticationTermsViewModel.displayError(.mxError(mxError.error)) + let message = mxError.authenticationErrorMessage() + authenticationTermsViewModel.displayError(.mxError(message)) return } diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Coordinator/AuthenticationVerifyEmailCoordinator.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Coordinator/AuthenticationVerifyEmailCoordinator.swift index 9208840487..30752d8d08 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Coordinator/AuthenticationVerifyEmailCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Coordinator/AuthenticationVerifyEmailCoordinator.swift @@ -190,7 +190,8 @@ final class AuthenticationVerifyEmailCoordinator: Coordinator, Presentable { /// Processes an error to either update the flow or display it to the user. @MainActor private func handleError(_ error: Error) { if let mxError = MXError(nsError: error as NSError) { - authenticationVerifyEmailViewModel.displayError(.mxError(mxError.error)) + let message = mxError.authenticationErrorMessage() + authenticationVerifyEmailViewModel.displayError(.mxError(message)) return } diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Coordinator/AuthenticationVerifyMsisdnCoordinator.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Coordinator/AuthenticationVerifyMsisdnCoordinator.swift index 0d7b42dc45..492eefc2e1 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Coordinator/AuthenticationVerifyMsisdnCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Coordinator/AuthenticationVerifyMsisdnCoordinator.swift @@ -204,7 +204,8 @@ final class AuthenticationVerifyMsisdnCoordinator: Coordinator, Presentable { /// Processes an error to either update the flow or display it to the user. @MainActor private func handleError(_ error: Error) { if let mxError = MXError(nsError: error as NSError) { - authenticationVerifyMsisdnViewModel.displayError(.mxError(mxError.error)) + let message = mxError.authenticationErrorMessage() + authenticationVerifyMsisdnViewModel.displayError(.mxError(message)) return } diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift index f5adcc75ac..eebab63f0f 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Coordinator/ComposerCreateActionListCoordinator.swift @@ -41,14 +41,23 @@ final class ComposerCreateActionListCoordinator: NSObject, Coordinator, Presenta // MARK: - Setup init(actions: [ComposerCreateAction], wysiwygEnabled: Bool, textFormattingEnabled: Bool) { + let isScrollingEnabled: Bool + if #available(iOS 16, *) { + isScrollingEnabled = false + } else { + isScrollingEnabled = true + } viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState( actions: actions, wysiwygEnabled: wysiwygEnabled, + isScrollingEnabled: isScrollingEnabled, bindings: ComposerCreateActionListBindings(textFormattingEnabled: textFormattingEnabled))) view = ComposerCreateActionList(viewModel: viewModel.context) let hostingVC = VectorHostingController(rootView: view) + let height = hostingVC.sizeThatFits(in: CGSize(width: hostingVC.view.frame.width, height: UIView.layoutFittingCompressedSize.height)).height hostingVC.bottomSheetPreferences = VectorHostingBottomSheetPreferences( - detents: [.custom(height: 470)], + // on iOS 15 custom will be replaced by medium which may require some scrolling + detents: [.custom(height: height)], prefersGrabberVisible: true, cornerRadius: 20, prefersScrollingExpandsWhenScrolledToEdge: false @@ -56,6 +65,7 @@ final class ComposerCreateActionListCoordinator: NSObject, Coordinator, Presenta hostingController = hostingVC super.init() hostingVC.presentationController?.delegate = self + hostingVC.bottomSheetPreferences?.setup(viewController: hostingVC) } // MARK: - Public diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/MockComposerCreateActionListScreenState.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/MockComposerCreateActionListScreenState.swift index cb1a53b886..8089ab7ae2 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/MockComposerCreateActionListScreenState.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/MockComposerCreateActionListScreenState.swift @@ -36,6 +36,7 @@ enum MockComposerCreateActionListScreenState: MockScreenState, CaseIterable { let viewModel = ComposerCreateActionListViewModel(initialViewState: ComposerCreateActionListViewState( actions: actions, wysiwygEnabled: true, + isScrollingEnabled: false, bindings: ComposerCreateActionListBindings(textFormattingEnabled: true))) return ( diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift index 6c42041b7c..d1194e1755 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Model/ComposerCreateActionListModels.swift @@ -38,6 +38,7 @@ struct ComposerCreateActionListViewState: BindableState { /// The list of composer create actions to display to the user let actions: [ComposerCreateAction] let wysiwygEnabled: Bool + let isScrollingEnabled: Bool var bindings: ComposerCreateActionListBindings } diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Test/Unit/ComposerCreateActionListTests.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Test/Unit/ComposerCreateActionListTests.swift index 35532a2124..c1e158d9bb 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Test/Unit/ComposerCreateActionListTests.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/Test/Unit/ComposerCreateActionListTests.swift @@ -27,6 +27,7 @@ class ComposerCreateActionListTests: XCTestCase { initialViewState: ComposerCreateActionListViewState( actions: ComposerCreateAction.allCases, wysiwygEnabled: true, + isScrollingEnabled: false, bindings: ComposerCreateActionListBindings(textFormattingEnabled: true) ) ) diff --git a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift index 5da2b1d119..706c2f1d92 100644 --- a/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift +++ b/RiotSwiftUI/Modules/Room/Composer/CreateActionList/View/ComposerCreateActionList.swift @@ -32,56 +32,71 @@ struct ComposerCreateActionList: View { // MARK: Public @ObservedObject var viewModel: ComposerCreateActionListViewModel.Context - - var body: some View { - ScrollView { - VStack(alignment: .leading) { - ForEach(viewModel.viewState.actions) { action in - HStack(spacing: 16) { - Image(action.icon) - .renderingMode(.template) - .foregroundColor(theme.colors.accent) - Text(action.title) - .foregroundColor(theme.colors.primaryContent) - .font(theme.fonts.body) - .accessibilityIdentifier(action.accessibilityIdentifier) - Spacer() - } - .contentShape(Rectangle()) - .onTapGesture { - viewModel.send(viewAction: .selectAction(action)) - } - .padding(.horizontal, 16) - .padding(.vertical, 12) + + private var internalView: some View { + VStack(alignment: .leading) { + ForEach(viewModel.viewState.actions) { action in + HStack(spacing: 16) { + Image(action.icon) + .renderingMode(.template) + .foregroundColor(theme.colors.accent) + Text(action.title) + .foregroundColor(theme.colors.primaryContent) + .font(theme.fonts.body) + .accessibilityIdentifier(action.accessibilityIdentifier) + Spacer() } - if viewModel.viewState.wysiwygEnabled { - SeparatorLine() - HStack(spacing: 16) { - Image(textFormattingIcon) - .renderingMode(.template) - .foregroundColor(theme.colors.accent) - Text(VectorL10n.wysiwygComposerStartActionTextFormatting) - .foregroundColor(theme.colors.primaryContent) - .font(theme.fonts.body) - .accessibilityIdentifier("textFormatting") - Spacer() - Toggle("", isOn: $viewModel.textFormattingEnabled) - .toggleStyle(ComposerToggleActionStyle()) - .labelsHidden() - .onChange(of: viewModel.textFormattingEnabled) { isOn in - viewModel.send(viewAction: .toggleTextFormatting(isOn)) - } - } - .contentShape(Rectangle()) - .padding(.horizontal, 16) - .padding(.vertical, 12) - + .contentShape(Rectangle()) + .onTapGesture { + viewModel.send(viewAction: .selectAction(action)) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + if viewModel.viewState.wysiwygEnabled { + SeparatorLine() + HStack(spacing: 16) { + Image(textFormattingIcon) + .renderingMode(.template) + .foregroundColor(theme.colors.accent) + Text(VectorL10n.wysiwygComposerStartActionTextFormatting) + .foregroundColor(theme.colors.primaryContent) + .font(theme.fonts.body) + .accessibilityIdentifier("textFormatting") + Spacer() + Toggle("", isOn: $viewModel.textFormattingEnabled) + .labelsHidden() + .toggleStyle(SwitchToggleStyle(tint: theme.colors.accent)) + .onChange(of: viewModel.textFormattingEnabled) { isOn in + viewModel.send(viewAction: .toggleTextFormatting(isOn)) + } + } + .contentShape(Rectangle()) + .onTapGesture { + viewModel.textFormattingEnabled.toggle() } + .padding(.horizontal, 16) + .padding(.vertical, 12) + + } + } + } + + var body: some View { + if viewModel.viewState.isScrollingEnabled { + ScrollView { + internalView } - Spacer() + .padding(.top, 23) + .background(theme.colors.background.ignoresSafeArea()) + } else { + VStack { + internalView + Spacer() + } + .padding(.top, 23) + .background(theme.colors.background.ignoresSafeArea()) } - .padding(.top, 23) - .background(theme.colors.background.ignoresSafeArea()) } } @@ -93,35 +108,3 @@ struct ComposerCreateActionList_Previews: PreviewProvider { stateRenderer.screenGroup() } } - -struct ComposerToggleActionStyle: ToggleStyle { - @Environment(\.theme) private var theme - - func makeBody(configuration: Configuration) -> some View { - HStack { - Rectangle() - .foregroundColor(.clear) - .frame(width: 50, height: 30, alignment: .center) - .overlay( - Rectangle() - .foregroundColor(configuration.isOn - ? theme.colors.accent.opacity(0.5) - : theme.colors.primaryContent.opacity(0.25)) - .cornerRadius(7) - .padding(.all, 8) - ) - .overlay( - Circle() - .foregroundColor(configuration.isOn - ? theme.colors.accent - : theme.colors.background) - .padding(.all, 3) - .offset(x: configuration.isOn ? 11 : -11, y: 0) - .shadow(radius: configuration.isOn ? 0.0 : 2.0) - .animation(Animation.linear(duration: 0.1)) - - ).cornerRadius(20) - .onTapGesture { configuration.isOn.toggle() } - } - } -} diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift index 87c7cbe7a7..5d8ec53203 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift @@ -43,13 +43,17 @@ struct Composer: View { } private var cornerRadius: CGFloat { - if viewModel.viewState.shouldDisplayContext || wysiwygViewModel.idealHeight > wysiwygViewModel.minHeight { + if shouldFixRoundCorner { return 14 } else { return borderHeight / 2 } } + private var shouldFixRoundCorner: Bool { + viewModel.viewState.shouldDisplayContext || wysiwygViewModel.idealHeight > wysiwygViewModel.minHeight + } + private var actionButtonAccessibilityIdentifier: String { viewModel.viewState.sendMode == .edit ? "editButton" : "sendButton" } @@ -103,7 +107,7 @@ struct Composer: View { .padding(.top, 8) .padding(.horizontal, horizontalPadding) } - HStack(alignment: .top, spacing: 0) { + HStack(alignment: shouldFixRoundCorner ? .top : .center, spacing: 0) { WysiwygComposerView( focused: $viewModel.focused, viewModel: wysiwygViewModel @@ -210,10 +214,12 @@ struct Composer: View { HStack(alignment: .bottom, spacing: 0) { if !viewModel.viewState.textFormattingEnabled { sendMediaButton + .padding(.bottom, 1) } composerContainer if !viewModel.viewState.textFormattingEnabled { sendButton + .padding(.bottom, 1) } } if viewModel.viewState.textFormattingEnabled { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift index 0d17cc35fb..a07ff8fed4 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift @@ -137,6 +137,9 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic if let audioPlayer = audioPlayer, audioPlayer.isPlaying { audioPlayer.pause() + } else { + state.playbackState = .paused + state.playingState.isLive = false } } @@ -196,10 +199,6 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic return } - if (isActuallyPaused == false && state.playbackState == .paused) { - state.playbackState = .buffering - } - guard !isProcessingVoiceBroadcastChunk else { // Chunks caching is already in progress return @@ -233,41 +232,41 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic self.voiceBroadcastAttachmentCacheManagerLoadResults.append(result) - if let audioPlayer = self.audioPlayer { - // 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 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 { + // Instanciate audioPlayer if needed. + if self.audioPlayer == nil { // Init and start the player on the first chunk let audioPlayer = self.mediaServiceProvider.audioPlayerForIdentifier(result.eventIdentifier) audioPlayer.registerDelegate(self) audioPlayer.loadContentFromURL(result.url, displayName: chunk.attachment.originalFileName) - self.displayLink.isPaused = false - audioPlayer.play() - if let time = self.seekToChunkTime { - audioPlayer.seekToTime(time) - self.seekToChunkTime = nil - } self.audioPlayer = audioPlayer + } else { + // Append the chunk to the current playlist + self.audioPlayer?.addContentFromURL(result.url) + } + + guard let audioPlayer = self.audioPlayer else { + MXLog.error("[VoiceBroadcastPlaybackViewModel] processVoiceBroadcastChunkQueue: audioPlayer is nil !") + return } + // Start or Resume the player. Needed after a buffering + if self.state.playbackState == .buffering { + if audioPlayer.isPlaying == false { + MXLog.debug("[VoiceBroadcastPlaybackViewModel] processNextVoiceBroadcastChunk: Start or Resume the player") + self.displayLink.isPaused = false + audioPlayer.play() + } else { + self.state.playbackState = .playing + self.state.playingState.isLive = self.isLivePlayback + } + } + + if let time = self.seekToChunkTime { + audioPlayer.seekToTime(time) + self.seekToChunkTime = nil + } + case .failure (let error): MXLog.error("[VoiceBroadcastPlaybackViewModel] processVoiceBroadcastChunkQueue: loadAttachment error", context: error) if self.voiceBroadcastChunkQueue.count == 0 { @@ -317,6 +316,10 @@ 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) seekToChunkTime = TimeInterval(time / 1000) + // Check the condition to resume the playback when data will be ready (after the chunk process). + if state.playbackState != .stopped, isActuallyPaused == false { + state.playbackState = .buffering + } processPendingVoiceBroadcastChunks() } } @@ -380,7 +383,7 @@ extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate { updateDuration() - if state.playbackState != .stopped { + if state.playbackState != .stopped, !isActuallyPaused { handleVoiceBroadcastChunksProcessing() } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift index c06d749765..c518f7e598 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift @@ -30,6 +30,7 @@ struct VoiceBroadcastPlaybackView: View { // MARK: Private @Environment(\.theme) private var theme: ThemeSwiftUI + @State private var bufferingSpinnerRotationValue = 0.0 private var backgroundColor: Color { if viewModel.viewState.playingState.isLive { @@ -61,12 +62,33 @@ struct VoiceBroadcastPlaybackView: View { } icon: { Image(uiImage: Asset.Images.voiceBroadcastTileMic.image) } - Label { - Text(VectorL10n.voiceBroadcastTile) - .foregroundColor(theme.colors.secondaryContent) - .font(theme.fonts.caption1) - } icon: { - Image(uiImage: Asset.Images.voiceBroadcastTileLive.image) + if viewModel.viewState.playbackState != .buffering { + Label { + Text(VectorL10n.voiceBroadcastTile) + .foregroundColor(theme.colors.secondaryContent) + .font(theme.fonts.caption1) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastTileLive.image) + } + } else { + Label { + Text(VectorL10n.voiceBroadcastBuffering) + .foregroundColor(theme.colors.secondaryContent) + .font(theme.fonts.caption1) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastSpinner.image) + .frame(width: 16.0, height: 16.0) + .rotationEffect(Angle.degrees(bufferingSpinnerRotationValue)) + .onAppear { + let baseAnimation = Animation.linear(duration: 1.0).repeatForever(autoreverses: false) + withAnimation(baseAnimation) { + bufferingSpinnerRotationValue = 360.0 + } + } + .onDisappear { + bufferingSpinnerRotationValue = 0.0 + } + } } }.frame(maxWidth: .infinity, alignment: .leading) @@ -89,13 +111,13 @@ struct VoiceBroadcastPlaybackView: View { VoiceBroadcastPlaybackErrorView() } else { ZStack { - if viewModel.viewState.playbackState == .playing { + if viewModel.viewState.playbackState == .playing || viewModel.viewState.playbackState == .buffering { Button { viewModel.send(viewAction: .pause) } label: { Image(uiImage: Asset.Images.voiceBroadcastPause.image) .renderingMode(.original) } .accessibilityIdentifier("pauseButton") - } else { + } else { Button { viewModel.send(viewAction: .play) } label: { Image(uiImage: Asset.Images.voiceBroadcastPlay.image) .renderingMode(.original) @@ -104,7 +126,6 @@ struct VoiceBroadcastPlaybackView: View { .accessibilityIdentifier("playButton") } } - .activityIndicator(show: viewModel.viewState.playbackState == .buffering) } Slider(value: $viewModel.progress, in: 0...viewModel.viewState.playingState.duration) { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift index e5e0afe3c6..d0205a9fb5 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Coordinator/VoiceBroadcastRecorderCoordinator.swift @@ -57,7 +57,6 @@ final class VoiceBroadcastRecorderCoordinator: Coordinator, Presentable { func toPresentable() -> UIViewController { let view = VoiceBroadcastRecorderView(viewModel: voiceBroadcastRecorderViewModel.context) - .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) return VectorHostingController(rootView: view) } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift index 10538095d2..5c4eae74ac 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift @@ -91,7 +91,8 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { UIApplication.shared.isIdleTimerDisabled = false invalidateTimer() - voiceBroadcastService?.stopVoiceBroadcast(success: { [weak self] _ in + voiceBroadcastService?.stopVoiceBroadcast(lastChunkSequence: chunkFileNumber, + success: { [weak self] _ in MXLog.debug("[VoiceBroadcastRecorderService] Stopped") guard let self = self else { return } @@ -121,7 +122,8 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { UIApplication.shared.isIdleTimerDisabled = false invalidateTimer() - voiceBroadcastService?.pauseVoiceBroadcast(success: { [weak self] _ in + voiceBroadcastService?.pauseVoiceBroadcast(lastChunkSequence: chunkFileNumber, + success: { [weak self] _ in guard let self = self else { return } // Send current chunk diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift index 6c2c21e3cb..13df1f5ac7 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift @@ -23,6 +23,8 @@ struct VoiceBroadcastRecorderView: View { @Environment(\.theme) private var theme: ThemeSwiftUI + @State private var showingStopAlert = false + private var backgroundColor: Color { if viewModel.viewState.recordingState != .paused { return theme.colors.alert @@ -97,11 +99,20 @@ struct VoiceBroadcastRecorderView: View { .accessibilityIdentifier("recordButton") Button { - viewModel.send(viewAction: .stop) + showingStopAlert = true } label: { Image("voice_broadcast_stop") .renderingMode(.original) } + .alert(isPresented:$showingStopAlert) { + Alert(title: Text(VectorL10n.voiceBroadcastStopAlertTitle), + message: Text(VectorL10n.voiceBroadcastStopAlertDescription), + primaryButton: .cancel(), + secondaryButton: .default(Text(VectorL10n.voiceBroadcastStopAlertAgreeButton), + action: { + viewModel.send(viewAction: .stop) + })) + } .accessibilityIdentifier("stopButton") .disabled(viewModel.viewState.recordingState == .stopped) .mask(Color.black.opacity(viewModel.viewState.recordingState == .stopped ? 0.3 : 1.0)) diff --git a/RiotSwiftUI/Modules/Settings/ChangePassword/Coordinator/ChangePasswordCoordinator.swift b/RiotSwiftUI/Modules/Settings/ChangePassword/Coordinator/ChangePasswordCoordinator.swift index bd3f6fc295..ef6f8a16dd 100644 --- a/RiotSwiftUI/Modules/Settings/ChangePassword/Coordinator/ChangePasswordCoordinator.swift +++ b/RiotSwiftUI/Modules/Settings/ChangePassword/Coordinator/ChangePasswordCoordinator.swift @@ -127,7 +127,8 @@ final class ChangePasswordCoordinator: Coordinator, Presentable { /// Processes an error to either update the flow or display it to the user. @MainActor private func handleError(_ error: Error) { if let mxError = MXError(nsError: error as NSError) { - changePasswordViewModel.displayError(.mxError(mxError.error)) + let message = mxError.authenticationErrorMessage() + changePasswordViewModel.displayError(.mxError(message)) return } diff --git a/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardViewData.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardViewData.swift index d997567f3b..79cd2d8120 100644 --- a/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardViewData.swift +++ b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardViewData.swift @@ -86,7 +86,7 @@ struct UserSessionCardViewData { case .unverified: return isCurrentSessionDisplayMode ? VectorL10n.userSessionUnverifiedAdditionalInfo : VectorL10n.userOtherSessionUnverifiedAdditionalInfo + " %@" case .permanentlyUnverified: - return VectorL10n.userOtherSessionPermanentlyUnverifiedAdditionalInfo + return isCurrentSessionDisplayMode ? VectorL10n.userOtherSessionPermanentlyUnverifiedAdditionalInfo : VectorL10n.userOtherSessionPermanentlyUnverifiedAdditionalInfo + " %@" case .unknown: return VectorL10n.userSessionVerificationUnknownAdditionalInfo } diff --git a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift index 76c1be95b3..79fd9e5738 100644 --- a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift @@ -476,8 +476,10 @@ private extension UserSessionInfo { var bottomSheetDescription: String { switch verificationState { - case .unverified, .permanentlyUnverified: + case .unverified: return VectorL10n.userSessionUnverifiedSessionDescription + case .permanentlyUnverified: + return VectorL10n.userSessionPermanentlyUnverifiedSessionDescription case .verified: return VectorL10n.userSessionVerifiedSessionDescription case .unknown: diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift index 746fa38ba9..643c28cbe9 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift @@ -96,6 +96,7 @@ class UserSessionOverviewUITests: MockScreenTestCase { func test_whenPermanentlySessionSelected_copyIsCorrect() { app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.otherSession(sessionState: .permanentlyUnverified).title) - XCTAssertTrue(app.buttons[VectorL10n.userOtherSessionPermanentlyUnverifiedAdditionalInfo].exists) + let buttonId = "\(VectorL10n.userOtherSessionPermanentlyUnverifiedAdditionalInfo) \(VectorL10n.userSessionLearnMore)" + XCTAssertTrue(app.buttons[buttonId].exists) } } diff --git a/SiriIntents/target.yml b/SiriIntents/target.yml index d20501785d..8bb8034f5b 100644 --- a/SiriIntents/target.yml +++ b/SiriIntents/target.yml @@ -64,3 +64,4 @@ targets: excludes: - "**/*.md" # excludes all files with the .md extension - path: ../Riot/Modules/Room/TimelineCells/Styles/RoomTimelineStyleIdentifier.swift + - path: ../Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastSettings.swift diff --git a/project.yml b/project.yml index 6a08562d4d..dd978f21b5 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: 1fbffd0321eb47abcd664ad19c6c943b60abf399 + revision: 38ad28bedbe63b3587126158245659b6c989ec2c DeviceKit: url: https://github.com/devicekit/DeviceKit majorVersion: 4.7.0