From 1e8b2f4f268a3c256c7964284a231e710f7a939a Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 29 Nov 2022 19:50:07 +0300 Subject: [PATCH 1/2] Release 1.9.13 (#7123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Renamed sign out to logout * Renamed sign out to logout * Renamed sign out to logout * Sign out of all other sessions * Fix typo in issue automation * Fixed unit tests * Translations update from Weblate (#7017) * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (German) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (German) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com> Co-authored-by: Priit Jõerüüt Co-authored-by: Vri Co-authored-by: Johan Smits * Prepare for new sprint * Prepare for new sprint * Threads: added support to read receipts (MSC3771) - Update after review * Threads: added support to notifications count (MSC3773) * Update RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift Co-authored-by: aringenbach <80891108+aringenbach@users.noreply.github.com> * Update RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift Co-authored-by: aringenbach <80891108+aringenbach@users.noreply.github.com> * Comment fix * the test may fail on CI without blocking the task/check * tests may fail on CI * test improvement * test may fail on CI * Hide push toggles for http pushers when there is no server support * changelog * Code review fixes * Threads: added support to read receipts (MSC3771) - Update after review * Synchronise composer and toolbar resizing animation duration * Add kResizeComposerAnimationDuration constant description * fix for 6946 * Threads: add support to labs flag for read receipts * Cleanup * Code review fixes, created DestructiveButton * Update issue automation Stop using deprecated ProjectNext API in favour of the new ProjectV2 one * Update PR automation Stop using deprecated ProjectNext API in favour of the new ProjectV2 one * Code review fixes * Map location info * Map location info * Add location feature in UserSessionsOverview * Add “show location” feature in other sessions list * Add “show location“ feature in session overview * Fix Package.resolved * Cleanup merge leftovers * Cleanup code * Cleanup * Add show/hide ip persistency * Add location info in UserOtherSessions * Refine settings logic * Mock settings in UserSessionsOverviewViewModel * Add settings service in UserOtherSessionsViewModel * Inject setting service in UserSessionOverviewViewModel * Add changelog.d file * Fix UTs * Cleanup merge leftovers * Add animations * Fix failing test * another possible impl * this solution looks promising * Amend title font * fixed the positioning * Amend copies * trick without using the window but the nav controller * Device Manager: Session list item is not tappable everywhere * changelog * Threads notification count in main timeline including un participated threads * Changed title and body * Removed "Do not ask again" button * Remove indication about plain text mode coming soon * Prevent `Unable to activate constraint with anchors .. because they have no common ancestor.` crashes. Only link toasts to the top safe area instead of the navigation controller * Revert "Replace attributed string height calculation with a more reliable implementation" This reverts commit 47512127ecee2adbffe7b770ceb94534e5ea99ad. * Revert "Fix timeline items text height calculation" This reverts commit 27f4feb8d9831df4489750d989bab8a388b54514. * Revert "Fixes vector-im/element-ios/issues/6441 - Incorrect timeline item text height calculation (#6679)" This reverts commit bd017d1a7722e3ff3e6e3ef83d39bec791e58178. * Fixes vector-im/element-ios/issues/6441 - Incorrect timeline item text height calculation * Prepare for new sprint * fullscreen mode with navigation controller superview * full screen improvements * Refine bottom sheet layout * fix for iPad * fix for iPad * updated pod * changelog * Switch to using an API key for interactions with AppStoreConnect while on CI; update fastlane and dependencies * Rich-text editor: Fix text formatting enabled inconsistent state * Labs: Rich-text editor - Fix text formatting switch losing the current content of the composer * Re-order View computed properties and move to private mark * Add intrinsic sized bottom sheet * Snooze controller * Changelog * pan gesture * Fix composer view model tests * some pr suggestions * Rich-text editor: enable translations between Markdown and HTML when toggling text formatting * improved the pan animation to be as the designs * Force a layout on the room bubble cell messageTextView to get a correct frame * Move Move UserAgentParserTests * Add UserSessionDetailsUITests * Improve UserSessionNameUITests * Cleanup tests * Improve UserSessionNameViewModelTests * Test empty state for UserOtherSessions * Fix typo * Cleanup unused code * Add changelog.d file * Threads: removed "unread_thread_notifications" from sync filters for server that doesn't support MSC3773 * keyboard dismissal issue fixed * this works but we need to expose also the maxCompressed height and the minHeight * code improvements * Remove 10s wait on failed initial sync * updated to the latest version of the swiftpm * swipe animation improvements * Threads: removed "unread_thread_notifications" from sync filters for server that doesn't support MSC3773 - Update after review * Revert "Device Manager: Session list item is not tappable everywhere" This reverts commit 221f4ad1061d908865aa31f5c1766ec4213717e3. * Fixup session list item is not tappable everywhere * improving animations * animation improvement with and without keyboard * Fix accessibility id in UserOtherSessions * fullscreen mode implemented * fixing a bad indentation * Poll not usable after logging out and back in * Changelog * Removed init * voice dictation now works * changelog * plain text * Add voice broadcast slider (#7010) * Display number of unread messages above threads button * Import progress for room keys * Translations update from Weblate (#7080) * Translated using Weblate (Dutch) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ * Translated using Weblate (German) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ * Translated using Weblate (German) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Albanian) Currently translated at 99.6% (2303 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Italian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ * Translated using Weblate (German) Currently translated at 100.0% (2313 of 2313 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2313 of 2313 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2313 of 2313 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ * Translated using Weblate (German) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ * Translated using Weblate (Italian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Albanian) Currently translated at 99.5% (2305 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ * Translated using Weblate (German) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Japanese) Currently translated at 66.2% (1534 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Japanese) Currently translated at 66.2% (1534 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ * Translated using Weblate (German) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (German) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Italian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (French) Currently translated at 97.6% (2272 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/fr/ * Translated using Weblate (Russian) Currently translated at 80.5% (1873 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ru/ * Translated using Weblate (Russian) Currently translated at 80.7% (1879 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ru/ Co-authored-by: Roel ter Maat Co-authored-by: Vri Co-authored-by: Besnik Bleta Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com> Co-authored-by: Ihor Hordiichuk Co-authored-by: Priit Jõerüüt Co-authored-by: Linerly Co-authored-by: Jozef Gaal Co-authored-by: random Co-authored-by: Szimszon Co-authored-by: Suguru Hirahara Co-authored-by: Thibault Martin Co-authored-by: Platon Terekhov * Prepare for new sprint * first type of fix * Fix Weblate conflict * dismiss fullscreen when sending a message * improvements * full screen improvements * improving the dismiss of the keyboard * improved code and stability * Fix verification state algorithm * Add localisation for UserSessionCardViewData * Add tests * Improve unit tests * Add changelog.d file * scrollable bottom sheet, with custom size on iOS 16 * changelog * Add VerificationState.isUnverified * some more documentation on how to use the custom detent * Update Rich text editor library version * Fix a crash when a voice message finishes. * hide button and revert state when rotating on an iPhone, also fixed the positioning of the strikethrough and underline, and the maxCompressed size in landscape mode is always adapted to be visible. * changelog * fix to make the function run on RiotSwiftUI * removed a reference to a constraint that was not needed but only created a constraint issue when restored during the animation for dismissal * Add Voice Broadcast left time countdown * Display sync progress on the loading screen * Allow alpha builds to run, if tagged accordingly, when pushing new commits * suggest PR changes * better naming * Update on VoiceBroadcast currentRecordingState creation * fixing tests * Fix scroll issues with VoiceBroadcastPlayback cells (by fixing SizableBaseRoomCell) * Refactor InfoSheet presentation * Add changelog.d file * Add Towncrier file * Add Towncrier file * Update Voice Broadcast Time left asset * Support live playback * update comments * Fix PR comments * Fix the live icon must be displayed (in grey) if the record is paused - It is hidden currently * Update live indicator * Display the playback duration in the default state * Remove useless check condition * Add VBAggregatorLaunchState in Aggregator * Remove useless error states * Add missing buffering state * Support voice broadcast live playback (#7094) * Fix build after automatic rebase from GH * Add Towncrier file * Fix PR comments * Update from latest PR changes requests * Debounce poll messages more * Add changelog.d file * fix * changelog * better changelog * VoiceBroadcast: Fix chunk processing (#7113) * Translated using Weblate (Dutch) (#7119) Currently translated at 100.0% (49 of 49 strings) Translation: Element iOS/Element iOS (Push) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-push/nl/ Co-authored-by: Roel ter Maat * Translations update from Weblate (#7120) * Translated using Weblate (Dutch) Currently translated at 100.0% (8 of 8 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/nl/ * Translated using Weblate (Albanian) Currently translated at 100.0% (8 of 8 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/sq/ * Translated using Weblate (Russian) Currently translated at 100.0% (8 of 8 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/ru/ * Translated using Weblate (Spanish) Currently translated at 100.0% (8 of 8 strings) Translation: Element iOS/Element iOS (Dialogs) Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios-dialogs/es/ Co-authored-by: Roel ter Maat Co-authored-by: Besnik Bleta Co-authored-by: Platon Terekhov Co-authored-by: iaiz * Translations update from Weblate (#7121) * Translated using Weblate (Dutch) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ * Translated using Weblate (German) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ * Translated using Weblate (German) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Albanian) Currently translated at 99.6% (2303 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Italian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ * Translated using Weblate (German) Currently translated at 100.0% (2313 of 2313 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2313 of 2313 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2313 of 2313 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ * Translated using Weblate (German) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ * Translated using Weblate (Italian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Albanian) Currently translated at 99.5% (2305 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ * Translated using Weblate (German) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Japanese) Currently translated at 66.2% (1534 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Japanese) Currently translated at 66.2% (1534 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ * Translated using Weblate (German) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (German) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Italian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (French) Currently translated at 97.6% (2272 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/fr/ * Translated using Weblate (Russian) Currently translated at 80.5% (1873 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ru/ * Translated using Weblate (Russian) Currently translated at 80.7% (1879 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ru/ * Translated using Weblate (Albanian) Currently translated at 98.9% (2302 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ Co-authored-by: Roel ter Maat Co-authored-by: Vri Co-authored-by: Besnik Bleta Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com> Co-authored-by: Ihor Hordiichuk Co-authored-by: Priit Jõerüüt Co-authored-by: Linerly Co-authored-by: Jozef Gaal Co-authored-by: random Co-authored-by: Szimszon Co-authored-by: Suguru Hirahara Co-authored-by: Thibault Martin Co-authored-by: Platon Terekhov Co-authored-by: Ismail Gulek Co-authored-by: Weblate * Translations update from Weblate (#7122) * Translated using Weblate (Dutch) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ * Translated using Weblate (German) Currently translated at 100.0% (2311 of 2311 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ * Translated using Weblate (German) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Albanian) Currently translated at 99.6% (2303 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Italian) Currently translated at 100.0% (2312 of 2312 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ * Translated using Weblate (German) Currently translated at 100.0% (2313 of 2313 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2313 of 2313 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2313 of 2313 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Dutch) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/ * Translated using Weblate (German) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ * Translated using Weblate (Italian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2315 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Albanian) Currently translated at 99.5% (2305 of 2315 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ * Translated using Weblate (German) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Japanese) Currently translated at 66.2% (1534 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2317 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Japanese) Currently translated at 66.2% (1534 of 2317 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/ * Translated using Weblate (German) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (German) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ * Translated using Weblate (Slovak) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/ * Translated using Weblate (Italian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2326 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/ * Translated using Weblate (French) Currently translated at 97.6% (2272 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/fr/ * Translated using Weblate (Russian) Currently translated at 80.5% (1873 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ru/ * Translated using Weblate (Russian) Currently translated at 80.7% (1879 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ru/ * Translated using Weblate (Albanian) Currently translated at 98.9% (2302 of 2326 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/ Co-authored-by: Roel ter Maat Co-authored-by: Vri Co-authored-by: Besnik Bleta Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com> Co-authored-by: Ihor Hordiichuk Co-authored-by: Priit Jõerüüt Co-authored-by: Linerly Co-authored-by: Jozef Gaal Co-authored-by: random Co-authored-by: Szimszon Co-authored-by: Suguru Hirahara Co-authored-by: Thibault Martin Co-authored-by: Platon Terekhov Co-authored-by: Ismail Gulek Co-authored-by: Weblate * changelog.d: Upgrade MatrixSDK version ([v0.24.5](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.5)). * version++ Co-authored-by: paleksandrs Co-authored-by: Kat Gerasimova Co-authored-by: Anderas Co-authored-by: Element Translate Bot Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com> Co-authored-by: Priit Jõerüüt Co-authored-by: Vri Co-authored-by: Johan Smits Co-authored-by: gulekismail Co-authored-by: Gil Eluard Co-authored-by: Aleksandrs Proskurins Co-authored-by: aringenbach <80891108+aringenbach@users.noreply.github.com> Co-authored-by: Mauro Romito Co-authored-by: Alfonso Grillo Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> Co-authored-by: aringenbach Co-authored-by: Alfonso Grillo Co-authored-by: Stefan Ceriu Co-authored-by: Stefan Ceriu Co-authored-by: Yoan Pintas Co-authored-by: Roel ter Maat Co-authored-by: Besnik Bleta Co-authored-by: Ihor Hordiichuk Co-authored-by: Linerly Co-authored-by: Jozef Gaal Co-authored-by: random Co-authored-by: Szimszon Co-authored-by: Suguru Hirahara Co-authored-by: Thibault Martin Co-authored-by: Platon Terekhov Co-authored-by: Doug Co-authored-by: Philippe Loriaux Co-authored-by: Phl-Pro Co-authored-by: Giom Foret Co-authored-by: iaiz Co-authored-by: Weblate --- .github/workflows/release-alpha.yml | 1 - CHANGES.md | 79 ++++++++ Config/AppVersion.xcconfig | 4 +- Config/BuildSettings.swift | 2 +- Podfile | 2 +- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../Contents.json | 12 ++ .../voice_broadcast_time_left.svg | 3 + Riot/Assets/en.lproj/Vector.strings | 8 + Riot/Assets/es.lproj/InfoPlist.strings | 4 +- Riot/Assets/nl.lproj/InfoPlist.strings | 14 +- Riot/Assets/nl.lproj/Localizable.strings | 10 +- Riot/Assets/ru.lproj/InfoPlist.strings | 6 +- Riot/Assets/sq.lproj/InfoPlist.strings | 6 +- Riot/Assets/sq.lproj/Vector.strings | 2 +- Riot/Generated/Images.swift | 1 + Riot/Generated/Strings.swift | 20 ++ .../Analytics/SentryMonitoringClient.swift | 4 + Riot/Modules/Application/LegacyAppDelegate.m | 12 +- .../AuthenticationCoordinator.swift | 3 +- .../LegacyAuthenticationCoordinator.swift | 3 +- .../VectorHostingBottomSheetPreferences.swift | 21 ++ .../LaunchLoading/LaunchLoadingView.swift | 40 +++- .../LaunchLoading/LaunchLoadingView.xib | 31 ++- .../LaunchLoadingViewController.swift | 4 +- .../MXKRoomInputToolbarView.h | 3 + Riot/Modules/Room/MXKRoomViewController.h | 4 +- Riot/Modules/Room/RoomViewController.h | 8 + Riot/Modules/Room/RoomViewController.m | 13 +- Riot/Modules/Room/RoomViewController.swift | 107 ++++++++++ Riot/Modules/Room/RoomViewController.xib | 8 +- .../SizableCell/SizableBaseRoomCell.swift | 30 ++- .../VoiceBroadcastPlaybackPlainCell.swift | 2 +- .../VoiceBroadcastRecorderPlainCell.swift | 2 +- .../Views/InputToolbar/RoomInputToolbarView.h | 2 + .../WysiwygInputToolbarView.swift | 146 +++++++++++++- .../VoiceMessageAudioPlayer.swift | 3 +- ...SettingsSecureBackupTableViewSection.swift | 39 +++- .../SettingsSecureBackupViewModel.swift | 30 ++- .../SettingsSecureBackupViewState.swift | 2 +- .../Security/SecurityViewController.m | 4 +- .../VoiceBroadcastAggregator.swift | 37 ++-- .../VoiceBroadcastInfo.swift | 19 +- .../VoiceBroadcastInfoState.swift | 22 ++ .../VoiceBroadcastService.swift | 18 +- .../VoiceBroadcastServiceProvider.swift | 14 +- .../Coordinator/InfoSheetCoordinator.swift | 32 ++- .../ComposerCreateActionListCoordinator.swift | 5 +- .../View/ComposerCreateActionList.swift | 7 +- .../Composer/MockComposerScreenState.swift | 6 +- .../Room/Composer/Model/ComposerModels.swift | 2 +- .../Composer/Model/ComposerViewState.swift | 5 + .../Test/Unit/ComposerViewModelTests.swift | 9 +- .../Modules/Room/Composer/View/Composer.swift | 26 ++- .../ViewModel/ComposerViewModel.swift | 17 ++ .../ViewModel/ComposerViewModelProtocol.swift | 3 + .../Coordinator/TimelinePollCoordinator.swift | 2 +- .../VoiceBroadcastPlaybackCoordinator.swift | 2 +- .../VoiceBroadcastPlaybackProvider.swift | 2 +- .../VoiceBroadcastPlaybackViewModel.swift | 190 +++++++++--------- .../View/VoiceBroadcastPlaybackView.swift | 33 +-- .../VoiceBroadcastPlaybackModels.swift | 12 +- .../VoiceBroadcastPlaybackScreenState.swift | 2 +- .../VoiceBroadcastRecorderService.swift | 40 +++- ...oiceBroadcastRecorderServiceProtocol.swift | 1 + .../View/VoiceBroadcastRecorderView.swift | 8 + .../VoiceBroadcastRecorderModels.swift | 6 + .../VoiceBroadcastRecorderScreenState.swift | 3 +- .../VoiceBroadcastRecorderViewModel.swift | 19 ++ .../UserSessions/Common/UserSessionInfo.swift | 6 + .../Common/View/DeviceAvatarViewData.swift | 2 +- .../Common/View/UserSessionCardViewData.swift | 8 +- .../UserSessionsFlowCoordinator.swift | 4 +- .../UserOtherSessionsViewModelTests.swift | 26 ++- .../UserOtherSessionsViewModel.swift | 2 +- .../MockUserSessionOverviewScreenState.swift | 1 + .../Test/UI/UserSessionOverviewUITests.swift | 5 + .../MatrixSDK/UserSessionsDataProvider.swift | 4 +- .../UserSessionsOverviewService.swift | 2 +- .../UserSessionListItemViewDataFactory.swift | 2 +- RiotSwiftUI/target.yml | 1 + RiotSwiftUI/targetUITests.yml | 1 + RiotTests/UserSessionsDataProviderTests.swift | 18 ++ project.yml | 2 +- 84 files changed, 992 insertions(+), 331 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/voice_broadcast_time_left.svg create mode 100644 Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastInfoState.swift diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml index 2c932c52f..f8d03f08b 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 15c1ca2e6..65b409b52 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 ffb7d901a..c5f0aa65d 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 d6b022948..2f85f3c13 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 154c119d4..aaf64e8ac 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 816ccb018..a3f2f6fc9 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 000000000..6dbed5648 --- /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 000000000..82b9eb425 --- /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 fa68764c3..e9295cc9e 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 6b8c19112..da6fdb6c6 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 2ef529d61..f31554ac8 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 0176086d1..eeab4c807 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 c3bb844cb..b1ddc44b6 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 740651215..a4d354d56 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 847de8e42..8d6e3cbfd 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 94352b0be..dcf78a2e1 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 36091cc12..f773146b2 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 d15cf8039..54933a7ab 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 3558a4a4d..7c2975522 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -2394,7 +2394,17 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni { 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 5aa6b3731..8b9898992 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 e8ca770ab..583419075 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 cb5afa51a..d7f94c626 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 f2843db62..55f3aff05 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 c933d1e00..81a6b64f9 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 bd7dba409..1da229b79 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 2e71969ea..d7bf9d8fc 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 dd3bfd205..7c1eaa577 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 af49e8f6e..5b162b58c 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 a0834940a..25170d023 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -187,7 +187,6 @@ static CGSize kThreadListBarButtonItemImageSize; MXTaskProfile *notificationTaskProfile; } -@property (nonatomic, weak) IBOutlet UIView *overlayContainerView; @property (nonatomic, strong) RemoveJitsiWidgetView *removeJitsiWidgetView; @@ -470,6 +469,9 @@ static CGSize kThreadListBarButtonItemImageSize; self.jumpToLastUnreadBanner.backgroundColor = ThemeService.shared.theme.colors.navigation; [self.jumpToLastUnreadBanner vc_removeShadow]; self.resetReadMarkerButton.tintColor = ThemeService.shared.theme.colors.quarterlyContent; + if (self.maximisedToolbarDimmingView) { + self.maximisedToolbarDimmingView.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:0.29]; + } } else { @@ -481,6 +483,9 @@ static CGSize kThreadListBarButtonItemImageSize; radius:8 opacity:0.1]; self.resetReadMarkerButton.tintColor = ThemeService.shared.theme.colors.tertiaryContent; + if (self.maximisedToolbarDimmingView) { + self.maximisedToolbarDimmingView.backgroundColor = [UIColor.blackColor colorWithAlphaComponent:0.12]; + } } self.scrollToBottomBadgeLabel.badgeColor = ThemeService.shared.theme.tintColor; @@ -603,6 +608,8 @@ static CGSize kThreadListBarButtonItemImageSize; // Stop the loading indicator even if the session is still in progress [self stopLoadingUserIndicator]; + + [self setMaximisedToolbarIsHiddenIfNeeded: YES]; } - (void)viewDidAppear:(BOOL)animated @@ -678,6 +685,8 @@ static CGSize kThreadListBarButtonItemImageSize; // Note: We have to wait for viewDidAppear before updating growingTextView (viewWillAppear is too early) self.inputToolbarView.attributedTextMessage = self.roomDataSource.partialAttributedTextMessage; } + + [self setMaximisedToolbarIsHiddenIfNeeded: NO]; } - (void)viewDidDisappear:(BOOL)animated @@ -1212,8 +1221,6 @@ static CGSize kThreadListBarButtonItemImageSize; if (!self.inputToolbarView || ![self.inputToolbarView isMemberOfClass:roomInputToolbarViewClass]) { [super setRoomInputToolbarViewClass:roomInputToolbarViewClass]; - - if ([self.inputToolbarView.class conformsToProtocol:@protocol(RoomInputToolbarViewProtocol)]) { id inputToolbar = (id)self.inputToolbarView; [inputToolbar setVoiceMessageToolbarView:self.voiceMessageController.voiceMessageToolbarView]; diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index 7ac56f0d2..c3857db81 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 073276604..f33d661bd 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 f33762144..b8ba675b2 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 8987cb1de..f673bebee 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 a65254be5..43047cfba 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 4bdea353b..4e351806c 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 e6fad8e09..cee4a9c11 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 231773f2b..4a242a91c 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 ffeed2121..58982bb2a 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 66b720861..5039bef6f 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 1390f56ba..96e698a42 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 d3c7c6c35..cb72f38df 100644 --- a/Riot/Modules/Settings/Security/SecurityViewController.m +++ b/Riot/Modules/Settings/Security/SecurityViewController.m @@ -1218,9 +1218,9 @@ TableViewSectionsDelegate> 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 fb90d834d..31dd0045b 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 b2bc1afe4..5e6218f29 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 000000000..e808ddeb3 --- /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 e6d6171a8..6a3072ec3 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 e39c838b7..83051e1d5 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 6fb7fc110..54a9719b2 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 cb52281eb..f5adcc75a 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 7f2733e2b..5da2b1d11 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 c0602ab03..35a628d02 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 badcd2b20..84b5f8f95 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 c4293eafc..66724cdce 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 073c6f357..a49314062 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 f255c25a5..87c7cbe7a 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 8ad3ebd27..4e6442303 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 a1674ff4d..0d23be3cc 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 1acd907a4..74cfaf65b 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 d353e2f55..652d6f7b2 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 29b6252df..eaae1b11f 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 ff237a320..0d17cc35f 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 fb2da1ddf..c06d74976 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 c9133f68e..18d80d3af 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 4159d9aa7..d88f7dfa8 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 f2e28e5da..10538095d 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 7b97eb83a..e457eb843 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 411ce0333..6c2c21e3c 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 7a2566aad..cb807a430 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 bc915d36a..c2b57dc5c 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 6e1444162..ba9690bfb 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 d3e7690ea..bee286294 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 1fcf65cf1..ce820eedc 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 60adcb4c8..d997567f3 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 78df04c0b..76c1be95b 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 bce987575..270891d91 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 c82532dd2..84ea6f9ad 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 6b7040a2c..adb01d9ec 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 af7141247..746fa38ba 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 1028dd3cb..a5c90d65d 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 666232a0c..9869ecd1b 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 250283410..227ed5d01 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 99c555cc8..dbcf4854a 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 e2db2be61..533efab5f 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 3780dcd65..4695d9fc3 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 0fe95db19..6a08562d4 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 From 5ff23d86c83db8ad319b306d44e717b4347ff2b9 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 29 Nov 2022 19:52:18 +0300 Subject: [PATCH 2/2] finish version++ --- Podfile.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index d9ebb08f4..5244de7b2 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -55,9 +55,9 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixSDK (0.24.3): - - MatrixSDK/Core (= 0.24.3) - - MatrixSDK/Core (0.24.3): + - MatrixSDK (0.24.5): + - MatrixSDK/Core (= 0.24.5) + - MatrixSDK/Core (0.24.5): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) @@ -65,9 +65,9 @@ PODS: - OLMKit (~> 3.2.5) - Realm (= 10.27.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/CryptoSDK (0.24.3): + - MatrixSDK/CryptoSDK (0.24.5): - MatrixSDKCrypto (= 0.1.5) - - MatrixSDK/JingleCallStack (0.24.3): + - MatrixSDK/JingleCallStack (0.24.5): - JitsiMeetSDK (= 5.0.2) - MatrixSDK/Core - MatrixSDKCrypto (0.1.5) @@ -122,8 +122,8 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.24.3) - - MatrixSDK/JingleCallStack (= 0.24.3) + - MatrixSDK (= 0.24.5) + - MatrixSDK/JingleCallStack (= 0.24.5) - OLMKit - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) @@ -220,7 +220,7 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: caa83fd5fa63872295bf868afbd334e0ca67dda2 + MatrixSDK: 1557b3ed0a211db43a865cfdad93f07c2be92c9e MatrixSDKCrypto: dcab554bc7157cad31c01fc1137cf5acb01959a4 OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f @@ -241,6 +241,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 154d5d74ff0efd23cb8c97e9ee2a000103270e09 +PODFILE CHECKSUM: c93b326deaf9de3916d42a49d39d737612ab1d94 COCOAPODS: 1.11.2