diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 000000000..1873b0a6d --- /dev/null +++ b/.swiftformat @@ -0,0 +1,15 @@ +--swiftversion 5.6 + +--exclude build, CommonKit, Config, DesignKit, fastlane, Pods, Riot, RiotNSE, RiotShareExtension, RiotTests, SiriIntents, Tools, Variants, vendor + +--disable wrapMultiLineStatementBraces +--disable hoistPatternLet + +--commas inline +--ifdef no-indent +--nospaceoperators ...,..< +--stripunusedargs closure-only +--trimwhitespace nonblank-lines +--wrapparameters after-first +--redundanttype inferred +--emptybraces spaced \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index 6d0f6fef5..f7dc561ab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,72 @@ +## Changes in 1.9.8 (2022-10-04) + +🙌 Improvements + +- UserSessions: Extended device information (PSG-772). ([#6766](https://github.com/vector-im/element-ios/pull/6766)) +- Settings: Add labs flags for new session manager (PSG-792, PSG-799). ([#6780](https://github.com/vector-im/element-ios/pull/6780)) +- User session details: Include browser version for web sessions (PSG-761). ([#6788](https://github.com/vector-im/element-ios/pull/6788)) +- Upgrade MatrixSDK version ([v0.24.0](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.0)). +- Include concrete device model in user agent (PSG-754) ([#6742](https://github.com/vector-im/element-ios/issues/6742)) +- CryptoV2: Manual device verification ([#6781](https://github.com/vector-im/element-ios/issues/6781)) +- User sessions: Add support for MSC3881 ([#6787](https://github.com/vector-im/element-ios/issues/6787)) + +🐛 Bugfixes + +- Voiceover: Add labels to User Menu and My Spaces buttons on the All Chats view. ([#6580](https://github.com/vector-im/element-ios/issues/6580)) +- Media: Avoid redundant reloads on media viewer. ([#6717](https://github.com/vector-im/element-ios/issues/6717)) +- Pass country code into MSISDN requests ([#6723](https://github.com/vector-im/element-ios/issues/6723)) +- Fix render of links with both characters requiring percent encoding and markdown-like syntax ([#6748](https://github.com/vector-im/element-ios/issues/6748)) +- Fix crash when scrolling chat list ([#6749](https://github.com/vector-im/element-ios/issues/6749)) +- App Layout: Unable to send message after filtering for room ([#6755](https://github.com/vector-im/element-ios/issues/6755)) +- App Layout: Fix missing toolbar for users with no rooms. ([#6796](https://github.com/vector-im/element-ios/pull/6796)) +- Fix code block background colour ([#6778](https://github.com/vector-im/element-ios/issues/6778)) +- Fix navigation bars visibility on iOS 16. ([#6799](https://github.com/vector-im/element-ios/pull/6799)) + +🧱 Build + +- Fix crash when scrolling chat list ([#6757](https://github.com/vector-im/element-ios/pull/6757)) + +🚧 In development 🚧 + +- Device manager: User session details screen. ([#6693](https://github.com/vector-im/element-ios/issues/6693)) +- Device manager: Session overview screen. ([#6753](https://github.com/vector-im/element-ios/issues/6753)) +- Device Manager: Link current session to session overview. ([#6770](https://github.com/vector-im/element-ios/issues/6770)) + + +## Changes in 1.9.7 (2022-09-28) + +🙌 Improvements + +- Upgrade MatrixSDK version ([v0.23.19](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.23.19)). + +🐛 Bugfixes + +- Missing decoration for events decrypted with untrusted Megolm sessions ([Security advisory](https://github.com/vector-im/element-ios/security/advisories/GHSA-fm8m-99j7-323g)) +- Fix crash when scrolling chat list ([#6749](https://github.com/vector-im/element-ios/issues/6749)) + + +## Changes in 1.9.6 (2022-09-20) + +🙌 Improvements + +- Sentry: Disable unnecessary network breadcrumbs ([#6726](https://github.com/vector-im/element-ios/pull/6726)) + +🐛 Bugfixes + +- Fix crash when some opus audio files are added to a room. ([#6584](https://github.com/vector-im/element-ios/issues/6584)) +- Fixed failed to join room (was not legal room) ([#6653](https://github.com/vector-im/element-ios/issues/6653)) +- Fix crash presenting Sign Out or Invite to Element menu items on iPad. ([#6654](https://github.com/vector-im/element-ios/issues/6654)) +- Fix crash on logout when syncing is currently in progress ([#6705](https://github.com/vector-im/element-ios/issues/6705)) +- New layout: Fixed Low priority rooms titles obscured by bottom bar when side scrolling ([#6707](https://github.com/vector-im/element-ios/issues/6707)) +- Message Composer: Stop the keyboard jumping after sending a message on certain devices. ([#6708](https://github.com/vector-im/element-ios/issues/6708)) +- New App Layout: Make sure the green loading spinner is dismissed after clearing the cache. ([#6709](https://github.com/vector-im/element-ios/issues/6709)) +- Fix a crash when previewing a room. ([#6712](https://github.com/vector-im/element-ios/issues/6712)) +- Fix login crash on Xcode 14 builds ([#6722](https://github.com/vector-im/element-ios/issues/6722)) +- Fix replied event content parsing for nested replies ([#6729](https://github.com/vector-im/element-ios/issues/6729)) +- Room: Fix a composer crash after long unsent messages. ([#6734](https://github.com/vector-im/element-ios/issues/6734)) +- New App Layout: fixed rooms list doesn't update after log out and log into another account ([#6739](https://github.com/vector-im/element-ios/issues/6739)) + + ## Changes in 1.9.5 (2022-09-12) 🐛 Bugfixes diff --git a/Config/CommonConfiguration.swift b/Config/CommonConfiguration.swift index 0000d536c..b796b5955 100644 --- a/Config/CommonConfiguration.swift +++ b/Config/CommonConfiguration.swift @@ -18,6 +18,10 @@ import Foundation import MatrixSDK +#if !os(OSX) +import DeviceKit +#endif + /// CommonConfiguration is the central point to setup settings for MatrixSDK, MatrixKit and common configurations for all targets. class CommonConfiguration: NSObject, Configurable { @@ -67,9 +71,6 @@ class CommonConfiguration: NSObject, Configurable { // Disable identicon use sdkOptions.disableIdenticonUseForUserAvatar = true - // Use UIKit BackgroundTask for handling background tasks in the SDK - sdkOptions.backgroundModeHandler = MXUIKitBackgroundModeHandler() - // Pass httpAdditionalHeaders to the SDK sdkOptions.httpAdditionalHeaders = UserAgentService().bwiUserAgentDict @@ -85,7 +86,64 @@ class CommonConfiguration: NSObject, Configurable { // Configure key provider delegate MXKeyProvider.sharedInstance().delegate = EncryptionKeyManager.shared + EncryptionKeyManager.shared.initKeys() + + sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature + } + + private func makeASCIIUserAgent() -> String? { + guard var userAgent = makeUserAgent() else { + return nil + } + if !userAgent.canBeConverted(to: .ascii) { + let mutableUserAgent = NSMutableString(string: userAgent) + if CFStringTransform(mutableUserAgent, nil, "Any-Latin; Latin-ASCII; [:^ASCII:] Remove" as CFString, false) { + userAgent = mutableUserAgent as String + } + } + return userAgent + } + + private func makeUserAgent() -> String? { + let appInfo = AppInfo.current + let clientName = appInfo.displayName + let clientVersion = appInfo.appVersion?.bundleShortVersion ?? "unknown" + + #if os(iOS) + return String( + format: "%@/%@ (%@; iOS %@; Scale/%0.2f)", + clientName, + clientVersion, + Device.current.safeDescription, + UIDevice.current.systemVersion, + UIScreen.main.scale) + #elseif os(tvOS) + return String( + format: "%@/%@ (%@; tvOS %@; Scale/%0.2f)", + clientName, + clientVersion, + Device.current.safeDescription, + UIDevice.current.systemVersion, + UIScreen.main.scale) + #elseif os(watchOS) + return String( + format: "%@/%@ (%@; watchOS %@; Scale/%0.2f)", + clientName, + clientVersion, + Device.current.safeDescription, + WKInterfaceDevice.current.systemVersion, + WKInterfaceDevice.currentDevice.screenScale) + #elseif os(OSX) + return String( + format: "%@/%@ (Mac; Mac OS X %@)", + clientName, + clientVersion, + NSProcessInfo.processInfo.operatingSystemVersionString) + #else + return nil + #endif +>>>>>>> 7f488705214bbb905bb50f2485ac3aa880c9e8b7 } diff --git a/Podfile b/Podfile index e8f5572fb..ff67692e1 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.23.18' +$matrixSDKVersion = '= 0.24.0' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } diff --git a/Podfile.lock b/Podfile.lock index dbf43b3fb..0f0faf7a8 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -38,7 +38,6 @@ PODS: - DTFoundation/Core - DTFoundation/UIKit (1.7.18): - DTFoundation/Core - - DTTJailbreakDetection (0.4.0) - ffmpeg-kit-ios-audio (4.5.1) - FLEX (4.5.0) - FlowCommoniOS (1.12.2) @@ -57,12 +56,9 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatomoTracker (7.4.1): - - MatomoTracker/Core (= 7.4.1) - - MatomoTracker/Core (7.4.1) - - MatrixSDK (0.23.18): - - MatrixSDK/Core (= 0.23.18) - - MatrixSDK/Core (0.23.18): + - MatrixSDK (0.24.0): + - MatrixSDK/Core (= 0.24.0) + - MatrixSDK/Core (0.24.0): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) @@ -70,9 +66,9 @@ PODS: - OLMKit (~> 3.2.5) - Realm (= 10.27.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/CryptoSDK (0.23.18): + - MatrixSDK/CryptoSDK (0.24.0): - MatrixSDKCrypto (= 0.1.0) - - MatrixSDK/JingleCallStack (0.23.18): + - MatrixSDK/JingleCallStack (0.24.0): - JitsiMeetSDK (= 5.0.2) - MatrixSDK/Core - MatrixSDKCrypto (0.1.0) @@ -119,7 +115,6 @@ DEPENDENCIES: - DSBottomSheet (~> 0.3) - DSWaveformImage (~> 6.1.1) - DTCoreText (~> 1.6.25) - - DTTJailbreakDetection (~> 0.4.0) - ffmpeg-kit-ios-audio (= 4.5.1) - FLEX (~> 4.5.0) - FlowCommoniOS (~> 1.12.0) @@ -128,9 +123,8 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatomoTracker (~> 7.4.1) - - MatrixSDK (from `https://dl-gitlab.example.com/bwmessenger/ios-matrix-sdk`, tag `v0.23.18_bwi_dev2`) - - MatrixSDK/JingleCallStack (from `https://dl-gitlab.example.com/bwmessenger/ios-matrix-sdk`, tag `v0.23.18_bwi_dev2`) + - MatrixSDK (= 0.24.0) + - MatrixSDK/JingleCallStack (= 0.24.0) - OLMKit - PostHog (~> 1.4.4) - ReadMoreTextView (~> 3.0.1) @@ -158,7 +152,6 @@ SPEC REPOS: - DSWaveformImage - DTCoreText - DTFoundation - - DTTJailbreakDetection - ffmpeg-kit-ios-audio - FLEX - FlowCommoniOS @@ -173,7 +166,7 @@ SPEC REPOS: - libPhoneNumber-iOS - LoggerAPI - Logging - - MatomoTracker + - MatrixSDK - MatrixSDKCrypto - OLMKit - PostHog @@ -197,17 +190,11 @@ EXTERNAL SOURCES: AnalyticsEvents: :branch: release/swift :git: https://github.com/matrix-org/matrix-analytics-events.git - MatrixSDK: - :git: https://dl-gitlab.example.com/bwmessenger/ios-matrix-sdk - :tag: v0.23.18_bwi_dev2 CHECKOUT OPTIONS: AnalyticsEvents: :commit: 53ad46ba1ea1ee8f21139dda3c351890846a202f :git: https://github.com/matrix-org/matrix-analytics-events.git - MatrixSDK: - :git: https://dl-gitlab.example.com/bwmessenger/ios-matrix-sdk - :tag: v0.23.18_bwi_dev2 SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce @@ -220,7 +207,6 @@ SPEC CHECKSUMS: DSWaveformImage: 3c718a0cf99291887ee70d1d0c18d80101d3d9ce DTCoreText: ec749e013f2e1f76de5e7c7634642e600a7467ce DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536 - DTTJailbreakDetection: 5e356c5badc17995f65a83ed9483f787a0057b71 ffmpeg-kit-ios-audio: 662ce2064e56733ca7d8216705efbc38d9e1c3fe FLEX: e51461dd6f0bfb00643c262acdfea5d5d12c596b FlowCommoniOS: ca92071ab526dc89905495a37844fd7e78d1a7f2 @@ -235,8 +221,7 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatomoTracker: 24a846c9d3aa76933183fe9d47fd62c9efa863fb - MatrixSDK: 26da2e3a9f3b02fc6ea67f5bc311d30f06f9ffba + MatrixSDK: 196ae670143c5169ca9d02ff4d4a87f1e4dd8f08 MatrixSDKCrypto: 4b9146d5ef484550341be056a164c6930038028e OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f @@ -256,6 +241,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 6249659a7f2317b7109adfec20b337ad4d8bb45d +PODFILE CHECKSUM: f8957c6eb2617c1f8a9174c1c5eac5f2bd67a599 -COCOAPODS: 1.11.3 +COCOAPODS: 1.11.2 diff --git a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme index 4e299eba2..012a5a109 100644 --- a/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme +++ b/Riot.xcodeproj/xcshareddata/xcschemes/Riot.xcscheme @@ -1,10 +1,11 @@ + version = "1.7"> + buildImplicitDependencies = "YES" + runPostActionsOnFailure = "NO"> @@ -27,12 +28,14 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" - disableMainThreadChecker = "YES"> + disableMainThreadChecker = "YES" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> @@ -49,6 +52,17 @@ + + + + + + + + + + diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index a4ed2f4c0..7b45c9e98 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "devicekit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/devicekit/DeviceKit", + "state" : { + "revision" : "20e0991f3975916ab0f6d58db84d8bc64f883537", + "version" : "4.7.0" + } + }, { "identity" : "maplibre-gl-native-distribution", "kind" : "remoteSourceControl", @@ -42,7 +51,7 @@ "location" : "https://github.com/vector-im/swift-ogg", "state" : { "branch" : "main", - "revision" : "0ffad3f7b45a6a4760db090d503b00f094bbecc0" + "revision" : "e9a9e7601da662fd8b97d93781ff5c60b4becf88" } } ], diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_inactive.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_inactive.imageset/Contents.json new file mode 100644 index 000000000..7261e4ae4 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_inactive.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "user_sessions_inactive.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_inactive.imageset/user_sessions_inactive.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_inactive.imageset/user_sessions_inactive.svg new file mode 100644 index 000000000..d7baa4786 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_inactive.imageset/user_sessions_inactive.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_unverified.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_unverified.imageset/Contents.json new file mode 100644 index 000000000..2c0d97214 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_unverified.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "user_sessions_unverified.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_unverified.imageset/user_sessions_unverified.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_unverified.imageset/user_sessions_unverified.svg new file mode 100644 index 000000000..4db2abe67 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_unverified.imageset/user_sessions_unverified.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Riot/Assets/Images.xcassets/Encryption/Contents.json b/Riot/Assets/Images.xcassets/Encryption/Contents.json index da4a164c9..73c00596a 100644 --- a/Riot/Assets/Images.xcassets/Encryption/Contents.json +++ b/Riot/Assets/Images.xcassets/Encryption/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/Contents.json b/Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/Contents.json new file mode 100644 index 000000000..d309a9a02 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "encryption_untrusted.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "encryption_untrusted@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "encryption_untrusted@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/encryption_untrusted.png b/Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/encryption_untrusted.png new file mode 100644 index 000000000..830f9fc3c Binary files /dev/null and b/Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/encryption_untrusted.png differ diff --git a/Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/encryption_untrusted@2x.png b/Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/encryption_untrusted@2x.png new file mode 100644 index 000000000..f75d4e074 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/encryption_untrusted@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/encryption_untrusted@3x.png b/Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/encryption_untrusted@3x.png new file mode 100644 index 000000000..884106c1f Binary files /dev/null and b/Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/encryption_untrusted@3x.png differ diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 569d7a78d..1942aa352 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -64,16 +64,16 @@ "auth_reset_password_next_step_button" = "Ich habe meine E-Mail-Adresse verifiziert"; "auth_reset_password_email_validation_message" = "Eine E-Mail wurde an %@ gesendet. Wenn du dem Link dort gefolgt bist, klicke unten."; // Chat creation -"room_creation_title" = "Neuer Chat"; +"room_creation_title" = "Neue Unterhaltung"; "room_creation_account" = "Konto"; "room_creation_appearance" = "Aussehen"; "room_creation_appearance_name" = "Name"; "room_creation_appearance_picture" = "Avatar (optional)"; "room_creation_privacy" = "Datenschutz"; -"room_creation_private_room" = "Dieser Chat ist privat"; -"room_creation_public_room" = "Dieser Chat ist öffentlich"; +"room_creation_private_room" = "Diese Unterhaltung ist privat"; +"room_creation_public_room" = "Diese Unterhaltung ist öffentlich"; "room_creation_make_public" = "Mache öffentlich"; -"room_creation_make_public_prompt_title" = "Diesen Chat öffentlich machen?"; +"room_creation_make_public_prompt_title" = "Diese Unterhaltung öffentlich machen?"; "room_creation_keep_private" = "Privat lassen"; "room_creation_make_private" = "Mache privat"; "room_creation_wait_for_creation" = "Es wird gerade schon ein Raum erstellt. Bitte warten."; @@ -85,7 +85,7 @@ "room_recents_no_conversation" = "Keine Räume"; "room_recents_low_priority_section" = "NIEDRIGE PRIORITÄT"; "room_recents_invites_section" = "EINLADUNGEN"; -"room_recents_start_chat_with" = "Starte Chat"; +"room_recents_start_chat_with" = "Unterhaltung beginnen"; "room_recents_create_empty_room" = "Erstelle Raum"; "room_recents_join_room" = "Tritt Raum bei"; "room_recents_join_room_title" = "Trete einem Raum bei"; @@ -121,9 +121,9 @@ "room_participants_leave_prompt_title" = "Raum verlassen"; "room_participants_leave_prompt_msg" = "Bist du sicher, dass du den Raum verlassen willst?"; "room_participants_remove_prompt_title" = "Bestätigung"; -"room_participants_remove_prompt_msg" = "Bist du sicher, dass du %@ aus dem Chat entfernen möchtest?"; +"room_participants_remove_prompt_msg" = "Bist du sicher, dass du %@ aus der Unterhaltung entfernen möchtest?"; "room_participants_invite_prompt_title" = "Bestätigung"; -"room_participants_invite_prompt_msg" = "Bist du sicher, dass du %@ in diesen Chat einladen möchtest?"; +"room_participants_invite_prompt_msg" = "Bist du sicher, dass du %@ in diese Unterhaltung einladen möchtest?"; "room_participants_filter_room_members" = "Mitglieder filtern"; "room_participants_invite_malformed_id_title" = "Fehler beim Einladen"; "room_participants_invited_section" = "EINGELADEN"; @@ -131,7 +131,7 @@ "room_participants_offline" = "Offline"; "room_participants_unknown" = "Unbekannt"; "room_participants_now" = "jetzt"; -"room_participants_action_section_direct_chats" = "Direkte Chats"; +"room_participants_action_section_direct_chats" = "Direktnachrichten"; "room_participants_action_section_devices" = "Sitzungen"; "room_participants_action_section_other" = "Optionen"; "room_participants_action_invite" = "Einladen"; @@ -142,7 +142,7 @@ "room_participants_action_unignore" = "Alle Nachrichten dieser Person zeigen"; "room_participants_action_set_moderator" = "Moderationsrechte vergeben"; "room_participants_action_set_admin" = "Administrationsrechte vergeben"; -"room_participants_action_start_new_chat" = "Starte neuen Chat"; +"room_participants_action_start_new_chat" = "Unterhaltung beginnen"; "room_participants_action_start_video_call" = "Starte Video-Anruf"; "room_participants_action_mention" = "Erwähnen"; // Chat @@ -239,7 +239,7 @@ "settings_new_password" = "Neues Passwort"; "settings_confirm_password" = "Bestätige Passwort"; "settings_password_updated" = "Das Passwort deines Matrix-Kontos wurde aktualisiert"; -"settings_crypto_device_name" = "Öffentlicher Sitzungs-Name: "; +"settings_crypto_device_name" = "Sitzungsname: "; "settings_crypto_device_key" = "\nSitzungs-Schlüssel:\n"; "settings_crypto_export" = "Schlüssel exportieren"; "room_details_settings" = "Einstellungen"; @@ -303,7 +303,7 @@ "room_participants_action_start_voice_call" = "Starte Sprach-Anruf"; "room_ongoing_conference_call" = "Laufender Konferenz-Anruf. Trete bei als %@ oder %@."; "room_event_action_redact" = "Entfernen"; -"room_warning_about_encryption" = "Ende-zu-Ende Verschlüsselung ist in Beta und ist evtl. nicht zuverlässig.\n\nMan sollte noch nicht darauf vertrauen, dass die Daten sicher sind.\n\nGeräte werden Nachrichten von vor dem Beitritt des Raumes nicht entschlüsseln können.\n\nVerschlüsselte Nachrichten sind nicht lesbar auf Clients, die die Verschlüsselung noch nicht implementiert haben."; +"room_warning_about_encryption" = "Ende-zu-Ende-Verschlüsselung ist in Beta und ist evtl. nicht zuverlässig.\n\nMan sollte noch nicht darauf vertrauen, dass die Daten sicher sind.\n\nGeräte werden Nachrichten von vor dem Beitritt des Raumes nicht entschlüsseln können.\n\nVerschlüsselte Nachrichten sind nicht lesbar in Anwendungen, die die Verschlüsselung noch nicht implementiert haben."; "unknown_devices_alert" = "Dieser Raum enthält unbekannte Sitzungen, die nicht verifiziert wurden.\nDas bedeutet, es gibt keine Garantie, dass sie den angegebenen Benutzern gehört.\nWir empfehlen eine Überprüfung für jedes Gerät, bevor du weitermachst. Du kannst die Nachricht auch ohne Verifizierung erneut senden."; "room_preview_unlinked_email_warning" = "Diese Einladung wurde an %@ gesendet, welche diesem Konto nicht zugeordnet ist. Möglicherweise möchtest du dich mit einem anderen Konto anmelden oder diese E-Mail zu deinem Konto hinzufügen."; "room_preview_try_join_an_unknown_room" = "Du versuchst auf %@ zuzugreifen. Möchtest du dem Raum beitreten um teilzunehmen?"; @@ -313,12 +313,12 @@ "settings_phone_number" = "Telefon"; "settings_add_phone_number" = "Telefonnummer hinzufügen"; "settings_fail_to_update_profile" = "Profilaktualisierung fehlgeschlagen"; -"settings_global_settings_info" = "Globale Benachrichtigungseinstellungen sind auf deinem %@ web-Client verfügbar"; +"settings_global_settings_info" = "Globale Benachrichtigungseinstellungen sind in deiner %@ Web-Anwendung verfügbar"; "settings_pin_rooms_with_missed_notif" = "Pinnen von Räumen mit verpassten Benachrichtigungen"; "settings_pin_rooms_with_unread" = "Pinnen von Räumen mit ungelesenen Nachrichten"; "settings_contacts_discover_matrix_users" = "Entdecke andere Benutzer mittels E-Mail-Adressen oder Telefonnummern"; "settings_labs_e2e_encryption_prompt_message" = "Zum Fertigstellen der Verschlüsselung bitte neu anmelden."; -"settings_third_party_notices" = "Anmerkungen von Dritten"; +"settings_third_party_notices" = "Drittanbieter-Lizenzen"; "settings_clear_cache" = "Cache löschen"; "settings_fail_to_update_password" = "Passwortänderung des Matrix-Kontos fehlgeschlagen"; "settings_crypto_device_id" = "\nSitzungs-ID: "; @@ -391,7 +391,7 @@ "bug_report_description" = "Beschreibe den Fehler. Was hast du gemacht? Was hast du erwartet? Was ist tatsächlich passiert?"; "bug_crash_report_title" = "Absturzmeldung"; "bug_crash_report_description" = "Bitte beschreibe was du vor dem Absturz gemacht hast:"; -"bug_report_logs_description" = "Um Probleme analysieren zu können werden Protokolldateien dieses Clients mit dem Fehlerbericht gesendet. Wenn du nur den Text oben senden willst, bitte abwählen:"; +"bug_report_logs_description" = "Um Probleme analysieren zu können, werden Protokolldateien dieser Anwendung mit dem Fehlerbericht gesendet. Wenn du nur den Text oben senden willst, bitte abwählen:"; "bug_report_send_logs" = "Sende Protokolldateien"; "bug_report_send_screenshot" = "Sende Bildschirmfoto"; "bug_report_progress_zipping" = "Sammele Protokolle"; @@ -427,7 +427,7 @@ "send_to" = "Sende an %@"; "sending" = "Senden"; "room_event_failed_to_send" = "Senden fehlgeschlagen"; -"room_details_direct_chat" = "Direkter Chat"; +"room_details_direct_chat" = "Direktnachrichten"; "room_details_fail_to_update_room_direct" = "Setzen der Direktkennzeichnung in diesem Raum fehlgeschlagen"; "event_formatter_widget_added" = "Widget %@ wurde von %@ hinzugefügt"; "event_formatter_widget_removed" = "Widget %@ wurde von %@ entfernt"; @@ -610,14 +610,14 @@ "key_backup_recover_invalid_passphrase" = "Sicherung konnte mit dieser Phrase nicht entschlüsselt werden: Bitte stelle sicher, dass du die richtige Sicherungsphrase eingegeben hast."; "key_backup_recover_invalid_recovery_key_title" = "Ungültiger Sicherungsschlüssel"; "key_backup_recover_invalid_recovery_key" = "Sicherung konnte mit diesem Schlüssel nicht entschlüsselt werden: Bitte stelle sicher, dass du den richtigen Sicherungsschlüssel eingegeben hast."; -"key_backup_recover_from_passphrase_info" = "Nutze deine Sicherungsphrase um deinen sicheren Chatverlauf zu entschlüsseln"; +"key_backup_recover_from_passphrase_info" = "Nutze deine Sicherungsphrase, um deinen sicheren Nachrichtenverlauf zu entschlüsseln"; "key_backup_recover_from_passphrase_passphrase_title" = "Eingeben"; "key_backup_recover_from_passphrase_passphrase_placeholder" = "Phrase eingeben"; "key_backup_recover_from_passphrase_recover_action" = "Historie entschlüsseln"; "key_backup_recover_from_passphrase_lost_passphrase_action_part1" = "Kennst deine Sicherungsphrase nicht? Du kannst "; "key_backup_recover_from_passphrase_lost_passphrase_action_part2" = "deinen Sicherungsschlüssel nutzen"; "key_backup_recover_from_passphrase_lost_passphrase_action_part3" = "."; -"key_backup_recover_from_recovery_key_info" = "Nutze deinen Sicherungsschlüssel, um deine Chatverläufe zu entschlüsseln"; +"key_backup_recover_from_recovery_key_info" = "Nutze deinen Sicherungsschlüssel, um deinen Nachrichtenverlauf zu entschlüsseln"; "key_backup_recover_from_recovery_key_recovery_key_title" = "Eingeben"; "key_backup_recover_from_recovery_key_recovery_key_placeholder" = "Sicherungsschlüssel eingeben"; "key_backup_recover_from_recovery_key_recover_action" = "Verlauf entschlüsseln"; @@ -649,7 +649,7 @@ "key_backup_setup_intro_manual_export_action" = "Manueller Schlüssel-Export"; // String for App Store "store_short_description" = "Sicherer, dezentralisierter Chat/VoIP"; -"store_full_description" = "Element ist die neue Art von Messenger- und Kooperations-App, die:\n\n1. dir die Kontrolle gibt deine Privatsphäre zu schützen,\n2. dir die Kommunikation mit anderen Personen im Matrix-Netzwerk und darüber hinaus Integration in Apps wie Slack ermöglicht,\n3. dich vor Werbung, Datenerfassung, Hintertüren und geschlossene Plattformen schützt,\n4. dich durch End-to-End-Verschlüsselung absichert und mit Quersignaturen andere überprüft.\n\nElement unterscheidet sich grundlegend von anderen Messaging- und Kooperations-Diensten, da es dezentralisiert und open-source ist.\n\nMit Element kannst du dich selbst hosten oder einen Host auswählen, sodass du Datenschutz, Eigentum und Kontrolle über deine Daten und Konversationen hast. Du erhältst Zugriff auf ein offenes Netzwerk und bist nicht auf Element-Nutzer beschränkt. Und es ist sehr sicher.\n\nElement ist in der Lage, all dies zu tun, da es mit Matrix arbeitet - dem Standard für offene, dezentrale Kommunikation.\n\nMit Element hast du die Kontrolle, indem du auswählen kannst, wer deine Konversationen hostet. In der Element-App kannst du verschiedene Hosts auswählen:\n\n1. Hole dir ein kostenloses Konto auf dem öffentlichen Server von matrix.org\n2. Hoste dein Konto selbst, indem du einen Server auf deiner eigenen Hardware ausführst\n3. Melde dich für ein Konto auf einem benutzerdefinierten Server an, indem du einfach die Hosting-Plattform von Element Matrix Services abonnierst\n\nWarum Element?\n\nBESITZE DEINE DATEN: Du entscheidest, wo deine Daten und Nachrichten aufbewahrt werden sollen. Du besitzt und kontrollierst sie, nicht irgendein MEGAKONZERN, die deine Daten verwertet oder Dritten Zugriff gewährt.\n\nOPEN MESSAGING UND ZUSAMMENARBEIT: Du kannst mit allen anderen Mitgliedern des Matrix-Netzwerks chatten, unabhängig davon, ob sie Element oder eine andere Matrix-App verwenden, sogar selbst wenn sie ein anderes Messaging-System wie Slack, IRC oder XMPP verwenden.\n\nSUPERSICHER: Echte End-to-End-Verschlüsselung (nur diejenigen in der Konversation können Nachrichten entschlüsseln) und Quersignierung, um die Geräte der Konversationsteilnehmer zu überprüfen.\n\nVOLLSTÄNDIGE KOMMUNIKATION: Messaging, Sprach- und Videoanrufe, Dateifreigabe, Bildschirmfreigabe und eine ganze Reihe von Integrationen, Bots und Widgets. Baue Räume, Gemeinschaften, bleib in Kontakt und erledige Dinge.\n\nÜBERALL, WO DU BIST: Bleibe mit dem vollständig synchronisierten Nachrichtenverlauf auf all deinen Geräten und im Internet (unter https://element.io/app) unabhängig voneinander in Kontakt."; +"store_full_description" = "Element ist die neue Art von Kommunikations- und Kooperations-App, die:\n\n1. dir die Kontrolle gibt, deine Privatsphäre zu schützen\n2. dir die Kommunikation mit anderen Personen im Matrix-Netzwerk und darüber hinaus Integration in Apps wie Slack ermöglicht\n3. dich vor Werbung, Datenerfassung, Hintertüren und geschlossene Plattformen schützt\n4. dich durch Ende-zu-Ende-Verschlüsselung absichert und mit Quersignaturen andere überprüft\n\nElement unterscheidet sich grundlegend von anderen Kommunikations- und Kooperations-Diensten, da es dezentralisiert und Open-Source ist.\n\nElement lässt dir die Wahl, ob du einen eigenen Server betreibst oder einen bestehenden wählst, sodass du Datenschutz, Eigentum und Kontrolle über deine Daten und Konversationen hast. Du erhältst Zugriff auf ein offenes Netzwerk und bist nicht auf Element-Nutzer beschränkt. Und es ist sehr sicher.\n\nElement ist in der Lage, all dies zu tun, da es mit Matrix arbeitet – dem Standard für offene, dezentrale Kommunikation.\n\nMit Element hast du die Kontrolle, indem du auswählen kannst, bei wem deine Unterhaltungen liegen. In der Element-App kannst du verschiedene Betreiber auswählen:\n\n1. Hole dir ein kostenloses Konto auf dem öffentlichen Server von matrix.org\n2. Beherberge dein Konto selbst, indem du einen Server auf deiner eigenen Hardware betreibst\n3. Registriere ein Konto auf einem maßgeschneiderten Server, indem du einfach die Element-Matrix-Services abonnierst\n\nWarum Element?\n\nBESITZE DEINE DATEN: Du entscheidest, wo deine Daten und Nachrichten aufbewahrt werden sollen. Du besitzt und kontrollierst sie, nicht irgendein MEGAKONZERN, der deine Daten verwertet oder dritten Zugriff gewährt.\n\nOFFENE KOMMUNIKATION UND ZUSAMMENARBEIT: Du kannst mit allen anderen Mitgliedern des Matrix-Netzwerks schreiben, unabhängig davon, ob sie Element oder eine andere Matrix-App verwenden, selbst wenn sie eine andere Plattform wie beispielsweise Slack, IRC oder XMPP verwenden.\n\nSUPER SICHER: Echte Ende-zu-Ende-Verschlüsselung (nur diejenigen in der Konversation können Nachrichten entschlüsseln) und Quersignierung, um die Geräte der Konversationsteilnehmer zu überprüfen.\n\nVOLLSTÄNDIGE KOMMUNIKATION: Schreiben, Sprach- und Videoanrufe, Dateifreigabe, Bildschirmfreigabe und eine ganze Reihe von Integrationen, Bots und Widgets. Erschaffe Räume, Gemeinschaften, bleib in Kontakt und erledige Dinge.\n\nÜBERALL, WO DU BIST: Bleibe mit dem vollständig synchronisierten Nachrichtenverlauf auf all deinen Geräten und im Internet (unter https://element.io/app) unabhängig voneinander in Kontakt."; "auth_login_single_sign_on" = "Anmelden"; "auth_autodiscover_invalid_response" = "Ungültige Antwort beim Entdecken des Heimservers"; "room_message_unable_open_link_error_message" = "Konnte Link nicht öffnen."; @@ -751,7 +751,7 @@ // MARK: Start "device_verification_start_title" = "Verifizieren durch Vergleichen eines kurzen Textes"; "device_verification_start_wait_partner" = "Warten auf Annahme durch Partner…"; -"device_verification_start_use_legacy" = "Passiert nichts? Noch nicht alle Clients unterstützen die interaktive Verifizierung. Verwende die alte Verifizierungsmethode."; +"device_verification_start_use_legacy" = "Passiert nichts? Noch nicht alle Anwendungen unterstützen die interaktive Verifizierung. Verwende die alte Verifizierungsmethode."; "device_verification_start_use_legacy_action" = "Verwende alte Verifizierungsmethode"; // MARK: Verify "device_verification_verify_title_emoji" = "Überprüfe diese Sitzung indem du bestätigst, dass das folgende Emoji auf dem Bildschirm des Gegenüber angezeigt wird"; @@ -797,7 +797,7 @@ "photo_library_access_not_granted" = "%@ hat keine Berechtigung zum Zugriff auf die Fotobibliothek. Bitte ändere die Datenschutzeinstellungen"; "auth_forgot_password_error_no_configured_identity_server" = "Es ist kein Identitätsserver konfiguriert: Füge einen hinzu, um das Kennwort deines Matrix-Kontos zurückzusetzen."; "room_creation_error_invite_user_by_email_without_identity_server" = "Es ist kein Identitätsserver konfiguriert, sodass du keine Teilnehmenden mit einer E-Mail hinzufügen kannst."; -"room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Es ist kein Identitätsserver konfiguriert, sodass du keinen Chat mit einem Kontakt über eine E-Mail starten kannst."; +"room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Da kein Identitätsserver konfiguriert wurde, kannst du keine Unterhaltung mittels E-Mail-Adresse beginnen."; // Service terms "service_terms_modal_title" = "Nutzungsbedingungen"; "service_terms_modal_accept_button" = "Akzeptieren"; @@ -1003,7 +1003,7 @@ "security_settings_secure_backup_delete" = "Backup löschen"; "security_settings_crosssigning_info_ok" = "Quersignierung ist bereit zur Anwendung."; "security_settings_crosssigning_reset" = "Zurücksetzen"; -"security_settings_coming_soon" = "Entschuldigung, diese Funktion ist noch nicht für %@ iOS verfügbar. Bitte nutze einen anderen Matrix-Client, um es einzurichten. %@ iOS wird es benutzen."; +"security_settings_coming_soon" = "Entschuldigung, diese Funktion ist noch nicht für %@ iOS verfügbar. Bitte nutze eine andere Matrix-Anwendung, um es einzurichten. %@ iOS wird es benutzen."; "security_settings_user_password_description" = "Bestätige deine Identität durch Eingabe des Passworts deines Matrix-Kontos"; // AuthenticatedSessionViewControllerFactory "authenticated_session_flow_not_supported" = "Diese App unterstützt nicht diese Authentifizierungsmethode für deinen Heimserver."; @@ -1051,7 +1051,7 @@ "key_verification_self_verify_unverified_sessions_alert_message" = "Verifiziere alle deine Sitzungen, um sicher zu stellen, dass dein Konto und deine Nachrichten sicher sind."; "key_verification_self_verify_unverified_sessions_alert_validate_action" = "Überprüfung"; "device_verification_self_verify_wait_new_sign_in_title" = "Diese Anmeldung verifizieren"; -"device_verification_self_verify_wait_additional_information" = "Dies funktioniert mit %@ oder einem anderen Matrix-Client, der Quersignierung unterstützt."; +"device_verification_self_verify_wait_additional_information" = "Dies funktioniert mit %@ oder einer anderen Matrix-Anwendung, die Quersignierung unterstützt."; "device_verification_self_verify_wait_recover_secrets_without_passphrase" = "Sicherungsschlüssel verwenden"; "device_verification_self_verify_wait_recover_secrets_with_passphrase" = "Sicherungsphrase oder -schlüssel verwenden"; "device_verification_self_verify_wait_recover_secrets_additional_information" = "Falls du keinen Zugang zu einer existierenden Sitzung hast"; @@ -1062,7 +1062,7 @@ "key_verification_verify_sas_additional_information" = "Für ultimative Sicherheit verwende ein anderes vertrauenswürdiges Kommunikationsmittel oder mache es persönlich."; "key_verification_manually_verify_device_title" = "Verifiziere manuell mit einem Text"; "key_verification_manually_verify_device_instruction" = "Vergleiche die folgenden Zeichen mit den Einstellungen in der Sitzung des anderen Nutzers und bestätige:"; -"key_verification_manually_verify_device_name_title" = "Name der Sitzung"; +"key_verification_manually_verify_device_name_title" = "Sitzungsname"; "key_verification_manually_verify_device_id_title" = "Sitzungs-ID"; "key_verification_manually_verify_device_key_title" = "Sitzungsschlüssel"; "key_verification_manually_verify_device_additional_information" = "Falls sie nicht übereinstimmen, wurde die Kommunikation vielleicht kompromittiert."; @@ -1178,19 +1178,19 @@ "searchable_directory_x_network" = "%@ Netzwerk"; "searchable_directory_search_placeholder" = "Name oder ID"; "create_room_title" = "Neuer Raum"; -"create_room_section_header_name" = "Raumname"; +"create_room_section_header_name" = "NAME"; "create_room_placeholder_name" = "Name"; "create_room_section_header_topic" = "THEMA (OPTIONAL)"; "create_room_placeholder_topic" = "Um was geht es in diesem Raum?"; -"create_room_section_header_encryption" = "Raumverschlüsselung"; +"create_room_section_header_encryption" = "VERSCHLÜSSELUNG"; "create_room_enable_encryption" = "Verschlüsselung aktivieren"; "create_room_section_footer_encryption" = "Verschlüsselung kann im Nachhinein nicht deaktiviert werden."; -"create_room_section_header_type" = "Raumtyp"; +"create_room_section_header_type" = "BEITRITTSBERECHTIGTE"; "create_room_type_private" = "Privater Raum (nur Eingeladene)"; "create_room_type_public" = "Öffentlicher Raum (jeder hat Zugriff)"; "create_room_section_footer_type" = "Personen können einen privaten Raum nur mit Einladung betreten."; "create_room_show_in_directory" = "Im Raumverzeichnis zeigen"; -"create_room_section_header_address" = "Raum-Adresse"; +"create_room_section_header_address" = "ADRESSE"; "create_room_placeholder_address" = "#testraum:matrix.org"; "room_info_list_room_encrypted" = "Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt"; "room_info_list_one_member" = "1 Mitglied"; @@ -1241,9 +1241,9 @@ // MARK: - Favourites "favourites_empty_view_title" = "Favorisierte Räume und Personen"; -"favourites_empty_view_information" = "Um einen Lieblingschat hinzuzufügen, halte diesen gedrückt und wähle den Stern aus. Du findest deine Lieblingschats gesammelt hier."; -"home_empty_view_information" = "Die sichere Chat-App für Teams, Freunde und Organisationen. Tippe unten auf das +, um Leute und Räume hinzuzufügen."; -"rooms_empty_view_information" = "Noch keine Chat-Räume vorhanden. Tippe auf das +, um einem Raum beizutreten oder einen neuen zu erstellen."; +"favourites_empty_view_information" = "Du kannst Unterhaltungen favorisieren – am schnellsten geht das, indem du sie gedrückt hältst. Tippe auf den Stern und schon werden sie hier angezeigt."; +"home_empty_view_information" = "Die sichere Kommunikations-App für Gruppen, Freunde und Organisationen. Tippe unten auf das +, um Personen und Räume hinzuzufügen."; +"rooms_empty_view_information" = "Räume sind großartig für Gruppenunterhaltungen, ob privat oder öffentlich. Tippe auf +, bestehende Räume zu finden oder neue zu erstellen."; "invite_friends_share_text" = "Hey! Sprich mit mir auf %@: %@"; // MARK: - Invite friends @@ -1318,7 +1318,7 @@ "room_accessibility_video_call" = "Videoanruf"; "room_message_replying_to" = "%@ anworten"; "room_message_editing" = "Bearbeitung"; -"space_beta_announce_information" = "Spaces bieten neue Möglichkeiten um Räume und Personen zu gruppieren. Sie sind noch nicht auf iOS verfügbar, aber du kannst sie jetzt schon mit Web und Desktop nutzen."; +"space_beta_announce_information" = "Spaces sind eine neue Möglichkeit, Räume und Personen zu gruppieren. Sie sind noch nicht auf iOS verfügbar, aber du kannst sie jetzt schon mit Web und Desktop nutzen."; "space_feature_unavailable_information" = "Spaces bieten neue Möglichkeiten um Räume und Personen zu gruppieren.\n\nBald werden sie auch hier verfügbar sein. Für den Moment kannst du ihnen auf einer der anderen Plattformen beitreten und hier auf alle Räume zugreifen, denen du dort beitrittst."; "space_beta_announce_subtitle" = "Die verbesserte Version von Communities"; "space_beta_announce_title" = "Spaces sind bald verfügbar"; @@ -1423,8 +1423,8 @@ "settings_messages_containing_at_room" = "@room"; "settings_messages_containing_user_name" = "Meinem Benutzernamen"; "settings_messages_containing_display_name" = "Meinem Anzeigenamen"; -"settings_encrypted_group_messages" = "Verschlüsselten Gruppenchats"; -"settings_group_messages" = "Gruppenchats"; +"settings_encrypted_group_messages" = "Verschlüsselte Gruppennachrichten"; +"settings_group_messages" = "Gruppennachrichten"; "settings_encrypted_direct_messages" = "Verschlüsselten Direktnachrichten"; "settings_direct_messages" = "Direktnachrichten"; "settings_notify_me_for" = "Benachrichtige mich bei"; @@ -1500,7 +1500,7 @@ "contacts_address_book_permission_denied_alert_title" = "Kontakte deaktiviert"; "poll_edit_form_create_options" = "Erstelle Optionen"; "settings_discovery_accept_terms" = "Bedingungen des Identitätsservers akzeptieren"; -"find_your_contacts_message" = "Lass dir von %@ deine Kontakte anzeigen um schnell mit denen zu chatten, die du am besten kennst."; +"find_your_contacts_message" = "Lass dir von %@ deine Kontakte anzeigen, um schnell mit denen zu schreiben, die du am besten kennst."; "poll_timeline_votes_count" = "%lu Stimmen"; "poll_timeline_one_vote" = "1 Stimme"; "poll_edit_form_add_option" = "Option hinzufügen"; @@ -1627,7 +1627,7 @@ "room_event_action_reply_in_thread" = "Thread"; "room_event_action_view_in_room" = "Im Raum anzeigen"; "location_sharing_open_open_street_maps" = "In OpenStreetMap öffnen"; -"onboarding_use_case_title" = "Mit wem wirst du am meisten chatten?"; +"onboarding_use_case_title" = "Mit wem wirst du am meisten schreiben?"; "onboarding_use_case_message" = "Wir helfen dir, dich zu verbinden"; "onboarding_use_case_personal_messaging" = "Freunde und Familie"; "onboarding_use_case_work_messaging" = "Teams"; @@ -1694,7 +1694,7 @@ "set_default_power_level" = "Berechtigungsstufe zurücksetzen"; "set_moderator" = "Moderator setzen"; "set_admin" = "Administrator setzen"; -"start_chat" = "Chat starten"; +"start_chat" = "Unterhaltung beginnen"; "start_voice_call" = "Sprachanruf starten"; "start_video_call" = "Videoanruf starten"; "mention" = "Erwähnung"; @@ -1872,7 +1872,7 @@ "attachment_cancel_upload" = "Hochladen abbrechen?"; "attachment_multiselection_size_prompt" = "Bilder senden als:"; "attachment_multiselection_original" = "Originalgröße"; -"attachment_e2e_keys_file_prompt" = "Diese Datei enthält von einem Matrixclient exportierte Schlüssel.\nMöchtest du den Dateiinhalt sehen oder die Schlüssel importieren?"; +"attachment_e2e_keys_file_prompt" = "Diese Datei enthält von einer Matrix-Anwendung exportierte Schlüssel.\nMöchtest du den Dateiinhalt sehen oder die Schlüssel importieren?"; "attachment_e2e_keys_import" = "Importiere..."; // Contacts "contact_mx_users" = "Matrixbenutzer"; @@ -1888,12 +1888,12 @@ "format_time_d" = "t"; // E2E import "e2e_import_room_keys" = "Importiere Raumschlüssel"; -"e2e_import_prompt" = "Dieser Prozess erlaubt es dir, Schlüssel zu importieren, die du vorher von einem anderen Matrixclient exportiert hast. Du kannst anschließend alle Nachrichten entschlüsseln, die auch bereits der andere Client entschlüsseln konnte.\nDie Exportdatei ist mit einer Passphrase geschützt. Gib die Passphrase hier ein, um die Datei zu importieren."; +"e2e_import_prompt" = "Dieser Prozess erlaubt es dir, Schlüssel zu importieren, die du vorher von einer anderen Matrix-Anwendung exportiert hast. Du kannst anschließend alle Nachrichten entschlüsseln, die auch bereits die andere Anwendung entschlüsseln konnte.\nDie Exportdatei ist mit einer Passphrase geschützt. Gib die Passphrase hier ein, um die Datei zu importieren."; "e2e_import" = "Importieren"; "e2e_passphrase_enter" = "Passphrase eingeben"; // E2E export "e2e_export_room_keys" = "Exportiere Raumschlüssel"; -"e2e_export_prompt" = "Dieser Prozess erlaubt den Export von Schlüsseln, die du in verschlüsselten Räumen empfangen hast, in eine lokale Datei. Du kannst dann die Datei in einem anderen Matrixclient in Zukunft importieren, so dass dieser Client die Nachrichten auch entschlüsseln kann.\nDie exportierte Datei wird jedem der sie lesen kann erlauben, alle verschlüsselten Nachrichten sehen können, also verwahre die Datei sicher."; +"e2e_export_prompt" = "Dieser Prozess erlaubt den Export von Schlüsseln, die du in verschlüsselten Räumen empfangen hast, in eine lokale Datei. Du kannst dann die Datei in einer anderen Matrix-Anwendung importieren, sodass diese die Nachrichten auch entschlüsseln kann.\nDie exportierte Datei wird jedem, der sie lesen kann, erlauben, alle verschlüsselten Nachrichten sehen können, also verwahre die Datei sicher."; "e2e_export" = "Exportiere"; "e2e_passphrase_confirm" = "Passphrase bestätigen"; "e2e_passphrase_empty" = "Die Passphrase darf nicht leer sein"; @@ -2025,7 +2025,7 @@ "membership_ban" = "Gesperrt"; "num_members_one" = "%@ Benutzer"; "num_members_other" = "%@ Benutzer"; -"kick" = "Entfernen"; +"kick" = "Aus Unterhaltung entfernen"; "ban" = "Sperren"; "unban" = "Entsperren"; "message_unsaved_changes" = "Es gibt nicht gespeicherte Änderungen. Verlassen wird diese verwerfen."; @@ -2046,7 +2046,7 @@ // room details dialog screen // contacts list screen -"invitation_message" = "Ich würde gerne über Matrix mit dir chatten. Du kannst dich auf https://matrix.org darüber informieren."; +"invitation_message" = "Ich würde gerne über Matrix mit dir schreiben. Du kannst dich auf https://matrix.org darüber informieren."; // Settings screen "settings_title_config" = "Konfiguration"; "settings_title_notifications" = "Benachrichtigungen"; @@ -2054,7 +2054,7 @@ "notification_settings_disable_all" = "Alle Benachrichtigungen ausschalten"; "notification_settings_enable_notifications" = "Benachrichtigungen einschalten"; "notification_settings_enable_notifications_warning" = "Alle Benachrichtigungen sind momentan für alle Geräte ausgeschaltet."; -"notification_settings_global_info" = "Benachrichtigungseinstellungen werden in deinem Benutzerkonto gespeichert und zwischen allen Clients die das unterstützen geteilt (inklusive Desktop Benachrichtigungen). \n\nRegeln werden der Reihe nach angewandt; die erste Regel, die zutrifft, bestimmt das Resultat für die Nachricht.\nPro-Wort-Benachrichtigungen sind wichtiger als Pro-Raum-Benachrichtigungen, die wichtiger sind wie Pro-Absender-Benachrichtigungen.\nBei mehrfachen Regeln des gleichen Typs wird die erste in der Liste die zutrifft angewendet."; +"notification_settings_global_info" = "Benachrichtigungseinstellungen werden in deinem Benutzerkonto gespeichert und zwischen allen Anwendungen, die das unterstützen geteilt (inklusive Desktop-Benachrichtigungen). \n\nRegeln werden der Reihe nach angewandt; die erste Regel, die zutrifft, bestimmt das Resultat für die Nachricht.\nPro-Wort-Benachrichtigungen sind wichtiger als Pro-Raum-Benachrichtigungen, die wiederum wichtiger als Pro-Absender-Benachrichtigungen sind.\nBei mehrfachen Regeln des gleichen Typs wird, die erste in der Liste, die zutrifft, angewandt."; "notification_settings_per_word_notifications" = "Pro-Wort-Benachrichtigungen"; "notification_settings_per_word_info" = "Suchwörter ignorieren Groß-/Kleinschreibung und können ein *-Platzhalter enthalten. Beispiele:\nfoo findet den String foo umgeben durch Trennzeichen (Satzzeichen, Leerzeichen, Zeilenanfang/ende).\nfoo* findet Worte die mit foo beginnen.\n*foo* findet jedes Wort das foo an beliebiger Stelle enthält."; "notification_settings_always_notify" = "Immer benachrichtigen"; @@ -2129,7 +2129,7 @@ "room_displayname_all_other_members_left" = "%@ (Verlassen)"; "notice_error_unformattable_event" = "** Nachricht kann nicht dargestellt werden. Bitte erstelle einen Bug-Report"; "home_syncing" = "Synchronisiere"; -"settings_labs_use_only_latest_user_avatar_and_name" = "Immer aktuelle Profilbilder und Nicknamen anzeigen"; +"settings_labs_use_only_latest_user_avatar_and_name" = "Aktuelle Profilbilder und Anzeigenamen im Verlauf anzeigen"; "room_participants_leave_success" = "Raum verlassen"; "room_participants_leave_processing" = "Verlassen"; "onboarding_display_name_title" = "Wähle einen Anzeigenamen"; @@ -2220,7 +2220,7 @@ "authentication_forgot_password_waiting_button" = "E-Mail erneut senden"; "authentication_verify_email_input_title" = "Gib deine E-Mail-Adresse ein"; /* The placeholder will show the full Matrix ID that has been entered. */ -"authentication_registration_username_footer_available" = "Andere Nutzer können dich als %@ finden"; +"authentication_registration_username_footer_available" = "Andere können dich als %@ finden"; "authentication_registration_username" = "Nutzername"; "onboarding_celebration_message" = "Dein kannst dein Profil jederzeit in den Einstellungen ändern"; "authentication_registration_password_footer" = "8 Zeichen oder mehr"; @@ -2351,7 +2351,7 @@ // Mark: - Space Creation -"spaces_creation_hint" = "Spaces sind eine neue Art, Räume und Personen zu gruppieren."; +"spaces_creation_hint" = "Spaces sind eine neue Möglichkeit, Räume und Personen zu gruppieren."; "space_settings_current_address_message" = "Dein Space ist sichtbar unter\n%@"; "space_settings_update_failed_message" = "Aktualisieren der Space-Einstellungen fehlgeschlagen. Erneut versuchen?"; "space_settings_access_section" = "Wer hat Zugriff auf diesen Space?"; @@ -2398,7 +2398,7 @@ "room_access_space_chooser_other_spaces_section" = "Andere Spaces oder Räume"; "room_access_settings_screen_setting_room_access" = "Lege Raumzugriff fest"; "room_access_settings_screen_upgrade_alert_upgrading" = "Raum upgraden"; -"room_access_settings_screen_upgrade_alert_upgrade_button" = "Upgrade"; +"room_access_settings_screen_upgrade_alert_upgrade_button" = "Aktualisieren"; "room_access_settings_screen_upgrade_alert_auto_invite_switch" = "Mitglieder automatisch zu neuem Raum einladen"; "room_access_settings_screen_upgrade_alert_message" = "Jeder in %@ kann diesen Raum finden und ihm beitreten - jeden manuell einzuladen ist nicht nötig. Du kannst diese Einstellung jederzeit ändern."; "room_access_settings_screen_upgrade_alert_title" = "Raum upgraden"; @@ -2448,16 +2448,16 @@ "room_invites_empty_view_title" = "Nichts neues."; "all_chats_edit_layout_add_filters_title" = "Filtere deine Nachrichten"; "all_chats_edit_layout_unreads" = "Ungelesene"; -"all_chats_section_title" = "Chats"; +"all_chats_section_title" = "Unterhaltungen"; // Mark: - All Chats -"all_chats_title" = "Alle Chats"; +"all_chats_title" = "Alle Unterhaltungen"; "all_chats_empty_view_title" = "%@\nsieht ein bisschen leer aus."; "all_chats_all_filter" = "Alle"; "all_chats_edit_layout_show_filters" = "Filter anzeigen"; "all_chats_edit_menu_leave_space" = "%@ verlassen"; -"all_chats_onboarding_page_title3" = "Feedback geben"; +"all_chats_onboarding_page_title3" = "Rückmeldung geben"; "room_invites_empty_view_information" = "Hier erscheinen deine Einladungen."; "all_chats_onboarding_try_it" = "Probiere es aus"; "threads_discourage_information_1" = "Dein Heimserver unterstützt aktuell keine Threads, weshalb diese Funktion unzuverlässig sein könnte. Manche Thread-Nachrichten könnten nicht zuverlässig verfügbar sein. "; @@ -2465,29 +2465,59 @@ "spaces_create_subspace_title" = "Sub-Space erstellen"; "room_access_settings_screen_upgrade_alert_note" = "Bitte beachte, dass das Upgrade eine neue Version dieses Raums erstellt. Alle aktuellen Nachrichten bleiben in diesem archivierten Raum."; "invite_to" = "Zu %@ einladen"; -"all_chats_empty_unreads_placeholder_message" = "Hier werden ungelesene Nachrichten angezeigt, wenn welche vorhanden sind."; +"all_chats_empty_unreads_placeholder_message" = "Hier werden deine ungelesenen Nachrichten erscheinen, wenn du welche hast."; "all_chats_edit_layout_show_recents" = "Historie anzeigen"; -"all_chats_empty_list_placeholder_title" = "Keine ungelesenen Nachrichten vorhanden."; +"all_chats_empty_list_placeholder_title" = "Du bist auf dem neuesten Stand."; "spaces_explore_rooms_format" = "%@ erkunden"; "room_access_settings_screen_upgrade_alert_message_no_param" = "Jeder in einem übergeordneten Space kann diesen Raum finden und ihm beitreten - jeden manuell einzuladen ist nicht nötig. Du kannst diese Einstellung jederzeit ändern."; "room_access_settings_screen_public_message" = "Sichtbar und zugänglich für jeden."; "room_access_settings_screen_restricted_message" = "Sichtbar und betretbar für jeden Nutzer in einem Space.\nDu wählst, für welche Spaces dies gilt."; "room_access_settings_screen_private_message" = "Nur sichtbar und betretbar für eingeladene Personen."; "location_sharing_allow_background_location_message" = "Wenn du deinen Live-Standort teilen möchtest, benötigt Element den Standortzugriff auch im Hintergrund. Um den Zugriff zu ermöglichen, tippe auf Einstellungen > Standort und wähle ‘Immer‘ aus"; -"space_selector_empty_view_information" = "Spaces sind ein Weg um Räume und Personen zu gruppieren. Erstelle einen Space um loszulegen."; -"all_chats_onboarding_title" = "Was ist neu?"; +"space_selector_empty_view_information" = "Spaces sind eine neue Möglichkeit, Räume und Personen zu gruppieren. Erstelle einen Space, um zu beginnen."; +"all_chats_onboarding_title" = "Was ist neu"; "all_chats_onboarding_page_message3" = "Drücke auf dein Profil um uns Wissen zu lassen, was du denkst."; -"all_chats_onboarding_page_message2" = "Greife auf deine Spaces (unten-links) schneller und einfacher denn je zu."; +"all_chats_onboarding_page_message2" = "Greife auf deine Spaces (unten links) schneller und einfacher denn je zu."; "all_chats_onboarding_page_title2" = "Auf Spaces zugreifen"; -"all_chats_onboarding_page_message1" = "Um Element zu vereinfachen, sind Tabs nun optional. Verwalte diese mit dem Menü oben-rechts."; -"all_chats_empty_view_information" = "Die alles-in-einem sichere Chat-App für Teams, Freunde und Organisationen. Erstelle einen Chat oder trete einem existierenden Raum bei, um zu starten."; -"all_chats_empty_space_information" = "Spaces sind ein neuer Weg um Räume und Personen zu gruppieren. Füge einen existierenden Raum hinzu oder erstelle einen neuen, indem du den Knopf unten-rechts verwendest."; +"all_chats_onboarding_page_message1" = "Um dein Element zu vereinfachen, sind Tabs nun optional. Verwalte sie mit dem Menü oben rechts."; +"all_chats_empty_view_information" = "Die Komplettlösung für sichere Kommunikation unter Freunden, in Gruppen oder in Organisationen. Erstelle eine Unterhaltung oder trete einem bestehenden Raum bei, um loszulegen."; +"all_chats_empty_space_information" = "Spaces sind eine neue Möglichkeit, Räume und Personen zu gruppieren. Füge einen bestehenden Raum hinzu oder erstelle einen neuen mit der Schaltfläche unten rechts."; "all_chats_edit_layout_sorting_options_title" = "Sortiere deine Nachrichten nach"; "space_detail_nav_title" = "Space-Details"; -"all_chats_onboarding_page_title1" = "Willkommen zum neuen Layout!"; +"all_chats_onboarding_page_title1" = "Willkommen in einer neuen Übersicht!"; "all_chats_edit_menu_space_settings" = "Space-Einstellungen"; -"all_chats_user_menu_settings" = "Benutzer-Einstellungen"; +"all_chats_user_menu_settings" = "Nutzereinstellungen"; "room_recents_recently_viewed_section" = "Kürzlich angesehen"; "all_chats_nothing_found_placeholder_message" = "Versuche, deine Suche anzupassen."; "all_chats_edit_layout_recents" = "Historie"; -"all_chats_edit_layout" = "Layout-Einstellungen"; +"all_chats_edit_layout" = "Layouteinstellungen"; +"spaces_creation_new_rooms_message" = "Wir werden für jedes Thema einen Raum erstellen."; +"create_room_section_footer_type_public" = "Sichtbar und betretbar für alle eingeladenen Personen, nicht nur jene, die sich im Space befinden."; + +// First item is client name and second item is session display name +"user_session_name" = "%@: %@"; +"existing" = "Bestehend"; +"device_name_web" = "%@ Web"; +"spaces_subspace_creation_visibility_title" = "Welche Art von Subspace möchtest du erstellen?"; +"all_chats_edit_layout_pin_spaces_title" = "Spaces anpinnen"; +"all_chats_edit_layout_add_filters_message" = "Filtere deine Nachrichten automatisch in Kategorien deiner Wahl"; +"all_chats_edit_layout_add_section_message" = "Abschnitt an Startseite für schnellen Zugriff anpinnen"; +"all_chats_edit_layout_add_section_title" = "Abschnitt zur Startseite hinzufügen"; +"device_name_desktop" = "%@ Desktop"; +"user_sessions_overview_current_session_section_title" = "AKTUELLE SITZUNG"; +"user_sessions_overview_other_sessions_section_title" = "ANDERE SITZUNGEN"; +"device_name_unknown" = "Unbekannte Anwendung"; +"device_name_mobile" = "%@ Mobil"; +"user_session_item_details" = "%@ · Neueste Aktivität %@"; +"user_session_unverified_additional_info" = "Verifiziere deine aktuelle Sitzung für besonders sichere Kommunikation."; +"user_session_verified_additional_info" = "Deine aktuelle Sitzung ist für sichere Kommunikation bereit."; +"user_session_learn_more" = "Mehr erfahren"; +"user_session_view_details" = "Details anzeigen"; +"user_session_verify_action" = "Sitzung verifizieren"; +"user_session_unverified_short" = "Nicht verifiziert"; +"user_session_verified_short" = "Verifiziert"; +"user_session_unverified" = "Nicht verifizierte Sitzungen"; +"user_session_verified" = "Verifizierte Sitzungen"; +"user_sessions_overview_other_sessions_section_info" = "Für bestmögliche Sicherheit verifiziere deine Sitzungen und melde dich von allen ab, die du nicht erkennst oder nutzt."; +"settings_labs_enable_new_app_layout" = "Neues App-Layout"; +"room_first_message_placeholder" = "Schreibe deine erste Nachricht …"; diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index f346e5586..571cf8959 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -762,6 +762,8 @@ Tap the + to start adding people."; "settings_labs_enable_auto_report_decryption_errors" = "Auto Report Decryption Errors"; "settings_labs_use_only_latest_user_avatar_and_name" = "Show latest avatar and name for users in message history"; "settings_labs_enable_live_location_sharing" = "Live location sharing - share current location (active development, and temporarily, locations persist in room history)"; +"settings_labs_enable_new_session_manager" = "New session manager"; +"settings_labs_enable_new_client_info_feature" = "Record the client name, version, and url to recognise sessions more easily in session manager"; "settings_labs_enable_new_app_layout" = "New Application Layout"; "settings_version" = "Version %@"; @@ -2103,10 +2105,7 @@ Tap the + to start adding people."; "spaces_creation_in_many_spaces" = "in %@ spaces"; "spaces_creation_in_one_space" = "in 1 space"; -"spaces_invite_people" = "Invite people"; -"spaces_add_room" = "Add room"; "spaces_add_room_missing_permission_message" = "You do not have permissions to add rooms to this space."; -"spaces_add_space" = "Add space"; // Mark: Leave space @@ -2187,12 +2186,13 @@ Tap the + to start adding people."; "all_chats_empty_space_information" = "Spaces are a new way to group rooms and people. Add an existing room, or create a new one, using the bottom-right button."; "all_chats_empty_view_information" = "The all-in-one secure chat app for teams, friends and organisations. Create a chat, or join an existing room, to get started."; "all_chats_empty_list_placeholder_title" = "You’re all caught up."; -"all_chats_empty_unreads_placeholder_message" = "This is where you're unread messages will show up, when you have some."; +"all_chats_empty_unreads_placeholder_message" = "This is where your unread messages will show up, when you have some."; "all_chats_nothing_found_placeholder_title" = "Nothing found."; "all_chats_nothing_found_placeholder_message" = "Try adjusting your search."; "room_recents_recently_viewed_section" = "Recently viewed"; +"all_chats_user_menu_accessibility_label" = "User menu"; "all_chats_user_menu_settings" = "User settings"; "all_chats_edit_menu_leave_space" = "Leave %@"; @@ -2358,14 +2358,36 @@ To enable access, tap Settings> Location and select Always"; "user_sessions_overview_title" = "Sessions"; -"user_sessions_overview_other_sessions_section_title" = "OTHER SESSIONS"; +"user_sessions_overview_security_recommendations_section_title" = "Security recommendations"; +"user_sessions_overview_security_recommendations_section_info" = "Improve your account security by following these recommendations."; + +"user_sessions_overview_security_recommendations_unverified_title" = "Unverified sessions"; +"user_sessions_overview_security_recommendations_unverified_info" = "Verify or sign out from unverified sessions."; + +"user_sessions_overview_security_recommendations_inactive_title" = "Inactive sessions"; +"user_sessions_overview_security_recommendations_inactive_info" = "Consider signing out from old sessions (90 days or older) you don’t use anymore."; + +"user_sessions_overview_other_sessions_section_title" = "Other sessions"; "user_sessions_overview_other_sessions_section_info" = "For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore."; +"user_sessions_overview_current_session_section_title" = "Current session"; + +"user_sessions_view_all_action" = "View all (%d)"; + "user_session_verified" = "Verified session"; "user_session_unverified" = "Unverified session"; "user_session_verified_short" = "Verified"; "user_session_unverified_short" = "Unverified"; +"user_session_verify_action" = "Verify session"; +"user_session_view_details" = "View details"; +"user_session_learn_more" = "Learn more"; +"user_session_verified_additional_info" = "Your current session is ready for secure messaging."; +"user_session_unverified_additional_info" = "Verify your current session for enhanced secure messaging."; + +"user_session_push_notifications" = "Push notifications"; +"user_session_push_notifications_message" = "When turned on, this session will receive push notifications."; + // First item is client name and second item is session display name "user_session_name" = "%@: %@"; @@ -2376,6 +2398,24 @@ To enable access, tap Settings> Location and select Always"; "device_name_mobile" = "%@ Mobile"; "device_name_unknown" = "Unknown client"; +"user_session_details_title" = "Session details"; +"user_session_details_session_section_header" = "Session"; +"user_session_details_application_section_header" = "Application"; +"user_session_details_device_section_header" = "Device"; +"user_session_details_session_name" = "Session name"; +"user_session_details_session_id" = "Session ID"; +"user_session_details_session_section_footer" = "Copy any data by tapping on it and holding it down."; +"user_session_details_device_ip_address" = "IP address"; +"user_session_details_device_ip_location" = "IP location"; +"user_session_details_device_model" = "Model"; +"user_session_details_device_browser" = "Browser"; +"user_session_details_device_os" = "Operating System"; +"user_session_details_application_name" = "Name"; +"user_session_details_application_version" = "Version"; +"user_session_details_application_url" = "URL"; +"user_session_overview_current_session_title" = "Current session"; +"user_session_overview_session_title" = "Session"; +"user_session_overview_session_details_button_title" = "Session details"; // MARK: - MatrixKit @@ -2565,6 +2605,7 @@ To enable access, tap Settings> Location and select Always"; "room_event_encryption_info_unverify" = "Unverify"; "room_event_encryption_info_block" = "Blacklist"; "room_event_encryption_info_unblock" = "Unblacklist"; +"room_event_encryption_info_key_authenticity_not_guaranteed" = "The authenticity of this encrypted message can't be guaranteed on this device."; "room_event_encryption_verify_title" = "Verify session\n\n"; "room_event_encryption_verify_message" = "To verify that this session can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this session matches the key below:\n\n\tSession name: %@\n\tSession ID: %@\n\tSession key: %@\n\nIf it matches, press the verify button below. If it doesnt, then someone else is intercepting this session and you probably want to press the blacklist button instead.\n\nIn future this verification process will be more sophisticated."; "room_event_encryption_verify_ok" = "Verify"; @@ -2684,12 +2725,9 @@ To enable access, tap Settings> Location and select Always"; "user_id_title" = "User ID:"; "offline" = "offline"; "unsent" = "Unsent"; -"error" = "Error"; "error_common_message" = "An error occured. Please try again later."; "not_supported_yet" = "Not supported yet"; "default" = "default"; -"private" = "Private"; -"public" = "Public"; "power_level" = "Power Level"; "network_error_not_reachable" = "Please check your network connectivity"; "user_id_placeholder" = "ex: @bob:homeserver"; diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 579b52229..d84210c12 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -2430,10 +2430,32 @@ "all_chats_empty_unreads_placeholder_message" = "Kui sul on lugemata sõnumeid, siis nad on siit leitavad."; "all_chats_empty_list_placeholder_title" = "Ei tea... kõik vist on nüüd tehtud."; "all_chats_empty_view_information" = "Paljude võimalustega turvaline suhtlusrakendus sõprade, kogukondade ja tiimide jaoks. Alustamiseks loo mõni uus vestlus või liitu olemasoleva jututoaga."; -"all_chats_empty_space_information" = "Kogukonnad on võimalus jututubade ja inimeste ühendamiseks. Kasutades all vasakul olevat nuppu lisa mõni olemasolev jututuba või loo uus."; +"all_chats_empty_space_information" = "Kogukonnad on võimalus jututubade ja inimeste ühendamiseks. Kasutades all paremal olevat nuppu lisa mõni olemasolev jututuba või loo uus."; "all_chats_empty_view_title" = "%@\ntundub olema tühjavõitu."; "spaces_explore_rooms_format" = "Uuri kogukonda: %@"; // User sessions management "user_sessions_settings" = "Halda sessioone"; "invite_to" = "Saada kutse siia: %@"; +"device_name_unknown" = "Tundmatu klientrakendus"; +"device_name_mobile" = "%@ Mobile"; +"device_name_web" = "%@ Web"; +"device_name_desktop" = "%@ Desktop"; +"user_session_item_details" = "%@ · Viimati kasutusel %@"; + +// First item is client name and second item is session display name +"user_session_name" = "%@: %@"; +"user_session_unverified_additional_info" = "Turvalise sõnumivahetuse nimel palun verifitseeri oma praegune sessioon."; +"user_session_verified_additional_info" = "Sinu praegune sessioon on valmis turvaliseks sõnumivahetuseks."; +"user_session_learn_more" = "Loe lisateavet"; +"user_session_view_details" = "Vaata lisateavet"; +"user_session_verify_action" = "Verifitseeri sessioon"; +"user_session_unverified_short" = "Verifitseerimata"; +"user_session_verified_short" = "Verifitseeritud"; +"user_session_unverified" = "Verifitseerimata sessioon"; +"user_session_verified" = "Verifitseeritud sessioon"; +"user_sessions_overview_current_session_section_title" = "PRAEGUNE SESSIOON"; +"user_sessions_overview_other_sessions_section_info" = "Parima turvalisuse nimel verifitseeri kõik oma sessioonid ning logi välja neist, mida sa enam ei kasuta."; +"user_sessions_overview_other_sessions_section_title" = "MUUD SESSIOONID"; +"settings_labs_enable_new_app_layout" = "Rakenduse uus paigutus"; +"room_first_message_placeholder" = "Saada oma esimene sõnum…"; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index ee4805a2d..87b74c5bb 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2485,3 +2485,25 @@ // User sessions management "user_sessions_settings" = "Munkamenetek kezelése"; "invite_to" = "Meghívó ide: %@"; +"device_name_unknown" = "Ismeretlen kliens"; +"device_name_mobile" = "%@ Mobil"; +"device_name_desktop" = "%@ Alkalmazás"; +"device_name_web" = "%@ Web"; +"user_session_item_details" = "%@ · Utolsó aktivitás %@"; + +// First item is client name and second item is session display name +"user_session_name" = "%@: %@"; +"user_session_unverified_additional_info" = "Az aktuális munkamenet készen áll a biztonságos üzenetküldésre."; +"user_session_verified_additional_info" = "Az aktuális munkamenet készen áll a biztonságos üzenetküldésre."; +"user_session_learn_more" = "Tudj meg többet"; +"user_session_view_details" = "Részletek megtekintése"; +"user_session_verify_action" = "Munkamenet hitelesítése"; +"user_session_unverified_short" = "Ellenőrizetlen"; +"user_session_verified_short" = "Hitelesített"; +"user_session_unverified" = "Ellenőrizetlen munkamenet"; +"user_session_verified" = "Ellenőrzött munkamenet"; +"user_sessions_overview_current_session_section_title" = "JELENLEGI MUNKAMENET"; +"user_sessions_overview_other_sessions_section_info" = "A legjobb biztonság érdekében ellenőrizd a munkameneteket, és jelentkezz ki minden olyan munkamenetből, melyet már nem ismersz fel vagy nem használsz."; +"user_sessions_overview_other_sessions_section_title" = "TOVÁBBI MUNKAMENETEK"; +"settings_labs_enable_new_app_layout" = "Új alkalmazás kinézet"; +"room_first_message_placeholder" = "Küld el az első üzenetedet…"; diff --git a/Riot/Assets/id.lproj/Vector.strings b/Riot/Assets/id.lproj/Vector.strings index 1bf9d6fa1..d698f6115 100644 --- a/Riot/Assets/id.lproj/Vector.strings +++ b/Riot/Assets/id.lproj/Vector.strings @@ -2692,3 +2692,25 @@ // User sessions management "user_sessions_settings" = "Kelola sesi"; "invite_to" = "Undang ke %@"; +"device_name_unknown" = "Klien tidak diketahui"; +"device_name_mobile" = "%@ Ponsel"; +"device_name_web" = "%@ Web"; +"device_name_desktop" = "%@ Desktop"; +"user_session_item_details" = "%@ · Aktivitas terakhir %@"; + +// First item is client name and second item is session display name +"user_session_name" = "%@: %@"; +"user_session_unverified_additional_info" = "Verifikasi sesi Anda saat ini untuk perpesanan aman yang baik."; +"user_session_verified_additional_info" = "Sesi Anda saat ini siap untuk perpesanan aman."; +"user_session_learn_more" = "Pelajari lebih lanjut"; +"user_session_view_details" = "Tampilkan detail"; +"user_session_verify_action" = "Verifikasi sesi"; +"user_session_unverified_short" = "Belum diverifikasi"; +"user_session_verified_short" = "Terverifikasi"; +"user_session_unverified" = "Sesi belum diverifikasi"; +"user_session_verified" = "Sesi terverifikasi"; +"user_sessions_overview_current_session_section_title" = "SESI SAAT INI"; +"user_sessions_overview_other_sessions_section_info" = "Untuk keamanan yang terbaik, verifikasi sesi Anda dan keluarkan dari sesi yang Anda tidak kenal atau tidak digunakan lagi."; +"user_sessions_overview_other_sessions_section_title" = "SESI LAINNYA"; +"settings_labs_enable_new_app_layout" = "Tata Letak Aplikasi Baru"; +"room_first_message_placeholder" = "Kirim pesan pertama Anda…"; diff --git a/Riot/Assets/is.lproj/Vector.strings b/Riot/Assets/is.lproj/Vector.strings index c2c8af304..ad9ba1194 100644 --- a/Riot/Assets/is.lproj/Vector.strings +++ b/Riot/Assets/is.lproj/Vector.strings @@ -1279,7 +1279,7 @@ "settings_key_backup_info_signout_warning" = "Taktu öryggisafrit af dulritunarlyklunum áður en þú skráir þig út svo þeir tapist ekki."; "settings_key_backup_info_none" = "Dulritunarlyklarnir þínir eru ekki öryggisafritaðir úr þessari setu."; "settings_key_backup_info_checking" = "Athuga…"; -"settings_deactivate_my_account" = "Gera notandaaðganginn minn óvirkann"; +"settings_deactivate_my_account" = "Gera notandaaðganginn endanlega óvirkan"; "settings_add_3pid_invalid_password_message" = "Ógild auðkenni"; "settings_add_3pid_password_title_msidsn" = "Bæta við símanúmeri"; "settings_add_3pid_password_title_email" = "Bæta við tölvupóstfangi"; @@ -1462,7 +1462,7 @@ "auth_reset_password_error_not_found" = "Tölvupóstfangið þitt lítur ekki út fyrir að vera tengt við Matrix-auðkenni á þessum heimaþjóni."; "auth_login_single_sign_on" = "Skrá inn"; "onboarding_use_case_existing_server_button" = "Tengjast þjóni"; -"onboarding_use_case_skip_button" = "sleppt þessari spurningu"; +"onboarding_use_case_skip_button" = "Sleppa þessari spurningu"; "onboarding_use_case_community_messaging" = "Samfélög"; "onboarding_use_case_work_messaging" = "Teymi"; "onboarding_use_case_personal_messaging" = "Vinir og fjölskylda"; @@ -1714,7 +1714,7 @@ "contacts_address_book_permission_denied_alert_title" = "Tengiliðir óvirkir"; "directory_search_results_title" = "Vafra í niðurstöðum möppu"; /* The placeholder string contains onboarding_use_case_skip_button as a tappable action */ -"onboarding_use_case_not_sure_yet" = "Ekki ennþá viss? Þú getur %@"; +"onboarding_use_case_not_sure_yet" = "Ekki ennþá viss? %@"; "call_invite_expired" = "Boð í símtal er útrunnið"; "notification_settings_suppress_from_bots" = "Útiloka tilkynningar frá vélmennum"; "notification_settings_sender_hint" = "@notandi:lén.is"; @@ -1966,7 +1966,7 @@ "security_settings_crosssigning_complete_security" = "Algjört öryggi"; "settings_key_backup_info" = "Öryggi dulritaðra skilaboða er tryggt með enda-í-enda dulritun. Einungis þú og viðtakendurnir hafa dulritunarlyklana til að lesa slík skilaboð."; "settings_labs_enable_ringing_for_group_calls" = "Hringing fyrir hópsímtöl"; -"onboarding_use_case_message" = "Við munum hjálpa þér að tengjast."; +"onboarding_use_case_message" = "Við munum hjálpa þér að tengjast"; "callbar_only_single_active_group" = "Ýttu til að taka þátt í hópsímtalinu (%@)"; "call_video_with_user" = "Myndsímtal við %@"; "notice_room_history_visible_to_members_from_joined_point_by_you_for_dm" = "Þú gerðir skilaboð héðan í frá sýnileg fyrir alla meðlimi spjallrásarinnar síðan þeir skráðu sig."; @@ -1980,7 +1980,7 @@ "location_sharing_live_timer_selector_long" = "í 8 klukkustundir"; "location_sharing_live_timer_selector_medium" = "í 1 klukkustund"; "location_sharing_live_timer_selector_short" = "í 15 mínútur"; -"location_sharing_live_list_item_stop_sharing_action" = "Hætta deilingu"; +"location_sharing_live_list_item_stop_sharing_action" = "Stöðva"; "location_sharing_live_list_item_current_user_display_name" = "Þú"; "location_sharing_live_list_item_last_update_invalid" = "Síðasta uppfærsla óþekkt"; "location_sharing_live_list_item_last_update" = "Uppfært fyrir %@ síðan"; @@ -2091,8 +2091,8 @@ "threads_beta_title" = "Spjallþræðir"; "threads_notice_done" = "Náði því"; "onboarding_celebration_button" = "Hefjumst handa"; -"onboarding_celebration_message" = "Kjörstillingarnar þínar hafa verið vistaðar."; -"onboarding_celebration_title" = "Nú ertu tilbúin(n)!"; +"onboarding_celebration_message" = "Farðu hvenær sem er í stillingarnar til að breyta notandasniðinu þínu."; +"onboarding_celebration_title" = "Lítur vel út!"; "onboarding_avatar_accessibility_label" = "Auðkennismynd"; "onboarding_avatar_message" = "Þú getur breytt þessu hvenær sem er."; "onboarding_avatar_title" = "Bættu við auðkennismynd"; @@ -2116,3 +2116,64 @@ "joining" = "Gengur til liðs við spjallrás"; // String for App Store "store_short_description" = "Öruggt dreifvinnsluspjall/VoIP"; +"device_name_unknown" = "Óþekkt forrit (biðlari)"; + +// First item is client name and second item is session display name +"user_session_name" = "%@: %@"; +"user_session_learn_more" = "Kanna nánar"; +"user_session_view_details" = "Skoða nánar"; +"user_session_verify_action" = "Sannprófa setu"; +"user_session_unverified_short" = "Óstaðfest"; +"user_session_verified_short" = "Staðfest"; +"user_session_unverified" = "Óstaðfest seta"; +"user_session_verified" = "Staðfest seta"; + +// MARK: User sessions management + +"user_sessions_overview_title" = "Setur"; +"space_selector_create_space" = "Búa til svæði"; +"all_chats_onboarding_try_it" = "Prófaðu það"; +"all_chats_edit_menu_space_settings" = "Stillingar svæðis"; +"all_chats_edit_menu_leave_space" = "Yfirgefa %@"; +"room_recents_recently_viewed_section" = "Nýlega skoðað"; +"all_chats_nothing_found_placeholder_title" = "Ekkert fannst."; +"all_chats_empty_list_placeholder_title" = "Þú hefur klárað allt."; +"all_chats_all_filter" = "Allt"; +"all_chats_edit_layout_show_filters" = "Sýna síur"; +"all_chats_edit_layout_show_recents" = "Birta nýlegt"; +"all_chats_edit_layout_unreads" = "Ólesið"; +"all_chats_edit_layout_recents" = "Nýlegt"; + +// Mark: Leave space + +"leave_space_action" = "Yfirgefa svæði"; +"spaces_creation_email_invites_title" = "Bjóddu félögum þínum"; +"spaces_creation_empty_room_name_error" = "Nafn er nauðsynlegt"; +"spaces_explore_rooms_format" = "Kanna %@"; +"room_info_back_button_title" = "Upplýsingar um spjallrás"; +"secrets_setup_recovery_passphrase_information" = "Settu inn öryggisfrasa sem aðeins þú þekkir, þetta er notað til að verja leyndarmálin sem þú geymir á netþjóninum þínum."; +"room_access_settings_screen_title" = "Hver hefur aðgang að þessari spjallrás?"; +"settings_presence_offline_mode" = "Ónettengdur hamur"; +"room_accessibility_record_voice_message" = "Taka upp talskilaboð"; +"authentication_recaptcha_title" = "Ertu mannvera?"; +"authentication_verify_msisdn_invalid_phone_number" = "Ógilt símanúmer"; +"authentication_verify_msisdn_waiting_button" = "Endursenda kóða"; +"authentication_verify_msisdn_text_field_placeholder" = "Símanúmer"; +"authentication_verify_msisdn_input_title" = "Settu inn símanúmerið þitt"; +"authentication_choose_password_not_verified_message" = "Athugaðu pósthólfið þitt"; +"authentication_choose_password_submit_button" = "Endurstilla lykilorð"; +"authentication_choose_password_text_field_placeholder" = "Nýtt lykilorð"; +"authentication_choose_password_input_title" = "Veldu nýtt lykilorð"; +"authentication_forgot_password_waiting_button" = "Senda tölvupóst aftur"; +"authentication_forgot_password_waiting_title" = "Athugaðu tölvupóstinn þinn."; +"authentication_forgot_password_text_field_placeholder" = "Tölvupóstur"; +"authentication_forgot_password_input_title" = "Settu inn tölvupóstfangið þitt"; +"authentication_verify_email_waiting_button" = "Senda tölvupóst aftur"; +"authentication_verify_email_waiting_title" = "Skoðaðu tölvupóstinn þinn."; +"authentication_verify_email_text_field_placeholder" = "Tölvupóstfang"; +"authentication_verify_email_input_title" = "Settu inn tölvupóstfangið þitt"; +"authentication_server_selection_server_url" = "Slóð á heimaþjón"; +"authentication_login_forgot_password" = "Gleymt lykilorð"; +"authentication_login_username" = "Notandanafn / tölvupóstfang / símanúmer"; +"authentication_login_title" = "Velkomin(n) aftur!"; +"authentication_registration_username" = "Notandanafn"; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 55b76e8c7..ec31cebb3 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -2465,3 +2465,25 @@ // User sessions management "user_sessions_settings" = "Gestisci sessioni"; "invite_to" = "Invita in %@"; +"device_name_unknown" = "Client sconosciuto"; +"device_name_mobile" = "%@ Mobile"; +"device_name_web" = "%@ Web"; +"device_name_desktop" = "%@ Desktop"; +"user_session_item_details" = "%@ · Ultima attività %@"; + +// First item is client name and second item is session display name +"user_session_name" = "%@: %@"; +"user_session_unverified_additional_info" = "Verifica la tua sessione attuale per messaggi più sicuri."; +"user_session_verified_additional_info" = "La tua sessione attuale è pronta per i messaggi sicuri."; +"user_session_learn_more" = "Maggiori informazioni"; +"user_session_view_details" = "Vedi dettagli"; +"user_session_verify_action" = "Verifica la sessione"; +"user_session_unverified_short" = "Non verificata"; +"user_session_verified_short" = "Verificata"; +"user_session_unverified" = "Sessione non verificata"; +"user_session_verified" = "Sessione verificata"; +"user_sessions_overview_current_session_section_title" = "SESSIONE ATTUALE"; +"user_sessions_overview_other_sessions_section_info" = "Per una maggiore sicurezza, verifica le tue sessioni e disconnetti quelle che non riconosci o che non usi più."; +"user_sessions_overview_other_sessions_section_title" = "ALTRE SESSIONI"; +"settings_labs_enable_new_app_layout" = "Nuova disposizione dell'applicazione"; +"room_first_message_placeholder" = "Invia il tuo primo messaggio…"; diff --git a/Riot/Assets/ja.lproj/Vector.strings b/Riot/Assets/ja.lproj/Vector.strings index ddd2b31e4..df4ef10d1 100644 --- a/Riot/Assets/ja.lproj/Vector.strings +++ b/Riot/Assets/ja.lproj/Vector.strings @@ -142,8 +142,8 @@ "contacts_address_book_matrix_users_toggle" = "Matrix利用者のみ"; "contacts_address_book_no_contact" = "端末内電話帳に連絡先がありません"; "contacts_address_book_permission_required" = "端末内電話帳へのアクセス権限が必要です"; -"contacts_user_directory_section" = "利用者一覧"; -"contacts_user_directory_offline_section" = "利用者一覧(オフライン)"; +"contacts_user_directory_section" = "ユーザー一覧"; +"contacts_user_directory_offline_section" = "ユーザー一覧 (オフライン)"; // Chat participants "room_participants_title" = "参加者"; "room_participants_add_participant" = "参加者を追加"; @@ -172,7 +172,7 @@ "room_participants_action_section_devices" = "セッション一覧"; "room_participants_action_section_other" = "オプション"; "room_participants_action_invite" = "招待"; -"room_participants_action_leave" = "このルームを出る"; +"room_participants_action_leave" = "このルームを退出"; "room_participants_action_remove" = "このルームから削除"; "room_participants_action_ban" = "このルームからブロック"; "room_participants_action_unban" = "ブロックを解除"; @@ -208,7 +208,7 @@ "room_delete_unsent_messages" = "未送信の文を削除"; "room_event_action_copy" = "コピー"; "room_event_action_quote" = "引用"; -"room_event_action_redact" = "移動して削除"; +"room_event_action_redact" = "削除"; "room_event_action_more" = "さらに"; "room_event_action_share" = "共有"; "room_event_action_permalink" = "メッセージへのリンクをコピー"; @@ -332,7 +332,7 @@ "room_details_low_priority_tag" = "低優先度"; "room_details_mute_notifs" = "発言があっても通知しない"; "room_details_direct_chat" = "対話"; -"room_details_access_section" = "ルームにアクセスできる人"; +"room_details_access_section" = "このルームにアクセスできる人は?"; "room_details_access_section_invited_only" = "招待された人のみ"; "room_details_access_section_anyone_apart_from_guest" = "ルームのリンクを知っている人なら誰でも(ゲストユーザーを除く)"; "room_details_access_section_anyone" = "ルームのリンクを知っている人なら誰でも(ゲストユーザーを含む)"; @@ -541,7 +541,7 @@ "deactivate_account_forget_messages_information_part3" = ":これは将来のユーザーに会話の不完全なビューが表示される)"; "deactivate_account_validate_action" = "無効なアカウント"; "deactivate_account_password_alert_title" = "無効なアカウント"; -"deactivate_account_password_alert_message" = "続行するには、パスワードを入力してください"; +"deactivate_account_password_alert_message" = "続行するには、Matrix アカウントのパスワードを入力してください"; // Re-request confirmation dialog "rerequest_keys_alert_title" = "要求が送信されました"; "rerequest_keys_alert_message" = "鍵をこのセッションに送信できるように、メッセージを復号化できる他の端末で%@を起動してください。"; @@ -565,12 +565,12 @@ "room_info_list_one_member" = "1人のメンバー"; "create_room_placeholder_address" = "#testroom:matrix.org"; -"create_room_section_header_address" = "ルームのアドレス"; -"create_room_show_in_directory" = "このルームを一覧に掲載"; +"create_room_section_header_address" = "アドレス"; +"create_room_show_in_directory" = "ルーム一覧に掲載"; "create_room_section_footer_type" = "非公開のルームは、ルームに招待された人のみ参加できます。"; "create_room_type_public" = "公開ルーム (誰でも参加可能)"; "create_room_type_private" = "非公開ルーム (招待者のみ参加可能)"; -"create_room_section_header_type" = "ルームの種類"; +"create_room_section_header_type" = "アクセスできる人"; "create_room_section_footer_encryption" = "暗号化はあとから無効にすることはできません。"; "create_room_section_header_encryption" = "ルームの暗号化"; "create_room_placeholder_topic" = "トピック"; @@ -674,7 +674,7 @@ "room_details_access_section_directory_toggle_for_dm" = "ルーム一覧に掲載"; "room_details_access_section_anyone_apart_from_guest_for_dm" = "リンクを知っている人なら誰でも(ゲストユーザーを除く)"; "room_details_access_section_anyone_for_dm" = "リンクを知っている人なら誰でも(ゲストユーザーを含む)"; -"room_details_access_section_for_dm" = "誰がアクセスできますか?"; +"room_details_access_section_for_dm" = "これにアクセスできる人は?"; "room_details_photo_for_dm" = "写真"; "room_details_integrations" = "インテグレーション"; "room_details_search" = "ルーム内検索"; @@ -707,7 +707,7 @@ "settings_key_backup_info_version" = "鍵のバックアップのバージョン:%@"; "settings_key_backup_info_none" = "あなたの鍵は、このセッションからバックアップされていません。"; "settings_key_backup_info_checking" = "確認しています…"; -"settings_add_3pid_password_message" = "続けるにはパスワードを入力してください"; +"settings_add_3pid_password_message" = "続行するには、Matrix アカウントのパスワードを入力してください"; "settings_add_3pid_invalid_password_message" = "無効な認証情報"; "settings_add_3pid_password_title_email" = "メールアドレスを追加"; "settings_integrations_allow_description" = "インテグレーションマネージャー(%@)を使用して、ボット、ブリッジ、ウィジェット、ステッカーパックを管理します。\n\n設定データを受け取り、お客様に代わってウィジェットの変更、ルーム招待の送信、権限の設定を行うことができます。"; @@ -735,7 +735,7 @@ "room_participants_action_security_status_warning" = "警告"; "room_participants_action_security_status_complete_security" = "セキュリティーを確認"; "room_participants_action_security_status_verify" = "認証"; -"room_participants_action_security_status_verified" = "認証済"; +"room_participants_action_security_status_verified" = "検証済み"; "room_participants_action_section_security" = "セキュリティー"; "room_participants_start_new_chat_error_using_user_email_without_identity_server" = "IDサーバーが設定されていないため、メールアドレスを使って連絡先とチャットを開始することができません。"; "room_participants_filter_room_members_for_dm" = "メンバーを検索"; @@ -870,7 +870,7 @@ "user_verification_session_details_additional_information_untrusted_other_user" = "ユーザーがこのセッションを信頼するまでは、セッションとの間で送受信されるメッセージには警告が表示されます。また、手動で認証することもできます。"; "user_verification_session_details_information_untrusted_other_user" = " 新しいセッションを使ってサインインしました:"; "user_verification_session_details_information_untrusted_current_user" = "このセッションを認証することで、信頼できるものとしてマークし、暗号化されたメッセージへのアクセスを許可します。"; -"user_verification_session_details_information_trusted_other_user_part2" = " 認証済:"; +"user_verification_session_details_information_trusted_other_user_part2" = " 検証済み:"; "user_verification_session_details_information_trusted_other_user_part1" = "このセッションは安全なものとして信頼されています。なぜなら "; "user_verification_session_details_information_trusted_current_user" = "このセッションは、認証されたため安全なものとして信頼されています。"; "user_verification_session_details_untrusted_title" = "信頼されていません"; @@ -928,7 +928,7 @@ "key_verification_incoming_request_incoming_alert_message" = "%@は認証を要求しています"; "key_verification_tile_conclusion_warning_title" = "信頼されていないサインイン"; -"key_verification_tile_conclusion_done_title" = "認証済"; +"key_verification_tile_conclusion_done_title" = "検証済み"; "key_verification_tile_request_incoming_approval_decline" = "却下"; "key_verification_tile_request_incoming_approval_accept" = "承認"; "key_verification_tile_request_status_accepted" = "あなたは承認しました"; @@ -1283,7 +1283,7 @@ "room_event_encryption_info_device_id" = "ID\n"; "room_event_encryption_info_device_verification" = "認証\n"; "room_event_encryption_info_device_fingerprint" = "Ed25519 fingerprint\n"; -"room_event_encryption_info_device_verified" = "認証済"; +"room_event_encryption_info_device_verified" = "検証済み"; "room_event_encryption_info_device_not_verified" = "認証されていない"; "room_event_encryption_info_device_blocked" = "ブラックリストに載せた"; "room_event_encryption_info_verify" = "認証しています…"; @@ -1512,7 +1512,7 @@ "notice_room_invite_by_you" = "%@を招待しました"; "notice_room_invite_you" = "%@があなたを招待しました"; "notice_room_join_by_you" = "参加しました"; -"notice_room_leave_by_you" = "退出しました"; +"notice_room_leave_by_you" = "あなたが退出しました"; "notice_room_kick_by_you" = "%@をキックしました"; "notice_room_unban_by_you" = "%@のブロックを解除しました"; "notice_room_ban_by_you" = "%@をブロックしました"; @@ -1587,7 +1587,7 @@ "biometrics_usage_reason" = "アプリを開くには認証が必要です"; "settings_sending_media" = "画像と動画の送信"; "invite_friends_share_text" = "%@ での連絡先: %@"; -"side_menu_action_invite_friends" = "招待する"; +"side_menu_action_invite_friends" = "友だちを招待する"; "call_more_actions_change_audio_device" = "オーディオデバイスを変更"; "call_more_actions_dialpad" = "ダイヤルパッド"; "onboarding_splash_login_button_title" = "既にアカウントを持っています"; @@ -1656,7 +1656,7 @@ "space_topic" = "説明文"; "spaces_creation_cancel_message" = "進捗状況は失われます。"; "spaces_creation_cancel_title" = "スペースの作成を停止しますか?"; -"create_room_section_footer_type_private" = "招待した人のみが検索や参加できます。"; +"create_room_section_footer_type_private" = "招待した人のみが検索・参加できます。"; // MARK: - Searchable Directory View Controller @@ -1668,3 +1668,128 @@ // MARK: Sign out warning "sign_out_existing_key_backup_alert_title" = "サインアウトしてよろしいですか?"; +"find_your_contacts_message" = "%@ であなたの連絡先を表示し、知人とのチャットを素早く始めます。"; +"find_your_contacts_footer" = "この設定はいつでも無効にできます"; +"find_your_contacts_button_title" = "連絡先を検索する"; +"find_your_contacts_title" = "連絡先をリストアップする"; + +// Mark: - Room avatar view + +"room_avatar_view_accessibility_label" = "アバター"; + +// MARK: - Invite friends + +"invite_friends_action" = "友だちを %@ に招待する"; +"call_transfer_error_title" = "エラー"; +"home_context_menu_mark_as_read" = "既読にする"; +"home_context_menu_normal_priority" = "通常優先度"; +"home_context_menu_low_priority" = "低優先度"; +"home_context_menu_unmute" = "ミュート解除"; +"home_context_menu_mute" = "ミュート"; + +// MARK: - Call Transfer +"call_transfer_title" = "転送"; +"room_info_back_button_title" = "ルーム情報"; +"create_room_processing" = "ルーム作成中"; +"call_transfer_users" = "ユーザー"; +"home_context_menu_notifications" = "通知"; +"home_context_menu_make_dm" = "連絡先に移動"; +"home_context_menu_make_room" = "ルームに移動"; +"leave_space_title" = "%@ を退出"; +"room_participants_leave_success" = "ルームを退出しました"; +"room_participants_leave_processing" = "退出中"; +"event_formatter_group_call_leave" = "退出"; +"home_context_menu_leave" = "退出"; + +// Mark: Leave space + +"leave_space_action" = "スペースを退出"; +"leave_space_selection_title" = "ルームを選択"; +"create_room_section_footer_type_restricted" = "誰でもスペース名で検索・参加できます。"; +"create_room_suggest_room" = "スペースメンバーにおすすめ"; +"create_room_show_in_directory_footer" = "他の人が検索・参加できるようになります。"; +"create_room_promotion_header" = "PR"; +"searchable_directory_search_placeholder" = "名前または ID"; +"room_suggestion_settings_screen_title" = "スペースにおすすめのルームを作成"; +"room_suggestion_settings_screen_message" = "おすすめのルームは、スペースメンバーに参加を推奨するものとして PR されます。"; + +// Room suggestion Settings +"room_suggestion_settings_screen_nav_title" = "おすすめのルーム"; +"room_details_promote_room_suggest_title" = "スペースメンバーへのおすすめ"; +"settings_default" = "デフォルトの通知"; +"pin_protection_reset_alert_action_reset" = "リセット"; +"authentication_recaptcha_title" = "あなたは人間ですか?"; +"authentication_verify_msisdn_waiting_button" = "コードを再送信"; +"authentication_choose_password_submit_button" = "パスワードをリセット"; +"authentication_choose_password_signout_all_devices" = "全ての端末からサインアウト"; +"authentication_choose_password_text_field_placeholder" = "新しいパスワード"; +"authentication_terms_title" = "プライバシーポリシー"; +"authentication_verify_msisdn_text_field_placeholder" = "電話番号"; +"password_validation_error_contain_symbol" = "記号を含める"; +"password_validation_error_contain_number" = "数字を含める"; +"password_validation_error_contain_uppercase_letter" = "大文字を含める"; +"password_validation_error_contain_lowercase_letter" = "小文字を含める"; +/* The placeholder will show a number */ +"password_validation_error_max_length" = "%d 文字以下"; +/* The placeholder will show a number */ +"password_validation_error_min_length" = "%d 文字以上"; + +// MARK: Password Validation +"password_validation_info_header" = "以下の条件を満たすパスワードを設定してください:"; +"space_selector_empty_view_title" = "まだスペースがありません"; +"all_chats_empty_list_placeholder_title" = "未読はありません"; +"all_chats_empty_unreads_placeholder_message" = "未読のメッセージがある場合は、ここに表示されます。"; +"room_notifs_settings_account_settings" = "アカウント設定"; +"room_access_settings_screen_upgrade_alert_upgrading" = "ルームをアップグレードしています"; +"room_access_settings_screen_upgrade_alert_upgrade_button" = "アップグレード"; +"room_access_settings_screen_edit_spaces" = "スペースを編集"; +"room_access_settings_screen_upgrade_required" = "アップグレードが必要"; +"room_access_settings_screen_upgrade_alert_title" = "ルームをアップグレード"; +"room_access_settings_screen_public_message" = "誰でも検索・参加できます。"; +"room_access_settings_screen_private_message" = "招待された人だけが検索・参加できます。"; +"room_access_settings_screen_message" = "誰が %@ を検索・参加できるか選択してください。"; +"space_settings_access_section" = "このスペースにアクセスできる人は?"; +"room_access_settings_screen_title" = "このルームにアクセスできる人は?"; +"room_notifs_settings_none" = "なし"; + +// Room Notification Settings +"room_notifs_settings_notify_me_for" = "以下の場合に通知"; +"room_details_access_row_title" = "アクセス"; +"room_details_notifs" = "通知"; +"location_sharing_invalid_power_level_title" = "ライブ現在地共有に必要な権限がありません"; +"settings_labs_enable_live_location_sharing" = "ライブ現在地共有 - 現在地を共有します (開発中の機能、位置情報が一時的にルームの履歴に残ります)"; +"event_formatter_message_deleted" = "削除済みのメッセージ"; +"home_context_menu_unfavourite" = "お気に入りから削除"; +"home_context_menu_favourite" = "お気に入り"; +"all_chats_user_menu_settings" = "ユーザー設定"; +"all_chats_edit_layout_show_filters" = "フィルターを表示"; +"all_chats_edit_layout_show_recents" = "最近使用したものを表示"; +"all_chats_edit_layout_alphabetical_order" = "A-Z で並び替え"; +"all_chats_edit_layout_activity_order" = "アクティビティで並び替え"; +"space_selector_create_space" = "スペースを作成"; +"space_selector_empty_view_information" = "スペースは、ルームや連絡先をグループ化する方法です。以下からスペースを作成できます。"; +"all_chats_all_filter" = "全て"; +"all_chats_edit_layout_recents" = "最近"; +"all_chats_edit_layout_unreads" = "未読"; +"all_chats_section_title" = "チャット"; + +// Mark: - All Chats + +"all_chats_title" = "全てのチャット"; +"location_sharing_live_loading" = "ライブ現在地共有を読み込み中..."; +"location_sharing_live_list_item_stop_sharing_action" = "停止"; +"location_sharing_live_list_item_current_user_display_name" = "あなた"; +"live_location_sharing_banner_stop" = "停止"; +"live_location_sharing_ended" = "ライブ現在地共有が終了"; +"live_location_sharing_banner_title" = "ライブ現在地共有が有効"; +"location_sharing_invalid_authorization_settings" = "設定"; + +// MARK: Live location sharing + +"location_sharing_live_share_title" = "ライブ現在地共有"; +"service_terms_modal_decline_button" = "拒否する"; +"service_terms_modal_accept_button" = "同意する"; +"service_terms_modal_description_identity_server" = "この操作により、端末の連絡先にあなたの電話番号や電子メールを保存している人があなたを検索できるようになります。"; + +// Service terms +"service_terms_modal_title_message" = "続行するには、以下の利用規約に同意してください"; diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index ca607998b..362dd9620 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -2230,7 +2230,7 @@ "settings_labs_enable_auto_report_decryption_errors" = "Auto Reportar Erros de Decriptação"; "room_preview_decline_invitation_options" = "Você quer declinar o convite ou ignorar esta(e) usuária(o)?"; "threads_beta_cancel" = "Não agora"; -"threads_beta_enable" = "Teste aí"; +"threads_beta_enable" = "Experimentar"; "threads_beta_information_link" = "Saber mais"; "threads_beta_information" = "Mantenha discussões organizadas com threads.\n\nThreads ajudam manter suas conversas em-tópico e fáceis de rastrear. "; "threads_beta_title" = "Threads"; @@ -2446,12 +2446,12 @@ // Mark: - Room invites "room_invites_empty_view_title" = "Nada novo."; -"all_chats_onboarding_try_it" = "Testa aí"; +"all_chats_onboarding_try_it" = "Experimentar"; "all_chats_onboarding_title" = "O que tem de novo"; "all_chats_onboarding_page_message3" = "Toque em seu perfil para nos deixar sabendo do que você acha."; -"all_chats_onboarding_page_title3" = "Dar Feedback"; -"all_chats_onboarding_page_message2" = "Acesse seus Espaços (esquerdo fundo) mais rápido e fácil que jamais antes."; -"all_chats_onboarding_page_title2" = "Acessar Espaços"; +"all_chats_onboarding_page_title3" = "Dê Feedback"; +"all_chats_onboarding_page_message2" = "Acesse seus Espaços (esquerda fundo) mais rápido e fácil que jamais antes."; +"all_chats_onboarding_page_title2" = "Acesse Espaços"; "all_chats_onboarding_page_message1" = "Para simplificar seu Element, abas são agora opcionais. Gerencie-as usando o menu direito topo."; "all_chats_onboarding_page_title1" = "Boas vindas a uma nova visão!"; "all_chats_nothing_found_placeholder_message" = "Tente ajustar sua pesquisa."; @@ -2466,3 +2466,25 @@ // User sessions management "user_sessions_settings" = "Gerenciar sessões"; "invite_to" = "Convidar para %@"; +"device_name_unknown" = "Cliente desconhecido"; +"device_name_mobile" = "%@ Mobile"; +"device_name_web" = "%@ Web"; +"device_name_desktop" = "%@ Desktop"; +"user_session_item_details" = "%@ · Última atividade %@"; + +// First item is client name and second item is session display name +"user_session_name" = "%@: %@"; +"user_session_unverified_additional_info" = "Verifique sua sessão atual para mensageria segura melhorada."; +"user_session_verified_additional_info" = "Sua sessão atual está pronta para mensageria segura."; +"user_session_learn_more" = "Saber mais"; +"user_session_view_details" = "Visualizar detalhes"; +"user_session_verify_action" = "Verificar sessão"; +"user_session_unverified_short" = "Não-verificada"; +"user_session_verified_short" = "Verificada"; +"user_session_unverified" = "Sessão não-verificada"; +"user_session_verified" = "Sessão verificada"; +"user_sessions_overview_current_session_section_title" = "SESSÃO ATUAL"; +"user_sessions_overview_other_sessions_section_info" = "Para melhor segurança, verifique suas sessões e faça signout de qualquer sessão que você não reconhece ou usa mais."; +"user_sessions_overview_other_sessions_section_title" = "OUTRAS SESSÕES"; +"settings_labs_enable_new_app_layout" = "Novo Layout de Aplicativo"; +"room_first_message_placeholder" = "Envie sua primeira mensagem…"; diff --git a/Riot/Assets/sk.lproj/Vector.strings b/Riot/Assets/sk.lproj/Vector.strings index ecfcb9734..5cecf24a5 100644 --- a/Riot/Assets/sk.lproj/Vector.strings +++ b/Riot/Assets/sk.lproj/Vector.strings @@ -2688,3 +2688,25 @@ // User sessions management "user_sessions_settings" = "Spravovať relácie"; "invite_to" = "Pozvať do %@"; +"device_name_web" = "%@ Web"; +"device_name_unknown" = "Neznámy klient"; +"device_name_mobile" = "%@ Mobil"; +"device_name_desktop" = "%@ Stolný počítač"; +"user_session_item_details" = "%@ · Posledná aktivita %@"; + +// First item is client name and second item is session display name +"user_session_name" = "%@: %@"; +"user_session_unverified_additional_info" = "Overte svoju aktuálnu reláciu pre vylepšené bezpečné zasielanie správ."; +"user_session_verified_additional_info" = "Vaša aktuálna relácia je pripravená na bezpečné zasielanie správ."; +"user_session_learn_more" = "Zistiť viac"; +"user_session_view_details" = "Zobraziť podrobnosti"; +"user_session_verify_action" = "Overiť reláciu"; +"user_session_unverified_short" = "Neoverené"; +"user_session_verified_short" = "Overené"; +"user_session_unverified" = "Neoverená relácia"; +"user_session_verified" = "Overená relácia"; +"user_sessions_overview_current_session_section_title" = "AKTUÁLNA RELÁCIA"; +"user_sessions_overview_other_sessions_section_info" = "V záujme čo najlepšieho zabezpečenia overte svoje relácie a odhláste sa z každej relácie, ktorú už nepoznáte alebo nepoužívate."; +"user_sessions_overview_other_sessions_section_title" = "OSTATNÉ RELÁCIE"; +"settings_labs_enable_new_app_layout" = "Nové usporiadanie aplikácie"; +"room_first_message_placeholder" = "Pošlite svoju prvú správu…"; diff --git a/Riot/Assets/third_party_licenses.html b/Riot/Assets/third_party_licenses.html index a9365018f..ebd5e6b11 100644 --- a/Riot/Assets/third_party_licenses.html +++ b/Riot/Assets/third_party_licenses.html @@ -1908,6 +1908,32 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+
  • + DeviceKit (https://github.com/devicekit/DeviceKit) +

    + MIT License +

    + Copyright (c) 2015 Dennis Weissmann +

    + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: +

    + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. +

    + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +

    +
  • diff --git a/Riot/Assets/uk.lproj/Vector.strings b/Riot/Assets/uk.lproj/Vector.strings index 783f461a6..4d561fe3d 100644 --- a/Riot/Assets/uk.lproj/Vector.strings +++ b/Riot/Assets/uk.lproj/Vector.strings @@ -227,7 +227,7 @@ "room_directory_no_public_room" = "Загальнодоступних кімнат немає"; "settings_security" = "БЕЗПЕКА"; "settings_pin_rooms_with_missed_notif" = "Приколоти кімнати із пропущеними сповіщеннями"; -"settings_global_settings_info" = "Глобальні налаштування сповіщень доступні у вашому %@ веб-клієнті"; +"settings_global_settings_info" = "Глобальні налаштування сповіщень доступні у вашому %@ вебклієнті"; "settings_enable_push_notif" = "Сповіщення на цьому пристрої"; "settings_fail_to_update_profile" = "Не вдалося оновити профіль"; "settings_night_mode" = "Нічний режим"; @@ -684,7 +684,7 @@ "space_beta_announce_badge" = "БЕТА"; "space_beta_announce_title" = "Простори скоро з'являться"; "space_beta_announce_subtitle" = "Нова версія спільнот"; -"space_beta_announce_information" = "Простори — це новий спосіб об'єднати кімнати та людей. Їх ще не додано на iOS, але ви вже можете користуватися ними у вебпереглядачі та на комп'ютері."; +"space_beta_announce_information" = "Простори — це новий спосіб об'єднати кімнати та людей. Їх ще не додано на iOS, але ви вже можете користуватися ними у браузері та на комп'ютері."; // No VoIP support "no_voip_title" = "Вхідний виклик"; @@ -1066,7 +1066,7 @@ "room_participants_start_new_chat_error_using_user_email_without_identity_server" = "Поки жоден сервер ідентифікації не налаштований, ви не можете почати бесіду з кимось за адресою е-пошти."; "find_your_contacts_message" = "Дозвольте %@ показувати ваші контакти, щоб ви могли швидко почати бесіду з тими, кого знаєте найкраще."; "find_your_contacts_title" = "Почніть із переліку своїх контактів"; -"store_full_description" = "Element — застосунок для листування й співпраці нового покоління:\n\n1. Надає вам контроль над збереженням вашої приватності\n2. Дає змогу спілкуватися з будь-ким у мережі Matrix і навіть за її межами, інтегруючись із такими застосунками, як Slack\n3. Оберігає вас від реклами, збору даних, бекдорів і прив'язаності до провайдера\n4. Захищає вас наскрізним шифруванням і звіркою інших перехресним підписуванням\n\nElement суттєво відрізняється від інших застосунків для листування й співпраці тим, що децентралізований і має відкритий код.\n\nElement дає змогу самостійно встановити сервер або обрати з-поміж загальнодоступних, щоб ви зберігали приватність своїх даних і розмов, власність і контроль над ними. Він надає вам доступ до відкритої мережі; тож ви можете спілкуватися з користувачами інших застосунків, не лише Element. А ще він добре захищений.\n\nElement здатен на це все завдяки своїй основі Matrix — стандарту відкритого, децентралізованого спілкування.\n\nElement надає вам контроль, даючи змогу обрати, в кого зберігаються ваші розмови. У застосунку Element ви можете обрати між такими шляхами:\n\n1. Зареєструвати безплатний обліковий запис на загальнодоступному сервері matrix.org\n2. Самостійно розмістити свій обліковий запис, встановивши сервер на власному обладнанні\n3. Отримати обліковий запис на виділеному сервері, просто передплативши хостинг-платформу Element Matrix Services\n\nЧому Element?\n\nВОЛОДІЙТЕ СВОЇМИ ДАНИМИ: Ви обираєте, де зберігати свої дані й повідомлення. Ви володієте й керуєте ними, не якась МЕГАКОРПОРАЦІЯ, що аналізує ваші дані й передає їх стороннім особам.\n\nВІДКРИТЕ ЛИСТУВАННЯ Й СПІВПРАЦЯ: Можете розмовляти з будь-ким іншим у мережі Matrix незалежно від того, використовують вони Element, інший застосунок Matrix чи навіть сторонню систему листування на зразок Slack, IRC чи XMPP.\n\nНАДБЕЗПЕКА: Справжнє наскрізне шифрування (лише учасники розмови можуть розшифрувати повідомлення) й звірка пристроїв учасників розмови перехресним підписуванням.\n\nДОСКОНАЛЕ СПІЛКУВАННЯ: Листуйтеся, робіть голосові й відеовиклики, діліться файлами, транслюйте екран, підʼєднуйте різноманітні інтеграції, ботів і віджети. Розбудовуйте кімнати, спільноти, будьте на звʼязку й досягайте цілей.\n\nСКРІЗЬ, ДЕ ВИ: Будьте на зв'язку, де б ви не були, завдяки повній синхронізації історії повідомлень між усіма вашим пристроями та онлайн-клієнтом https://app.element.io."; +"store_full_description" = "Element — застосунок для листування й співпраці нового покоління:\n\n1. Надає вам контроль над збереженням вашої приватності\n2. Дає змогу спілкуватися з будь-ким у мережі Matrix і навіть за її межами, інтегруючись із такими застосунками, як Slack\n3. Оберігає вас від реклами, збору даних, бекдорів і прив'язаності до провайдера\n4. Захищає вас наскрізним шифруванням і звіркою інших перехресним підписуванням\n\nElement суттєво відрізняється від інших застосунків для листування й співпраці тим, що децентралізований і має відкритий код.\n\nElement дає змогу самостійно встановити сервер або обрати з-поміж загальнодоступних, щоб ви зберігали приватність своїх даних і розмов, власність і контроль над ними. Він надає вам доступ до відкритої мережі; тож ви можете спілкуватися з користувачами інших застосунків, не лише Element. А ще він добре захищений.\n\nElement здатен на це все завдяки своїй основі Matrix — стандарту відкритого, децентралізованого спілкування.\n\nElement надає вам контроль, даючи змогу обрати, в кого зберігаються ваші розмови. У застосунку Element ви можете обрати між такими шляхами:\n\n1. Зареєструвати безплатний обліковий запис на загальнодоступному сервері matrix.org\n2. Самостійно розмістити свій обліковий запис, встановивши сервер на власному обладнанні\n3. Отримати обліковий запис на виділеному сервері, просто передплативши хостинг-платформу Element Matrix Services\n\nЧому Element?\n\nВОЛОДІЙТЕ СВОЇМИ ДАНИМИ: Ви обираєте, де зберігати свої дані й повідомлення. Ви володієте й керуєте ними, не якась МЕГАКОРПОРАЦІЯ, що аналізує ваші дані й передає їх стороннім особам.\n\nВІДКРИТЕ ЛИСТУВАННЯ Й СПІВПРАЦЯ: Можете розмовляти з будь-ким іншим у мережі Matrix незалежно від того, використовують вони Element, інший застосунок Matrix чи навіть сторонню систему листування на зразок Slack, IRC чи XMPP.\n\nНАДБЕЗПЕКА: Справжнє наскрізне шифрування (лише учасники розмови можуть розшифрувати повідомлення) й звірка пристроїв учасників розмови перехресним підписуванням.\n\nДОСКОНАЛЕ СПІЛКУВАННЯ: Листуйтеся, робіть голосові й відеовиклики, діліться файлами, транслюйте екран, підʼєднуйте різноманітні інтеграції, ботів і віджети. Розбудовуйте кімнати, спільноти, будьте на звʼязку й досягайте цілей.\n\nСКРІЗЬ, ДЕ ВИ: Будьте на зв'язку, де б ви не були, завдяки повній синхронізації історії повідомлень між усіма вашими пристроями та вебклієнтом https://app.element.io."; "security_settings_crosssigning_info_not_bootstrapped" = "Перехресне підписування ще не налаштовано."; "security_settings_crosssigning" = "ПЕРЕХРЕСНЕ ПІДПИСУВАННЯ"; "security_settings_backup" = "РЕЗЕРВНЕ КОПІЮВАННЯ ПОВІДОМЛЕНЬ"; @@ -1386,7 +1386,7 @@ "room_intro_cell_information_room_without_topic_sentence2_part2" = " щоб люди знали, про що ця кімната."; "room_intro_cell_information_dm_sentence2" = "У цій розмові лише ви двоє, більше ніхто не може приєднатися."; "room_intro_cell_information_multiple_dm_sentence2" = "У цій розмові лише ви, поки хтось із вас не запросить іще когось приєднатися."; -"space_feature_unavailable_subtitle" = "Простори ще не готові на iOS, але доступні у вебпереглядачі та на комп'ютері"; +"space_feature_unavailable_subtitle" = "Простори ще не готові на iOS, але доступні у браузері та на комп'ютері"; "space_feature_unavailable_information" = "Простори — новий спосіб групувати кімнати й людей.\n\nСкоро ми їх допрацюємо. Якщо приєднаєтеся до простору на іншій платформі, всі його кімнати стануть доступні й тут."; // Success from secure backup @@ -1408,7 +1408,7 @@ "spaces_suggested_room" = "Пропоновано"; "room_details_access_section_anyone_for_dm" = "Будь-хто з посиланням, зокрема гості"; "spaces_no_room_found_detail" = "Деяких результатів може бути не видно, бо вони закриті й потребують запрошення."; -"spaces_no_member_found_detail" = "Шукаєте когось, хто ще не в %@? Запросіть їх вебпереглядачем або комп'ютером."; +"spaces_no_member_found_detail" = "Шукаєте когось, хто ще не в %@? Запросіть їх через браузер або комп'ютер."; "spaces_invites_coming_soon_title" = "Запрошення ще в розробці"; "spaces_coming_soon_detail" = "Ця можливість ще не втілена, але скоро буде. Поки що можете зробити це в %@ на комп'ютері."; "space_participants_action_remove" = "Вилучити з цього простору"; @@ -1560,7 +1560,7 @@ "widget_integration_manager_disabled" = "Необхідно увімкнути менеджер інтеграцій у налаштуваннях"; "widget_menu_remove" = "Вилучити для всіх"; "widget_menu_revoke_permission" = "Відкликати мій доступ"; -"widget_menu_open_outside" = "Відкрити у переглядачі"; +"widget_menu_open_outside" = "Відкрити у браузері"; "widget_menu_refresh" = "Оновити"; "widget_sticker_picker_no_stickerpacks_alert_add_now" = "Додати зараз?"; "widget_sticker_picker_no_stickerpacks_alert" = "На разі жоден пакунок наліпок не увімкнено."; @@ -2690,3 +2690,25 @@ // User sessions management "user_sessions_settings" = "Керувати сеансами"; "invite_to" = "Запросити до %@"; +"device_name_unknown" = "Невідомий клієнт"; +"device_name_mobile" = "%@ Мобільний"; +"device_name_web" = "%@ Браузер"; +"device_name_desktop" = "%@ Комп'ютер"; +"user_session_item_details" = "%@ · Остання активність %@"; + +// First item is client name and second item is session display name +"user_session_name" = "%@: %@"; +"user_session_unverified_additional_info" = "Звірте свій поточний сеанс для посилення безпеки обміну повідомленнями."; +"user_session_verified_additional_info" = "Ваш поточний сеанс готовий до безпечного обміну повідомленнями."; +"user_session_learn_more" = "Докладніше"; +"user_session_view_details" = "Переглянути подробиці"; +"user_session_verify_action" = "Звірити сеанс"; +"user_session_unverified_short" = "Не звірений"; +"user_session_verified_short" = "Звірений"; +"user_session_unverified" = "Не звірений сеанс"; +"user_session_verified" = "Звірений сеанс"; +"user_sessions_overview_current_session_section_title" = "ПОТОЧНИЙ СЕАНС"; +"user_sessions_overview_other_sessions_section_info" = "Звірте свої сеанси та вийдіть з усіх сеансів, які ви більше не розпізнаєте або не використовуєте для кращої безпеки."; +"user_sessions_overview_other_sessions_section_title" = "ІНШІ СЕАНСИ"; +"settings_labs_enable_new_app_layout" = "Новий вигляд застосунку"; +"room_first_message_placeholder" = "Надішліть своє перше повідомлення…"; diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index eab6d4cbe..7c462f71d 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -117,11 +117,14 @@ internal class Asset: NSObject { internal static let deviceTypeWeb = ImageAsset(name: "device_type_web") internal static let userSessionUnverified = ImageAsset(name: "user_session_unverified") internal static let userSessionVerified = ImageAsset(name: "user_session_verified") + internal static let userSessionsInactive = ImageAsset(name: "user_sessions_inactive") + internal static let userSessionsUnverified = ImageAsset(name: "user_sessions_unverified") internal static let e2eBlocked = ImageAsset(name: "e2e_blocked") internal static let e2eUnencrypted = ImageAsset(name: "e2e_unencrypted") internal static let e2eWarning = ImageAsset(name: "e2e_warning") internal static let encryptionNormal = ImageAsset(name: "encryption_normal") internal static let encryptionTrusted = ImageAsset(name: "encryption_trusted") + internal static let encryptionUntrusted = ImageAsset(name: "encryption_untrusted") internal static let encryptionWarning = ImageAsset(name: "encryption_warning") internal static let favouritesEmptyScreenArtwork = ImageAsset(name: "favourites_empty_screen_artwork") internal static let favouritesEmptyScreenArtworkDark = ImageAsset(name: "favourites_empty_screen_artwork_dark") @@ -133,6 +136,7 @@ internal class Asset: NSObject { internal static let roomActionPriorityHigh = ImageAsset(name: "room_action_priority_high") internal static let roomActionPriorityLow = ImageAsset(name: "room_action_priority_low") internal static let roomActionResttime = ImageAsset(name: "room_action_resttime") + internal static let allChatsEditIcon = ImageAsset(name: "all_chats_edit_icon") internal static let allChatsEmptyListPlaceholderIcon = ImageAsset(name: "all_chats_empty_list_placeholder_icon") internal static let allChatsEmptyScreenArtwork = ImageAsset(name: "all_chats_empty_screen_artwork") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 42a3929a0..f8c0cc121 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -187,7 +187,7 @@ public class VectorL10n: NSObject { public static var allChatsEmptySpaceInformation: String { return VectorL10n.tr("Vector", "all_chats_empty_space_information") } - /// This is where you're unread messages will show up, when you have some. + /// This is where your unread messages will show up, when you have some. public static var allChatsEmptyUnreadsPlaceholderMessage: String { return VectorL10n.tr("Vector", "all_chats_empty_unreads_placeholder_message") } @@ -247,6 +247,10 @@ public class VectorL10n: NSObject { public static var allChatsTitle: String { return VectorL10n.tr("Vector", "all_chats_title") } + /// User menu + public static var allChatsUserMenuAccessibilityLabel: String { + return VectorL10n.tr("Vector", "all_chats_user_menu_accessibility_label") + } /// User settings public static var allChatsUserMenuSettings: String { return VectorL10n.tr("Vector", "all_chats_user_menu_settings") @@ -5623,6 +5627,10 @@ public class VectorL10n: NSObject { public static var roomEventEncryptionInfoEventUserId: String { return VectorL10n.tr("Vector", "room_event_encryption_info_event_user_id") } + /// The authenticity of this encrypted message can't be guaranteed on this device. + public static var roomEventEncryptionInfoKeyAuthenticityNotGuaranteed: String { + return VectorL10n.tr("Vector", "room_event_encryption_info_key_authenticity_not_guaranteed") + } /// End-to-end encryption information\n\n public static var roomEventEncryptionInfoTitle: String { return VectorL10n.tr("Vector", "room_event_encryption_info_title") @@ -7375,6 +7383,14 @@ public class VectorL10n: NSObject { public static var settingsLabsEnableNewAppLayout: String { return VectorL10n.tr("Vector", "settings_labs_enable_new_app_layout") } + /// Record the client name, version, and url to recognise sessions more easily in session manager + public static var settingsLabsEnableNewClientInfoFeature: String { + return VectorL10n.tr("Vector", "settings_labs_enable_new_client_info_feature") + } + /// New session manager + public static var settingsLabsEnableNewSessionManager: String { + return VectorL10n.tr("Vector", "settings_labs_enable_new_session_manager") + } /// Ring for group calls public static var settingsLabsEnableRingingForGroupCalls: String { return VectorL10n.tr("Vector", "settings_labs_enable_ringing_for_group_calls") @@ -8467,18 +8483,106 @@ public class VectorL10n: NSObject { public static var userIdTitle: String { return VectorL10n.tr("Vector", "user_id_title") } + /// Name + public static var userSessionDetailsApplicationName: String { + return VectorL10n.tr("Vector", "user_session_details_application_name") + } + /// Application + public static var userSessionDetailsApplicationSectionHeader: String { + return VectorL10n.tr("Vector", "user_session_details_application_section_header") + } + /// URL + public static var userSessionDetailsApplicationUrl: String { + return VectorL10n.tr("Vector", "user_session_details_application_url") + } + /// Version + public static var userSessionDetailsApplicationVersion: String { + return VectorL10n.tr("Vector", "user_session_details_application_version") + } + /// Browser + public static var userSessionDetailsDeviceBrowser: String { + return VectorL10n.tr("Vector", "user_session_details_device_browser") + } + /// IP address + public static var userSessionDetailsDeviceIpAddress: String { + return VectorL10n.tr("Vector", "user_session_details_device_ip_address") + } + /// IP location + public static var userSessionDetailsDeviceIpLocation: String { + return VectorL10n.tr("Vector", "user_session_details_device_ip_location") + } + /// Model + public static var userSessionDetailsDeviceModel: String { + return VectorL10n.tr("Vector", "user_session_details_device_model") + } + /// Operating System + public static var userSessionDetailsDeviceOs: String { + return VectorL10n.tr("Vector", "user_session_details_device_os") + } + /// Device + public static var userSessionDetailsDeviceSectionHeader: String { + return VectorL10n.tr("Vector", "user_session_details_device_section_header") + } + /// Session ID + public static var userSessionDetailsSessionId: String { + return VectorL10n.tr("Vector", "user_session_details_session_id") + } + /// Session name + public static var userSessionDetailsSessionName: String { + return VectorL10n.tr("Vector", "user_session_details_session_name") + } + /// Copy any data by tapping on it and holding it down. + public static var userSessionDetailsSessionSectionFooter: String { + return VectorL10n.tr("Vector", "user_session_details_session_section_footer") + } + /// Session + public static var userSessionDetailsSessionSectionHeader: String { + return VectorL10n.tr("Vector", "user_session_details_session_section_header") + } + /// Session details + public static var userSessionDetailsTitle: String { + return VectorL10n.tr("Vector", "user_session_details_title") + } /// %@ · Last activity %@ public static func userSessionItemDetails(_ p1: String, _ p2: String) -> String { return VectorL10n.tr("Vector", "user_session_item_details", p1, p2) } + /// Learn more + public static var userSessionLearnMore: String { + return VectorL10n.tr("Vector", "user_session_learn_more") + } /// %@: %@ public static func userSessionName(_ p1: String, _ p2: String) -> String { return VectorL10n.tr("Vector", "user_session_name", p1, p2) } + /// Current session + public static var userSessionOverviewCurrentSessionTitle: String { + return VectorL10n.tr("Vector", "user_session_overview_current_session_title") + } + /// Session details + public static var userSessionOverviewSessionDetailsButtonTitle: String { + return VectorL10n.tr("Vector", "user_session_overview_session_details_button_title") + } + /// Session + public static var userSessionOverviewSessionTitle: String { + return VectorL10n.tr("Vector", "user_session_overview_session_title") + } + /// Push notifications + public static var userSessionPushNotifications: String { + return VectorL10n.tr("Vector", "user_session_push_notifications") + } + /// When turned on, this session will receive push notifications. + public static var userSessionPushNotificationsMessage: String { + return VectorL10n.tr("Vector", "user_session_push_notifications_message") + } /// Unverified session public static var userSessionUnverified: String { return VectorL10n.tr("Vector", "user_session_unverified") } + /// Verify your current session for enhanced secure messaging. + public static var userSessionUnverifiedAdditionalInfo: String { + return VectorL10n.tr("Vector", "user_session_unverified_additional_info") + } /// Unverified public static var userSessionUnverifiedShort: String { return VectorL10n.tr("Vector", "user_session_unverified_short") @@ -8487,18 +8591,58 @@ public class VectorL10n: NSObject { public static var userSessionVerified: String { return VectorL10n.tr("Vector", "user_session_verified") } + /// Your current session is ready for secure messaging. + public static var userSessionVerifiedAdditionalInfo: String { + return VectorL10n.tr("Vector", "user_session_verified_additional_info") + } /// Verified public static var userSessionVerifiedShort: String { return VectorL10n.tr("Vector", "user_session_verified_short") } + /// Verify session + public static var userSessionVerifyAction: String { + return VectorL10n.tr("Vector", "user_session_verify_action") + } + /// View details + public static var userSessionViewDetails: String { + return VectorL10n.tr("Vector", "user_session_view_details") + } + /// Current session + public static var userSessionsOverviewCurrentSessionSectionTitle: String { + return VectorL10n.tr("Vector", "user_sessions_overview_current_session_section_title") + } /// For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore. public static var userSessionsOverviewOtherSessionsSectionInfo: String { return VectorL10n.tr("Vector", "user_sessions_overview_other_sessions_section_info") } - /// OTHER SESSIONS + /// Other sessions public static var userSessionsOverviewOtherSessionsSectionTitle: String { return VectorL10n.tr("Vector", "user_sessions_overview_other_sessions_section_title") } + /// Consider signing out from old sessions (90 days or older) you don’t use anymore. + public static var userSessionsOverviewSecurityRecommendationsInactiveInfo: String { + return VectorL10n.tr("Vector", "user_sessions_overview_security_recommendations_inactive_info") + } + /// Inactive sessions + public static var userSessionsOverviewSecurityRecommendationsInactiveTitle: String { + return VectorL10n.tr("Vector", "user_sessions_overview_security_recommendations_inactive_title") + } + /// Improve your account security by following these recommendations. + public static var userSessionsOverviewSecurityRecommendationsSectionInfo: String { + return VectorL10n.tr("Vector", "user_sessions_overview_security_recommendations_section_info") + } + /// Security recommendations + public static var userSessionsOverviewSecurityRecommendationsSectionTitle: String { + return VectorL10n.tr("Vector", "user_sessions_overview_security_recommendations_section_title") + } + /// Verify or sign out from unverified sessions. + public static var userSessionsOverviewSecurityRecommendationsUnverifiedInfo: String { + return VectorL10n.tr("Vector", "user_sessions_overview_security_recommendations_unverified_info") + } + /// Unverified sessions + public static var userSessionsOverviewSecurityRecommendationsUnverifiedTitle: String { + return VectorL10n.tr("Vector", "user_sessions_overview_security_recommendations_unverified_title") + } /// Sessions public static var userSessionsOverviewTitle: String { return VectorL10n.tr("Vector", "user_sessions_overview_title") @@ -8507,6 +8651,10 @@ public class VectorL10n: NSObject { public static var userSessionsSettings: String { return VectorL10n.tr("Vector", "user_sessions_settings") } + /// View all (%d) + public static func userSessionsViewAllAction(_ p1: Int) -> String { + return VectorL10n.tr("Vector", "user_sessions_view_all_action", p1) + } /// If you didn’t sign in to this session, your account may be compromised. public static var userVerificationSessionDetailsAdditionalInformationUntrustedCurrentUser: String { return VectorL10n.tr("Vector", "user_verification_session_details_additional_information_untrusted_current_user") diff --git a/Riot/Managers/Call/CallPresenter.swift b/Riot/Managers/Call/CallPresenter.swift index b0c7c8ca3..9d9465f8b 100644 --- a/Riot/Managers/Call/CallPresenter.swift +++ b/Riot/Managers/Call/CallPresenter.swift @@ -496,12 +496,12 @@ class CallPresenter: NSObject { #if canImport(JitsiMeetSDK) JMCallKitProxy.removeListener(self) - guard let session = sessions.first else { + guard let sessionInfo = sessions.first else { return } if let widgetEventsListener = widgetEventsListener { - session.removeListener(widgetEventsListener) + sessionInfo.removeListener(widgetEventsListener) } widgetEventsListener = nil #endif @@ -872,11 +872,11 @@ extension CallPresenter: JMCallKitListener { } - func providerDidActivateAudioSession(session: AVAudioSession) { + func providerDidActivateAudioSession(sessionInfo: AVAudioSession) { } - func providerDidDeactivateAudioSession(session: AVAudioSession) { + func providerDidDeactivateAudioSession(sessionInfo: AVAudioSession) { } diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 42cda0356..f5b91c71b 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -177,7 +177,15 @@ final class RiotSettings: NSObject { NotificationCenter.default.post(name: RiotSettings.didUpdateLiveLocationSharingActivation, object: self) } } - + + /// Flag indicating if the new session manager is enabled + @UserDefault(key: "enableNewSessionManager", defaultValue: false, storage: defaults) + var enableNewSessionManager + + /// Flag indicating if the new client information feature is enabled + @UserDefault(key: "enableClientInformationFeature", defaultValue: false, storage: defaults) + var enableClientInformationFeature + // MARK: Calls /// Indicate if `allowStunServerFallback` settings has been set once. diff --git a/Riot/Modules/Analytics/SentryMonitoringClient.swift b/Riot/Modules/Analytics/SentryMonitoringClient.swift index e296c0d5d..d15cf8039 100644 --- a/Riot/Modules/Analytics/SentryMonitoringClient.swift +++ b/Riot/Modules/Analytics/SentryMonitoringClient.swift @@ -37,6 +37,10 @@ struct SentryMonitoringClient { options.sampleRate = 0.1 options.tracesSampleRate = 0.1 + // Disable unnecessary network tracking + options.enableNetworkBreadcrumbs = false + options.enableNetworkTracking = false + options.beforeSend = { event in MXLog.debug("[SentryMonitoringClient] Issue detected: \(event)") return event diff --git a/Riot/Modules/Common/Recents/RecentsViewController.h b/Riot/Modules/Common/Recents/RecentsViewController.h index 895d6fcb6..2e5d037af 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.h +++ b/Riot/Modules/Common/Recents/RecentsViewController.h @@ -93,6 +93,12 @@ FOUNDATION_EXPORT NSString *const RecentsViewControllerDataReadyNotification; */ @property (nonatomic, weak) RootTabEmptyView *emptyView; +/** + The bottom anchor used to layout `emptyView` in the absence of a FAB. + If this value is `nil` the empty view will be anchored to the bottom of its superview. + */ +@property (nonatomic, weak) NSLayoutYAxisAnchor *emptyViewBottomAnchor; + /** The screen timer used for analytics if they've been enabled. The default value is nil. */ diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index ce2d74db6..0e9f0f5da 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -2232,7 +2232,8 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro [self.view addSubview:emptyView]; } - emptyViewBottomConstraint = [emptyView.bottomAnchor constraintEqualToAnchor:emptyView.superview.bottomAnchor]; + NSLayoutYAxisAnchor *bottomAnchor = self.emptyViewBottomAnchor ?: emptyView.superview.bottomAnchor; + emptyViewBottomConstraint = [emptyView.bottomAnchor constraintEqualToAnchor:bottomAnchor constant:-1]; // 1pt spacing for UIToolbar's divider. emptyView.translatesAutoresizingMaskIntoConstraints = NO; diff --git a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift index 9ea8b7a75..740019d06 100644 --- a/Riot/Modules/Common/SwiftUI/VectorHostingController.swift +++ b/Riot/Modules/Common/SwiftUI/VectorHostingController.swift @@ -73,14 +73,6 @@ class VectorHostingController: UIHostingController { bottomSheetPreferences?.setup(viewController: self) } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - if isNavigationBarHidden { - self.navigationController?.isNavigationBarHidden = true - } - } - override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -93,6 +85,14 @@ class VectorHostingController: UIHostingController { } } + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + if let navigationController = navigationController, navigationController.isNavigationBarHidden != isNavigationBarHidden { + navigationController.isNavigationBarHidden = isNavigationBarHidden + } + } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() diff --git a/Riot/Modules/DeepLink/MXRoomAliasResolution+Deeplink.swift b/Riot/Modules/DeepLink/MXRoomAliasResolution+Deeplink.swift index 234f07849..ddded6927 100644 --- a/Riot/Modules/DeepLink/MXRoomAliasResolution+Deeplink.swift +++ b/Riot/Modules/DeepLink/MXRoomAliasResolution+Deeplink.swift @@ -29,9 +29,7 @@ import MatrixSDK return nil } - return MXTools.encodeURIComponent( - fragment(for: roomId) - ) + return fragment(for: roomId) } private func fragment(for roomId: String) -> String { diff --git a/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift b/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift index f29d16858..a8326e27d 100644 --- a/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift +++ b/Riot/Modules/Home/AllChats/AllChatsCoordinator.swift @@ -352,7 +352,8 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol { var subMenuActions: [UIAction] = [] if BuildSettings.sideMenuShowInviteFriends { subMenuActions.append(UIAction(title: VectorL10n.sideMenuActionInviteFriends, image: UIImage(systemName: "square.and.arrow.up.fill")) { [weak self] action in - self?.showInviteFriends(from: nil) + guard let self = self else { return } + self.showInviteFriends(from: self.avatarMenuButton) }) } @@ -377,6 +378,7 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol { button.menu = menu button.showsMenuAsPrimaryAction = true button.autoresizingMask = [.flexibleHeight, .flexibleWidth] + button.accessibilityLabel = VectorL10n.allChatsUserMenuAccessibilityLabel view.addSubview(button) self.avatarMenuButton = button @@ -593,7 +595,7 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol { signOutAlertPresenter.present(for: keyBackup.state, areThereKeysToBackup: keyBackup.hasKeysToBackup, from: self.allChatsViewController, - sourceView: nil, + sourceView: avatarMenuButton, animated: true) } @@ -775,7 +777,7 @@ extension AllChatsCoordinator: AllChatsViewControllerDelegate { self.showRoom(withNavigationParameters: roomNavigationParameters, completion: completion) } - func allChatsViewController(_ allChatsViewController: AllChatsViewController, didSelectRoomPreviewWithParameters roomPreviewNavigationParameters: RoomPreviewNavigationParameters, completion: @escaping () -> Void) { + func allChatsViewController(_ allChatsViewController: AllChatsViewController, didSelectRoomPreviewWithParameters roomPreviewNavigationParameters: RoomPreviewNavigationParameters, completion: (() -> Void)?) { self.showRoomPreview(withNavigationParameters: roomPreviewNavigationParameters, completion: completion) } diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.swift b/Riot/Modules/Home/AllChats/AllChatsViewController.swift index 9ccba2ebe..88a973557 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.swift +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.swift @@ -22,7 +22,7 @@ import Reusable protocol AllChatsViewControllerDelegate: AnyObject { func allChatsViewControllerDidCompleteAuthentication(_ allChatsViewController: AllChatsViewController) func allChatsViewController(_ allChatsViewController: AllChatsViewController, didSelectRoomWithParameters roomNavigationParameters: RoomNavigationParameters, completion: @escaping () -> Void) - func allChatsViewController(_ allChatsViewController: AllChatsViewController, didSelectRoomPreviewWithParameters roomPreviewNavigationParameters: RoomPreviewNavigationParameters, completion: @escaping () -> Void) + func allChatsViewController(_ allChatsViewController: AllChatsViewController, didSelectRoomPreviewWithParameters roomPreviewNavigationParameters: RoomPreviewNavigationParameters, completion: (() -> Void)?) func allChatsViewController(_ allChatsViewController: AllChatsViewController, didSelectContact contact: MXKContact, with presentationParameters: ScreenPresentationParameters) } @@ -75,6 +75,23 @@ class AllChatsViewController: HomeViewController { private var currentAlert: UIAlertController? + @IBOutlet private var toolbar: UIToolbar! + private var isToolbarHidden: Bool = false { + didSet { + if isViewLoaded { + toolbar.transform = isToolbarHidden ? CGAffineTransform(translationX: 0, y: 2 * toolbarHeight) : .identity + self.view.layoutIfNeeded() + } + } + } + + private func setToolbarHidden(_ isHidden: Bool, animated: Bool) { + UIView.animate(withDuration: animated ? 0.3 : 0) { + self.isToolbarHidden = isHidden + } + + } + // MARK: - SplitViewMasterViewControllerProtocol // References on the currently selected room @@ -91,6 +108,8 @@ class AllChatsViewController: HomeViewController { // Tell whether the onboarding screen is preparing. private(set) var isOnboardingInProgress: Bool = false + + private var toolbarHeight: CGFloat = 0 // MARK: - Lifecycle @@ -107,6 +126,9 @@ class AllChatsViewController: HomeViewController { recentsTableView.register(RecentsInvitesTableViewCell.nib, forCellReuseIdentifier: RecentsInvitesTableViewCell.reuseIdentifier) recentsTableView.contentInsetAdjustmentBehavior = .automatic + toolbarHeight = toolbar.frame.height + emptyViewBottomAnchor = toolbar.topAnchor + updateUI() navigationItem.largeTitleDisplayMode = .automatic @@ -122,8 +144,7 @@ class AllChatsViewController: HomeViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - self.navigationController?.isToolbarHidden = false - self.navigationController?.toolbar.tintColor = ThemeService.shared().theme.colors.accent + self.toolbar.tintColor = ThemeService.shared().theme.colors.accent if self.navigationItem.searchController == nil { self.navigationItem.searchController = searchController } @@ -164,12 +185,6 @@ class AllChatsViewController: HomeViewController { } } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - self.navigationController?.isToolbarHidden = true - } - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) @@ -208,7 +223,27 @@ class AllChatsViewController: HomeViewController { override func addMatrixSession(_ mxSession: MXSession!) { super.addMatrixSession(mxSession) - initDataSource() + + if let dataSource = dataSource, !dataSource.mxSessions.contains(where: { $0 as? MXSession == mxSession }) { + dataSource.addMatrixSession(mxSession) + // Setting the delegate is required to send a RecentsViewControllerDataReadyNotification. + // Without this, when clearing the cache we end up with an infinite green spinner. + (dataSource as? RecentsDataSource)?.setDelegate(self, andRecentsDataSourceMode: recentsDataSourceMode) + } else { + initDataSource() + } + } + + override func removeMatrixSession(_ mxSession: MXSession!) { + super.removeMatrixSession(mxSession) + + guard let dataSource = dataSource else { return } + dataSource.removeMatrixSession(mxSession) + + if dataSource.mxSessions.isEmpty { + // The user logged out -> we need to reset the data source + displayList(nil) + } } private func initDataSource() { @@ -332,20 +367,28 @@ class AllChatsViewController: HomeViewController { private var initialScrollPosition: Double = 0 private func scrollPosition(of scrollView: UIScrollView) -> Double { - return scrollView.contentOffset.y + scrollView.adjustedContentInset.top + scrollView.adjustedContentInset.bottom + return scrollView.contentOffset.y + scrollView.adjustedContentInset.top } override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + guard scrollView == recentsTableView else { + return + } + initialScrollPosition = scrollPosition(of: scrollView) } override func scrollViewDidScroll(_ scrollView: UIScrollView) { super.scrollViewDidScroll(scrollView) + guard scrollView == recentsTableView else { + return + } + let scrollPosition = scrollPosition(of: scrollView) - if !self.recentsTableView.isDragging && scrollPosition == 0 && self.navigationController?.isToolbarHidden == true { - self.navigationController?.setToolbarHidden(false, animated: true) + if !self.recentsTableView.isDragging && scrollPosition == 0 && self.isToolbarHidden == true { + self.setToolbarHidden(false, animated: true) } guard self.recentsTableView.isDragging else { @@ -357,8 +400,8 @@ class AllChatsViewController: HomeViewController { } let isToolBarHidden: Bool = scrollPosition - initialScrollPosition > 0 - if isToolBarHidden != self.navigationController?.isToolbarHidden { - self.navigationController?.setToolbarHidden(isToolBarHidden, animated: true) + if isToolBarHidden != self.isToolbarHidden { + self.setToolbarHidden(isToolBarHidden, animated: true) } } @@ -466,13 +509,21 @@ class AllChatsViewController: HomeViewController { } private func updateToolbar(with menu: UIMenu) { - self.navigationController?.isToolbarHidden = false + guard isViewLoaded else { + return + } + + self.isToolbarHidden = false self.update(with: ThemeService.shared().theme) - self.setToolbarItems([ - UIBarButtonItem(image: Asset.Images.allChatsSpacesIcon.image, style: .done, target: self, action: #selector(self.showSpaceSelectorAction(sender: ))), + + let spacesButton = UIBarButtonItem(image: Asset.Images.allChatsSpacesIcon.image, style: .done, target: self, action: #selector(self.showSpaceSelectorAction(sender: ))) + spacesButton.accessibilityLabel = VectorL10n.spaceSelectorTitle + + self.toolbar.items = [ + spacesButton, UIBarButtonItem.flexibleSpace(), UIBarButtonItem(image: Asset.Images.allChatsEditIcon.image, menu: menu) - ], animated: true) + ] } private func showCreateSpace(parentSpaceId: String?) { @@ -854,7 +905,7 @@ extension AllChatsViewController: SplitViewMasterViewControllerProtocol { /// - Parameters: /// - parameters: the presentation parameters that contains room preview information plus display information. /// - completion: the block to execute at the end of the operation. - func selectRoomPreview(with parameters: RoomPreviewNavigationParameters, completion: @escaping () -> Void) { + func selectRoomPreview(with parameters: RoomPreviewNavigationParameters, completion: (() -> Void)?) { releaseSelectedItem() let roomPreviewData = parameters.previewData diff --git a/Riot/Modules/Home/AllChats/AllChatsViewController.xib b/Riot/Modules/Home/AllChats/AllChatsViewController.xib index 83b4849ab..59bdeffa1 100644 --- a/Riot/Modules/Home/AllChats/AllChatsViewController.xib +++ b/Riot/Modules/Home/AllChats/AllChatsViewController.xib @@ -11,8 +11,7 @@ - - + @@ -25,25 +24,24 @@ - - - - + + - + - + + - - - - + + + + diff --git a/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewModel.swift b/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewModel.swift index d0d900290..fe54a2467 100644 --- a/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewModel.swift +++ b/Riot/Modules/KeyVerification/Common/Verify/Scanning/KeyVerificationVerifyByScanningViewModel.swift @@ -172,8 +172,9 @@ final class KeyVerificationVerifyByScanningViewModel: KeyVerificationVerifyBySca // Remove pending QR code transaction, as we are going to use SAS verification self.removePendingQRCodeTransaction() - - if keyVerificationTransaction is MXSASTransaction == false || keyVerificationTransaction.isIncoming { + + // Check due to legacy implementation of key verification which could pass incorrect type of transaction + if keyVerificationTransaction is MXIncomingSASTransaction { MXLog.debug("[KeyVerificationVerifyByScanningViewModel] SAS transaction should be outgoing") self.unregisterTransactionDidStateChangeNotification() self.update(viewState: .error(KeyVerificationVerifyByScanningViewModelError.unknown)) diff --git a/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift b/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift index b56748a40..45e8e378f 100644 --- a/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift +++ b/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift @@ -209,6 +209,7 @@ extension UserVerificationCoordinator: KeyVerificationManuallyVerifyCoordinatorD self.presenter.toPresentable().dismiss(animated: true) { self.remove(childCoordinator: coordinator) } + delegate?.userVerificationCoordinatorDidComplete(self) } func keyVerificationManuallyVerifyCoordinatorDidCancel(_ coordinator: KeyVerificationManuallyVerifyCoordinatorType) { diff --git a/Riot/Modules/KeyVerification/User/UserVerificationCoordinatorBridgePresenter.swift b/Riot/Modules/KeyVerification/User/UserVerificationCoordinatorBridgePresenter.swift index af19e72f3..ca718aa1e 100644 --- a/Riot/Modules/KeyVerification/User/UserVerificationCoordinatorBridgePresenter.swift +++ b/Riot/Modules/KeyVerification/User/UserVerificationCoordinatorBridgePresenter.swift @@ -77,8 +77,14 @@ final class UserVerificationCoordinatorBridgePresenter: NSObject { } else { userVerificationCoordinator = UserVerificationCoordinator(presenter: self.presenter, session: self.session, userId: self.userId, userDisplayName: self.userDisplayName) } - + userVerificationCoordinator.delegate = self userVerificationCoordinator.start() self.coordinator = userVerificationCoordinator } } + +extension UserVerificationCoordinatorBridgePresenter: UserVerificationCoordinatorDelegate { + func userVerificationCoordinatorDidComplete(_ coordinator: UserVerificationCoordinatorType) { + delegate?.userVerificationCoordinatorBridgePresenterDelegateDidComplete(self) + } +} diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m index 8ee0a6190..c91d51e7a 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m @@ -920,7 +920,7 @@ { for (MXKRoomBubbleComponent *component in bubbleComponents) { - if (component.showEncryptionBadge) + if (component.encryptionDecoration != EventEncryptionDecorationNone) { containsBubbleComponentWithEncryptionBadge = YES; break; diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.h index e2f94423e..36bf173e9 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.h @@ -18,6 +18,7 @@ #import "MXKEventFormatter.h" #import "MXKURLPreviewDataProtocol.h" +#import "EventEncryptionDecoration.h" @protocol MXThreadProtocol; @@ -101,9 +102,9 @@ typedef enum : NSUInteger { @property (nonatomic) MXEventScan *eventScan; /** - Indicate if an encryption badge should be shown. + Type of encryption decoration (if any) for this event */ -@property (nonatomic, readonly) BOOL showEncryptionBadge; +@property (nonatomic, readonly) EventEncryptionDecoration encryptionDecoration; /** Thread for the bubble component. Should only exist for thread root events. diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m index 2e3cf29b8..6d231262c 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m @@ -73,7 +73,7 @@ } } - _showEncryptionBadge = [self shouldShowWarningBadgeForEvent:event roomState:(MXRoomState*)roomState session:session]; + _encryptionDecoration = [self encryptionDecorationForEvent:event roomState:(MXRoomState*)roomState session:session]; [self updateLinkWithRoomState:roomState]; @@ -116,7 +116,7 @@ andLatestRoomState:latestRoomState error:&error]; - _showEncryptionBadge = [self shouldShowWarningBadgeForEvent:event roomState:roomState session:session]; + _encryptionDecoration = [self encryptionDecorationForEvent:event roomState:roomState session:session]; [self updateLinkWithRoomState:roomState]; } @@ -167,24 +167,24 @@ self.link = url; } -- (BOOL)shouldShowWarningBadgeForEvent:(MXEvent*)event roomState:(MXRoomState*)roomState session:(MXSession*)session +- (EventEncryptionDecoration)encryptionDecorationForEvent:(MXEvent*)event roomState:(MXRoomState*)roomState session:(MXSession*)session { // Warning badges are unnecessary in unencrypted rooms if (!roomState.isEncrypted) { - return NO; + return EventEncryptionDecorationNone; } // Not all events are encrypted (e.g. state/reactions/redactions) and we only have encrypted cell subclasses for messages and attachments. if (event.eventType != MXEventTypeRoomMessage && !event.isMediaAttachment) { - return NO; + return EventEncryptionDecorationNone; } // Always show a warning badge if there was a decryption error. if (event.decryptionError) { - return YES; + return EventEncryptionDecorationDecryptionError; } // Unencrypted message events should show a warning unless they're pending local echoes @@ -193,10 +193,10 @@ if (event.isLocalEvent || event.contentHasBeenEdited) // Local echo for an edit is clear but uses a true event id, the one of the edited event { - return NO; + return EventEncryptionDecorationNone; } - return YES; + return EventEncryptionDecorationNotEncrypted; } // The encryption is in a good state. @@ -208,12 +208,17 @@ if (userTrustLevel.isVerified && !deviceInfo.trustLevel.isVerified) { - return YES; + return EventEncryptionDecorationUntrustedDevice; } } + if (event.isUntrusted) + { + return EventEncryptionDecorationUnsafeKey; + } + // Everything was fine - return NO; + return EventEncryptionDecorationNone; } @end diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index fa6495fd5..63f86617a 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -1846,7 +1846,17 @@ static NSString *const kHTMLATagRegexPattern = @"( } else { - MXJSONModelSetString(repliedEventContent, repliedEvent.content[@"formatted_body"]); + MXReplyEventParser *parser = [[MXReplyEventParser alloc] init]; + MXReplyEventParts *parts = [parser parse:repliedEvent]; + MXJSONModelSetString(repliedEventContent, parts.formattedBodyParts.replyText) + if (!repliedEventContent) + { + MXJSONModelSetString(repliedEventContent, parts.bodyParts.replyText) + } + if (!repliedEventContent) + { + MXJSONModelSetString(repliedEventContent, repliedEvent.content[@"formatted_body"]); + } if (!repliedEventContent) { MXJSONModelSetString(repliedEventContent, repliedEvent.content[kMXMessageBodyKey]); diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MarkdownToHTMLRenderer.swift b/Riot/Modules/MatrixKit/Utils/EventFormatter/MarkdownToHTMLRenderer.swift index 0800f4307..8c697dc6c 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MarkdownToHTMLRenderer.swift +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MarkdownToHTMLRenderer.swift @@ -166,10 +166,11 @@ private extension CMarkNode { private extension String { /// Returns array of URLs detected inside the String. var containedUrls: [NSTextCheckingResult] { - guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { + guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue), + let percentEncoded = self.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return [] } - return detector.matches(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count)) + return detector.matches(in: percentEncoded, options: [], range: NSRange(location: 0, length: percentEncoded.utf16.count)) } } diff --git a/Riot/Modules/MatrixKit/Views/EncryptionInfoView/MXKEncryptionInfoView.m b/Riot/Modules/MatrixKit/Views/EncryptionInfoView/MXKEncryptionInfoView.m index cbaea4f84..878742076 100644 --- a/Riot/Modules/MatrixKit/Views/EncryptionInfoView/MXKEncryptionInfoView.m +++ b/Riot/Modules/MatrixKit/Views/EncryptionInfoView/MXKEncryptionInfoView.m @@ -192,7 +192,7 @@ static NSAttributedString *verticalWhitespace = nil; NSString *claimedKey = _mxEvent.keysClaimed[@"ed25519"]; NSString *algorithm = _mxEvent.wireContent[@"algorithm"]; NSString *sessionId = _mxEvent.wireContent[@"session_id"]; - NSString *untrusted = _mxEvent.isUntrusted ? [VectorL10n userVerificationSessionsListSessionUntrusted] : [VectorL10n userVerificationSessionsListSessionTrusted]; + NSString *untrusted = _mxEvent.isUntrusted ? [VectorL10n roomEventEncryptionInfoKeyAuthenticityNotGuaranteed] : [VectorL10n userVerificationSessionsListSessionTrusted]; NSString *decryptionError; if (_mxEvent.decryptionError) diff --git a/Riot/Modules/Room/Attachements/MXKAttachmentsViewController.m b/Riot/Modules/Room/Attachements/MXKAttachmentsViewController.m index 37759b261..331121341 100644 --- a/Riot/Modules/Room/Attachements/MXKAttachmentsViewController.m +++ b/Riot/Modules/Room/Attachements/MXKAttachmentsViewController.m @@ -346,6 +346,11 @@ - (void)displayAttachments:(NSArray*)attachmentArray focusOn:(NSString*)eventId { + if ([attachmentArray isEqualToArray:attachments] && eventId.length == 0) + { + // neither the attachments nor the focus changed, can be ignored + return; + } NSString *currentAttachmentEventId = eventId; NSString *currentAttachmentOriginalFileName = nil; diff --git a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m index 0b24d0af1..10b90fce1 100644 --- a/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m +++ b/Riot/Modules/Room/Members/Detail/RoomMemberDetailsViewController.m @@ -33,7 +33,7 @@ #define TABLEVIEW_ROW_CELL_HEIGHT 46 #define TABLEVIEW_SECTION_HEADER_HEIGHT 28 -@interface RoomMemberDetailsViewController () +@interface RoomMemberDetailsViewController () { RoomMemberTitleView* memberTitleView; @@ -496,6 +496,7 @@ session:self.mxRoom.mxSession userId:self.mxRoomMember.userId userDisplayName:self.mxRoomMember.displayname]; + userVerificationCoordinatorBridgePresenter.delegate = self; [userVerificationCoordinatorBridgePresenter start]; self.userVerificationCoordinatorBridgePresenter = userVerificationCoordinatorBridgePresenter; } @@ -1672,4 +1673,11 @@ } } +#pragma mark - UserVerificationCoordinatorBridgePresenterDelegate + +- (void)userVerificationCoordinatorBridgePresenterDelegateDidComplete:(UserVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter +{ + [self refreshUserEncryptionTrustLevel]; +} + @end diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 2b0ad02c5..a01f6c567 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -55,6 +55,7 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification; // The preview header @property (weak, nonatomic, nullable) IBOutlet UIView *previewHeaderContainer; @property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *previewHeaderContainerHeightConstraint; +@property (weak, nonatomic, nullable) IBOutlet NSLayoutConstraint *userSuggestionContainerHeightConstraint; // The jump to last unread banner @property (weak, nonatomic, nullable) IBOutlet UIView *jumpToLastUnreadBannerContainer; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index a6a46cbb0..b73503b24 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1557,10 +1557,6 @@ static CGSize kThreadListBarButtonItemImageSize; */ - (void)createDiscussionIfNeeded:(void (^)(BOOL readyToSend))onComplete { - // Disable the input tool bar during this operation. This prevents us from creating several discussions, or - // trying to send several invites. - self.inputToolbarView.userInteractionEnabled = false; - void(^completion)(BOOL) = ^(BOOL readyToSend) { self.inputToolbarView.userInteractionEnabled = true; if (onComplete) { @@ -1570,6 +1566,10 @@ static CGSize kThreadListBarButtonItemImageSize; if (self.directChatTargetUser) { + // Disable the input tool bar during this operation. This prevents us from creating several discussions, or + // trying to send several invites. + self.inputToolbarView.userInteractionEnabled = false; + [self createDiscussionWithUser:self.directChatTargetUser completion:completion]; } else @@ -7826,6 +7826,16 @@ static CGSize kThreadListBarButtonItemImageSize; [self mention:member]; } +- (void)userSuggestionCoordinatorBridge:(UserSuggestionCoordinatorBridge *)coordinator didUpdateViewHeight:(CGFloat)height +{ + if (self.userSuggestionContainerHeightConstraint.constant != height) + { + self.userSuggestionContainerHeightConstraint.constant = height; + + [self.view layoutIfNeeded]; + } +} + #pragma mark - ThreadsCoordinatorBridgePresenterDelegate - (void)threadsCoordinatorBridgePresenterDelegateDidComplete:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter diff --git a/Riot/Modules/Room/RoomViewController.xib b/Riot/Modules/Room/RoomViewController.xib index 888aa3b75..073276604 100644 --- a/Riot/Modules/Room/RoomViewController.xib +++ b/Riot/Modules/Room/RoomViewController.xib @@ -32,6 +32,7 @@ + @@ -171,7 +172,7 @@ - + diff --git a/Riot/Modules/Room/TimelineCells/Encryption/EventEncryptionDecoration.h b/Riot/Modules/Room/TimelineCells/Encryption/EventEncryptionDecoration.h new file mode 100644 index 000000000..f6d4264bd --- /dev/null +++ b/Riot/Modules/Room/TimelineCells/Encryption/EventEncryptionDecoration.h @@ -0,0 +1,30 @@ +// +// 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. +// + +#ifndef EventEncryptionDecoration_h +#define EventEncryptionDecoration_h + +typedef NS_ENUM(NSUInteger, EventEncryptionDecoration) +{ + EventEncryptionDecorationNone, + EventEncryptionDecorationUnsafeKey, + EventEncryptionDecorationDecryptionError, + EventEncryptionDecorationNotEncrypted, + EventEncryptionDecorationUntrustedDevice +}; + + +#endif /* EventEncryptionDecoration_h */ diff --git a/Riot/Modules/Room/TimelineCells/Encryption/RoomEncryptedDataBubbleCell.m b/Riot/Modules/Room/TimelineCells/Encryption/RoomEncryptedDataBubbleCell.m index 7a5e50ff9..07d23f209 100644 --- a/Riot/Modules/Room/TimelineCells/Encryption/RoomEncryptedDataBubbleCell.m +++ b/Riot/Modules/Room/TimelineCells/Encryption/RoomEncryptedDataBubbleCell.m @@ -24,12 +24,18 @@ NSString *const kRoomEncryptedDataBubbleCellTapOnEncryptionIcon = @"kRoomEncrypt + (UIImage*)encryptionIconForBubbleComponent:(MXKRoomBubbleComponent *)bubbleComponent { - if (!bubbleComponent.showEncryptionBadge) - { - return nil; + switch (bubbleComponent.encryptionDecoration) { + case EventEncryptionDecorationNone: + return nil; + case EventEncryptionDecorationUnsafeKey: + return AssetImages.encryptionUntrusted.image; + case EventEncryptionDecorationDecryptionError: + case EventEncryptionDecorationNotEncrypted: + case EventEncryptionDecorationUntrustedDevice: + return AssetImages.encryptionWarning.image; + default: + return nil; } - - return AssetImages.encryptionWarning.image; } + (void)addEncryptionStatusFromBubbleData:(MXKRoomBubbleCellData *)bubbleData inContainerView:(UIView *)containerView diff --git a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m index 308721ab3..cb43c261e 100644 --- a/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m +++ b/Riot/Modules/Settings/Security/ManageSession/ManageSessionViewController.m @@ -46,7 +46,7 @@ enum { }; -@interface ManageSessionViewController () +@interface ManageSessionViewController () { // The device to display MXDevice *device; @@ -670,6 +670,7 @@ enum { userId:self.mainSession.myUser.userId userDisplayName:nil deviceId:device.deviceId]; + userVerificationCoordinatorBridgePresenter.delegate = self; [userVerificationCoordinatorBridgePresenter start]; self.userVerificationCoordinatorBridgePresenter = userVerificationCoordinatorBridgePresenter; } @@ -722,4 +723,11 @@ enum { self.reauthenticationCoordinatorBridgePresenter = reauthenticationPresenter; } +#pragma mark - UserVerificationCoordinatorBridgePresenterDelegate + +- (void)userVerificationCoordinatorBridgePresenterDelegateDidComplete:(UserVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter +{ + [self reloadDeviceWithCompletion:^{}]; +} + @end diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index aeb53c59f..a53083ba4 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -222,7 +222,9 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE) LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0, LABS_ENABLE_THREADS_INDEX, LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS, - LABS_ENABLE_LIVE_LOCATION_SHARING + LABS_ENABLE_LIVE_LOCATION_SHARING, + LABS_ENABLE_NEW_SESSION_MANAGER, + LABS_ENABLE_NEW_CLIENT_INFO_FEATURE }; typedef NS_ENUM(NSUInteger, SECURITY) @@ -494,7 +496,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> Section *sectionSecurity = [Section sectionWithTag:SECTION_TAG_SECURITY]; [sectionSecurity addRowWithTag:SECURITY_BUTTON_INDEX]; - if (BuildSettings.deviceManagerEnabled) + if (RiotSettings.shared.enableNewSessionManager) { // NOTE: Add device manager entry point in the security section atm for debug purpose [sectionSecurity addRowWithTag:DEVICE_MANAGER_INDEX]; @@ -748,6 +750,8 @@ ChangePasswordCoordinatorBridgePresenterDelegate> { [sectionLabs addRowWithTag:LABS_ENABLE_LIVE_LOCATION_SHARING]; } + [sectionLabs addRowWithTag:LABS_ENABLE_NEW_SESSION_MANAGER]; + [sectionLabs addRowWithTag:LABS_ENABLE_NEW_CLIENT_INFO_FEATURE]; sectionLabs.headerTitle = [VectorL10n settingsLabs]; if (sectionLabs.hasAnyRows) { @@ -2850,6 +2854,30 @@ ChangePasswordCoordinatorBridgePresenterDelegate> { cell = [self buildLiveLocationSharingCellForTableView:tableView atIndexPath:indexPath]; } + else if (row == LABS_ENABLE_NEW_SESSION_MANAGER) + { + MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = [VectorL10n settingsLabsEnableNewSessionManager]; + labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.enableNewSessionManager; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableNewSessionManager:) forControlEvents:UIControlEventTouchUpInside]; + + cell = labelAndSwitchCell; + } + else if (row == LABS_ENABLE_NEW_CLIENT_INFO_FEATURE) + { + MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = [VectorL10n settingsLabsEnableNewClientInfoFeature]; + labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.enableClientInformationFeature; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableNewClientInfoFeature:) forControlEvents:UIControlEventTouchUpInside]; + + cell = labelAndSwitchCell; + } } else if (section == SECTION_TAG_SECURITY) { @@ -3692,6 +3720,19 @@ ChangePasswordCoordinatorBridgePresenterDelegate> [[AppDelegate theDelegate] restoreEmptyDetailsViewController]; } +- (void)toggleEnableNewSessionManager:(UISwitch *)sender +{ + RiotSettings.shared.enableNewSessionManager = sender.isOn; + [self updateSections]; +} + +- (void)toggleEnableNewClientInfoFeature:(UISwitch *)sender +{ + BOOL isEnabled = sender.isOn; + RiotSettings.shared.enableClientInformationFeature = isEnabled; + MXSDKOptions.sharedInstance.enableNewClientInformationFeature = isEnabled; +} + - (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender { RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn; @@ -4130,11 +4171,13 @@ ChangePasswordCoordinatorBridgePresenterDelegate> msisdn = [NSString stringWithFormat:@"+%@", [e164 substringFromIndex:2]]; } + NSString *countryCode = newPhoneNumberCell.isoCountryCode; + [self showAuthenticationIfNeededForAdding:kMX3PIDMediumMSISDN withSession:session completion:^(NSDictionary *authParams) { [self startActivityIndicator]; __block MX3PidAddSession *new3Pid; - new3Pid = [session.threePidAddManager startAddPhoneNumberSessionWithPhoneNumber:msisdn countryCode:nil success:^{ + new3Pid = [session.threePidAddManager startAddPhoneNumberSessionWithPhoneNumber:msisdn countryCode:countryCode success:^{ [self showValidationMsisdnDialogWithMessage:[VectorL10n accountMsisdnValidationMessage] for3PidAddSession:new3Pid threePidAddManager:session.threePidAddManager authenticationParameters:authParams]; diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index efbb28811..64da8950b 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -469,8 +469,8 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; { calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; - // Use the secondary bg color to set the background color in the default CSS. - NSUInteger bgColor = [MXKTools rgbValueWithColor:ThemeService.shared.theme.headerBackgroundColor]; + // Use the selected bg color to set the code block background color in the default CSS. + NSUInteger bgColor = [MXKTools rgbValueWithColor:ThemeService.shared.theme.selectedBackgroundColor]; self.defaultCSS = [NSString stringWithFormat:@" \ pre,code { \ background-color: #%06lX; \ diff --git a/Riot/target.yml b/Riot/target.yml index 403e4b221..bc2359bb6 100644 --- a/Riot/target.yml +++ b/Riot/target.yml @@ -24,6 +24,9 @@ schemes: disableMainThreadChecker: true targets: - RiotTests + gatherCoverageData: true + coverageTargets: + - Riot targets: Riot: @@ -39,6 +42,7 @@ targets: - package: Mapbox - package: OrderedCollections - package: SwiftOGG + - package: DeviceKit configFiles: Debug: Debug.xcconfig diff --git a/RiotNSE/target.yml b/RiotNSE/target.yml index f42188407..888381fc0 100644 --- a/RiotNSE/target.yml +++ b/RiotNSE/target.yml @@ -31,6 +31,9 @@ targets: platform: iOS type: app-extension + dependencies: + - package: DeviceKit + configFiles: Debug: Debug.xcconfig Release: Release.xcconfig diff --git a/RiotShareExtension/target.yml b/RiotShareExtension/target.yml index 4a067c167..1edc9858b 100644 --- a/RiotShareExtension/target.yml +++ b/RiotShareExtension/target.yml @@ -31,6 +31,9 @@ targets: platform: iOS type: app-extension + dependencies: + - package: DeviceKit + configFiles: Debug: Debug.xcconfig Release: Release.xcconfig diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift index 6e4e34f30..39f339e87 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift index 3fa94d351..ab2937f62 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift @@ -14,14 +14,12 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI + +typealias AnalyticsPromptViewModelType = StateStoreViewModel -typealias AnalyticsPromptViewModelType = StateStoreViewModel class AnalyticsPromptViewModel: AnalyticsPromptViewModelType { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift index c41749369..b4d9daf80 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift @@ -22,17 +22,16 @@ struct AnalyticsPromptCoordinatorParameters { } final class AnalyticsPromptCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private private let parameters: AnalyticsPromptCoordinatorParameters - private let analyticsPromptHostingController: UIViewController - private var _analyticsPromptViewModel: Any? = nil + private let analyticsPromptHostingController: VectorHostingController + private var _analyticsPromptViewModel: Any? fileprivate var analyticsPromptViewModel: AnalyticsPromptViewModel { - return _analyticsPromptViewModel as! AnalyticsPromptViewModel + _analyticsPromptViewModel as! AnalyticsPromptViewModel } // MARK: Public @@ -60,6 +59,7 @@ final class AnalyticsPromptCoordinator: Coordinator, Presentable { let view = AnalyticsPrompt(viewModel: viewModel.context) _analyticsPromptViewModel = viewModel analyticsPromptHostingController = VectorHostingController(rootView: view) + analyticsPromptHostingController.isNavigationBarHidden = true } // MARK: - Public @@ -84,6 +84,6 @@ final class AnalyticsPromptCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.analyticsPromptHostingController + analyticsPromptHostingController } } diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift index 9ada08435..0d5d844c8 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptStrings.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,4 +25,3 @@ struct AnalyticsPromptStrings: AnalyticsPromptStringsProtocol { withAllowedTags: ["b", "p"], font: UIFont.systemFont(ofSize: UIFont.systemFontSize)) } - diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift index 290a88836..7d222aa31 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift index 37a38e32d..2fd7c0018 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptStrings.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift index 2013f0866..46eed139c 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class AnalyticsPromptUITests: MockScreenTestCase { /// Verify that the prompt is displayed correctly for new users. diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift index 16cbeb989..a400af7aa 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import SwiftUI /// A prompt that asks the user whether they would like to enable Analytics or not. struct AnalyticsPrompt: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptCheckmarkItem.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptCheckmarkItem.swift index cec25f2d7..1ec120069 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptCheckmarkItem.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPromptCheckmarkItem.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct AnalyticsPromptCheckmarkItem: View { - // MARK: - Properties // MARK: Private @@ -40,7 +39,7 @@ struct AnalyticsPromptCheckmarkItem: View { let range = NSRange(location: 0, length: attributedString.length) let string = attributedString.string as NSString - attributedString.enumerateAttributes(in: range, options: []) { attributes, range, stop in + attributedString.enumerateAttributes(in: range, options: []) { attributes, range, _ in var isBold = false if let font = attributes[.font] as? UIFont { @@ -54,7 +53,7 @@ struct AnalyticsPromptCheckmarkItem: View { } init(string: String) { - self.components = [StringComponent(string: string, isBold: false)] + components = [StringComponent(string: string, isBold: false)] } // MARK: - Views @@ -75,11 +74,10 @@ struct AnalyticsPromptCheckmarkItem: View { // MARK: - Previews struct AnalyticsPromptCheckmarkItem_Previews: PreviewProvider { - static let strings = MockAnalyticsPromptStrings() static var previews: some View { - VStack(alignment:.leading) { + VStack(alignment: .leading) { AnalyticsPromptCheckmarkItem(attributedString: strings.point1) AnalyticsPromptCheckmarkItem(attributedString: strings.point2) AnalyticsPromptCheckmarkItem(attributedString: strings.longString) diff --git a/RiotSwiftUI/Modules/Authentication/ChoosePassword/AuthenticationChoosePasswordModels.swift b/RiotSwiftUI/Modules/Authentication/ChoosePassword/AuthenticationChoosePasswordModels.swift index 6c8230b18..838767317 100644 --- a/RiotSwiftUI/Modules/Authentication/ChoosePassword/AuthenticationChoosePasswordModels.swift +++ b/RiotSwiftUI/Modules/Authentication/ChoosePassword/AuthenticationChoosePasswordModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Authentication/ChoosePassword/AuthenticationChoosePasswordViewModel.swift b/RiotSwiftUI/Modules/Authentication/ChoosePassword/AuthenticationChoosePasswordViewModel.swift index 89f418a07..328b17161 100644 --- a/RiotSwiftUI/Modules/Authentication/ChoosePassword/AuthenticationChoosePasswordViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/ChoosePassword/AuthenticationChoosePasswordViewModel.swift @@ -16,11 +16,9 @@ import SwiftUI -typealias AuthenticationChoosePasswordViewModelType = StateStoreViewModel -class AuthenticationChoosePasswordViewModel: AuthenticationChoosePasswordViewModelType, AuthenticationChoosePasswordViewModelProtocol { +typealias AuthenticationChoosePasswordViewModelType = StateStoreViewModel +class AuthenticationChoosePasswordViewModel: AuthenticationChoosePasswordViewModelType, AuthenticationChoosePasswordViewModelProtocol { // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Authentication/ChoosePassword/AuthenticationChoosePasswordViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/ChoosePassword/AuthenticationChoosePasswordViewModelProtocol.swift index 47c57304e..46a148ec4 100644 --- a/RiotSwiftUI/Modules/Authentication/ChoosePassword/AuthenticationChoosePasswordViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/ChoosePassword/AuthenticationChoosePasswordViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol AuthenticationChoosePasswordViewModelProtocol { - var callback: (@MainActor (AuthenticationChoosePasswordViewModelResult) -> Void)? { get set } var context: AuthenticationChoosePasswordViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Authentication/ChoosePassword/Coordinator/AuthenticationChoosePasswordCoordinator.swift b/RiotSwiftUI/Modules/Authentication/ChoosePassword/Coordinator/AuthenticationChoosePasswordCoordinator.swift index 9d02e4000..df3f7d793 100644 --- a/RiotSwiftUI/Modules/Authentication/ChoosePassword/Coordinator/AuthenticationChoosePasswordCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/ChoosePassword/Coordinator/AuthenticationChoosePasswordCoordinator.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import SwiftUI import CommonKit +import SwiftUI struct AuthenticationChoosePasswordCoordinatorParameters { let loginWizard: LoginWizard @@ -29,7 +29,6 @@ enum AuthenticationChoosePasswordCoordinatorResult { } final class AuthenticationChoosePasswordCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -79,7 +78,7 @@ final class AuthenticationChoosePasswordCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.authenticationChoosePasswordHostingController + authenticationChoosePasswordHostingController } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Authentication/ChoosePassword/MockAuthenticationChoosePasswordScreenState.swift b/RiotSwiftUI/Modules/Authentication/ChoosePassword/MockAuthenticationChoosePasswordScreenState.swift index c18b1398a..afe021fbc 100644 --- a/RiotSwiftUI/Modules/Authentication/ChoosePassword/MockAuthenticationChoosePasswordScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/ChoosePassword/MockAuthenticationChoosePasswordScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,7 +34,7 @@ enum MockAuthenticationChoosePasswordScreenState: MockScreenState, CaseIterable } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel: AuthenticationChoosePasswordViewModel switch self { case .emptyPassword: diff --git a/RiotSwiftUI/Modules/Authentication/ChoosePassword/Test/UI/AuthenticationChoosePasswordUITests.swift b/RiotSwiftUI/Modules/Authentication/ChoosePassword/Test/UI/AuthenticationChoosePasswordUITests.swift index d1fffa0fb..2d6539e1f 100644 --- a/RiotSwiftUI/Modules/Authentication/ChoosePassword/Test/UI/AuthenticationChoosePasswordUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/ChoosePassword/Test/UI/AuthenticationChoosePasswordUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class AuthenticationChoosePasswordUITests: MockScreenTestCase { func testEmptyPassword() { @@ -93,7 +93,6 @@ class AuthenticationChoosePasswordUITests: MockScreenTestCase { XCTAssertTrue(signoutAllDevicesToggle.exists, "Sign out all devices toggle should exist") XCTAssertTrue(signoutAllDevicesToggle.isOn, "Sign out all devices should be checked") } - } extension XCUIElement { diff --git a/RiotSwiftUI/Modules/Authentication/ChoosePassword/Test/Unit/AuthenticationChoosePasswordViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/ChoosePassword/Test/Unit/AuthenticationChoosePasswordViewModelTests.swift index ac9219ea1..34d236e09 100644 --- a/RiotSwiftUI/Modules/Authentication/ChoosePassword/Test/Unit/AuthenticationChoosePasswordViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/ChoosePassword/Test/Unit/AuthenticationChoosePasswordViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import XCTest @testable import RiotSwiftUI class AuthenticationChoosePasswordViewModelTests: XCTestCase { - @MainActor func testInitialState() async { let viewModel = AuthenticationChoosePasswordViewModel() let context = viewModel.context @@ -29,5 +28,4 @@ class AuthenticationChoosePasswordViewModelTests: XCTestCase { XCTAssert(context.viewState.hasInvalidPassword, "The view model should start with an invalid password.") XCTAssertFalse(context.signoutAllDevices, "The view model should start with sign out of all devices unchecked.") } - } diff --git a/RiotSwiftUI/Modules/Authentication/ChoosePassword/View/AuthenticationChoosePasswordScreen.swift b/RiotSwiftUI/Modules/Authentication/ChoosePassword/View/AuthenticationChoosePasswordScreen.swift index ff76409d6..704ca6a36 100644 --- a/RiotSwiftUI/Modules/Authentication/ChoosePassword/View/AuthenticationChoosePasswordScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/ChoosePassword/View/AuthenticationChoosePasswordScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct AuthenticationChoosePasswordScreen: View { - // MARK: - Properties // MARK: Private @@ -100,7 +99,7 @@ struct AuthenticationChoosePasswordScreen: View { configuration: UIKitTextInputConfiguration(returnKeyType: .done, isSecureTextEntry: true), onCommit: submit) - .accessibilityIdentifier("passwordTextField") + .accessibilityIdentifier("passwordTextField") } /// Sends the `send` view action so long as a valid email address has been input. diff --git a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationHomeserverViewData.swift b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationHomeserverViewData.swift index c5fdf1bb4..7952e7db3 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationHomeserverViewData.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationHomeserverViewData.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,11 +37,11 @@ extension AuthenticationHomeserverViewData { showLoginForm: true, showRegistrationForm: true, ssoIdentityProviders: [ - SSOIdentityProvider(id: "1", name: "Apple", brand: "apple", iconURL: nil), - SSOIdentityProvider(id: "2", name: "Facebook", brand: "facebook", iconURL: nil), - SSOIdentityProvider(id: "3", name: "GitHub", brand: "github", iconURL: nil), - SSOIdentityProvider(id: "4", name: "GitLab", brand: "gitlab", iconURL: nil), - SSOIdentityProvider(id: "5", name: "Google", brand: "google", iconURL: nil) + SSOIdentityProvider(id: "1", name: "Apple", brand: "apple", iconURL: nil), + SSOIdentityProvider(id: "2", name: "Facebook", brand: "facebook", iconURL: nil), + SSOIdentityProvider(id: "3", name: "GitHub", brand: "github", iconURL: nil), + SSOIdentityProvider(id: "4", name: "GitLab", brand: "gitlab", iconURL: nil), + SSOIdentityProvider(id: "5", name: "Google", brand: "google", iconURL: nil) ]) } @@ -68,5 +68,4 @@ extension AuthenticationHomeserverViewData { showRegistrationForm: false, ssoIdentityProviders: []) } - } diff --git a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift index 1e78da544..e59aa0189 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -78,7 +78,7 @@ enum LoginError: String, Error { case resetPasswordNotStarted } -@objcMembers +@objcMembers class HomeserverAddress: NSObject { /// Sanitizes a user entered homeserver address with the following rules /// - Trim any whitespace. diff --git a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationSSOButton.swift b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationSSOButton.swift index 767ed7d18..6f440d943 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationSSOButton.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationSSOButton.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import SwiftUI /// A button that displays the icon and name of an SSO provider. struct AuthenticationSSOButton: View { - // MARK: - Constants enum Brand: String { diff --git a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationServerInfoSection.swift b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationServerInfoSection.swift index edfc703c4..ea62c52c6 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/AuthenticationServerInfoSection.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/AuthenticationServerInfoSection.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import SwiftUI /// A view that shows information about the chosen homeserver, /// along with an edit button to pick a different one. struct AuthenticationServerInfoSection: View { - // MARK: - Private @Environment(\.theme) private var theme diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationPendingData.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationPendingData.swift index 2aa4bc499..6f637d5b9 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationPendingData.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationPendingData.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationRestClient.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationRestClient.swift index 058ac3d18..33d20f17b 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationRestClient.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationRestClient.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,7 @@ import Foundation protocol AuthenticationRestClient: AnyObject { // MARK: Configuration + var homeserver: String! { get } var identityServer: String! { get set } var credentials: MXCredentials! { get } @@ -26,24 +27,27 @@ protocol AuthenticationRestClient: AnyObject { init(homeServer: URL, unrecognizedCertificateHandler handler: MXHTTPClientOnUnrecognizedCertificate?) // MARK: Login + var loginFallbackURL: URL { get } func wellKnown() async throws -> MXWellKnown func getLoginSession() async throws -> MXAuthenticationSession func login(parameters: LoginParameters) async throws -> MXCredentials - func login(parameters: [String : Any]) async throws -> MXCredentials + func login(parameters: [String: Any]) async throws -> MXCredentials // MARK: Registration + var registerFallbackURL: URL { get } func getRegisterSession() async throws -> MXAuthenticationSession func isUsernameAvailable(_ username: String) async throws -> Bool func register(parameters: RegistrationParameters) async throws -> MXLoginResponse - func register(parameters: [String : Any]) async throws -> MXLoginResponse + func register(parameters: [String: Any]) async throws -> MXLoginResponse func requestTokenDuringRegistration(for threePID: RegisterThreePID, clientSecret: String, sendAttempt: UInt) async throws -> RegistrationThreePIDTokenResponse // MARK: Forgot Password + func forgetPassword(for email: String, clientSecret: String, sendAttempt: UInt) async throws -> String func resetPassword(parameters: CheckResetPasswordParameters) async throws - func resetPassword(parameters: [String : Any]) async throws + func resetPassword(parameters: [String: Any]) async throws } extension MXRestClient: AuthenticationRestClient { } diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift index 9e0372a0e..2f9a5f274 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,7 +39,6 @@ protocol AuthenticationServiceDelegate: AnyObject { @objcMembers class AuthenticationService: NSObject { - /// The shared service object. static let shared = AuthenticationService() @@ -97,7 +96,7 @@ class AuthenticationService: NSObject { let hsUrl = universalLink.homeserverUrl let isUrl = universalLink.identityServerUrl - if hsUrl == nil && isUrl == nil { + if hsUrl == nil, isUrl == nil { MXLog.debug("[AuthenticationService] handleServerProvisioningLink: no hsUrl or isUrl") return false } @@ -166,7 +165,7 @@ class AuthenticationService: NSObject { // The state and client are set after trying the registration flow to // ensure the existing state isn't wiped out when an error occurs. - self.state = AuthenticationState(flow: flow, homeserver: homeserver) + state = AuthenticationState(flow: flow, homeserver: homeserver) self.client = client } @@ -200,9 +199,9 @@ class AuthenticationService: NSObject { // completeness revert to the default homeserver if requested anyway. let address = useDefaultServer ? BuildSettings.serverConfigDefaultHomeserverUrlString : state.homeserver.addressFromUser ?? state.homeserver.address let identityServer = state.identityServer - self.state = AuthenticationState(flow: .login, - homeserverAddress: address, - identityServer: identityServer) + state = AuthenticationState(flow: .login, + homeserverAddress: address, + identityServer: identityServer) } /// Continues an SSO flow when completion comes via a deep link. @@ -289,7 +288,7 @@ class AuthenticationService: NSObject { let identityProviders = loginFlowResponse.flows?.compactMap { $0 as? MXLoginSSOFlow }.first?.identityProviders ?? [] return LoginFlowResult(supportedLoginTypes: loginFlowResponse.flows?.compactMap { $0 } ?? [], - ssoIdentityProviders: identityProviders.sorted { $0.name < $1.name }.map { $0.ssoIdentityProvider }, + ssoIdentityProviders: identityProviders.sorted { $0.name < $1.name }.map(\.ssoIdentityProvider), homeserverAddress: client.homeserver) } diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationState.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationState.swift index cac59552f..e2a48e315 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationState.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/AuthenticationState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,7 +29,7 @@ struct AuthenticationState { init(flow: AuthenticationFlow, homeserverAddress: String, identityServer: String? = nil) { self.flow = flow - self.homeserver = Homeserver(address: homeserverAddress) + homeserver = Homeserver(address: homeserverAddress) self.identityServer = identityServer } @@ -72,7 +72,7 @@ struct AuthenticationState { /// Needs authentication fallback for login var needsLoginFallback: Bool { - return preferredLoginMode.isUnsupported + preferredLoginMode.isUnsupported } /// Needs authentication fallback for registration diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginModels.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginModels.swift index c3332146c..90c12f503 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -93,4 +93,3 @@ enum LoginMode { struct ResetPasswordData { let addThreePIDSessionID: String } - diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginParameters.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginParameters.swift index 4e4bacb15..6146b3999 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginParameters.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginParameters.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -110,7 +110,7 @@ struct CheckResetPasswordParameters: DictionaryEncodable { } init(clientSecret: String, sessionID: String, newPassword: String, signoutAllDevices: Bool) { - self.auth = AuthenticationParameters.resetPasswordParameters(clientSecret: clientSecret, sessionID: sessionID) + auth = AuthenticationParameters.resetPasswordParameters(clientSecret: clientSecret, sessionID: sessionID) self.newPassword = newPassword self.signoutAllDevices = signoutAllDevices } diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginWizard.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginWizard.swift index f56319f37..f31902506 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginWizard.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/LoginWizard.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -40,7 +40,7 @@ class LoginWizard { self.client = client self.sessionCreator = sessionCreator - self.state = State() + state = State() } /// Login to the homeserver. @@ -78,9 +78,9 @@ class LoginWizard { } let credentials = try await client.login(parameters: parameters) - return sessionCreator.createSession(credentials: credentials, - client: client, - removeOtherAccounts: removeOtherAccounts) + return await sessionCreator.createSession(credentials: credentials, + client: client, + removeOtherAccounts: removeOtherAccounts) } /// Exchange a login token to an access token. @@ -91,9 +91,9 @@ class LoginWizard { func login(with token: String, removeOtherAccounts: Bool = false) async throws -> MXSession { let parameters = LoginTokenParameters(token: token) let credentials = try await client.login(parameters: parameters) - return sessionCreator.createSession(credentials: credentials, - client: client, - removeOtherAccounts: removeOtherAccounts) + return await sessionCreator.createSession(credentials: credentials, + client: client, + removeOtherAccounts: removeOtherAccounts) } /// Ask the homeserver to reset the user password. The password will not be diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationModels.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationModels.swift index af3ffbf78..590941353 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -114,7 +114,7 @@ struct FlowResult { } /// Whether fallback registration should be used due to unsupported stages. - var needsFallback : Bool { + var needsFallback: Bool { missingStages.filter(\.isMandatory).contains { stage in if case .other = stage { return true } else { return false } } diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationParameters.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationParameters.swift index d4a1416e9..cdc336e30 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationParameters.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationParameters.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationWizard.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationWizard.swift index 9f05a582b..76b470ef9 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationWizard.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/RegistrationWizard.swift @@ -63,7 +63,7 @@ class RegistrationWizard { self.client = client self.sessionCreator = sessionCreator - self.state = State() + state = State() } /// Call this method to get the possible registration flow of the current homeserver. @@ -164,7 +164,7 @@ class RegistrationWizard { /// Send the code received by SMS to validate a msisdn. /// If the code is correct, the registration request will be executed to validate the msisdn. func handleValidateThreePID(code: String) async throws -> RegistrationResult { - return try await validateThreePid(code: code) + try await validateThreePid(code: code) } /// Useful to poll the homeserver when waiting for the email to be validated by the user. @@ -197,7 +197,6 @@ class RegistrationWizard { throw RegistrationError.missingThreePIDURL } - let validationBody = ThreePIDValidationCodeBody(clientSecret: state.clientSecret, sessionID: threePIDData.registrationResponse.sessionID, code: code) @@ -255,7 +254,7 @@ class RegistrationWizard { do { let response = try await client.register(parameters: parameters) let credentials = MXCredentials(loginResponse: response, andDefaultCredentials: client.credentials) - return .success(sessionCreator.createSession(credentials: credentials, client: client, removeOtherAccounts: false)) + return await .success(sessionCreator.createSession(credentials: credentials, client: client, removeOtherAccounts: false)) } catch { let nsError = error as NSError @@ -278,7 +277,7 @@ class RegistrationWizard { /// Checks for a dummy stage and handles it automatically when possible. private func handleDummyStage(flowResult: FlowResult) async throws -> RegistrationResult { // If the dummy stage is mandatory, do the dummy stage now - guard flowResult.missingStages.contains(where: { $0.isDummy }) else { return .flowResponse(flowResult) } + guard flowResult.missingStages.contains(where: \.isDummy) else { return .flowResponse(flowResult) } return try await dummy() } diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/SessionCreator.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/SessionCreator.swift index c51f2bb61..78d183924 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/SessionCreator.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/SessionCreator.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,18 +23,19 @@ protocol SessionCreatorProtocol { /// - client: The client that completed the authentication. /// - removeOtherAccounts: Flag to remove other accounts than the account specified with the `credentials.userId`. /// - Returns: A new `MXSession` for the account. + @MainActor func createSession(credentials: MXCredentials, client: AuthenticationRestClient, removeOtherAccounts: Bool) -> MXSession } /// A struct that provides common functionality to create a new session. struct SessionCreator: SessionCreatorProtocol { - private let accountManager: MXKAccountManager init(withAccountManager accountManager: MXKAccountManager = .shared()) { self.accountManager = accountManager } + @MainActor func createSession(credentials: MXCredentials, client: AuthenticationRestClient, removeOtherAccounts: Bool) -> MXSession { // Use identity server provided in the client if credentials.identityServer == nil { @@ -42,7 +43,7 @@ struct SessionCreator: SessionCreatorProtocol { } if removeOtherAccounts { - let otherAccounts = accountManager.accounts.filter({ $0.mxCredentials.userId != credentials.userId }) + let otherAccounts = accountManager.accounts.filter { $0.mxCredentials.userId != credentials.userId } for account in otherAccounts { accountManager.removeAccount(account, completion: nil) } diff --git a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/ThreePIDModels.swift b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/ThreePIDModels.swift index 478cbf9f2..852f6a60a 100644 --- a/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/ThreePIDModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Common/Service/MatrixSDK/ThreePIDModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -53,13 +53,13 @@ struct RegistrationThreePIDTokenResponse { /// /// If this field is not present, the client can assume that verification will happen without the client's involvement provided /// the homeserver advertises this specification version in the /versions response (ie: r0.5.0). - var submitURL: String? = nil + var submitURL: String? // MARK: - Additional data that may be needed - var msisdn: String? = nil - var formattedMSISDN: String? = nil - var success: Bool? = nil + var msisdn: String? + var formattedMSISDN: String? + var success: Bool? enum CodingKeys: String, CodingKey { case sessionID = "sid" diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordModels.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordModels.swift index 788a33552..6d5f60a30 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordModels.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordViewModel.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordViewModel.swift index 461e064a6..e9ec459b3 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordViewModel.swift @@ -16,11 +16,9 @@ import SwiftUI -typealias AuthenticationForgotPasswordViewModelType = StateStoreViewModel -class AuthenticationForgotPasswordViewModel: AuthenticationForgotPasswordViewModelType, AuthenticationForgotPasswordViewModelProtocol { +typealias AuthenticationForgotPasswordViewModelType = StateStoreViewModel +class AuthenticationForgotPasswordViewModel: AuthenticationForgotPasswordViewModelType, AuthenticationForgotPasswordViewModelProtocol { // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordViewModelProtocol.swift index 673138290..baffd45d1 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/AuthenticationForgotPasswordViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol AuthenticationForgotPasswordViewModelProtocol { - var callback: (@MainActor (AuthenticationForgotPasswordViewModelResult) -> Void)? { get set } var context: AuthenticationForgotPasswordViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Coordinator/AuthenticationForgotPasswordCoordinator.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Coordinator/AuthenticationForgotPasswordCoordinator.swift index 3f19180cf..deea1e803 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Coordinator/AuthenticationForgotPasswordCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Coordinator/AuthenticationForgotPasswordCoordinator.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import SwiftUI import CommonKit +import SwiftUI struct AuthenticationForgotPasswordCoordinatorParameters { let navigationRouter: NavigationRouterType @@ -32,7 +32,6 @@ enum AuthenticationForgotPasswordCoordinatorResult { } final class AuthenticationForgotPasswordCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -83,7 +82,7 @@ final class AuthenticationForgotPasswordCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.authenticationForgotPasswordHostingController + authenticationForgotPasswordHostingController } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/MockAuthenticationForgotPasswordScreenState.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/MockAuthenticationForgotPasswordScreenState.swift index 1d126e998..e69cbee41 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/MockAuthenticationForgotPasswordScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/MockAuthenticationForgotPasswordScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,7 +33,7 @@ enum MockAuthenticationForgotPasswordScreenState: MockScreenState, CaseIterable } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel: AuthenticationForgotPasswordViewModel switch self { case .emptyAddress: diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/UI/AuthenticationForgotPasswordUITests.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/UI/AuthenticationForgotPasswordUITests.swift index 06478ce2a..f633349a2 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/UI/AuthenticationForgotPasswordUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/UI/AuthenticationForgotPasswordUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class AuthenticationForgotPasswordUITests: MockScreenTestCase { func testEmptyAddress() { @@ -98,5 +98,4 @@ class AuthenticationForgotPasswordUITests: MockScreenTestCase { XCTAssertTrue(backButton.exists, "Back button should be shown.") XCTAssertEqual(backButton.label, "Back") } - } diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/Unit/AuthenticationForgotPasswordViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/Unit/AuthenticationForgotPasswordViewModelTests.swift index c05dfccaa..925370625 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/Unit/AuthenticationForgotPasswordViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/Test/Unit/AuthenticationForgotPasswordViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import XCTest @testable import RiotSwiftUI class AuthenticationForgotPasswordViewModelTests: XCTestCase { - var viewModel: AuthenticationForgotPasswordViewModelProtocol! var context: AuthenticationForgotPasswordViewModelType.Context! diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/View/AuthenticationForgotPasswordForm.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/View/AuthenticationForgotPasswordForm.swift index 9c19f94fb..815831159 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/View/AuthenticationForgotPasswordForm.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/View/AuthenticationForgotPasswordForm.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import SwiftUI /// The form shown to enter an email address. struct AuthenticationForgotPasswordForm: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Authentication/ForgotPassword/View/AuthenticationForgotPasswordScreen.swift b/RiotSwiftUI/Modules/Authentication/ForgotPassword/View/AuthenticationForgotPasswordScreen.swift index a2a6619e1..423f4d8b6 100644 --- a/RiotSwiftUI/Modules/Authentication/ForgotPassword/View/AuthenticationForgotPasswordScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/ForgotPassword/View/AuthenticationForgotPasswordScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct AuthenticationForgotPasswordScreen: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginModels.swift b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginModels.swift index 823a56499..e273d0d16 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginModels.swift @@ -57,7 +57,7 @@ struct AuthenticationLoginViewState: BindableState { /// Data about the selected homeserver. var homeserver: AuthenticationHomeserverViewData /// Whether a new homeserver is currently being loaded. - var isLoading: Bool = false + var isLoading = false /// View state that can be bound to from SwiftUI. var bindings: AuthenticationLoginBindings diff --git a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModel.swift b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModel.swift index aa73f0586..6c5274d62 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModel.swift @@ -16,12 +16,9 @@ import SwiftUI -typealias AuthenticationLoginViewModelType = StateStoreViewModel +typealias AuthenticationLoginViewModelType = StateStoreViewModel class AuthenticationLoginViewModel: AuthenticationLoginViewModelType, AuthenticationLoginViewModelProtocol { - // MARK: - Properties // MARK: Public @@ -52,7 +49,7 @@ class AuthenticationLoginViewModel: AuthenticationLoginViewModelType, Authentica case .fallback: Task { await callback?(.fallback) } case .continueWithSSO(let provider): - Task { await callback?(.continueWithSSO(provider))} + Task { await callback?(.continueWithSSO(provider)) } } } diff --git a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModelProtocol.swift index afe9d3a92..87ef46fce 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/AuthenticationLoginViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol AuthenticationLoginViewModelProtocol { - var callback: (@MainActor (AuthenticationLoginViewModelResult) -> Void)? { get set } var context: AuthenticationLoginViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift b/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift index 080917097..596e1cad7 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/Coordinator/AuthenticationLoginCoordinator.swift @@ -14,9 +14,9 @@ // limitations under the License. // -import SwiftUI import CommonKit import MatrixSDK +import SwiftUI struct AuthenticationLoginCoordinatorParameters { let navigationRouter: NavigationRouterType @@ -47,7 +47,6 @@ enum AuthenticationLoginCoordinatorResult: CustomStringConvertible { } final class AuthenticationLoginCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -96,6 +95,7 @@ final class AuthenticationLoginCoordinator: Coordinator, Presentable { } // MARK: - Public + func start() { MXLog.debug("[AuthenticationLoginCoordinator] did start.") Task { await setupViewModel() } diff --git a/RiotSwiftUI/Modules/Authentication/Login/MockAuthenticationLoginScreenState.swift b/RiotSwiftUI/Modules/Authentication/Login/MockAuthenticationLoginScreenState.swift index 1e242c7e1..09ffa9ce6 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/MockAuthenticationLoginScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/MockAuthenticationLoginScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,7 +35,7 @@ enum MockAuthenticationLoginScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel: AuthenticationLoginViewModel switch self { case .matrixDotOrg: @@ -52,7 +52,6 @@ enum MockAuthenticationLoginScreenState: MockScreenState, CaseIterable { viewModel = AuthenticationLoginViewModel(homeserver: .mockFallback) } - // can simulate service and viewModel actions here if needs be. return ( diff --git a/RiotSwiftUI/Modules/Authentication/Login/Test/UI/AuthenticationLoginUITests.swift b/RiotSwiftUI/Modules/Authentication/Login/Test/UI/AuthenticationLoginUITests.swift index 8dcf6eb63..8075ccc9f 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/Test/UI/AuthenticationLoginUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/Test/UI/AuthenticationLoginUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class AuthenticationLoginUITests: MockScreenTestCase { func testMatrixDotOrg() { @@ -120,5 +120,4 @@ class AuthenticationLoginUITests: MockScreenTestCase { XCTAssertTrue(fallbackButton.exists, "The fallback button should be shown for \(state).") XCTAssertTrue(fallbackButton.isEnabled, "The fallback button should be enabled for \(state).") } - } diff --git a/RiotSwiftUI/Modules/Authentication/Login/Test/Unit/AuthenticationLoginViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/Login/Test/Unit/AuthenticationLoginViewModelTests.swift index dde66c2fb..ae9b3b7ec 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/Test/Unit/AuthenticationLoginViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/Test/Unit/AuthenticationLoginViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift b/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift index 8e4a5776f..3ff67aaf2 100644 --- a/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/Login/View/AuthenticationLoginScreen.swift @@ -17,7 +17,6 @@ import SwiftUI struct AuthenticationLoginScreen: View { - // MARK: - Properties // MARK: Private @@ -52,7 +51,7 @@ struct AuthenticationLoginScreen: View { loginForm } - if viewModel.viewState.homeserver.showLoginForm && viewModel.viewState.showSSOButtons { + if viewModel.viewState.homeserver.showLoginForm, viewModel.viewState.showSSOButtons { Text(VectorL10n.or) .foregroundColor(theme.colors.secondaryContent) .padding(.top, 16) @@ -63,10 +62,9 @@ struct AuthenticationLoginScreen: View { .padding(.top, 16) } - if !viewModel.viewState.homeserver.showLoginForm && !viewModel.viewState.showSSOButtons { + if !viewModel.viewState.homeserver.showLoginForm, !viewModel.viewState.showSSOButtons { fallbackButton } - } .readableFrame() .padding(.horizontal, 16) @@ -104,8 +102,8 @@ struct AuthenticationLoginScreen: View { autocorrectionType: .no), onEditingChanged: usernameEditingChanged, onCommit: { isPasswordFocused = true }) - .accessibilityIdentifier("usernameTextField") - .padding(.bottom, 7) + .accessibilityIdentifier("usernameTextField") + .padding(.bottom, 7) RoundedBorderTextField(placeHolder: VectorL10n.authPasswordPlaceholder, text: $viewModel.password, @@ -114,7 +112,7 @@ struct AuthenticationLoginScreen: View { isSecureTextEntry: true), onEditingChanged: passwordEditingChanged, onCommit: submit) - .accessibilityIdentifier("passwordTextField") + .accessibilityIdentifier("passwordTextField") Button { viewModel.send(viewAction: .forgotPassword) } label: { Text(VectorL10n.authenticationLoginForgotPassword) diff --git a/RiotSwiftUI/Modules/Authentication/ReCaptcha/AuthenticationReCaptchaModels.swift b/RiotSwiftUI/Modules/Authentication/ReCaptcha/AuthenticationReCaptchaModels.swift index 27b95d713..7c0f90bc5 100644 --- a/RiotSwiftUI/Modules/Authentication/ReCaptcha/AuthenticationReCaptchaModels.swift +++ b/RiotSwiftUI/Modules/Authentication/ReCaptcha/AuthenticationReCaptchaModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Authentication/ReCaptcha/AuthenticationReCaptchaViewModel.swift b/RiotSwiftUI/Modules/Authentication/ReCaptcha/AuthenticationReCaptchaViewModel.swift index 08a733d93..755675e1f 100644 --- a/RiotSwiftUI/Modules/Authentication/ReCaptcha/AuthenticationReCaptchaViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/ReCaptcha/AuthenticationReCaptchaViewModel.swift @@ -16,12 +16,9 @@ import SwiftUI -typealias AuthenticationReCaptchaViewModelType = StateStoreViewModel +typealias AuthenticationReCaptchaViewModelType = StateStoreViewModel class AuthenticationReCaptchaViewModel: AuthenticationReCaptchaViewModelType, AuthenticationReCaptchaViewModelProtocol { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Authentication/ReCaptcha/AuthenticationReCaptchaViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/ReCaptcha/AuthenticationReCaptchaViewModelProtocol.swift index d29a25f13..097af6e9c 100644 --- a/RiotSwiftUI/Modules/Authentication/ReCaptcha/AuthenticationReCaptchaViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/ReCaptcha/AuthenticationReCaptchaViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol AuthenticationReCaptchaViewModelProtocol { - var callback: (@MainActor (AuthenticationReCaptchaViewModelResult) -> Void)? { get set } var context: AuthenticationReCaptchaViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Authentication/ReCaptcha/Coordinator/AuthenticationReCaptchaCoordinator.swift b/RiotSwiftUI/Modules/Authentication/ReCaptcha/Coordinator/AuthenticationReCaptchaCoordinator.swift index 356beb6cb..efeafb184 100644 --- a/RiotSwiftUI/Modules/Authentication/ReCaptcha/Coordinator/AuthenticationReCaptchaCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/ReCaptcha/Coordinator/AuthenticationReCaptchaCoordinator.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import SwiftUI import CommonKit +import SwiftUI struct AuthenticationReCaptchaCoordinatorParameters { let registrationWizard: RegistrationWizard @@ -26,7 +26,6 @@ struct AuthenticationReCaptchaCoordinatorParameters { } final class AuthenticationReCaptchaCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -76,7 +75,7 @@ final class AuthenticationReCaptchaCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.authenticationReCaptchaHostingController + authenticationReCaptchaHostingController } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Authentication/ReCaptcha/MockAuthenticationReCaptchaScreenState.swift b/RiotSwiftUI/Modules/Authentication/ReCaptcha/MockAuthenticationReCaptchaScreenState.swift index 057ce257f..d96456d0f 100644 --- a/RiotSwiftUI/Modules/Authentication/ReCaptcha/MockAuthenticationReCaptchaScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/ReCaptcha/MockAuthenticationReCaptchaScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,7 +31,7 @@ enum MockAuthenticationReCaptchaScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel: AuthenticationReCaptchaViewModel switch self { case .standard: diff --git a/RiotSwiftUI/Modules/Authentication/ReCaptcha/Test/UI/AuthenticationReCaptchaUITests.swift b/RiotSwiftUI/Modules/Authentication/ReCaptcha/Test/UI/AuthenticationReCaptchaUITests.swift index ac8ff628c..450d5bfe1 100644 --- a/RiotSwiftUI/Modules/Authentication/ReCaptcha/Test/UI/AuthenticationReCaptchaUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/ReCaptcha/Test/UI/AuthenticationReCaptchaUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class AuthenticationReCaptchaUITests: MockScreenTestCase { // Nothing to test as the view only has a single state. diff --git a/RiotSwiftUI/Modules/Authentication/ReCaptcha/Test/Unit/AuthenticationReCaptchaViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/ReCaptcha/Test/Unit/AuthenticationReCaptchaViewModelTests.swift index 1d8d15b37..37d76e135 100644 --- a/RiotSwiftUI/Modules/Authentication/ReCaptcha/Test/Unit/AuthenticationReCaptchaViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/ReCaptcha/Test/Unit/AuthenticationReCaptchaViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaScreen.swift b/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaScreen.swift index 41dd2d896..16dc70bf9 100644 --- a/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct AuthenticationReCaptchaScreen: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaWebView.swift b/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaWebView.swift index fbd000cb5..06e7ff784 100644 --- a/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaWebView.swift +++ b/RiotSwiftUI/Modules/Authentication/ReCaptcha/View/AuthenticationReCaptchaWebView.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import SwiftUI import WebKit struct AuthenticationRecaptchaWebView: UIViewRepresentable { - // MARK: - Properties // MARK: Public @@ -84,7 +83,7 @@ struct AuthenticationRecaptchaWebView: UIViewRepresentable { var completion: ((String) -> Void)? init(isLoading: Binding) { - self._isLoading = isLoading + _isLoading = isLoading } /// Generates the HTML page to show for the given `siteKey` and `theme`. @@ -134,4 +133,3 @@ struct AuthenticationRecaptchaWebView: UIViewRepresentable { } } } - diff --git a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationModels.swift b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationModels.swift index 1652a3978..830edf3fc 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -62,7 +62,7 @@ struct AuthenticationRegistrationViewState: BindableState { /// Data about the selected homeserver. var homeserver: AuthenticationHomeserverViewData /// Whether a new homeserver is currently being loaded. - var isLoading: Bool = false + var isLoading = false /// View state that can be bound to from SwiftUI. var bindings: AuthenticationRegistrationBindings /// Whether or not the username field has been edited yet. diff --git a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModel.swift b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModel.swift index 9b2e1d699..dcfe72a7a 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModel.swift @@ -14,15 +14,12 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI -typealias AuthenticationRegistrationViewModelType = StateStoreViewModel +typealias AuthenticationRegistrationViewModelType = StateStoreViewModel class AuthenticationRegistrationViewModel: AuthenticationRegistrationViewModelType, AuthenticationRegistrationViewModelProtocol { - // MARK: - Properties // MARK: Public diff --git a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModelProtocol.swift index 036f720e7..96468ba56 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/AuthenticationRegistrationViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol AuthenticationRegistrationViewModelProtocol { - var callback: (@MainActor (AuthenticationRegistrationViewModelResult) -> Void)? { get set } var context: AuthenticationRegistrationViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift b/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift index f749aa32b..72fa8f441 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/Coordinator/AuthenticationRegistrationCoordinator.swift @@ -14,9 +14,9 @@ // limitations under the License. // -import SwiftUI import CommonKit import MatrixSDK +import SwiftUI struct AuthenticationRegistrationCoordinatorParameters { let navigationRouter: NavigationRouterType @@ -49,7 +49,6 @@ enum AuthenticationRegistrationCoordinatorResult: CustomStringConvertible { } final class AuthenticationRegistrationCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -97,13 +96,14 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable { } // MARK: - Public + func start() { MXLog.debug("[AuthenticationRegistrationCoordinator] did start.") Task { await setupViewModel() } } func toPresentable() -> UIViewController { - return self.authenticationRegistrationHostingController + authenticationRegistrationHostingController } // MARK: - Private @@ -117,7 +117,7 @@ final class AuthenticationRegistrationCoordinator: Coordinator, Presentable { switch result { case .selectServer: self.presentServerSelectionScreen() - case.validateUsername(let username): + case .validateUsername(let username): self.validateUsername(username) case .createAccount(let username, let password): self.createAccount(username: username, password: password) diff --git a/RiotSwiftUI/Modules/Authentication/Registration/MockAuthenticationRegistrationScreenState.swift b/RiotSwiftUI/Modules/Authentication/Registration/MockAuthenticationRegistrationScreenState.swift index a85021a17..d3086def6 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/MockAuthenticationRegistrationScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/MockAuthenticationRegistrationScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,7 +36,7 @@ enum MockAuthenticationRegistrationScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel: AuthenticationRegistrationViewModel switch self { case .matrixDotOrg: @@ -58,7 +58,6 @@ enum MockAuthenticationRegistrationScreenState: MockScreenState, CaseIterable { viewModel = AuthenticationRegistrationViewModel(homeserver: .mockFallback) } - // can simulate service and viewModel actions here if needs be. return ( diff --git a/RiotSwiftUI/Modules/Authentication/Registration/Test/UI/AuthenticationRegistrationUITests.swift b/RiotSwiftUI/Modules/Authentication/Registration/Test/UI/AuthenticationRegistrationUITests.swift index 223d20c40..d49ecd580 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/Test/UI/AuthenticationRegistrationUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/Test/UI/AuthenticationRegistrationUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class AuthenticationRegistrationUITests: MockScreenTestCase { func testMatrixDotOrg() { @@ -88,7 +88,6 @@ class AuthenticationRegistrationUITests: MockScreenTestCase { validateFallbackButtonIsShown(for: state) } - /// Checks that the username and password text fields are shown along with the next button. func validateRegistrationFormIsVisible(for state: String) { let usernameTextField = app.textFields.element diff --git a/RiotSwiftUI/Modules/Authentication/Registration/Test/Unit/AuthenticationRegistrationViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/Registration/Test/Unit/AuthenticationRegistrationViewModelTests.swift index 2d5f3830f..734c174f9 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/Test/Unit/AuthenticationRegistrationViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/Test/Unit/AuthenticationRegistrationViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI @@ -110,7 +110,7 @@ import Combine XCTAssertEqual(context.viewState.usernameFooterMessage, VectorL10n.authenticationRegistrationUsernameFooterAvailable("@bob:matrix.org"), "The footer message should display that the username is available.") XCTAssertFalse(context.viewState.isUsernameInvalid, - "The username should continue to be valid when it is available.") + "The username should continue to be valid when it is available.") // When clearing the error. context.send(viewAction: .resetUsernameAvailability) @@ -190,7 +190,7 @@ import Combine // Then the username should be valid but the credentials should still be invalid. XCTAssertTrue(context.viewState.isPasswordInvalid, "An empty password should be invalid.") - XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid when unverified.") + XCTAssertFalse(context.viewState.isUsernameInvalid, "The username should be valid when unverified.") XCTAssertFalse(context.viewState.hasValidCredentials, "The credentials should be invalid.") } diff --git a/RiotSwiftUI/Modules/Authentication/Registration/View/AuthenticationRegistrationScreen.swift b/RiotSwiftUI/Modules/Authentication/Registration/View/AuthenticationRegistrationScreen.swift index 4f62560b3..c333f047a 100644 --- a/RiotSwiftUI/Modules/Authentication/Registration/View/AuthenticationRegistrationScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/Registration/View/AuthenticationRegistrationScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct AuthenticationRegistrationScreen: View { - // MARK: - Properties // MARK: Private @@ -50,7 +49,7 @@ struct AuthenticationRegistrationScreen: View { registrationForm } - if viewModel.viewState.homeserver.showRegistrationForm && viewModel.viewState.showSSOButtons { + if viewModel.viewState.homeserver.showRegistrationForm, viewModel.viewState.showSSOButtons { Text(VectorL10n.or) .foregroundColor(theme.colors.secondaryContent) .padding(.top, 16) @@ -61,10 +60,9 @@ struct AuthenticationRegistrationScreen: View { .padding(.top, 16) } - if !viewModel.viewState.homeserver.showRegistrationForm && !viewModel.viewState.showSSOButtons { + if !viewModel.viewState.homeserver.showRegistrationForm, !viewModel.viewState.showSSOButtons { fallbackButton } - } .readableFrame() .padding(.horizontal, 16) @@ -110,8 +108,8 @@ struct AuthenticationRegistrationScreen: View { autocorrectionType: .no), onEditingChanged: usernameEditingChanged, onCommit: { isPasswordFocused = true }) - .onChange(of: viewModel.username) { _ in viewModel.send(viewAction: .resetUsernameAvailability) } - .accessibilityIdentifier("usernameTextField") + .onChange(of: viewModel.username) { _ in viewModel.send(viewAction: .resetUsernameAvailability) } + .accessibilityIdentifier("usernameTextField") RoundedBorderTextField(title: nil, placeHolder: VectorL10n.authPasswordPlaceholder, @@ -123,7 +121,7 @@ struct AuthenticationRegistrationScreen: View { isSecureTextEntry: true), onEditingChanged: passwordEditingChanged, onCommit: submit) - .accessibilityIdentifier("passwordTextField") + .accessibilityIdentifier("passwordTextField") Button(action: submit) { Text(VectorL10n.next) diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionModels.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionModels.swift index 8f4a07f62..c94df50ca 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionModels.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModel.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModel.swift index 8a5326593..b7c6d7c09 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModel.swift @@ -16,12 +16,9 @@ import SwiftUI -typealias AuthenticationServerSelectionViewModelType = StateStoreViewModel +typealias AuthenticationServerSelectionViewModelType = StateStoreViewModel class AuthenticationServerSelectionViewModel: AuthenticationServerSelectionViewModelType, AuthenticationServerSelectionViewModelProtocol { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModelProtocol.swift index e57a2d86e..a4e8abe4a 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/AuthenticationServerSelectionViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol AuthenticationServerSelectionViewModelProtocol { - var callback: (@MainActor (AuthenticationServerSelectionViewModelResult) -> Void)? { get set } var context: AuthenticationServerSelectionViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift index 0f3888146..c5d521701 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import SwiftUI import CommonKit +import SwiftUI struct AuthenticationServerSelectionCoordinatorParameters { let authenticationService: AuthenticationService @@ -31,7 +31,6 @@ enum AuthenticationServerSelectionCoordinatorResult { } final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -78,7 +77,7 @@ final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.authenticationServerSelectionHostingController + authenticationServerSelectionHostingController } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/MockAuthenticationServerSelectionScreenState.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/MockAuthenticationServerSelectionScreenState.swift index c7e2f12c7..baf8faf53 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/MockAuthenticationServerSelectionScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/MockAuthenticationServerSelectionScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,7 +35,7 @@ enum MockAuthenticationServerSelectionScreenState: MockScreenState, CaseIterable } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel: AuthenticationServerSelectionViewModel switch self { case .matrix: diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift index e72e00ec4..1bdcd6698 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/UI/AuthenticationServerSelectionUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class AuthenticationServerSelectionUITests: MockScreenTestCase { func testRegisterState() { @@ -41,7 +41,6 @@ class AuthenticationServerSelectionUITests: MockScreenTestCase { XCTAssertTrue(dismissButton.exists, "The dismiss button should be shown during modal presentation.") } - func testLoginState() { app.goToScreenWithIdentifier(MockAuthenticationServerSelectionScreenState.login.title) diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/Unit/AuthenticationServerSelectionViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/Unit/AuthenticationServerSelectionViewModelTests.swift index ed75e67aa..ab62bf4aa 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/Unit/AuthenticationServerSelectionViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Test/Unit/AuthenticationServerSelectionViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift index 687d6ec92..54144006b 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct AuthenticationServerSelectionScreen: View { - // MARK: - Properties // MARK: Private @@ -37,7 +36,7 @@ struct AuthenticationServerSelectionScreen: View { // MARK: Views var body: some View { - GeometryReader { geometry in + GeometryReader { _ in ScrollView { VStack(spacing: 0) { header diff --git a/RiotSwiftUI/Modules/Authentication/SoftLogout/AuthenticationSoftLogoutModels.swift b/RiotSwiftUI/Modules/Authentication/SoftLogout/AuthenticationSoftLogoutModels.swift index 7222af762..c6bb92d12 100644 --- a/RiotSwiftUI/Modules/Authentication/SoftLogout/AuthenticationSoftLogoutModels.swift +++ b/RiotSwiftUI/Modules/Authentication/SoftLogout/AuthenticationSoftLogoutModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +17,7 @@ import SwiftUI // MARK: Data + struct SoftLogoutCredentials { let userId: String let homeserverName: String diff --git a/RiotSwiftUI/Modules/Authentication/SoftLogout/AuthenticationSoftLogoutViewModel.swift b/RiotSwiftUI/Modules/Authentication/SoftLogout/AuthenticationSoftLogoutViewModel.swift index bd9fcee28..f5b7971dc 100644 --- a/RiotSwiftUI/Modules/Authentication/SoftLogout/AuthenticationSoftLogoutViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/SoftLogout/AuthenticationSoftLogoutViewModel.swift @@ -16,11 +16,9 @@ import SwiftUI -typealias AuthenticationSoftLogoutViewModelType = StateStoreViewModel -class AuthenticationSoftLogoutViewModel: AuthenticationSoftLogoutViewModelType, AuthenticationSoftLogoutViewModelProtocol { +typealias AuthenticationSoftLogoutViewModelType = StateStoreViewModel +class AuthenticationSoftLogoutViewModel: AuthenticationSoftLogoutViewModelType, AuthenticationSoftLogoutViewModelProtocol { // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Authentication/SoftLogout/AuthenticationSoftLogoutViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/SoftLogout/AuthenticationSoftLogoutViewModelProtocol.swift index f4e948f5b..45f700d41 100644 --- a/RiotSwiftUI/Modules/Authentication/SoftLogout/AuthenticationSoftLogoutViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/SoftLogout/AuthenticationSoftLogoutViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol AuthenticationSoftLogoutViewModelProtocol { - var callback: (@MainActor (AuthenticationSoftLogoutViewModelResult) -> Void)? { get set } var context: AuthenticationSoftLogoutViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Authentication/SoftLogout/Coordinator/AuthenticationSoftLogoutCoordinator.swift b/RiotSwiftUI/Modules/Authentication/SoftLogout/Coordinator/AuthenticationSoftLogoutCoordinator.swift index 5b2fba433..a76ce9a0b 100644 --- a/RiotSwiftUI/Modules/Authentication/SoftLogout/Coordinator/AuthenticationSoftLogoutCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/SoftLogout/Coordinator/AuthenticationSoftLogoutCoordinator.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import SwiftUI import CommonKit +import SwiftUI struct AuthenticationSoftLogoutCoordinatorParameters { let navigationRouter: NavigationRouterType @@ -50,7 +50,6 @@ enum AuthenticationSoftLogoutCoordinatorResult: CustomStringConvertible { } final class AuthenticationSoftLogoutCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -107,7 +106,7 @@ final class AuthenticationSoftLogoutCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.authenticationSoftLogoutHostingController + authenticationSoftLogoutHostingController } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Authentication/SoftLogout/MockAuthenticationSoftLogoutScreenState.swift b/RiotSwiftUI/Modules/Authentication/SoftLogout/MockAuthenticationSoftLogoutScreenState.swift index 8efa80d67..8127665d5 100644 --- a/RiotSwiftUI/Modules/Authentication/SoftLogout/MockAuthenticationSoftLogoutScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/SoftLogout/MockAuthenticationSoftLogoutScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,7 +36,7 @@ enum MockAuthenticationSoftLogoutScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel: AuthenticationSoftLogoutViewModel let credentials = SoftLogoutCredentials(userId: "@mock:matrix.org", homeserverName: "matrix.org", diff --git a/RiotSwiftUI/Modules/Authentication/SoftLogout/Test/UI/AuthenticationSoftLogoutUITests.swift b/RiotSwiftUI/Modules/Authentication/SoftLogout/Test/UI/AuthenticationSoftLogoutUITests.swift index 52268610a..4487fee04 100644 --- a/RiotSwiftUI/Modules/Authentication/SoftLogout/Test/UI/AuthenticationSoftLogoutUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/SoftLogout/Test/UI/AuthenticationSoftLogoutUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class AuthenticationSoftLogoutUITests: MockScreenTestCase { func testEmptyPassword() { @@ -185,5 +185,4 @@ class AuthenticationSoftLogoutUITests: MockScreenTestCase { XCTAssertFalse(app.staticTexts["messageLabel2"].exists, "The message 2 should not be shown.") } - } diff --git a/RiotSwiftUI/Modules/Authentication/SoftLogout/Test/Unit/AuthenticationSoftLogoutViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/SoftLogout/Test/Unit/AuthenticationSoftLogoutViewModelTests.swift index 4d9c8142c..bacaf8638 100644 --- a/RiotSwiftUI/Modules/Authentication/SoftLogout/Test/Unit/AuthenticationSoftLogoutViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/SoftLogout/Test/Unit/AuthenticationSoftLogoutViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import XCTest @testable import RiotSwiftUI class AuthenticationSoftLogoutViewModelTests: XCTestCase { - @MainActor func testInitialStateForMatrixOrg() async { let credentials = SoftLogoutCredentials(userId: "mock_user_id", homeserverName: "https://matrix.org", @@ -55,5 +54,4 @@ class AuthenticationSoftLogoutViewModelTests: XCTestCase { XCTAssert(context.viewState.showLoginForm, "The view model should show login form for the given homeserver.") XCTAssertFalse(context.viewState.showRecoverEncryptionKeysMessage, "The view model should not show recover encryption keys message.") } - } diff --git a/RiotSwiftUI/Modules/Authentication/SoftLogout/View/AuthenticationSoftLogoutScreen.swift b/RiotSwiftUI/Modules/Authentication/SoftLogout/View/AuthenticationSoftLogoutScreen.swift index b7d20d88b..ced206e1c 100644 --- a/RiotSwiftUI/Modules/Authentication/SoftLogout/View/AuthenticationSoftLogoutScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/SoftLogout/View/AuthenticationSoftLogoutScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct AuthenticationSoftLogoutScreen: View { - // MARK: - Properties // MARK: Private @@ -153,7 +152,7 @@ struct AuthenticationSoftLogoutScreen: View { configuration: UIKitTextInputConfiguration(returnKeyType: .done, isSecureTextEntry: true), onCommit: login) - .accessibilityIdentifier("passwordTextField") + .accessibilityIdentifier("passwordTextField") } /// A list of SSO buttons that can be used for login. @@ -188,7 +187,6 @@ struct AuthenticationSoftLogoutScreen: View { func clearData() { viewModel.send(viewAction: .clearAllData) } - } // MARK: - Previews diff --git a/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsModels.swift b/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsModels.swift index 8ff128be2..88301c6cb 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsModels.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,7 +27,7 @@ struct AuthenticationTermsPolicy: Identifiable, Equatable { /// The policy's subtitle. let subtitle: String /// Whether or not the policy has been accepted. - var accepted: Bool = false + var accepted = false } // MARK: View model diff --git a/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsViewModel.swift b/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsViewModel.swift index e35366899..275eded69 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsViewModel.swift @@ -16,13 +16,9 @@ import SwiftUI - -typealias AuthenticationTermsViewModelType = StateStoreViewModel +typealias AuthenticationTermsViewModelType = StateStoreViewModel class AuthenticationTermsViewModel: AuthenticationTermsViewModelType, AuthenticationTermsViewModelProtocol { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsViewModelProtocol.swift index bd924d7cb..4a5bb4773 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/AuthenticationTermsViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol AuthenticationTermsViewModelProtocol { - var callback: (@MainActor (AuthenticationTermsViewModelResult) -> Void)? { get set } var context: AuthenticationTermsViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Authentication/Terms/Coordinator/AuthenticationTermsCoordinator.swift b/RiotSwiftUI/Modules/Authentication/Terms/Coordinator/AuthenticationTermsCoordinator.swift index d05e3a5aa..5baeb2d3f 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/Coordinator/AuthenticationTermsCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/Coordinator/AuthenticationTermsCoordinator.swift @@ -14,9 +14,9 @@ // limitations under the License. // -import SwiftUI import CommonKit import SafariServices +import SwiftUI struct AuthenticationTermsCoordinatorParameters { let registrationWizard: RegistrationWizard @@ -27,7 +27,6 @@ struct AuthenticationTermsCoordinatorParameters { } final class AuthenticationTermsCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -80,7 +79,7 @@ final class AuthenticationTermsCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.authenticationTermsHostingController + authenticationTermsHostingController } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Authentication/Terms/MockAuthenticationTermsScreenState.swift b/RiotSwiftUI/Modules/Authentication/Terms/MockAuthenticationTermsScreenState.swift index 0bb977c46..7624872e9 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/MockAuthenticationTermsScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/MockAuthenticationTermsScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,7 +33,7 @@ enum MockAuthenticationTermsScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel: AuthenticationTermsViewModel switch self { case .matrixDotOrg: diff --git a/RiotSwiftUI/Modules/Authentication/Terms/Test/UI/AuthenticationTermsUITests.swift b/RiotSwiftUI/Modules/Authentication/Terms/Test/UI/AuthenticationTermsUITests.swift index 3cb31c36a..c9f92a96c 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/Test/UI/AuthenticationTermsUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/Test/UI/AuthenticationTermsUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class AuthenticationTermsUITests: MockScreenTestCase { func testMatrixDotOrg() { @@ -38,5 +38,4 @@ class AuthenticationTermsUITests: MockScreenTestCase { XCTAssertTrue(nextButton.exists, "The next button should always exist.") XCTAssertEqual(nextButton.isEnabled, accepted, "The next button should be enabled when the terms are accepted") } - } diff --git a/RiotSwiftUI/Modules/Authentication/Terms/Test/Unit/AuthenticationTermsViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/Terms/Test/Unit/AuthenticationTermsViewModelTests.swift index 70166d010..ccbc50721 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/Test/Unit/AuthenticationTermsViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/Test/Unit/AuthenticationTermsViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsListItem.swift b/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsListItem.swift index d8d8456b3..9cdb3d1d9 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsListItem.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsListItem.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct AuthenticationTermsListItem: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsScreen.swift b/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsScreen.swift index d8241ef62..ff2aa02cf 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct AuthenticationTermsScreen: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsToggleStyle.swift b/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsToggleStyle.swift index b8bdec32f..f5e94ce84 100644 --- a/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsToggleStyle.swift +++ b/RiotSwiftUI/Modules/Authentication/Terms/View/AuthenticationTermsToggleStyle.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailModels.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailModels.swift index dfe29221b..c3fce72fe 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailModels.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailViewModel.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailViewModel.swift index fca58b047..b3a31d585 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailViewModel.swift @@ -16,11 +16,9 @@ import SwiftUI -typealias AuthenticationVerifyEmailViewModelType = StateStoreViewModel -class AuthenticationVerifyEmailViewModel: AuthenticationVerifyEmailViewModelType, AuthenticationVerifyEmailViewModelProtocol { +typealias AuthenticationVerifyEmailViewModelType = StateStoreViewModel +class AuthenticationVerifyEmailViewModel: AuthenticationVerifyEmailViewModelType, AuthenticationVerifyEmailViewModelProtocol { // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailViewModelProtocol.swift index 1c72824e5..6700cd157 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/AuthenticationVerifyEmailViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol AuthenticationVerifyEmailViewModelProtocol { - var callback: (@MainActor (AuthenticationVerifyEmailViewModelResult) -> Void)? { get set } var context: AuthenticationVerifyEmailViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Coordinator/AuthenticationVerifyEmailCoordinator.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Coordinator/AuthenticationVerifyEmailCoordinator.swift index bd68b0e44..920884048 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Coordinator/AuthenticationVerifyEmailCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Coordinator/AuthenticationVerifyEmailCoordinator.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import SwiftUI import CommonKit +import SwiftUI struct AuthenticationVerifyEmailCoordinatorParameters { let registrationWizard: RegistrationWizard @@ -24,7 +24,6 @@ struct AuthenticationVerifyEmailCoordinatorParameters { } final class AuthenticationVerifyEmailCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -74,7 +73,7 @@ final class AuthenticationVerifyEmailCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.authenticationVerifyEmailHostingController + authenticationVerifyEmailHostingController } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/MockAuthenticationVerifyEmailScreenState.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/MockAuthenticationVerifyEmailScreenState.swift index 3226f2f00..2474d00cc 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/MockAuthenticationVerifyEmailScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/MockAuthenticationVerifyEmailScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,7 +33,7 @@ enum MockAuthenticationVerifyEmailScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel: AuthenticationVerifyEmailViewModel switch self { case .emptyAddress: diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/UI/AuthenticationVerifyEmailUITests.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/UI/AuthenticationVerifyEmailUITests.swift index 73706abcd..52dad31a0 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/UI/AuthenticationVerifyEmailUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/UI/AuthenticationVerifyEmailUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class AuthenticationVerifyEmailUITests: MockScreenTestCase { func testEmptyAddress() { @@ -78,5 +78,4 @@ class AuthenticationVerifyEmailUITests: MockScreenTestCase { XCTAssertTrue(backButton.exists, "Back button should be shown.") XCTAssertEqual(backButton.label, "Back") } - } diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/Unit/AuthenticationVerifyEmailViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/Unit/AuthenticationVerifyEmailViewModelTests.swift index 3b340a9bf..553858d25 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/Unit/AuthenticationVerifyEmailViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/Test/Unit/AuthenticationVerifyEmailViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import XCTest @testable import RiotSwiftUI class AuthenticationVerifyEmailViewModelTests: XCTestCase { - var viewModel: AuthenticationVerifyEmailViewModelProtocol! var context: AuthenticationVerifyEmailViewModelType.Context! diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/View/AuthenticationVerifyEmailForm.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/View/AuthenticationVerifyEmailForm.swift index 274a0a942..79ca40c12 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/View/AuthenticationVerifyEmailForm.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/View/AuthenticationVerifyEmailForm.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import SwiftUI /// The form shown to enter an email address. struct AuthenticationVerifyEmailForm: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Authentication/VerifyEmail/View/AuthenticationVerifyEmailScreen.swift b/RiotSwiftUI/Modules/Authentication/VerifyEmail/View/AuthenticationVerifyEmailScreen.swift index 97283a9f9..2081f58d9 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyEmail/View/AuthenticationVerifyEmailScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyEmail/View/AuthenticationVerifyEmailScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct AuthenticationVerifyEmailScreen: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnModels.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnModels.swift index c9d9aec6e..a89016fda 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnModels.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnViewModel.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnViewModel.swift index de688374a..354813296 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnViewModel.swift @@ -16,11 +16,9 @@ import SwiftUI -typealias AuthenticationVerifyMsisdnViewModelType = StateStoreViewModel -class AuthenticationVerifyMsisdnViewModel: AuthenticationVerifyMsisdnViewModelType, AuthenticationVerifyMsisdnViewModelProtocol { +typealias AuthenticationVerifyMsisdnViewModelType = StateStoreViewModel +class AuthenticationVerifyMsisdnViewModel: AuthenticationVerifyMsisdnViewModelType, AuthenticationVerifyMsisdnViewModelProtocol { // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnViewModelProtocol.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnViewModelProtocol.swift index 56c565398..b9412cd46 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/AuthenticationVerifyMsisdnViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol AuthenticationVerifyMsisdnViewModelProtocol { - var callback: (@MainActor (AuthenticationVerifyMsisdnViewModelResult) -> Void)? { get set } var context: AuthenticationVerifyMsisdnViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Coordinator/AuthenticationVerifyMsisdnCoordinator.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Coordinator/AuthenticationVerifyMsisdnCoordinator.swift index 1962efe56..0d7b42dc4 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Coordinator/AuthenticationVerifyMsisdnCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Coordinator/AuthenticationVerifyMsisdnCoordinator.swift @@ -14,9 +14,9 @@ // limitations under the License. // -import SwiftUI import CommonKit import libPhoneNumber_iOS +import SwiftUI struct AuthenticationVerifyMsisdnCoordinatorParameters { let registrationWizard: RegistrationWizard @@ -25,7 +25,6 @@ struct AuthenticationVerifyMsisdnCoordinatorParameters { } final class AuthenticationVerifyMsisdnCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -75,7 +74,7 @@ final class AuthenticationVerifyMsisdnCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.authenticationVerifyMsisdnHostingController + authenticationVerifyMsisdnHostingController } // MARK: - Private @@ -174,7 +173,6 @@ final class AuthenticationVerifyMsisdnCoordinator: Coordinator, Presentable { self?.stopLoading() self?.handleError(error) } - } } diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/MockAuthenticationVerifyMsisdnScreenState.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/MockAuthenticationVerifyMsisdnScreenState.swift index 63242fa42..622176a74 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/MockAuthenticationVerifyMsisdnScreenState.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/MockAuthenticationVerifyMsisdnScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,7 +34,7 @@ enum MockAuthenticationVerifyMsisdnScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel: AuthenticationVerifyMsisdnViewModel switch self { case .emptyPhoneNumber: diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/UI/AuthenticationVerifyMsisdnUITests.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/UI/AuthenticationVerifyMsisdnUITests.swift index f5528acfa..535565860 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/UI/AuthenticationVerifyMsisdnUITests.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/UI/AuthenticationVerifyMsisdnUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class AuthenticationVerifyMsisdnUITests: MockScreenTestCase { func testEmptyPhoneNumber() { @@ -127,5 +127,4 @@ class AuthenticationVerifyMsisdnUITests: MockScreenTestCase { XCTAssertTrue(backButton.exists, "Back button should be shown.") XCTAssertEqual(backButton.label, "Back") } - } diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/Unit/AuthenticationVerifyMsisdnViewModelTests.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/Unit/AuthenticationVerifyMsisdnViewModelTests.swift index 77a117d37..fb4c1965c 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/Unit/AuthenticationVerifyMsisdnViewModelTests.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/Test/Unit/AuthenticationVerifyMsisdnViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import XCTest @testable import RiotSwiftUI class AuthenticationVerifyMsisdnViewModelTests: XCTestCase { - var viewModel: AuthenticationVerifyMsisdnViewModelProtocol! var context: AuthenticationVerifyMsisdnViewModelType.Context! diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnForm.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnForm.swift index 9701a680d..97a7b6f20 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnForm.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnForm.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import SwiftUI /// The form shown to enter an email address. struct AuthenticationVerifyMsisdnForm: View { - // MARK: - Properties // MARK: Private @@ -65,7 +64,7 @@ struct AuthenticationVerifyMsisdnForm: View { /// The text field and submit button where the user enters a phone number. var mainContent: some View { - return VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 12) { if #available(iOS 15.0, *) { textField .onSubmit(sendSMS) diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnOTPForm.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnOTPForm.swift index e56badab4..926892791 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnOTPForm.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnOTPForm.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import SwiftUI /// The form shown to enter an OTP for phone number vaildation struct AuthenticationVerifyMsisdnOTPForm: View { - // MARK: - Properties // MARK: Private @@ -65,7 +64,7 @@ struct AuthenticationVerifyMsisdnOTPForm: View { /// The text field and submit button where the user enters an OTP. var mainContent: some View { - return VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 12) { if #available(iOS 15.0, *) { textField .onSubmit(submitOTP) @@ -93,7 +92,7 @@ struct AuthenticationVerifyMsisdnOTPForm: View { /// The text field, extracted for iOS 15 modifiers to be applied. var textField: some View { - return TextField(VectorL10n.authenticationVerifyMsisdnOtpTextFieldPlaceholder, text: $viewModel.otp) { + TextField(VectorL10n.authenticationVerifyMsisdnOtpTextFieldPlaceholder, text: $viewModel.otp) { isEditingTextField = $0 } .textFieldStyle(BorderedInputFieldStyle(isEditing: isEditingTextField, isError: false)) @@ -104,7 +103,7 @@ struct AuthenticationVerifyMsisdnOTPForm: View { /// Sends the `submitOTP` view action so long as a valid OTP has been input. func submitOTP() { - guard viewModel.viewState.hasSentSMS && !viewModel.viewState.hasInvalidOTP else { return } + guard viewModel.viewState.hasSentSMS, !viewModel.viewState.hasInvalidOTP else { return } viewModel.send(viewAction: .submitOTP) } } diff --git a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnScreen.swift b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnScreen.swift index 867e58224..c6bd7943d 100644 --- a/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/VerifyMsisdn/View/AuthenticationVerifyMsisdnScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct AuthenticationVerifyMsisdnScreen: View { - // MARK: - Properties // MARK: Private @@ -31,7 +30,7 @@ struct AuthenticationVerifyMsisdnScreen: View { // MARK: Views var body: some View { - GeometryReader { geometry in + GeometryReader { _ in VStack { ScrollView { if viewModel.viewState.hasSentSMS { diff --git a/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicator.swift b/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicator.swift index 51c4ecdd8..32b48c821 100644 --- a/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicator.swift +++ b/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicator.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import SwiftUI /// A visual cue to user that something is in progress. struct ActivityIndicator: View { - private enum Constants { static let backgroundColor = Color(UIColor(white: 0.8, alpha: 0.9)) } @@ -29,7 +28,6 @@ struct ActivityIndicator: View { .padding() .background(Constants.backgroundColor) .cornerRadius(5) - } } diff --git a/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift b/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift index cd4bb859f..3d935ea67 100644 --- a/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift +++ b/RiotSwiftUI/Modules/Common/ActivityIndicator/ActivityIndicatorModifier.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,6 +37,6 @@ struct ActivityIndicatorModifier: ViewModifier { extension View { func activityIndicator(show: Bool) -> some View { - self.modifier(ActivityIndicatorModifier(show: show)) + modifier(ActivityIndicatorModifier(show: show)) } } diff --git a/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarInputProtocol.swift b/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarInputProtocol.swift index 2b36660a0..fe53976f1 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarInputProtocol.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarInputProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarProtocol.swift b/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarProtocol.swift index 7963c333a..b3342b8b3 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarProtocol.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/Model/AvatarProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Common/Avatar/Model/Avatarable.swift b/RiotSwiftUI/Modules/Common/Avatar/Model/Avatarable.swift index 4e5062b5f..ce8e4ee40 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/Model/Avatarable.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/Model/Avatarable.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,7 @@ import Foundation /// A protocol that any class or struct can conform to /// so that it can easily produce avatar data. -/// +/// /// E.g. MXRoom, MxUser can conform to this making it /// easy to grab the avatar data for display. protocol Avatarable: AvatarInputProtocol { } diff --git a/RiotSwiftUI/Modules/Common/Avatar/Model/Mock/MockAvatarInput.swift b/RiotSwiftUI/Modules/Common/Avatar/Model/Mock/MockAvatarInput.swift index 4e62c0b50..773c6d439 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/Model/Mock/MockAvatarInput.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/Model/Mock/MockAvatarInput.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift b/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift index 2e6ca5a94..02369980c 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/Service/MatrixSDK/AvatarService.swift @@ -14,10 +14,10 @@ // limitations under the License. // -import Foundation -import MatrixSDK import Combine import DesignKit +import Foundation +import MatrixSDK enum AvatarServiceError: Error { case pathNotfound @@ -25,7 +25,6 @@ enum AvatarServiceError: Error { } class AvatarService: AvatarServiceProtocol { - private enum Constants { static let mimeType = "image/jpeg" static let thumbnailMethod = MXThumbnailingMethodCrop @@ -34,7 +33,7 @@ class AvatarService: AvatarServiceProtocol { private let mediaManager: MXMediaManager static func instantiate(mediaManager: MXMediaManager) -> AvatarServiceProtocol { - return AvatarService(mediaManager: mediaManager) + AvatarService(mediaManager: mediaManager) } init(mediaManager: MXMediaManager) { @@ -49,13 +48,13 @@ class AvatarService: AvatarServiceProtocol { /// - avatarSize: The size of avatar to retrieve as defined in the DesignKit spec. /// - Returns: A Future of UIImage that returns an error if it fails to fetch the image. func avatarImage(mxContentUri: String, avatarSize: AvatarSize) -> Future { - let cachePath = MXMediaManager.thumbnailCachePath( forMatrixContentURI: mxContentUri, andType: Constants.mimeType, - inFolder: nil, + inFolder: nil, toFitViewSize: avatarSize.size, - with: Constants.thumbnailMethod) + with: Constants.thumbnailMethod + ) return Future { promise in if let image = MXMediaManager.loadThroughCache(withFilePath: cachePath), @@ -69,7 +68,8 @@ class AvatarService: AvatarServiceProtocol { withType: Constants.mimeType, inFolder: nil, toFitViewSize: avatarSize.size, - with: Constants.thumbnailMethod) { path in + with: Constants.thumbnailMethod + ) { path in guard let path = path else { promise(.failure(AvatarServiceError.pathNotfound)) return diff --git a/RiotSwiftUI/Modules/Common/Avatar/Service/Mock/MockAvatarService.swift b/RiotSwiftUI/Modules/Common/Avatar/Service/Mock/MockAvatarService.swift index 94ea01c55..eff0be3f4 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/Service/Mock/MockAvatarService.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/Service/Mock/MockAvatarService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,9 +14,9 @@ // limitations under the License. // -import Foundation import Combine import DesignKit +import Foundation import UIKit class MockAvatarService: AvatarServiceProtocol { diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift index 2b7fa9e60..4f51f574a 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/View/AvatarImage.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +14,10 @@ // limitations under the License. // -import SwiftUI import DesignKit +import SwiftUI struct AvatarImage: View { - @Environment(\.theme) var theme: ThemeSwiftUI @Environment(\.dependencies) var dependencies: DependencyContainer @StateObject var viewModel = AvatarViewModel() @@ -75,7 +74,7 @@ extension AvatarImage { /// Use display name color as border color by default func border() -> some View { let borderColor = theme.userColor(for: matrixItemId) - return self.border(color: borderColor) + return border(color: borderColor) } } diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/PlaceholderAvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/PlaceholderAvatarImage.swift index f119a7e14..55a1b6854 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/View/PlaceholderAvatarImage.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/View/PlaceholderAvatarImage.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,7 +22,6 @@ import SwiftUI /// This view has a forced 1:1 aspect ratio but will appear very large until a `.frame` /// modifier is applied. struct PlaceholderAvatarImage: View { - // MARK: - Private @Environment(\.theme) private var theme diff --git a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift index d82e2107f..8e967fe18 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/View/SpaceAvatarImage.swift @@ -14,11 +14,10 @@ // limitations under the License. // -import SwiftUI import DesignKit +import SwiftUI struct SpaceAvatarImage: View { - @Environment(\.theme) var theme: ThemeSwiftUI @Environment(\.dependencies) var dependencies: DependencyContainer @StateObject var viewModel = AvatarViewModel() diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarServiceProtocol.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarServiceProtocol.swift index 1f505a7db..578c34834 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarServiceProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +14,11 @@ // limitations under the License. // -import Foundation -import DesignKit import Combine +import DesignKit +import Foundation import UIKit - /// Provides a simple api to retrieve and cache avatar images protocol AvatarServiceProtocol { func avatarImage(mxContentUri: String, avatarSize: AvatarSize) -> Future diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift index 90f736da6..433fb9cba 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewModel.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,13 +14,12 @@ // limitations under the License. // -import Foundation import Combine import DesignKit +import Foundation /// Simple ViewModel that supports loading an avatar image class AvatarViewModel: InjectableObject, ObservableObject { - @Inject var avatarService: AvatarServiceProtocol @Published private(set) var viewState = AvatarViewState.empty @@ -34,24 +33,22 @@ class AvatarViewModel: InjectableObject, ObservableObject { /// - displayName: Display name of the avatar. /// - colorCount: The count of total avatar colors used to generate the stable color index. /// - avatarSize: The size of the avatar to fetch (as defined within DesignKit). - func loadAvatar( - mxContentUri: String?, - matrixItemId: String, - displayName: String?, - colorCount: Int, - avatarSize: AvatarSize) { - + func loadAvatar(mxContentUri: String?, + matrixItemId: String, + displayName: String?, + colorCount: Int, + avatarSize: AvatarSize) { let placeholderViewModel = PlaceholderAvatarViewModel(displayName: displayName, matrixItemId: matrixItemId, colorCount: colorCount) - self.viewState = .placeholder(placeholderViewModel.firstCharacterCapitalized, placeholderViewModel.stableColorIndex) + viewState = .placeholder(placeholderViewModel.firstCharacterCapitalized, placeholderViewModel.stableColorIndex) guard let mxContentUri = mxContentUri, mxContentUri.count > 0 else { return } - avatarService.avatarImage(mxContentUri: mxContentUri, avatarSize: avatarSize) + avatarService.avatarImage(mxContentUri: mxContentUri, avatarSize: avatarSize) .sink { completion in guard case let .failure(error) = completion else { return } UILog.error("[AvatarService] Failed to retrieve avatar", context: error) diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewState.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewState.swift index cac2a70d4..97b09ea41 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewState.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/AvatarViewState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/PlaceholderAvatarViewModel.swift b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/PlaceholderAvatarViewModel.swift index d5c131b39..618d6f284 100644 --- a/RiotSwiftUI/Modules/Common/Avatar/ViewModel/PlaceholderAvatarViewModel.swift +++ b/RiotSwiftUI/Modules/Common/Avatar/ViewModel/PlaceholderAvatarViewModel.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,7 +27,7 @@ struct PlaceholderAvatarViewModel { /// Get the first character of the display name capitalized or else a space character. var firstCharacterCapitalized: Character { - return displayName?.capitalized.first ?? " " + displayName?.capitalized.first ?? " " } /// Provides the same color each time for a specified matrixId @@ -39,7 +39,7 @@ struct PlaceholderAvatarViewModel { var stableColorIndex: Int { // Sum all characters let sum = matrixItemId.utf8 - .map({ UInt($0) }) + .map { UInt($0) } .reduce(0, +) // modulo the color count return Int(sum) % colorCount diff --git a/RiotSwiftUI/Modules/Common/Bridging/VectorContentView.swift b/RiotSwiftUI/Modules/Common/Bridging/VectorContentView.swift index 4783fd01c..dfdd5e8b3 100644 --- a/RiotSwiftUI/Modules/Common/Bridging/VectorContentView.swift +++ b/RiotSwiftUI/Modules/Common/Bridging/VectorContentView.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,7 +20,6 @@ import SwiftUI /// /// Provides any app level configuration the SwiftUI hierarchy might need (E.g. to monitor theme changes). struct VectorContentModifier: ViewModifier { - @ObservedObject private var themePublisher = ThemePublisher.shared @Environment(\.layoutDirection) private var defaultLayoutDirection @@ -39,6 +38,6 @@ struct VectorContentModifier: ViewModifier { extension View { func vectorContent() -> some View { - self.modifier(VectorContentModifier()) + modifier(VectorContentModifier()) } } diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift index c3c0169fd..d09fa87f4 100644 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift +++ b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainer.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import Foundation /// Used for storing and resolving dependencies at runtime. struct DependencyContainer { - // Stores the dependencies with type information removed. private var dependencyStore: [String: Any] = [:] @@ -26,7 +25,7 @@ struct DependencyContainer { /// /// Given a particular `Type` (Inferred from return type), /// generate a key and retrieve from storage. - /// + /// /// - Returns: The resolved dependency. func resolve() -> T { let key = String(describing: T.self) diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift index b788b362d..4bde8956e 100644 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift +++ b/RiotSwiftUI/Modules/Common/DependencyInjection/DependencyContainerKey.swift @@ -33,7 +33,6 @@ extension EnvironmentValues { } extension View { - /// A modifier for adding a dependency to the SwiftUI view hierarchy's dependency container. /// /// Important: When adding a dependency to cast it to the type in which it will be injected. diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift index e81457678..d45907eeb 100644 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift +++ b/RiotSwiftUI/Modules/Common/DependencyInjection/Inject.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,12 +22,9 @@ import Foundation /// @Inject var someClass: SomeClass /// ``` @propertyWrapper struct Inject { - - static subscript( - _enclosingInstance instance: T, - wrapped wrappedKeyPath: ReferenceWritableKeyPath, - storage storageKeyPath: ReferenceWritableKeyPath - ) -> Value { + static subscript(_enclosingInstance instance: T, + wrapped wrappedKeyPath: ReferenceWritableKeyPath, + storage storageKeyPath: ReferenceWritableKeyPath) -> Value { get { // Resolve dependencies from enclosing instance's `dependencies` property let v: Value = instance.dependencies.resolve() @@ -38,9 +35,9 @@ import Foundation } } - @available(*, unavailable, message: "This property wrapper can only be applied to classes") + @available(*, unavailable, message: "This property wrapper can only be applied to classes") var wrappedValue: Value { get { fatalError("wrappedValue get not used") } - set { fatalError("wrappedValue set not used. \(newValue)" ) } + set { fatalError("wrappedValue set not used. \(newValue)") } } } diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift index 96e5eef64..b05b966e4 100644 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift +++ b/RiotSwiftUI/Modules/Common/DependencyInjection/Injectable.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,9 +21,7 @@ protocol Injectable: AnyObject { var dependencies: DependencyContainer! { get set } } - extension Injectable { - /// Used to inject the dependency container into an Injectable. /// - Parameter dependencies: The `DependencyContainer` to inject. func inject(dependencies: DependencyContainer) { diff --git a/RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift b/RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift index bf38a0707..eab3cdcdf 100644 --- a/RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift +++ b/RiotSwiftUI/Modules/Common/DependencyInjection/InjectableObject.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Common/EffectsScene/EffectsScene.swift b/RiotSwiftUI/Modules/Common/EffectsScene/EffectsScene.swift index 38eb5db11..6224c7185 100644 --- a/RiotSwiftUI/Modules/Common/EffectsScene/EffectsScene.swift +++ b/RiotSwiftUI/Modules/Common/EffectsScene/EffectsScene.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import SceneKit import SwiftUI class EffectsScene: SCNScene { - // MARK: - Constants private enum Constants { @@ -31,14 +30,14 @@ class EffectsScene: SCNScene { static func confetti(with theme: ThemeSwiftUI) -> EffectsScene? { guard let scene = EffectsScene(named: Constants.confettiSceneName) else { return nil } - let colors: [[Float]] = theme.colors.namesAndAvatars.compactMap { $0.floatComponents } + let colors: [[Float]] = theme.colors.namesAndAvatars.compactMap(\.floatComponents) if let particles = scene.rootNode.childNode(withName: Constants.particlesNodeName, recursively: false)?.particleSystems?.first { // The particles need a non-zero color variation for the handler to affect the color particles.particleColorVariation = SCNVector4(x: 0, y: 0, z: 0, w: 0.1) // Add a handler to customize the color of the particles. - particles.handle(.birth, forProperties: [.color]) { data, dataStride, indices, count in + particles.handle(.birth, forProperties: [.color]) { data, dataStride, _, count in for index in 0..: Identifiable { /// The alert's title. let title: String /// The alert's message (optional). - var message: String? = nil + var message: String? /// The alert's primary button title and action. Defaults to an Ok button with no action. var primaryButton: (title: String, action: (() -> Void)?) = (VectorL10n.ok, nil) /// The alert's secondary button title and action. - var secondaryButton: (title: String, action: (() -> Void)?)? = nil + var secondaryButton: (title: String, action: (() -> Void)?)? } extension AlertInfo { @@ -47,7 +47,7 @@ extension AlertInfo { /// - id: An ID that identifies the error. /// - error: The Error that occurred. init?(id: T, error: NSError? = nil) { - guard error?.domain != NSURLErrorDomain && error?.code != NSURLErrorCancelled else { return nil } + guard error?.domain != NSURLErrorDomain, error?.code != NSURLErrorCancelled else { return nil } self.id = id title = error?.userInfo[NSLocalizedFailureReasonErrorKey] as? String ?? VectorL10n.error diff --git a/RiotSwiftUI/Modules/Common/Extensions/CLLocationCoordinate2D.swift b/RiotSwiftUI/Modules/Common/Extensions/CLLocationCoordinate2D.swift index cdc21e100..7a5fdf63c 100644 --- a/RiotSwiftUI/Modules/Common/Extensions/CLLocationCoordinate2D.swift +++ b/RiotSwiftUI/Modules/Common/Extensions/CLLocationCoordinate2D.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,18 +14,16 @@ // limitations under the License. // -import Foundation import CoreLocation +import Foundation extension CLLocationCoordinate2D { - /// Compare two coordinates /// - parameter coordinate: another coordinate to compare /// - parameter precision:it represente how close you want the two coordinates /// - return: bool value func isEqual(to coordinate: CLLocationCoordinate2D, precision: Double) -> Bool { - - if fabs(self.latitude - coordinate.latitude) <= precision && fabs(self.longitude - coordinate.longitude) <= precision { + if fabs(latitude - coordinate.latitude) <= precision, fabs(longitude - coordinate.longitude) <= precision { return true } return false diff --git a/RiotSwiftUI/Modules/UserSessions/DeviceType/DeviceType.swift b/RiotSwiftUI/Modules/Common/Extensions/Collection.swift similarity index 73% rename from RiotSwiftUI/Modules/UserSessions/DeviceType/DeviceType.swift rename to RiotSwiftUI/Modules/Common/Extensions/Collection.swift index 1ae053b1a..710e4dc21 100644 --- a/RiotSwiftUI/Modules/UserSessions/DeviceType/DeviceType.swift +++ b/RiotSwiftUI/Modules/Common/Extensions/Collection.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,12 +15,10 @@ // import Foundation -import SwiftUI -/// Client type -enum DeviceType { - case desktop - case web - case mobile - case unknown +extension Collection { + /// Returns the element at the specified index if it is within bounds, otherwise nil. + subscript(safe index: Index) -> Element? { + indices.contains(index) ? self[index] : nil + } } diff --git a/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXRoomAvatarable.swift b/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXRoomAvatarable.swift index e18feed41..ac7a7ebbc 100644 --- a/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXRoomAvatarable.swift +++ b/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXRoomAvatarable.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,5 +27,4 @@ extension MXRoom: Avatarable { var displayName: String? { summary.displayname } - } diff --git a/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXUserAvatarable.swift b/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXUserAvatarable.swift index 701c2f75f..f6843b063 100644 --- a/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXUserAvatarable.swift +++ b/RiotSwiftUI/Modules/Common/Extensions/MatrixSDK/MXUserAvatarable.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,5 +27,4 @@ extension MXUser: Avatarable { var displayName: String? { displayname } - } diff --git a/RiotSwiftUI/Modules/Common/Extensions/Publisher.swift b/RiotSwiftUI/Modules/Common/Extensions/Publisher.swift index 892608cef..c5e3622ad 100644 --- a/RiotSwiftUI/Modules/Common/Extensions/Publisher.swift +++ b/RiotSwiftUI/Modules/Common/Extensions/Publisher.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,17 +14,15 @@ // limitations under the License. // -import Foundation import Combine +import Foundation extension Publisher where Failure == Never { /// Same as `assign(to:on:)` but maintains a weak reference to object /// /// Useful in cases where you want to pass self and not cause a retain cycle. - func weakAssign( - to keyPath: ReferenceWritableKeyPath, - on object: T - ) -> AnyCancellable { + func weakAssign(to keyPath: ReferenceWritableKeyPath, + on object: T) -> AnyCancellable { sink { [weak object] value in object?[keyPath: keyPath] = value } diff --git a/RiotSwiftUI/Modules/Common/Locale/LocaleProvider.swift b/RiotSwiftUI/Modules/Common/Locale/LocaleProvider.swift index a1ba74921..11f44e8be 100644 --- a/RiotSwiftUI/Modules/Common/Locale/LocaleProvider.swift +++ b/RiotSwiftUI/Modules/Common/Locale/LocaleProvider.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,6 @@ import Foundation class LocaleProvider: LocaleProviderType { static var locale: Locale? { - return nil + nil } } diff --git a/RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift b/RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift index bedcb6a67..6f2eabbf1 100644 --- a/RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift +++ b/RiotSwiftUI/Modules/Common/Logging/LoggerProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift b/RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift index 85587467f..decebbc42 100644 --- a/RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift +++ b/RiotSwiftUI/Modules/Common/Logging/PrintLogger.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,15 +23,19 @@ class PrintLogger: LoggerProtocol { static func verbose(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { print(message()) } + static func debug(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { print(message()) } + static func info(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { print(message()) } + static func warning(_ message: @autoclosure () -> Any, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { print(message()) } + static func error(_ message: @autoclosure () -> StaticString, _ file: String = #file, _ function: String = #function, line: Int = #line, context: Any? = nil) { print(message()) } diff --git a/RiotSwiftUI/Modules/Common/Logging/UILog.swift b/RiotSwiftUI/Modules/Common/Logging/UILog.swift index ee0c4f22b..97fc113e4 100644 --- a/RiotSwiftUI/Modules/Common/Logging/UILog.swift +++ b/RiotSwiftUI/Modules/Common/Logging/UILog.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,54 +20,48 @@ import Foundation /// /// It can be configured at runtime with a suitable logger. class UILog: LoggerProtocol { - static var _logger: LoggerProtocol.Type? static func configure(logger: LoggerProtocol.Type) { _logger = logger } - static func verbose( - _ message: @autoclosure () -> Any, - _ file: String = #file, - _ function: String = #function, - line: Int = #line, - context: Any? = nil) { + static func verbose(_ message: @autoclosure () -> Any, + _ file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { _logger?.verbose(message(), file, function, line: line, context: context) } - static func debug( - _ message: @autoclosure () -> Any, - _ file: String = #file, - _ function: String = #function, - line: Int = #line, - context: Any? = nil) { + static func debug(_ message: @autoclosure () -> Any, + _ file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { _logger?.debug(message(), file, function, line: line, context: context) } - static func info( - _ message: @autoclosure () -> Any, - _ file: String = #file, - _ function: String = #function, - line: Int = #line, - context: Any? = nil) { + static func info(_ message: @autoclosure () -> Any, + _ file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { _logger?.info(message(), file, function, line: line, context: context) } - static func warning( - _ message: @autoclosure () -> Any, - _ file: String = #file, - _ function: String = #function, - line: Int = #line, - context: Any? = nil) { + static func warning(_ message: @autoclosure () -> Any, + _ file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { _logger?.warning(message(), file, function, line: line, context: context) } - static func error( - _ message: @autoclosure () -> StaticString, - _ file: String = #file, - _ function: String = #function, - line: Int = #line, - context: Any? = nil) { + static func error(_ message: @autoclosure () -> StaticString, + _ file: String = #file, + _ function: String = #function, + line: Int = #line, + context: Any? = nil) { _logger?.error(message(), file, function, line: line, context: context) } } diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index e52ac8e29..05cc50234 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,8 @@ import Foundation enum MockAppScreens { static let appScreens: [MockScreenState.Type] = [ MockUserSessionsOverviewScreenState.self, + MockUserSessionDetailsScreenState.self, + MockUserSessionOverviewScreenState.self, MockLiveLocationLabPromotionScreenState.self, MockLiveLocationSharingViewerScreenState.self, MockAuthenticationLoginScreenState.self, @@ -61,4 +63,3 @@ enum MockAppScreens { MockSpaceSelectorScreenState.self ] } - diff --git a/RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift b/RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift index 073d32381..0129807cd 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,12 +24,11 @@ protocol MockScreenState { } extension MockScreenState { - /// Get a list of the screens for every screen state. static var stateRenderer: StateRenderer { - let depsAndViews = screenStates.map(\.screenView) - let deps = depsAndViews.map({ $0.0 }) - let views = depsAndViews.map({ $0.1 }) + let depsAndViews = screenStates.map(\.screenView) + let deps = depsAndViews.map(\.0) + let views = depsAndViews.map(\.1) let titles = screenStates.map(\.title) var states = [ScreenStateInfo]() @@ -45,7 +44,7 @@ extension MockScreenState { /// All available screen state keys static var screenNames: [String] { - screenStates.map { $0.title } + screenStates.map(\.title) } /// A title to represent the screen and it's screen state @@ -54,12 +53,12 @@ extension MockScreenState { } private func simpleTypeName(_ type: Any) -> String { - String(describing: type).components(separatedBy: .punctuationCharacters).filter { $0.count > 0}.last! + String(describing: type).components(separatedBy: .punctuationCharacters).filter { $0.count > 0 }.last! } } extension MockScreenState where Self: CaseIterable { static var screenStates: [MockScreenState] { - return Array(self.allCases) + Array(allCases) } } diff --git a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift index 6f7f221b3..91cf8937f 100644 --- a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift +++ b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift @@ -13,11 +13,9 @@ // See the License for the specific language governing permissions and // limitations under the License. - import SwiftUI struct ScreenList: View { - private let allStates: [ScreenStateInfo] @State private var searchQuery = "" @@ -25,8 +23,9 @@ struct ScreenList: View { init(screens: [MockScreenState.Type]) { let states = screens + // swiftformat:disable:next preferKeyPath .map { $0.stateRenderer } - .flatMap { $0.states } + .flatMap(\.states) allStates = states filteredStates = states diff --git a/RiotSwiftUI/Modules/Common/Mock/ScreenStateInfo.swift b/RiotSwiftUI/Modules/Common/Mock/ScreenStateInfo.swift index 6ebc6f591..0534c80dc 100644 --- a/RiotSwiftUI/Modules/Common/Mock/ScreenStateInfo.swift +++ b/RiotSwiftUI/Modules/Common/Mock/ScreenStateInfo.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Common/Mock/StateRenderer.swift b/RiotSwiftUI/Modules/Common/Mock/StateRenderer.swift index 207146794..627a54803 100644 --- a/RiotSwiftUI/Modules/Common/Mock/StateRenderer.swift +++ b/RiotSwiftUI/Modules/Common/Mock/StateRenderer.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,7 +35,7 @@ class StateRenderer { addNavigation: Bool = false ) -> some View { Group { - ForEach(0..(_ wrap: Bool, view: V) -> some View { if wrap { - NavigationView{ + NavigationView { view .navigationBarTitleDisplayMode(.inline) } diff --git a/RiotSwiftUI/Modules/Common/Test/UI/MockScreenTest.swift b/RiotSwiftUI/Modules/Common/Test/UI/MockScreenTest.swift index 3c00337d1..d5be4d0e4 100644 --- a/RiotSwiftUI/Modules/Common/Test/UI/MockScreenTest.swift +++ b/RiotSwiftUI/Modules/Common/Test/UI/MockScreenTest.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest /// XCTestCase subclass to ease testing of `MockScreenState`. /// Launches the app with an environment variable used to disable animations. @@ -26,7 +26,7 @@ import RiotSwiftUI class MockScreenTestCase: XCTestCase { let app = XCUIApplication() - open override func setUpWithError() throws { + override open func setUpWithError() throws { app.launchEnvironment = ["IS_RUNNING_UI_TESTS": "1"] app.launch() } diff --git a/RiotSwiftUI/Modules/Common/Test/UI/XCUIApplication+Riot.swift b/RiotSwiftUI/Modules/Common/Test/UI/XCUIApplication+Riot.swift index 09e5799f7..a2e7dc2b5 100644 --- a/RiotSwiftUI/Modules/Common/Test/UI/XCUIApplication+Riot.swift +++ b/RiotSwiftUI/Modules/Common/Test/UI/XCUIApplication+Riot.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,11 +23,11 @@ extension XCUIApplication { textFields["searchQueryTextField"].tap() typeText(identifier) - let button = self.buttons[identifier] + let button = buttons[identifier] let footer = staticTexts["footerText"] - while !button.isHittable && !footer.isHittable { - self.tables.firstMatch.swipeUp() + while !button.isHittable, !footer.isHittable { + tables.firstMatch.swipeUp() } button.tap() diff --git a/RiotSwiftUI/Modules/Common/Test/XCTestPublisherExtensions.swift b/RiotSwiftUI/Modules/Common/Test/XCTestPublisherExtensions.swift index 11db89923..d749ec858 100644 --- a/RiotSwiftUI/Modules/Common/Test/XCTestPublisherExtensions.swift +++ b/RiotSwiftUI/Modules/Common/Test/XCTestPublisherExtensions.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import Combine +import XCTest extension XCTestCase { /// XCTest utility to wait for results from publishers, so that the output can be used for assertions. @@ -29,11 +29,9 @@ extension XCTestCase { /// - timeout: A timeout after which we give up. /// - Throws: If it can't get the unwrapped result. /// - Returns: The unwrapped result. - func xcAwait( - _ publisher: T, - timeout: TimeInterval = 10 - ) throws -> T.Output { - return try xcAwaitDeferred(publisher, timeout: timeout)() + func xcAwait(_ publisher: T, + timeout: TimeInterval = 10) throws -> T.Output { + try xcAwaitDeferred(publisher, timeout: timeout)() } /// XCTest utility that allows for a deferred wait of results from publishers, so that the output can be used for assertions. @@ -48,12 +46,10 @@ extension XCTestCase { /// - publisher: The publisher to wait on. /// - timeout: A timeout after which we give up. /// - Returns: A closure that starts the waiting of results when called. The closure will return the unwrapped result. - func xcAwaitDeferred( - _ publisher: T, - timeout: TimeInterval = 10 - ) -> (() throws -> (T.Output)) { + func xcAwaitDeferred(_ publisher: T, + timeout: TimeInterval = 10) -> (() throws -> (T.Output)) { var result: Result? - let expectation = self.expectation(description: "Awaiting publisher") + let expectation = expectation(description: "Awaiting publisher") let cancellable = publisher.sink( receiveCompletion: { completion in diff --git a/RiotSwiftUI/Modules/Common/Theme/ThemeIdentifierExtensions.swift b/RiotSwiftUI/Modules/Common/Theme/ThemeIdentifierExtensions.swift index d5401d27e..90a05a540 100644 --- a/RiotSwiftUI/Modules/Common/Theme/ThemeIdentifierExtensions.swift +++ b/RiotSwiftUI/Modules/Common/Theme/ThemeIdentifierExtensions.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import DesignKit +import Foundation extension ThemeIdentifier { fileprivate static let defaultTheme = DefaultThemeSwiftUI() diff --git a/RiotSwiftUI/Modules/Common/Theme/ThemeKey.swift b/RiotSwiftUI/Modules/Common/Theme/ThemeKey.swift index a2e742b7f..79403c0dc 100644 --- a/RiotSwiftUI/Modules/Common/Theme/ThemeKey.swift +++ b/RiotSwiftUI/Modules/Common/Theme/ThemeKey.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,35 +14,35 @@ // limitations under the License. // +import DesignKit import Foundation import SwiftUI -import DesignKit private struct ThemeKey: EnvironmentKey { static let defaultValue = ThemePublisher.shared.theme } extension EnvironmentValues { - var theme: ThemeSwiftUI { - get { self[ThemeKey.self] } - set { self[ThemeKey.self] = newValue } - } + var theme: ThemeSwiftUI { + get { self[ThemeKey.self] } + set { self[ThemeKey.self] = newValue } + } } extension View { /// A theme modifier for setting the theme for this view and all its descendants in the hierarchy. /// - Parameter theme: A theme to be set as the environment value. /// - Returns: The target view with the theme applied. - func theme(_ theme: ThemeSwiftUI) -> some View { - environment(\.theme, theme) - } + func theme(_ theme: ThemeSwiftUI) -> some View { + environment(\.theme, theme) + } } extension View { /// A theme modifier for setting the theme by id for this view and all its descendants in the hierarchy. /// - Parameter themeId: ThemeIdentifier of a theme to be set as the environment value. /// - Returns: The target view with the theme applied. - func theme(_ themeId: ThemeIdentifier) -> some View { - return environment(\.theme, themeId.themeSwiftUI) - } + func theme(_ themeId: ThemeIdentifier) -> some View { + environment(\.theme, themeId.themeSwiftUI) + } } diff --git a/RiotSwiftUI/Modules/Common/Theme/ThemePublisher.swift b/RiotSwiftUI/Modules/Common/Theme/ThemePublisher.swift index ff0a39eca..1dccf7058 100644 --- a/RiotSwiftUI/Modules/Common/Theme/ThemePublisher.swift +++ b/RiotSwiftUI/Modules/Common/Theme/ThemePublisher.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,21 +14,20 @@ // limitations under the License. // -import Foundation import Combine +import Foundation /// Provides the theme and theme updates to SwiftUI. /// /// Replaces the old ThemeObserver. Riot app can push updates to this class /// removing the dependency of this class on the `ThemeService`. class ThemePublisher: ObservableObject { - - private static var _shared: ThemePublisher? = nil + private static var _shared: ThemePublisher? static var shared: ThemePublisher { - if _shared == nil { - configure(themeId: .light) - } - return _shared! + if _shared == nil { + configure(themeId: .light) + } + return _shared! } @Published private(set) var theme: ThemeSwiftUI @@ -38,7 +37,7 @@ class ThemePublisher: ObservableObject { } init(themeId: ThemeIdentifier) { - _theme = Published.init(initialValue: themeId.themeSwiftUI) + _theme = Published(initialValue: themeId.themeSwiftUI) } func republish(themeIdPublisher: AnyPublisher) { diff --git a/RiotSwiftUI/Modules/Common/Theme/ThemeSwiftUI.swift b/RiotSwiftUI/Modules/Common/Theme/ThemeSwiftUI.swift index f5a15424f..930338076 100644 --- a/RiotSwiftUI/Modules/Common/Theme/ThemeSwiftUI.swift +++ b/RiotSwiftUI/Modules/Common/Theme/ThemeSwiftUI.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import DesignKit +import Foundation protocol ThemeSwiftUI: ThemeSwiftUIType { var identifier: ThemeIdentifier { get } diff --git a/RiotSwiftUI/Modules/Common/Theme/ThemeUsersColorsExtension.swift b/RiotSwiftUI/Modules/Common/Theme/ThemeUsersColorsExtension.swift index ad1eeb222..a75c2f2be 100644 --- a/RiotSwiftUI/Modules/Common/Theme/ThemeUsersColorsExtension.swift +++ b/RiotSwiftUI/Modules/Common/Theme/ThemeUsersColorsExtension.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import Foundation import SwiftUI extension ThemeSwiftUI { - /// Get the stable display user color based on userId. /// - Parameter userId: The user id used to hash. /// - Returns: The SwiftUI color for the associated userId. diff --git a/RiotSwiftUI/Modules/Common/Theme/Themes/DarkThemeSwiftUI.swift b/RiotSwiftUI/Modules/Common/Theme/Themes/DarkThemeSwiftUI.swift index 0e9250070..f418a0203 100644 --- a/RiotSwiftUI/Modules/Common/Theme/Themes/DarkThemeSwiftUI.swift +++ b/RiotSwiftUI/Modules/Common/Theme/Themes/DarkThemeSwiftUI.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +14,12 @@ // limitations under the License. // -import Foundation import DesignKit +import Foundation struct DarkThemeSwiftUI: ThemeSwiftUI { var identifier: ThemeIdentifier = .dark - let isDark: Bool = true + let isDark = true var colors: ColorSwiftUI = DarkColors.swiftUI - var fonts: FontSwiftUI = FontSwiftUI(values: ElementFonts()) + var fonts = FontSwiftUI(values: ElementFonts()) } diff --git a/RiotSwiftUI/Modules/Common/Theme/Themes/DefaultThemeSwiftUI.swift b/RiotSwiftUI/Modules/Common/Theme/Themes/DefaultThemeSwiftUI.swift index 85ba4d810..d0b12fd79 100644 --- a/RiotSwiftUI/Modules/Common/Theme/Themes/DefaultThemeSwiftUI.swift +++ b/RiotSwiftUI/Modules/Common/Theme/Themes/DefaultThemeSwiftUI.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +14,12 @@ // limitations under the License. // -import Foundation import DesignKit +import Foundation struct DefaultThemeSwiftUI: ThemeSwiftUI { var identifier: ThemeIdentifier = .light - let isDark: Bool = false + let isDark = false var colors: ColorSwiftUI = LightColors.swiftUI - var fonts: FontSwiftUI = FontSwiftUI(values: ElementFonts()) + var fonts = FontSwiftUI(values: ElementFonts()) } diff --git a/RiotSwiftUI/Modules/Common/Util/BorderModifier.swift b/RiotSwiftUI/Modules/Common/Util/BorderModifier.swift index 6d5ad7aef..f88e8141f 100644 --- a/RiotSwiftUI/Modules/Common/Util/BorderModifier.swift +++ b/RiotSwiftUI/Modules/Common/Util/BorderModifier.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct BorderModifier: ViewModifier { - var color: Color var borderWidth: CGFloat var shape: Shape diff --git a/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift b/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift index fe75aa300..6859e4712 100644 --- a/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift +++ b/RiotSwiftUI/Modules/Common/Util/BorderedInputFieldStyle.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,20 +15,19 @@ // import Foundation -import SwiftUI import Introspect +import SwiftUI /// A bordered style of text input /// /// As defined in: /// https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=2039%3A26415 struct BorderedInputFieldStyle: TextFieldStyle { - @Environment(\.theme) private var theme: ThemeSwiftUI @Environment(\.isEnabled) private var isEnabled: Bool - var isEditing: Bool = false - var isError: Bool = false + var isEditing = false + var isError = false private var borderColor: Color { if isError { @@ -47,7 +46,7 @@ struct BorderedInputFieldStyle: TextFieldStyle { } private var textColor: Color { - if (theme.identifier == ThemeIdentifier.dark) { + if theme.identifier == ThemeIdentifier.dark { return (isEnabled ? theme.colors.primaryContent : theme.colors.tertiaryContent) } else { return (isEnabled ? theme.colors.primaryContent : theme.colors.quarterlyContent) @@ -55,18 +54,18 @@ struct BorderedInputFieldStyle: TextFieldStyle { } private var backgroundColor: Color { - if !isEnabled && (theme.identifier == ThemeIdentifier.dark) { + if !isEnabled, theme.identifier == ThemeIdentifier.dark { return theme.colors.quinaryContent } return theme.colors.background } private var placeholderColor: Color { - return theme.colors.tertiaryContent + theme.colors.tertiaryContent } private var borderWidth: CGFloat { - return isEditing || isError ? 2.0 : 1.5 + isEditing || isError ? 2.0 : 1.5 } func _body(configuration: TextField<_Label>) -> some View { diff --git a/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift b/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift index 7eb67d39c..209c61aa1 100644 --- a/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift +++ b/RiotSwiftUI/Modules/Common/Util/ClearViewModifier.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,7 @@ import SwiftUI extension ThemableTextEditor { func showClearButton(text: Binding, alignment: VerticalAlignment = .top) -> some View { - return modifier(ClearViewModifier(alignment: alignment, text: text)) + modifier(ClearViewModifier(alignment: alignment, text: text)) } } diff --git a/RiotSwiftUI/Modules/Common/Util/InlineTextButton.swift b/RiotSwiftUI/Modules/Common/Util/InlineTextButton.swift index fa394b827..e5673b12c 100644 --- a/RiotSwiftUI/Modules/Common/Util/InlineTextButton.swift +++ b/RiotSwiftUI/Modules/Common/Util/InlineTextButton.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import SwiftUI @available(iOS, introduced: 14.0, deprecated: 15.0, message: "Use Text with an AttributedString instead that includes a link and handle the tap by adding an OpenURLAction to the environment.") /// A `Button`, that fakes having a tappable string inside of a regular string. struct InlineTextButton: View { - private struct StringComponent { let string: Substring let isTinted: Bool @@ -33,7 +32,6 @@ struct InlineTextButton: View { private let components: [StringComponent] private let action: () -> Void - // MARK: - Setup /// Creates a new `InlineTextButton`. @@ -43,7 +41,7 @@ struct InlineTextButton: View { /// - action: The action to perform when tapping the button. internal init(_ mainText: String, tappableText: String, action: @escaping () -> Void) { guard let range = mainText.range(of: "%@") else { - self.components = [StringComponent(string: Substring(mainText), isTinted: false)] + components = [StringComponent(string: Substring(mainText), isTinted: false)] self.action = action return } @@ -52,7 +50,7 @@ struct InlineTextButton: View { let middleComponent = StringComponent(string: Substring(tappableText), isTinted: true) let lastComponent = StringComponent(string: mainText[range.upperBound...], isTinted: false) - self.components = [firstComponent, middleComponent, lastComponent] + components = [firstComponent, middleComponent, lastComponent] self.action = action } @@ -63,7 +61,7 @@ struct InlineTextButton: View { EmptyView() } .buttonStyle(Style(components: components)) - .accessibilityLabel(components.map { $0.string }.joined()) + .accessibilityLabel(components.map(\.string).joined()) } private struct Style: ButtonStyle { diff --git a/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift b/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift index 5e20f11b0..08e1712ab 100644 --- a/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift +++ b/RiotSwiftUI/Modules/Common/Util/MultilineTextField.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,14 +17,13 @@ import SwiftUI struct MultilineTextField: View { - @Environment(\.theme) private var theme: ThemeSwiftUI @Binding private var text: String @State private var dynamicHeight: CGFloat = 100 @State private var isEditing = false - private var placeholder: String = "" + private var placeholder = "" private var showingPlaceholder: Bool { text.isEmpty @@ -32,11 +31,11 @@ struct MultilineTextField: View { init(_ placeholder: String, text: Binding) { self.placeholder = placeholder - self._text = text + _text = text } private var textColor: Color { - if (theme.identifier == ThemeIdentifier.dark) { + if theme.identifier == ThemeIdentifier.dark { return theme.colors.primaryContent } else { return theme.colors.primaryContent @@ -44,11 +43,11 @@ struct MultilineTextField: View { } private var backgroundColor: Color { - return theme.colors.background + theme.colors.background } private var placeholderColor: Color { - return theme.colors.tertiaryContent + theme.colors.tertiaryContent } private var borderColor: Color { @@ -60,7 +59,7 @@ struct MultilineTextField: View { } private var borderWidth: CGFloat { - return isEditing ? 2.0 : 1.5 + isEditing ? 2.0 : 1.5 } var body: some View { @@ -91,7 +90,7 @@ struct MultilineTextField: View { } } -fileprivate struct UITextViewWrapper: UIViewRepresentable { +private struct UITextViewWrapper: UIViewRepresentable { typealias UIViewType = UITextView @Binding var text: String @@ -115,8 +114,8 @@ fileprivate struct UITextViewWrapper: UIViewRepresentable { } func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) { - if uiView.text != self.text { - uiView.text = self.text + if uiView.text != text { + uiView.text = text } UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight) @@ -132,7 +131,7 @@ fileprivate struct UITextViewWrapper: UIViewRepresentable { } func makeCoordinator() -> Coordinator { - return Coordinator(text: $text, height: $calculatedHeight, isEditing: $isEditing) + Coordinator(text: $text, height: $calculatedHeight, isEditing: $isEditing) } final class Coordinator: NSObject, UITextViewDelegate { @@ -142,7 +141,7 @@ fileprivate struct UITextViewWrapper: UIViewRepresentable { init(text: Binding, height: Binding, isEditing: Binding) { self.text = text - self.calculatedHeight = height + calculatedHeight = height self.isEditing = isEditing } @@ -171,9 +170,8 @@ fileprivate struct UITextViewWrapper: UIViewRepresentable { } struct MultilineTextField_Previews: PreviewProvider { - static var previews: some View { - return Group { + Group { VStack { PreviewWrapper() PlaceholderPreviewWrapper() diff --git a/RiotSwiftUI/Modules/Common/Util/OptionButton.swift b/RiotSwiftUI/Modules/Common/Util/OptionButton.swift index 17e54bbda..7758ae5c7 100644 --- a/RiotSwiftUI/Modules/Common/Util/OptionButton.swift +++ b/RiotSwiftUI/Modules/Common/Util/OptionButton.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,11 +17,9 @@ import SwiftUI struct OptionButton: View { - // MARK: - Style private struct Style: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.97 : 1) @@ -61,8 +59,7 @@ struct OptionButton: View { .background(theme.colors.quinaryContent) .foregroundColor(theme.colors.secondaryContent) .clipShape(RoundedRectangle(cornerRadius: 8)) - } - ) + }) .buttonStyle(Style()) } } @@ -73,14 +70,14 @@ struct OptionButton_Previews: PreviewProvider { static var previews: some View { Group { VStack { - OptionButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", detailMessage: "Some details for this option", action: {}).theme(.light) - OptionButton(icon: nil, title: "A title", detailMessage: "Some details for this option", action: {}).theme(.light) - OptionButton(icon: nil, title: "A title", detailMessage: nil, action: {}).theme(.light) + OptionButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", detailMessage: "Some details for this option", action: { }).theme(.light) + OptionButton(icon: nil, title: "A title", detailMessage: "Some details for this option", action: { }).theme(.light) + OptionButton(icon: nil, title: "A title", detailMessage: nil, action: { }).theme(.light) } VStack { - OptionButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", detailMessage: "Some details for this option", action: {}).theme(.dark) - OptionButton(icon: nil, title: "A title", detailMessage: "Some details for this option", action: {}).theme(.dark) - OptionButton(icon: nil, title: "A title", detailMessage: nil, action: {}).theme(.dark) + OptionButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", detailMessage: "Some details for this option", action: { }).theme(.dark) + OptionButton(icon: nil, title: "A title", detailMessage: "Some details for this option", action: { }).theme(.dark) + OptionButton(icon: nil, title: "A title", detailMessage: nil, action: { }).theme(.dark) }.preferredColorScheme(.dark) } .padding() diff --git a/RiotSwiftUI/Modules/Common/Util/PasswordButtonModifier.swift b/RiotSwiftUI/Modules/Common/Util/PasswordButtonModifier.swift index 94fd8525a..0e0f38586 100644 --- a/RiotSwiftUI/Modules/Common/Util/PasswordButtonModifier.swift +++ b/RiotSwiftUI/Modules/Common/Util/PasswordButtonModifier.swift @@ -19,7 +19,6 @@ import SwiftUI /// Adds a reveal password button (e.g. an eye button) on the /// right side of the view. For use with `ThemableTextField`. struct PasswordButtonModifier: ViewModifier { - // MARK: - Properties let text: String diff --git a/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift b/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift index 61dc58ad3..269150af2 100644 --- a/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift +++ b/RiotSwiftUI/Modules/Common/Util/PrimaryActionButtonStyle.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,7 +20,7 @@ struct PrimaryActionButtonStyle: ButtonStyle { @Environment(\.theme) private var theme @Environment(\.isEnabled) private var isEnabled - var customColor: Color? = nil + var customColor: Color? private var fontColor: Color { // Always white unless disabled with a dark theme. @@ -69,7 +69,7 @@ struct PrimaryActionButtonStyle_Previews: PreviewProvider { .buttonStyle(PrimaryActionButtonStyle(customColor: .clear)) Button("Red BG") { } - .buttonStyle(PrimaryActionButtonStyle(customColor: .red)) + .buttonStyle(PrimaryActionButtonStyle(customColor: .red)) } .padding() } diff --git a/RiotSwiftUI/Modules/Common/Util/RadioButton.swift b/RiotSwiftUI/Modules/Common/Util/RadioButton.swift index 69e408410..77835c45f 100644 --- a/RiotSwiftUI/Modules/Common/Util/RadioButton.swift +++ b/RiotSwiftUI/Modules/Common/Util/RadioButton.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct RadioButton: View { - // MARK: - Properties var title: String @@ -61,8 +60,8 @@ struct RadioButton_Previews: PreviewProvider { static var buttonGroup: some View { VStack { - RadioButton(title: "A title", selected: false, action: {}) - RadioButton(title: "A title", selected: true, action: {}) + RadioButton(title: "A title", selected: false, action: { }) + RadioButton(title: "A title", selected: true, action: { }) } } } diff --git a/RiotSwiftUI/Modules/Common/Util/ReadableFrameModifier.swift b/RiotSwiftUI/Modules/Common/Util/ReadableFrameModifier.swift index d04f413b2..257431e35 100644 --- a/RiotSwiftUI/Modules/Common/Util/ReadableFrameModifier.swift +++ b/RiotSwiftUI/Modules/Common/Util/ReadableFrameModifier.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,7 @@ import SwiftUI /// Positions this view within an invisible frame that fills the width of its parent view, /// whilst limiting the width of the content to a readable size (which is customizable). -fileprivate struct ReadableFrameModifier: ViewModifier { +private struct ReadableFrameModifier: ViewModifier { var maxWidth: CGFloat func body(content: Content) -> some View { diff --git a/RiotSwiftUI/Modules/Common/Util/ResponderManager.swift b/RiotSwiftUI/Modules/Common/Util/ResponderManager.swift index 21738bae9..3148c2cf4 100644 --- a/RiotSwiftUI/Modules/Common/Util/ResponderManager.swift +++ b/RiotSwiftUI/Modules/Common/Util/ResponderManager.swift @@ -18,8 +18,7 @@ import UIKit /// `ResponderManager` is used to chain `SwiftUI` text editing views that embed `UIKit` text editing views using `UIViewRepresentable` class ResponderManager { - - private static var tagIndex: Int = 1000 + private static var tagIndex = 1000 private static var registeredResponders = NSMapTable(keyOptions: .strongMemory, valueOptions: .weakMemory) private static var nextIndex: Int { @@ -63,7 +62,7 @@ class ResponderManager { /// Tries to get the focused registered responder and give the focus to it's next responder /// - Returns: `True` if the next responder has been found and is successfully focused. `False` otherwise. static func makeActiveNextResponder() -> Bool { - guard let firstResponder = self.firstResponder else { + guard let firstResponder = firstResponder else { return false } diff --git a/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextEditor.swift b/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextEditor.swift index 38e50ff19..240b9ebba 100644 --- a/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextEditor.swift +++ b/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextEditor.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,17 +17,16 @@ import SwiftUI struct RoundedBorderTextEditor: View { - // MARK: - Properties - var title: String? = nil + var title: String? let placeHolder: String @Binding var text: String - var textMaxHeight: CGFloat? = nil - var error: String? = nil + var textMaxHeight: CGFloat? + var error: String? - var onTextChanged: ((String) -> Void)? = nil - var onEditingChanged: ((Bool) -> Void)? = nil + var onTextChanged: ((String) -> Void)? + var onEditingChanged: ((Bool) -> Void)? @State private var editing = false @@ -62,7 +61,7 @@ struct RoundedBorderTextEditor: View { }) .showClearButton(text: $text) // Found no good solution here. Hidding next button for the moment - // .modifier(NextViewModifier(alignment: .bottomTrailing, isEditing: $editing)) + // .modifier(NextViewModifier(alignment: .bottomTrailing, isEditing: $editing)) .padding(EdgeInsets(top: 2, leading: 6, bottom: 0, trailing: 0)) .onChange(of: text, perform: { newText in onTextChanged?(newText) @@ -82,7 +81,7 @@ struct RoundedBorderTextEditor: View { } .background(RoundedRectangle(cornerRadius: 8).fill(theme.colors.background)) .overlay(RoundedRectangle(cornerRadius: 8) - .stroke(editing ? theme.colors.accent : (error == nil ? theme.colors.quinaryContent : theme.colors.alert), lineWidth: editing || error != nil ? 2 : 1)) + .stroke(editing ? theme.colors.accent : (error == nil ? theme.colors.quinaryContent : theme.colors.alert), lineWidth: editing || error != nil ? 2 : 1)) .frame(height: textMaxHeight) if let error = self.error { Text(error) @@ -101,7 +100,6 @@ struct RoundedBorderTextEditor: View { struct ThemableTextEditor_Previews: PreviewProvider { static var previews: some View { - Group { sampleView.theme(.light).preferredColorScheme(.light) sampleView.theme(.dark).preferredColorScheme(.dark) diff --git a/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift b/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift index c0c69a209..7acf765cc 100644 --- a/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift +++ b/RiotSwiftUI/Modules/Common/Util/RoundedBorderTextField.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,22 +17,21 @@ import SwiftUI struct RoundedBorderTextField: View { - // MARK: - Properties - var title: String? = nil + var title: String? let placeHolder: String @Binding var text: String - var footerText: String? = nil - var isError: Bool = false + var footerText: String? + var isError = false var isFirstResponder = false - var configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration() + var configuration = UIKitTextInputConfiguration() @State var isSecureTextVisible = false - var onTextChanged: ((String) -> Void)? = nil - var onEditingChanged: ((Bool) -> Void)? = nil - var onCommit: (() -> Void)? = nil + var onTextChanged: ((String) -> Void)? + var onEditingChanged: ((Bool) -> Void)? + var onCommit: (() -> Void)? // MARK: Private @@ -101,7 +100,7 @@ struct RoundedBorderTextField: View { private var borderColor: Color { if isEditing { return theme.colors.accent - } else if footerText != nil && isError { + } else if footerText != nil, isError { return theme.colors.alert } else { return theme.colors.quinaryContent @@ -118,7 +117,6 @@ struct RoundedBorderTextField: View { struct TextFieldWithError_Previews: PreviewProvider { static var previews: some View { - Group { sampleView.theme(.light).preferredColorScheme(.light) sampleView.theme(.dark).preferredColorScheme(.dark) diff --git a/RiotSwiftUI/Modules/Common/Util/RoundedCornerShape.swift b/RiotSwiftUI/Modules/Common/Util/RoundedCornerShape.swift index 0fc77b56e..e34197eec 100644 --- a/RiotSwiftUI/Modules/Common/Util/RoundedCornerShape.swift +++ b/RiotSwiftUI/Modules/Common/Util/RoundedCornerShape.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import Foundation import SwiftUI struct RoundedCornerShape: Shape { - let radius: CGFloat let corners: UIRectCorner diff --git a/RiotSwiftUI/Modules/Common/Util/SafeBindingCollectionEnumerator.swift b/RiotSwiftUI/Modules/Common/Util/SafeBindingCollectionEnumerator.swift index 92d6135cd..f6a16c21f 100644 --- a/RiotSwiftUI/Modules/Common/Util/SafeBindingCollectionEnumerator.swift +++ b/RiotSwiftUI/Modules/Common/Util/SafeBindingCollectionEnumerator.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,7 +22,6 @@ import SwiftUI Replace with Swift 5.5 bindings enumerator later. */ struct SafeBindingCollectionEnumerator: View { - typealias BoundElement = Binding private let binding: BoundElement private let content: (BoundElement) -> C diff --git a/RiotSwiftUI/Modules/Common/Util/ScreenTrackerViewModifier.swift b/RiotSwiftUI/Modules/Common/Util/ScreenTrackerViewModifier.swift index b8eaf74d2..fe5ec5bb5 100644 --- a/RiotSwiftUI/Modules/Common/Util/ScreenTrackerViewModifier.swift +++ b/RiotSwiftUI/Modules/Common/Util/ScreenTrackerViewModifier.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,6 +33,6 @@ struct ScreenTrackerViewModifier: ViewModifier { extension View { func track(screen: AnalyticsScreen) -> some View { - return self.modifier(ScreenTrackerViewModifier(screen: screen)) + modifier(ScreenTrackerViewModifier(screen: screen)) } } diff --git a/RiotSwiftUI/Modules/Common/Util/SearchBar.swift b/RiotSwiftUI/Modules/Common/Util/SearchBar.swift index 4edaa2e5c..63b3863b9 100644 --- a/RiotSwiftUI/Modules/Common/Util/SearchBar.swift +++ b/RiotSwiftUI/Modules/Common/Util/SearchBar.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct SearchBar: View { - // MARK: - Properties var placeholder: String @@ -49,7 +48,7 @@ struct SearchBar: View { .foregroundColor(theme.colors.quarterlyContent) .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) - if isEditing && !text.isEmpty { + if isEditing, !text.isEmpty { Button(action: { self.text = "" }) { diff --git a/RiotSwiftUI/Modules/Common/Util/SecondaryActionButtonStyle.swift b/RiotSwiftUI/Modules/Common/Util/SecondaryActionButtonStyle.swift index 8f0eb6aac..917ad1997 100644 --- a/RiotSwiftUI/Modules/Common/Util/SecondaryActionButtonStyle.swift +++ b/RiotSwiftUI/Modules/Common/Util/SecondaryActionButtonStyle.swift @@ -20,7 +20,7 @@ struct SecondaryActionButtonStyle: ButtonStyle { @Environment(\.theme) private var theme @Environment(\.isEnabled) private var isEnabled - var customColor: Color? = nil + var customColor: Color? func makeBody(configuration: Self.Configuration) -> some View { configuration.label @@ -29,8 +29,8 @@ struct SecondaryActionButtonStyle: ButtonStyle { .foregroundColor(customColor ?? theme.colors.accent) .font(theme.fonts.body) .background(RoundedRectangle(cornerRadius: 8) - .strokeBorder() - .foregroundColor(customColor ?? theme.colors.accent)) + .strokeBorder() + .foregroundColor(customColor ?? theme.colors.accent)) .opacity(opacity(when: configuration.isPressed)) } @@ -62,7 +62,7 @@ struct SecondaryActionButtonStyle_Previews: PreviewProvider { .disabled(true) Button("Red BG") { } - .buttonStyle(SecondaryActionButtonStyle(customColor: .red)) + .buttonStyle(SecondaryActionButtonStyle(customColor: .red)) Button { } label: { Text("Custom") diff --git a/RiotSwiftUI/Modules/Common/Util/StyledText.swift b/RiotSwiftUI/Modules/Common/Util/StyledText.swift index 1c4b0a74f..0ed73d4a9 100644 --- a/RiotSwiftUI/Modules/Common/Util/StyledText.swift +++ b/RiotSwiftUI/Modules/Common/Util/StyledText.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,14 +14,13 @@ // limitations under the License. // -import SwiftUI import DesignKit +import SwiftUI @available(iOS, introduced: 14.0, deprecated: 15.0, message: "Use Text with an AttributedString instead.") /// A `Text` view that renders attributed strings with their `.font` and `.foregroundColor` attributes. /// This view is a workaround for iOS 13/14 not supporting `AttributedString`. struct StyledText: View { - // MARK: - Properties // MARK: Private @@ -31,8 +30,8 @@ struct StyledText: View { /// A string with a bold property. private struct StringComponent { let string: String - var font: Font? = nil - var color: Color? = nil + var font: Font? + var color: Color? } /// Internal representation of the string as composable parts. @@ -47,7 +46,7 @@ struct StyledText: View { let range = NSRange(location: 0, length: attributedString.length) let string = attributedString.string as NSString - attributedString.enumerateAttributes(in: range, options: []) { attributes, range, stop in + attributedString.enumerateAttributes(in: range, options: []) { attributes, range, _ in let font = attributes[.font] as? UIFont let color = attributes[.foregroundColor] as? UIColor @@ -66,7 +65,7 @@ struct StyledText: View { /// Creates a `StyledText` using a plain string. /// - Parameter string: The plain string to display init(_ string: String) { - self.components = [StringComponent(string: string, font: nil)] + components = [StringComponent(string: string, font: nil)] } // MARK: - Views @@ -80,7 +79,6 @@ struct StyledText: View { } } - struct StyledText_Previews: PreviewProvider { static func prettyText() -> NSAttributedString { let string = NSMutableAttributedString(string: "T", attributes: [ diff --git a/RiotSwiftUI/Modules/Common/Util/ThemableButton.swift b/RiotSwiftUI/Modules/Common/Util/ThemableButton.swift index 61a066c38..61dd9a84e 100644 --- a/RiotSwiftUI/Modules/Common/Util/ThemableButton.swift +++ b/RiotSwiftUI/Modules/Common/Util/ThemableButton.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,11 +17,9 @@ import SwiftUI struct ThemableButton: View { - // MARK: - Style private struct Style: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.97 : 1) @@ -67,12 +65,12 @@ struct ThemableButton_Previews: PreviewProvider { static var previews: some View { Group { VStack(alignment: .center, spacing: 20) { - ThemableButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", action: {}).theme(.light).preferredColorScheme(.light) - ThemableButton(icon: nil, title: "A title", action: {}).theme(.light).preferredColorScheme(.light) + ThemableButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", action: { }).theme(.light).preferredColorScheme(.light) + ThemableButton(icon: nil, title: "A title", action: { }).theme(.light).preferredColorScheme(.light) } VStack(alignment: .center, spacing: 20) { - ThemableButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", action: {}).theme(.dark).preferredColorScheme(.dark) - ThemableButton(icon: nil, title: "A title", action: {}).theme(.dark).preferredColorScheme(.dark) + ThemableButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", action: { }).theme(.dark).preferredColorScheme(.dark) + ThemableButton(icon: nil, title: "A title", action: { }).theme(.dark).preferredColorScheme(.dark) } } .padding() diff --git a/RiotSwiftUI/Modules/Common/Util/ThemableNavigationBar.swift b/RiotSwiftUI/Modules/Common/Util/ThemableNavigationBar.swift index 2c6fc3727..3f7bf833e 100644 --- a/RiotSwiftUI/Modules/Common/Util/ThemableNavigationBar.swift +++ b/RiotSwiftUI/Modules/Common/Util/ThemableNavigationBar.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct ThemableNavigationBar: View { - // MARK: - Style // MARK: - Properties @@ -36,8 +35,7 @@ struct ThemableNavigationBar: View { @ViewBuilder var body: some View { HStack { - Button(action: {backAction()}) - { + Button(action: { backAction() }) { Image(uiImage: Asset.Images.spacesModalBack.image) .renderingMode(.template) .foregroundColor(theme.colors.secondaryContent) @@ -49,8 +47,7 @@ struct ThemableNavigationBar: View { .foregroundColor(theme.colors.primaryContent) } Spacer() - Button(action: {closeAction()}) - { + Button(action: { closeAction() }) { Image(uiImage: Asset.Images.spacesModalClose.image) .renderingMode(.template) .foregroundColor(theme.colors.secondaryContent) @@ -68,16 +65,16 @@ struct NavigationBar_Previews: PreviewProvider { static var previews: some View { Group { VStack { - ThemableNavigationBar(title: nil, showBackButton: true, backAction: {}, closeAction: {}) - ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: {}, closeAction: {}) - ThemableNavigationBar(title: nil, showBackButton: false, backAction: {}, closeAction: {}) - ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: {}, closeAction: {}) + ThemableNavigationBar(title: nil, showBackButton: true, backAction: { }, closeAction: { }) + ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: { }, closeAction: { }) + ThemableNavigationBar(title: nil, showBackButton: false, backAction: { }, closeAction: { }) + ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: { }, closeAction: { }) } VStack { - ThemableNavigationBar(title: nil, showBackButton: true, backAction: {}, closeAction: {}).theme(.dark) - ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: {}, closeAction: {}).theme(.dark) - ThemableNavigationBar(title: nil, showBackButton: false, backAction: {}, closeAction: {}).theme(.dark) - ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: {}, closeAction: {}).theme(.dark) + ThemableNavigationBar(title: nil, showBackButton: true, backAction: { }, closeAction: { }).theme(.dark) + ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: { }, closeAction: { }).theme(.dark) + ThemableNavigationBar(title: nil, showBackButton: false, backAction: { }, closeAction: { }).theme(.dark) + ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: { }, closeAction: { }).theme(.dark) }.preferredColorScheme(.dark) } .padding() diff --git a/RiotSwiftUI/Modules/Common/Util/ThemableTextEditor.swift b/RiotSwiftUI/Modules/Common/Util/ThemableTextEditor.swift index d9f0d1ca7..bdc529822 100644 --- a/RiotSwiftUI/Modules/Common/Util/ThemableTextEditor.swift +++ b/RiotSwiftUI/Modules/Common/Util/ThemableTextEditor.swift @@ -16,20 +16,18 @@ import SwiftUI - struct ThemableTextEditor: UIViewRepresentable { - // MARK: Properties @Binding var text: String - @State var configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration() + @State var configuration = UIKitTextInputConfiguration() var onEditingChanged: ((_ edit: Bool) -> Void)? // MARK: Private @Environment(\.theme) private var theme: ThemeSwiftUI - private let textView: UITextView = UITextView() + private let textView = UITextView() private let internalParams = InternalParams() // MARK: Setup @@ -37,8 +35,8 @@ struct ThemableTextEditor: UIViewRepresentable { init(text: Binding, configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration(), onEditingChanged: ((_ edit: Bool) -> Void)? = nil) { - self._text = text - self._configuration = State(initialValue: configuration) + _text = text + _configuration = State(initialValue: configuration) self.onEditingChanged = onEditingChanged ResponderManager.register(view: textView) @@ -63,8 +61,8 @@ struct ThemableTextEditor: UIViewRepresentable { uiView.textColor = UIColor(theme.colors.primaryContent) uiView.tintColor = UIColor(theme.colors.accent) - if uiView.text != self.text { - uiView.text = self.text + if uiView.text != text { + uiView.text = text } uiView.keyboardType = configuration.keyboardType @@ -81,7 +79,7 @@ struct ThemableTextEditor: UIViewRepresentable { // MARK: - Private private func replaceText(with newText: String) { - self.text = newText + text = newText } private class InternalParams { @@ -91,7 +89,7 @@ struct ThemableTextEditor: UIViewRepresentable { // MARK: - Coordinator func makeCoordinator() -> Coordinator { - return Coordinator(self) + Coordinator(self) } class Coordinator: NSObject, UITextViewDelegate { diff --git a/RiotSwiftUI/Modules/Common/Util/ThemableTextField.swift b/RiotSwiftUI/Modules/Common/Util/ThemableTextField.swift index fa31d1f02..a179d36ab 100644 --- a/RiotSwiftUI/Modules/Common/Util/ThemableTextField.swift +++ b/RiotSwiftUI/Modules/Common/Util/ThemableTextField.swift @@ -19,18 +19,17 @@ import SwiftUI struct UIKitTextInputConfiguration { var keyboardType: UIKeyboardType = .default var returnKeyType: UIReturnKeyType = .default - var isSecureTextEntry: Bool = false + var isSecureTextEntry = false var autocapitalizationType: UITextAutocapitalizationType = .sentences var autocorrectionType: UITextAutocorrectionType = .default } struct ThemableTextField: UIViewRepresentable { - // MARK: Properties @State var placeholder: String? @Binding var text: String - @State var configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration() + @State var configuration = UIKitTextInputConfiguration() @Binding var isSecureTextVisible: Bool var onEditingChanged: ((_ edit: Bool) -> Void)? var onCommit: (() -> Void)? @@ -39,7 +38,7 @@ struct ThemableTextField: UIViewRepresentable { @Environment(\.theme) private var theme: ThemeSwiftUI - private let textField: UITextField = UITextField() + private let textField = UITextField() private let internalParams = InternalParams() // MARK: Setup @@ -50,10 +49,10 @@ struct ThemableTextField: UIViewRepresentable { isSecureTextVisible: Binding = .constant(false), onEditingChanged: ((_ edit: Bool) -> Void)? = nil, onCommit: (() -> Void)? = nil) { - self._text = text - self._placeholder = State(initialValue: placeholder) - self._configuration = State(initialValue: configuration) - self._isSecureTextVisible = isSecureTextVisible + _text = text + _placeholder = State(initialValue: placeholder) + _configuration = State(initialValue: configuration) + _isSecureTextVisible = isSecureTextVisible self.onEditingChanged = onEditingChanged self.onCommit = onCommit @@ -84,8 +83,8 @@ struct ThemableTextField: UIViewRepresentable { uiView.textColor = UIColor(theme.colors.primaryContent) uiView.tintColor = UIColor(theme.colors.accent) - if uiView.text != self.text { - uiView.text = self.text + if uiView.text != text { + uiView.text = text } uiView.placeholder = placeholder @@ -103,17 +102,16 @@ struct ThemableTextField: UIViewRepresentable { // MARK: - Private private func replaceText(with newText: String) { - self.text = newText + text = newText } // MARK: - Coordinator func makeCoordinator() -> Coordinator { - return Coordinator(self) + Coordinator(self) } class Coordinator: NSObject, UITextFieldDelegate { - var parent: ThemableTextField init(_ parent: ThemableTextField) { @@ -146,14 +144,13 @@ struct ThemableTextField: UIViewRepresentable { private class InternalParams { var isFirstResponder = false } - } // MARK: - modifiers extension ThemableTextField { func makeFirstResponder() -> ThemableTextField { - return makeFirstResponder(true) + makeFirstResponder(true) } func makeFirstResponder(_ isFirstResponder: Bool) -> ThemableTextField { @@ -167,7 +164,7 @@ extension ThemableTextField { /// - alignment: The vertical alignment of the button in the text field. Default to `center` @ViewBuilder func addButton(_ show: Bool, alignment: VerticalAlignment = .center) -> some View { - if show && configuration.isSecureTextEntry { + if show, configuration.isSecureTextEntry { modifier(PasswordButtonModifier(text: text, isSecureTextVisible: $isSecureTextVisible, alignment: alignment)) diff --git a/RiotSwiftUI/Modules/Common/Util/View+Riot.swift b/RiotSwiftUI/Modules/Common/Util/View+Riot.swift index d126ec8d0..89aae532c 100644 --- a/RiotSwiftUI/Modules/Common/Util/View+Riot.swift +++ b/RiotSwiftUI/Modules/Common/Util/View+Riot.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,7 +20,7 @@ import SwiftUI extension View { @ViewBuilder func isHidden(_ isHidden: Bool) -> some View { if isHidden { - self.hidden() + hidden() } else { self } diff --git a/RiotSwiftUI/Modules/Common/Util/WaitOverlay.swift b/RiotSwiftUI/Modules/Common/Util/WaitOverlay.swift index 60f7315bb..db88cc486 100644 --- a/RiotSwiftUI/Modules/Common/Util/WaitOverlay.swift +++ b/RiotSwiftUI/Modules/Common/Util/WaitOverlay.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import SwiftUI /// A modifier for showing the wait overlay view over a view. struct WaitOverlayModifier: ViewModifier { - var allowUserInteraction: Bool var show: Bool var message: String? @@ -27,15 +26,16 @@ struct WaitOverlayModifier: ViewModifier { func body(content: Content) -> some View { content .modifier(WaitOverlay( - allowUserInteraction: allowUserInteraction, - message: message, - isLoading: show)) + allowUserInteraction: allowUserInteraction, + message: message, + isLoading: show + )) } } extension View { func waitOverlay(show: Bool, message: String? = nil, allowUserInteraction: Bool = true) -> some View { - self.modifier(WaitOverlayModifier(allowUserInteraction: allowUserInteraction, show: show, message: message)) + modifier(WaitOverlayModifier(allowUserInteraction: allowUserInteraction, show: show, message: message)) } } @@ -44,7 +44,7 @@ struct WaitOverlay: ViewModifier { // MARK: - Properties var alignment: Alignment = .center - var allowUserInteraction: Bool = true + var allowUserInteraction = true var message: String? var isLoading: Bool @@ -66,8 +66,7 @@ struct WaitOverlay: ViewModifier { // MARK: - Public - public func body(content: Content) -> some View - { + public func body(content: Content) -> some View { ZStack { content if isLoading { @@ -89,7 +88,7 @@ struct WaitOverlay: ViewModifier { } .padding(12) .background(RoundedRectangle(cornerRadius: 8, style: .continuous) - .fill(theme.colors.navigation.opacity(0.9))) + .fill(theme.colors.navigation.opacity(0.9))) } .edgesIgnoringSafeArea(.all) .transition(.opacity) @@ -103,24 +102,24 @@ struct WaitOverlay_Previews: PreviewProvider { static var previews: some View { Group { VStack { - ThemableNavigationBar(title: nil, showBackButton: true, backAction: {}, closeAction: {}) - ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: {}, closeAction: {}) - ThemableNavigationBar(title: nil, showBackButton: false, backAction: {}, closeAction: {}) - ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: {}, closeAction: {}) + ThemableNavigationBar(title: nil, showBackButton: true, backAction: { }, closeAction: { }) + ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: { }, closeAction: { }) + ThemableNavigationBar(title: nil, showBackButton: false, backAction: { }, closeAction: { }) + ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: { }, closeAction: { }) } .modifier(WaitOverlay(isLoading: true)) VStack { - ThemableNavigationBar(title: nil, showBackButton: true, backAction: {}, closeAction: {}) - ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: {}, closeAction: {}) - ThemableNavigationBar(title: nil, showBackButton: false, backAction: {}, closeAction: {}) - ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: {}, closeAction: {}) + ThemableNavigationBar(title: nil, showBackButton: true, backAction: { }, closeAction: { }) + ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: { }, closeAction: { }) + ThemableNavigationBar(title: nil, showBackButton: false, backAction: { }, closeAction: { }) + ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: { }, closeAction: { }) } - .modifier(WaitOverlay(alignment:.topLeading, isLoading: true)) + .modifier(WaitOverlay(alignment: .topLeading, isLoading: true)) VStack { - ThemableNavigationBar(title: nil, showBackButton: true, backAction: {}, closeAction: {}).theme(.dark) - ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: {}, closeAction: {}).theme(.dark) - ThemableNavigationBar(title: nil, showBackButton: false, backAction: {}, closeAction: {}).theme(.dark) - ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: {}, closeAction: {}).theme(.dark) + ThemableNavigationBar(title: nil, showBackButton: true, backAction: { }, closeAction: { }).theme(.dark) + ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: { }, closeAction: { }).theme(.dark) + ThemableNavigationBar(title: nil, showBackButton: false, backAction: { }, closeAction: { }).theme(.dark) + ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: { }, closeAction: { }).theme(.dark) } .modifier(WaitOverlay(isLoading: true)).theme(.dark) diff --git a/RiotSwiftUI/Modules/Common/ViewFrameReader/FramePreferenceKey.swift b/RiotSwiftUI/Modules/Common/ViewFrameReader/FramePreferenceKey.swift index 1eb7e97dd..63eb8df9d 100644 --- a/RiotSwiftUI/Modules/Common/ViewFrameReader/FramePreferenceKey.swift +++ b/RiotSwiftUI/Modules/Common/ViewFrameReader/FramePreferenceKey.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Common/ViewFrameReader/ViewFrameReader.swift b/RiotSwiftUI/Modules/Common/ViewFrameReader/ViewFrameReader.swift index 834c018a7..1bb6c50de 100644 --- a/RiotSwiftUI/Modules/Common/ViewFrameReader/ViewFrameReader.swift +++ b/RiotSwiftUI/Modules/Common/ViewFrameReader/ViewFrameReader.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Common/ViewModel/BindableState.swift b/RiotSwiftUI/Modules/Common/ViewModel/BindableState.swift index 79e658af4..8e3d9ce79 100644 --- a/RiotSwiftUI/Modules/Common/ViewModel/BindableState.swift +++ b/RiotSwiftUI/Modules/Common/ViewModel/BindableState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift index 63c1ae7ae..dd1b27d3e 100644 --- a/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift +++ b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift @@ -14,12 +14,11 @@ // limitations under the License. // -import Foundation import Combine - import Foundation -import Combine +import Combine +import Foundation /// A constrained and concise interface for interacting with the ViewModel. /// @@ -34,7 +33,7 @@ import Combine /// It provides a nice layer of consistency and also safety. As we are not passing the `ViewModel` to the view directly, shortcuts/hacks /// can't be made into the `ViewModel`. @dynamicMemberLookup -class ViewModelContext: ObservableObject { +class ViewModelContext: ObservableObject { // MARK: - Properties // MARK: Private @@ -74,8 +73,7 @@ class ViewModelContext: ObservableObject { /// a specific portion of state that can be safely bound to. /// If we decide to add more features to our state management (like doing state processing off the main thread) /// we can do it in this centralised place. -class StateStoreViewModel { - +class StateStoreViewModel { typealias Context = ViewModelContext // MARK: - Properties @@ -98,45 +96,17 @@ class StateStoreViewModel { // MARK: Setup init(initialViewState: State) { - self.context = Context(initialViewState: initialViewState) - self.context.viewActions.sink { [weak self] action in + context = Context(initialViewState: initialViewState) + context.viewActions.sink { [weak self] action in guard let self = self else { return } self.process(viewAction: action) } .store(in: &cancellables) } - /// Send state actions to modify the state within the reducer. - /// - Parameter action: The state action to send to the reducer. - @available(*, deprecated, message: "Mutate state directly instead") - func dispatch(action: StateAction) { - Self.reducer(state: &context.viewState, action: action) - } - - /// Send state actions from a publisher to modify the state within the reducer. - /// - Parameter actionPublisher: The publisher that produces actions to be sent to the reducer - @available(*, deprecated, message: "Mutate state directly instead") - func dispatch(actionPublisher: AnyPublisher) { - actionPublisher.sink { [weak self] action in - guard let self = self else { return } - Self.reducer(state: &self.context.viewState, action: action) - } - .store(in: &cancellables) - } - - /// Override to handle mutations to the `State` - /// - /// A redux style reducer, all modifications to state happen here. - /// - Parameters: - /// - state: The `inout` state to be modified, - /// - action: The action that defines which state modification should take place. - class func reducer(state: inout State, action: StateAction) { - //Default implementation, -no-op - } - /// Override to handles incoming `ViewAction`s from the `ViewModel`. /// - Parameter viewAction: The `ViewAction` to be processed in `ViewModel` implementation. func process(viewAction: ViewAction) { - //Default implementation, -no-op + // Default implementation, -no-op } } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Coordinator/LiveLocationLabPromotionCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Coordinator/LiveLocationLabPromotionCoordinator.swift index fbc5c6c9f..4bbb7c8bf 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Coordinator/LiveLocationLabPromotionCoordinator.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Coordinator/LiveLocationLabPromotionCoordinator.swift @@ -17,7 +17,6 @@ import SwiftUI final class LiveLocationLabPromotionCoordinator: NSObject, Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -50,7 +49,7 @@ final class LiveLocationLabPromotionCoordinator: NSObject, Coordinator, Presenta func start() { MXLog.debug("[LiveLocationLabPromotionCoordinator] did start.") - self.liveLocationLabPromotionViewModel.completion = { [weak self] enableLiveLocation in + liveLocationLabPromotionViewModel.completion = { [weak self] enableLiveLocation in guard let self = self else { return } RiotSettings.shared.enableLiveLocationSharing = enableLiveLocation @@ -62,15 +61,14 @@ final class LiveLocationLabPromotionCoordinator: NSObject, Coordinator, Presenta } func toPresentable() -> UIViewController { - return self.liveLocationLabPromotionHostingController + liveLocationLabPromotionHostingController } } // MARK: - UIAdaptivePresentationControllerDelegate extension LiveLocationLabPromotionCoordinator: UIAdaptivePresentationControllerDelegate { - func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { - self.completion?(RiotSettings.shared.enableLiveLocationSharing) + completion?(RiotSettings.shared.enableLiveLocationSharing) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionModels.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionModels.swift index 266b2d01c..7f5f19429 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionModels.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import Foundation // MARK: View struct LiveLocationLabPromotionViewState: BindableState { - var bindings: LiveLocationLabPromotionBindings } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionViewModel.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionViewModel.swift index eb4ea189c..e3408be02 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionViewModel.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionViewModel.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,12 +16,9 @@ import SwiftUI -typealias LiveLocationLabPromotionViewModelType = StateStoreViewModel +typealias LiveLocationLabPromotionViewModelType = StateStoreViewModel class LiveLocationLabPromotionViewModel: LiveLocationLabPromotionViewModelType, LiveLocationLabPromotionViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -42,7 +39,7 @@ class LiveLocationLabPromotionViewModel: LiveLocationLabPromotionViewModelType, override func process(viewAction: LiveLocationLabPromotionViewAction) { switch viewAction { case .complete: - completion?(self.state.bindings.enableLabFlag) + completion?(state.bindings.enableLabFlag) } } } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionViewModelProtocol.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionViewModelProtocol.swift index a8d741751..45d75cd1c 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/LiveLocationLabPromotionViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol LiveLocationLabPromotionViewModelProtocol { - /// Closure called when screen completes. Indicates true if the lab flag has been enabled. var completion: ((Bool) -> Void)? { get set } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/MockLiveLocationLabPromotionScreenState.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/MockLiveLocationLabPromotionScreenState.swift index f062bfe5f..c49c9e1de 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/MockLiveLocationLabPromotionScreenState.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/MockLiveLocationLabPromotionScreenState.swift @@ -31,7 +31,7 @@ enum MockLiveLocationLabPromotionScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel = LiveLocationLabPromotionViewModel() // can simulate service and viewModel actions here if needs be. @@ -39,6 +39,7 @@ enum MockLiveLocationLabPromotionScreenState: MockScreenState, CaseIterable { return ( [self, viewModel], AnyView(LiveLocationLabPromotionView(viewModel: viewModel.context) - )) + ) + ) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/UI/LiveLocationLabPromotionUITests.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/UI/LiveLocationLabPromotionUITests.swift index 4b71228fc..4a8af4a5b 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/UI/LiveLocationLabPromotionUITests.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/UI/LiveLocationLabPromotionUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class LiveLocationLabPromotionUITests: MockScreenTestCase { // Nothing to test as the view is completely static diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/Unit/LiveLocationLabPromotionViewModelTests.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/Unit/LiveLocationLabPromotionViewModelTests.swift index 284b27694..9dc77e699 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/Unit/LiveLocationLabPromotionViewModelTests.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/Test/Unit/LiveLocationLabPromotionViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/View/LiveLocationLabPromotionView.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/View/LiveLocationLabPromotionView.swift index 8ca11dd23..c92c0e2df 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/View/LiveLocationLabPromotionView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationLabPromotion/View/LiveLocationLabPromotionView.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct LiveLocationLabPromotionView: View { - // MARK: - Properties // MARK: Private @@ -49,7 +48,6 @@ struct LiveLocationLabPromotionView: View { .padding(.top, 1) Toggle(isOn: $viewModel.enableLabFlag) { - Text(VectorL10n.locationSharingLiveLabPromotionActivation) .font(theme.fonts.body) .foregroundColor(theme.colors.primaryContent) diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift index 19be80eec..fc5c6b87a 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift @@ -23,7 +23,6 @@ struct LiveLocationSharingViewerCoordinatorParameters { } final class LiveLocationSharingViewerCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -51,7 +50,8 @@ final class LiveLocationSharingViewerCoordinator: Coordinator, Presentable { let viewModel = LiveLocationSharingViewerViewModel( mapStyleURL: parameters.session.vc_homeserverConfiguration().tileServer.mapStyleURL, - service: service) + service: service + ) let view = LiveLocationSharingViewer(viewModel: viewModel.context) .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) liveLocationSharingViewerViewModel = viewModel @@ -61,6 +61,7 @@ final class LiveLocationSharingViewerCoordinator: Coordinator, Presentable { } // MARK: - Public + func start() { MXLog.debug("[LiveLocationSharingViewerCoordinator] did start.") liveLocationSharingViewerViewModel.completion = { [weak self] result in @@ -74,7 +75,7 @@ final class LiveLocationSharingViewerCoordinator: Coordinator, Presentable { } } - let viewController: UIViewController = self.liveLocationSharingViewerHostingController + let viewController: UIViewController = liveLocationSharingViewerHostingController if navigationRouter.modules.count > 1 { navigationRouter.push(viewController, animated: true, popCompletion: nil) @@ -84,14 +85,13 @@ final class LiveLocationSharingViewerCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return navigationRouter.toPresentable() + navigationRouter.toPresentable() .vc_setModalFullScreen(true) // Set fullscreen as DSBottomSheet is not working with modal pan gesture recognizer } func presentLocationActivityController(with coordinate: CLLocationCoordinate2D) { - let shareActivityController = shareLocationActivityControllerBuilder.build(with: coordinate) - self.liveLocationSharingViewerHostingController.present(shareActivityController, animated: true) + liveLocationSharingViewerHostingController.present(shareActivityController, animated: true) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift index 3cf3271cc..5c4f8a841 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,15 +14,15 @@ // limitations under the License. // -import Foundation import Combine import CoreLocation +import Foundation // MARK: - Coordinator // MARK: View model -enum LiveLocationSharingViewerViewModelResult { +enum LiveLocationSharingViewerViewModelResult { case done case share(_ coordinate: CLLocationCoordinate2D) } @@ -30,7 +30,6 @@ enum LiveLocationSharingViewerViewModelResult { // MARK: View struct LiveLocationSharingViewerViewState: BindableState { - /// Map style URL let mapStyleURL: URL @@ -43,7 +42,7 @@ struct LiveLocationSharingViewerViewState: BindableState { /// Live location list items var listItemsViewData: [LiveLocationListItemViewData] - var showLoadingIndicator: Bool = false + var showLoadingIndicator = false var shareButtonEnabled: Bool { !showLoadingIndicator @@ -51,14 +50,14 @@ struct LiveLocationSharingViewerViewState: BindableState { /// True to indicate that everybody stopped to share live location sharing in the room var isAllLocationSharingEnded: Bool { - return listItemsViewData.isEmpty + listItemsViewData.isEmpty } var isBottomSheetVisible: Bool { - return isAllLocationSharingEnded == false + isAllLocationSharingEnded == false } - var showMapLoadingError: Bool = false + var showMapLoadingError = false let errorSubject = PassthroughSubject() diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift index 9a20870d6..ebfd06220 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift @@ -14,15 +14,13 @@ // limitations under the License. // -import SwiftUI import Combine import Mapbox +import SwiftUI + +typealias LiveLocationSharingViewerViewModelType = StateStoreViewModel -typealias LiveLocationSharingViewerViewModelType = StateStoreViewModel class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType, LiveLocationSharingViewerViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -44,7 +42,6 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType // MARK: - Setup init(mapStyleURL: URL, service: LiveLocationSharingViewerServiceProtocol) { - let viewState = LiveLocationSharingViewerViewState(mapStyleURL: mapStyleURL, annotations: [], highlightedAnnotation: nil, listItemsViewData: []) liveLocationSharingViewerService = service @@ -57,8 +54,8 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType self.processError(error) }.store(in: &cancellables) - self.setupLocationSharingService() - self.setupScreenUpdateTimer() + setupLocationSharingService() + setupScreenUpdateTimer() } // MARK: - Public @@ -70,7 +67,7 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType case .stopSharing: stopUserLocationSharing() case .tapListItem(let userId): - self.highlighAnnotation(with: userId) + highlighAnnotation(with: userId) case .share(let userLocationAnnotation): completion?(.share(userLocationAnnotation.coordinate)) case .mapCreditsDidTap: @@ -81,20 +78,20 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType // MARK: - Private private func setupLocationSharingService() { - self.updateUsersLiveLocation(highlightFirstLocation: true) + updateUsersLiveLocation(highlightFirstLocation: true) liveLocationSharingViewerService.didUpdateUsersLiveLocation = { [weak self] liveLocations in self?.update(with: liveLocations, highlightFirstLocation: false) } - self.liveLocationSharingViewerService.startListeningLiveLocationUpdates() + liveLocationSharingViewerService.startListeningLiveLocationUpdates() } private func updateUsersLiveLocation(highlightFirstLocation: Bool) { - self.update(with: liveLocationSharingViewerService.usersLiveLocation, highlightFirstLocation: highlightFirstLocation) + update(with: liveLocationSharingViewerService.usersLiveLocation, highlightFirstLocation: highlightFirstLocation) } private func setupScreenUpdateTimer() { - self.screenUpdateTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] timer in + screenUpdateTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] _ in self?.updateUsersLiveLocation(highlightFirstLocation: false) } @@ -113,7 +110,7 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType switch error { case .invalidLocationAuthorization: - if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { + if let applicationSettingsURL = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(applicationSettingsURL) } else { self?.completion?(.done) @@ -127,21 +124,19 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType } private func userLocationAnnotations(from usersLiveLocation: [UserLiveLocation]) -> [UserLocationAnnotation] { - - return usersLiveLocation.map { userLiveLocation in - return UserLocationAnnotation(avatarData: userLiveLocation.avatarData, coordinate: userLiveLocation.coordinate) + usersLiveLocation.map { userLiveLocation in + UserLocationAnnotation(avatarData: userLiveLocation.avatarData, coordinate: userLiveLocation.coordinate) } } private func currentUserLocationAnnotation(from annotations: [UserLocationAnnotation]) -> UserLocationAnnotation? { annotations.first { annotation in - return liveLocationSharingViewerService.isCurrentUserId(annotation.userId) + liveLocationSharingViewerService.isCurrentUserId(annotation.userId) } } private func getHighlightedAnnotation(from annotations: [UserLocationAnnotation]) -> UserLocationAnnotation? { - - if let userAnnotation = self.currentUserLocationAnnotation(from: annotations) { + if let userAnnotation = currentUserLocationAnnotation(from: annotations) { return userAnnotation } else { return annotations.first @@ -149,25 +144,22 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType } private func listItemsViewData(from usersLiveLocation: [UserLiveLocation]) -> [LiveLocationListItemViewData] { - var listItemsViewData: [LiveLocationListItemViewData] = [] let sortedUsersLiveLocation = usersLiveLocation.sorted { userLiveLocation1, userLiveLocation2 in - return userLiveLocation1.displayName > userLiveLocation2.displayName + userLiveLocation1.displayName > userLiveLocation2.displayName } - listItemsViewData = sortedUsersLiveLocation.map({ userLiveLocation in - return self.listItemViewData(from: userLiveLocation) - }) - + listItemsViewData = sortedUsersLiveLocation.map { userLiveLocation in + self.listItemViewData(from: userLiveLocation) + } let currentUserIndex = listItemsViewData.firstIndex { viewData in - return viewData.isCurrentUser + viewData.isCurrentUser } // Move current user as first item if let currentUserIndex = currentUserIndex { - let currentUserViewData = listItemsViewData[currentUserIndex] listItemsViewData.remove(at: currentUserIndex) listItemsViewData.insert(currentUserViewData, at: 0) @@ -177,41 +169,39 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType } private func listItemViewData(from userLiveLocation: UserLiveLocation) -> LiveLocationListItemViewData { + let isCurrentUser = liveLocationSharingViewerService.isCurrentUserId(userLiveLocation.userId) - let isCurrentUser = self.liveLocationSharingViewerService.isCurrentUserId(userLiveLocation.userId) - - let expirationDate = userLiveLocation.timestamp + userLiveLocation.timeout + let expirationDate = userLiveLocation.timestamp + userLiveLocation.timeout return LiveLocationListItemViewData(userId: userLiveLocation.userId, isCurrentUser: isCurrentUser, avatarData: userLiveLocation.avatarData, displayName: userLiveLocation.displayName, expirationDate: expirationDate, lastUpdate: userLiveLocation.lastUpdate) } private func update(with usersLiveLocation: [UserLiveLocation], highlightFirstLocation: Bool) { - - let annotations: [UserLocationAnnotation] = self.userLocationAnnotations(from: usersLiveLocation) + let annotations: [UserLocationAnnotation] = userLocationAnnotations(from: usersLiveLocation) var highlightedAnnotation: LocationAnnotation? if highlightFirstLocation { - highlightedAnnotation = self.getHighlightedAnnotation(from: annotations) + highlightedAnnotation = getHighlightedAnnotation(from: annotations) } - if let highlightableAnnotation = self.getHighlightedAnnotation(from: annotations) { - self.lastHighlightableAnnotation = highlightableAnnotation + if let highlightableAnnotation = getHighlightedAnnotation(from: annotations) { + lastHighlightableAnnotation = highlightableAnnotation } - if let lastHighlightableAnnotation = self.lastHighlightableAnnotation, usersLiveLocation.isEmpty { + if let lastHighlightableAnnotation = lastHighlightableAnnotation, usersLiveLocation.isEmpty { highlightedAnnotation = InvisibleLocationAnnotation(coordinate: lastHighlightableAnnotation.coordinate) } - let listViewItems = self.listItemsViewData(from: usersLiveLocation) + let listViewItems = listItemsViewData(from: usersLiveLocation) - self.state.annotations = annotations - self.state.highlightedAnnotation = highlightedAnnotation - self.state.listItemsViewData = listViewItems + state.annotations = annotations + state.highlightedAnnotation = highlightedAnnotation + state.listItemsViewData = listViewItems } private func highlighAnnotation(with userId: String) { - let foundUserAnnotation = self.state.annotations.first { annotation in + let foundUserAnnotation = state.annotations.first { annotation in annotation.userId == userId } @@ -219,20 +209,19 @@ class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType return } - self.state.highlightedAnnotation = foundUserAnnotation + state.highlightedAnnotation = foundUserAnnotation } private func stopUserLocationSharing() { + state.showLoadingIndicator = true - self.state.showLoadingIndicator = true - - self.liveLocationSharingViewerService.stopUserLiveLocationSharing { result in + liveLocationSharingViewerService.stopUserLiveLocationSharing { result in self.state.showLoadingIndicator = false switch result { case .success: break - case.failure: + case .failure: self.state.bindings.alertInfo = AlertInfo(id: .stopLocationSharingError, title: VectorL10n.error, message: VectorL10n.locationSharingLiveStopSharingError, diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModelProtocol.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModelProtocol.swift index a8626b528..890b96cf7 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/LiveLocationSharingViewerViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol LiveLocationSharingViewerViewModelProtocol { - var completion: ((LiveLocationSharingViewerViewModelResult) -> Void)? { get set } var context: LiveLocationSharingViewerViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/MockLiveLocationSharingViewerScreenState.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/MockLiveLocationSharingViewerScreenState.swift index 61353c169..a3451f669 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/MockLiveLocationSharingViewerScreenState.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/MockLiveLocationSharingViewerScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,12 +33,11 @@ enum MockLiveLocationSharingViewerScreenState: MockScreenState, CaseIterable { /// A list of screen state definitions static var allCases: [MockLiveLocationSharingViewerScreenState] { - return [.currentUser, .multipleUsers] + [.currentUser, .multipleUsers] } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { - + var screenView: ([Any], AnyView) { let service: LiveLocationSharingViewerServiceProtocol switch self { diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift index 3b0cbeeaa..cb9e107a5 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +14,11 @@ // limitations under the License. // -import Foundation import Combine import CoreLocation +import Foundation protocol LiveLocationSharingViewerServiceProtocol { - /// All shared users live location var usersLiveLocation: [UserLiveLocation] { get } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift index f7b4155b6..9b646a1bf 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +14,11 @@ // limitations under the License. // -import Foundation import CoreLocation +import Foundation import MatrixSDK class LiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol { - // MARK: - Properties private(set) var usersLiveLocation: [UserLiveLocation] = [] @@ -40,17 +39,17 @@ class LiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol self.session = session self.roomId = roomId - self.updateUsersLiveLocation(notifyUpdate: false) + updateUsersLiveLocation(notifyUpdate: false) } // MARK: - Public func isCurrentUserId(_ userId: String) -> Bool { - return self.session.myUserId == userId + session.myUserId == userId } func startListeningLiveLocationUpdates() { - self.beaconInfoSummaryListener = self.session.aggregations.beaconAggregations.listenToBeaconInfoSummaryUpdateInRoom(withId: self.roomId) { [weak self] _ in + beaconInfoSummaryListener = session.aggregations.beaconAggregations.listenToBeaconInfoSummaryUpdateInRoom(withId: roomId) { [weak self] _ in self?.updateUsersLiveLocation(notifyUpdate: true) } @@ -58,17 +57,17 @@ class LiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol func stopListeningLiveLocationUpdates() { if let listener = beaconInfoSummaryListener { - self.session.aggregations.removeListener(listener) - self.beaconInfoSummaryListener = nil + session.aggregations.removeListener(listener) + beaconInfoSummaryListener = nil } } func stopUserLiveLocationSharing(completion: @escaping (Result) -> Void) { - self.session.locationService.stopUserLocationSharing(inRoomWithId: roomId) { response in + session.locationService.stopUserLocationSharing(inRoomWithId: roomId) { response in switch response { case .success: - completion(.success(Void())) + completion(.success(())) case .failure(let error): completion(.failure(error)) } @@ -78,17 +77,16 @@ class LiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol // MARK: - Private private func updateUsersLiveLocation(notifyUpdate: Bool) { - let beaconInfoSummaries = self.session.locationService.getDisplayableBeaconInfoSummaries(inRoomWithId: roomId) - self.usersLiveLocation = Self.usersLiveLocation(fromBeaconInfoSummaries: beaconInfoSummaries, session: session) + let beaconInfoSummaries = session.locationService.getDisplayableBeaconInfoSummaries(inRoomWithId: roomId) + usersLiveLocation = Self.usersLiveLocation(fromBeaconInfoSummaries: beaconInfoSummaries, session: session) if notifyUpdate { - self.didUpdateUsersLiveLocation?(self.usersLiveLocation) + didUpdateUsersLiveLocation?(usersLiveLocation) } } - class private func usersLiveLocation(fromBeaconInfoSummaries beaconInfoSummaries: [MXBeaconInfoSummaryProtocol], session: MXSession) -> [UserLiveLocation] { - - return beaconInfoSummaries.compactMap { beaconInfoSummary in + private class func usersLiveLocation(fromBeaconInfoSummaries beaconInfoSummaries: [MXBeaconInfoSummaryProtocol], session: MXSession) -> [UserLiveLocation] { + beaconInfoSummaries.compactMap { beaconInfoSummary in let beaconInfo = beaconInfoSummary.beaconInfo @@ -98,9 +96,9 @@ class LiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol let avatarData = session.avatarInput(for: beaconInfoSummary.userId) - let timestamp = TimeInterval(beaconInfo.timestamp/1000) - let timeout = TimeInterval(beaconInfo.timeout/1000) - let lastUpdate = TimeInterval(lastBeacon.timestamp/1000) + let timestamp = TimeInterval(beaconInfo.timestamp / 1000) + let timeout = TimeInterval(beaconInfo.timeout / 1000) + let lastUpdate = TimeInterval(lastBeacon.timestamp / 1000) let coordinate = CLLocationCoordinate2D(latitude: lastBeacon.location.latitude, longitude: lastBeacon.location.longitude) diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift index ac192ec37..cfb34ef04 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +14,11 @@ // limitations under the License. // -import Foundation import Combine import CoreLocation +import Foundation class MockLiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol { - // MARK: Properties private(set) var usersLiveLocation: [UserLiveLocation] = [] @@ -29,17 +28,15 @@ class MockLiveLocationSharingViewerService: LiveLocationSharingViewerServiceProt // MARK: Setup init(generateRandomUsers: Bool = false) { + let firstUserLiveLocation = createFirstUserLiveLocation() - let firstUserLiveLocation = self.createFirstUserLiveLocation() - - let secondUserLiveLocation = self.createSecondUserLiveLocation() + let secondUserLiveLocation = createSecondUserLiveLocation() var usersLiveLocation: [UserLiveLocation] = [firstUserLiveLocation, secondUserLiveLocation] - if generateRandomUsers { for _ in 1...20 { - let randomUser = self.createRandomUserLiveLocation() + let randomUser = createRandomUserLiveLocation() usersLiveLocation.append(randomUser) } } @@ -50,20 +47,14 @@ class MockLiveLocationSharingViewerService: LiveLocationSharingViewerServiceProt // MARK: Public func isCurrentUserId(_ userId: String) -> Bool { - return "@alice:matrix.org" == userId + userId == "@alice:matrix.org" } - func startListeningLiveLocationUpdates() { - - } + func startListeningLiveLocationUpdates() { } - func stopListeningLiveLocationUpdates() { - - } + func stopListeningLiveLocationUpdates() { } - func stopUserLiveLocationSharing(completion: @escaping (Result) -> Void) { - - } + func stopUserLiveLocationSharing(completion: @escaping (Result) -> Void) { } // MARK: Private @@ -80,7 +71,6 @@ class MockLiveLocationSharingViewerService: LiveLocationSharingViewerServiceProt } private func createSecondUserLiveLocation() -> UserLiveLocation { - let userAvatarData = AvatarInput(mxContentUri: nil, matrixItemId: "@bob:matrix.org", displayName: "Bob") let coordinate = CLLocationCoordinate2D(latitude: 51.4952641, longitude: -0.259096) @@ -93,9 +83,7 @@ class MockLiveLocationSharingViewerService: LiveLocationSharingViewerServiceProt return UserLiveLocation(avatarData: userAvatarData, timestamp: timestamp, timeout: timeout, lastUpdate: lastUpdate, coordinate: coordinate) } - private func createRandomUserLiveLocation() -> UserLiveLocation { - let uuidString = UUID().uuidString.suffix(8) let random = Double.random(in: 0.005...0.010) diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/UserLiveLocation.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/UserLiveLocation.swift index 015d09088..dc8c9898d 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/UserLiveLocation.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Service/UserLiveLocation.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,18 +14,17 @@ // limitations under the License. // -import Foundation import CoreLocation +import Foundation /// Represents user live location struct UserLiveLocation { - var userId: String { - return avatarData.matrixItemId + avatarData.matrixItemId } var displayName: String { - return avatarData.displayName ?? self.userId + avatarData.displayName ?? userId } let avatarData: AvatarInputProtocol diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/UI/LiveLocationSharingViewerUITests.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/UI/LiveLocationSharingViewerUITests.swift index 6d4333d4c..de996c2b0 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/UI/LiveLocationSharingViewerUITests.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/UI/LiveLocationSharingViewerUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class LiveLocationSharingViewerUITests: MockScreenTestCase { // Tests to be implemented. diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift index 39718e54d..cefe1c2ed 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,13 +14,12 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI class LiveLocationSharingViewerViewModelTests: XCTestCase { - var service: MockLiveLocationSharingViewerService! var viewModel: LiveLocationSharingViewerViewModelProtocol! var context: LiveLocationSharingViewerViewModelType.Context! diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationListItem.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationListItem.swift index b6af40483..e32de2818 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationListItem.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationListItem.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct LiveLocationListItem: View { - // MARK: - Properties // MARK: Private @@ -29,10 +28,9 @@ struct LiveLocationListItem: View { let viewData: LiveLocationListItemViewData var timeoutText: String { - let timeLeftString: String - if let elapsedTimeString = self.elapsedTimeString(from: viewData.expirationDate, isPastDate: false) { + if let elapsedTimeString = elapsedTimeString(from: viewData.expirationDate, isPastDate: false) { timeLeftString = VectorL10n.locationSharingLiveListItemTimeLeft(elapsedTimeString) } else { timeLeftString = VectorL10n.locationSharingLiveListItemSharingExpired @@ -42,10 +40,9 @@ struct LiveLocationListItem: View { } var lastUpdateText: String { - let timeLeftString: String - if let elapsedTimeString = self.elapsedTimeString(from: viewData.lastUpdate, isPastDate: true) { + if let elapsedTimeString = elapsedTimeString(from: viewData.lastUpdate, isPastDate: true) { timeLeftString = VectorL10n.locationSharingLiveListItemLastUpdate(elapsedTimeString) } else { timeLeftString = VectorL10n.locationSharingLiveListItemLastUpdateInvalid @@ -55,12 +52,12 @@ struct LiveLocationListItem: View { } var displayName: String { - return viewData.isCurrentUser ? VectorL10n.locationSharingLiveListItemCurrentUserDisplayName : viewData.displayName + viewData.isCurrentUser ? VectorL10n.locationSharingLiveListItemCurrentUserDisplayName : viewData.displayName } - var onStopSharingAction: (() -> (Void))? = nil + var onStopSharingAction: (() -> Void)? - var onBackgroundTap: ((String) -> (Void))? = nil + var onBackgroundTap: ((String) -> Void)? // MARK: - Body @@ -69,9 +66,9 @@ struct LiveLocationListItem: View { HStack(spacing: 18) { AvatarImage(avatarData: viewData.avatarData, size: .medium) .border() - VStack(alignment: .leading, spacing: 2) { Text(displayName) - .font(theme.fonts.bodySB) - .foregroundColor(theme.colors.primaryContent) + VStack(alignment: .leading, spacing: 2) { Text(displayName) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) Text(timeoutText) .font(theme.fonts.caption1) .foregroundColor(theme.colors.primaryContent) @@ -97,7 +94,6 @@ struct LiveLocationListItem: View { // MARK: - Private private func elapsedTimeString(from timestamp: TimeInterval, isPastDate: Bool) -> String? { - let formatter = DateComponentsFormatter() formatter.unitsStyle = .abbreviated @@ -121,19 +117,16 @@ struct LiveLocationListItem: View { } struct LiveLocationListPreview: View { - let liveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol = MockLiveLocationSharingViewerService() var viewDataList: [LiveLocationListItemViewData] { - return self.listItemsViewData(from: liveLocationSharingViewerService.usersLiveLocation) + listItemsViewData(from: liveLocationSharingViewerService.usersLiveLocation) } var body: some View { VStack(alignment: .leading, spacing: 14) { ForEach(viewDataList) { viewData in - LiveLocationListItem(viewData: viewData, onStopSharingAction: { - - }, onBackgroundTap: { userId in + LiveLocationListItem(viewData: viewData, onStopSharingAction: { }, onBackgroundTap: { _ in }) } @@ -143,24 +136,22 @@ struct LiveLocationListPreview: View { } private func listItemsViewData(from usersLiveLocation: [UserLiveLocation]) -> [LiveLocationListItemViewData] { - var listItemsViewData: [LiveLocationListItemViewData] = [] let sortedUsersLiveLocation = usersLiveLocation.sorted { userLiveLocation1, userLiveLocation2 in - return userLiveLocation1.displayName > userLiveLocation2.displayName + userLiveLocation1.displayName > userLiveLocation2.displayName } - listItemsViewData = sortedUsersLiveLocation.map({ userLiveLocation in - return self.listItemViewData(from: userLiveLocation) - }) + listItemsViewData = sortedUsersLiveLocation.map { userLiveLocation in + self.listItemViewData(from: userLiveLocation) + } let currentUserIndex = listItemsViewData.firstIndex { viewData in - return viewData.isCurrentUser + viewData.isCurrentUser } // Move current user as first item if let currentUserIndex = currentUserIndex { - let currentUserViewData = listItemsViewData[currentUserIndex] listItemsViewData.remove(at: currentUserIndex) listItemsViewData.insert(currentUserViewData, at: 0) @@ -170,10 +161,9 @@ struct LiveLocationListPreview: View { } private func listItemViewData(from userLiveLocation: UserLiveLocation) -> LiveLocationListItemViewData { + let isCurrentUser = liveLocationSharingViewerService.isCurrentUserId(userLiveLocation.userId) - let isCurrentUser = self.liveLocationSharingViewerService.isCurrentUserId(userLiveLocation.userId) - - let expirationDate = userLiveLocation.timestamp + userLiveLocation.timeout + let expirationDate = userLiveLocation.timestamp + userLiveLocation.timeout return LiveLocationListItemViewData(userId: userLiveLocation.userId, isCurrentUser: isCurrentUser, avatarData: userLiveLocation.avatarData, displayName: userLiveLocation.displayName, expirationDate: expirationDate, lastUpdate: userLiveLocation.lastUpdate) } diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationListItemViewData.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationListItemViewData.swift index 3e2392225..e87b0bd3d 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationListItemViewData.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationListItemViewData.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,9 +18,8 @@ import Foundation /// View data for LiveLocationListItem struct LiveLocationListItemViewData: Identifiable { - var id: String { - return userId + userId } let userId: String diff --git a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift index 1fd256b5a..9678028b8 100644 --- a/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift +++ b/RiotSwiftUI/Modules/LocationSharing/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +14,10 @@ // limitations under the License. // -import SwiftUI import DSBottomSheet +import SwiftUI struct LiveLocationSharingViewer: View { - // MARK: - Properties // MARK: Private @@ -37,7 +36,6 @@ struct LiveLocationSharingViewer: View { var body: some View { ZStack(alignment: .bottom) { - if !viewModel.viewState.showMapLoadingError { LocationSharingMapView(tileServerMapURL: viewModel.viewState.mapStyleURL, annotations: viewModel.viewState.annotations, @@ -48,10 +46,10 @@ struct LiveLocationSharingViewer: View { userLocation: Binding.constant(nil), mapCenterCoordinate: Binding.constant(nil), onCalloutTap: { annotation in - if let userLocationAnnotation = annotation as? UserLocationAnnotation { - viewModel.send(viewAction: .share(userLocationAnnotation)) - } - }, + if let userLocationAnnotation = annotation as? UserLocationAnnotation { + viewModel.send(viewAction: .share(userLocationAnnotation)) + } + }, errorSubject: viewModel.viewState.errorSubject) // Show map credits above collapsed bottom sheet height if bottom sheet is visible @@ -61,7 +59,7 @@ struct LiveLocationSharingViewer: View { MapCreditsView(action: { viewModel.send(viewAction: .mapCreditsDidTap) }) - .offset(y: -(bottomSheetCollapsedHeight)) // Put the copyright action above the collapsed bottom sheet + .offset(y: -bottomSheetCollapsedHeight) // Put the copyright action above the collapsed bottom sheet .padding(.bottom, 10) } .ignoresSafeArea() @@ -112,7 +110,7 @@ struct LiveLocationSharingViewer: View { .background(theme.colors.system.ignoresSafeArea()) .bottomSheet(sheet, if: viewModel.viewState.isBottomSheetVisible) .actionSheet(isPresented: $viewModel.showMapCreditsSheet) { - return MapCreditsActionSheet(openURL: { url in + MapCreditsActionSheet(openURL: { url in openURL(url) }).sheet } @@ -142,8 +140,8 @@ struct LiveLocationSharingViewer: View { } // MARK: - Bottom sheet -extension LiveLocationSharingViewer { +extension LiveLocationSharingViewer { var sheetStyle: BottomSheetStyle { var bottomSheetStyle = BottomSheetStyle.standard @@ -162,9 +160,10 @@ extension LiveLocationSharingViewer { isExpanded: $isBottomSheetExpanded, minHeight: .points(bottomSheetCollapsedHeight), maxHeight: .available, - style: sheetStyle) { - userLocationList - } + style: sheetStyle + ) { + userLocationList + } } } diff --git a/RiotSwiftUI/Modules/LocationSharing/MapCredits/MapCreditsActionSheet.swift b/RiotSwiftUI/Modules/LocationSharing/MapCredits/MapCreditsActionSheet.swift index 3f08da5ac..927aceef0 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapCredits/MapCreditsActionSheet.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapCredits/MapCreditsActionSheet.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct MapCreditsActionSheet { - // Open URL action let openURL: (URL) -> Void diff --git a/RiotSwiftUI/Modules/LocationSharing/MapCredits/View/MapCreditsView.swift b/RiotSwiftUI/Modules/LocationSharing/MapCredits/View/MapCreditsView.swift index 3edb2fbfc..3d30f358b 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapCredits/View/MapCreditsView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapCredits/View/MapCreditsView.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct MapCreditsView: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/LocationSharing/MapError/MapViewErrorAlertInfoBuilder.swift b/RiotSwiftUI/Modules/LocationSharing/MapError/MapViewErrorAlertInfoBuilder.swift index 9f1120d2a..fa02eef50 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapError/MapViewErrorAlertInfoBuilder.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapError/MapViewErrorAlertInfoBuilder.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,30 +17,27 @@ import Foundation struct MapViewErrorAlertInfoBuilder { - func build(with error: LocationSharingViewError, primaryButtonCompletion: (() -> Void)?) -> AlertInfo? { - let alertInfo: AlertInfo? switch error { case .failedLoadingMap: alertInfo = AlertInfo(id: .mapLoadingError, - title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.ok, primaryButtonCompletion)) + title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, primaryButtonCompletion)) case .failedLocatingUser: alertInfo = AlertInfo(id: .userLocatingError, - title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.ok, primaryButtonCompletion)) + title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, primaryButtonCompletion)) case .invalidLocationAuthorization: alertInfo = AlertInfo(id: .authorizationError, - title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, primaryButtonCompletion), - secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, primaryButtonCompletion)) + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, primaryButtonCompletion), + secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, primaryButtonCompletion)) default: alertInfo = nil } return alertInfo } - } diff --git a/RiotSwiftUI/Modules/LocationSharing/MapError/View/MapLoadingErrorView.swift b/RiotSwiftUI/Modules/LocationSharing/MapError/View/MapLoadingErrorView.swift index 018d4a0ed..dc65ab4b5 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapError/View/MapLoadingErrorView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapError/View/MapLoadingErrorView.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct MapLoadingErrorView: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/LocationSharing/MapView/LocationAnnotation.swift b/RiotSwiftUI/Modules/LocationSharing/MapView/LocationAnnotation.swift index 1ddec21c5..1bd69ff27 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapView/LocationAnnotation.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapView/LocationAnnotation.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import Mapbox /// Base class to handle a map annotation class LocationAnnotation: NSObject, MGLAnnotation { - // MARK: - Properties // Title property is needed to enable annotation selection and callout view showing @@ -36,15 +35,14 @@ class LocationAnnotation: NSObject, MGLAnnotation { } /// POI map annotation -class PinLocationAnnotation: LocationAnnotation {} +class PinLocationAnnotation: LocationAnnotation { } /// User map annotation class UserLocationAnnotation: LocationAnnotation { - // MARK: - Properties var userId: String { - return avatarData.matrixItemId + avatarData.matrixItemId } let avatarData: AvatarInputProtocol @@ -53,13 +51,12 @@ class UserLocationAnnotation: LocationAnnotation { init(avatarData: AvatarInputProtocol, coordinate: CLLocationCoordinate2D) { - self.avatarData = avatarData super.init(coordinate: coordinate) - super.title = self.avatarData.displayName ?? self.userId + super.title = self.avatarData.displayName ?? userId } } /// Invisible annotation -class InvisibleLocationAnnotation: LocationAnnotation {} +class InvisibleLocationAnnotation: LocationAnnotation { } diff --git a/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift b/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift index d244437ae..9b5073b33 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMapView.swift @@ -14,15 +14,14 @@ // limitations under the License. // -import SwiftUI import Combine import Mapbox +import SwiftUI struct LocationSharingMapView: UIViewRepresentable { - // MARK: - Constants - private struct Constants { + private enum Constants { static let mapZoomLevel = 15.0 } @@ -41,10 +40,10 @@ struct LocationSharingMapView: UIViewRepresentable { let userAvatarData: AvatarInputProtocol? /// True to indicate to show and follow current user location - var showsUserLocation: Bool = false + var showsUserLocation = false /// True to indicate that a touch on user annotation can show a callout - var userAnnotationCanShowCallout: Bool = false + var userAnnotationCanShowCallout = false /// Last user location if `showsUserLocation` has been enabled @Binding var userLocation: CLLocationCoordinate2D? @@ -64,8 +63,7 @@ struct LocationSharingMapView: UIViewRepresentable { // MARK: - UIViewRepresentable func makeUIView(context: Context) -> MGLMapView { - - let mapView = self.makeMapView() + let mapView = makeMapView() mapView.delegate = context.coordinator let panGesture = UIPanGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.didPan)) panGesture.delegate = context.coordinator @@ -74,15 +72,14 @@ struct LocationSharingMapView: UIViewRepresentable { } func updateUIView(_ mapView: MGLMapView, context: Context) { - mapView.vc_removeAllAnnotations() - mapView.addAnnotations(self.annotations) + mapView.addAnnotations(annotations) - if let highlightedAnnotation = self.highlightedAnnotation { + if let highlightedAnnotation = highlightedAnnotation { mapView.setCenter(highlightedAnnotation.coordinate, zoomLevel: Constants.mapZoomLevel, animated: false) } - if self.showsUserLocation { + if showsUserLocation { mapView.showsUserLocation = true mapView.userTrackingMode = .follow } else { @@ -108,24 +105,22 @@ struct LocationSharingMapView: UIViewRepresentable { } // MARK: - Coordinator + extension LocationSharingMapView { - class Coordinator: NSObject, MGLMapViewDelegate, UIGestureRecognizerDelegate { - // MARK: - Properties var locationSharingMapView: LocationSharingMapView // MARK: - Setup - init(_ locationSharingMapView: LocationSharingMapView) { - self.locationSharingMapView = locationSharingMapView - } + init(_ locationSharingMapView: LocationSharingMapView) { + self.locationSharingMapView = locationSharingMapView + } // MARK: - MGLMapViewDelegate func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? { - if let userLocationAnnotation = annotation as? UserLocationAnnotation { return LocationAnnotationView(userLocationAnnotation: userLocationAnnotation) } else if let pinLocationAnnotation = annotation as? PinLocationAnnotation { @@ -168,7 +163,7 @@ extension LocationSharingMapView { // MARK: Callout func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool { - return annotation is UserLocationAnnotation && locationSharingMapView.userAnnotationCanShowCallout + annotation is UserLocationAnnotation && locationSharingMapView.userAnnotationCanShowCallout } func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? { @@ -188,7 +183,7 @@ extension LocationSharingMapView { // MARK: UIGestureRecognizer func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return gestureRecognizer is UIPanGestureRecognizer + gestureRecognizer is UIPanGestureRecognizer } @objc @@ -199,12 +194,12 @@ extension LocationSharingMapView { } // MARK: - MGLMapView convenient methods + extension MGLMapView { - func vc_removeAllAnnotations() { - guard let annotations = self.annotations else { + guard let annotations = annotations else { return } - self.removeAnnotations(annotations) + removeAnnotations(annotations) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMarkerView.swift b/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMarkerView.swift index 0450b16a3..7cf7f7e4a 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMarkerView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapView/View/LocationSharingMarkerView.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct LocationSharingMarkerView: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserAnnotationCalloutContentView.swift b/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserAnnotationCalloutContentView.swift index 419213972..556b76030 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserAnnotationCalloutContentView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserAnnotationCalloutContentView.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import Foundation import Reusable class UserAnnotationCalloutContentView: UIView, Themable, NibLoadable { - // MARK: - Constants private static let sizingView = UserAnnotationCalloutContentView.instantiate() @@ -39,16 +38,16 @@ class UserAnnotationCalloutContentView: UIView, Themable, NibLoadable { // MARK: - Setup static func instantiate() -> UserAnnotationCalloutContentView { - return UserAnnotationCalloutContentView.loadFromNib() + UserAnnotationCalloutContentView.loadFromNib() } // MARK: - Public func update(theme: Theme) { - self.backgroundView.backgroundColor = theme.colors.background - self.titleLabel.textColor = theme.colors.secondaryContent - self.titleLabel.font = theme.fonts.callout - self.shareButton.tintColor = theme.colors.secondaryContent + backgroundView.backgroundColor = theme.colors.background + titleLabel.textColor = theme.colors.secondaryContent + titleLabel.font = theme.fonts.callout + shareButton.tintColor = theme.colors.secondaryContent } // MARK: - Life cycle @@ -56,18 +55,18 @@ class UserAnnotationCalloutContentView: UIView, Themable, NibLoadable { override func awakeFromNib() { super.awakeFromNib() - self.titleLabel.text = VectorL10n.locationSharingLiveMapCalloutTitle - self.backgroundView.layer.masksToBounds = true + titleLabel.text = VectorL10n.locationSharingLiveMapCalloutTitle + backgroundView.layer.masksToBounds = true } override func layoutSubviews() { super.layoutSubviews() - self.backgroundView.layer.cornerRadius = Constants.cornerRadius + backgroundView.layer.cornerRadius = Constants.cornerRadius } static func contentViewSize() -> CGSize { - let sizingView = self.sizingView + let sizingView = sizingView sizingView.frame = CGRect(x: 0, y: 0, width: 1, height: Constants.height) @@ -77,8 +76,8 @@ class UserAnnotationCalloutContentView: UIView, Themable, NibLoadable { let fittingSize = CGSize(width: UIView.layoutFittingCompressedSize.width, height: Constants.height) let size = sizingView.systemLayoutSizeFitting(fittingSize, - withHorizontalFittingPriority: .fittingSizeLevel, - verticalFittingPriority: .required) + withHorizontalFittingPriority: .fittingSizeLevel, + verticalFittingPriority: .required) return size } diff --git a/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserAnnotationCalloutView.swift b/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserAnnotationCalloutView.swift index 64edcfd30..f405c179a 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserAnnotationCalloutView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserAnnotationCalloutView.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Mapbox class UserAnnotationCalloutView: UIView, MGLCalloutView, Themable { - // MARK: - Constants private enum Constants { @@ -31,16 +30,16 @@ class UserAnnotationCalloutView: UIView, MGLCalloutView, Themable { var representedObject: MGLAnnotation - lazy var leftAccessoryView: UIView = UIView() + lazy var leftAccessoryView = UIView() - lazy var rightAccessoryView: UIView = UIView() + lazy var rightAccessoryView = UIView() var delegate: MGLCalloutViewDelegate? // Allow the callout to remain open during panning. - let dismissesAutomatically: Bool = false + let dismissesAutomatically = false - let isAnchoredToAnnotation: Bool = true + let isAnchoredToAnnotation = true // https://github.com/mapbox/mapbox-gl-native/issues/9228 override var center: CGPoint { @@ -50,33 +49,31 @@ class UserAnnotationCalloutView: UIView, MGLCalloutView, Themable { super.center = newCenter } get { - return super.center + super.center } } // MARK: Private - lazy var contentView: UserAnnotationCalloutContentView = { - return UserAnnotationCalloutContentView.instantiate() - }() + lazy var contentView = UserAnnotationCalloutContentView.instantiate() // MARK: - Setup required init(userLocationAnnotation: UserLocationAnnotation) { - - self.representedObject = userLocationAnnotation + representedObject = userLocationAnnotation super.init(frame: .zero) - self.vc_addSubViewMatchingParent(self.contentView) + vc_addSubViewMatchingParent(contentView) - self.update(theme: ThemeService.shared().theme) + update(theme: ThemeService.shared().theme) let size = UserAnnotationCalloutContentView.contentViewSize() - self.frame = CGRect(origin: .zero, size: size) + frame = CGRect(origin: .zero, size: size) } + @available(*, unavailable) required init?(coder decoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -84,16 +81,15 @@ class UserAnnotationCalloutView: UIView, MGLCalloutView, Themable { // MARK: - Public func update(theme: Theme) { - self.contentView.update(theme: theme) + contentView.update(theme: theme) } // MARK: - Overrides func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) { - // Set callout above the marker view - self.center = view.center.applying(CGAffineTransform(translationX: 0, y: view.bounds.height/2 + self.bounds.height)) + center = view.center.applying(CGAffineTransform(translationX: 0, y: view.bounds.height / 2 + bounds.height)) delegate?.calloutViewWillAppear?(self) @@ -101,10 +97,10 @@ class UserAnnotationCalloutView: UIView, MGLCalloutView, Themable { if isCalloutTappable() { // Handle taps and eventually try to send them to the delegate (usually the map view). - self.contentView.shareButton.addTarget(self, action: #selector(UserAnnotationCalloutView.calloutTapped), for: .touchUpInside) + contentView.shareButton.addTarget(self, action: #selector(UserAnnotationCalloutView.calloutTapped), for: .touchUpInside) } else { // Disable tapping and highlighting. - self.contentView.shareButton.isUserInteractionEnabled = false + contentView.shareButton.isUserInteractionEnabled = false } if animated { @@ -124,7 +120,7 @@ class UserAnnotationCalloutView: UIView, MGLCalloutView, Themable { } func dismissCallout(animated: Bool) { - if (superview != nil) { + if superview != nil { if animated { UIView.animate(withDuration: Constants.animationDuration, animations: { [weak self] in self?.alpha = 0 @@ -149,7 +145,7 @@ class UserAnnotationCalloutView: UIView, MGLCalloutView, Themable { } @objc func calloutTapped() { - if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped)) { + if isCalloutTappable(), delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped)) { delegate!.calloutViewTapped!(self) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserLocationAnnotationView.swift b/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserLocationAnnotationView.swift index 4662ae485..25a5b9848 100644 --- a/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserLocationAnnotationView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/MapView/View/UserLocationAnnotationView.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,11 +15,10 @@ // import Foundation -import SwiftUI import Mapbox +import SwiftUI class LocationAnnotationView: MGLUserLocationAnnotationView { - // MARK: - Constants private enum Constants { @@ -34,31 +33,30 @@ class LocationAnnotationView: MGLUserLocationAnnotationView { override init(annotation: MGLAnnotation?, reuseIdentifier: String?) { super.init(annotation: annotation, reuseIdentifier: - reuseIdentifier) - self.frame = Constants.defaultFrame + reuseIdentifier) + frame = Constants.defaultFrame } convenience init(avatarData: AvatarInputProtocol) { self.init(annotation: nil, reuseIdentifier: nil) - self.addUserMarkerView(with: avatarData) + addUserMarkerView(with: avatarData) } convenience init(userLocationAnnotation: UserLocationAnnotation) { - // TODO: Use a reuseIdentifier self.init(annotation: userLocationAnnotation, reuseIdentifier: nil) - self.addUserMarkerView(with: userLocationAnnotation.avatarData) + addUserMarkerView(with: userLocationAnnotation.avatarData) } convenience init(pinLocationAnnotation: PinLocationAnnotation) { - // TODO: Use a reuseIdentifier self.init(annotation: pinLocationAnnotation, reuseIdentifier: nil) - self.addPinMarkerView() + addPinMarkerView() } + @available(*, unavailable) required init?(coder: NSCoder) { fatalError() } @@ -66,7 +64,6 @@ class LocationAnnotationView: MGLUserLocationAnnotationView { // MARK: - Private private func addUserMarkerView(with avatarData: AvatarInputProtocol) { - guard let avatarMarkerView = UIHostingController(rootView: LocationSharingMarkerView(backgroundColor: theme.userColor(for: avatarData.matrixItemId)) { AvatarImage(avatarData: avatarData, size: .medium) .border() @@ -90,11 +87,10 @@ class LocationAnnotationView: MGLUserLocationAnnotationView { } private func addMarkerView(_ markerView: UIView) { - markerView.backgroundColor = .clear addSubview(markerView) - markerView.frame = self.bounds + markerView.frame = bounds } } diff --git a/RiotSwiftUI/Modules/LocationSharing/ShareLocationActivity/ShareLocationActivityController.swift b/RiotSwiftUI/Modules/LocationSharing/ShareLocationActivity/ShareLocationActivityController.swift index 4b3e6ca72..6c5f00154 100644 --- a/RiotSwiftUI/Modules/LocationSharing/ShareLocationActivity/ShareLocationActivityController.swift +++ b/RiotSwiftUI/Modules/LocationSharing/ShareLocationActivity/ShareLocationActivityController.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,16 +14,15 @@ // limitations under the License. // -import Foundation import CoreLocation +import Foundation /// Build a UIActivityViewController to share a location class ShareLocationActivityControllerBuilder { - func build(with location: CLLocationCoordinate2D) -> UIActivityViewController { - return UIActivityViewController(activityItems: [ShareToMapsAppActivity.urlForMapsAppType(.apple, location: location)], - applicationActivities: [ShareToMapsAppActivity(type: .apple, location: location), - ShareToMapsAppActivity(type: .google, location: location), - ShareToMapsAppActivity(type: .osm, location: location)]) + UIActivityViewController(activityItems: [ShareToMapsAppActivity.urlForMapsAppType(.apple, location: location)], + applicationActivities: [ShareToMapsAppActivity(type: .apple, location: location), + ShareToMapsAppActivity(type: .google, location: location), + ShareToMapsAppActivity(type: .osm, location: location)]) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/ShareLocationActivity/ShareToMapsAppActivity.swift b/RiotSwiftUI/Modules/LocationSharing/ShareLocationActivity/ShareToMapsAppActivity.swift index 53b96121d..1d325e733 100644 --- a/RiotSwiftUI/Modules/LocationSharing/ShareLocationActivity/ShareToMapsAppActivity.swift +++ b/RiotSwiftUI/Modules/LocationSharing/ShareLocationActivity/ShareToMapsAppActivity.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import UIKit import CoreLocation +import UIKit extension UIActivity.ActivityType { static let shareToMapsApp = UIActivity.ActivityType("Element.ShareToMapsApp") @@ -31,7 +31,7 @@ class ShareToMapsAppActivity: UIActivity { private let type: MapsAppType private let location: CLLocationCoordinate2D - private override init() { + override private init() { fatalError() } @@ -63,15 +63,15 @@ class ShareToMapsAppActivity: UIActivity { } var activityCategory: UIActivity.Category { - return .action + .action } override var activityType: UIActivity.ActivityType { - return .shareToMapsApp + .shareToMapsApp } override func canPerform(withActivityItems activityItems: [Any]) -> Bool { - return true + true } override func prepare(withActivityItems activityItems: [Any]) { diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift index 2bf40ea08..71ec3c776 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Coordinator/LocationSharingCoordinator.swift @@ -15,9 +15,9 @@ // import Foundation -import UIKit -import SwiftUI import MatrixSDK +import SwiftUI +import UIKit struct LocationSharingCoordinatorParameters { let session: MXSession @@ -57,7 +57,6 @@ extension LocationSharingCoordinateType { } final class LocationSharingCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -82,8 +81,10 @@ final class LocationSharingCoordinator: Coordinator, Presentable { let viewModel = LocationSharingViewModel( mapStyleURL: parameters.session.vc_homeserverConfiguration().tileServer.mapStyleURL, avatarData: parameters.avatarData, + isLiveLocationSharingEnabled: RiotSettings.shared.enableLiveLocationSharing, service: locationSharingService) + let view = LocationSharingView(context: viewModel.context) .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) @@ -93,6 +94,7 @@ final class LocationSharingCoordinator: Coordinator, Presentable { } // MARK: - Public + func start() { locationSharingViewModel.completion = { [weak self] result in guard let self = self else { return } @@ -111,22 +113,22 @@ final class LocationSharingCoordinator: Coordinator, Presentable { } static func shareLocationActivityController(_ location: CLLocationCoordinate2D) -> UIActivityViewController { - return UIActivityViewController(activityItems: [ShareToMapsAppActivity.urlForMapsAppType(.apple, location: location)], - applicationActivities: [ShareToMapsAppActivity(type: .apple, location: location), - ShareToMapsAppActivity(type: .google, location: location), - ShareToMapsAppActivity(type: .osm, location: location)]) + UIActivityViewController(activityItems: [ShareToMapsAppActivity.urlForMapsAppType(.apple, location: location)], + applicationActivities: [ShareToMapsAppActivity(type: .apple, location: location), + ShareToMapsAppActivity(type: .google, location: location), + ShareToMapsAppActivity(type: .osm, location: location)]) } // MARK: - Private private func presentShareLocationActivity(with location: CLLocationCoordinate2D) { - self.locationSharingHostingController.present(Self.shareLocationActivityController(location), animated: true) + locationSharingHostingController.present(Self.shareLocationActivityController(location), animated: true) } private func shareStaticLocation(latitude: Double, longitude: Double, coordinateType: LocationSharingCoordinateType) { - self.locationSharingViewModel.startLoading() + locationSharingViewModel.startLoading() - self.parameters.roomDataSource.sendLocation(withLatitude: latitude, longitude: longitude, description: nil, coordinateType: coordinateType.eventAssetType()) { [weak self] _ in + parameters.roomDataSource.sendLocation(withLatitude: latitude, longitude: longitude, description: nil, coordinateType: coordinateType.eventAssetType()) { [weak self] _ in guard let self = self else { return } self.locationSharingViewModel.stopLoading() @@ -140,8 +142,8 @@ final class LocationSharingCoordinator: Coordinator, Presentable { } private func startLiveLocationSharing(with timeout: TimeInterval) { - guard let locationService = self.parameters.roomDataSource.mxSession.locationService, let roomId = self.parameters.roomDataSource.roomId else { - self.locationSharingViewModel.stopLoading(error: .locationSharingError) + guard let locationService = parameters.roomDataSource.mxSession.locationService, let roomId = parameters.roomDataSource.roomId else { + locationSharingViewModel.stopLoading(error: .locationSharingError) return } @@ -166,16 +168,15 @@ final class LocationSharingCoordinator: Coordinator, Presentable { } private func checkLiveLocationCanBeStarted(completion: @escaping ((Result) -> Void)) { - - guard self.canShareLiveLocation() else { + guard canShareLiveLocation() else { completion(.failure(LiveLocationStartError.powerLevelNotHighEnough)) return } - self.showLabFlagPromotionIfNeeded { labFlagEnabled in + showLabFlagPromotionIfNeeded { labFlagEnabled in if labFlagEnabled { - completion(.success(Void())) + completion(.success(())) } else { completion(.failure(LiveLocationStartError.labFlagNotEnabled)) } @@ -184,18 +185,18 @@ final class LocationSharingCoordinator: Coordinator, Presentable { // Check if user can send beacon info state event private func canShareLiveLocation() -> Bool { - guard let myUserId = self.parameters.roomDataSource.mxSession.myUserId else { + guard let myUserId = parameters.roomDataSource.mxSession.myUserId else { return false } - let userPowerLevelRawValue = self.parameters.roomDataSource.roomState.powerLevels.powerLevelOfUser(withUserID: myUserId) + let userPowerLevelRawValue = parameters.roomDataSource.roomState.powerLevels.powerLevelOfUser(withUserID: myUserId) guard let userPowerLevel = RoomPowerLevel(rawValue: userPowerLevelRawValue) else { return false } return userPowerLevel.rawValue >= RoomPowerLevel.moderator.rawValue - } + } private func showLabFlagPromotionIfNeeded(completion: @escaping ((Bool) -> Void)) { guard RiotSettings.shared.enableLiveLocationSharing == false else { @@ -204,11 +205,10 @@ final class LocationSharingCoordinator: Coordinator, Presentable { return } - self.showLabFlagPromotion(completion: completion) + showLabFlagPromotion(completion: completion) } private func showLabFlagPromotion(completion: @escaping ((Bool) -> Void)) { - // TODO: Use a NavigationRouter instead of using NavigationView inside LocationSharingView // In order to use `NavigationRouter.present` @@ -224,14 +224,14 @@ final class LocationSharingCoordinator: Coordinator, Presentable { } } - self.locationSharingHostingController.present(coordinator.toPresentable(), animated: true) + locationSharingHostingController.present(coordinator.toPresentable(), animated: true) - self.add(childCoordinator: coordinator) + add(childCoordinator: coordinator) } // MARK: - Presentable func toPresentable() -> UIViewController { - return locationSharingHostingController + locationSharingHostingController } } diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingModels.swift index 1a5304b87..3178bcb87 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingModels.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingModels.swift @@ -14,10 +14,10 @@ // limitations under the License. // -import Foundation -import SwiftUI import Combine import CoreLocation +import Foundation +import SwiftUI // This is the equivalent of MXEventAssetType in the MatrixSDK enum LocationSharingCoordinateType { @@ -27,9 +27,9 @@ enum LocationSharingCoordinateType { enum LiveLocationSharingTimeout: TimeInterval { // Timer are in milliseconde because timestamp are in millisecond in Matrix SDK - case short = 900000 // 15 minutes - case medium = 3600000 // 1 hour - case long = 28800000 // 8 hours + case short = 900_000 // 15 minutes + case medium = 3_600_000 // 1 hour + case long = 28_800_000 // 8 hours } enum LocationSharingViewAction { @@ -47,7 +47,7 @@ enum LocationSharingViewModelResult { case cancel case share(latitude: Double, longitude: Double, coordinateType: LocationSharingCoordinateType) case shareLiveLocation(timeout: TimeInterval) - case checkLiveLocationCanBeStarted(_ completion: ((Result) -> Void)) + case checkLiveLocationCanBeStarted(_ completion: (Result) -> Void) } enum LiveLocationStartError: Error { @@ -63,7 +63,6 @@ enum LocationSharingViewError { } struct LocationSharingViewState: BindableState { - /// Map style URL let mapStyleURL: URL @@ -77,21 +76,21 @@ struct LocationSharingViewState: BindableState { var highlightedAnnotation: LocationAnnotation? /// Indicates whether the user has moved around the map to drop a pin somewhere other than their current location - var isPinDropSharing: Bool = false + var isPinDropSharing = false - var showLoadingIndicator: Bool = false + var showLoadingIndicator = false /// True to indicate to show and follow current user location - var showsUserLocation: Bool = false + var showsUserLocation = false /// Used to hide live location sharing features - var isLiveLocationSharingEnabled: Bool = false + var isLiveLocationSharingEnabled = false var shareButtonEnabled: Bool { !showLoadingIndicator } - var showMapLoadingError: Bool = false + var showMapLoadingError = false let errorSubject = PassthroughSubject() diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift index defbf11fc..3b86f4e89 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,9 +14,9 @@ // limitations under the License. // +import CoreLocation import Foundation import SwiftUI -import CoreLocation enum MockLocationSharingScreenState: MockScreenState, CaseIterable { case shareUserLocation @@ -25,8 +25,7 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable { LocationSharingView.self } - var screenView: ([Any], AnyView) { - + var screenView: ([Any], AnyView) { let locationSharingService = MockLocationSharingService() let mapStyleURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")! @@ -35,6 +34,6 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable { isLiveLocationSharingEnabled: true, service: locationSharingService) return ([viewModel], AnyView(LocationSharingView(context: viewModel.context) - .addDependency(MockAvatarService.example))) + .addDependency(MockAvatarService.example))) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModel.swift index 56cdb1f1c..39dbea5c8 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModel.swift @@ -14,15 +14,13 @@ // limitations under the License. // -import SwiftUI import Combine import CoreLocation +import SwiftUI + +typealias LocationSharingViewModelType = StateStoreViewModel -typealias LocationSharingViewModelType = StateStoreViewModel class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -36,8 +34,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie // MARK: - Setup init(mapStyleURL: URL, avatarData: AvatarInputProtocol, isLiveLocationSharingEnabled: Bool = false, service: LocationSharingServiceProtocol) { - - self.locationSharingService = service + locationSharingService = service let viewState = LocationSharingViewState(mapStyleURL: mapStyleURL, userAvatarData: avatarData, @@ -79,7 +76,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie state.showsUserLocation = true state.isPinDropSharing = false case .startLiveSharing: - self.startLiveLocationSharing() + startLiveLocationSharing() case .shareLiveLocation(let timeout): state.bindings.showingTimerSelector = false completion?(.shareLiveLocation(timeout: timeout.rawValue)) @@ -101,8 +98,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie state.showLoadingIndicator = false if let error = error { - - let alertInfo: AlertInfo + let alertInfo: AlertInfo switch error { case .locationSharingPowerLevelError: @@ -128,7 +124,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie return } - let primaryButtonCompletion = { [weak self] () -> Void in + let primaryButtonCompletion: (() -> Void)? = { [weak self] () in self?.completion?(.cancel) } @@ -149,16 +145,15 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, primaryButtonCompletion), secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, { - UIApplication.shared.vc_openSettings() - })) + UIApplication.shared.vc_openSettings() + })) default: break } } private func checkLocationAuthorizationAndPresentTimerSelector() { - - self.locationSharingService.requestAuthorization { [weak self] authorizationStatus in + locationSharingService.requestAuthorization { [weak self] authorizationStatus in guard let self = self else { return @@ -168,15 +163,15 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie case .unknown, .denied: // Show error alert self.state.bindings.alertInfo = AlertInfo(id: .userLocatingError, - title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), + title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), primaryButton: (VectorL10n.ok, { UIApplication.shared.vc_openSettings() - })) + })) case .authorizedInForeground: // When user only authorized location in foreground, advize to use background location self.state.bindings.alertInfo = AlertInfo(id: .userLocatingError, title: VectorL10n.locationSharingAllowBackgroundLocationTitle, - message: VectorL10n.locationSharingAllowBackgroundLocationMessage, - primaryButton: (VectorL10n.locationSharingAllowBackgroundLocationCancelAction, {}), + message: VectorL10n.locationSharingAllowBackgroundLocationMessage, + primaryButton: (VectorL10n.locationSharingAllowBackgroundLocationCancelAction, { }), secondaryButton: (VectorL10n.locationSharingAllowBackgroundLocationValidateAction, { UIApplication.shared.vc_openSettings() })) case .authorizedAlways: self.state.bindings.showingTimerSelector = true @@ -185,12 +180,11 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie } private func startLiveLocationSharing() { - guard let completion = completion else { return } - completion(.checkLiveLocationCanBeStarted({ result in + completion(.checkLiveLocationCanBeStarted { result in switch result { case .success: @@ -200,6 +194,6 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie self.stopLoading(error: .locationSharingPowerLevelError) } } - })) + }) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModelProtocol.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModelProtocol.swift index a15349a3b..eeed96aff 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/LocationSharingViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Service/LocationSharingServiceProtocol.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Service/LocationSharingServiceProtocol.swift index 64cfac575..288a7cc7a 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Service/LocationSharingServiceProtocol.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Service/LocationSharingServiceProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,15 +14,14 @@ // limitations under the License. // -import Foundation import Combine import CoreLocation +import Foundation /// Location authorization request handler typealias LocationAuthorizationHandler = (_ authorizationStatus: LocationAuthorizationStatus) -> Void protocol LocationSharingServiceProtocol { - /// Request location authorization func requestAuthorization(_ handler: @escaping LocationAuthorizationHandler) } diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Service/MatrixSDK/LocationSharingService.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Service/MatrixSDK/LocationSharingService.swift index d7692c29e..980220384 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Service/MatrixSDK/LocationSharingService.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Service/MatrixSDK/LocationSharingService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +14,11 @@ // limitations under the License. // -import Foundation import CoreLocation +import Foundation import MatrixSDK class LocationSharingService: LocationSharingServiceProtocol { - // MARK: - Properties // MARK: Private @@ -27,7 +26,7 @@ class LocationSharingService: LocationSharingServiceProtocol { private let session: MXSession private var userLocationService: UserLocationServiceProtocol? { - return self.session.userLocationService + session.userLocationService } // MARK: Public @@ -41,8 +40,7 @@ class LocationSharingService: LocationSharingServiceProtocol { // MARK: - Public func requestAuthorization(_ handler: @escaping LocationAuthorizationHandler) { - guard let userLocationService = self.userLocationService else { - + guard let userLocationService = userLocationService else { MXLog.error("[LocationSharingService] No userLocationService found for the current session") handler(LocationAuthorizationStatus.unknown) return diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Service/Mock/MockLocationSharingService.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Service/Mock/MockLocationSharingService.swift index 8b8de5a5a..a36d3b726 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Service/Mock/MockLocationSharingService.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Service/Mock/MockLocationSharingService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,9 +14,9 @@ // limitations under the License. // -import Foundation import Combine import CoreLocation +import Foundation class MockLocationSharingService: LocationSharingServiceProtocol { func requestAuthorization(_ handler: @escaping LocationAuthorizationHandler) { diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Test/UI/LocationSharingUITests.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Test/UI/LocationSharingUITests.swift index d9fb19faa..0b55053f8 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Test/UI/LocationSharingUITests.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Test/UI/LocationSharingUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class LocationSharingUITests: MockScreenTestCase { func testInitialUserLocation() { diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Test/Unit/LocationSharingViewModelTests.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Test/Unit/LocationSharingViewModelTests.swift index 1eacdee00..1914b4478 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Test/Unit/LocationSharingViewModelTests.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/Test/Unit/LocationSharingViewModelTests.swift @@ -14,14 +14,13 @@ // limitations under the License. // -import XCTest import Combine import CoreLocation +import XCTest @testable import RiotSwiftUI class LocationSharingViewModelTests: XCTestCase { - var cancellables = Set() func testInitialState() { @@ -40,7 +39,7 @@ class LocationSharingViewModelTests: XCTestCase { func testCancellation() { let viewModel = buildViewModel() - let expectation = self.expectation(description: "Cancellation completion should be invoked") + let expectation = expectation(description: "Cancellation completion should be invoked") viewModel.completion = { result in switch result { @@ -95,10 +94,9 @@ class LocationSharingViewModelTests: XCTestCase { } private func buildViewModel() -> LocationSharingViewModel { - let service = MockLocationSharingService() return LocationSharingViewModel(mapStyleURL: URL(string: "http://empty.com")!, - avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: ""), service: service) + avatarData: AvatarInput(mxContentUri: "", matrixItemId: "", displayName: ""), service: service) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingOptionButton.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingOptionButton.swift index 964784f57..556f410f4 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingOptionButton.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingOptionButton.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct LocationSharingOptionButton: View { - // MARK: - Properties // MARK: Private @@ -25,7 +24,7 @@ struct LocationSharingOptionButton: View { @Environment(\.theme) private var theme: ThemeSwiftUI let text: String - let action: () -> (Void) + let action: () -> Void @ViewBuilder var buttonIcon: Content var body: some View { @@ -44,21 +43,15 @@ struct LocationSharingOptionButton: View { struct LocationSharingOptionButton_Previews: PreviewProvider { static var previews: some View { VStack(alignment: .leading) { - LocationSharingOptionButton(text: VectorL10n.locationSharingStaticShareTitle) { - - } buttonIcon: { + LocationSharingOptionButton(text: VectorL10n.locationSharingStaticShareTitle) { } buttonIcon: { AvatarImage(avatarData: AvatarInput(mxContentUri: nil, matrixItemId: "Alice", displayName: "Alice"), size: .medium) .border() } - LocationSharingOptionButton(text: VectorL10n.locationSharingLiveShareTitle) { - - } buttonIcon: { + LocationSharingOptionButton(text: VectorL10n.locationSharingLiveShareTitle) { } buttonIcon: { Image(uiImage: Asset.Images.locationLiveIcon.image) .resizable() } - LocationSharingOptionButton(text: VectorL10n.locationSharingPinDropShareTitle) { - - } buttonIcon: { + LocationSharingOptionButton(text: VectorL10n.locationSharingPinDropShareTitle) { } buttonIcon: { Image(uiImage: Asset.Images.locationPinIcon.image) .resizable() } diff --git a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingView.swift b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingView.swift index 5f82bce08..a2acb30f1 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StartLocationSharing/View/LocationSharingView.swift @@ -14,11 +14,10 @@ // limitations under the License. // -import SwiftUI import CoreLocation +import SwiftUI struct LocationSharingView: View { - // MARK: - Properties // MARK: Private @@ -39,7 +38,6 @@ struct LocationSharingView: View { } VStack(spacing: 0) { - if context.viewState.showMapLoadingError { MapLoadingErrorView() } else { @@ -50,7 +48,7 @@ struct LocationSharingView: View { }) .padding(.bottom, 10.0) .actionSheet(isPresented: $context.showMapCreditsSheet) { - return MapCreditsActionSheet(openURL: { url in + MapCreditsActionSheet(openURL: { url in openURL(url) }).sheet } @@ -99,8 +97,8 @@ struct LocationSharingView: View { mapCenterCoordinate: $context.pinLocation, errorSubject: context.viewState.errorSubject, userDidPan: { - context.send(viewAction: .userDidPan) - }) + context.send(viewAction: .userDidPan) + }) if context.viewState.isPinDropSharing { LocationSharingMarkerView(backgroundColor: theme.colors.accent) { Image(uiImage: Asset.Images.locationPinIcon.image) diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift index 9475eeac8..b125fbcd6 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Coordinator/StaticLocationViewingCoordinator.swift @@ -15,9 +15,9 @@ // import Foundation -import UIKit -import SwiftUI import MatrixSDK +import SwiftUI +import UIKit struct StaticLocationViewingCoordinatorParameters { let session: MXSession @@ -28,7 +28,6 @@ struct StaticLocationViewingCoordinatorParameters { } final class StaticLocationViewingCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -54,7 +53,8 @@ final class StaticLocationViewingCoordinator: Coordinator, Presentable { mapStyleURL: parameters.session.vc_homeserverConfiguration().tileServer.mapStyleURL, avatarData: parameters.avatarData, location: parameters.location, - coordinateType: parameters.coordinateType) + coordinateType: parameters.coordinateType + ) let view = StaticLocationView(viewModel: viewModel.context) .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) staticLocationViewingViewModel = viewModel @@ -62,6 +62,7 @@ final class StaticLocationViewingCoordinator: Coordinator, Presentable { } // MARK: - Public + func start() { MXLog.debug("[StaticLocationSharingViewerCoordinator] did start.") staticLocationViewingViewModel.completion = { [weak self] result in @@ -77,13 +78,12 @@ final class StaticLocationViewingCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.staticLocationViewingHostingController + staticLocationViewingHostingController } func presentLocationActivityController(with coordinate: CLLocationCoordinate2D) { - let shareActivityController = shareLocationActivityControllerBuilder.build(with: coordinate) - self.staticLocationViewingHostingController.present(shareActivityController, animated: true) + staticLocationViewingHostingController.present(shareActivityController, animated: true) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift index 800057980..2ea0f0aec 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/MockStaticLocationViewingScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,9 +14,9 @@ // limitations under the License. // +import CoreLocation import Foundation import SwiftUI -import CoreLocation /// Using an enum for the screen allows you define the different state cases with /// the relevant associated data for each case. @@ -34,11 +34,11 @@ enum MockStaticLocationViewingScreenState: MockScreenState, CaseIterable { /// A list of screen state definitions static var allCases: [MockStaticLocationViewingScreenState] { - return [.showUserLocation, .showPinLocation] + [.showUserLocation, .showPinLocation] } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let location = CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096) let coordinateType: LocationSharingCoordinateType = self == .showUserLocation ? .user : .pin @@ -50,7 +50,6 @@ enum MockStaticLocationViewingScreenState: MockScreenState, CaseIterable { return ([viewModel], AnyView(StaticLocationView(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) - ) + .addDependency(MockAvatarService.example))) } } diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift index 1f250e4e1..d05b72055 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,9 +14,9 @@ // limitations under the License. // -import Foundation import Combine import CoreLocation +import Foundation // MARK: View model @@ -33,7 +33,6 @@ enum StaticLocationViewingViewModelResult { // MARK: View struct StaticLocationViewingViewState: BindableState { - /// Map style URL let mapStyleURL: URL @@ -43,7 +42,7 @@ struct StaticLocationViewingViewState: BindableState { /// Shared annotation to display existing location let sharedAnnotation: LocationAnnotation - var showLoadingIndicator: Bool = false + var showLoadingIndicator = false var shareButtonEnabled: Bool { !showLoadingIndicator diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift index 8564596a1..83bdb0110 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift @@ -14,14 +14,12 @@ // limitations under the License. // -import SwiftUI import CoreLocation +import SwiftUI + +typealias StaticLocationViewingViewModelType = StateStoreViewModel -typealias StaticLocationViewingViewModelType = StateStoreViewModel class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, StaticLocationViewingViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -79,7 +77,7 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static switch error { case .invalidLocationAuthorization: - if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { + if let applicationSettingsURL = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(applicationSettingsURL) } else { self?.completion?(.close) diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModelProtocol.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModelProtocol.swift index 7835e5c4e..e61ed6b3a 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/StaticLocationViewingViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol StaticLocationViewingViewModelProtocol { - var completion: ((StaticLocationViewingViewModelResult) -> Void)? { get set } var context: StaticLocationViewingViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift index d6f26f247..ae2cea9aa 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class StaticLocationViewingUITests: MockScreenTestCase { // This test has issues running consistently on CI. Removed for now until the issue has been fixed. diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift index 22ce395a7..f9aa24324 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/Test/Unit/StaticLocationViewingViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,14 +14,13 @@ // limitations under the License. // -import XCTest import Combine import CoreLocation +import XCTest @testable import RiotSwiftUI class StaticLocationViewingViewModelTests: XCTestCase { - var cancellables = Set() func testInitialState() { @@ -39,7 +38,7 @@ class StaticLocationViewingViewModelTests: XCTestCase { func testCancellation() { let viewModel = buildViewModel() - let expectation = self.expectation(description: "Cancellation completion should be invoked") + let expectation = expectation(description: "Cancellation completion should be invoked") viewModel.completion = { result in switch result { @@ -58,7 +57,7 @@ class StaticLocationViewingViewModelTests: XCTestCase { func testShareExistingLocation() { let viewModel = buildViewModel() - let expectation = self.expectation(description: "Share completion should be invoked") + let expectation = expectation(description: "Share completion should be invoked") viewModel.completion = { result in switch result { diff --git a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift index 81c18a0e0..a23dc6621 100644 --- a/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift +++ b/RiotSwiftUI/Modules/LocationSharing/StaticLocationSharingViewer/View/StaticLocationView.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct StaticLocationView: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift index 216a65ea4..e3f3de9cd 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/Coordinator/OnboardingAvatarCoordinator.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import SwiftUI import CommonKit +import SwiftUI struct OnboardingAvatarCoordinatorParameters { let userSession: UserSession @@ -32,7 +32,6 @@ enum OnboardingAvatarCoordinatorResult { } final class OnboardingAvatarCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -84,7 +83,6 @@ final class OnboardingAvatarCoordinator: Coordinator, Presentable { indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: onboardingAvatarHostingController) } - // MARK: - Public func start() { @@ -106,7 +104,7 @@ final class OnboardingAvatarCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.onboardingAvatarHostingController + onboardingAvatarHostingController } // MARK: - Private @@ -144,8 +142,8 @@ final class OnboardingAvatarCoordinator: Coordinator, Presentable { guard let avatarData = MXKTools.forceImageOrientationUp(image)?.jpegData(compressionQuality: 0.5) else { MXLog.error("[OnboardingAvatarCoordinator] Failed to create jpeg data.") - self.stopWaiting() - self.onboardingAvatarViewModel.processError(nil) + stopWaiting() + onboardingAvatarViewModel.processError(nil) return } diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift index 7cf96984a..3c972b602 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/MockOnboardingAvatarScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -43,7 +43,7 @@ enum MockOnboardingAvatarScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let avatarColorCount = DefaultThemeSwiftUI().colors.namesAndAvatars.count let viewModel: OnboardingAvatarViewModel switch self { diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarModels.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarModels.swift index 7f0187ebb..036498577 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarModels.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModel.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModel.swift index 7d1c23f5c..21d02db5c 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModel.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModel.swift @@ -14,15 +14,12 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI -typealias OnboardingAvatarViewModelType = StateStoreViewModel +typealias OnboardingAvatarViewModelType = StateStoreViewModel class OnboardingAvatarViewModel: OnboardingAvatarViewModelType, OnboardingAvatarViewModelProtocol { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModelProtocol.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModelProtocol.swift index 0c3eb58c8..bda30b06e 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/OnboardingAvatarViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI protocol OnboardingAvatarViewModelProtocol { - var callback: ((OnboardingAvatarViewModelResult) -> Void)? { get set } var context: OnboardingAvatarViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/Test/UI/OnboardingAvatarUITests.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/UI/OnboardingAvatarUITests.swift index dc71847b5..13318da78 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/Test/UI/OnboardingAvatarUITests.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/UI/OnboardingAvatarUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class OnboardingAvatarUITests: MockScreenTestCase { let userId = "@example:matrix.org" diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/Test/Unit/OnboardingAvatarViewModelTests.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/Unit/OnboardingAvatarViewModelTests.swift index fd0f284d3..228ae6c01 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/Test/Unit/OnboardingAvatarViewModelTests.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/Test/Unit/OnboardingAvatarViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI diff --git a/RiotSwiftUI/Modules/Onboarding/Avatar/View/OnboardingAvatarScreen.swift b/RiotSwiftUI/Modules/Onboarding/Avatar/View/OnboardingAvatarScreen.swift index dcb1af1dd..f93f4cfff 100644 --- a/RiotSwiftUI/Modules/Onboarding/Avatar/View/OnboardingAvatarScreen.swift +++ b/RiotSwiftUI/Modules/Onboarding/Avatar/View/OnboardingAvatarScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +14,10 @@ // limitations under the License. // -import SwiftUI import DesignKit +import SwiftUI struct OnboardingAvatarScreen: View { - // MARK: - Properties // MARK: Private @@ -53,7 +52,6 @@ struct OnboardingAvatarScreen: View { .accentColor(theme.colors.accent) } - /// The user's avatar along with a picker button var avatar: some View { Group { diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/Coordinator/OnboardingCelebrationCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/Coordinator/OnboardingCelebrationCoordinator.swift index 5cfcd8d95..9db9439df 100644 --- a/RiotSwiftUI/Modules/Onboarding/Celebration/Coordinator/OnboardingCelebrationCoordinator.swift +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/Coordinator/OnboardingCelebrationCoordinator.swift @@ -21,7 +21,6 @@ struct OnboardingCelebrationCoordinatorParameters { } final class OnboardingCelebrationCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -46,9 +45,11 @@ final class OnboardingCelebrationCoordinator: Coordinator, Presentable { onboardingCelebrationViewModel = viewModel onboardingCelebrationHostingController = VectorHostingController(rootView: view) onboardingCelebrationHostingController.enableNavigationBarScrollEdgeAppearance = true + onboardingCelebrationHostingController.isNavigationBarHidden = true } // MARK: - Public + func start() { MXLog.debug("[OnboardingCelebrationCoordinator] did start.") onboardingCelebrationViewModel.completion = { [weak self] in @@ -59,6 +60,6 @@ final class OnboardingCelebrationCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.onboardingCelebrationHostingController + onboardingCelebrationHostingController } } diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift index 630fbe6bc..d911f8249 100644 --- a/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/MockOnboardingCelebrationScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,7 +31,7 @@ enum MockOnboardingCelebrationScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel = OnboardingCelebrationViewModel() // can simulate service and viewModel actions here if needs be. diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationModels.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationModels.swift index 49549b9e1..5029fa7f0 100644 --- a/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationModels.swift +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModel.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModel.swift index 67bee880d..6bfaaf7ac 100644 --- a/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModel.swift +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModel.swift @@ -16,12 +16,9 @@ import SwiftUI -typealias OnboardingCelebrationViewModelType = StateStoreViewModel +typealias OnboardingCelebrationViewModelType = StateStoreViewModel class OnboardingCelebrationViewModel: OnboardingCelebrationViewModelType, OnboardingCelebrationViewModelProtocol { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModelProtocol.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModelProtocol.swift index 7c16672b4..c3c5550f2 100644 --- a/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/OnboardingCelebrationViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol OnboardingCelebrationViewModelProtocol { - var completion: (() -> Void)? { get set } var context: OnboardingCelebrationViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/Test/UI/OnboardingCelebrationUITests.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/Test/UI/OnboardingCelebrationUITests.swift index 12d0f210a..587dfb576 100644 --- a/RiotSwiftUI/Modules/Onboarding/Celebration/Test/UI/OnboardingCelebrationUITests.swift +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/Test/UI/OnboardingCelebrationUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class OnboardingCelebrationUITests: MockScreenTestCase { // Nothing to test as the view is completely static diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/Test/Unit/OnboardingCelebrationViewModelTests.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/Test/Unit/OnboardingCelebrationViewModelTests.swift index 3874d1dd2..c512e319b 100644 --- a/RiotSwiftUI/Modules/Onboarding/Celebration/Test/Unit/OnboardingCelebrationViewModelTests.swift +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/Test/Unit/OnboardingCelebrationViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Onboarding/Celebration/View/OnboardingCelebrationScreen.swift b/RiotSwiftUI/Modules/Onboarding/Celebration/View/OnboardingCelebrationScreen.swift index 1d6638114..2ebe7aa28 100644 --- a/RiotSwiftUI/Modules/Onboarding/Celebration/View/OnboardingCelebrationScreen.swift +++ b/RiotSwiftUI/Modules/Onboarding/Celebration/View/OnboardingCelebrationScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +14,10 @@ // limitations under the License. // -import SwiftUI import SceneKit +import SwiftUI struct OnboardingCelebrationScreen: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Onboarding/Common/OnboardingBreakerScreenBackground.swift b/RiotSwiftUI/Modules/Onboarding/Common/OnboardingBreakerScreenBackground.swift index 39fd14915..d690ade43 100644 --- a/RiotSwiftUI/Modules/Onboarding/Common/OnboardingBreakerScreenBackground.swift +++ b/RiotSwiftUI/Modules/Onboarding/Common/OnboardingBreakerScreenBackground.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct OnboardingBreakerScreenBackground: View { - @Environment(\.theme) private var theme /// Flag indicating whether the gradient enabled on light theme @@ -43,21 +42,20 @@ struct OnboardingBreakerScreenBackground: View { ZStack(alignment: .top) { theme.colors.background - if isGradientEnabled && !theme.isDark { + if isGradientEnabled, !theme.isDark { LinearGradient(gradient: gradient, startPoint: .leading, endPoint: .trailing) - .opacity(0.3) - .mask(LinearGradient(colors: [.white, .clear], - startPoint: .top, - endPoint: .bottom)) - .frame(height: geometry.size.height * 0.65) + .opacity(0.3) + .mask(LinearGradient(colors: [.white, .clear], + startPoint: .top, + endPoint: .bottom)) + .frame(height: geometry.size.height * 0.65) } } } .ignoresSafeArea() } - } // MARK: - Previews diff --git a/RiotSwiftUI/Modules/Onboarding/Common/OnboardingButtonStyle.swift b/RiotSwiftUI/Modules/Onboarding/Common/OnboardingButtonStyle.swift index f0ba669dd..f4ee8a8ba 100644 --- a/RiotSwiftUI/Modules/Onboarding/Common/OnboardingButtonStyle.swift +++ b/RiotSwiftUI/Modules/Onboarding/Common/OnboardingButtonStyle.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Onboarding/Common/OnboardingIcon.swift b/RiotSwiftUI/Modules/Onboarding/Common/OnboardingIcon.swift index 598f07446..4993ce002 100644 --- a/RiotSwiftUI/Modules/Onboarding/Common/OnboardingIcon.swift +++ b/RiotSwiftUI/Modules/Onboarding/Common/OnboardingIcon.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct OnboardingIconImage: View { - @Environment(\.theme) private var theme let image: ImageAsset diff --git a/RiotSwiftUI/Modules/Onboarding/Common/OnboardingMetrics.swift b/RiotSwiftUI/Modules/Onboarding/Common/OnboardingMetrics.swift index 6eb697ed3..654d0e0f0 100644 --- a/RiotSwiftUI/Modules/Onboarding/Common/OnboardingMetrics.swift +++ b/RiotSwiftUI/Modules/Onboarding/Common/OnboardingMetrics.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Onboarding/Common/OnboardingTintedFullStopText.swift b/RiotSwiftUI/Modules/Onboarding/Common/OnboardingTintedFullStopText.swift index 5c6aa2ab3..d79484065 100644 --- a/RiotSwiftUI/Modules/Onboarding/Common/OnboardingTintedFullStopText.swift +++ b/RiotSwiftUI/Modules/Onboarding/Common/OnboardingTintedFullStopText.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,10 +19,10 @@ import SwiftUI /// A view that displays text, highlighting the first occurrence of /// the character `.` in the theme's accent color. struct OnboardingTintedFullStopText: View { - // MARK: - Properties // MARK: Private + @Environment(\.theme) private var theme private struct StringComponent { diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/Coordinator/OnboardingCongratulationsCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/Coordinator/OnboardingCongratulationsCoordinator.swift index 59cec36c4..d2f23340e 100644 --- a/RiotSwiftUI/Modules/Onboarding/Congratulations/Coordinator/OnboardingCongratulationsCoordinator.swift +++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/Coordinator/OnboardingCongratulationsCoordinator.swift @@ -32,7 +32,6 @@ enum OnboardingCongratulationsCoordinatorResult { } final class OnboardingCongratulationsCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -59,9 +58,11 @@ final class OnboardingCongratulationsCoordinator: Coordinator, Presentable { onboardingCongratulationsViewModel = viewModel onboardingCongratulationsHostingController = VectorHostingController(rootView: view) onboardingCongratulationsHostingController.statusBarStyle = .lightContent + onboardingCongratulationsHostingController.isNavigationBarHidden = true } // MARK: - Public + func start() { MXLog.debug("[OnboardingCongratulationsCoordinator] did start.") onboardingCongratulationsViewModel.completion = { [weak self] result in @@ -78,6 +79,6 @@ final class OnboardingCongratulationsCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.onboardingCongratulationsHostingController + onboardingCongratulationsHostingController } } diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/MockOnboardingCongratulationsScreenState.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/MockOnboardingCongratulationsScreenState.swift index afe5fb92e..98e45e2c3 100644 --- a/RiotSwiftUI/Modules/Onboarding/Congratulations/MockOnboardingCongratulationsScreenState.swift +++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/MockOnboardingCongratulationsScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,7 +32,7 @@ enum MockOnboardingCongratulationsScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel: OnboardingCongratulationsViewModel switch self { diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift index 03b38e57f..937bd975c 100644 --- a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift +++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModel.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModel.swift index 5bb90c0d7..448cde79b 100644 --- a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModel.swift +++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModel.swift @@ -16,12 +16,9 @@ import SwiftUI -typealias OnboardingCongratulationsViewModelType = StateStoreViewModel +typealias OnboardingCongratulationsViewModelType = StateStoreViewModel class OnboardingCongratulationsViewModel: OnboardingCongratulationsViewModelType, OnboardingCongratulationsViewModelProtocol { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModelProtocol.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModelProtocol.swift index 531e5614a..15e985922 100644 --- a/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/OnboardingCongratulationsViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol OnboardingCongratulationsViewModelProtocol { - var completion: ((OnboardingCongratulationsViewModelResult) -> Void)? { get set } var context: OnboardingCongratulationsViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/UI/OnboardingCongratulationsUITests.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/UI/OnboardingCongratulationsUITests.swift index 9b4c8f9f6..ca32b079b 100644 --- a/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/UI/OnboardingCongratulationsUITests.swift +++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/UI/OnboardingCongratulationsUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class OnboardingCongratulationsUITests: MockScreenTestCase { func testButtons() { diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/Unit/OnboardingCongratulationsViewModelTests.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/Unit/OnboardingCongratulationsViewModelTests.swift index 8d46382e8..d46d1be32 100644 --- a/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/Unit/OnboardingCongratulationsViewModelTests.swift +++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/Test/Unit/OnboardingCongratulationsViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift b/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift index d0ca409b0..a6f165920 100644 --- a/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift +++ b/RiotSwiftUI/Modules/Onboarding/Congratulations/View/OnboardingCongratulationsScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct OnboardingCongratulationsScreen: View { - // MARK: - Properties // MARK: Private @@ -62,7 +61,7 @@ struct OnboardingCongratulationsScreen: View { .overlay(effects.ignoresSafeArea()) .background(theme.colors.accent.ignoresSafeArea()) .navigationBarHidden(true) - .preferredColorScheme(.dark) // make the status bar white + .preferredColorScheme(.dark) // make the status bar white .accentColor(.white) } diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/Coordinator/OnboardingDisplayNameCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/Coordinator/OnboardingDisplayNameCoordinator.swift index feeab4018..78837e8a2 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/Coordinator/OnboardingDisplayNameCoordinator.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/Coordinator/OnboardingDisplayNameCoordinator.swift @@ -14,15 +14,14 @@ // limitations under the License. // -import SwiftUI import CommonKit +import SwiftUI struct OnboardingDisplayNameCoordinatorParameters { let userSession: UserSession } final class OnboardingDisplayNameCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -58,6 +57,7 @@ final class OnboardingDisplayNameCoordinator: Coordinator, Presentable { } // MARK: - Public + func start() { MXLog.debug("[OnboardingDisplayNameCoordinator] did start.") onboardingDisplayNameViewModel.completion = { [weak self] result in @@ -74,7 +74,7 @@ final class OnboardingDisplayNameCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.onboardingDisplayNameHostingController + onboardingDisplayNameHostingController } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/MockOnboardingDisplayNameScreenState.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/MockOnboardingDisplayNameScreenState.swift index 0e50d478c..b9a027d31 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/MockOnboardingDisplayNameScreenState.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/MockOnboardingDisplayNameScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,7 +44,7 @@ enum MockOnboardingDisplayNameScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel: OnboardingDisplayNameViewModel switch self { case .emptyTextField: diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameModels.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameModels.swift index 22857f69c..bae11189e 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameModels.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModel.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModel.swift index 547a71b22..6fb6e38a7 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModel.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModel.swift @@ -14,15 +14,12 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI -typealias OnboardingDisplayNameViewModelType = StateStoreViewModel +typealias OnboardingDisplayNameViewModelType = StateStoreViewModel class OnboardingDisplayNameViewModel: OnboardingDisplayNameViewModelType, OnboardingDisplayNameViewModelProtocol { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModelProtocol.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModelProtocol.swift index 7380bee9a..ae768a816 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/OnboardingDisplayNameViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol OnboardingDisplayNameViewModelProtocol { - var completion: ((OnboardingDisplayNameViewModelResult) -> Void)? { get set } var context: OnboardingDisplayNameViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/UI/OnboardingDisplayNameUITests.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/UI/OnboardingDisplayNameUITests.swift index 6c27e09de..9c006dc6b 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/UI/OnboardingDisplayNameUITests.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/UI/OnboardingDisplayNameUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class OnboardingDisplayNameUITests: MockScreenTestCase { func testEmptyTextField() { @@ -55,8 +55,8 @@ class OnboardingDisplayNameUITests: MockScreenTestCase { func testLongDisplayName() { let displayName = """ - Bacon ipsum dolor amet filet mignon chicken kevin andouille. Doner shoulder beef, brisket bresaola turkey jowl venison. Ham hock cow turducken, chislic venison doner short loin strip steak tri-tip jowl. Sirloin pork belly hamburger ribeye. Tail capicola alcatra short ribs turkey doner. - """ + Bacon ipsum dolor amet filet mignon chicken kevin andouille. Doner shoulder beef, brisket bresaola turkey jowl venison. Ham hock cow turducken, chislic venison doner short loin strip steak tri-tip jowl. Sirloin pork belly hamburger ribeye. Tail capicola alcatra short ribs turkey doner. + """ app.goToScreenWithIdentifier(MockOnboardingDisplayNameScreenState.longDisplayName(displayName: displayName).title) let textField = app.textFields.element diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/Unit/OnboardingDisplayNameViewModelTests.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/Unit/OnboardingDisplayNameViewModelTests.swift index c9b72f045..2dbc015e3 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/Unit/OnboardingDisplayNameViewModelTests.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/Test/Unit/OnboardingDisplayNameViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI diff --git a/RiotSwiftUI/Modules/Onboarding/DisplayName/View/OnboardingDisplayNameScreen.swift b/RiotSwiftUI/Modules/Onboarding/DisplayName/View/OnboardingDisplayNameScreen.swift index 005a33f2c..09fc1ac10 100644 --- a/RiotSwiftUI/Modules/Onboarding/DisplayName/View/OnboardingDisplayNameScreen.swift +++ b/RiotSwiftUI/Modules/Onboarding/DisplayName/View/OnboardingDisplayNameScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct OnboardingDisplayNameScreen: View { - // MARK: - Properties // MARK: Private @@ -64,7 +63,6 @@ struct OnboardingDisplayNameScreen: View { /// The icon, title and message views. var header: some View { VStack(spacing: 8) { - OnboardingIconImage(image: Asset.Images.onboardingCongratulationsIcon) .padding(.bottom, 8) diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/Coordinator/OnboardingSplashScreenCoordinator.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/Coordinator/OnboardingSplashScreenCoordinator.swift index 534bb4e64..669ac19cb 100644 --- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/Coordinator/OnboardingSplashScreenCoordinator.swift +++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/Coordinator/OnboardingSplashScreenCoordinator.swift @@ -14,15 +14,14 @@ // limitations under the License. // -import SwiftUI import CommonKit +import SwiftUI protocol OnboardingSplashScreenCoordinatorProtocol: Coordinator, Presentable { var completion: ((OnboardingSplashScreenViewModelResult) -> Void)? { get set } } final class OnboardingSplashScreenCoordinator: OnboardingSplashScreenCoordinatorProtocol { - // MARK: - Properties // MARK: Private @@ -47,11 +46,13 @@ final class OnboardingSplashScreenCoordinator: OnboardingSplashScreenCoordinator onboardingSplashScreenViewModel = viewModel onboardingSplashScreenHostingController = VectorHostingController(rootView: view) onboardingSplashScreenHostingController.vc_removeBackTitle() + onboardingSplashScreenHostingController.isNavigationBarHidden = true indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: onboardingSplashScreenHostingController) } // MARK: - Public + func start() { MXLog.debug("[OnboardingSplashScreenCoordinator] did start.") onboardingSplashScreenViewModel.completion = { [weak self] result in @@ -68,7 +69,7 @@ final class OnboardingSplashScreenCoordinator: OnboardingSplashScreenCoordinator } func toPresentable() -> UIViewController { - return onboardingSplashScreenHostingController + onboardingSplashScreenHostingController } /// Stops any ongoing activities in the coordinator. diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/MockOnboardingSplashScreenScreenState.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/MockOnboardingSplashScreenScreenState.swift index 511e7cb6e..6e823719a 100644 --- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/MockOnboardingSplashScreenScreenState.swift +++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/MockOnboardingSplashScreenScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,7 +36,7 @@ enum MockOnboardingSplashScreenScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel = OnboardingSplashScreenViewModel() // can simulate service and viewModel actions here if needs be. diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenModels.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenModels.swift index 6bd22223f..3b81de78e 100644 --- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenModels.swift +++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,7 +36,6 @@ enum OnboardingSplashScreenViewModelResult { // MARK: View struct OnboardingSplashScreenViewState: BindableState, CustomDebugStringConvertible { - /// The colours of the background gradient shown behind the 4 pages. private let gradientColors = [ Color(red: 0.95, green: 0.98, blue: 0.96), @@ -67,7 +66,7 @@ struct OnboardingSplashScreenViewState: BindableState, CustomDebugStringConverti let locale = Locale.current let page4Title = locale.identifier.hasPrefix("en") ? "Cut the slack from teams." : VectorL10n.onboardingSplashPage4TitleNoPun - self.content = [ + content = [ OnboardingSplashScreenPageContent(title: VectorL10n.onboardingSplashPage1Title, message: VectorL10n.onboardingSplashPage1Message, image: Asset.Images.onboardingSplashScreenPage1, @@ -83,9 +82,9 @@ struct OnboardingSplashScreenViewState: BindableState, CustomDebugStringConverti OnboardingSplashScreenPageContent(title: page4Title, message: VectorL10n.onboardingSplashPage4Message, image: Asset.Images.onboardingSplashScreenPage4, - darkImage: Asset.Images.onboardingSplashScreenPage4Dark), + darkImage: Asset.Images.onboardingSplashScreenPage4Dark) ] - self.bindings = OnboardingSplashScreenBindings() + bindings = OnboardingSplashScreenBindings() } } diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenViewModel.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenViewModel.swift index 222f70f60..0d9c9c563 100644 --- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenViewModel.swift +++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenViewModel.swift @@ -14,12 +14,10 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI -typealias OnboardingSplashScreenViewModelType = StateStoreViewModel +typealias OnboardingSplashScreenViewModelType = StateStoreViewModel protocol OnboardingSplashScreenViewModelProtocol { var completion: ((OnboardingSplashScreenViewModelResult) -> Void)? { get set } @@ -27,7 +25,6 @@ protocol OnboardingSplashScreenViewModelProtocol { } class OnboardingSplashScreenViewModel: OnboardingSplashScreenViewModelType, OnboardingSplashScreenViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -55,7 +52,7 @@ class OnboardingSplashScreenViewModel: OnboardingSplashScreenViewModelType, Onbo state.bindings.pageIndex = (state.bindings.pageIndex + 1) % state.content.count case .previousPage: // Prevent the hidden page at index -1 from being shown. - state.bindings.pageIndex = max(0, (state.bindings.pageIndex - 1)) + state.bindings.pageIndex = max(0, state.bindings.pageIndex - 1) case .hiddenPage: // Hidden page for a nicer animation when looping back to the start. state.bindings.pageIndex = -1 diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/Test/Unit/OnboardingSplashScreenViewModelTests.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/Test/Unit/OnboardingSplashScreenViewModelTests.swift index 4b47e0501..55b19703b 100644 --- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/Test/Unit/OnboardingSplashScreenViewModelTests.swift +++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/Test/Unit/OnboardingSplashScreenViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +14,9 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI -class OnboardingSplashScreenViewModelTests: XCTestCase { - -} +class OnboardingSplashScreenViewModelTests: XCTestCase { } diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreen.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreen.swift index 6cc28e72c..b94a590f3 100644 --- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreen.swift +++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import SwiftUI /// The splash screen shown at the beginning of the onboarding flow. struct OnboardingSplashScreen: View { - // MARK: - Properties // MARK: Private @@ -46,7 +45,6 @@ struct OnboardingSplashScreen: View { // The main content of the carousel HStack(alignment: .top, spacing: 0) { - // Add a hidden page at the start of the carousel duplicating the content of the last page OnboardingSplashScreenPage(content: viewModel.viewState.content[pageCount - 1]) .frame(width: geometry.size.width) @@ -55,7 +53,6 @@ struct OnboardingSplashScreen: View { OnboardingSplashScreenPage(content: viewModel.viewState.content[index]) .frame(width: geometry.size.width) } - } .offset(x: pageOffset(in: geometry)) @@ -63,8 +60,8 @@ struct OnboardingSplashScreen: View { OnboardingSplashScreenPageIndicator(pageCount: pageCount, pageIndex: viewModel.pageIndex) - .frame(width: geometry.size.width) - .padding(.bottom) + .frame(width: geometry.size.width) + .padding(.bottom) Spacer() @@ -133,7 +130,7 @@ struct OnboardingSplashScreen: View { private func startTimer() { guard pageTimer == nil else { return } - pageTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { timer in + pageTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in if viewModel.pageIndex == pageCount - 1 { viewModel.send(viewAction: .hiddenPage) @@ -167,7 +164,7 @@ struct OnboardingSplashScreen: View { /// - Parameter width: The gesture's translation width. /// - Returns: `true` if there is another page to drag to. private func shouldSwipeForTranslation(_ width: CGFloat) -> Bool { - if viewModel.pageIndex == 0 { + if viewModel.pageIndex == 0 { return isLeftToRight ? width < 0 : width > 0 } else if viewModel.pageIndex == pageCount - 1 { return isLeftToRight ? width > 0 : width < 0 diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPage.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPage.swift index e8e66d773..4047580d9 100644 --- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPage.swift +++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/View/OnboardingSplashScreenPage.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,13 +17,14 @@ import SwiftUI struct OnboardingSplashScreenPage: View { - // MARK: - Properties // MARK: Private + @Environment(\.theme) private var theme // MARK: Public + /// The content that this page should display. let content: OnboardingSplashScreenPageContent @@ -35,8 +36,8 @@ struct OnboardingSplashScreenPage: View { .resizable() .scaledToFit() .frame(maxWidth: 310) // This value is problematic. 300 results in dropped frames - // on iPhone 12/13 Mini. 305 the same on iPhone 12/13. As of - // iOS 15, 310 seems fine on all supported screen widths 🤞. + // on iPhone 12/13 Mini. 305 the same on iPhone 12/13. As of + // iOS 15, 310 seems fine on all supported screen widths 🤞. .padding(20) .accessibilityHidden(true) @@ -60,7 +61,7 @@ struct OnboardingSplashScreenPage: View { struct OnboardingSplashScreenPage_Previews: PreviewProvider { static let content = OnboardingSplashScreenViewState().content static var previews: some View { - ForEach(0.. UIViewController { - return self.onboardingUseCaseHostingController + onboardingUseCaseHostingController } /// Stops any ongoing activities in the coordinator. diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift index 4eb90c305..ed580b834 100644 --- a/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift +++ b/RiotSwiftUI/Modules/Onboarding/UseCase/MockOnboardingUseCaseScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,7 +37,7 @@ enum MockOnboardingUseCaseSelectionScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel = OnboardingUseCaseViewModel() // can simulate service and viewModel actions here if needs be. diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseModels.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseModels.swift index a7160ee67..796ba8e72 100644 --- a/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseModels.swift +++ b/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,10 +20,6 @@ import Foundation // MARK: View model -enum OnboardingUseCaseStateAction { - case viewAction(OnboardingUseCaseViewAction) -} - enum OnboardingUseCaseViewModelResult { case personalMessaging case workMessaging diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseViewModel.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseViewModel.swift index 4d0cc3135..b7eb7eb2a 100644 --- a/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseViewModel.swift +++ b/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseViewModel.swift @@ -16,12 +16,9 @@ import SwiftUI -typealias OnboardingUseCaseViewModelType = StateStoreViewModel +typealias OnboardingUseCaseViewModelType = StateStoreViewModel class OnboardingUseCaseViewModel: OnboardingUseCaseViewModelType, OnboardingUseCaseViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -44,8 +41,4 @@ class OnboardingUseCaseViewModel: OnboardingUseCaseViewModelType, OnboardingUseC completion?(result) } } - - override class func reducer(state: inout OnboardingUseCaseViewState, action: OnboardingUseCaseStateAction) { - // There is no mutable state to reduce :) - } } diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseViewModelProtocol.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseViewModelProtocol.swift index 44d56cfba..f9a89e4eb 100644 --- a/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Onboarding/UseCase/OnboardingUseCaseViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol OnboardingUseCaseViewModelProtocol { - var completion: ((OnboardingUseCaseViewModelResult) -> Void)? { get set } var context: OnboardingUseCaseViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/Test/UI/OnboardingUseCaseUITests.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/Test/UI/OnboardingUseCaseUITests.swift index fbb0a9a64..375c93865 100644 --- a/RiotSwiftUI/Modules/Onboarding/UseCase/Test/UI/OnboardingUseCaseUITests.swift +++ b/RiotSwiftUI/Modules/Onboarding/UseCase/Test/UI/OnboardingUseCaseUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class OnboardingUseCaseUITests: MockScreenTestCase { // The view has no parameters or changing state to test. diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/Test/Unit/OnboardingUseCaseViewModelTests.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/Test/Unit/OnboardingUseCaseViewModelTests.swift index 9f5fa2b8f..eed714cf5 100644 --- a/RiotSwiftUI/Modules/Onboarding/UseCase/Test/Unit/OnboardingUseCaseViewModelTests.swift +++ b/RiotSwiftUI/Modules/Onboarding/UseCase/Test/Unit/OnboardingUseCaseViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/View/OnboardingUseCaseButton.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/View/OnboardingUseCaseButton.swift index 7916fe280..bbbd9999c 100644 --- a/RiotSwiftUI/Modules/Onboarding/UseCase/View/OnboardingUseCaseButton.swift +++ b/RiotSwiftUI/Modules/Onboarding/UseCase/View/OnboardingUseCaseButton.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import SwiftUI /// A button used for the Use Case selection. struct OnboardingUseCaseButton: View { - // MARK: Private @Environment(\.theme) private var theme diff --git a/RiotSwiftUI/Modules/Onboarding/UseCase/View/OnboardingUseCaseSelectionScreen.swift b/RiotSwiftUI/Modules/Onboarding/UseCase/View/OnboardingUseCaseSelectionScreen.swift index 49eebceb7..3d3bb2a70 100644 --- a/RiotSwiftUI/Modules/Onboarding/UseCase/View/OnboardingUseCaseSelectionScreen.swift +++ b/RiotSwiftUI/Modules/Onboarding/UseCase/View/OnboardingUseCaseSelectionScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import SwiftUI /// The screen shown to a new user to select their use case for the app. struct OnboardingUseCaseSelectionScreen: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingModels.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingModels.swift index c33638ddc..76beb1205 100644 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingModels.swift +++ b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModel.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModel.swift index 75712d550..0ba19872a 100644 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModel.swift @@ -14,15 +14,12 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI -typealias AllChatsOnboardingViewModelType = StateStoreViewModel +typealias AllChatsOnboardingViewModelType = StateStoreViewModel class AllChatsOnboardingViewModel: AllChatsOnboardingViewModelType, AllChatsOnboardingViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -34,7 +31,7 @@ class AllChatsOnboardingViewModel: AllChatsOnboardingViewModelType, AllChatsOnbo // MARK: - Setup static func makeAllChatsOnboardingViewModel() -> AllChatsOnboardingViewModelProtocol { - return AllChatsOnboardingViewModel() + AllChatsOnboardingViewModel() } private init() { @@ -42,7 +39,7 @@ class AllChatsOnboardingViewModel: AllChatsOnboardingViewModelType, AllChatsOnbo } private static func defaultState() -> AllChatsOnboardingViewState { - return AllChatsOnboardingViewState(pages: [ + AllChatsOnboardingViewState(pages: [ AllChatsOnboardingPageData(image: Asset.Images.allChatsOnboarding1.image, title: VectorL10n.allChatsOnboardingPageTitle1, message: VectorL10n.allChatsOnboardingPageMessage1), diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModelProtocol.swift index a0cc2137d..dd963c407 100644 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/AllChatsOnboardingViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol AllChatsOnboardingViewModelProtocol { - var completion: ((AllChatsOnboardingViewModelResult) -> Void)? { get set } static func makeAllChatsOnboardingViewModel() -> AllChatsOnboardingViewModelProtocol var context: AllChatsOnboardingViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinator.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinator.swift index de793e30e..df189c144 100644 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinator.swift @@ -14,12 +14,11 @@ // limitations under the License. // -import SwiftUI import CommonKit +import SwiftUI /// All Chats onboarding screen final class AllChatsOnboardingCoordinator: NSObject, Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -42,8 +41,8 @@ final class AllChatsOnboardingCoordinator: NSObject, Coordinator, Presentable { let viewModel = AllChatsOnboardingViewModel.makeAllChatsOnboardingViewModel() let view = AllChatsOnboarding(viewModel: viewModel.context) self.viewModel = viewModel - self.hostingController = VectorHostingController(rootView: view) - self.indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingController) + hostingController = VectorHostingController(rootView: view) + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingController) super.init() @@ -65,7 +64,7 @@ final class AllChatsOnboardingCoordinator: NSObject, Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.hostingController + hostingController } // MARK: - Private @@ -87,9 +86,7 @@ final class AllChatsOnboardingCoordinator: NSObject, Coordinator, Presentable { // MARK: - UIAdaptivePresentationControllerDelegate extension AllChatsOnboardingCoordinator: UIAdaptivePresentationControllerDelegate { - func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { completion?() } - } diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinatorBridgePresenter.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinatorBridgePresenter.swift index b367175ef..75977054d 100644 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinatorBridgePresenter.swift +++ b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/Coordinator/AllChatsOnboardingCoordinatorBridgePresenter.swift @@ -26,7 +26,6 @@ import Foundation /// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. @objcMembers final class AllChatsOnboardingCoordinatorBridgePresenter: NSObject { - // MARK: - Properties // MARK: Private @@ -53,7 +52,7 @@ final class AllChatsOnboardingCoordinatorBridgePresenter: NSObject { } func dismiss(animated: Bool, completion: (() -> Void)?) { - guard let coordinator = self.coordinator else { + guard let coordinator = coordinator else { return } coordinator.toPresentable().dismiss(animated: animated) { @@ -62,4 +61,3 @@ final class AllChatsOnboardingCoordinatorBridgePresenter: NSObject { } } } - diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboarding.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboarding.swift index b13f9a287..513cc55a3 100644 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboarding.swift +++ b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboarding.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct AllChatsOnboarding: View { - // MARK: - Properties // MARK: Private @@ -36,12 +35,12 @@ struct AllChatsOnboarding: View { .foregroundColor(theme.colors.primaryContent) .padding() TabView(selection: $selectedTab) { - ForEach(viewModel.viewState.pages.indices) { index in + ForEach(viewModel.viewState.pages.indices, id: \.self) { index in let page = viewModel.viewState.pages[index] AllChatsOnboardingPage(image: page.image, title: page.title, message: page.message) - .tag(index) + .tag(index) } } .tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic)) @@ -61,7 +60,7 @@ struct AllChatsOnboarding: View { // MARK: - Private private func onCallToAction() { - if (selectedTab == viewModel.viewState.pages.count - 1) { + if selectedTab == viewModel.viewState.pages.count - 1 { viewModel.send(viewAction: .cancel) } else { withAnimation { diff --git a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboardingPage.swift b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboardingPage.swift index 04725ccae..c6a5f06fa 100644 --- a/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboardingPage.swift +++ b/RiotSwiftUI/Modules/Room/AllChatsOnboarding/View/AllChatsOnboardingPage.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct AllChatsOnboardingPage: View { - // MARK: - Properties let image: UIImage @@ -55,7 +54,7 @@ struct AllChatsOnboardingPage_Previews: PreviewProvider { preview.theme(.dark).preferredColorScheme(.dark) } - static private var preview: some View { + private static var preview: some View { AllChatsOnboardingPage(image: Asset.Images.allChatsOnboarding1.image, title: VectorL10n.allChatsOnboardingPageTitle1, message: VectorL10n.allChatsOnboardingPageMessage1) diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsBridgePresenter.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsBridgePresenter.swift index 0be2f18a1..be7571107 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsBridgePresenter.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsBridgePresenter.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,7 +25,6 @@ import Foundation /// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. @objcMembers final class RoomNotificationSettingsCoordinatorBridgePresenter: NSObject { - // MARK: - Properties // MARK: Private @@ -61,11 +60,11 @@ final class RoomNotificationSettingsCoordinatorBridgePresenter: NSObject { viewController.present(navigationController, animated: animated, completion: nil) roomNotificationSettingsCoordinator.start() - self.coordinator = roomNotificationSettingsCoordinator + coordinator = roomNotificationSettingsCoordinator } func dismiss(animated: Bool, completion: (() -> Void)?) { - guard let coordinator = self.coordinator else { + guard let coordinator = coordinator else { return } coordinator.toPresentable().dismiss(animated: animated) { @@ -79,22 +78,21 @@ final class RoomNotificationSettingsCoordinatorBridgePresenter: NSObject { } // MARK: - RoomNotificationSettingsCoordinatorDelegate + extension RoomNotificationSettingsCoordinatorBridgePresenter: RoomNotificationSettingsCoordinatorDelegate { func roomNotificationSettingsCoordinatorDidCancel(_ coordinator: RoomNotificationSettingsCoordinatorType) { - self.delegate?.roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self) + delegate?.roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self) } func roomNotificationSettingsCoordinatorDidComplete(_ coordinator: RoomNotificationSettingsCoordinatorType) { - self.delegate?.roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self) + delegate?.roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self) } } // MARK: - UIAdaptivePresentationControllerDelegate extension RoomNotificationSettingsCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate { - func roomNotificationSettingsCoordinatorDidComplete(_ presentationController: UIPresentationController) { - self.delegate?.roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self) + delegate?.roomNotificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self) } - } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift index fef123281..e0d909898 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift @@ -15,14 +15,14 @@ // import Foundation -import UIKit import SwiftUI +import UIKit final class RoomNotificationSettingsCoordinator: RoomNotificationSettingsCoordinatorType { - // MARK: - Properties // MARK: Private + private var roomNotificationSettingsViewModel: RoomNotificationSettingsViewModelType private let roomNotificationSettingsViewController: UIViewController @@ -47,34 +47,35 @@ final class RoomNotificationSettingsCoordinator: RoomNotificationSettingsCoordin roomNotificationService: roomNotificationService, avatarData: avatarData, displayName: room.summary.displayname, - roomEncrypted: room.summary.isEncrypted) + roomEncrypted: room.summary.isEncrypted + ) let avatarService: AvatarServiceProtocol = AvatarService(mediaManager: room.mxSession.mediaManager) let view = RoomNotificationSettings(viewModel: viewModel, presentedModally: presentedModally) .addDependency(avatarService) let viewController = VectorHostingController(rootView: view) - self.roomNotificationSettingsViewModel = viewModel - self.roomNotificationSettingsViewController = viewController + roomNotificationSettingsViewModel = viewModel + roomNotificationSettingsViewController = viewController } // MARK: - Public methods - func start() { - self.roomNotificationSettingsViewModel.coordinatorDelegate = self + func start() { + roomNotificationSettingsViewModel.coordinatorDelegate = self } func toPresentable() -> UIViewController { - return self.roomNotificationSettingsViewController + roomNotificationSettingsViewController } } // MARK: - RoomNotificationSettingsViewModelCoordinatorDelegate + extension RoomNotificationSettingsCoordinator: RoomNotificationSettingsViewModelCoordinatorDelegate { - func roomNotificationSettingsViewModelDidComplete(_ viewModel: RoomNotificationSettingsViewModelType) { - self.delegate?.roomNotificationSettingsCoordinatorDidComplete(self) + delegate?.roomNotificationSettingsCoordinatorDidComplete(self) } func roomNotificationSettingsViewModelDidCancel(_ viewModel: RoomNotificationSettingsViewModelType) { - self.delegate?.roomNotificationSettingsCoordinatorDidCancel(self) + delegate?.roomNotificationSettingsCoordinatorDidCancel(self) } } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewState.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewState.swift index ba8e92916..96773de68 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewState.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -40,4 +40,3 @@ extension RoomNotificationSettingsViewState { roomEncrypted ? VectorL10n.roomNotifsSettingsEncryptedRoomNotice : "" } } - diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewStateType.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewStateType.swift index 0b9c7e0b6..d17fcc3dc 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewStateType.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationSettingsViewStateType.swift @@ -26,4 +26,3 @@ protocol RoomNotificationSettingsViewStateType { var avatarData: AvatarProtocol? { get } var displayName: String? { get } } - diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationState.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationState.swift index 00c51d6fc..e34bb5faa 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationState.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Model/RoomNotificationState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,7 +25,7 @@ enum RoomNotificationState: Int { extension RoomNotificationState: CaseIterable { } extension RoomNotificationState: Identifiable { - var id: Int { self.rawValue } + var id: Int { rawValue } } extension RoomNotificationState { diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift index 03b868a83..e9d192b49 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Service/MatrixSDK/MXRoomNotificationSettingsService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceType { - typealias Completion = () -> Void // MARK: - Properties @@ -50,11 +49,11 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy // MARK: - Public func observeNotificationState(listener: @escaping RoomNotificationStateCallback) { - let observer = NotificationCenter.default.addObserver( forName: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules), object: nil, - queue: OperationQueue.main) { [weak self] _ in + queue: OperationQueue.main + ) { [weak self] _ in guard let self = self else { return } listener(self.room.notificationState) } @@ -88,7 +87,7 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy } guard let rule = room.overridePushRule else { - self.addPushRuleToMute(completion: completion) + addPushRuleToMute(completion: completion) return } @@ -96,7 +95,7 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy MXLog.debug("[RoomNotificationSettingsService] Request in progress: ignore push rule update") completion() return - } + } // if the user defined one, use it if rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify) { @@ -130,7 +129,7 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy MXLog.debug("[MXRoom+Riot] Request in progress: ignore push rule update") completion() return - } + } // if the user defined one, use it if rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify) { @@ -140,11 +139,10 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy self.addPushRuleToMentionOnly(completion: completion) } } - } private func allMessages(completion: @escaping Completion) { - if !room.isMentionsOnly && !room.isMuted { + if !room.isMentionsOnly, !room.isMuted { completion() return } @@ -172,7 +170,8 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy room.roomId, notify: false, sound: false, - highlight: false) + highlight: false + ) } private func addPushRuleToMute(completion: @escaping Completion) { @@ -207,18 +206,19 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy private func enablePushRule(rule: MXPushRule, completion: @escaping Completion) { handleUpdateCallback(completion) { // No way to check whether this notification concerns the push rule. Consider the change is applied. - return true + true } handleFailureCallback(completion) room.mxSession.notificationCenter.enableRule(rule, isEnabled: true) } - private func handleUpdateCallback(_ completion: @escaping Completion, releaseCheck: @escaping () -> Bool) { + private func handleUpdateCallback(_ completion: @escaping Completion, releaseCheck: @escaping () -> Bool) { notificationCenterDidUpdateObserver = NotificationCenter.default.addObserver( forName: NSNotification.Name(rawValue: kMXNotificationCenterDidUpdateRules), object: nil, - queue: OperationQueue.main) { [weak self] _ in + queue: OperationQueue.main + ) { [weak self] _ in guard let self = self else { return } if releaseCheck() { self.removeObservers() @@ -231,7 +231,8 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy notificationCenterDidFailObserver = NotificationCenter.default.addObserver( forName: NSNotification.Name(rawValue: kMXNotificationCenterDidFailRulesUpdate), object: nil, - queue: OperationQueue.main) { [weak self] _ in + queue: OperationQueue.main + ) { [weak self] _ in guard let self = self else { return } self.removeObservers() completion() @@ -239,23 +240,23 @@ final class MXRoomNotificationSettingsService: RoomNotificationSettingsServiceTy } func removeObservers() { - if let observer = self.notificationCenterDidUpdateObserver { + if let observer = notificationCenterDidUpdateObserver { NotificationCenter.default.removeObserver(observer) - self.notificationCenterDidUpdateObserver = nil + notificationCenterDidUpdateObserver = nil } - if let observer = self.notificationCenterDidFailObserver { + if let observer = notificationCenterDidFailObserver { NotificationCenter.default.removeObserver(observer) - self.notificationCenterDidFailObserver = nil + notificationCenterDidFailObserver = nil } } } -extension MXRoom { - public var isMuted: Bool { +public extension MXRoom { + var isMuted: Bool { // Check whether an override rule has been defined with the roomm id as rule id. // This kind of rule is created to mute the room - guard let rule = self.overridePushRule, + guard let rule = overridePushRule, rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify), rule.conditionIsEnabled(kind: .eventMatch, for: roomId) else { return false @@ -263,7 +264,7 @@ extension MXRoom { return rule.enabled } - public var isMentionsOnly: Bool { + var isMentionsOnly: Bool { // Check push rules at room level guard let rule = roomPushRule else { return false } return rule.enabled && rule.actionsContains(actionType: MXPushRuleActionTypeDontNotify) @@ -271,8 +272,7 @@ extension MXRoom { } // We could move these to their own file and make available in global namespace or move to sdk but they are only used here at the moment -fileprivate extension MXRoom { - +private extension MXRoom { typealias Completion = () -> Void func getRoomRule(from rules: [Any]) -> MXPushRule? { guard let pushRules = rules as? [MXPushRule] else { @@ -285,19 +285,18 @@ fileprivate extension MXRoom { var overridePushRule: MXPushRule? { guard let overrideRules = mxSession.notificationCenter.rules.global.override else { return nil - } + } return getRoomRule(from: overrideRules) } var roomPushRule: MXPushRule? { guard let roomRules = mxSession.notificationCenter.rules.global.room else { return nil - } + } return getRoomRule(from: roomRules) } var notificationState: RoomNotificationState { - if isMuted { return .mute } @@ -306,10 +305,9 @@ fileprivate extension MXRoom { } return .all } - } -fileprivate extension MXPushRule { +private extension MXPushRule { func actionsContains(actionType: MXPushRuleActionType) -> Bool { guard let actions = actions as? [MXPushRuleAction] else { return false @@ -323,8 +321,8 @@ fileprivate extension MXPushRule { } let ruleContainsCondition = conditions.contains { condition in guard case kind = MXPushRuleConditionType(identifier: condition.kind), - let key = condition.parameters["key"] as? String, - let pattern = condition.parameters["pattern"] as? String + let key = condition.parameters["key"] as? String, + let pattern = condition.parameters["pattern"] as? String else { return false } return key == "room_id" && pattern == roomId } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Service/Mock/MockRoomNotificationSettingsService.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Service/Mock/MockRoomNotificationSettingsService.swift index 2330d07a5..9e04706de 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Service/Mock/MockRoomNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Service/Mock/MockRoomNotificationSettingsService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation class MockRoomNotificationSettingsService: RoomNotificationSettingsServiceType { - static let example = MockRoomNotificationSettingsService(initialState: .all) var listener: RoomNotificationStateCallback? @@ -32,7 +31,7 @@ class MockRoomNotificationSettingsService: RoomNotificationSettingsServiceType { } func update(state: RoomNotificationState, completion: @escaping UpdateRoomNotificationStateCompletion) { - self.notificationState = state + notificationState = state completion() listener?(state) } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Service/RoomNotificationSettingsServiceType.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Service/RoomNotificationSettingsServiceType.swift index 22961ad03..87443ceed 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Service/RoomNotificationSettingsServiceType.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Service/RoomNotificationSettingsServiceType.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,7 +20,6 @@ typealias UpdateRoomNotificationStateCompletion = () -> Void typealias RoomNotificationStateCallback = (RoomNotificationState) -> Void protocol RoomNotificationSettingsServiceType { - func observeNotificationState(listener: @escaping RoomNotificationStateCallback) func update(state: RoomNotificationState, completion: @escaping UpdateRoomNotificationStateCompletion) var notificationState: RoomNotificationState { get } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormItemButtonStyle.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormItemButtonStyle.swift index 385aacff0..c3b0a61ff 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormItemButtonStyle.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormItemButtonStyle.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormPickerItem.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormPickerItem.swift index 43aee6da8..05a3b161f 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormPickerItem.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormPickerItem.swift @@ -17,7 +17,6 @@ import SwiftUI struct FormPickerItem: View { - typealias TapCallback = () -> Void @Environment(\.theme) var theme: ThemeSwiftUI @@ -53,7 +52,6 @@ struct FormPickerItem: View { } struct FormPickerItem_Previews: PreviewProvider { - static let items = ["Item 1", "Item 2", "Item 3"] static var selected: String = items[0] static var previews: some View { diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormSectionFooter.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormSectionFooter.swift index 3465c40c9..4fc9e6211 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormSectionFooter.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormSectionFooter.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct FormSectionFooter: View { - @Environment(\.theme) var theme: ThemeSwiftUI var text: String diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormSectionHeader.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormSectionHeader.swift index 0ca745ef4..483be9164 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormSectionHeader.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/FormSectionHeader.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct FormSectionHeader: View { - @Environment(\.theme) var theme: ThemeSwiftUI var text: String diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift index 864482e4b..3034f50db 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettings.swift @@ -17,7 +17,6 @@ import SwiftUI struct RoomNotificationSettings: View { - @Environment(\.theme) var theme: ThemeSwiftUI @ObservedObject var viewModel: RoomNotificationSettingsSwiftUIViewModel @@ -42,7 +41,7 @@ struct RoomNotificationSettings: View { var body: some View { VectorForm { - if let avatarData = viewModel.viewState.avatarData as? AvatarInputProtocol { + if let avatarData = viewModel.viewState.avatarData as? AvatarInputProtocol { RoomNotificationSettingsHeader( avatarData: avatarData, displayName: viewModel.viewState.displayName @@ -74,7 +73,6 @@ struct RoomNotificationSettings: View { } struct RoomNotificationSettings_Previews: PreviewProvider { - static let mockViewModel = RoomNotificationSettingsSwiftUIViewModel( roomNotificationService: MockRoomNotificationSettingsService.example, avatarData: MockAvatarInput.example, diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift index a0b602fd8..20066b961 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/RoomNotificationSettingsHeader.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct RoomNotificationSettingsHeader: View { - @Environment(\.theme) var theme: ThemeSwiftUI var avatarData: AvatarInputProtocol var displayName: String? diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/View/VectorForm.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/View/VectorForm.swift index 1ca9933ec..f7e4b3dbc 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/View/VectorForm.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/View/VectorForm.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct VectorForm: View { - @Environment(\.theme) var theme: ThemeSwiftUI var content: () -> Content @@ -38,12 +37,10 @@ struct VectorForm: View { ) .background(theme.colors.system) .edgesIgnoringSafeArea(.bottom) - } } struct VectorForm_Previews: PreviewProvider { - static var previews: some View { Group { VectorForm { diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsSwiftUIViewModel.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsSwiftUIViewModel.swift index d9b6d9bc8..1697601c2 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsSwiftUIViewModel.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsSwiftUIViewModel.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,17 +14,16 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class RoomNotificationSettingsSwiftUIViewModel: RoomNotificationSettingsViewModel, ObservableObject { - @Published var viewState: RoomNotificationSettingsViewState lazy var cancellables = Set() override init(roomNotificationService: RoomNotificationSettingsServiceType, initialState: RoomNotificationSettingsViewState) { - self.viewState = initialState + viewState = initialState super.init(roomNotificationService: roomNotificationService, initialState: initialState) } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsViewModel.swift index 6b09b81d6..977945f8c 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsViewModel.swift @@ -16,11 +16,10 @@ limitations under the License. */ -import Foundation import Combine +import Foundation class RoomNotificationSettingsViewModel: RoomNotificationSettingsViewModelType { - // MARK: - Properties // MARK: Private @@ -40,12 +39,10 @@ class RoomNotificationSettingsViewModel: RoomNotificationSettingsViewModelType { // MARK: - Setup - init( - roomNotificationService: RoomNotificationSettingsServiceType, - initialState: RoomNotificationSettingsViewState - ) { + init(roomNotificationService: RoomNotificationSettingsServiceType, + initialState: RoomNotificationSettingsViewState) { self.roomNotificationService = roomNotificationService - self.state = initialState + state = initialState self.roomNotificationService.observeNotificationState { [weak self] state in guard let self = self else { return } @@ -53,12 +50,10 @@ class RoomNotificationSettingsViewModel: RoomNotificationSettingsViewModelType { } } - convenience init( - roomNotificationService: RoomNotificationSettingsServiceType, - avatarData: AvatarProtocol?, - displayName: String?, - roomEncrypted: Bool - ) { + convenience init(roomNotificationService: RoomNotificationSettingsServiceType, + avatarData: AvatarProtocol?, + displayName: String?, + roomEncrypted: Bool) { let notificationState = Self.mapNotificationStateOnRead(encrypted: roomEncrypted, state: roomNotificationService.notificationState) let initialState = RoomNotificationSettingsViewState( @@ -71,16 +66,16 @@ class RoomNotificationSettingsViewModel: RoomNotificationSettingsViewModelType { self.init(roomNotificationService: roomNotificationService, initialState: initialState) } - // MARK: - Public + // MARK: - Public func process(viewAction: RoomNotificationSettingsViewAction) { switch viewAction { case .load: - update(viewState: self.state) + update(viewState: state) case .selectNotificationState(let state): self.state.notificationState = state case .save: - self.state.saving = true + state.saving = true roomNotificationService.update(state: state.notificationState) { [weak self] in guard let self = self else { return } self.state.saving = false @@ -103,6 +98,6 @@ class RoomNotificationSettingsViewModel: RoomNotificationSettingsViewModelType { } func update(viewState: RoomNotificationSettingsViewState) { - self.viewDelegate?.roomNotificationSettingsViewModel(self, didUpdateViewState: viewState) + viewDelegate?.roomNotificationSettingsViewModel(self, didUpdateViewState: viewState) } } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsViewModelType.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsViewModelType.swift index e2fe135d4..5df765f25 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsViewModelType.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/ViewModel/RoomNotificationSettingsViewModelType.swift @@ -28,8 +28,7 @@ protocol RoomNotificationSettingsViewModelCoordinatorDelegate: AnyObject { } /// Protocol describing the view model used by `RoomNotificationSettingsViewController` -protocol RoomNotificationSettingsViewModelType { - +protocol RoomNotificationSettingsViewModelType { var viewDelegate: RoomNotificationSettingsViewModelViewDelegate? { get set } var coordinatorDelegate: RoomNotificationSettingsViewModelCoordinatorDelegate? { get set } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift index 14ed174de..dcda5b5ab 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -15,8 +15,8 @@ // import Foundation -import UIKit import SwiftUI +import UIKit struct PollEditFormCoordinatorParameters { let room: MXRoom @@ -24,7 +24,6 @@ struct PollEditFormCoordinatorParameters { } final class PollEditFormCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -50,7 +49,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .editing, pollDetails: EditFormPollDetails(type: Self.pollKindKeyToDetailsType(pollContent.kind), question: pollContent.question, - answerOptions: pollContent.answerOptions.map { $0.text }))) + answerOptions: pollContent.answerOptions.map(\.text)))) } else { viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .creation, pollDetails: .default)) @@ -63,6 +62,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { } // MARK: - Public + func start() { pollEditFormViewModel.completion = { [weak self] result in guard let self = self else { return } @@ -75,7 +75,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { self.pollEditFormViewModel.startLoading() - self.parameters.room.sendPollStart(withContent: pollStartContent, threadId: nil, localEcho: nil) { [weak self] result in + self.parameters.room.sendPollStart(withContent: pollStartContent, threadId: nil, localEcho: nil) { [weak self] _ in guard let self = self else { return } self.pollEditFormViewModel.stopLoading() @@ -103,7 +103,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { self.parameters.room.sendPollUpdate(for: pollStartEvent, oldContent: oldPollContent, - newContent: newPollContent, localEcho: nil) { [weak self] result in + newContent: newPollContent, localEcho: nil) { [weak self] _ in guard let self = self else { return } self.pollEditFormViewModel.stopLoading() @@ -113,7 +113,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { MXLog.error("Failed updating poll", context: error) self.pollEditFormViewModel.stopLoading(errorAlertType: .failedUpdatingPoll) - } + } } } } @@ -121,7 +121,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { // MARK: - Presentable func toPresentable() -> UIViewController { - return pollEditFormHostingController + pollEditFormHostingController } // MARK: - Private @@ -136,21 +136,20 @@ final class PollEditFormCoordinator: Coordinator, Presentable { kind: Self.pollDetailsTypeToKindKey(details.type), maxSelections: NSNumber(value: details.maxSelections), answerOptions: options) - } private static func pollDetailsTypeToKindKey(_ type: EditFormPollType) -> String { - let mapping = [EditFormPollType.disclosed : kMXMessageContentKeyExtensiblePollKindDisclosedMSC3381, - EditFormPollType.undisclosed : kMXMessageContentKeyExtensiblePollKindUndisclosedMSC3381] + let mapping = [EditFormPollType.disclosed: kMXMessageContentKeyExtensiblePollKindDisclosedMSC3381, + EditFormPollType.undisclosed: kMXMessageContentKeyExtensiblePollKindUndisclosedMSC3381] return mapping[type] ?? kMXMessageContentKeyExtensiblePollKindDisclosedMSC3381 } private static func pollKindKeyToDetailsType(_ key: String) -> EditFormPollType { - let mapping = [kMXMessageContentKeyExtensiblePollKindDisclosed : EditFormPollType.disclosed, - kMXMessageContentKeyExtensiblePollKindDisclosedMSC3381 : EditFormPollType.disclosed, - kMXMessageContentKeyExtensiblePollKindUndisclosed : EditFormPollType.undisclosed, - kMXMessageContentKeyExtensiblePollKindUndisclosedMSC3381 : EditFormPollType.undisclosed] + let mapping = [kMXMessageContentKeyExtensiblePollKindDisclosed: EditFormPollType.disclosed, + kMXMessageContentKeyExtensiblePollKindDisclosedMSC3381: EditFormPollType.disclosed, + kMXMessageContentKeyExtensiblePollKindUndisclosed: EditFormPollType.undisclosed, + kMXMessageContentKeyExtensiblePollKindUndisclosedMSC3381: EditFormPollType.undisclosed] return mapping[key] ?? EditFormPollType.disclosed } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift index 75fbf84ab..9e2171bf7 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -82,14 +82,14 @@ struct PollEditFormViewState: BindableState { var confirmationButtonEnabled: Bool { !bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && - bindings.answerOptions.filter({ !$0.text.isEmpty }).count >= minAnswerOptionsCount + bindings.answerOptions.filter { !$0.text.isEmpty }.count >= minAnswerOptionsCount } var addAnswerOptionButtonEnabled: Bool { bindings.answerOptions.count < maxAnswerOptionsCount } - var showLoadingIndicator: Bool = false + var showLoadingIndicator = false } struct PollEditFormViewStateBindings { diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift index 3d15e3be4..d4982b357 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,7 @@ enum MockPollEditFormScreenState: MockScreenState, CaseIterable { PollEditForm.self } - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .creation, pollDetails: .default)) return ([viewModel], AnyView(PollEditForm(viewModel: viewModel.context))) } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift index 6b22edc50..43ce0dc72 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift @@ -14,20 +14,18 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI struct PollEditFormViewModelParameters { let mode: PollEditFormMode let pollDetails: EditFormPollDetails } -typealias PollEditFormViewModelType = StateStoreViewModel +typealias PollEditFormViewModelType = StateStoreViewModel + class PollEditFormViewModel: PollEditFormViewModelType, PollEditFormViewModelProtocol { - - private struct Constants { + private enum Constants { static let minAnswerOptionsCount = 2 static let maxAnswerOptionsCount = 20 static let maxQuestionLength = 340 @@ -102,11 +100,11 @@ class PollEditFormViewModel: PollEditFormViewModelType, PollEditFormViewModelPro // MARK: - Private private func buildPollDetails() -> EditFormPollDetails { - return EditFormPollDetails(type: state.bindings.type, - question: state.bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines), - answerOptions: state.bindings.answerOptions.compactMap({ answerOption in - let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines) - return text.isEmpty ? nil : text - })) + EditFormPollDetails(type: state.bindings.type, + question: state.bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines), + answerOptions: state.bindings.answerOptions.compactMap { answerOption in + let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines) + return text.isEmpty ? nil : text + }) } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModelProtocol.swift index da2824270..ce87af060 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift index d56c88391..0742cf7bb 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class PollEditFormUITests: MockScreenTestCase { func testInitialStateComponents() { diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift index 4f587a519..571cfd736 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift index 6f8419f28..12d8807d5 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct PollEditForm: View { - // MARK: - Properties // MARK: Private @@ -33,7 +32,6 @@ struct PollEditForm: View { GeometryReader { proxy in ScrollView { VStack(alignment: .leading, spacing: 32.0) { - PollEditFormTypePicker(selectedType: $viewModel.type) VStack(alignment: .leading, spacing: 16.0) { diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormAnswerOptionView.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormAnswerOptionView.swift index ccae185aa..325ae540b 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormAnswerOptionView.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormAnswerOptionView.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct PollEditFormAnswerOptionView: View { - @Environment(\.theme) private var theme: ThemeSwiftUI @State private var focused = false @@ -39,7 +38,7 @@ struct PollEditFormAnswerOptionView: View { }) .textFieldStyle(BorderedInputFieldStyle(isEditing: focused)) Button(action: onDelete) { - Image(uiImage:Asset.Images.pollDeleteOptionIcon.image) + Image(uiImage: Asset.Images.pollDeleteOptionIcon.image) } .accessibilityIdentifier("Delete answer option") } @@ -50,12 +49,8 @@ struct PollEditFormAnswerOptionView: View { struct PollEditFormAnswerOptionView_Previews: PreviewProvider { static var previews: some View { VStack(spacing: 32.0) { - PollEditFormAnswerOptionView(text: Binding.constant(""), index: 0) { - - } - PollEditFormAnswerOptionView(text: Binding.constant("Test"), index: 5) { - - } + PollEditFormAnswerOptionView(text: Binding.constant(""), index: 0) { } + PollEditFormAnswerOptionView(text: Binding.constant("Test"), index: 5) { } } } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormTypePicker.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormTypePicker.swift index cb7a5563d..931cf4b43 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormTypePicker.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormTypePicker.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -45,7 +45,6 @@ private struct PollEditFormTypeButton: View { selectedType = type } label: { HStack(alignment: .top, spacing: 8.0) { - Image(uiImage: selectionImage) VStack(alignment: .leading, spacing: 2) { diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/Coordinator/RoomAccessCoordinator.swift b/RiotSwiftUI/Modules/Room/RoomAccess/Coordinator/RoomAccessCoordinator.swift index 5ca4563dc..0d109ecdc 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/Coordinator/RoomAccessCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/Coordinator/RoomAccessCoordinator.swift @@ -26,7 +26,6 @@ enum RoomAccessCoordinatorCoordinatorAction { @objcMembers final class RoomAccessCoordinator: Coordinator { - // MARK: - Properties // MARK: Private @@ -35,7 +34,7 @@ final class RoomAccessCoordinator: Coordinator { private var upgradedRoomId: String? private var navigationRouter: NavigationRouterType { - return self.parameters.navigationRouter + parameters.navigationRouter } // MARK: Public @@ -58,32 +57,31 @@ final class RoomAccessCoordinator: Coordinator { init(parameters: RoomAccessCoordinatorParameters) { self.parameters = parameters - } + } // MARK: - Public - func start() { MXLog.debug("[RoomAccessCoordinator] did start.") - let rootCoordinator = self.createRoomAccessTypeCoordinator() + let rootCoordinator = createRoomAccessTypeCoordinator() rootCoordinator.start() - self.add(childCoordinator: rootCoordinator) - self.accessCoordinator = rootCoordinator + add(childCoordinator: rootCoordinator) + accessCoordinator = rootCoordinator - if self.navigationRouter.modules.isEmpty == false { - self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in + if navigationRouter.modules.isEmpty == false { + navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in self?.remove(childCoordinator: rootCoordinator) }) } else { - self.navigationRouter.setRootModule(rootCoordinator) { [weak self] in + navigationRouter.setRootModule(rootCoordinator) { [weak self] in self?.remove(childCoordinator: rootCoordinator) } } } func toPresentable() -> UIViewController { - return self.navigationRouter.toPresentable() + navigationRouter.toPresentable() } // MARK: - Private @@ -91,7 +89,7 @@ final class RoomAccessCoordinator: Coordinator { func pushScreen(with coordinator: Coordinator & Presentable) { add(childCoordinator: coordinator) - self.navigationRouter.push(coordinator, animated: true, popCompletion: { [weak self] in + navigationRouter.push(coordinator, animated: true, popCompletion: { [weak self] in self?.remove(childCoordinator: coordinator) }) @@ -103,13 +101,13 @@ final class RoomAccessCoordinator: Coordinator { coordinator.toPresentable().modalPresentationStyle = .overFullScreen coordinator.toPresentable().modalTransitionStyle = .crossDissolve - self.navigationRouter.present(coordinator, animated: true) + navigationRouter.present(coordinator, animated: true) coordinator.start() } private func createRoomAccessTypeCoordinator() -> RoomAccessTypeChooserCoordinator { - let coordinator: RoomAccessTypeChooserCoordinator = RoomAccessTypeChooserCoordinator(parameters: RoomAccessTypeChooserCoordinatorParameters(roomId: parameters.room.roomId, allowsRoomUpgrade: parameters.allowsRoomUpgrade, session: parameters.room.mxSession)) + let coordinator = RoomAccessTypeChooserCoordinator(parameters: RoomAccessTypeChooserCoordinatorParameters(roomId: parameters.room.roomId, allowsRoomUpgrade: parameters.allowsRoomUpgrade, session: parameters.room.mxSession)) coordinator.callback = { [weak self] result in guard let self = self else { return } @@ -132,7 +130,8 @@ final class RoomAccessCoordinator: Coordinator { let paramaters = MatrixItemChooserCoordinatorParameters( session: parameters.room.mxSession, viewProvider: RoomRestrictedAccessSpaceChooserViewProvider(navTitle: VectorL10n.roomAccessSettingsScreenNavTitle), - itemsProcessor: RoomRestrictedAccessSpaceChooserItemsProcessor(roomId: roomId, session: parameters.room.mxSession)) + itemsProcessor: RoomRestrictedAccessSpaceChooserItemsProcessor(roomId: roomId, session: parameters.room.mxSession) + ) let coordinator = MatrixItemChooserCoordinator(parameters: paramaters) coordinator.completion = { [weak self] result in guard let self = self else { return } @@ -154,7 +153,8 @@ final class RoomAccessCoordinator: Coordinator { session: parameters.room.mxSession, roomId: roomId, parentSpaceId: parameters.parentSpaceId, - versionOverride: versionOverride) + versionOverride: versionOverride + ) let coordinator = RoomUpgradeCoordinator(parameters: paramaters) coordinator.completion = { [weak self] result in diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/Coordinator/RoomAccessCoordinatorBridgePresenter.swift b/RiotSwiftUI/Modules/Room/RoomAccess/Coordinator/RoomAccessCoordinatorBridgePresenter.swift index 78a3215ba..623100b3b 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/Coordinator/RoomAccessCoordinatorBridgePresenter.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/Coordinator/RoomAccessCoordinatorBridgePresenter.swift @@ -1,4 +1,5 @@ // +import MatrixSDK // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,7 +15,6 @@ // limitations under the License. // import UIKit -import MatrixSDK @objc protocol RoomAccessCoordinatorBridgePresenterDelegate { func roomAccessCoordinatorBridgePresenterDelegate(_ coordinatorBridgePresenter: RoomAccessCoordinatorBridgePresenter, didCancelRoomWithId roomId: String) @@ -27,7 +27,6 @@ import MatrixSDK /// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. @objcMembers final class RoomAccessCoordinatorBridgePresenter: NSObject { - // MARK: - Properties // MARK: Private @@ -82,7 +81,7 @@ final class RoomAccessCoordinatorBridgePresenter: NSObject { } func dismiss(animated: Bool, completion: (() -> Void)?) { - guard let coordinator = self.coordinator else { + guard let coordinator = coordinator else { return } coordinator.toPresentable().dismiss(animated: animated) { @@ -98,13 +97,11 @@ final class RoomAccessCoordinatorBridgePresenter: NSObject { // MARK: - UIAdaptivePresentationControllerDelegate extension RoomAccessCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate { - func roomNotificationSettingsCoordinatorDidComplete(_ presentationController: UIPresentationController) { - if let roomId = self.coordinator?.currentRoomId { - self.delegate?.roomAccessCoordinatorBridgePresenterDelegate(self, didCancelRoomWithId: roomId) + if let roomId = coordinator?.currentRoomId { + delegate?.roomAccessCoordinatorBridgePresenterDelegate(self, didCancelRoomWithId: roomId) } else { - self.delegate?.roomAccessCoordinatorBridgePresenterDelegate(self, didCancelRoomWithId: self.room.roomId) + delegate?.roomAccessCoordinatorBridgePresenterDelegate(self, didCancelRoomWithId: room.roomId) } } - } diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/Coordinator/RoomAccessCoordinatorParameters.swift b/RiotSwiftUI/Modules/Room/RoomAccess/Coordinator/RoomAccessCoordinatorParameters.swift index a4459ea0a..50e128407 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/Coordinator/RoomAccessCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/Coordinator/RoomAccessCoordinatorParameters.swift @@ -20,7 +20,6 @@ import Foundation /// RoomAccessCoordinator input parameters struct RoomAccessCoordinatorParameters { - /// The Matrix room let room: MXRoom diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Coordinator/RoomAccessTypeChooserCoordinator.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Coordinator/RoomAccessTypeChooserCoordinator.swift index a46542a42..a6e6d2280 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Coordinator/RoomAccessTypeChooserCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Coordinator/RoomAccessTypeChooserCoordinator.swift @@ -23,7 +23,6 @@ struct RoomAccessTypeChooserCoordinatorParameters { } final class RoomAccessTypeChooserCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -70,10 +69,10 @@ final class RoomAccessTypeChooserCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.roomAccessTypeChooserHostingController + roomAccessTypeChooserHostingController } func handleRoomUpgradeResult(_ result: RoomUpgradeCoordinatorResult) { - self.roomAccessTypeChooserViewModel.handleRoomUpgradeResult(result) + roomAccessTypeChooserViewModel.handleRoomUpgradeResult(result) } } diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift index 47037d07f..fd8e74103 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/MockRoomAccessTypeChooserScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation import SwiftUI - /// Using an enum for the screen allows you define the different state cases with /// the relevant associated data for each case. enum MockRoomAccessTypeChooserScreenState: MockScreenState, CaseIterable { @@ -42,7 +41,7 @@ enum MockRoomAccessTypeChooserScreenState: MockScreenState, CaseIterable { service = MockRoomAccessTypeChooserService(accessItems: [ RoomAccessTypeChooserAccessItem(id: .private, isSelected: true, title: VectorL10n.private, detail: VectorL10n.roomAccessSettingsScreenPrivateMessage, badgeText: nil), RoomAccessTypeChooserAccessItem(id: .restricted, isSelected: false, title: VectorL10n.createRoomTypeRestricted, detail: VectorL10n.roomAccessSettingsScreenRestrictedMessage, badgeText: VectorL10n.roomAccessSettingsScreenUpgradeRequired), - RoomAccessTypeChooserAccessItem(id: .public, isSelected: false, title: VectorL10n.public, detail: VectorL10n.roomAccessSettingsScreenPublicMessage, badgeText: nil), + RoomAccessTypeChooserAccessItem(id: .public, isSelected: false, title: VectorL10n.public, detail: VectorL10n.roomAccessSettingsScreenPublicMessage, badgeText: nil) ]) } let viewModel = RoomAccessTypeChooserViewModel(roomAccessTypeChooserService: service) diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/RoomAccessTypeChooserModels.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/RoomAccessTypeChooserModels.swift index 2797adab9..0268cc754 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/RoomAccessTypeChooserModels.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/RoomAccessTypeChooserModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -42,13 +42,6 @@ enum RoomAccessTypeChooserCoordinatorAction { // MARK: - View model -/// Actions to be performed on the `ViewModel` State -enum RoomAccessTypeChooserStateAction { - case updateAccessItems([RoomAccessTypeChooserAccessItem]) - case updateShowUpgradeRoomAlert(Bool) - case updateWaitingMessage(String?) -} - /// Actions sent by the`ViewModel` to the `Coordinator`. enum RoomAccessTypeChooserViewModelAction { case spaceSelection(String, RoomAccessTypeChooserAccessType) diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/RoomAccessTypeChooserViewModel.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/RoomAccessTypeChooserViewModel.swift index 7b97bd753..2bd0d483c 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/RoomAccessTypeChooserViewModel.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/RoomAccessTypeChooserViewModel.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,14 +14,12 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI -typealias RoomAccessTypeChooserViewModelType = StateStoreViewModel +typealias RoomAccessTypeChooserViewModelType = StateStoreViewModel + class RoomAccessTypeChooserViewModel: RoomAccessTypeChooserViewModelType, RoomAccessTypeChooserViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -43,15 +41,19 @@ class RoomAccessTypeChooserViewModel: RoomAccessTypeChooserViewModelType, RoomAc private static func defaultState(roomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol) -> RoomAccessTypeChooserViewState { let bindings = RoomAccessTypeChooserViewModelBindings( showUpgradeRoomAlert: roomAccessTypeChooserService.roomUpgradeRequiredSubject.value, - waitingMessage: roomAccessTypeChooserService.waitingMessageSubject.value, isLoading: roomAccessTypeChooserService.waitingMessageSubject.value != nil) + waitingMessage: roomAccessTypeChooserService.waitingMessageSubject.value, isLoading: roomAccessTypeChooserService.waitingMessageSubject.value != nil + ) return RoomAccessTypeChooserViewState(accessItems: roomAccessTypeChooserService.accessItemsSubject.value, bindings: bindings) } private func startObservingService() { - let accessTypePublisher = roomAccessTypeChooserService.accessItemsSubject - .map(RoomAccessTypeChooserStateAction.updateAccessItems) - .eraseToAnyPublisher() - dispatch(actionPublisher: accessTypePublisher) + roomAccessTypeChooserService + .accessItemsSubject + .sink(receiveValue: { [weak self] accessItems in + self?.state.accessItems = accessItems + }) + .store(in: &cancellables) + roomAccessTypeChooserService .roomUpgradeRequiredSubject .sink { [weak self] isUpgradeRequired in @@ -60,10 +62,13 @@ class RoomAccessTypeChooserViewModel: RoomAccessTypeChooserViewModelType, RoomAc } } .store(in: &cancellables) - let waitingMessagePublisher = roomAccessTypeChooserService.waitingMessageSubject - .map(RoomAccessTypeChooserStateAction.updateWaitingMessage) - .eraseToAnyPublisher() - dispatch(actionPublisher: waitingMessagePublisher) + + roomAccessTypeChooserService.waitingMessageSubject + .sink(receiveValue: { [weak self] message in + self?.state.bindings.waitingMessage = message + self?.state.bindings.isLoading = message != nil + }) + .store(in: &cancellables) } // MARK: - Public @@ -78,19 +83,7 @@ class RoomAccessTypeChooserViewModel: RoomAccessTypeChooserViewModelType, RoomAc cancel() } } - - override class func reducer(state: inout RoomAccessTypeChooserViewState, action: RoomAccessTypeChooserStateAction) { - switch action { - case .updateAccessItems(let accessItems): - state.accessItems = accessItems - case .updateShowUpgradeRoomAlert(let show): - state.bindings.showUpgradeRoomAlert = show - case .updateWaitingMessage(let message): - state.bindings.waitingMessage = message - state.bindings.isLoading = message != nil - } - } - + func handleRoomUpgradeResult(_ result: RoomUpgradeCoordinatorResult) { switch result { case .cancel(let roomId): @@ -130,7 +123,7 @@ class RoomAccessTypeChooserViewModel: RoomAccessTypeChooserViewModelType, RoomAc private func didSelect(accessType: RoomAccessTypeChooserAccessType) { roomAccessTypeChooserService.updateSelection(with: accessType) - if accessType == .restricted && !roomAccessTypeChooserService.roomUpgradeRequiredSubject.value { + if accessType == .restricted, !roomAccessTypeChooserService.roomUpgradeRequiredSubject.value { callback?(.spaceSelection(roomAccessTypeChooserService.currentRoomId, .restricted)) } } diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/RoomAccessTypeChooserViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/RoomAccessTypeChooserViewModelProtocol.swift index 92b0b430a..8fdbc6f11 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/RoomAccessTypeChooserViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/RoomAccessTypeChooserViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/MatrixSDK/RoomAccessTypeChooserService.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/MatrixSDK/RoomAccessTypeChooserService.swift index 1058f6b3b..ec307ca7f 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/MatrixSDK/RoomAccessTypeChooserService.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/MatrixSDK/RoomAccessTypeChooserService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +14,11 @@ // limitations under the License. // -import Foundation import Combine +import Foundation import MatrixSDK class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol { - // MARK: - Properties // MARK: Private @@ -40,6 +39,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol { accessItemsSubject.send(accessItems) } } + private(set) var selectedType: RoomAccessTypeChooserAccessType = .private { didSet { for (index, item) in accessItems.enumerated() { @@ -48,6 +48,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol { accessItemsSubject.send(accessItems) } } + private var roomJoinRule: MXRoomJoinRule = .private private var currentOperation: MXHTTPOperation? @@ -67,8 +68,8 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol { self.roomId = roomId self.allowsRoomUpgrade = allowsRoomUpgrade self.session = session - self.currentRoomId = roomId - self.versionOverride = session.homeserverCapabilitiesService.versionOverrideForFeature(.restricted) + currentRoomId = roomId + versionOverride = session.homeserverCapabilitiesService.versionOverrideForFeature(.restricted) roomUpgradeRequiredSubject = CurrentValueSubject(false) waitingMessageSubject = CurrentValueSubject(nil) @@ -92,7 +93,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol { self.selectedType = selectedType if selectedType == .restricted { - if roomUpgradeRequired && roomUpgradeRequiredSubject.value == false { + if roomUpgradeRequired, roomUpgradeRequiredSubject.value == false { roomUpgradeRequiredSubject.send(true) } } @@ -108,14 +109,14 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol { let _joinRule: MXRoomJoinRule? - switch self.selectedType { + switch selectedType { case .private: _joinRule = .invite case .public: _joinRule = .public case .restricted: _joinRule = nil - if roomUpgradeRequired && roomUpgradeRequiredSubject.value == false { + if roomUpgradeRequired, roomUpgradeRequiredSubject.value == false { roomUpgradeRequiredSubject.send(true) } else { completion() @@ -123,7 +124,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol { } if let joinRule = _joinRule { - self.waitingMessageSubject.send(VectorL10n.roomAccessSettingsScreenSettingRoomAccess) + waitingMessageSubject.send(VectorL10n.roomAccessSettingsScreenSettingRoomAccess) room.setJoinRule(joinRule) { [weak self] response in guard let self = self else { return } @@ -140,7 +141,7 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol { } func updateRoomId(with roomId: String) { - self.currentRoomId = roomId + currentRoomId = roomId readRoomState() } @@ -148,17 +149,17 @@ class RoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol { private func setupAccessItems() { guard let spaceService = session.spaceService, let ancestors = spaceService.ancestorsPerRoomId[currentRoomId], !ancestors.isEmpty, allowsRoomUpgrade || !roomUpgradeRequired else { - self.accessItems = [ + accessItems = [ RoomAccessTypeChooserAccessItem(id: .private, isSelected: false, title: VectorL10n.private, detail: VectorL10n.roomAccessSettingsScreenPrivateMessage, badgeText: nil), - RoomAccessTypeChooserAccessItem(id: .public, isSelected: false, title: VectorL10n.public, detail: VectorL10n.roomAccessSettingsScreenPublicMessage, badgeText: nil), + RoomAccessTypeChooserAccessItem(id: .public, isSelected: false, title: VectorL10n.public, detail: VectorL10n.roomAccessSettingsScreenPublicMessage, badgeText: nil) ] return } - self.accessItems = [ + accessItems = [ RoomAccessTypeChooserAccessItem(id: .private, isSelected: false, title: VectorL10n.private, detail: VectorL10n.roomAccessSettingsScreenPrivateMessage, badgeText: nil), RoomAccessTypeChooserAccessItem(id: .restricted, isSelected: false, title: VectorL10n.createRoomTypeRestricted, detail: VectorL10n.roomAccessSettingsScreenRestrictedMessage, badgeText: roomUpgradeRequired ? VectorL10n.roomAccessSettingsScreenUpgradeRequired : VectorL10n.roomAccessSettingsScreenEditSpaces), - RoomAccessTypeChooserAccessItem(id: .public, isSelected: false, title: VectorL10n.public, detail: VectorL10n.roomAccessSettingsScreenPublicMessage, badgeText: nil), + RoomAccessTypeChooserAccessItem(id: .public, isSelected: false, title: VectorL10n.public, detail: VectorL10n.roomAccessSettingsScreenPublicMessage, badgeText: nil) ] accessItemsSubject.send(accessItems) diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/Mock/MockRoomAccessTypeChooserService.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/Mock/MockRoomAccessTypeChooserService.swift index 5bd115685..f0e4a3b11 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/Mock/MockRoomAccessTypeChooserService.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/Mock/MockRoomAccessTypeChooserService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,15 +14,14 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class MockRoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol { - static let mockAccessItems: [RoomAccessTypeChooserAccessItem] = [ RoomAccessTypeChooserAccessItem(id: .private, isSelected: true, title: VectorL10n.private, detail: VectorL10n.roomAccessSettingsScreenPrivateMessage, badgeText: nil), RoomAccessTypeChooserAccessItem(id: .restricted, isSelected: false, title: VectorL10n.createRoomTypeRestricted, detail: VectorL10n.roomAccessSettingsScreenRestrictedMessage, badgeText: VectorL10n.roomAccessSettingsScreenUpgradeRequired), - RoomAccessTypeChooserAccessItem(id: .public, isSelected: false, title: VectorL10n.public, detail: VectorL10n.roomAccessSettingsScreenPublicMessage, badgeText: nil), + RoomAccessTypeChooserAccessItem(id: .public, isSelected: false, title: VectorL10n.public, detail: VectorL10n.roomAccessSettingsScreenPublicMessage, badgeText: nil) ] private(set) var accessItemsSubject: CurrentValueSubject<[RoomAccessTypeChooserAccessItem], Never> @@ -31,9 +30,9 @@ class MockRoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol { private(set) var errorSubject: CurrentValueSubject private(set) var selectedType: RoomAccessTypeChooserAccessType = .private - var currentRoomId: String = "!aaabaa:matrix.org" + var currentRoomId = "!aaabaa:matrix.org" var versionOverride: String? { - return "9" + "9" } init(accessItems: [RoomAccessTypeChooserAccessItem] = mockAccessItems) { @@ -44,18 +43,14 @@ class MockRoomAccessTypeChooserService: RoomAccessTypeChooserServiceProtocol { } func simulateUpdate(accessItems: [RoomAccessTypeChooserAccessItem]) { - self.accessItemsSubject.send(accessItems) + accessItemsSubject.send(accessItems) } - func updateSelection(with selectedType: RoomAccessTypeChooserAccessType) { - - } + func updateSelection(with selectedType: RoomAccessTypeChooserAccessType) { } func updateRoomId(with roomId: String) { currentRoomId = roomId } - func applySelection(completion: @escaping () -> Void) { - - } + func applySelection(completion: @escaping () -> Void) { } } diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/RoomAccessTypeChooserServiceProtocol.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/RoomAccessTypeChooserServiceProtocol.swift index b96bb2ad0..3f4935e6d 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/RoomAccessTypeChooserServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Service/RoomAccessTypeChooserServiceProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation protocol RoomAccessTypeChooserServiceProtocol { var accessItemsSubject: CurrentValueSubject<[RoomAccessTypeChooserAccessItem], Never> { get } diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/UI/RoomAccessTypeChooserUITests.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/UI/RoomAccessTypeChooserUITests.swift index d6f5b3dbb..d7203387b 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/UI/RoomAccessTypeChooserUITests.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/UI/RoomAccessTypeChooserUITests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class RoomAccessTypeChooserUITests: MockScreenTestCase { // Tests to be implemented. diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/Unit/RoomAccessTypeChooserViewModelTests.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/Unit/RoomAccessTypeChooserViewModelTests.swift index 7eda5de05..f7522b442 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/Unit/RoomAccessTypeChooserViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/Test/Unit/RoomAccessTypeChooserViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +14,9 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI -class RoomAccessTypeChooserViewModelTests: XCTestCase { - -} +class RoomAccessTypeChooserViewModelTests: XCTestCase { } diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooser.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooser.swift index 9786273d3..f7d076d6d 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooser.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooser.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct RoomAccessTypeChooser: View { - // MARK: - Properties // MARK: Private @@ -55,7 +54,7 @@ struct RoomAccessTypeChooser: View { @ViewBuilder private var listContent: some View { - ScrollView{ + ScrollView { VStack(alignment: .leading) { Text(VectorL10n.roomAccessSettingsScreenTitle) .foregroundColor(theme.colors.primaryContent) @@ -84,7 +83,6 @@ struct RoomAccessTypeChooser: View { // MARK: - Previews struct RoomAccessTypeChooser_Previews: PreviewProvider { - static let stateRenderer = MockRoomAccessTypeChooserScreenState.stateRenderer static var previews: some View { stateRenderer.screenGroup(addNavigation: true) diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooserRow.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooserRow.swift index a38cd0efe..b5b3776c1 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooserRow.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomAccessTypeChooser/View/RoomAccessTypeChooserRow.swift @@ -17,7 +17,6 @@ import SwiftUI struct RoomAccessTypeChooserRow: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/Coordinator/RoomRestrictedAccessSpaceChooserViewProvider.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/Coordinator/RoomRestrictedAccessSpaceChooserViewProvider.swift index 389716a8f..fd1b5c71c 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/Coordinator/RoomRestrictedAccessSpaceChooserViewProvider.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/Coordinator/RoomRestrictedAccessSpaceChooserViewProvider.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI class RoomRestrictedAccessSpaceChooserViewProvider: MatrixItemChooserCoordinatorViewProvider { - private let navTitle: String? init(navTitle: String?) { @@ -25,6 +24,6 @@ class RoomRestrictedAccessSpaceChooserViewProvider: MatrixItemChooserCoordinator } func view(with viewModel: MatrixItemChooserViewModelType.Context) -> AnyView { - return AnyView(RoomRestrictedAccessSpaceChooserSelector(viewModel: viewModel, navTitle: navTitle)) + AnyView(RoomRestrictedAccessSpaceChooserSelector(viewModel: viewModel, navTitle: navTitle)) } } diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/Service/MatrixSDK/RoomRestrictedAccessSpaceChooserItemsProcessor.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/Service/MatrixSDK/RoomRestrictedAccessSpaceChooserItemsProcessor.swift index 15d334091..679d41016 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/Service/MatrixSDK/RoomRestrictedAccessSpaceChooserItemsProcessor.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/Service/MatrixSDK/RoomRestrictedAccessSpaceChooserItemsProcessor.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation class RoomRestrictedAccessSpaceChooserItemsProcessor: MatrixItemChooserProcessorProtocol { - // MARK: Private private let roomId: String @@ -28,7 +27,7 @@ class RoomRestrictedAccessSpaceChooserItemsProcessor: MatrixItemChooserProcessor init(roomId: String, session: MXSession) { self.roomId = roomId self.session = session - self.dataSource = MatrixItemChooserRoomRestrictedAllowedParentsDataSource(roomId: roomId) + dataSource = MatrixItemChooserRoomRestrictedAllowedParentsDataSource(roomId: roomId) } // MARK: MatrixItemChooserSelectionProcessorProtocol @@ -50,7 +49,7 @@ class RoomRestrictedAccessSpaceChooserItemsProcessor: MatrixItemChooserProcessor } } - func isItemIncluded(_ item: (MatrixListItemData)) -> Bool { - return true + func isItemIncluded(_ item: MatrixListItemData) -> Bool { + true } } diff --git a/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/View/RoomRestrictedAccessSpaceChooserSelector.swift b/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/View/RoomRestrictedAccessSpaceChooserSelector.swift index 3b52c9e41..25019eb15 100644 --- a/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/View/RoomRestrictedAccessSpaceChooserSelector.swift +++ b/RiotSwiftUI/Modules/Room/RoomAccess/RoomRestrictedAccessSpaceChooser/View/RoomRestrictedAccessSpaceChooserSelector.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct RoomRestrictedAccessSpaceChooserSelector: View { - // MARK: Properties @ObservedObject var viewModel: MatrixItemChooserViewModel.Context @@ -51,5 +50,4 @@ struct RoomRestrictedAccessSpaceChooserSelector: View { } } } - } diff --git a/RiotSwiftUI/Modules/Room/RoomSuggestion/Coordinator/RoomSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/RoomSuggestion/Coordinator/RoomSuggestionCoordinator.swift index ffdfc5e3f..e18ca2bdf 100644 --- a/RiotSwiftUI/Modules/Room/RoomSuggestion/Coordinator/RoomSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/RoomSuggestion/Coordinator/RoomSuggestionCoordinator.swift @@ -24,7 +24,6 @@ enum RoomSuggestionCoordinatorCoordinatorAction { @objcMembers final class RoomSuggestionCoordinator: Coordinator { - // MARK: - Properties // MARK: Private @@ -32,7 +31,7 @@ final class RoomSuggestionCoordinator: Coordinator { private let parameters: RoomSuggestionCoordinatorParameters private var navigationRouter: NavigationRouterType { - return self.parameters.navigationRouter + parameters.navigationRouter } // MARK: Public @@ -46,31 +45,30 @@ final class RoomSuggestionCoordinator: Coordinator { init(parameters: RoomSuggestionCoordinatorParameters) { self.parameters = parameters - } + } // MARK: - Public - func start() { MXLog.debug("[RoomSuggestionCoordinator] did start.") - let rootCoordinator = self.createRoomSuggestionSpaceChooser() + let rootCoordinator = createRoomSuggestionSpaceChooser() rootCoordinator.start() - self.add(childCoordinator: rootCoordinator) + add(childCoordinator: rootCoordinator) - if self.navigationRouter.modules.isEmpty == false { - self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in + if navigationRouter.modules.isEmpty == false { + navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in self?.remove(childCoordinator: rootCoordinator) }) } else { - self.navigationRouter.setRootModule(rootCoordinator) { [weak self] in + navigationRouter.setRootModule(rootCoordinator) { [weak self] in self?.remove(childCoordinator: rootCoordinator) } } } func toPresentable() -> UIViewController { - return self.navigationRouter.toPresentable() + navigationRouter.toPresentable() } // MARK: - Private @@ -78,7 +76,7 @@ final class RoomSuggestionCoordinator: Coordinator { func pushScreen(with coordinator: Coordinator & Presentable) { add(childCoordinator: coordinator) - self.navigationRouter.push(coordinator, animated: true, popCompletion: { [weak self] in + navigationRouter.push(coordinator, animated: true, popCompletion: { [weak self] in self?.remove(childCoordinator: coordinator) }) @@ -91,7 +89,8 @@ final class RoomSuggestionCoordinator: Coordinator { title: VectorL10n.roomSuggestionSettingsScreenTitle, detail: VectorL10n.roomSuggestionSettingsScreenMessage, viewProvider: RoomSuggestionSpaceChooserViewProvider(navTitle: VectorL10n.roomAccessSettingsScreenNavTitle), - itemsProcessor: RoomSuggestionSpaceChooserItemsProcessor(roomId: parameters.room.roomId, session: parameters.room.mxSession)) + itemsProcessor: RoomSuggestionSpaceChooserItemsProcessor(roomId: parameters.room.roomId, session: parameters.room.mxSession) + ) let coordinator = MatrixItemChooserCoordinator(parameters: paramaters) coordinator.completion = { [weak self] result in guard let self = self else { return } @@ -107,5 +106,4 @@ final class RoomSuggestionCoordinator: Coordinator { } return coordinator } - } diff --git a/RiotSwiftUI/Modules/Room/RoomSuggestion/Coordinator/RoomSuggestionCoordinatorBridgePresenter.swift b/RiotSwiftUI/Modules/Room/RoomSuggestion/Coordinator/RoomSuggestionCoordinatorBridgePresenter.swift index a6bfa2ee5..7c1e71c02 100644 --- a/RiotSwiftUI/Modules/Room/RoomSuggestion/Coordinator/RoomSuggestionCoordinatorBridgePresenter.swift +++ b/RiotSwiftUI/Modules/Room/RoomSuggestion/Coordinator/RoomSuggestionCoordinatorBridgePresenter.swift @@ -26,7 +26,6 @@ import UIKit /// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. @objcMembers final class RoomSuggestionCoordinatorBridgePresenter: NSObject { - // MARK: - Properties // MARK: Private @@ -70,7 +69,7 @@ final class RoomSuggestionCoordinatorBridgePresenter: NSObject { } func dismiss(animated: Bool, completion: (() -> Void)?) { - guard let coordinator = self.coordinator else { + guard let coordinator = coordinator else { return } coordinator.toPresentable().dismiss(animated: animated) { @@ -86,9 +85,7 @@ final class RoomSuggestionCoordinatorBridgePresenter: NSObject { // MARK: - UIAdaptivePresentationControllerDelegate extension RoomSuggestionCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate { - func roomNotificationSettingsCoordinatorDidComplete(_ presentationController: UIPresentationController) { - self.delegate?.roomSuggestionCoordinatorBridgePresenterDelegateDidCancel(self) + delegate?.roomSuggestionCoordinatorBridgePresenterDelegateDidCancel(self) } - } diff --git a/RiotSwiftUI/Modules/Room/RoomSuggestion/Coordinator/RoomSuggestionCoordinatorParameters.swift b/RiotSwiftUI/Modules/Room/RoomSuggestion/Coordinator/RoomSuggestionCoordinatorParameters.swift index 1ac2ce0eb..f3bc5580f 100644 --- a/RiotSwiftUI/Modules/Room/RoomSuggestion/Coordinator/RoomSuggestionCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Room/RoomSuggestion/Coordinator/RoomSuggestionCoordinatorParameters.swift @@ -18,7 +18,6 @@ import Foundation /// RoomSuggestionCoordinator input parameters struct RoomSuggestionCoordinatorParameters { - /// The Matrix room let room: MXRoom diff --git a/RiotSwiftUI/Modules/Room/RoomSuggestion/RoomSuggestionSpaceChooser/Coordinator/RoomSuggestionSpaceChooserViewProvider.swift b/RiotSwiftUI/Modules/Room/RoomSuggestion/RoomSuggestionSpaceChooser/Coordinator/RoomSuggestionSpaceChooserViewProvider.swift index a5b261fd0..7c04225ee 100644 --- a/RiotSwiftUI/Modules/Room/RoomSuggestion/RoomSuggestionSpaceChooser/Coordinator/RoomSuggestionSpaceChooserViewProvider.swift +++ b/RiotSwiftUI/Modules/Room/RoomSuggestion/RoomSuggestionSpaceChooser/Coordinator/RoomSuggestionSpaceChooserViewProvider.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI class RoomSuggestionSpaceChooserViewProvider: MatrixItemChooserCoordinatorViewProvider { - private let navTitle: String? init(navTitle: String?) { @@ -25,6 +24,6 @@ class RoomSuggestionSpaceChooserViewProvider: MatrixItemChooserCoordinatorViewPr } func view(with viewModel: MatrixItemChooserViewModelType.Context) -> AnyView { - return AnyView(RoomSuggestionSpaceChooserSelector(viewModel: viewModel, navTitle: navTitle)) + AnyView(RoomSuggestionSpaceChooserSelector(viewModel: viewModel, navTitle: navTitle)) } } diff --git a/RiotSwiftUI/Modules/Room/RoomSuggestion/RoomSuggestionSpaceChooser/Service/MatrixSDK/RoomSuggestionSpaceChooserItemsProcessor.swift b/RiotSwiftUI/Modules/Room/RoomSuggestion/RoomSuggestionSpaceChooser/Service/MatrixSDK/RoomSuggestionSpaceChooserItemsProcessor.swift index 016e17dd1..6a8d664fc 100644 --- a/RiotSwiftUI/Modules/Room/RoomSuggestion/RoomSuggestionSpaceChooser/Service/MatrixSDK/RoomSuggestionSpaceChooserItemsProcessor.swift +++ b/RiotSwiftUI/Modules/Room/RoomSuggestion/RoomSuggestionSpaceChooser/Service/MatrixSDK/RoomSuggestionSpaceChooserItemsProcessor.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,7 +22,6 @@ public enum RoomSuggestionSpaceChooserItemsProcessorError: Int, Error { } class RoomSuggestionSpaceChooserItemsProcessor: MatrixItemChooserProcessorProtocol { - // MARK: Private private let roomId: String @@ -35,7 +34,7 @@ class RoomSuggestionSpaceChooserItemsProcessor: MatrixItemChooserProcessorProtoc init(roomId: String, session: MXSession) { self.roomId = roomId self.session = session - self.dataSource = MatrixItemChooserRoomDirectParentsDataSource(roomId: roomId, preselectionMode: .suggestedRoom) + dataSource = MatrixItemChooserRoomDirectParentsDataSource(roomId: roomId, preselectionMode: .suggestedRoom) } deinit { @@ -43,6 +42,7 @@ class RoomSuggestionSpaceChooserItemsProcessor: MatrixItemChooserProcessorProtoc NotificationCenter.default.removeObserver(observer) } } + // MARK: MatrixItemChooserSelectionProcessorProtocol private(set) var dataSource: MatrixItemChooserDataSource @@ -53,11 +53,11 @@ class RoomSuggestionSpaceChooserItemsProcessor: MatrixItemChooserProcessorProtoc let unselectedItems: [String] let selectedItems: [String] if let preselectedItems = dataSource.preselectedItemIds { - unselectedItems = preselectedItems.compactMap({ itemId in - return !itemsIds.contains(itemId) ? itemId : nil - }) + unselectedItems = preselectedItems.compactMap { itemId in + !itemsIds.contains(itemId) ? itemId : nil + } selectedItems = itemsIds.compactMap { itemId in - return !preselectedItems.contains(itemId) ? itemId : nil + !preselectedItems.contains(itemId) ? itemId : nil } } else { unselectedItems = [] @@ -78,7 +78,7 @@ class RoomSuggestionSpaceChooserItemsProcessor: MatrixItemChooserProcessorProtoc if let firstError = self.computationErrorList.first { completion(.failure(firstError)) } else { - self.didBuildSpaceGraphObserver = NotificationCenter.default.addObserver(forName: MXSpaceService.didBuildSpaceGraph, object: nil, queue: OperationQueue.main) { [weak self] notification in + self.didBuildSpaceGraphObserver = NotificationCenter.default.addObserver(forName: MXSpaceService.didBuildSpaceGraph, object: nil, queue: OperationQueue.main) { [weak self] _ in guard let self = self else { return } if let observer = self.didBuildSpaceGraphObserver { @@ -93,8 +93,8 @@ class RoomSuggestionSpaceChooserItemsProcessor: MatrixItemChooserProcessorProtoc } } - func isItemIncluded(_ item: (MatrixListItemData)) -> Bool { - return true + func isItemIncluded(_ item: MatrixListItemData) -> Bool { + true } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Room/RoomSuggestion/RoomSuggestionSpaceChooser/View/RoomSuggestionSpaceChooserSelector.swift b/RiotSwiftUI/Modules/Room/RoomSuggestion/RoomSuggestionSpaceChooser/View/RoomSuggestionSpaceChooserSelector.swift index d0c1e4e28..4b2f5d2bc 100644 --- a/RiotSwiftUI/Modules/Room/RoomSuggestion/RoomSuggestionSpaceChooser/View/RoomSuggestionSpaceChooserSelector.swift +++ b/RiotSwiftUI/Modules/Room/RoomSuggestion/RoomSuggestionSpaceChooser/View/RoomSuggestionSpaceChooserSelector.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct RoomSuggestionSpaceChooserSelector: View { - // MARK: Properties @ObservedObject var viewModel: MatrixItemChooserViewModel.Context @@ -48,5 +47,4 @@ struct RoomSuggestionSpaceChooserSelector: View { } } } - } diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift index fec7cd3e0..bc89e53cb 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/Coordinator/RoomUpgradeCoordinator.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import SwiftUI import MatrixSDK +import SwiftUI struct RoomUpgradeCoordinatorParameters { let session: MXSession @@ -25,7 +25,6 @@ struct RoomUpgradeCoordinatorParameters { } final class RoomUpgradeCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -53,6 +52,7 @@ final class RoomUpgradeCoordinator: Coordinator, Presentable { } // MARK: - Public + func start() { MXLog.debug("[RoomUpgradeCoordinator] did start.") roomUpgradeViewModel.completion = { [weak self] result in @@ -68,6 +68,6 @@ final class RoomUpgradeCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.roomUpgradeHostingController + roomUpgradeHostingController } } diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift index 0052fd05b..bca1f0ea7 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/MockRoomUpgradeScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,7 +36,7 @@ enum MockRoomUpgradeScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let service: MockRoomUpgradeService switch self { case .initial: diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/RoomUpgradeModels.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/RoomUpgradeModels.swift index 6d74a7f30..37966272e 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/RoomUpgradeModels.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/RoomUpgradeModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/RoomUpgradeViewModel.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/RoomUpgradeViewModel.swift index 4a8b876d2..ea3097399 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/RoomUpgradeViewModel.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/RoomUpgradeViewModel.swift @@ -14,14 +14,12 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI + +typealias RoomUpgradeViewModelType = StateStoreViewModel -typealias RoomUpgradeViewModelType = StateStoreViewModel class RoomUpgradeViewModel: RoomUpgradeViewModelType, RoomUpgradeViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -35,7 +33,7 @@ class RoomUpgradeViewModel: RoomUpgradeViewModelType, RoomUpgradeViewModelProtoc // MARK: - Setup static func makeRoomUpgradeViewModel(roomUpgradeService: RoomUpgradeServiceProtocol) -> RoomUpgradeViewModelProtocol { - return RoomUpgradeViewModel(roomUpgradeService: roomUpgradeService) + RoomUpgradeViewModel(roomUpgradeService: roomUpgradeService) } private init(roomUpgradeService: RoomUpgradeServiceProtocol) { @@ -45,7 +43,7 @@ class RoomUpgradeViewModel: RoomUpgradeViewModelType, RoomUpgradeViewModelProtoc } private static func defaultState(roomUpgradeService: RoomUpgradeServiceProtocol) -> RoomUpgradeViewState { - return RoomUpgradeViewState(waitingMessage: nil, isLoading: false, parentSpaceName: roomUpgradeService.parentSpaceName) + RoomUpgradeViewState(waitingMessage: nil, isLoading: false, parentSpaceName: roomUpgradeService.parentSpaceName) } private func setupObservers() { @@ -53,7 +51,7 @@ class RoomUpgradeViewModel: RoomUpgradeViewModelType, RoomUpgradeViewModelProtoc .upgradingSubject .sink { [weak self] isUpgrading in self?.state.isLoading = isUpgrading - self?.state.waitingMessage = isUpgrading ? VectorL10n.roomAccessSettingsScreenUpgradeAlertUpgrading: nil + self?.state.waitingMessage = isUpgrading ? VectorL10n.roomAccessSettingsScreenUpgradeAlertUpgrading : nil } .store(in: &cancellables) } @@ -65,7 +63,7 @@ class RoomUpgradeViewModel: RoomUpgradeViewModelType, RoomUpgradeViewModelProtoc case .cancel: completion?(.cancel(roomUpgradeService.currentRoomId)) case .done(let autoInviteUsers): - roomUpgradeService.upgradeRoom(autoInviteUsers: autoInviteUsers) { [weak self] success, roomId in + roomUpgradeService.upgradeRoom(autoInviteUsers: autoInviteUsers) { [weak self] success, _ in guard let self = self else { return } if success { self.completion?(.done(self.roomUpgradeService.currentRoomId)) diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/RoomUpgradeViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/RoomUpgradeViewModelProtocol.swift index eb6136d1d..e9dcefdaa 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/RoomUpgradeViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/RoomUpgradeViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol RoomUpgradeViewModelProtocol { - var completion: ((RoomUpgradeViewModelResult) -> Void)? { get set } static func makeRoomUpgradeViewModel(roomUpgradeService: RoomUpgradeServiceProtocol) -> RoomUpgradeViewModelProtocol var context: RoomUpgradeViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/Service/MatrixSDK/RoomUpgradeService.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/Service/MatrixSDK/RoomUpgradeService.swift index 02d9b5b9f..adc01c5ee 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/Service/MatrixSDK/RoomUpgradeService.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/Service/MatrixSDK/RoomUpgradeService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +14,11 @@ // limitations under the License. // -import Foundation import Combine +import Foundation import MatrixSDK class RoomUpgradeService: RoomUpgradeServiceProtocol { - // MARK: - Properties // MARK: Private @@ -37,7 +36,7 @@ class RoomUpgradeService: RoomUpgradeServiceProtocol { private(set) var currentRoomId: String var parentSpaceName: String? { - guard let parentId = self.parentSpaceId else { + guard let parentId = parentSpaceId else { return nil } @@ -53,11 +52,11 @@ class RoomUpgradeService: RoomUpgradeServiceProtocol { init(session: MXSession, roomId: String, parentSpaceId: String?, versionOverride: String) { self.session = session - self.currentRoomId = roomId + currentRoomId = roomId self.parentSpaceId = parentSpaceId self.versionOverride = versionOverride - self.upgradingSubject = CurrentValueSubject(false) - self.errorSubject = CurrentValueSubject(nil) + upgradingSubject = CurrentValueSubject(false) + errorSubject = CurrentValueSubject(nil) } deinit { @@ -70,18 +69,18 @@ class RoomUpgradeService: RoomUpgradeServiceProtocol { func upgradeRoom(autoInviteUsers: Bool, completion: @escaping (Bool, String) -> Void) { upgradingSubject.send(true) - if autoInviteUsers, let room = session.room(withRoomId: self.currentRoomId) { - self.currentOperation = room.members { [weak self] response in + if autoInviteUsers, let room = session.room(withRoomId: currentRoomId) { + currentOperation = room.members { [weak self] response in guard let self = self else { return } switch response { case .success(let members): - let memberIds: [String] = members?.members.compactMap({ member in + let memberIds: [String] = members?.members.compactMap { member in guard member.membership == .join, member.userId != self.session.myUserId else { return nil } return member.userId - }) ?? [] + } ?? [] self.upgradeRoom(to: self.versionOverride, inviteUsers: memberIds, completion: completion) case .failure(let error): self.upgradingSubject.send(false) @@ -89,7 +88,7 @@ class RoomUpgradeService: RoomUpgradeServiceProtocol { } } } else { - self.upgradeRoom(to: versionOverride, inviteUsers: [], completion: completion) + upgradeRoom(to: versionOverride, inviteUsers: [], completion: completion) } } @@ -98,7 +97,7 @@ class RoomUpgradeService: RoomUpgradeServiceProtocol { private func upgradeRoom(to versionOverride: String, inviteUsers userIds: [String], completion: @escaping (Bool, String) -> Void) { // Need to disable graph update during this process as a lot of syncs will occure session.spaceService.graphUpdateEnabled = false - currentOperation = session.matrixRestClient.upgradeRoom(withId: self.currentRoomId, to: versionOverride) { [weak self] response in + currentOperation = session.matrixRestClient.upgradeRoom(withId: currentRoomId, to: versionOverride) { [weak self] response in guard let self = self else { return } switch response { @@ -108,7 +107,7 @@ class RoomUpgradeService: RoomUpgradeServiceProtocol { let parentSpaces = self.session.spaceService.directParentIds(ofRoomWithId: oldRoomId) self.moveRoom(from: oldRoomId, to: replacementRoomId, within: Array(parentSpaces), at: 0) { self.session.spaceService.graphUpdateEnabled = true - self.didBuildSpaceGraphObserver = NotificationCenter.default.addObserver(forName: MXSpaceService.didBuildSpaceGraph, object: nil, queue: OperationQueue.main) { [weak self] notification in + self.didBuildSpaceGraphObserver = NotificationCenter.default.addObserver(forName: MXSpaceService.didBuildSpaceGraph, object: nil, queue: OperationQueue.main) { [weak self] _ in guard let self = self else { return } if let observer = self.didBuildSpaceGraphObserver { @@ -144,7 +143,7 @@ class RoomUpgradeService: RoomUpgradeServiceProtocol { } space.moveChild(withRoomId: roomId, to: newRoomId) { [weak self] response in - guard let self = self else { return } + guard let self = self else { return } if let error = response.error { MXLog.warning("[RoomUpgradeService] moveRoom \(roomId) to \(newRoomId) within \(space.spaceId): failed due to error: \(error)") @@ -158,13 +157,13 @@ class RoomUpgradeService: RoomUpgradeServiceProtocol { /// Recurse to the next index once done. private func inviteUser(from userIds: [String], at index: Int, completion: @escaping (Bool, String) -> Void) { guard index < userIds.count else { - self.upgradingSubject.send(false) + upgradingSubject.send(false) completion(true, currentRoomId) return } currentOperation = session.matrixRestClient.invite(.userId(userIds[index]), toRoom: currentRoomId) { [weak self] response in - guard let self = self else { return } + guard let self = self else { return } self.currentOperation = nil if let error = response.error { diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/Service/Mock/MockRoomUpgradeService.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/Service/Mock/MockRoomUpgradeService.swift index a31619875..aed04fc38 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/Service/Mock/MockRoomUpgradeService.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/Service/Mock/MockRoomUpgradeService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,24 +14,22 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class MockRoomUpgradeService: RoomUpgradeServiceProtocol { - var currentRoomId: String = "!sfdlksjdflkfjds:matrix.org" + var currentRoomId = "!sfdlksjdflkfjds:matrix.org" var errorSubject: CurrentValueSubject var upgradingSubject: CurrentValueSubject var parentSpaceName: String? { - return "Parent space name" + "Parent space name" } init() { - self.errorSubject = CurrentValueSubject(nil) - self.upgradingSubject = CurrentValueSubject(false) + errorSubject = CurrentValueSubject(nil) + upgradingSubject = CurrentValueSubject(false) } - func upgradeRoom(autoInviteUsers: Bool, completion: @escaping (Bool, String) -> Void) { - - } + func upgradeRoom(autoInviteUsers: Bool, completion: @escaping (Bool, String) -> Void) { } } diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/Service/RoomUpgradeServiceProtocol.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/Service/RoomUpgradeServiceProtocol.swift index 49cba3100..20a096d7c 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/Service/RoomUpgradeServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/Service/RoomUpgradeServiceProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation protocol RoomUpgradeServiceProtocol { var currentRoomId: String { get } diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift index 5b59c1c5d..011e870ce 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class RoomUpgradeUITests: MockScreenTestCase { // Tests to be implemented. diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/Unit/RoomUpgradeViewModelTests.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/Unit/RoomUpgradeViewModelTests.swift index 41cf7f572..19f2cfc72 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/Unit/RoomUpgradeViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/Unit/RoomUpgradeViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI @@ -24,7 +24,5 @@ class RoomUpgradeViewModelTests: XCTestCase { var viewModel: RoomUpgradeViewModelProtocol! var context: RoomUpgradeViewModelType.Context! - override func setUpWithError() throws { - } - + override func setUpWithError() throws { } } diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/View/RoomUpgrade.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/View/RoomUpgrade.swift index 83eb51991..76c096531 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/View/RoomUpgrade.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/View/RoomUpgrade.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,10 +17,9 @@ import SwiftUI struct RoomUpgrade: View { - // MARK: - Properties - @State var autoInviteUsers: Bool = true + @State var autoInviteUsers = true // MARK: Private @@ -86,13 +85,13 @@ struct RoomUpgrade: View { .padding(.horizontal, 24) .padding(.vertical, 16) } - .background(RoundedRectangle.init(cornerRadius: 8).foregroundColor(theme.colors.background)) + .background(RoundedRectangle(cornerRadius: 8).foregroundColor(theme.colors.background)) .padding(.horizontal, 20) .frame(minWidth: 0, maxWidth: 500) } private func noteText(_ message: String) -> some View { - return Text(message) + Text(message) .multilineTextAlignment(.center) .font(theme.fonts.subheadline) .foregroundColor(theme.colors.secondaryContent) diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index 7bdbcb77d..a587b23d8 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -14,9 +14,9 @@ // limitations under the License. // -import SwiftUI -import MatrixSDK import Combine +import MatrixSDK +import SwiftUI struct TimelinePollCoordinatorParameters { let session: MXSession @@ -25,7 +25,6 @@ struct TimelinePollCoordinatorParameters { } final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDelegate { - // MARK: - Properties // MARK: Private @@ -81,25 +80,24 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel } // MARK: - Public - func start() { - - } + + func start() { } func toPresentable() -> UIViewController { - return VectorHostingController(rootView: TimelinePollView(viewModel: viewModel.context), - forceZeroSafeAreaInsets: true) + VectorHostingController(rootView: TimelinePollView(viewModel: viewModel.context), + forceZeroSafeAreaInsets: true) } func canEndPoll() -> Bool { - return pollAggregator.poll.isClosed == false + pollAggregator.poll.isClosed == false } func canEditPoll() -> Bool { - return pollAggregator.poll.isClosed == false && pollAggregator.poll.totalAnswerCount == 0 + pollAggregator.poll.isClosed == false && pollAggregator.poll.totalAnswerCount == 0 } func endPoll() { - parameters.room.sendPollEnd(for: parameters.pollStartEvent, threadId: nil, localEcho: nil, success: nil) { [weak self] error in + parameters.room.sendPollEnd(for: parameters.pollStartEvent, threadId: nil, localEcho: nil, success: nil) { [weak self] _ in self?.viewModel.showClosingFailure() } } @@ -110,17 +108,11 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel viewModel.updateWithPollDetails(buildTimelinePollFrom(aggregator.poll)) } - func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { - - } + func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { } - func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { - - } + func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) { } - func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { - - } + func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) { } // MARK: - Private @@ -129,19 +121,19 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel func buildTimelinePollFrom(_ poll: PollProtocol) -> TimelinePollDetails { let answerOptions = poll.answerOptions.map { pollAnswerOption in TimelinePollAnswerOption(id: pollAnswerOption.id, - text: pollAnswerOption.text, - count: pollAnswerOption.count, - winner: pollAnswerOption.isWinner, - selected: pollAnswerOption.isCurrentUserSelection) + text: pollAnswerOption.text, + count: pollAnswerOption.count, + winner: pollAnswerOption.isWinner, + selected: pollAnswerOption.isCurrentUserSelection) } return TimelinePollDetails(question: poll.text, - answerOptions: answerOptions, - closed: poll.isClosed, - totalAnswerCount: poll.totalAnswerCount, - type: pollKindToTimelinePollType(poll.kind), - maxAllowedSelections: poll.maxAllowedSelections, - hasBeenEdited: poll.hasBeenEdited) + answerOptions: answerOptions, + closed: poll.isClosed, + totalAnswerCount: poll.totalAnswerCount, + type: pollKindToTimelinePollType(poll.kind), + maxAllowedSelections: poll.maxAllowedSelections, + hasBeenEdited: poll.hasBeenEdited) } private func pollKindToTimelinePollType(_ kind: PollKind) -> TimelinePollType { diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift index d40e86757..78b1d8ab7 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,9 +22,7 @@ class TimelinePollProvider { var session: MXSession? var coordinatorsForEventIdentifiers = [String: TimelinePollCoordinator]() - private init() { - - } + private init() { } /// Create or retrieve the poll timeline coordinator for this event and return /// a view to be displayed in the timeline @@ -49,6 +47,6 @@ class TimelinePollProvider { /// Retrieve the poll timeline coordinator for the given event or nil if it hasn't been created yet func timelinePollCoordinatorForEventIdentifier(_ eventIdentifier: String) -> TimelinePollCoordinator? { - return coordinatorsForEventIdentifiers[eventIdentifier] + coordinatorsForEventIdentifiers[eventIdentifier] } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift index 98b520ea7..5e7eaceef 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class TimelinePollUITests: MockScreenTestCase { func testOpenDisclosedPoll() { diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift index 247f35b0d..0e102dc39 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index 619088c25..528ad7c17 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,7 @@ import Foundation import SwiftUI -typealias TimelinePollViewModelCallback = ((TimelinePollViewModelResult) -> Void) +typealias TimelinePollViewModelCallback = (TimelinePollViewModelResult) -> Void enum TimelinePollViewAction { case selectAnswerOptionWithIdentifier(String) @@ -63,7 +63,7 @@ struct TimelinePollDetails { var totalAnswerCount: UInt var type: TimelinePollType var maxAllowedSelections: UInt - var hasBeenEdited: Bool = true + var hasBeenEdited = true init(question: String, answerOptions: [TimelinePollAnswerOption], closed: Bool, @@ -81,7 +81,7 @@ struct TimelinePollDetails { } var hasCurrentUserVoted: Bool { - answerOptions.filter { $0.selected == true}.count > 0 + answerOptions.filter { $0.selected == true }.count > 0 } var shouldDiscloseResults: Bool { diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift index 0ac107a6e..01fb82c4a 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,16 +27,16 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { TimelinePollDetails.self } - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let answerOptions = [TimelinePollAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false), TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true), TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)] let poll = TimelinePollDetails(question: "Question", answerOptions: answerOptions, - closed: (self == .closedDisclosed || self == .closedUndisclosed ? true : false), + closed: self == .closedDisclosed || self == .closedUndisclosed ? true : false, totalAnswerCount: 20, - type: (self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed), + type: self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed, maxAllowedSelections: 1, hasBeenEdited: false) diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift index 99d15980c..a86862cf4 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift @@ -14,14 +14,12 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI + +typealias TimelinePollViewModelType = StateStoreViewModel -typealias TimelinePollViewModelType = StateStoreViewModel class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -40,14 +38,13 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro override func process(viewAction: TimelinePollViewAction) { switch viewAction { - // Update local state. An update will be pushed from the coordinator once sent. case .selectAnswerOptionWithIdentifier(let identifier): guard !state.poll.closed else { return } - if (state.poll.maxAllowedSelections == 1) { + if state.poll.maxAllowedSelections == 1 { updateSingleSelectPollLocalState(selectedAnswerIdentifier: identifier, callback: completion) } else { updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion) @@ -98,12 +95,12 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro let isDeselecting = selectedAnswerOptions.filter { $0.id == selectedAnswerIdentifier }.count > 0 - if !isDeselecting && selectedAnswerOptions.count >= state.poll.maxAllowedSelections { + if !isDeselecting, selectedAnswerOptions.count >= state.poll.maxAllowedSelections { return } state.poll.answerOptions.updateEach { answerOption in - if (answerOption.id != selectedAnswerIdentifier) { + if answerOption.id != selectedAnswerIdentifier { return } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift index 7bd9184eb..492f7f7a3 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift index 5a3498aa9..aaaba7c37 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct TimelinePollAnswerOptionButton: View { - // MARK: - Properties // MARK: Private @@ -47,7 +46,6 @@ struct TimelinePollAnswerOptionButton: View { var answerOptionLabel: some View { VStack(alignment: .leading, spacing: 12.0) { HStack(alignment: .top, spacing: 8.0) { - if !poll.closed { Image(uiImage: answerOption.selected ? Asset.Images.pollCheckboxSelected.image : Asset.Images.pollCheckboxDefault.image) } @@ -56,7 +54,7 @@ struct TimelinePollAnswerOptionButton: View { .font(theme.fonts.body) .foregroundColor(theme.colors.primaryContent) - if poll.closed && answerOption.winner { + if poll.closed, answerOption.winner { Spacer() Image(uiImage: Asset.Images.pollWinnerIcon.image) } @@ -69,7 +67,7 @@ struct TimelinePollAnswerOptionButton: View { .progressViewStyle(LinearProgressViewStyle()) .scaleEffect(x: 1.0, y: 1.2, anchor: .center) - if (poll.shouldDiscloseResults) { + if poll.shouldDiscloseResults { Text(answerOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(answerOption.count))) .font(theme.fonts.footnote) .foregroundColor(poll.closed && answerOption.winner ? theme.colors.accent : theme.colors.secondaryContent) @@ -107,33 +105,33 @@ struct TimelinePollAnswerOptionButton_Previews: PreviewProvider { VStack { TimelinePollAnswerOptionButton(poll: buildPoll(closed: false, type: type), answerOption: buildAnswerOption(selected: false), - action: {}) + action: { }) TimelinePollAnswerOptionButton(poll: buildPoll(closed: false, type: type), answerOption: buildAnswerOption(selected: true), - action: {}) + action: { }) TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), answerOption: buildAnswerOption(selected: false, winner: false), - action: {}) + action: { }) TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), answerOption: buildAnswerOption(selected: false, winner: true), - action: {}) + action: { }) TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), answerOption: buildAnswerOption(selected: true, winner: false), - action: {}) + action: { }) TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), answerOption: buildAnswerOption(selected: true, winner: true), - action: {}) + action: { }) let longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), answerOption: buildAnswerOption(text: longText, selected: true, winner: true), - action: {}) + action: { }) } } } @@ -141,12 +139,12 @@ struct TimelinePollAnswerOptionButton_Previews: PreviewProvider { static func buildPoll(closed: Bool, type: TimelinePollType) -> TimelinePollDetails { TimelinePollDetails(question: "", - answerOptions: [], - closed: closed, - totalAnswerCount: 100, - type: type, - maxAllowedSelections: 1, - hasBeenEdited: false) + answerOptions: [], + closed: closed, + totalAnswerCount: 100, + type: type, + maxAllowedSelections: 1, + hasBeenEdited: false) } static func buildAnswerOption(text: String = "Test", selected: Bool, winner: Bool = false) -> TimelinePollAnswerOption { diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift index 0b2cf80e8..ff2ce2541 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct TimelinePollView: View { - // MARK: - Properties // MARK: Private @@ -32,7 +31,6 @@ struct TimelinePollView: View { let poll = viewModel.viewState.poll VStack(alignment: .leading, spacing: 16.0) { - Text(poll.question) .font(theme.fonts.bodySB) .foregroundColor(theme.colors.primaryContent) + @@ -77,12 +75,12 @@ struct TimelinePollView: View { return VectorL10n.pollTimelineTotalNoVotes case 1: return (poll.hasCurrentUserVoted || poll.type == .undisclosed ? - VectorL10n.pollTimelineTotalOneVote : - VectorL10n.pollTimelineTotalOneVoteNotVoted) + VectorL10n.pollTimelineTotalOneVote : + VectorL10n.pollTimelineTotalOneVoteNotVoted) default: return (poll.hasCurrentUserVoted || poll.type == .undisclosed ? - VectorL10n.pollTimelineTotalVotes(Int(poll.totalAnswerCount)) : - VectorL10n.pollTimelineTotalVotesNotVoted(Int(poll.totalAnswerCount))) + VectorL10n.pollTimelineTotalVotes(Int(poll.totalAnswerCount)) : + VectorL10n.pollTimelineTotalVotesNotVoted(Int(poll.totalAnswerCount))) } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index 4d10fce83..c3812b5e8 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -14,12 +14,14 @@ // limitations under the License. // +import Combine import Foundation -import UIKit import SwiftUI +import UIKit protocol UserSuggestionCoordinatorDelegate: AnyObject { func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) + func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat) } struct UserSuggestionCoordinatorParameters { @@ -28,17 +30,18 @@ struct UserSuggestionCoordinatorParameters { } final class UserSuggestionCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private private let parameters: UserSuggestionCoordinatorParameters - private var userSuggestionHostingController: UIViewController + private var userSuggestionHostingController: UIHostingController private var userSuggestionService: UserSuggestionServiceProtocol private var userSuggestionViewModel: UserSuggestionViewModelProtocol private var roomMemberProvider: UserSuggestionCoordinatorRoomMemberProvider + + private var cancellables = Set() // MARK: Public @@ -77,30 +80,61 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { self.delegate?.userSuggestionCoordinator(self, didRequestMentionForMember: member, textTrigger: self.userSuggestionService.currentTextTrigger) } } + + userSuggestionService.items.sink { [weak self] _ in + guard let self = self else { return } + self.delegate?.userSuggestionCoordinator(self, + didUpdateViewHeight: self.calculateViewHeight()) + }.store(in: &cancellables) } func processTextMessage(_ textMessage: String) { userSuggestionService.processTextMessage(textMessage) } - + // MARK: - Public - func start() { - - } + + func start() { } func toPresentable() -> UIViewController { - return self.userSuggestionHostingController + userSuggestionHostingController + } + + // MARK: - Private + + private func calculateViewHeight() -> CGFloat { + let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService) + let view = UserSuggestionList(viewModel: viewModel.context) + .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) + + let controller = VectorHostingController(rootView: view) + guard let view = controller.view else { + return 0 + } + view.isHidden = true + + toPresentable().view.addSubview(view) + controller.didMove(toParent: toPresentable()) + + view.setNeedsLayout() + view.layoutIfNeeded() + + let result = view.intrinsicContentSize.height + + controller.didMove(toParent: nil) + view.removeFromSuperview() + + return result } } private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderProtocol { - private let room: MXRoom var roomMembers: [MXRoomMember] = [] init(room: MXRoom) { - self.room = room; + self.room = room } func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) { diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift index 1fd88e25b..c5b68eeee 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorBridge.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,14 +19,14 @@ import Foundation @objc protocol UserSuggestionCoordinatorBridgeDelegate: AnyObject { func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) + func userSuggestionCoordinatorBridge(_ coordinator: UserSuggestionCoordinatorBridge, didUpdateViewHeight height: CGFloat) } @objcMembers final class UserSuggestionCoordinatorBridge: NSObject { - - private var _userSuggestionCoordinator: Any? = nil + private var _userSuggestionCoordinator: Any? fileprivate var userSuggestionCoordinator: UserSuggestionCoordinator { - return _userSuggestionCoordinator as! UserSuggestionCoordinator + _userSuggestionCoordinator as! UserSuggestionCoordinator } weak var delegate: UserSuggestionCoordinatorBridgeDelegate? @@ -34,7 +34,7 @@ final class UserSuggestionCoordinatorBridge: NSObject { init(mediaManager: MXMediaManager, room: MXRoom) { let parameters = UserSuggestionCoordinatorParameters(mediaManager: mediaManager, room: room) let userSuggestionCoordinator = UserSuggestionCoordinator(parameters: parameters) - self._userSuggestionCoordinator = userSuggestionCoordinator + _userSuggestionCoordinator = userSuggestionCoordinator super.init() @@ -42,11 +42,11 @@ final class UserSuggestionCoordinatorBridge: NSObject { } func processTextMessage(_ textMessage: String) { - return self.userSuggestionCoordinator.processTextMessage(textMessage) + userSuggestionCoordinator.processTextMessage(textMessage) } func toPresentable() -> UIViewController? { - return self.userSuggestionCoordinator.toPresentable() + userSuggestionCoordinator.toPresentable() } } @@ -54,4 +54,8 @@ extension UserSuggestionCoordinatorBridge: UserSuggestionCoordinatorDelegate { func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) { delegate?.userSuggestionCoordinatorBridge(self, didRequestMentionForMember: member, textTrigger: textTrigger) } + + func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didUpdateViewHeight height: CGFloat) { + delegate?.userSuggestionCoordinatorBridge(self, didUpdateViewHeight: height) + } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift index cb135532f..bf8fa00a5 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation struct RoomMembersProviderMember { var userId: String @@ -34,7 +34,6 @@ struct UserSuggestionServiceItem: UserSuggestionItemProtocol { } class UserSuggestionService: UserSuggestionServiceProtocol { - // MARK: - Properties // MARK: Private @@ -58,7 +57,7 @@ class UserSuggestionService: UserSuggestionServiceProtocol { init(roomMemberProvider: RoomMembersProviderProtocol, shouldDebounce: Bool = true) { self.roomMemberProvider = roomMemberProvider - if (shouldDebounce) { + if shouldDebounce { currentTextTriggerSubject .debounce(for: 0.5, scheduler: RunLoop.main) .removeDuplicates() @@ -79,12 +78,12 @@ class UserSuggestionService: UserSuggestionServiceProtocol { let lastComponent = textMessage.components(separatedBy: .whitespaces).last, lastComponent.prefix(while: { $0 == "@" }).count == 1 // Partial username should start with one and only one "@" character else { - self.items.send([]) - self.currentTextTriggerSubject.send(nil) + items.send([]) + currentTextTriggerSubject.send(nil) return } - self.currentTextTriggerSubject.send(lastComponent) + currentTextTriggerSubject.send(lastComponent) } // MARK: - Private @@ -105,12 +104,12 @@ class UserSuggestionService: UserSuggestionServiceProtocol { UserSuggestionServiceItem(userId: member.userId, displayName: member.displayName, avatarUrl: member.avatarUrl) } - self.items.send(self.suggestionItems.filter({ userSuggestion in + self.items.send(self.suggestionItems.filter { userSuggestion in let containedInUsername = userSuggestion.userId.lowercased().contains(partialName.lowercased()) let containedInDisplayName = (userSuggestion.displayName ?? "").lowercased().contains(partialName.lowercased()) return (containedInUsername || containedInDisplayName) - })) + }) } } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift index e1ee70a9c..81edb0df9 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation protocol UserSuggestionItemProtocol: Avatarable { var userId: String { get } @@ -24,7 +24,6 @@ protocol UserSuggestionItemProtocol: Avatarable { } protocol UserSuggestionServiceProtocol { - var items: CurrentValueSubject<[UserSuggestionItemProtocol], Never> { get } var currentTextTrigger: String? { get } @@ -38,6 +37,7 @@ extension UserSuggestionItemProtocol { var mxContentUri: String? { avatarUrl } + var matrixItemId: String { userId } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift index 53e3c883d..23b204083 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class UserSuggestionUITests: MockScreenTestCase { func testUserSuggestionScreen() throws { diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift index 552eb4939..b32580c8d 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,13 +14,12 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI class UserSuggestionServiceTests: XCTestCase { - var service: UserSuggestionService? override func setUp() { @@ -107,12 +106,11 @@ class UserSuggestionServiceTests: XCTestCase { extension UserSuggestionServiceTests: RoomMembersProviderProtocol { func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) { - let users = [("Alice", "@alice:matrix.org"), ("Bob", "@bob:matrix.org")] - members(users.map({ user in + members(users.map { user in RoomMembersProviderMember(userId: user.1, displayName: user.0, avatarUrl: "") - })) + }) } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift index 8bc107a3c..d4e984f88 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift index f1b6999f3..f8a8acade 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,13 +20,13 @@ import SwiftUI enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { case multipleResults - static private var members: [RoomMembersProviderMember]! + private static var members: [RoomMembersProviderMember]! var screenType: Any.Type { UserSuggestionList.self } - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let service = UserSuggestionService(roomMemberProvider: self) let listViewModel = UserSuggestionViewModel(userSuggestionService: service) @@ -37,7 +37,7 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { return ( [service, listViewModel], AnyView(UserSuggestionListWithInput(viewModel: viewModel) - .addDependency(MockAvatarService.example)) + .addDependency(MockAvatarService.example)) ) } } @@ -52,7 +52,7 @@ extension MockUserSuggestionScreenState: RoomMembersProviderProtocol { } private func generateUsersWithCount(_ count: UInt) -> [RoomMembersProviderMember] { - return (0.. +typealias UserSuggestionViewModelType = StateStoreViewModel class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -39,15 +36,15 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo self.userSuggestionService = userSuggestionService let items = userSuggestionService.items.value.map { suggestionItem in - return UserSuggestionViewStateItem(id: suggestionItem.userId, avatar: suggestionItem, displayName: suggestionItem.displayName) + UserSuggestionViewStateItem(id: suggestionItem.userId, avatar: suggestionItem, displayName: suggestionItem.displayName) } super.init(initialViewState: UserSuggestionViewState(items: items)) userSuggestionService.items.sink { [weak self] items in - self?.state.items = items.map({ item in + self?.state.items = items.map { item in UserSuggestionViewStateItem(id: item.userId, avatar: item, displayName: item.displayName) - }) + } }.store(in: &cancellables) } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift index 7a04cf8c4..1d89ca9b4 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift index 9fc137833..859b0b414 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,7 @@ import SwiftUI struct UserSuggestionList: View { - private struct Constants { + private enum Constants { static let topPadding: CGFloat = 8.0 static let listItemPadding: CGFloat = 4.0 static let lineSpacing: CGFloat = 10.0 @@ -57,7 +57,7 @@ struct UserSuggestionList: View { userId: item.id ) .padding(.bottom, Constants.listItemPadding) - .padding(.top, (viewModel.viewState.items.first?.id == item.id ? Constants.listItemPadding + Constants.topPadding : Constants.listItemPadding)) + .padding(.top, viewModel.viewState.items.first?.id == item.id ? Constants.listItemPadding + Constants.topPadding : Constants.listItemPadding) } } .listStyle(PlainListStyle()) @@ -76,7 +76,6 @@ struct UserSuggestionList: View { } private struct BackgroundView: View { - var content: () -> Content @Environment(\.theme) private var theme: ThemeSwiftUI diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift index c5ade662b..0d3328b33 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,13 +17,14 @@ import SwiftUI struct UserSuggestionListItem: View { - // MARK: - Properties // MARK: Private + @Environment(\.theme) private var theme: ThemeSwiftUI // MARK: Public + let avatar: AvatarInputProtocol? let displayName: String? let userId: String diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift index cba0fae13..176be8ec4 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,11 +18,10 @@ import SwiftUI struct UserSuggestionListWithInputViewModel { let listViewModel: UserSuggestionViewModel - let callback: (String)->() + let callback: (String) -> Void } struct UserSuggestionListWithInput: View { - // MARK: - Properties // MARK: Private @@ -30,14 +29,14 @@ struct UserSuggestionListWithInput: View { // MARK: Public var viewModel: UserSuggestionListWithInputViewModel - @State private var inputText: String = "" + @State private var inputText = "" var body: some View { VStack(spacing: 0.0) { UserSuggestionList(viewModel: viewModel.listViewModel.context) TextField("Search for user", text: $inputText) .background(Color.white) - .onChange(of: inputText, perform:viewModel.callback) + .onChange(of: inputText, perform: viewModel.callback) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding([.leading, .trailing]) .onAppear { diff --git a/RiotSwiftUI/Modules/Settings/ChangePassword/ChangePasswordModels.swift b/RiotSwiftUI/Modules/Settings/ChangePassword/ChangePasswordModels.swift index b79c29044..6796dac14 100644 --- a/RiotSwiftUI/Modules/Settings/ChangePassword/ChangePasswordModels.swift +++ b/RiotSwiftUI/Modules/Settings/ChangePassword/ChangePasswordModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -42,8 +42,8 @@ struct ChangePasswordViewState: BindableState { /// Whether the user can submit the form: old password and new passwords should be entered var canSubmit: Bool { !bindings.oldPassword.isEmpty - && !bindings.newPassword1.isEmpty - && !bindings.newPassword2.isEmpty + && !bindings.newPassword1.isEmpty + && !bindings.newPassword2.isEmpty } } diff --git a/RiotSwiftUI/Modules/Settings/ChangePassword/ChangePasswordViewModel.swift b/RiotSwiftUI/Modules/Settings/ChangePassword/ChangePasswordViewModel.swift index b11309e9a..6cfcea0e6 100644 --- a/RiotSwiftUI/Modules/Settings/ChangePassword/ChangePasswordViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/ChangePassword/ChangePasswordViewModel.swift @@ -16,11 +16,9 @@ import SwiftUI -typealias ChangePasswordViewModelType = StateStoreViewModel -class ChangePasswordViewModel: ChangePasswordViewModelType, ChangePasswordViewModelProtocol { +typealias ChangePasswordViewModelType = StateStoreViewModel +class ChangePasswordViewModel: ChangePasswordViewModelType, ChangePasswordViewModelProtocol { // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Settings/ChangePassword/ChangePasswordViewModelProtocol.swift b/RiotSwiftUI/Modules/Settings/ChangePassword/ChangePasswordViewModelProtocol.swift index d6050d373..db6848c9c 100644 --- a/RiotSwiftUI/Modules/Settings/ChangePassword/ChangePasswordViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Settings/ChangePassword/ChangePasswordViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol ChangePasswordViewModelProtocol { - var callback: (@MainActor (ChangePasswordViewModelResult) -> Void)? { get set } var context: ChangePasswordViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Settings/ChangePassword/Coordinator/ChangePasswordBridgePresenter.swift b/RiotSwiftUI/Modules/Settings/ChangePassword/Coordinator/ChangePasswordBridgePresenter.swift index 15c99ade3..e84e61570 100644 --- a/RiotSwiftUI/Modules/Settings/ChangePassword/Coordinator/ChangePasswordBridgePresenter.swift +++ b/RiotSwiftUI/Modules/Settings/ChangePassword/Coordinator/ChangePasswordBridgePresenter.swift @@ -30,7 +30,6 @@ import MatrixSDK /// once the underlying Coordinator has been integrated by another Coordinator. @objcMembers final class ChangePasswordCoordinatorBridgePresenter: NSObject { - // MARK: - Constants // MARK: - Properties @@ -57,8 +56,7 @@ final class ChangePasswordCoordinatorBridgePresenter: NSObject { // MARK: - Public func present(from viewController: UIViewController, animated: Bool) { - - let params = ChangePasswordCoordinatorParameters(restClient: self.session.matrixRestClient) + let params = ChangePasswordCoordinatorParameters(restClient: session.matrixRestClient) let changePasswordCoordinator = ChangePasswordCoordinator(parameters: params) changePasswordCoordinator.callback = { [weak self] in @@ -74,11 +72,11 @@ final class ChangePasswordCoordinatorBridgePresenter: NSObject { viewController.present(navController, animated: animated, completion: nil) changePasswordCoordinator.start() - self.coordinator = changePasswordCoordinator + coordinator = changePasswordCoordinator } func dismiss(animated: Bool, completion: (() -> Void)?) { - guard let coordinator = self.coordinator else { + guard let coordinator = coordinator else { return } diff --git a/RiotSwiftUI/Modules/Settings/ChangePassword/Coordinator/ChangePasswordCoordinator.swift b/RiotSwiftUI/Modules/Settings/ChangePassword/Coordinator/ChangePasswordCoordinator.swift index d582e8721..bd3f6fc29 100644 --- a/RiotSwiftUI/Modules/Settings/ChangePassword/Coordinator/ChangePasswordCoordinator.swift +++ b/RiotSwiftUI/Modules/Settings/ChangePassword/Coordinator/ChangePasswordCoordinator.swift @@ -14,15 +14,14 @@ // limitations under the License. // -import SwiftUI import CommonKit +import SwiftUI struct ChangePasswordCoordinatorParameters { let restClient: MXRestClient } final class ChangePasswordCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -77,7 +76,7 @@ final class ChangePasswordCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.changePasswordHostingController + changePasswordHostingController } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Settings/ChangePassword/MockChangePasswordScreenState.swift b/RiotSwiftUI/Modules/Settings/ChangePassword/MockChangePasswordScreenState.swift index e4d109f23..e7a176e20 100644 --- a/RiotSwiftUI/Modules/Settings/ChangePassword/MockChangePasswordScreenState.swift +++ b/RiotSwiftUI/Modules/Settings/ChangePassword/MockChangePasswordScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,7 +34,7 @@ enum MockChangePasswordScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let viewModel: ChangePasswordViewModel switch self { case .allEmpty: diff --git a/RiotSwiftUI/Modules/Settings/ChangePassword/Test/UI/ChangePasswordUITests.swift b/RiotSwiftUI/Modules/Settings/ChangePassword/Test/UI/ChangePasswordUITests.swift index c96ef6f27..218d6f2c4 100644 --- a/RiotSwiftUI/Modules/Settings/ChangePassword/Test/UI/ChangePasswordUITests.swift +++ b/RiotSwiftUI/Modules/Settings/ChangePassword/Test/UI/ChangePasswordUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class ChangePasswordUITests: MockScreenTestCase { func testAllEmpty() { @@ -125,5 +125,4 @@ class ChangePasswordUITests: MockScreenTestCase { XCTAssertTrue(signoutAllDevicesToggle.exists, "Sign out all devices toggle should exist") XCTAssertTrue(signoutAllDevicesToggle.isOn, "Sign out all devices should be checked") } - } diff --git a/RiotSwiftUI/Modules/Settings/ChangePassword/Test/Unit/ChangePasswordViewModelTests.swift b/RiotSwiftUI/Modules/Settings/ChangePassword/Test/Unit/ChangePasswordViewModelTests.swift index e279c6b53..65f30d16b 100644 --- a/RiotSwiftUI/Modules/Settings/ChangePassword/Test/Unit/ChangePasswordViewModelTests.swift +++ b/RiotSwiftUI/Modules/Settings/ChangePassword/Test/Unit/ChangePasswordViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import XCTest @testable import RiotSwiftUI class ChangePasswordViewModelTests: XCTestCase { - @MainActor func testEmptyState() async { let viewModel = ChangePasswordViewModel() let context = viewModel.context @@ -46,5 +45,4 @@ class ChangePasswordViewModelTests: XCTestCase { XCTAssertTrue(context.viewState.canSubmit, "The view model should be able to submit.") XCTAssertTrue(context.signoutAllDevices, "Sign out of all devices should be checked.") } - } diff --git a/RiotSwiftUI/Modules/Settings/ChangePassword/View/ChangePasswordScreen.swift b/RiotSwiftUI/Modules/Settings/ChangePassword/View/ChangePasswordScreen.swift index 9a81ac24e..f432b44ab 100644 --- a/RiotSwiftUI/Modules/Settings/ChangePassword/View/ChangePasswordScreen.swift +++ b/RiotSwiftUI/Modules/Settings/ChangePassword/View/ChangePasswordScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct ChangePasswordScreen: View { - // MARK: - Properties // MARK: Private @@ -72,7 +71,7 @@ struct ChangePasswordScreen: View { configuration: UIKitTextInputConfiguration(returnKeyType: .next, isSecureTextEntry: true), onCommit: { focusedField = .newPassword1 }) - .accessibilityIdentifier("oldPasswordTextField") + .accessibilityIdentifier("oldPasswordTextField") RoundedBorderTextField(placeHolder: BWIL10n.settingsNewPassword, text: $viewModel.newPassword1, @@ -80,7 +79,7 @@ struct ChangePasswordScreen: View { configuration: UIKitTextInputConfiguration(returnKeyType: .next, isSecureTextEntry: true), onCommit: { focusedField = .newPassword2 }) - .accessibilityIdentifier("newPasswordTextField1") + .accessibilityIdentifier("newPasswordTextField1") RoundedBorderTextField(placeHolder: BWIL10n.settingsConfirmPassword, text: $viewModel.newPassword2, @@ -88,7 +87,7 @@ struct ChangePasswordScreen: View { configuration: UIKitTextInputConfiguration(returnKeyType: .done, isSecureTextEntry: true), onCommit: submit) - .accessibilityIdentifier("newPasswordTextField2") + .accessibilityIdentifier("newPasswordTextField2") HStack(alignment: .center, spacing: 8) { Toggle(VectorL10n.authenticationChoosePasswordSignoutAllDevices, isOn: $viewModel.signoutAllDevices) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsBridgePresenter.swift b/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsBridgePresenter.swift index f8a008b90..651ec3224 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsBridgePresenter.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsBridgePresenter.swift @@ -25,7 +25,6 @@ import Foundation /// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. @objcMembers final class NotificationSettingsCoordinatorBridgePresenter: NSObject { - // MARK: - Properties // MARK: Private @@ -48,7 +47,6 @@ final class NotificationSettingsCoordinatorBridgePresenter: NSObject { // MARK: - Public func push(from navigationController: UINavigationController, animated: Bool, screen: NotificationSettingsScreen, popCompletion: (() -> Void)?) { - let router = NavigationRouterStore.shared.navigationRouter(for: navigationController) let notificationSettingsCoordinator = NotificationSettingsCoordinator(session: session, screen: screen) @@ -61,12 +59,12 @@ final class NotificationSettingsCoordinatorBridgePresenter: NSObject { notificationSettingsCoordinator.start() - self.coordinator = notificationSettingsCoordinator + coordinator = notificationSettingsCoordinator self.router = router } func dismiss(animated: Bool, completion: (() -> Void)?) { - guard let coordinator = self.coordinator else { + guard let coordinator = coordinator else { return } coordinator.toPresentable().dismiss(animated: animated) { @@ -80,18 +78,17 @@ final class NotificationSettingsCoordinatorBridgePresenter: NSObject { } // MARK: - NotificationSettingsCoordinatorDelegate + extension NotificationSettingsCoordinatorBridgePresenter: NotificationSettingsCoordinatorDelegate { func notificationSettingsCoordinatorDidComplete(_ coordinator: NotificationSettingsCoordinatorType) { - self.delegate?.notificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self) + delegate?.notificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self) } } // MARK: - UIAdaptivePresentationControllerDelegate extension NotificationSettingsCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate { - func notificationSettingsCoordinatorDidComplete(_ presentationController: UIPresentationController) { - self.delegate?.notificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self) + delegate?.notificationSettingsCoordinatorBridgePresenterDelegateDidComplete(self) } - } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift index b903bb103..7ca59854f 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift @@ -18,7 +18,6 @@ import Foundation import SwiftUI final class NotificationSettingsCoordinator: NotificationSettingsCoordinatorType { - // MARK: - Properties // MARK: Private @@ -49,24 +48,25 @@ final class NotificationSettingsCoordinator: NotificationSettingsCoordinatorType case .other: viewController = VectorHostingController(rootView: OtherNotificationSettings(viewModel: viewModel)) } - self.notificationSettingsViewModel = viewModel - self.notificationSettingsViewController = viewController + notificationSettingsViewModel = viewModel + notificationSettingsViewController = viewController } // MARK: - Public methods - func start() { - self.notificationSettingsViewModel.coordinatorDelegate = self + func start() { + notificationSettingsViewModel.coordinatorDelegate = self } func toPresentable() -> UIViewController { - return self.notificationSettingsViewController + notificationSettingsViewController } } // MARK: - NotificationSettingsViewModelCoordinatorDelegate + extension NotificationSettingsCoordinator: NotificationSettingsViewModelCoordinatorDelegate { func notificationSettingsViewModelDidComplete(_ viewModel: NotificationSettingsViewModelType) { - self.delegate?.notificationSettingsCoordinatorDidComplete(self) + delegate?.notificationSettingsCoordinatorDidComplete(self) } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift index 6c67350af..2c4d16d8b 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/MatrixSDK/MXNotificationPushRule.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +14,11 @@ // limitations under the License. // -import Foundation import DesignKit +import Foundation // Conformance of MXPushRule to the abstraction `NotificationPushRule` for use in `NotificationSettingsViewModel`. extension MXPushRule: NotificationPushRuleType { - /// Given a rule, check it match the actions in the static definition. /// - Parameter standardActions: The standard actions to match against. /// - Returns: Wether `this` rule matches the standard actions. @@ -27,7 +26,7 @@ extension MXPushRule: NotificationPushRuleType { guard let standardActions = standardActions else { return false } - if !enabled && standardActions == .disabled { + if !enabled, standardActions == .disabled { return true } @@ -75,10 +74,10 @@ extension MXPushRule: NotificationPushRuleType { } var notify: Bool { - return getAction(actionType: MXPushRuleActionTypeNotify) != nil + getAction(actionType: MXPushRuleActionTypeNotify) != nil } var dontNotify: Bool { - return getAction(actionType: MXPushRuleActionTypeDontNotify) != nil + getAction(actionType: MXPushRuleActionTypeDontNotify) != nil } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift index 1c83c49f1..49166c99e 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/Mock/MockNotificationPushRule.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,6 @@ struct MockNotificationPushRule: NotificationPushRuleType { var ruleId: String! var enabled: Bool func matches(standardActions: NotificationStandardActions?) -> Bool { - return false + false } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift index 2facda9cd..88b11b3be 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationActions.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationIndex.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationIndex.swift index 89088159a..b4252ebbd 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationIndex.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationIndex.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,11 +33,11 @@ extension NotificationIndex { /// - Parameter enabled: Enabled/Disabled state. /// - Returns: The associated NotificationIndex static func index(when enabled: Bool) -> NotificationIndex { - return enabled ? .noisy : .off + enabled ? .noisy : .off } /// Used to map from the checked state back to the index. var enabled: Bool { - return self != .off + self != .off } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift index 10fa5ec90..77c6e1ef2 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleDefinitions.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,7 +16,6 @@ import Foundation - extension NotificationPushRuleId { /// A static definition of the push rule actions. /// diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift index 38ed2b521..d74968c8b 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleIds.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,7 +32,6 @@ enum NotificationPushRuleId: String { case keywords = "_keywords" } - extension NotificationPushRuleId: Identifiable { var id: String { rawValue diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift index 5f58befad..1f98242c7 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationPushRuleType.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationSettingsScreen.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationSettingsScreen.swift index 229b33102..4a349e504 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationSettingsScreen.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationSettingsScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,7 +26,7 @@ import Foundation extension NotificationSettingsScreen: CaseIterable { } extension NotificationSettingsScreen: Identifiable { - var id: Int { self.rawValue } + var id: Int { rawValue } } extension NotificationSettingsScreen { diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationStandardActions.swift b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationStandardActions.swift index 466b11595..89b8a55b5 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationStandardActions.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Model/NotificationStandardActions.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift index e7ac7a191..375b50ab9 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/MatrixSDK/MXNotificationSettingsService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +14,10 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class MXNotificationSettingsService: NotificationSettingsServiceType { - private let session: MXSession private var cancellables = Set() @@ -26,11 +25,11 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { @Published private var rules = [MXPushRule]() var rulesPublisher: AnyPublisher<[NotificationPushRuleType], Never> { - $rules.map({ $0.map({ $0 as NotificationPushRuleType }) }).eraseToAnyPublisher() + $rules.map { $0.map { $0 as NotificationPushRuleType } }.eraseToAnyPublisher() } var contentRulesPublisher: AnyPublisher<[NotificationPushRuleType], Never> { - $contentRules.map({ $0.map({ $0 as NotificationPushRuleType }) }).eraseToAnyPublisher() + $contentRules.map { $0.map { $0 as NotificationPushRuleType } }.eraseToAnyPublisher() } init(session: MXSession) { @@ -45,7 +44,7 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { // Observe future updates to content rules rulesUpdated - .compactMap({ _ in self.session.notificationCenter.rules.global.content as? [MXPushRule] }) + .compactMap { _ in self.session.notificationCenter.rules.global.content as? [MXPushRule] } .assign(to: &$contentRules) // Set initial value of rules @@ -54,7 +53,7 @@ class MXNotificationSettingsService: NotificationSettingsServiceType { } // Observe future updates to rules rulesUpdated - .compactMap({ _ in self.session.notificationCenter.flatRules as? [MXPushRule] }) + .compactMap { _ in self.session.notificationCenter.flatRules as? [MXPushRule] } .assign(to: &$rules) } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift index a7faceb52..44a553f6c 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/Mock/MockNotificationSettingsService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class MockNotificationSettingsService: NotificationSettingsServiceType, ObservableObject { static let example = MockNotificationSettingsService() @@ -44,7 +44,5 @@ class MockNotificationSettingsService: NotificationSettingsServiceType, Observab keywords.remove(keyword) } - func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) { - - } + func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?) { } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift b/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift index 6b0565ea6..a5a1671e3 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Service/NotificationSettingsServiceType.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation /// A service for changing notification settings and keywords protocol NotificationSettingsServiceType { diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/Chip.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/Chip.swift index 0c9bfd088..24e800acb 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/Chip.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/Chip.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,10 +16,8 @@ import SwiftUI - /// A single rounded rect chip to be rendered within `Chips` collection struct Chip: View { - @Environment(\.isEnabled) var isEnabled @Environment(\.theme) var theme: ThemeSwiftUI @@ -57,7 +55,6 @@ struct Chip: View { .background(backgroundColor) .foregroundColor(foregroundColor) .cornerRadius(20) - } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/Chips.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/Chips.swift index 9b66584d1..caccfad80 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/Chips.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/Chips.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,8 +18,7 @@ import SwiftUI /// Renders multiple chips in a flow layout. struct Chips: View { - - @State private var frame: CGRect = CGRect.zero + @State private var frame = CGRect.zero let titles: [String] let didDeleteChip: (String) -> Void @@ -56,7 +55,7 @@ struct Chips: View { } return result } - .alignmentGuide(.top) { dimension in + .alignmentGuide(.top) { _ in // Use next y value and reset if its the last. let result = y if chip == titles.last { @@ -82,6 +81,5 @@ struct Chips_Previews: PreviewProvider { Chips(titles: chips, didDeleteChip: { _ in }) .theme(.dark) } - } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/ChipsInput.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/ChipsInput.swift index 5c62ddd34..e913447da 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/ChipsInput.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/ChipsInput.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,17 +18,15 @@ import SwiftUI /// Renders an input field and a collection of chips. struct ChipsInput: View { - @Environment(\.theme) var theme: ThemeSwiftUI @Environment(\.isEnabled) var isEnabled - @State private var chipText: String = "" + @State private var chipText = "" let titles: [String] let didAddChip: (String) -> Void let didDeleteChip: (String) -> Void - var placeholder: String = "" - + var placeholder = "" var body: some View { VStack(spacing: 16) { @@ -55,6 +53,5 @@ struct ChipsInput_Previews: PreviewProvider { chips.remove(chip) } .disabled(true) - } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/DefaultNotificationSettings.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/DefaultNotificationSettings.swift index 8dd937614..5fecd7e29 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/DefaultNotificationSettings.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/DefaultNotificationSettings.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct DefaultNotificationSettings: View { - @ObservedObject var viewModel: NotificationSettingsViewModel var body: some View { @@ -38,6 +37,5 @@ struct DefaultNotifications_Previews: PreviewProvider { ) .navigationBarTitleDisplayMode(.inline) } - } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift index 3b35cd396..7232dd2c7 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/FormInputFieldStyle.swift @@ -19,7 +19,6 @@ import SwiftUI /// An input field style for forms. struct FormInputFieldStyle: TextFieldStyle { - @Environment(\.theme) var theme: ThemeSwiftUI @Environment(\.isEnabled) var isEnabled @@ -31,7 +30,7 @@ struct FormInputFieldStyle: TextFieldStyle { } private var backgroundColor: Color { - if !isEnabled && theme.identifier == .dark { + if !isEnabled, theme.identifier == .dark { return theme.colors.quinaryContent } return theme.colors.background @@ -47,7 +46,6 @@ struct FormInputFieldStyle: TextFieldStyle { } } - struct FormInputFieldStyle_Previews: PreviewProvider { static var previews: some View { Group { @@ -59,7 +57,6 @@ struct FormInputFieldStyle_Previews: PreviewProvider { TextField("Placeholder", text: .constant("Web")) .textFieldStyle(FormInputFieldStyle()) .disabled(true) - } .padding() VectorForm { @@ -70,11 +67,9 @@ struct FormInputFieldStyle_Previews: PreviewProvider { TextField("Placeholder", text: .constant("Web")) .textFieldStyle(FormInputFieldStyle()) .disabled(true) - } .padding() .theme(ThemeIdentifier.dark) } - } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/MentionsAndKeywordNotificationSettings.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/MentionsAndKeywordNotificationSettings.swift index 78a4d733c..ac1316bfd 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/MentionsAndKeywordNotificationSettings.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/MentionsAndKeywordNotificationSettings.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct MentionsAndKeywordNotificationSettings: View { - @ObservedObject var viewModel: NotificationSettingsViewModel var keywordSection: some View { @@ -28,6 +27,7 @@ struct MentionsAndKeywordNotificationSettings: View { NotificationSettingsKeywords(viewModel: viewModel) } } + var body: some View { NotificationSettings( viewModel: viewModel, diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift index 6943009ce..18be2680d 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettings.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,7 +21,6 @@ import SwiftUI /// Also renders an optional bottom section. /// Used in the case of keywords, for the keyword chips and input. struct NotificationSettings: View { - @ObservedObject var viewModel: NotificationSettingsViewModel var bottomSection: BottomSection? diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettingsKeywords.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettingsKeywords.swift index 6c358f555..a83980710 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettingsKeywords.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/NotificationSettingsKeywords.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,7 +27,6 @@ struct NotificationSettingsKeywords: View { placeholder: VectorL10n.settingsNewKeyword ) .disabled(!(viewModel.viewState.selectionState[.keywords] ?? false)) - } } diff --git a/RiotSwiftUI/Modules/Settings/Notifications/View/OtherNotificationSettings.swift b/RiotSwiftUI/Modules/Settings/Notifications/View/OtherNotificationSettings.swift index 5536a662b..ba906fc13 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/View/OtherNotificationSettings.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/View/OtherNotificationSettings.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift index ad1af1014..588597572 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/ViewModel/NotificationSettingsViewModel.swift @@ -16,12 +16,11 @@ limitations under the License. */ -import Foundation import Combine +import Foundation import SwiftUI final class NotificationSettingsViewModel: NotificationSettingsViewModelType, ObservableObject { - // MARK: - Properties // MARK: Private @@ -46,7 +45,7 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob init(notificationSettingsService: NotificationSettingsServiceType, ruleIds: [NotificationPushRuleId], initialState: NotificationSettingsViewState) { self.notificationSettingsService = notificationSettingsService self.ruleIds = ruleIds - self.viewState = initialState + viewState = initialState // Observe when the rules are updated, to subsequently update the state of the settings. notificationSettingsService.rulesPublisher @@ -57,11 +56,11 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob if ruleIds.contains(.keywords) { // Publisher of all the keyword push rules (keyword rules do not start with '.') let keywordsRules = notificationSettingsService.contentRulesPublisher - .map { $0.filter { !$0.ruleId.starts(with: ".")} } + .map { $0.filter { !$0.ruleId.starts(with: ".") } } // Map to just the keyword strings let keywords = keywordsRules - .map { Set($0.compactMap { $0.ruleId }) } + .map { Set($0.compactMap(\.ruleId)) } // Update the keyword set keywords @@ -100,7 +99,7 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob } convenience init(notificationSettingsService: NotificationSettingsServiceType, ruleIds: [NotificationPushRuleId]) { - let ruleState = Dictionary(uniqueKeysWithValues: ruleIds.map({ ($0, selected: true) })) + let ruleState = Dictionary(uniqueKeysWithValues: ruleIds.map { ($0, selected: true) }) self.init(notificationSettingsService: notificationSettingsService, ruleIds: ruleIds, initialState: NotificationSettingsViewState(saving: false, ruleIds: ruleIds, selectionState: ruleState)) } @@ -125,7 +124,7 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob private func updateKeywords(isChecked: Bool) { guard !keywordsOrdered.isEmpty else { - self.viewState.selectionState[.keywords]?.toggle() + viewState.selectionState[.keywords]?.toggle() return } // Get the static definition and update the actions and enabled state for every keyword. @@ -149,22 +148,23 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob } func remove(keyword: String) { - keywordsOrdered = keywordsOrdered.filter({ $0 != keyword }) + keywordsOrdered = keywordsOrdered.filter { $0 != keyword } notificationSettingsService.remove(keyword: keyword) } // MARK: - Private + private func rulesUpdated(newRules: [NotificationPushRuleType]) { for rule in newRules { guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId), ruleIds.contains(ruleId) else { continue } - self.viewState.selectionState[ruleId] = self.isChecked(rule: rule) + viewState.selectionState[ruleId] = isChecked(rule: rule) } } private func keywordRuleUpdated(anyEnabled: Bool) { if !keywordsOrdered.isEmpty { - self.viewState.selectionState[.keywords] = anyEnabled + viewState.selectionState[.keywords] = anyEnabled } } @@ -178,7 +178,7 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { return false } let firstIndex = NotificationIndex.allCases.first { nextIndex in - return rule.matches(standardActions: ruleId.standardActions(for: nextIndex)) + rule.matches(standardActions: ruleId.standardActions(for: nextIndex)) } guard let index = firstIndex else { @@ -187,5 +187,4 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob return index.enabled } - } diff --git a/RiotSwiftUI/Modules/Spaces/AddRoomSelector/Coordinator/AddRoomSelectorViewProvider.swift b/RiotSwiftUI/Modules/Spaces/AddRoomSelector/Coordinator/AddRoomSelectorViewProvider.swift index c24d5b6ed..af1fe6ee6 100644 --- a/RiotSwiftUI/Modules/Spaces/AddRoomSelector/Coordinator/AddRoomSelectorViewProvider.swift +++ b/RiotSwiftUI/Modules/Spaces/AddRoomSelector/Coordinator/AddRoomSelectorViewProvider.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,6 @@ import SwiftUI class AddRoomSelectorViewProvider: MatrixItemChooserCoordinatorViewProvider { func view(with viewModel: MatrixItemChooserViewModelType.Context) -> AnyView { - return AnyView(AddRoomSelector(viewModel: viewModel)) + AnyView(AddRoomSelector(viewModel: viewModel)) } } diff --git a/RiotSwiftUI/Modules/Spaces/AddRoomSelector/Service/MatrixSDK/AddRoomItemsProcessor.swift b/RiotSwiftUI/Modules/Spaces/AddRoomSelector/Service/MatrixSDK/AddRoomItemsProcessor.swift index 2696ee49b..ec2ad692c 100644 --- a/RiotSwiftUI/Modules/Spaces/AddRoomSelector/Service/MatrixSDK/AddRoomItemsProcessor.swift +++ b/RiotSwiftUI/Modules/Spaces/AddRoomSelector/Service/MatrixSDK/AddRoomItemsProcessor.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation class AddRoomItemsProcessor: MatrixItemChooserProcessorProtocol { - // MARK: Private private let parentSpace: MXSpace @@ -40,14 +39,14 @@ class AddRoomItemsProcessor: MatrixItemChooserProcessorProtocol { addChild(from: itemsIds, at: 0, completion: completion) } - func isItemIncluded(_ item: (MatrixListItemData)) -> Bool { - return !parentSpace.isRoomAChild(roomId: item.id) + func isItemIncluded(_ item: MatrixListItemData) -> Bool { + !parentSpace.isRoomAChild(roomId: item.id) } // MARK: Private /// Add room with roomId from list of room IDs at index to the parentSpace. - /// Recurse to the next index once done. + /// Recurse to the next index once done. func addChild(from roomIds: [String], at index: Int, completion: @escaping (Result) -> Void) { guard index < roomIds.count else { // last item has been processed or list is empty --> the recursion has finished diff --git a/RiotSwiftUI/Modules/Spaces/AddRoomSelector/View/AddRoomSelector.swift b/RiotSwiftUI/Modules/Spaces/AddRoomSelector/View/AddRoomSelector.swift index adfc48471..3babc259b 100644 --- a/RiotSwiftUI/Modules/Spaces/AddRoomSelector/View/AddRoomSelector.swift +++ b/RiotSwiftUI/Modules/Spaces/AddRoomSelector/View/AddRoomSelector.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct AddRoomSelector: View { - // MARK: Properties @ObservedObject var viewModel: MatrixItemChooserViewModel.Context @@ -26,7 +25,7 @@ struct AddRoomSelector: View { @Environment(\.theme) private var theme: ThemeSwiftUI private var isDoneEnabled: Bool { - return !viewModel.viewState.selectedItemIds.isEmpty && !viewModel.viewState.loading + !viewModel.viewState.selectedItemIds.isEmpty && !viewModel.viewState.loading } // MARK: Setup diff --git a/RiotSwiftUI/Modules/Spaces/LeaveSpace/Coordinator/LeaveSpaceViewProvider.swift b/RiotSwiftUI/Modules/Spaces/LeaveSpace/Coordinator/LeaveSpaceViewProvider.swift index e1e5df850..ced508155 100644 --- a/RiotSwiftUI/Modules/Spaces/LeaveSpace/Coordinator/LeaveSpaceViewProvider.swift +++ b/RiotSwiftUI/Modules/Spaces/LeaveSpace/Coordinator/LeaveSpaceViewProvider.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI class LeaveSpaceViewProvider: MatrixItemChooserCoordinatorViewProvider { - private let navTitle: String? init(navTitle: String?) { @@ -25,6 +24,6 @@ class LeaveSpaceViewProvider: MatrixItemChooserCoordinatorViewProvider { } func view(with viewModel: MatrixItemChooserViewModelType.Context) -> AnyView { - return AnyView(LeaveSpace(viewModel: viewModel, navTitle: navTitle)) + AnyView(LeaveSpace(viewModel: viewModel, navTitle: navTitle)) } } diff --git a/RiotSwiftUI/Modules/Spaces/LeaveSpace/Service/MatrixSDK/LeaveSpaceItemsProcessor.swift b/RiotSwiftUI/Modules/Spaces/LeaveSpace/Service/MatrixSDK/LeaveSpaceItemsProcessor.swift index 912ad551d..3fb4cbb99 100644 --- a/RiotSwiftUI/Modules/Spaces/LeaveSpace/Service/MatrixSDK/LeaveSpaceItemsProcessor.swift +++ b/RiotSwiftUI/Modules/Spaces/LeaveSpace/Service/MatrixSDK/LeaveSpaceItemsProcessor.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ import Foundation import MatrixSDK class LeaveSpaceItemsProcessor: MatrixItemChooserProcessorProtocol { - // MARK: Private private let spaceId: String @@ -29,7 +28,7 @@ class LeaveSpaceItemsProcessor: MatrixItemChooserProcessorProtocol { init(spaceId: String, session: MXSession) { self.spaceId = spaceId self.session = session - self.dataSource = MatrixItemChooserDirectChildrenDataSource(parentId: spaceId) + dataSource = MatrixItemChooserDirectChildrenDataSource(parentId: spaceId) } // MARK: MatrixItemChooserSelectionProcessorProtocol @@ -41,11 +40,11 @@ class LeaveSpaceItemsProcessor: MatrixItemChooserProcessorProtocol { } func computeSelection(withIds itemsIds: [String], completion: @escaping (Result) -> Void) { - guard let space = self.session.spaceService.getSpace(withId: self.spaceId) else { + guard let space = session.spaceService.getSpace(withId: spaceId) else { return } - self.leaveAllRooms(from: itemsIds, at: 0) { [weak self] result in + leaveAllRooms(from: itemsIds, at: 0) { [weak self] result in guard let self = self else { return } switch result { @@ -57,8 +56,8 @@ class LeaveSpaceItemsProcessor: MatrixItemChooserProcessorProtocol { } } - func isItemIncluded(_ item: (MatrixListItemData)) -> Bool { - return true + func isItemIncluded(_ item: MatrixListItemData) -> Bool { + true } // MARK: Private @@ -71,8 +70,8 @@ class LeaveSpaceItemsProcessor: MatrixItemChooserProcessorProtocol { return } - guard let room = self.session.room(withRoomId: roomIds[index]), !room.isDirect else { - self.leaveAllRooms(from: roomIds, at: index+1, completion: completion) + guard let room = session.room(withRoomId: roomIds[index]), !room.isDirect else { + leaveAllRooms(from: roomIds, at: index + 1, completion: completion) return } @@ -82,7 +81,7 @@ class LeaveSpaceItemsProcessor: MatrixItemChooserProcessorProtocol { switch response { case .success: - self.leaveAllRooms(from: roomIds, at: index+1, completion: completion) + self.leaveAllRooms(from: roomIds, at: index + 1, completion: completion) case .failure(let error): MXLog.error("[LeaveSpaceItemsProcessor] failed to leave room", context: error) completion(.failure(error)) diff --git a/RiotSwiftUI/Modules/Spaces/LeaveSpace/View/LeaveSpace.swift b/RiotSwiftUI/Modules/Spaces/LeaveSpace/View/LeaveSpace.swift index da065f36f..4de0a5af6 100644 --- a/RiotSwiftUI/Modules/Spaces/LeaveSpace/View/LeaveSpace.swift +++ b/RiotSwiftUI/Modules/Spaces/LeaveSpace/View/LeaveSpace.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct LeaveSpace: View { - // MARK: Properties @ObservedObject var viewModel: MatrixItemChooserViewModel.Context @@ -53,5 +52,4 @@ struct LeaveSpace: View { .buttonStyle(PrimaryActionButtonStyle(customColor: theme.colors.alert)) .padding() } - } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift index c8a632ada..467c69eb9 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Coordinator/MatrixItemChooserCoordinator.swift @@ -15,8 +15,8 @@ // import Foundation -import UIKit import SwiftUI +import UIKit internal protocol MatrixItemChooserCoordinatorViewProvider { func view(with viewModel: MatrixItemChooserViewModelType.Context) -> AnyView @@ -49,7 +49,6 @@ struct MatrixItemChooserCoordinatorParameters { } final class MatrixItemChooserCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -94,6 +93,6 @@ final class MatrixItemChooserCoordinator: Coordinator, Presentable { // MARK: - Presentable func toPresentable() -> UIViewController { - return self.matrixItemChooserHostingController + matrixItemChooserHostingController } } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserModels.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserModels.swift index da1374498..3493ae131 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserModels.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -58,7 +58,7 @@ struct MatrixListItemSectionData { } } -extension MatrixListItemSectionData: Identifiable, Equatable {} +extension MatrixListItemSectionData: Identifiable, Equatable { } struct MatrixListItemData { let id: String @@ -68,7 +68,7 @@ struct MatrixListItemData { let detailText: String? } -extension MatrixListItemData: Identifiable, Equatable {} +extension MatrixListItemData: Identifiable, Equatable { } struct MatrixItemChooserSelectionHeader { var title: String diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModel.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModel.swift index 9aa6786c8..8ef52c045 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModel.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModel.swift @@ -14,21 +14,19 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI + +typealias MatrixItemChooserViewModelType = StateStoreViewModel -typealias MatrixItemChooserViewModelType = StateStoreViewModel class MatrixItemChooserViewModel: MatrixItemChooserViewModelType, MatrixItemChooserViewModelProtocol { - // MARK: - Properties // MARK: Private private var matrixItemChooserService: MatrixItemChooserServiceProtocol - private var isLoading: Bool = false { + private var isLoading = false { didSet { state.loading = isLoading if isLoading { @@ -44,7 +42,7 @@ class MatrixItemChooserViewModel: MatrixItemChooserViewModelType, MatrixItemChoo // MARK: - Setup static func makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?, selectionHeader: MatrixItemChooserSelectionHeader?) -> MatrixItemChooserViewModelProtocol { - return MatrixItemChooserViewModel(matrixItemChooserService: matrixItemChooserService, title: title, detail: detail, selectionHeader: selectionHeader) + MatrixItemChooserViewModel(matrixItemChooserService: matrixItemChooserService, title: title, detail: detail, selectionHeader: selectionHeader) } private init(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?, selectionHeader: MatrixItemChooserSelectionHeader?) { @@ -98,13 +96,13 @@ class MatrixItemChooserViewModel: MatrixItemChooserViewModelType, MatrixItemChoo } } case .searchTextChanged(let searchText): - self.matrixItemChooserService.searchText = searchText + matrixItemChooserService.searchText = searchText case .itemTapped(let itemId): - self.matrixItemChooserService.reverseSelectionForItem(withId: itemId) + matrixItemChooserService.reverseSelectionForItem(withId: itemId) case .selectAll: - self.matrixItemChooserService.selectAllItems() + matrixItemChooserService.selectAllItems() case .selectNone: - self.matrixItemChooserService.deselectAllItems() + matrixItemChooserService.deselectAllItems() } } @@ -119,5 +117,4 @@ class MatrixItemChooserViewModel: MatrixItemChooserViewModelType, MatrixItemChoo private func back() { completion?(.back) } - } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModelProtocol.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModelProtocol.swift index 3bf385f7f..d9e7899e9 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MatrixItemChooserViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol MatrixItemChooserViewModelProtocol { - var completion: ((MatrixItemChooserViewModelResult) -> Void)? { get set } static func makeMatrixItemChooserViewModel(matrixItemChooserService: MatrixItemChooserServiceProtocol, title: String?, detail: String?, selectionHeader: MatrixItemChooserSelectionHeader?) -> MatrixItemChooserViewModelProtocol var context: MatrixItemChooserViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift index 8b32c9fb5..e18476c24 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixItemChooserServiceProtocol.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixItemChooserServiceProtocol.swift index 88218ff79..ce396d993 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixItemChooserServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixItemChooserServiceProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation protocol MatrixItemChooserServiceProtocol { var sectionsSubject: CurrentValueSubject<[MatrixListItemSectionData], Never> { get } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserDirectChildrenDataSource.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserDirectChildrenDataSource.swift index c48d6abdb..1a359d60c 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserDirectChildrenDataSource.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserDirectChildrenDataSource.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation class MatrixItemChooserDirectChildrenDataSource: MatrixItemChooserDataSource { - // MARK: - Private private let parentId: String @@ -34,13 +33,13 @@ class MatrixItemChooserDirectChildrenDataSource: MatrixItemChooserDataSource { func sections(with session: MXSession, completion: @escaping (Result<[MatrixListItemSectionData], Error>) -> Void) { let space = session.spaceService.getSpace(withId: parentId) - let children: [MatrixListItemData] = space?.childRoomIds.compactMap({ roomId in + let children: [MatrixListItemData] = space?.childRoomIds.compactMap { roomId in guard let room = session.room(withRoomId: roomId), !room.isDirect else { return nil } return MatrixListItemData(mxRoom: room, spaceService: session.spaceService) - }) ?? [] + } ?? [] completion(Result(catching: { [ MatrixListItemSectionData(items: children) diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomAncestorsDataSource.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomAncestorsDataSource.swift index aee17961b..d3ee2bee5 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomAncestorsDataSource.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomAncestorsDataSource.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,7 +28,7 @@ class MatrixItemChooserRoomAncestorsDataSource: MatrixItemChooserDataSource { func sections(with session: MXSession, completion: @escaping (Result<[MatrixListItemSectionData], Error>) -> Void) { let ancestorsIds = session.spaceService.ancestorsPerRoomId[roomId] ?? [] completion(Result(catching: { - return [ + [ MatrixListItemSectionData(title: VectorL10n.roomAccessSpaceChooserKnownSpacesSection(session.room(withRoomId: roomId)?.displayName ?? ""), items: ancestorsIds.compactMap { spaceId in guard let space = session.spaceService.getSpace(withId: spaceId) else { return nil diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomDirectParentsDataSource.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomDirectParentsDataSource.swift index 95e292356..dade8ced2 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomDirectParentsDataSource.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomDirectParentsDataSource.swift @@ -22,7 +22,6 @@ enum MatrixItemChooserRoomDirectParentsDataSourcePreselectionMode { } class MatrixItemChooserRoomDirectParentsDataSource: MatrixItemChooserDataSource { - private let roomId: String private let preselectionMode: MatrixItemChooserRoomDirectParentsDataSourcePreselectionMode @@ -44,7 +43,7 @@ class MatrixItemChooserRoomDirectParentsDataSource: MatrixItemChooserDataSource } completion(Result(catching: { - return [ + [ MatrixListItemSectionData(title: VectorL10n.roomAccessSpaceChooserKnownSpacesSection(session.room(withRoomId: roomId)?.displayName ?? ""), items: ancestorsIds.compactMap { spaceId in guard let space = session.spaceService.getSpace(withId: spaceId) else { return nil diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomRestrictedAllowedParentsDataSource.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomRestrictedAllowedParentsDataSource.swift index 4f4443c44..999d96cb9 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomRestrictedAllowedParentsDataSource.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomRestrictedAllowedParentsDataSource.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,7 +34,7 @@ class MatrixItemChooserRoomRestrictedAllowedParentsDataSource: MatrixItemChooser } room.state { [weak self] state in - guard let self = self else { return } + guard let self = self else { return } let joinRuleEvent = state?.stateEvents(with: .roomJoinRules)?.last let allowContent: [[String: String]] = joinRuleEvent?.wireContent[kMXJoinRulesContentKeyAllow] as? [[String: String]] ?? [] @@ -74,19 +74,20 @@ class MatrixItemChooserRoomRestrictedAllowedParentsDataSource: MatrixItemChooser sections.append(MatrixListItemSectionData( title: VectorL10n.roomAccessSpaceChooserOtherSpacesSection, infoText: VectorL10n.roomAccessSpaceChooserOtherSpacesSectionInfo(room.displayName ?? ""), - items: unknownParents.compactMap({ roomId in + items: unknownParents.compactMap { roomId in MatrixListItemData( id: roomId, type: .space, avatar: AvatarInput(mxContentUri: roomId, matrixItemId: roomId, displayName: roomId), displayName: roomId, - detailText: nil) - }) + detailText: nil + ) + } )) } completion(Result(catching: { - return sections + sections })) } } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomsDataSource.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomsDataSource.swift index 5906cdba1..348c4bb71 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomsDataSource.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserRoomsDataSource.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserService.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserService.swift index 22a624413..28ed8a754 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserService.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation protocol MatrixItemChooserDataSource { func sections(with session: MXSession, completion: @escaping (Result<[MatrixListItemSectionData], Error>) -> Void) @@ -25,12 +25,11 @@ protocol MatrixItemChooserDataSource { protocol MatrixItemChooserProcessorProtocol { var loadingText: String? { get } var dataSource: MatrixItemChooserDataSource { get } - func computeSelection(withIds itemsIds:[String], completion: @escaping (Result) -> Void) - func isItemIncluded(_ item: (MatrixListItemData)) -> Bool + func computeSelection(withIds itemsIds: [String], completion: @escaping (Result) -> Void) + func isItemIncluded(_ item: MatrixListItemData) -> Bool } class MatrixItemChooserService: MatrixItemChooserServiceProtocol { - // MARK: - Properties // MARK: Private @@ -45,6 +44,7 @@ class MatrixItemChooserService: MatrixItemChooserServiceProtocol { sectionsSubject.send(filteredSections) } } + private var selectedItemIds: Set private let itemsProcessor: MatrixItemChooserProcessorProtocol @@ -52,14 +52,16 @@ class MatrixItemChooserService: MatrixItemChooserServiceProtocol { private(set) var sectionsSubject: CurrentValueSubject<[MatrixListItemSectionData], Never> private(set) var selectedItemIdsSubject: CurrentValueSubject, Never> - var searchText: String = "" { + var searchText = "" { didSet { refresh() } } + var loadingText: String? { itemsProcessor.loadingText } + var itemCount: Int { var itemCount = 0 for section in sections { @@ -72,10 +74,10 @@ class MatrixItemChooserService: MatrixItemChooserServiceProtocol { init(session: MXSession, selectedItemIds: [String], itemsProcessor: MatrixItemChooserProcessorProtocol) { self.session = session - self.sectionsSubject = CurrentValueSubject(self.sections) + sectionsSubject = CurrentValueSubject(sections) self.selectedItemIds = Set(selectedItemIds) - self.selectedItemIdsSubject = CurrentValueSubject(self.selectedItemIds) + selectedItemIdsSubject = CurrentValueSubject(self.selectedItemIds) self.itemsProcessor = itemsProcessor itemsProcessor.dataSource.sections(with: session) { [weak self] result in @@ -115,7 +117,7 @@ class MatrixItemChooserService: MatrixItemChooserServiceProtocol { } func refresh() { - self.processingQueue.async { [weak self] in + processingQueue.async { [weak self] in guard let self = self else { return } let filteredSections = self.filter(sections: self.sections) @@ -132,12 +134,12 @@ class MatrixItemChooserService: MatrixItemChooserServiceProtocol { newSelection.insert(item.id) } } - self.selectedItemIds = newSelection + selectedItemIds = newSelection selectedItemIdsSubject.send(selectedItemIds) } func deselectAllItems() { - self.selectedItemIds = Set() + selectedItemIds = Set() selectedItemIdsSubject.send(selectedItemIds) } @@ -153,7 +155,7 @@ class MatrixItemChooserService: MatrixItemChooserServiceProtocol { itemsProcessor.isItemIncluded($0) } } else { - let lowercasedSearchText = self.searchText.lowercased() + let lowercasedSearchText = searchText.lowercased() items = section.items.filter { itemsProcessor.isItemIncluded($0) && ($0.id.lowercased().contains(lowercasedSearchText) || ($0.displayName ?? "").lowercased().contains(lowercasedSearchText)) } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserUsersDataSource.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserUsersDataSource.swift index 3194c37d7..184005866 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserUsersDataSource.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixItemChooserUsersDataSource.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixListItemData+Riot.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixListItemData+Riot.swift index 5e8cc6119..292ad2ee0 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixListItemData+Riot.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/MatrixSDK/MatrixListItemData+Riot.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation extension MatrixListItemData { - init(mxUser: MXUser) { self.init(id: mxUser.userId, type: .user, avatar: mxUser.avatarData, displayName: mxUser.displayname, detailText: mxUser.userId) } @@ -54,5 +53,4 @@ extension MatrixListItemData { } self.init(id: mxRoom.roomId, type: type, avatar: mxRoom.avatarData, displayName: mxRoom.summary.displayname, detailText: detailText) } - } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/Mock/MockMatrixItemChooserService.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/Mock/MockMatrixItemChooserService.swift index b08ede2fe..e1426bb6f 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/Mock/MockMatrixItemChooserService.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Service/Mock/MockMatrixItemChooserService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +14,10 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class MockMatrixItemChooserService: MatrixItemChooserServiceProtocol { - static let mockSections = [ MatrixListItemSectionData(title: "Section 1", infoText: "This is the first section with a very long description in order to check multi line description", items: [ MatrixListItemData(id: "!aaabaa:matrix.org", type: .room, avatar: MockAvatarInput.example, displayName: "Item #1 section #1", detailText: "Descripton of this room"), @@ -33,11 +32,12 @@ class MockMatrixItemChooserService: MatrixItemChooserServiceProtocol { ] var sectionsSubject: CurrentValueSubject<[MatrixListItemSectionData], Never> var selectedItemIdsSubject: CurrentValueSubject, Never> - var searchText: String = "" + var searchText = "" var selectedItemIds: Set = Set() var loadingText: String? { nil } + var itemCount: Int { var itemCount = 0 for section in sectionsSubject.value { @@ -82,9 +82,7 @@ class MockMatrixItemChooserService: MatrixItemChooserServiceProtocol { completion(Result.success(())) } - func refresh() { - - } + func refresh() { } func selectAllItems() { var newSelection: Set = Set() @@ -93,12 +91,12 @@ class MockMatrixItemChooserService: MatrixItemChooserServiceProtocol { newSelection.insert(item.id) } } - self.selectedItemIds = newSelection + selectedItemIds = newSelection selectedItemIdsSubject.send(selectedItemIds) } func deselectAllItems() { - self.selectedItemIds = Set() + selectedItemIds = Set() selectedItemIdsSubject.send(selectedItemIds) } } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift index edcd683a3..52bccd9aa 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class MatrixItemChooserUITests: MockScreenTestCase { func testEmptyScreen() { @@ -42,5 +42,4 @@ class MatrixItemChooserUITests: MockScreenTestCase { XCTAssertEqual(app.staticTexts["messageText"].label, VectorL10n.spacesCreationAddRoomsMessage) XCTAssertEqual(app.staticTexts["emptyListMessage"].exists, false) } - } diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/Unit/MatrixItemChooserViewModelTests.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/Unit/MatrixItemChooserViewModelTests.swift index 604674ce4..626707a1a 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/Unit/MatrixItemChooserViewModelTests.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/Unit/MatrixItemChooserViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift index eeb7743d3..6b7236788 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooser.swift @@ -19,12 +19,11 @@ import SwiftUI struct MatrixItemChooser: View { - // MARK: Properties @ObservedObject var viewModel: MatrixItemChooserViewModel.Context let listBottomPadding: CGFloat? - @State var searchText: String = "" + @State var searchText = "" // MARK: Private @@ -113,7 +112,7 @@ struct MatrixItemChooser: View { } Spacer().frame(height: spacerHeight) SearchBar(placeholder: VectorL10n.searchDefaultPlaceholder, text: $searchText) - .onChange(of: searchText) { value in + .onChange(of: searchText) { _ in viewModel.send(viewAction: .searchTextChanged(searchText)) } if let selectionHeader = viewModel.viewState.selectionHeader, searchText.isEmpty { @@ -124,7 +123,7 @@ struct MatrixItemChooser: View { } private func itemSelectionHeader(with selectionHeader: MatrixItemChooserSelectionHeader) -> some View { - VStack(alignment:.leading) { + VStack(alignment: .leading) { HStack { Text(selectionHeader.title) .font(theme.fonts.calloutSB) @@ -151,7 +150,6 @@ struct MatrixItemChooser: View { // MARK: - Previews struct MatrixItemChooser_Previews: PreviewProvider { - static let stateRenderer = MockMatrixItemChooserScreenState.stateRenderer static var previews: some View { stateRenderer.screenGroup(addNavigation: false) diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift index f5d6db9c8..1d67f2a72 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserListRow.swift @@ -17,7 +17,6 @@ import SwiftUI struct MatrixItemChooserListRow: View { - // MARK: - Properties // MARK: Private @@ -34,7 +33,7 @@ struct MatrixItemChooserListRow: View { @ViewBuilder var body: some View { - HStack{ + HStack { if type == .space { SpaceAvatarImage(avatarData: avatar, size: .small) } else { diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserSectionHeader.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserSectionHeader.swift index 632403587..a8a75c96a 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserSectionHeader.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/View/MatrixItemChooserSectionHeader.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct MatrixItemChooserSectionHeader: View { - // MARK: - Properties // MARK: Private diff --git a/RiotSwiftUI/Modules/Spaces/RoomAncestorSelector/Coordinator/RoomAncestorSelectorViewProvider.swift b/RiotSwiftUI/Modules/Spaces/RoomAncestorSelector/Coordinator/RoomAncestorSelectorViewProvider.swift index 070a31967..1ec2d21f3 100644 --- a/RiotSwiftUI/Modules/Spaces/RoomAncestorSelector/Coordinator/RoomAncestorSelectorViewProvider.swift +++ b/RiotSwiftUI/Modules/Spaces/RoomAncestorSelector/Coordinator/RoomAncestorSelectorViewProvider.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI class RoomAncestorSelectorViewProvider: MatrixItemChooserCoordinatorViewProvider { - private let navTitle: String? init(navTitle: String?) { @@ -25,6 +24,6 @@ class RoomAncestorSelectorViewProvider: MatrixItemChooserCoordinatorViewProvider } func view(with viewModel: MatrixItemChooserViewModelType.Context) -> AnyView { - return AnyView(RoomAncestorSelector(viewModel: viewModel, navTitle: navTitle)) + AnyView(RoomAncestorSelector(viewModel: viewModel, navTitle: navTitle)) } } diff --git a/RiotSwiftUI/Modules/Spaces/RoomAncestorSelector/Service/MatrixSDK/RoomAncestorSelectorItemsProcessor.swift b/RiotSwiftUI/Modules/Spaces/RoomAncestorSelector/Service/MatrixSDK/RoomAncestorSelectorItemsProcessor.swift index 0c8f8884c..7b28dc2b2 100644 --- a/RiotSwiftUI/Modules/Spaces/RoomAncestorSelector/Service/MatrixSDK/RoomAncestorSelectorItemsProcessor.swift +++ b/RiotSwiftUI/Modules/Spaces/RoomAncestorSelector/Service/MatrixSDK/RoomAncestorSelectorItemsProcessor.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation class RoomAncestorSelectorItemsProcessor: MatrixItemChooserProcessorProtocol { - // MARK: Private private let roomId: String @@ -26,7 +25,7 @@ class RoomAncestorSelectorItemsProcessor: MatrixItemChooserProcessorProtocol { init(roomId: String) { self.roomId = roomId - self.dataSource = MatrixItemChooserRoomAncestorsDataSource(roomId: roomId) + dataSource = MatrixItemChooserRoomAncestorsDataSource(roomId: roomId) } // MARK: MatrixItemChooserSelectionProcessorProtocol @@ -41,7 +40,7 @@ class RoomAncestorSelectorItemsProcessor: MatrixItemChooserProcessorProtocol { completion(Result.success(())) } - func isItemIncluded(_ item: (MatrixListItemData)) -> Bool { - return true + func isItemIncluded(_ item: MatrixListItemData) -> Bool { + true } } diff --git a/RiotSwiftUI/Modules/Spaces/RoomAncestorSelector/View/RoomAncestorSelector.swift b/RiotSwiftUI/Modules/Spaces/RoomAncestorSelector/View/RoomAncestorSelector.swift index 91ac64a40..3d1c2557a 100644 --- a/RiotSwiftUI/Modules/Spaces/RoomAncestorSelector/View/RoomAncestorSelector.swift +++ b/RiotSwiftUI/Modules/Spaces/RoomAncestorSelector/View/RoomAncestorSelector.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct RoomAncestorSelector: View { - // MARK: Properties @ObservedObject var viewModel: MatrixItemChooserViewModel.Context @@ -46,5 +45,4 @@ struct RoomAncestorSelector: View { } } } - } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift index cb4bd91a2..318fd549c 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinator.swift @@ -22,7 +22,6 @@ import UIKit @objcMembers final class SpaceCreationCoordinator: Coordinator { - // MARK: - Properties // MARK: Private @@ -30,7 +29,7 @@ final class SpaceCreationCoordinator: Coordinator { private let parameters: SpaceCreationCoordinatorParameters private var navigationRouter: NavigationRouterType { - return self.parameters.navigationRouter + parameters.navigationRouter } private let spaceVisibilityMenuParameters: SpaceCreationMenuCoordinatorParameters @@ -57,7 +56,7 @@ final class SpaceCreationCoordinator: Coordinator { } self.parameters = parameters - self.spaceVisibilityMenuParameters = SpaceCreationMenuCoordinatorParameters( + spaceVisibilityMenuParameters = SpaceCreationMenuCoordinatorParameters( session: parameters.session, creationParams: parameters.creationParameters, navTitle: VectorL10n.spacesCreateSpaceTitle, @@ -70,7 +69,7 @@ final class SpaceCreationCoordinator: Coordinator { ] ) - self.spaceSharingTypeMenuParameters = SpaceCreationMenuCoordinatorParameters( + spaceSharingTypeMenuParameters = SpaceCreationMenuCoordinatorParameters( session: parameters.session, creationParams: parameters.creationParameters, navTitle: nil, @@ -91,26 +90,26 @@ final class SpaceCreationCoordinator: Coordinator { Analytics.shared.trackScreen(.createSpace) - let rootCoordinator = self.createMenuCoordinator(with: spaceVisibilityMenuParameters) + let rootCoordinator = createMenuCoordinator(with: spaceVisibilityMenuParameters) rootCoordinator.start() - self.add(childCoordinator: rootCoordinator) + add(childCoordinator: rootCoordinator) - self.toPresentable().isModalInPresentation = true + toPresentable().isModalInPresentation = true - if self.navigationRouter.modules.isEmpty == false { - self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in + if navigationRouter.modules.isEmpty == false { + navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in self?.remove(childCoordinator: rootCoordinator) }) } else { - self.navigationRouter.setRootModule(rootCoordinator) { [weak self] in + navigationRouter.setRootModule(rootCoordinator) { [weak self] in self?.remove(childCoordinator: rootCoordinator) } } } func toPresentable() -> UIViewController { - return self.navigationRouter.toPresentable() + navigationRouter.toPresentable() } // MARK: - Private @@ -118,7 +117,7 @@ final class SpaceCreationCoordinator: Coordinator { func pushScreen(with coordinator: Coordinator & Presentable) { add(childCoordinator: coordinator) - self.navigationRouter.push(coordinator, animated: true, popCompletion: { [weak self] in + navigationRouter.push(coordinator, animated: true, popCompletion: { [weak self] in self?.remove(childCoordinator: coordinator) }) @@ -126,7 +125,7 @@ final class SpaceCreationCoordinator: Coordinator { } private func createMenuCoordinator(with parameters: SpaceCreationMenuCoordinatorParameters) -> SpaceCreationMenuCoordinator { - let coordinator: SpaceCreationMenuCoordinator = SpaceCreationMenuCoordinator(parameters: parameters) + let coordinator = SpaceCreationMenuCoordinator(parameters: parameters) coordinator.callback = { [weak self] result in MXLog.debug("[SpaceCreationCoordinator] SpaceCreationMenuCoordinator did complete with result \(result).") @@ -217,7 +216,8 @@ final class SpaceCreationCoordinator: Coordinator { detail: VectorL10n.spacesCreationInviteByUsernameMessage, selectedItemsIds: parameters.creationParameters.userIdInvites, viewProvider: SpaceCreationMatrixItemChooserViewProvider(), - itemsProcessor: SpaceCreationInviteUsersItemsProcessor(creationParams: parameters.creationParameters)) + itemsProcessor: SpaceCreationInviteUsersItemsProcessor(creationParams: parameters.creationParameters) + ) let coordinator = MatrixItemChooserCoordinator(parameters: parameters) coordinator.completion = { [weak self] result in guard let self = self else { return } @@ -240,7 +240,8 @@ final class SpaceCreationCoordinator: Coordinator { detail: VectorL10n.spacesCreationAddRoomsMessage, selectedItemsIds: parameters.creationParameters.addedRoomIds ?? [], viewProvider: SpaceCreationMatrixItemChooserViewProvider(), - itemsProcessor: SpaceCreationAddRoomsItemsProcessor(creationParams: parameters.creationParameters)) + itemsProcessor: SpaceCreationAddRoomsItemsProcessor(creationParams: parameters.creationParameters) + ) let coordinator = MatrixItemChooserCoordinator(parameters: parameters) coordinator.completion = { [weak self] result in guard let self = self else { return } @@ -275,13 +276,13 @@ final class SpaceCreationCoordinator: Coordinator { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) let alert = UIAlertController(title: VectorL10n.spacesCreationCancelTitle, message: VectorL10n.spacesCreationCancelMessage, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: VectorL10n.stop, style: .destructive, handler: { action in + alert.addAction(UIAlertAction(title: VectorL10n.stop, style: .destructive, handler: { _ in self.callback?(.cancel) })) alert.addAction(UIAlertAction(title: BWIL10n.continue, style: .cancel, handler: nil)) navigationRouter.present(alert, animated: true) } else { - self.callback?(.cancel) + callback?(.cancel) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinatorAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinatorAction.swift index 879d8e0f8..fee96634a 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinatorAction.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinatorAction.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinatorParameters.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinatorParameters.swift index f965b81ab..a78eebcfb 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/Coordinator/SpaceCreationCoordinatorParameters.swift @@ -22,7 +22,6 @@ import Foundation /// SpaceCreationCoordinator input parameters struct SpaceCreationCoordinatorParameters { - /// The Matrix session let session: MXSession @@ -30,7 +29,7 @@ struct SpaceCreationCoordinatorParameters { let parentSpaceId: String? /// Parameters needed to create the new space - let creationParameters: SpaceCreationParameters = SpaceCreationParameters() + let creationParameters = SpaceCreationParameters() /// The navigation router that manage physical navigation let navigationRouter: NavigationRouterType diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift index 986f03b35..28792056f 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinator.swift @@ -17,11 +17,10 @@ */ import Foundation -import UIKit import SwiftUI +import UIKit final class SpaceCreationEmailInvitesCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -75,7 +74,7 @@ final class SpaceCreationEmailInvitesCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.spaceCreationEmailInvitesHostingController + spaceCreationEmailInvitesHostingController } // MARK: - Identity service @@ -90,34 +89,34 @@ final class SpaceCreationEmailInvitesCoordinator: Coordinator, Presentable { let presenter = ServiceTermsModalCoordinatorBridgePresenter(session: parameters.session, baseUrl: baseUrl, serviceType: MXServiceTypeIdentityService, accessToken: accessToken) presenter.delegate = self - presenter.present(from: self.toPresentable(), animated: true) + presenter.present(from: toPresentable(), animated: true) serviceTermsModalCoordinatorBridgePresenter = presenter } private func showIdentityServiceFailure(_ error: Error?) { let alertController = UIAlertController(title: VectorL10n.findYourContactsIdentityServiceError, message: nil, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: VectorL10n.ok, style: .default, handler: nil)) - self.toPresentable().present(alertController, animated: true, completion: nil); + toPresentable().present(alertController, animated: true, completion: nil) } } extension SpaceCreationEmailInvitesCoordinator: ServiceTermsModalCoordinatorBridgePresenterDelegate { func serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept(_ coordinatorBridgePresenter: ServiceTermsModalCoordinatorBridgePresenter) { coordinatorBridgePresenter.dismiss(animated: true) { - self.serviceTermsModalCoordinatorBridgePresenter = nil; + self.serviceTermsModalCoordinatorBridgePresenter = nil self.callback?(.done) } } func serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline(_ coordinatorBridgePresenter: ServiceTermsModalCoordinatorBridgePresenter, session: MXSession) { coordinatorBridgePresenter.dismiss(animated: true) { - self.serviceTermsModalCoordinatorBridgePresenter = nil; + self.serviceTermsModalCoordinatorBridgePresenter = nil } } func serviceTermsModalCoordinatorBridgePresenterDelegateDidClose(_ coordinatorBridgePresenter: ServiceTermsModalCoordinatorBridgePresenter) { coordinatorBridgePresenter.dismiss(animated: true) { - self.serviceTermsModalCoordinatorBridgePresenter = nil; + self.serviceTermsModalCoordinatorBridgePresenter = nil } } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinatorParameters.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinatorParameters.swift index af2ac696c..3b2d32f78 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Coordinator/SpaceCreationEmailInvitesCoordinatorParameters.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesCoordinatorAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesCoordinatorAction.swift index cec48fc61..5eb47645f 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesCoordinatorAction.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesCoordinatorAction.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewAction.swift index 0f08acea8..b2fbfc34f 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewAction.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewAction.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewModelBindings.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewModelBindings.swift index 531689fa1..39b560c62 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewModelBindings.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewModelBindings.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewModelResult.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewModelResult.swift index d4f55e4a3..06bf52215 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewModelResult.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewModelResult.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewState.swift index e3028134f..ee7d48e08 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesViewState.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/MatrixSDK/SpaceCreationEmailInvitesService.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/MatrixSDK/SpaceCreationEmailInvitesService.swift index e011cbc59..e49d6030a 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/MatrixSDK/SpaceCreationEmailInvitesService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/MatrixSDK/SpaceCreationEmailInvitesService.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,11 +16,10 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class SpaceCreationEmailInvitesService: SpaceCreationEmailInvitesServiceProtocol { - private let session: MXSession private(set) var isLoadingSubject: CurrentValueSubject @@ -37,12 +36,12 @@ class SpaceCreationEmailInvitesService: SpaceCreationEmailInvitesServiceProtocol } func validate(_ emailAddresses: [String]) -> [Bool] { - return emailAddresses.map { $0.isEmpty || MXTools.isEmailAddress($0) } + emailAddresses.map { $0.isEmpty || MXTools.isEmailAddress($0) } } func prepareIdentityService(prepared: ((String?, String?) -> Void)?, failure: ((Error?) -> Void)?) { isLoadingSubject.send(true) - session.prepareIdentityServiceForTerms(withDefault: RiotSettings.shared.identityServerUrlString) { [weak self] session, baseURL, accessToken in + session.prepareIdentityServiceForTerms(withDefault: RiotSettings.shared.identityServerUrlString) { [weak self] _, baseURL, accessToken in self?.isLoadingSubject.send(false) prepared?(baseURL, accessToken) } failure: { [weak self] error in diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift index 09a3d4b82..0b70a8746 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesScreenState.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -41,7 +41,7 @@ enum MockSpaceCreationEmailInvitesScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let creationParams = SpaceCreationParameters() let service: MockSpaceCreationEmailInvitesService switch self { diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesService.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesService.swift index a34a9b801..5c308f044 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/Mock/MockSpaceCreationEmailInvitesService.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class MockSpaceCreationEmailInvitesService: SpaceCreationEmailInvitesServiceProtocol { var isLoadingSubject: CurrentValueSubject @@ -25,16 +25,16 @@ class MockSpaceCreationEmailInvitesService: SpaceCreationEmailInvitesServiceProt private let defaultValidation: Bool var isIdentityServiceReady: Bool { - return true + true } init(defaultValidation: Bool, isLoading: Bool) { self.defaultValidation = defaultValidation - self.isLoadingSubject = CurrentValueSubject(isLoading) + isLoadingSubject = CurrentValueSubject(isLoading) } func validate(_ emailAddresses: [String]) -> [Bool] { - return emailAddresses.map { _ in defaultValidation } + emailAddresses.map { _ in defaultValidation } } func prepareIdentityService(prepared: ((String?, String?) -> Void)?, failure: ((Error?) -> Void)?) { diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/SpaceCreationEmailInvitesServiceProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/SpaceCreationEmailInvitesServiceProtocol.swift index c92a2cd8d..cae561374 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/SpaceCreationEmailInvitesServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Service/SpaceCreationEmailInvitesServiceProtocol.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation protocol SpaceCreationEmailInvitesServiceProtocol { var isIdentityServiceReady: Bool { get } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Test/UI/SpaceCreationEmailInvitesUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Test/UI/SpaceCreationEmailInvitesUITests.swift index 897230312..9397206b4 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Test/UI/SpaceCreationEmailInvitesUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Test/UI/SpaceCreationEmailInvitesUITests.swift @@ -16,8 +16,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class SpaceCreationEmailInvitesUITests: MockScreenTestCase { func testDefaultEmailValues() { diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Test/Unit/SpaceCreationEmailInvitesViewModelTests.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Test/Unit/SpaceCreationEmailInvitesViewModelTests.swift index bbea5e3e2..b2477d145 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Test/Unit/SpaceCreationEmailInvitesViewModelTests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Test/Unit/SpaceCreationEmailInvitesViewModelTests.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,8 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/View/SpaceCreationEmailInvites.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/View/SpaceCreationEmailInvites.swift index 80f9abe87..ddaca92f1 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/View/SpaceCreationEmailInvites.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/View/SpaceCreationEmailInvites.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import SwiftUI struct SpaceCreationEmailInvites: View { - // MARK: - Properties @ObservedObject var viewModel: SpaceCreationEmailInvitesViewModel.Context @@ -85,7 +84,7 @@ struct SpaceCreationEmailInvites: View { private var formView: some View { VStack { VStack(spacing: 20) { - ForEach(viewModel.emailInvites.indices) { index in + ForEach(viewModel.emailInvites.indices, id: \.self) { index in RoundedBorderTextField(title: VectorL10n.spacesCreationEmailInvitesEmailTitle, placeHolder: VectorL10n.spacesCreationEmailInvitesEmailTitle, text: $viewModel.emailInvites[index], footerText: viewModel.viewState.emailAddressesValid[index] ? nil : VectorL10n.authInvalidEmail, isError: !viewModel.viewState.emailAddressesValid[index], configuration: UIKitTextInputConfiguration(keyboardType: .emailAddress, returnKeyType: index < viewModel.emailInvites.endIndex - 1 ? .next : .done, autocapitalizationType: .none, autocorrectionType: .no)) .accessibility(identifier: "emailTextField") } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/ViewModel/SpaceCreationEmailInvitesViewModel.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/ViewModel/SpaceCreationEmailInvitesViewModel.swift index 55a2c6750..92c906378 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/ViewModel/SpaceCreationEmailInvitesViewModel.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/ViewModel/SpaceCreationEmailInvitesViewModel.swift @@ -16,14 +16,12 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI + +typealias SpaceCreationEmailInvitesViewModelType = StateStoreViewModel -typealias SpaceCreationEmailInvitesViewModelType = StateStoreViewModel class SpaceCreationEmailInvitesViewModel: SpaceCreationEmailInvitesViewModelType, SpaceCreationEmailInvitesViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -42,12 +40,14 @@ class SpaceCreationEmailInvitesViewModel: SpaceCreationEmailInvitesViewModelType self.service = service super.init(initialViewState: SpaceCreationEmailInvitesViewModel.defaultState(creationParameters: creationParameters, service: service)) } - + private func setupServiceObserving() { - let publisher = service.isLoadingSubject - .map(SpaceCreationEmailInvitesStateAction.updateLoading) - .eraseToAnyPublisher() - dispatch(actionPublisher: publisher) + service + .isLoadingSubject + .sink(receiveValue: { [weak self] isLoading in + self?.state.loading = isLoading + }) + .store(in: &cancellables) } private static func defaultState(creationParameters: SpaceCreationParameters, service: SpaceCreationEmailInvitesServiceProtocol) -> SpaceCreationEmailInvitesViewState { @@ -76,24 +76,16 @@ class SpaceCreationEmailInvitesViewModel: SpaceCreationEmailInvitesViewModelType } } - override class func reducer(state: inout SpaceCreationEmailInvitesViewState, action: SpaceCreationEmailInvitesStateAction) { - switch action { - case .updateEmailValidity(let emailValidity): - state.emailAddressesValid = emailValidity - case .updateLoading(let isLoading): - state.loading = isLoading - } - } - private func done() { - self.creationParameters.emailInvites = self.context.emailInvites - self.creationParameters.inviteType = .email - let emailAddressesValidity = service.validate(self.context.emailInvites) + creationParameters.emailInvites = context.emailInvites + creationParameters.inviteType = .email - dispatch(action: .updateEmailValidity(emailAddressesValidity)) - if self.context.emailInvites.reduce(true, { $0 && $1.isEmpty }) { + let emailAddressesValidity = service.validate(context.emailInvites) + state.emailAddressesValid = emailAddressesValidity + + if context.emailInvites.reduce(true, { $0 && $1.isEmpty }) { completion?(.done) - } else if emailAddressesValidity.reduce(true, { $0 && $1}) { + } else if emailAddressesValidity.reduce(true, { $0 && $1 }) { if service.isIdentityServiceReady { completion?(.done) } else { diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/ViewModel/SpaceCreationEmailInvitesViewModelProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/ViewModel/SpaceCreationEmailInvitesViewModelProtocol.swift index 88c154805..72b28d33b 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/ViewModel/SpaceCreationEmailInvitesViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/ViewModel/SpaceCreationEmailInvitesViewModelProtocol.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import Foundation protocol SpaceCreationEmailInvitesViewModelProtocol { - var completion: ((SpaceCreationEmailInvitesViewModelResult) -> Void)? { get set } var context: SpaceCreationEmailInvitesViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Coordinator/SpaceCreationMatrixItemChooserViewProvider.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Coordinator/SpaceCreationMatrixItemChooserViewProvider.swift index a4e80561f..f22a4a978 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Coordinator/SpaceCreationMatrixItemChooserViewProvider.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Coordinator/SpaceCreationMatrixItemChooserViewProvider.swift @@ -18,6 +18,6 @@ import SwiftUI class SpaceCreationMatrixItemChooserViewProvider: MatrixItemChooserCoordinatorViewProvider { func view(with viewModel: MatrixItemChooserViewModelType.Context) -> AnyView { - return AnyView(SpaceCreationMatrixItemChooser(viewModel: viewModel)) + AnyView(SpaceCreationMatrixItemChooser(viewModel: viewModel)) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationAddRoomsItemsProcessor.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationAddRoomsItemsProcessor.swift index 4638d585e..03c32f7f7 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationAddRoomsItemsProcessor.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationAddRoomsItemsProcessor.swift @@ -17,7 +17,6 @@ import Foundation class SpaceCreationAddRoomsItemsProcessor: MatrixItemChooserProcessorProtocol { - // MARK: Private private let creationParams: SpaceCreationParameters @@ -41,8 +40,7 @@ class SpaceCreationAddRoomsItemsProcessor: MatrixItemChooserProcessorProtocol { completion(.success(())) } - func isItemIncluded(_ item: (MatrixListItemData)) -> Bool { - return true + func isItemIncluded(_ item: MatrixListItemData) -> Bool { + true } - } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationInviteUsersItemsProcessor.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationInviteUsersItemsProcessor.swift index 327f6589b..5fb15d88a 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationInviteUsersItemsProcessor.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/Service/MatrixSDK/SpaceCreationInviteUsersItemsProcessor.swift @@ -17,7 +17,6 @@ import Foundation class SpaceCreationInviteUsersItemsProcessor: MatrixItemChooserProcessorProtocol { - // MARK: Private private let creationParams: SpaceCreationParameters @@ -42,8 +41,7 @@ class SpaceCreationInviteUsersItemsProcessor: MatrixItemChooserProcessorProtocol completion(.success(())) } - func isItemIncluded(_ item: (MatrixListItemData)) -> Bool { - return true + func isItemIncluded(_ item: MatrixListItemData) -> Bool { + true } - } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/View/SpaceCreationMatrixItemChooser.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/View/SpaceCreationMatrixItemChooser.swift index 856b5c6c1..f1d6f5453 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/View/SpaceCreationMatrixItemChooser.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMatrixItemChooser/View/SpaceCreationMatrixItemChooser.swift @@ -17,7 +17,6 @@ import SwiftUI struct SpaceCreationMatrixItemChooser: View { - // MARK: Properties @ObservedObject var viewModel: MatrixItemChooserViewModel.Context @@ -52,7 +51,6 @@ struct SpaceCreationMatrixItemChooser: View { } } - @ViewBuilder private var footerView: some View { ThemableButton(icon: nil, title: viewModel.viewState.selectedItemIds.isEmpty ? VectorL10n.skip : VectorL10n.next) { diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift index 286f9b2e2..8d590936d 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinator.swift @@ -17,11 +17,10 @@ */ import Foundation -import UIKit import SwiftUI +import UIKit final class SpaceCreationMenuCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -63,12 +62,11 @@ final class SpaceCreationMenuCoordinator: Coordinator, Presentable { self.callback?(.cancel) case .back: self.callback?(.back) - break } } } func toPresentable() -> UIViewController { - return self.spaceCreationMenuHostingController + spaceCreationMenuHostingController } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinatorParamaters.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinatorParamaters.swift index 099c12b68..51c8108b3 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinatorParamaters.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Coordinator/SpaceCreationMenuCoordinatorParamaters.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuCoordinatorAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuCoordinatorAction.swift index c3715ee52..148623a05 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuCoordinatorAction.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuCoordinatorAction.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuRoom.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuRoom.swift index 27c95e48c..1d16fa0a2 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuRoom.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuRoom.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -38,4 +38,4 @@ struct SpaceCreationMenuRoomOption { let detail: String } -extension SpaceCreationMenuRoomOption: Identifiable, Equatable {} +extension SpaceCreationMenuRoomOption: Identifiable, Equatable { } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuViewAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuViewAction.swift index 7d0afb05e..35e44e4a1 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuViewAction.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuViewAction.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuViewModelAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuViewModelAction.swift index 079ddab25..f8e415d34 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuViewModelAction.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuViewModelAction.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuViewState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuViewState.swift index d25f2bf1a..fd71eaf89 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuViewState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuViewState.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationParameters.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationParameters.swift index a9c2036f4..a07e840de 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationParameters.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationParameters.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,26 +28,31 @@ class SpaceCreationParameters { isModified = true } } + var topic: String? { didSet { isModified = true } } + var address: String? { didSet { isModified = true } } + var userDefinedAddress: String? { didSet { isModified = true } } - var isPublic: Bool = false { + + var isPublic = false { didSet { isModified = true } } + var showAddress: Bool { isPublic } @@ -57,7 +62,8 @@ class SpaceCreationParameters { isModified = true } } - var isShared: Bool = false { + + var isShared = false { didSet { isModified = true } @@ -84,22 +90,26 @@ class SpaceCreationParameters { isModified = true } } + var userDefinedEmailInvites: [String] { - return emailInvites.filter { address in - return !address.isEmpty + emailInvites.filter { address in + !address.isEmpty } } + var userIdInvites: [String] = [] { didSet { isModified = true } } + var inviteType: SpaceCreationInviteType = .email { didSet { isModified = true } } - var isModified: Bool = false + + var isModified = false } struct SpaceCreationNewRoom: Equatable { @@ -107,6 +117,6 @@ struct SpaceCreationNewRoom: Equatable { var defaultName: String static func == (lhs: Self, rhs: Self) -> Bool { - return lhs.defaultName == rhs.defaultName && lhs.name == rhs.name + lhs.defaultName == rhs.defaultName && lhs.name == rhs.name } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Test/UI/SpaceCreationMenuUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Test/UI/SpaceCreationMenuUITests.swift index 251fb5429..d6c5d47df 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Test/UI/SpaceCreationMenuUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Test/UI/SpaceCreationMenuUITests.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,14 +16,14 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class SpaceCreationMenuUITests: MockScreenTestCase { func testSpaceCreationMenuOptions() { app.goToScreenWithIdentifier(MockSpaceCreationMenuScreenState.options.title) - let optionButtonCount = app.buttons.matching(identifier:"optionButton").count + let optionButtonCount = app.buttons.matching(identifier: "optionButton").count XCTAssertEqual(optionButtonCount, 2) let titleText = app.staticTexts["titleText"] diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Test/Unit/SpaceCreationMenuViewModelTests.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Test/Unit/SpaceCreationMenuViewModelTests.swift index 72c694556..9c3561724 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Test/Unit/SpaceCreationMenuViewModelTests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Test/Unit/SpaceCreationMenuViewModelTests.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,14 +16,13 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI class SpaceCreationMenuViewModelTests: XCTestCase { - private enum Constants { - } + private enum Constants { } let navTitle = VectorL10n.spacesCreateSpaceTitle var creationParams = SpaceCreationParameters() diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/View/SpaceCreationMenu.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/View/SpaceCreationMenu.swift index fc1c80593..aac7cb983 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/View/SpaceCreationMenu.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/View/SpaceCreationMenu.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import SwiftUI struct SpaceCreationMenu: View { - // MARK: - Properties @ObservedObject var viewModel: SpaceCreationMenuViewModelType.Context @@ -100,7 +99,6 @@ struct SpaceCreationMenu: View { // MARK: - Previews struct SpaceCreationMenu_Previews: PreviewProvider { - static let stateRenderer = MockSpaceCreationMenuScreenState.stateRenderer static var previews: some View { diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/ViewModel/SpaceCreationMenuViewModel.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/ViewModel/SpaceCreationMenuViewModel.swift index 62b48d3a7..b1bd7e395 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/ViewModel/SpaceCreationMenuViewModel.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/ViewModel/SpaceCreationMenuViewModel.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,14 +16,12 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI -typealias SpaceCreationMenuViewModelType = StateStoreViewModel +typealias SpaceCreationMenuViewModelType = StateStoreViewModel + class SpaceCreationMenuViewModel: SpaceCreationMenuViewModelType, SpaceCreationMenuViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -43,7 +41,7 @@ class SpaceCreationMenuViewModel: SpaceCreationMenuViewModelType, SpaceCreationM } private static func defaultState(navTitle: String?, creationParams: SpaceCreationParameters, title: String, detail: String, options: [SpaceCreationMenuRoomOption]) -> SpaceCreationMenuViewState { - var navigationTitle: String = "" + var navigationTitle = "" if let navTitle = navTitle { navigationTitle = navTitle } else { @@ -60,13 +58,13 @@ class SpaceCreationMenuViewModel: SpaceCreationMenuViewModelType, SpaceCreationM case .didSelectOption(let optionId): switch optionId { case .publicSpace: - self.creationParams.isPublic = true + creationParams.isPublic = true case .privateSpace: - self.creationParams.isPublic = false + creationParams.isPublic = false case .ownedPrivateSpace: - self.creationParams.isShared = false + creationParams.isShared = false case .sharedPrivateSpace: - self.creationParams.isShared = true + creationParams.isShared = true } didSelectOption(withId: optionId) diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/ViewModel/SpaceCreationMenuViewModelProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/ViewModel/SpaceCreationMenuViewModelProtocol.swift index f3660dd08..29fd9b10a 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/ViewModel/SpaceCreationMenuViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/ViewModel/SpaceCreationMenuViewModelProtocol.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift index f5c46f7e6..a1838458d 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinator.swift @@ -17,11 +17,10 @@ */ import Foundation -import UIKit import SwiftUI +import UIKit final class SpaceCreationPostProcessCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -50,6 +49,7 @@ final class SpaceCreationPostProcessCoordinator: Coordinator, Presentable { } // MARK: - Public + func start() { MXLog.debug("[SpaceCreationPostProcessCoordinator] did start.") spaceCreationPostProcessViewModel.completion = { [weak self] result in @@ -65,6 +65,6 @@ final class SpaceCreationPostProcessCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.spaceCreationPostProcessHostingController + spaceCreationPostProcessHostingController } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinatorParameters.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinatorParameters.swift index 68a99c879..b71b1f773 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Coordinator/SpaceCreationPostProcessCoordinatorParameters.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationPostProcess SpaceCreationPostProcess -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessCoordinatorAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessCoordinatorAction.swift index d384c3a5d..5d27c5fa2 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessCoordinatorAction.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessCoordinatorAction.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessStateAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessStateAction.swift index 31003ed4e..880de2a4d 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessStateAction.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessStateAction.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationPostProcess SpaceCreationPostProcess -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessTask.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessTask.swift index 6c2d8baf1..c02e27f55 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessTask.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessTask.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -38,11 +38,12 @@ struct SpaceCreationPostProcessTask: Equatable { let title: String var state: SpaceCreationPostProcessTaskState var isFinished: Bool { - return state == .failure || state == .success + state == .failure || state == .success } + var subTasks: [SpaceCreationPostProcessTask] = [] static func == (lhs: SpaceCreationPostProcessTask, rhs: SpaceCreationPostProcessTask) -> Bool { - return lhs.type == rhs.type && lhs.title == rhs.title && lhs.state == rhs.state && lhs.subTasks == lhs.subTasks + lhs.type == rhs.type && lhs.title == rhs.title && lhs.state == rhs.state && lhs.subTasks == lhs.subTasks } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessViewAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessViewAction.swift index dc7397f46..ad1a80a15 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessViewAction.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessViewAction.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationPostProcess SpaceCreationPostProcess -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessViewModelResult.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessViewModelResult.swift index 38ee7179d..39221a3c1 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessViewModelResult.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessViewModelResult.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationPostProcess SpaceCreationPostProcess -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessViewState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessViewState.swift index 311b2fcba..1fe80fff8 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessViewState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Model/SpaceCreationPostProcessViewState.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationPostProcess SpaceCreationPostProcess -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/MatrixSDK/SpaceCreationPostProcessService.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/MatrixSDK/SpaceCreationPostProcessService.swift index 99a45c3cd..4fd4fe06d 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/MatrixSDK/SpaceCreationPostProcessService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/MatrixSDK/SpaceCreationPostProcessService.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationPostProcess SpaceCreationPostProcess -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,12 +16,11 @@ // limitations under the License. // -import Foundation import Combine +import Foundation import MatrixSDK class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol { - // MARK: - Properties // MARK: Private @@ -39,19 +38,16 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol { createdSpaceId = createdSpace?.spaceId } } + private var createdRoomsByName: [String: MXRoom] = [:] private var currentSubTaskIndex = 0 private var processingQueue = DispatchQueue(label: "io.element.MXSpace.processingQueue", attributes: .concurrent) - private lazy var stateEventBuilder: MXRoomInitialStateEventBuilder = { - return MXRoomInitialStateEventBuilder() - }() + private lazy var stateEventBuilder = MXRoomInitialStateEventBuilder() - private lazy var mediaUploader: MXMediaLoader = { - return MXMediaManager.prepareUploader(withMatrixSession: session, initialRange: 0, andRange: 1.0) - }() + private lazy var mediaUploader: MXMediaLoader = MXMediaManager.prepareUploader(withMatrixSession: session, initialRange: 0, andRange: 1.0) // MARK: Public @@ -61,8 +57,9 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol { let alias = creationParams.userDefinedAddress.isEmptyOrNil ? creationParams.address : creationParams.userDefinedAddress return AvatarInput(mxContentUri: alias, matrixItemId: "", displayName: creationParams.name) } + var avatarImage: UIImage? { - return creationParams.userSelectedAvatar + creationParams.userSelectedAvatar } // MARK: - Setup @@ -71,18 +68,17 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol { self.session = session self.parentSpaceId = parentSpaceId self.creationParams = creationParams - self.tasks = Self.tasks(with: creationParams) - self.tasksSubject = CurrentValueSubject(tasks) + tasks = Self.tasks(with: creationParams) + tasksSubject = CurrentValueSubject(tasks) } - deinit { - } + deinit { } // MARK: - Public func run() { - self.isRetry = self.currentTaskIndex > 0 - self.currentTaskIndex = -1 + isRetry = currentTaskIndex > 0 + currentTaskIndex = -1 runNextTask() } @@ -106,13 +102,13 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol { tasks.append(SpaceCreationPostProcessTask(type: .addRooms, title: VectorL10n.spacesCreationPostProcessAddingRooms("\(addedRoomIds.count)"), state: .none, subTasks: subTasks)) } } else { - tasks.append(contentsOf: creationParams.newRooms.compactMap({ room in + tasks.append(contentsOf: creationParams.newRooms.compactMap { room in guard !room.name.isEmpty else { return nil } return SpaceCreationPostProcessTask(type: .createRoom(room.name), title: VectorL10n.spacesCreationPostProcessCreatingRoom(room.name), state: .none) - })) + }) } if creationParams.inviteType == .email { @@ -183,7 +179,7 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol { return } - parentSpace.addChild(roomId: createdSpaceId) { [weak self] response in + parentSpace.addChild(roomId: createdSpaceId) { [weak self] _ in guard let self = self else { return } self.updateCurrentTask(with: .success) @@ -194,45 +190,45 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol { } private func uploadAvatar(andUpdate task: SpaceCreationPostProcessTask) { - self.updateCurrentTask(with: .started) + updateCurrentTask(with: .started) - guard let avatar = creationParams.userSelectedAvatar, let spaceRoom = self.createdSpace?.room else { - self.updateCurrentTask(with: .success) - self.runNextTask() + guard let avatar = creationParams.userSelectedAvatar, let spaceRoom = createdSpace?.room else { + updateCurrentTask(with: .success) + runNextTask() return } let avatarUp = MXKTools.forceImageOrientationUp(avatar) mediaUploader.uploadData(avatarUp?.jpegData(compressionQuality: 0.5), filename: nil, mimeType: "image/jpeg", - success: { [weak self] (urlString) in - guard let self = self else { return } - guard let urlString = urlString else { return } - guard let url = URL(string: urlString) else { return } + success: { [weak self] urlString in + guard let self = self else { return } + guard let urlString = urlString else { return } + guard let url = URL(string: urlString) else { return } - self.setAvatar(ofRoom: spaceRoom, withURL: url, andUpdate: task) + self.setAvatar(ofRoom: spaceRoom, withURL: url, andUpdate: task) }, - failure: { [weak self] (error) in - guard let self = self else { return } + failure: { [weak self] _ in + guard let self = self else { return } - self.updateCurrentTask(with: .failure) - self.runNextTask() + self.updateCurrentTask(with: .failure) + self.runNextTask() }) } private func setAvatar(ofRoom room: MXRoom, withURL url: URL, andUpdate task: SpaceCreationPostProcessTask) { updateCurrentTask(with: .started) - room.setAvatar(url: url) { [weak self] (response) in + room.setAvatar(url: url) { [weak self] response in guard let self = self else { return } - self.updateCurrentTask(with: response.isSuccess ? .success: .failure) + self.updateCurrentTask(with: response.isSuccess ? .success : .failure) self.runNextTask() } } private func createRoom(withName roomName: String, andUpdate task: SpaceCreationPostProcessTask) { - guard let createdSpace = self.createdSpace else { + guard let createdSpace = createdSpace else { updateCurrentTask(with: .failure) runNextTask() return @@ -257,7 +253,7 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol { } private func addToSpace(room: MXRoom) { - guard let createdSpace = self.createdSpace else { + guard let createdSpace = createdSpace else { updateCurrentTask(with: .failure) runNextTask() return @@ -282,7 +278,7 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol { } private func inviteNextUserByEmail() { - guard let createdSpace = self.createdSpace, let room = createdSpace.room else { + guard let createdSpace = createdSpace, let room = createdSpace.room else { updateCurrentTask(with: .failure) runNextTask() return @@ -291,7 +287,7 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol { currentSubTaskIndex += 1 guard currentSubTaskIndex < tasks[currentTaskIndex].subTasks.count else { - let isSuccess = tasks[currentTaskIndex].subTasks.reduce(true, { $0 && $1.state == .success }) + let isSuccess = tasks[currentTaskIndex].subTasks.reduce(true) { $0 && $1.state == .success } updateCurrentTask(with: isSuccess ? .success : .failure) runNextTask() return @@ -306,7 +302,7 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol { } private func addNextExistingRoom() { - guard let createdSpace = self.createdSpace else { + guard let createdSpace = createdSpace else { updateCurrentTask(with: .failure) runNextTask() return @@ -315,7 +311,7 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol { currentSubTaskIndex += 1 guard currentSubTaskIndex < tasks[currentTaskIndex].subTasks.count else { - let isSuccess = tasks[currentTaskIndex].subTasks.reduce(true, { $0 && $1.state == .success }) + let isSuccess = tasks[currentTaskIndex].subTasks.reduce(true) { $0 && $1.state == .success } updateCurrentTask(with: isSuccess ? .success : .failure) runNextTask() return @@ -351,6 +347,6 @@ class SpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol { } tasks[currentTaskIndex].state = state - self.tasksSubject.send(tasks) + tasksSubject.send(tasks) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift index a8220d8cc..27c003b71 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessScreenState.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationPostProcess SpaceCreationPostProcess -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,6 @@ import SwiftUI enum MockSpaceCreationPostProcessScreenState: MockScreenState { static var screenStates: [MockScreenState] = [MockSpaceCreationPostProcessScreenState.running, MockSpaceCreationPostProcessScreenState.done, MockSpaceCreationPostProcessScreenState.doneWithError] - // A case for each state you want to represent // with specific, minimal associated data that will allow you // mock that screen. @@ -38,7 +37,7 @@ enum MockSpaceCreationPostProcessScreenState: MockScreenState { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let service: MockSpaceCreationPostProcessService switch self { case .running: diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessService.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessService.swift index 4e10a9abd..514b35fab 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/Mock/MockSpaceCreationPostProcessService.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationPostProcess SpaceCreationPostProcess -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,12 +16,11 @@ // limitations under the License. // -import Foundation import Combine +import Foundation import UIKit class MockSpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol { - static let defaultTasks: [SpaceCreationPostProcessTask] = [ SpaceCreationPostProcessTask(type: .createSpace, title: "Space creation", state: .success), SpaceCreationPostProcessTask(type: .createRoom("Room#1"), title: "Room#1 creation", state: .failure), @@ -53,22 +52,22 @@ class MockSpaceCreationPostProcessService: SpaceCreationPostProcessServiceProtoc var tasksSubject: CurrentValueSubject<[SpaceCreationPostProcessTask], Never> private(set) var createdSpaceId: String? var avatar: AvatarInput { - return AvatarInput(mxContentUri: nil, matrixItemId: "", displayName: "Some space") + AvatarInput(mxContentUri: nil, matrixItemId: "", displayName: "Some space") } + var avatarImage: UIImage? { - return nil + nil } init( tasks: [SpaceCreationPostProcessTask] = defaultTasks ) { - self.tasksSubject = CurrentValueSubject<[SpaceCreationPostProcessTask], Never>(tasks) + tasksSubject = CurrentValueSubject<[SpaceCreationPostProcessTask], Never>(tasks) } func simulateUpdate(tasks: [SpaceCreationPostProcessTask]) { - self.tasksSubject.send(tasks) + tasksSubject.send(tasks) } - func run() { - } + func run() { } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/SpaceCreationPostProcessServiceProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/SpaceCreationPostProcessServiceProtocol.swift index 7ff9e73cf..e9e4e711b 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/SpaceCreationPostProcessServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Service/SpaceCreationPostProcessServiceProtocol.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationPostProcess SpaceCreationPostProcess -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation import UIKit protocol SpaceCreationPostProcessServiceProtocol: AnyObject { diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Test/UI/SpaceCreationPostProcessUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Test/UI/SpaceCreationPostProcessUITests.swift index 75c624772..4bfcbc424 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Test/UI/SpaceCreationPostProcessUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Test/UI/SpaceCreationPostProcessUITests.swift @@ -16,8 +16,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class SpaceCreationPostProcessUITests: MockScreenTestCase { // No tests have been implemented diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Test/Unit/SpaceCreationPostProcessViewModelTests.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Test/Unit/SpaceCreationPostProcessViewModelTests.swift index cb274b1ba..6859b9565 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Test/Unit/SpaceCreationPostProcessViewModelTests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/Test/Unit/SpaceCreationPostProcessViewModelTests.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationPostProcess SpaceCreationPostProcess -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,13 +16,12 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI class SpaceCreationPostProcessViewModelTests: XCTestCase { - var service: MockSpaceCreationPostProcessService! var viewModel: SpaceCreationPostProcessViewModelProtocol! var context: SpaceCreationPostProcessViewModelType.Context! diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/View/SpaceCreationPostProcess.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/View/SpaceCreationPostProcess.swift index 3c5ad177e..38f99a935 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/View/SpaceCreationPostProcess.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/View/SpaceCreationPostProcess.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationPostProcess SpaceCreationPostProcess -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import SwiftUI struct SpaceCreationPostProcess: View { - // MARK: - Properties // MARK: Private @@ -44,7 +43,7 @@ struct SpaceCreationPostProcess: View { .navigationBarHidden(true) .background(theme.colors.background.ignoresSafeArea()) .frame(maxHeight: .infinity) - .onAppear() { + .onAppear { viewModel.send(viewAction: .runTasks) } } @@ -62,7 +61,7 @@ struct SpaceCreationPostProcess: View { @ViewBuilder private var tasksList: some View { VStack(alignment: .leading, spacing: 11) { - ForEach(viewModel.viewState.tasks.indices) { index in + ForEach(viewModel.viewState.tasks.indices, id: \.self) { index in SpaceCreationPostProcessItem(title: viewModel.viewState.tasks[index].title, state: viewModel.viewState.tasks[index].state) } } @@ -85,7 +84,7 @@ struct SpaceCreationPostProcess: View { private var avatarView: some View { ZStack { SpaceAvatarImage(mxContentUri: viewModel.viewState.avatar.mxContentUri, matrixItemId: viewModel.viewState.avatar.matrixItemId, displayName: viewModel.viewState.avatar.displayName, size: .xLarge) - .padding(6) + .padding(6) if let image = viewModel.viewState.avatarImage { Image(uiImage: image) .resizable() @@ -95,7 +94,6 @@ struct SpaceCreationPostProcess: View { } } } - } // MARK: - Previews diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/View/SpaceCreationPostProcessItem.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/View/SpaceCreationPostProcessItem.swift index 8fe554b10..58f69884f 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/View/SpaceCreationPostProcessItem.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/View/SpaceCreationPostProcessItem.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/ViewModel/SpaceCreationPostProcessViewModel.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/ViewModel/SpaceCreationPostProcessViewModel.swift index 57a736b09..c90e1926b 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/ViewModel/SpaceCreationPostProcessViewModel.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/ViewModel/SpaceCreationPostProcessViewModel.swift @@ -16,16 +16,12 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI +typealias SpaceCreationPostProcessViewModelType = StateStoreViewModel - -typealias SpaceCreationPostProcessViewModelType = StateStoreViewModel class SpaceCreationPostProcessViewModel: SpaceCreationPostProcessViewModelType, SpaceCreationPostProcessViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -40,7 +36,7 @@ class SpaceCreationPostProcessViewModel: SpaceCreationPostProcessViewModelType, // MARK: - Setup static func makeSpaceCreationPostProcessViewModel(spaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol) -> SpaceCreationPostProcessViewModelProtocol { - return SpaceCreationPostProcessViewModel(spaceCreationPostProcessService: spaceCreationPostProcessService) + SpaceCreationPostProcessViewModel(spaceCreationPostProcessService: spaceCreationPostProcessService) } private init(spaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol) { @@ -55,16 +51,27 @@ class SpaceCreationPostProcessViewModel: SpaceCreationPostProcessViewModelType, avatar: spaceCreationPostProcessService.avatar, avatarImage: spaceCreationPostProcessService.avatarImage, tasks: tasks, - isFinished: tasks.first?.state == .failure || tasks.reduce(true, { result, task in result && task.isFinished }), - errorCount: tasks.reduce(0, { result, task in result + (task.state == .failure ? 1 : 0) }) + isFinished: tasks.first?.state == .failure || tasks.reduce(true) { result, task in result && task.isFinished }, + errorCount: tasks.reduce(0) { result, task in result + (task.state == .failure ? 1 : 0) } ) } private func setupTasksObserving() { - let tasksUpdatePublisher = spaceCreationPostProcessService.tasksSubject - .map(SpaceCreationPostProcessStateAction.updateTasks) - .eraseToAnyPublisher() - dispatch(actionPublisher: tasksUpdatePublisher) + spaceCreationPostProcessService + .tasksSubject + .sink(receiveValue: { [weak self] tasks in + guard let self = self else { return } + + self.state.tasks = tasks + self.state.isFinished = tasks.first?.state == .failure || tasks.reduce(true) { result, task in result && task.isFinished } + self.state.errorCount = tasks.reduce(0) { result, task in result + (task.state == .failure ? 1 : 0) } + + NotificationCenter.default.post(name: SpaceCreationPostProcessViewModel.didUpdate, + object: nil, + userInfo: [SpaceCreationPostProcessViewModel.newStateKey: self.state]) + }) + .store(in: &cancellables) + updateNotificationObserver = NotificationCenter.default.addObserver(forName: SpaceCreationPostProcessViewModel.didUpdate, object: nil, queue: OperationQueue.main) { [weak self] notification in guard let self = self else { return @@ -74,7 +81,7 @@ class SpaceCreationPostProcessViewModel: SpaceCreationPostProcessViewModelType, return } - if state.isFinished && state.errorCount == 0 { + if state.isFinished, state.errorCount == 0 { guard let spaceId = self.spaceCreationPostProcessService.createdSpaceId else { self.cancel() return @@ -104,19 +111,6 @@ class SpaceCreationPostProcessViewModel: SpaceCreationPostProcessViewModelType, } } - override class func reducer(state: inout SpaceCreationPostProcessViewState, action: SpaceCreationPostProcessStateAction) { - switch action { - case .updateTasks(let tasks): - state.tasks = tasks - state.isFinished = tasks.first?.state == .failure || tasks.reduce(true, { result, task in result && task.isFinished }) - state.errorCount = tasks.reduce(0, { result, task in result + (task.state == .failure ? 1 : 0) }) - } - - NotificationCenter.default.post(name: SpaceCreationPostProcessViewModel.didUpdate, object: nil, userInfo: [SpaceCreationPostProcessViewModel.newStateKey : state]) - - UILog.debug("[SpaceCreationPostProcessViewModel] reducer with action \(action) produced state: \(state)") - } - private func done(spaceId: String) { completion?(.done(spaceId)) } @@ -131,6 +125,7 @@ class SpaceCreationPostProcessViewModel: SpaceCreationPostProcessViewModelType, } // MARK: - MXSpaceService notification constants + extension SpaceCreationPostProcessViewModel { /// Posted once the process is finished public static let didUpdate = Notification.Name("SpaceCreationPostProcessViewModelDidUpdate") diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/ViewModel/SpaceCreationPostProcessViewModelProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/ViewModel/SpaceCreationPostProcessViewModelProtocol.swift index f71785b35..4d5ea2713 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/ViewModel/SpaceCreationPostProcessViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationPostProcess/ViewModel/SpaceCreationPostProcessViewModelProtocol.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationPostProcess SpaceCreationPostProcess -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import Foundation protocol SpaceCreationPostProcessViewModelProtocol { - var completion: ((SpaceCreationPostProcessViewModelResult) -> Void)? { get set } static func makeSpaceCreationPostProcessViewModel(spaceCreationPostProcessService: SpaceCreationPostProcessServiceProtocol) -> SpaceCreationPostProcessViewModelProtocol var context: SpaceCreationPostProcessViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift index e3da21122..2e94feec2 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinator.swift @@ -17,11 +17,10 @@ */ import Foundation -import UIKit import SwiftUI +import UIKit final class SpaceCreationRoomsCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -68,6 +67,6 @@ final class SpaceCreationRoomsCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.spaceCreationRoomsHostingController + spaceCreationRoomsHostingController } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinatorParameters.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinatorParameters.swift index 6e095f71b..04a85dcac 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Coordinator/SpaceCreationRoomsCoordinatorParameters.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationRooms SpaceCreationRooms -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsCoordinatorAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsCoordinatorAction.swift index 749d87bd1..7455b5aa6 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsCoordinatorAction.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsCoordinatorAction.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsPresence.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsPresence.swift index 3790e6207..052148c8d 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsPresence.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsPresence.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationRooms SpaceCreationRooms -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewAction.swift index c7ed6ce24..a1586ca41 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewAction.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewAction.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationRooms SpaceCreationRooms -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewModelBindings.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewModelBindings.swift index f9d2ad6e7..e5f17b85a 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewModelBindings.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewModelBindings.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewModelResult.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewModelResult.swift index 8368baacc..96c036877 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewModelResult.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewModelResult.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationRooms SpaceCreationRooms -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewState.swift index 81bf820ec..7d469c314 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsViewState.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationRooms SpaceCreationRooms -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift index fd5919029..3ef06a64f 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Service/Mock/MockSpaceCreationRoomsScreenState.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationRooms SpaceCreationRooms -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,7 +39,7 @@ enum MockSpaceCreationRoomsScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let creationParams = SpaceCreationParameters() switch self { case .defaultValues: break diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Test/UI/SpaceCreationRoomsUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Test/UI/SpaceCreationRoomsUITests.swift index 2dedafb37..90cdff13b 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Test/UI/SpaceCreationRoomsUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Test/UI/SpaceCreationRoomsUITests.swift @@ -16,8 +16,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class SpaceCreationRoomsUITests: MockScreenTestCase { func testDefaultValues() { diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Test/Unit/SpaceCreationRoomsViewModelTests.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Test/Unit/SpaceCreationRoomsViewModelTests.swift index c09e85826..3960a2d05 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Test/Unit/SpaceCreationRoomsViewModelTests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Test/Unit/SpaceCreationRoomsViewModelTests.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationRooms SpaceCreationRooms -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,8 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/View/SpaceCreationRooms.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/View/SpaceCreationRooms.swift index 8137dce9f..fbb86d7f4 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/View/SpaceCreationRooms.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/View/SpaceCreationRooms.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationRooms SpaceCreationRooms -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import SwiftUI struct SpaceCreationRooms: View { - // MARK: - Properties // MARK: Private @@ -50,7 +49,7 @@ struct SpaceCreationRooms: View { VStack { GeometryReader { reader in ScrollView { - ScrollViewReader { scrollViewReader in + ScrollViewReader { _ in VStack(spacing: 20) { Text(VectorL10n.spacesCreationNewRoomsTitle) .multilineTextAlignment(.center) @@ -61,8 +60,8 @@ struct SpaceCreationRooms: View { .font(theme.fonts.body) .foregroundColor(theme.colors.secondaryContent) Spacer() - ForEach(viewModel.rooms.indices) { index in - RoundedBorderTextField(title: VectorL10n.spacesCreationNewRoomsRoomNameTitle, placeHolder: viewModel.rooms[index].defaultName, text: $viewModel.rooms[index].name, configuration: UIKitTextInputConfiguration( returnKeyType: index < viewModel.rooms.endIndex - 1 ? .next : .done)) + ForEach(viewModel.rooms.indices, id: \.self) { index in + RoundedBorderTextField(title: VectorL10n.spacesCreationNewRoomsRoomNameTitle, placeHolder: viewModel.rooms[index].defaultName, text: $viewModel.rooms[index].name, configuration: UIKitTextInputConfiguration(returnKeyType: index < viewModel.rooms.endIndex - 1 ? .next : .done)) .accessibility(identifier: "roomTextField") } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/ViewModel/SpaceCreationRoomsViewModel.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/ViewModel/SpaceCreationRoomsViewModel.swift index dfb989a80..d673cd2b2 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/ViewModel/SpaceCreationRoomsViewModel.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/ViewModel/SpaceCreationRoomsViewModel.swift @@ -16,16 +16,12 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI +typealias SpaceCreationRoomsViewModelType = StateStoreViewModel - -typealias SpaceCreationRoomsViewModelType = StateStoreViewModel class SpaceCreationRoomsViewModel: SpaceCreationRoomsViewModelType, SpaceCreationRoomsViewModelProtocol { - // MARK: - Setup // MARK: Private @@ -51,7 +47,6 @@ class SpaceCreationRoomsViewModel: SpaceCreationRoomsViewModelType, SpaceCreatio ) } - // MARK: - Public override func process(viewAction: SpaceCreationRoomsViewAction) { @@ -64,14 +59,11 @@ class SpaceCreationRoomsViewModel: SpaceCreationRoomsViewModelType, SpaceCreatio done() } } - - override class func reducer(state: inout SpaceCreationRoomsViewState, action: SpaceCreationRoomsStateAction) { - } // MARK: - Private private func done() { - self.creationParameters.newRooms = self.context.rooms + creationParameters.newRooms = context.rooms callback?(.done) } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/ViewModel/SpaceCreationRoomsViewModelProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/ViewModel/SpaceCreationRoomsViewModelProtocol.swift index f7bbd263b..dac8fbe83 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/ViewModel/SpaceCreationRoomsViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/ViewModel/SpaceCreationRoomsViewModelProtocol.swift @@ -1,6 +1,6 @@ // File created from SimpleUserProfileExample // $ createScreen.sh Spaces/SpaceCreation/SpaceCreationRooms SpaceCreationRooms -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import Foundation protocol SpaceCreationRoomsViewModelProtocol { - var callback: ((SpaceCreationRoomsViewModelResult) -> Void)? { get set } var context: SpaceCreationRoomsViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift index 863b5a4ee..d525297c6 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinator.swift @@ -17,11 +17,10 @@ */ import Foundation -import UIKit import SwiftUI +import UIKit final class SpaceCreationSettingsCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -72,13 +71,12 @@ final class SpaceCreationSettingsCoordinator: Coordinator, Presentable { self.callback?(.back) case .pickImage(let sourceRect): self.pickImage(from: sourceRect) - break } } } func toPresentable() -> UIViewController { - return self.spaceCreationSettingsHostingController + spaceCreationSettingsHostingController } // MARK: - Private @@ -91,6 +89,7 @@ final class SpaceCreationSettingsCoordinator: Coordinator, Presentable { } // MARK: - SingleImagePickerPresenterDelegate + extension SpaceCreationSettingsCoordinator: SingleImagePickerPresenterDelegate { func singleImagePickerPresenter(_ presenter: SingleImagePickerPresenter, didSelectImageData imageData: Data, withUTI uti: MXKUTI?) { spaceCreationSettingsViewModel.updateAvatarImage(with: UIImage(data: imageData)) diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinatorParamaters.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinatorParamaters.swift index f79a0c749..5a865710e 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinatorParamaters.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Coordinator/SpaceCreationSettingsCoordinatorParamaters.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsAddressValidationStatus.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsAddressValidationStatus.swift index b7262a430..2457e1ae8 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsAddressValidationStatus.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsAddressValidationStatus.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsCoordinatorAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsCoordinatorAction.swift index b94d0d3c2..719b213f7 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsCoordinatorAction.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsCoordinatorAction.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsStateAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsStateAction.swift deleted file mode 100644 index 670015eae..000000000 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsStateAction.swift +++ /dev/null @@ -1,29 +0,0 @@ -// File created from TemplateAdvancedRoomsExample -// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import UIKit - -/// Actions to be performed on the `ViewModel` State -enum SpaceCreationSettingsStateAction { - case updateRoomNameError(String?) - case updateRoomDefaultAddress(String) - case updateAddressValidationStatus(SpaceCreationSettingsAddressValidationStatus) - case updateAvatar(AvatarInputProtocol) - case updateAvatarImage(UIImage?) -} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewAction.swift index de2350fc9..c8d9dadcc 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewAction.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewAction.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewModelAction.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewModelAction.swift index 94b838e38..7eb605e8c 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewModelAction.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewModelAction.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewModelBindings.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewModelBindings.swift index 0a1757979..0dafd1f6c 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewModelBindings.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewModelBindings.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewState.swift index 663d968df..a61da6598 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Model/SpaceCreationSettingsViewState.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,4 +31,3 @@ struct SpaceCreationSettingsViewState: BindableState { var avatarImage: UIImage? var bindings: SpaceCreationSettingsViewModelBindings } - diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/MatrixSDK/SpaceCreationSettingsService.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/MatrixSDK/SpaceCreationSettingsService.swift index bf5835b62..9b7e2d846 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/MatrixSDK/SpaceCreationSettingsService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/MatrixSDK/SpaceCreationSettingsService.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,12 +16,11 @@ // limitations under the License. // -import Foundation import Combine +import Foundation import MatrixSDK class SpaceCreationSettingsService: SpaceCreationSettingsServiceProtocol { - // MARK: - Properties var roomName: String { @@ -30,6 +29,7 @@ class SpaceCreationSettingsService: SpaceCreationSettingsServiceProtocol { updateAvatar() } } + var userDefinedAddress: String? { didSet { validateAddress() @@ -45,10 +45,12 @@ class SpaceCreationSettingsService: SpaceCreationSettingsServiceProtocol { validateAddress() } } - private var lastValidatedAddress: String = "" + + private var lastValidatedAddress = "" private var currentAddress: String? { - return self.userDefinedAddress?.count ?? 0 > 0 ? self.userDefinedAddress : defaultAddress + userDefinedAddress?.count ?? 0 > 0 ? userDefinedAddress : defaultAddress } + private var currentOperation: MXHTTPOperation? // MARK: Public @@ -69,14 +71,14 @@ class SpaceCreationSettingsService: SpaceCreationSettingsServiceProtocol { init(roomName: String, userDefinedAddress: String?, session: MXSession) { self.session = session - self.defaultAddress = "" - self.defaultAddressSubject = CurrentValueSubject(defaultAddress) + defaultAddress = "" + defaultAddressSubject = CurrentValueSubject(defaultAddress) self.roomName = roomName - self.addressValidationSubject = CurrentValueSubject(.none("#")) - self.avatarViewDataSubject = CurrentValueSubject(AvatarInput(mxContentUri: userDefinedAddress, matrixItemId: "", displayName: roomName)) + addressValidationSubject = CurrentValueSubject(.none("#")) + avatarViewDataSubject = CurrentValueSubject(AvatarInput(mxContentUri: userDefinedAddress, matrixItemId: "", displayName: roomName)) - self.updateDefaultAddress() - self.validateAddress() + updateDefaultAddress() + validateAddress() } deinit { @@ -89,7 +91,7 @@ class SpaceCreationSettingsService: SpaceCreationSettingsServiceProtocol { // MARK: Private private func updateAvatar() { - self.avatarViewDataSubject.send(AvatarInput(mxContentUri: currentAddress, matrixItemId: "", displayName: roomName)) + avatarViewDataSubject.send(AvatarInput(mxContentUri: currentAddress, matrixItemId: "", displayName: roomName)) } private func updateDefaultAddress() { @@ -100,7 +102,7 @@ class SpaceCreationSettingsService: SpaceCreationSettingsServiceProtocol { currentOperation?.cancel() currentOperation = nil - guard let userDefinedAddress = self.userDefinedAddress, !userDefinedAddress.isEmpty else { + guard let userDefinedAddress = userDefinedAddress, !userDefinedAddress.isEmpty else { let fullAddress = MXTools.fullLocalAlias(from: defaultAddress, with: session) if defaultAddress.isEmpty { diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift index cf18234d6..8738289a9 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsScreenState.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import Foundation import SwiftUI - /// Using an enum for the screen allows you define the different state cases with /// the relevant associated data for each case. enum MockSpaceCreationSettingsScreenState: MockScreenState, CaseIterable { @@ -40,7 +39,7 @@ enum MockSpaceCreationSettingsScreenState: MockScreenState, CaseIterable { let creationParameters = SpaceCreationParameters() creationParameters.name = "Fake" - let service: MockSpaceCreationSettingsService = MockSpaceCreationSettingsService() + let service = MockSpaceCreationSettingsService() switch self { case .privateSpace: creationParameters.isPublic = false @@ -60,7 +59,7 @@ enum MockSpaceCreationSettingsScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(SpaceCreationSettings(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .addDependency(MockAvatarService.example)) ) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsService.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsService.swift index 5e9fa46d4..616381292 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/Mock/MockSpaceCreationSettingsService.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,19 +16,17 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class MockSpaceCreationSettingsService: SpaceCreationSettingsServiceProtocol { - - var addressValidationSubject: CurrentValueSubject var avatarViewDataSubject: CurrentValueSubject var defaultAddressSubject: CurrentValueSubject var spaceAddress: String? var roomName: String var userDefinedAddress: String? - var isAddressValid: Bool = true + var isAddressValid = true init() { roomName = "Fake" @@ -38,7 +36,7 @@ class MockSpaceCreationSettingsService: SpaceCreationSettingsServiceProtocol { } func simulateUpdate(addressValidationStatus: SpaceCreationSettingsAddressValidationStatus) { - self.addressValidationSubject.value = addressValidationStatus + addressValidationSubject.value = addressValidationStatus } // func simulateUpdate() diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/SpaceCreationSettingsServiceProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/SpaceCreationSettingsServiceProtocol.swift index 7b594fecc..35a176ed8 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/SpaceCreationSettingsServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Service/SpaceCreationSettingsServiceProtocol.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation protocol SpaceCreationSettingsServiceProtocol: AnyObject { var defaultAddressSubject: CurrentValueSubject { get } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Test/UI/SpaceCreationSettingsUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Test/UI/SpaceCreationSettingsUITests.swift index f1f0e705d..685488026 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Test/UI/SpaceCreationSettingsUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Test/UI/SpaceCreationSettingsUITests.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class SpaceCreationSettingsUITests: MockScreenTestCase { func testPrivateSpace() { diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Test/Unit/SpaceCreationSettingsViewModelTests.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Test/Unit/SpaceCreationSettingsViewModelTests.swift index dcefa9fb7..4a0e74afd 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Test/Unit/SpaceCreationSettingsViewModelTests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/Test/Unit/SpaceCreationSettingsViewModelTests.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,20 +16,18 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI class SpaceCreationSettingsViewModelTests: XCTestCase { - let creationParameters = SpaceCreationParameters() var service: MockSpaceCreationSettingsService! var viewModel: SpaceCreationSettingsViewModel! var context: SpaceCreationSettingsViewModel.Context! var cancellables = Set() - override func setUpWithError() throws { creationParameters.name = "Fake" creationParameters.isPublic = true diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/View/SpaceCreationSettings.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/View/SpaceCreationSettings.swift index d8e697dcd..9135bf8cc 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/View/SpaceCreationSettings.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/View/SpaceCreationSettings.swift @@ -16,11 +16,10 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI struct SpaceCreationSettings: View { - // MARK: - Properties @ObservedObject var viewModel: SpaceCreationSettingsViewModel.Context @@ -69,8 +68,8 @@ struct SpaceCreationSettings: View { ZStack(alignment: .bottomTrailing) { GeometryReader { reader in ZStack { - SpaceAvatarImage(mxContentUri: viewModel.viewState.avatar.mxContentUri, matrixItemId: viewModel.viewState.avatar.matrixItemId, displayName: viewModel.viewState.avatar.displayName, size: .xxLarge) - .padding(6) + SpaceAvatarImage(mxContentUri: viewModel.viewState.avatar.mxContentUri, matrixItemId: viewModel.viewState.avatar.matrixItemId, displayName: viewModel.viewState.avatar.displayName, size: .xxLarge) + .padding(6) if let image = viewModel.viewState.avatarImage { Image(uiImage: image) .resizable() @@ -79,10 +78,10 @@ struct SpaceCreationSettings: View { .clipShape(RoundedRectangle(cornerRadius: 8)) } }.padding(10) - .gesture(TapGesture().onEnded { _ in - ResponderManager.resignFirstResponder() - viewModel.send(viewAction: .pickImage(reader.frame(in: .global))) - }) + .gesture(TapGesture().onEnded { _ in + ResponderManager.resignFirstResponder() + viewModel.send(viewAction: .pickImage(reader.frame(in: .global))) + }) } Image(uiImage: Asset.Images.spaceCreationCamera.image) .renderingMode(.template) @@ -95,21 +94,21 @@ struct SpaceCreationSettings: View { @ViewBuilder private var formView: some View { - GeometryReader { geometryReader in + GeometryReader { _ in ScrollView { ScrollViewReader { scrollViewReader in VStack { headerView Spacer() avatarView - Spacer().frame(height:40) - RoundedBorderTextField(title: VectorL10n.createRoomPlaceholderName, placeHolder: "", text: $viewModel.roomName, footerText: viewModel.viewState.roomNameError, isError: true, isFirstResponder: false, configuration: UIKitTextInputConfiguration( returnKeyType: .next), onTextChanged: { newText in + Spacer().frame(height: 40) + RoundedBorderTextField(title: VectorL10n.createRoomPlaceholderName, placeHolder: "", text: $viewModel.roomName, footerText: viewModel.viewState.roomNameError, isError: true, isFirstResponder: false, configuration: UIKitTextInputConfiguration(returnKeyType: .next), onTextChanged: { newText in viewModel.send(viewAction: .nameChanged(newText)) }) .id("nameTextField") .padding(.horizontal, 2) .padding(.bottom, 20) - RoundedBorderTextEditor(title: nil, placeHolder: VectorL10n.spaceTopic, text: $viewModel.topic, textMaxHeight: 72, error: nil, onTextChanged: { + RoundedBorderTextEditor(title: nil, placeHolder: VectorL10n.spaceTopic, text: $viewModel.topic, textMaxHeight: 72, error: nil, onTextChanged: { newText in viewModel.send(viewAction: .topicChanged(newText)) }, onEditingChanged: { editing in @@ -121,7 +120,7 @@ struct SpaceCreationSettings: View { .padding(.horizontal, 2) .padding(.bottom, viewModel.viewState.showRoomAddress ? 20 : 3) if viewModel.viewState.showRoomAddress { - RoundedBorderTextField(title: VectorL10n.spacesCreationAddress, placeHolder: "# \(viewModel.viewState.defaultAddress)", text: $viewModel.address, footerText: viewModel.viewState.addressMessage, isError: !viewModel.viewState.isAddressValid, configuration: UIKitTextInputConfiguration(keyboardType: .URL, returnKeyType: .done, autocapitalizationType: .none), onTextChanged: { + RoundedBorderTextField(title: VectorL10n.spacesCreationAddress, placeHolder: "# \(viewModel.viewState.defaultAddress)", text: $viewModel.address, footerText: viewModel.viewState.addressMessage, isError: !viewModel.viewState.isAddressValid, configuration: UIKitTextInputConfiguration(keyboardType: .URL, returnKeyType: .done, autocapitalizationType: .none), onTextChanged: { newText in viewModel.send(viewAction: .addressChanged(newText)) }) diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/ViewModel/SpaceCreationSettingsViewModel.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/ViewModel/SpaceCreationSettingsViewModel.swift index 742504b41..6f5661a8d 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/ViewModel/SpaceCreationSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/ViewModel/SpaceCreationSettingsViewModel.swift @@ -16,15 +16,12 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI -typealias SpaceCreationSettingsViewModelType = StateStoreViewModel +typealias SpaceCreationSettingsViewModelType = StateStoreViewModel class SpaceCreationSettingsViewModel: SpaceCreationSettingsViewModelType, SpaceCreationSettingsViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -47,27 +44,35 @@ class SpaceCreationSettingsViewModel: SpaceCreationSettingsViewModelType, SpaceC } private func setupServiceObserving() { - let defaultAddressUpdatePublisher = spaceCreationSettingsService.defaultAddressSubject - .map(SpaceCreationSettingsStateAction.updateRoomDefaultAddress) - .eraseToAnyPublisher() - dispatch(actionPublisher: defaultAddressUpdatePublisher) + spaceCreationSettingsService + .defaultAddressSubject + .sink(receiveValue: { [weak self] address in + self?.state.defaultAddress = address + }) + .store(in: &cancellables) - let addressValidationUpdatePublisher = spaceCreationSettingsService.addressValidationSubject - .map(SpaceCreationSettingsStateAction.updateAddressValidationStatus) - .eraseToAnyPublisher() - dispatch(actionPublisher: addressValidationUpdatePublisher) + spaceCreationSettingsService + .addressValidationSubject + .sink(receiveValue: { [weak self] status in + self?.state.addressMessage = status.message + self?.state.isAddressValid = status.isValid + }) + .store(in: &cancellables) - let avatarUpdatePublisher = spaceCreationSettingsService.avatarViewDataSubject - .map(SpaceCreationSettingsStateAction.updateAvatar) - .eraseToAnyPublisher() - dispatch(actionPublisher: avatarUpdatePublisher) + spaceCreationSettingsService + .avatarViewDataSubject + .sink(receiveValue: { [weak self] avatar in + self?.state.avatar = avatar + }) + .store(in: &cancellables) } private static func defaultState(creationParameters: SpaceCreationParameters, validationStatus: SpaceCreationSettingsAddressValidationStatus) -> SpaceCreationSettingsViewState { let bindings = SpaceCreationSettingsViewModelBindings( roomName: creationParameters.name ?? "", topic: creationParameters.topic ?? "", - address: creationParameters.userDefinedAddress ?? "") + address: creationParameters.userDefinedAddress ?? "" + ) return SpaceCreationSettingsViewState( title: creationParameters.isPublic ? VectorL10n.spacesCreationPublicSpaceTitle : VectorL10n.spacesCreationPrivateSpaceTitle, @@ -78,14 +83,15 @@ class SpaceCreationSettingsViewModel: SpaceCreationSettingsViewModelType, SpaceC isAddressValid: validationStatus.isValid, avatar: AvatarInput(mxContentUri: nil, matrixItemId: "", displayName: nil), avatarImage: creationParameters.userSelectedAvatar, - bindings: bindings) + bindings: bindings + ) } // MARK: - Public func updateAvatarImage(with image: UIImage?) { creationParameters.userSelectedAvatar = image - dispatch(action: .updateAvatarImage(image)) + state.avatarImage = image } override func process(viewAction: SpaceCreationSettingsViewAction) { @@ -102,7 +108,7 @@ class SpaceCreationSettingsViewModel: SpaceCreationSettingsViewModelType, SpaceC spaceCreationSettingsService.roomName = newValue creationParameters.address = spaceCreationSettingsService.defaultAddressSubject.value creationParameters.name = newValue - dispatch(action: .updateRoomNameError(newValue.isEmpty ? VectorL10n.spacesCreationEmptyRoomNameError : nil)) + state.roomNameError = newValue.isEmpty ? VectorL10n.spacesCreationEmptyRoomNameError : nil case .addressChanged(let newValue): spaceCreationSettingsService.userDefinedAddress = newValue creationParameters.userDefinedAddress = newValue @@ -110,28 +116,12 @@ class SpaceCreationSettingsViewModel: SpaceCreationSettingsViewModelType, SpaceC creationParameters.topic = newValue } } - - override class func reducer(state: inout SpaceCreationSettingsViewState, action: SpaceCreationSettingsStateAction) { - switch action { - case .updateRoomNameError(let error): - state.roomNameError = error - case .updateRoomDefaultAddress(let defaultAddress): - state.defaultAddress = defaultAddress - case .updateAddressValidationStatus(let validationStatus): - state.addressMessage = validationStatus.message - state.isAddressValid = validationStatus.isValid - case .updateAvatar(let avatar): - state.avatar = avatar - case .updateAvatarImage(let image): - state.avatarImage = image - } - } - + // MARK: - Private private func done() { guard !context.roomName.isEmpty else { - dispatch(action: .updateRoomNameError(VectorL10n.spacesCreationEmptyRoomNameError)) + state.roomNameError = VectorL10n.spacesCreationEmptyRoomNameError return } @@ -144,7 +134,7 @@ class SpaceCreationSettingsViewModel: SpaceCreationSettingsViewModelType, SpaceC creationParameters.userDefinedAddress = context.address creationParameters.address = spaceCreationSettingsService.defaultAddressSubject.value - dispatch(action: .updateRoomNameError(nil)) + state.roomNameError = nil callback?(.done) } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/ViewModel/SpaceCreationSettingsViewModelProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/ViewModel/SpaceCreationSettingsViewModelProtocol.swift index 9802bca02..aa62cc895 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/ViewModel/SpaceCreationSettingsViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationSettings/ViewModel/SpaceCreationSettingsViewModelProtocol.swift @@ -1,6 +1,6 @@ // File created from TemplateAdvancedRoomsExample // $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomSheetCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomSheetCoordinator.swift index 7237b0aa3..a6c7539c2 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomSheetCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomSheetCoordinator.swift @@ -39,7 +39,6 @@ struct SpaceSelectorBottomSheetCoordinatorParameters { } final class SpaceSelectorBottomSheetCoordinator: NSObject, Coordinator, Presentable { - // MARK: - Properties private let parameters: SpaceSelectorBottomSheetCoordinatorParameters @@ -61,11 +60,11 @@ final class SpaceSelectorBottomSheetCoordinator: NSObject, Coordinator, Presenta navigationRouter: NavigationRouterType = NavigationRouter(navigationController: RiotNavigationController())) { self.parameters = parameters self.navigationRouter = navigationRouter - self.spaceIdStack = [] + spaceIdStack = [] super.init() - self.setupNavigationRouter() + setupNavigationRouter() } // MARK: - Public @@ -76,7 +75,7 @@ final class SpaceSelectorBottomSheetCoordinator: NSObject, Coordinator, Presenta } func toPresentable() -> UIViewController { - return self.navigationRouter.toPresentable() + navigationRouter.toPresentable() } // MARK: - Private @@ -84,7 +83,7 @@ final class SpaceSelectorBottomSheetCoordinator: NSObject, Coordinator, Presenta private func setupNavigationRouter() { guard #available(iOS 15.0, *) else { return } - guard let sheetController = self.navigationRouter.toPresentable().sheetPresentationController else { + guard let sheetController = navigationRouter.toPresentable().sheetPresentationController else { MXLog.debug("[SpaceSelectorBottomSheetCoordinator] setup: no sheetPresentationController found") return } @@ -94,14 +93,14 @@ final class SpaceSelectorBottomSheetCoordinator: NSObject, Coordinator, Presenta sheetController.selectedDetentIdentifier = .medium sheetController.prefersScrollingExpandsWhenScrolledToEdge = true - self.navigationRouter.toPresentable().presentationController?.delegate = self + navigationRouter.toPresentable().presentationController?.delegate = self } private func push(_ coordinator: Coordinator & Presentable) { - if self.navigationRouter.modules.isEmpty { - self.navigationRouter.setRootModule(coordinator) + if navigationRouter.modules.isEmpty { + navigationRouter.setRootModule(coordinator) } else { - self.navigationRouter.push(coordinator.toPresentable(), animated: true) { [weak self] in + navigationRouter.push(coordinator.toPresentable(), animated: true) { [weak self] in guard let self = self else { return } self.remove(childCoordinator: coordinator) @@ -145,10 +144,10 @@ final class SpaceSelectorBottomSheetCoordinator: NSObject, Coordinator, Presenta coordinator.start() - self.add(childCoordinator: coordinator) + add(childCoordinator: coordinator) if let spaceId = parentSpaceId { - self.spaceIdStack.append(spaceId) + spaceIdStack.append(spaceId) } return coordinator @@ -166,13 +165,12 @@ final class SpaceSelectorBottomSheetCoordinator: NSObject, Coordinator, Presenta self.completion?(.spaceJoined(spaceId)) case .open, .cancel, .dismiss: self.navigationRouter.popModule(animated: true) - break } } coordinator.start() - self.add(childCoordinator: coordinator) + add(childCoordinator: coordinator) return coordinator } @@ -194,9 +192,7 @@ final class SpaceSelectorBottomSheetCoordinator: NSObject, Coordinator, Presenta // MARK: - UIAdaptivePresentationControllerDelegate extension SpaceSelectorBottomSheetCoordinator: UIAdaptivePresentationControllerDelegate { - func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { completion?(.cancel) } - } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomSheetCoordinatorBridgePresenter.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomSheetCoordinatorBridgePresenter.swift index b80be9e30..425063a87 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomSheetCoordinatorBridgePresenter.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/Coordinator/SpaceSelectorBottomSheetCoordinatorBridgePresenter.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,7 +29,6 @@ import Foundation /// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. @objcMembers final class SpaceSelectorBottomSheetCoordinatorBridgePresenter: NSObject { - // MARK: - Properties // MARK: Private @@ -82,7 +81,7 @@ final class SpaceSelectorBottomSheetCoordinatorBridgePresenter: NSObject { } func dismiss(animated: Bool, completion: (() -> Void)?) { - guard let coordinator = self.coordinator else { + guard let coordinator = coordinator else { return } coordinator.toPresentable().dismiss(animated: animated) { @@ -92,4 +91,3 @@ final class SpaceSelectorBottomSheetCoordinatorBridgePresenter: NSObject { } } } - diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift index 8c13f0216..771ad0b8f 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Coordinator/SpaceSelectorCoordinator.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import SwiftUI import CommonKit +import SwiftUI struct SpaceSelectorCoordinatorParameters { let session: MXSession @@ -38,7 +38,6 @@ struct SpaceSelectorCoordinatorParameters { } final class SpaceSelectorCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -99,7 +98,7 @@ final class SpaceSelectorCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.hostingViewController + hostingViewController } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/MockSpaceSelectorScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/MockSpaceSelectorScreenState.swift index f267ef6a9..22a613be2 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/MockSpaceSelectorScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/MockSpaceSelectorScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -38,7 +38,7 @@ enum MockSpaceSelectorScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let service: MockSpaceSelectorService switch self { case .emptyList: diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift index 1ba496e9b..4db35bf69 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/MatrixSDK/SpaceSelectorService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +14,11 @@ // limitations under the License. // -import Foundation import Combine +import Foundation import MatrixSDK class SpaceSelectorService: SpaceSelectorServiceProtocol { - // MARK: - Properties // MARK: Private @@ -65,7 +64,7 @@ class SpaceSelectorService: SpaceSelectorServiceProtocol { var itemList: [SpaceSelectorListItemData] = [] itemList.append(contentsOf: invitedSpaces) - if showHomeSpace && parentSpaceId == nil { + if showHomeSpace, parentSpaceId == nil { itemList.append(SpaceSelectorListItemData(id: SpaceSelectorConstants.homeSpaceId, icon: Asset.Images.sideMenuActionIconFeedback.image, displayName: VectorL10n.allChatsTitle, isJoined: true)) } itemList.append(contentsOf: joinedSpaces) @@ -93,14 +92,14 @@ class SpaceSelectorService: SpaceSelectorServiceProtocol { self.session = session self.parentSpaceId = parentSpaceId self.showHomeSpace = showHomeSpace - self.spaceListSubject = CurrentValueSubject([]) - self.parentSpaceNameSubject = CurrentValueSubject(nil) + spaceListSubject = CurrentValueSubject([]) + parentSpaceNameSubject = CurrentValueSubject(nil) self.selectedSpaceId = selectedSpaceId spaceListSubject.send(spaceList) parentSpaceNameSubject.send(parentSpaceName) - NotificationCenter.default.addObserver(self, selector: #selector(self.spaceServiceDidUpdate), name: MXSpaceService.didBuildSpaceGraph, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(spaceServiceDidUpdate), name: MXSpaceService.didBuildSpaceGraph, object: nil) } @objc private func spaceServiceDidUpdate() { @@ -108,7 +107,7 @@ class SpaceSelectorService: SpaceSelectorServiceProtocol { } } -fileprivate extension SpaceSelectorListItemData { +private extension SpaceSelectorListItemData { static func itemData(with space: MXSpace, notificationCounter: MXSpaceNotificationCounter) -> SpaceSelectorListItemData? { guard let summary = space.summary else { return nil @@ -116,7 +115,7 @@ fileprivate extension SpaceSelectorListItemData { let notificationState = notificationCounter.notificationState(forSpaceWithId: space.spaceId) - return SpaceSelectorListItemData(id:summary.roomId, + return SpaceSelectorListItemData(id: summary.roomId, avatar: summary.room.avatarData, displayName: summary.displayname, notificationCount: notificationState?.groupMissedDiscussionsCount ?? 0, diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/Mock/MockSpaceSelectorService.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/Mock/MockSpaceSelectorService.swift index 5a3435252..590a5b849 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/Mock/MockSpaceSelectorService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/Mock/MockSpaceSelectorService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +14,11 @@ // limitations under the License. // -import Foundation import Combine +import Foundation import UIKit class MockSpaceSelectorService: SpaceSelectorServiceProtocol { - static let homeItem = SpaceSelectorListItemData(id: SpaceSelectorConstants.homeSpaceId, avatar: nil, icon: UIImage(systemName: "house"), displayName: "All Chats", notificationCount: 0, highlightedNotificationCount: 0, hasSubItems: false) static let defaultSpaceList = [ homeItem, @@ -35,8 +34,8 @@ class MockSpaceSelectorService: SpaceSelectorServiceProtocol { var selectedSpaceId: String? init(spaceList: [SpaceSelectorListItemData] = defaultSpaceList, parentSpaceName: String? = nil, selectedSpaceId: String = SpaceSelectorConstants.homeSpaceId) { - self.spaceListSubject = CurrentValueSubject(spaceList) - self.parentSpaceNameSubject = CurrentValueSubject(parentSpaceName) + spaceListSubject = CurrentValueSubject(spaceList) + parentSpaceNameSubject = CurrentValueSubject(parentSpaceName) self.selectedSpaceId = selectedSpaceId } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/SpaceSelectorServiceProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/SpaceSelectorServiceProtocol.swift index 48ebec3e6..e5a17462b 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/SpaceSelectorServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Service/SpaceSelectorServiceProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation protocol SpaceSelectorServiceProtocol { var spaceListSubject: CurrentValueSubject<[SpaceSelectorListItemData], Never> { get } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorModels.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorModels.swift index 1a9f16bc5..7511044fb 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorModels.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -76,7 +76,7 @@ struct SpaceSelectorListItemData { } } -extension SpaceSelectorListItemData: Identifiable, Equatable {} +extension SpaceSelectorListItemData: Identifiable, Equatable { } enum SpaceSelectorViewModelResult { /// Cancel button has been presed diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModel.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModel.swift index 37b7bc787..562b4e76a 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModel.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModel.swift @@ -14,15 +14,12 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI -typealias SpaceSelectorViewModelType = StateStoreViewModel +typealias SpaceSelectorViewModelType = StateStoreViewModel class SpaceSelectorViewModel: SpaceSelectorViewModelType, SpaceSelectorViewModelProtocol { - // MARK: - Properties // MARK: Private @@ -36,7 +33,7 @@ class SpaceSelectorViewModel: SpaceSelectorViewModelType, SpaceSelectorViewModel // MARK: - Setup static func makeViewModel(service: SpaceSelectorServiceProtocol, showCancel: Bool) -> SpaceSelectorViewModelProtocol { - return SpaceSelectorViewModel(service: service, showCancel: showCancel) + SpaceSelectorViewModel(service: service, showCancel: showCancel) } private init(service: SpaceSelectorServiceProtocol, showCancel: Bool) { @@ -55,7 +52,7 @@ class SpaceSelectorViewModel: SpaceSelectorViewModelType, SpaceSelectorViewModel private func setupObservers() { service.spaceListSubject.sink { [weak self] spaceList in - self?.state.items = spaceList + self?.state.items = spaceList } .store(in: &cancellables) } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModelProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModelProtocol.swift index 79263720d..ed0f477ba 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/SpaceSelectorViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol SpaceSelectorViewModelProtocol { - var completion: ((SpaceSelectorViewModelResult) -> Void)? { get set } static func makeViewModel(service: SpaceSelectorServiceProtocol, showCancel: Bool) -> SpaceSelectorViewModelProtocol var context: SpaceSelectorViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/UI/SpaceSelectorUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/UI/SpaceSelectorUITests.swift index 020127ea3..4cbc0fcad 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/UI/SpaceSelectorUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/UI/SpaceSelectorUITests.swift @@ -14,16 +14,15 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class SpaceSelectorUITests: MockScreenTestCase { - func testInitialDisplay() { app.goToScreenWithIdentifier(MockSpaceSelectorScreenState.initialList.title) let disclosureButtons = app.buttons.matching(identifier: "disclosureButton").allElementsBoundByIndex - XCTAssertEqual(disclosureButtons.count, MockSpaceSelectorService.defaultSpaceList.filter { $0.hasSubItems }.count) + XCTAssertEqual(disclosureButtons.count, MockSpaceSelectorService.defaultSpaceList.filter(\.hasSubItems).count) let notificationBadges = app.staticTexts.matching(identifier: "notificationBadge").allElementsBoundByIndex let itemsWithNotifications = MockSpaceSelectorService.defaultSpaceList.filter { $0.notificationCount > 0 || !$0.isJoined } @@ -65,5 +64,4 @@ class SpaceSelectorUITests: MockScreenTestCase { XCTAssertEqual(app.staticTexts["emptyListPlaceholderMessage"].exists, exists) XCTAssertEqual(app.buttons["createSpaceButton"].exists, exists) } - } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/Unit/SpaceSelectorViewModelTests.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/Unit/SpaceSelectorViewModelTests.swift index 58d5f9304..9395349a9 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/Unit/SpaceSelectorViewModelTests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/Test/Unit/SpaceSelectorViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,13 +14,12 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI class SpaceSelectorViewModelTests: XCTestCase { - var service: MockSpaceSelectorService! var viewModel: SpaceSelectorViewModelProtocol! var context: SpaceSelectorViewModelType.Context! @@ -37,5 +36,4 @@ class SpaceSelectorViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.items, MockSpaceSelectorService.defaultSpaceList) XCTAssertEqual(context.viewState.navigationTitle, VectorL10n.spaceSelectorTitle) } - } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelector.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelector.swift index 115a51444..df9c52906 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelector.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelector.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct SpaceSelector: View { - // MARK: - Properties // MARK: Private @@ -53,12 +52,11 @@ struct SpaceSelector: View { notificationCount: item.notificationCount, highlightedNotificationCount: item.highlightedNotificationCount, disclosureAction: { - viewModel.send(viewAction: .spaceDisclosure(item)) + viewModel.send(viewAction: .spaceDisclosure(item)) + }) + .onTapGesture { + viewModel.send(viewAction: .spaceSelected(item)) } - ) - .onTapGesture { - viewModel.send(viewAction: .spaceSelected(item)) - } } } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelectorListRow.swift b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelectorListRow.swift index aa60c5dd4..d9e67c9b9 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelectorListRow.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSelectorBottomSheet/SpaceSelector/View/SpaceSelectorListRow.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -70,7 +70,7 @@ struct SpaceSelectorListRow: View { .renderingMode(.template) .foregroundColor(theme.colors.secondaryContent) } - if hasSubItems && isJoined { + if hasSubItems, isJoined { Button { disclosureAction?() } label: { @@ -91,7 +91,7 @@ struct SpaceSelectorListRow: View { } private func badge(with text: String, color: Color) -> some View { - return Text(text) + Text(text) .multilineTextAlignment(.center) .foregroundColor(theme.colors.background) .font(theme.fonts.footnote) @@ -106,7 +106,6 @@ struct SpaceSelectorListRow: View { // MARK: - Previews struct SpaceSelectorListRow_Previews: PreviewProvider { - static var previews: some View { sampleView.theme(.light).preferredColorScheme(.light) sampleView.theme(.dark).preferredColorScheme(.dark) @@ -121,5 +120,4 @@ struct SpaceSelectorListRow_Previews: PreviewProvider { SpaceSelectorListRow(avatar: nil, icon: UIImage(systemName: "house"), displayName: "Space name", hasSubItems: true, isJoined: true, isSelected: true, notificationCount: 99, highlightedNotificationCount: 1, disclosureAction: nil) } } - } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinator.swift index b2d10971c..8e7da9792 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinator.swift @@ -24,7 +24,6 @@ enum SpaceSettingsModalCoordinatorAction { @objcMembers final class SpaceSettingsModalCoordinator: Coordinator { - // MARK: - Properties // MARK: Private @@ -36,7 +35,7 @@ final class SpaceSettingsModalCoordinator: Coordinator { } private var navigationRouter: NavigationRouterType { - return self.parameters.navigationRouter + parameters.navigationRouter } // MARK: Public @@ -50,31 +49,30 @@ final class SpaceSettingsModalCoordinator: Coordinator { init(parameters: SpaceSettingsModalCoordinatorParameters) { self.parameters = parameters - } + } // MARK: - Public - func start() { MXLog.debug("[SpaceSettingsModalCoordinator] did start.") - let rootCoordinator = self.createSpaceSettingsCoordinator() + let rootCoordinator = createSpaceSettingsCoordinator() rootCoordinator.start() - self.add(childCoordinator: rootCoordinator) + add(childCoordinator: rootCoordinator) - if self.navigationRouter.modules.isEmpty == false { - self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in + if navigationRouter.modules.isEmpty == false { + navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in self?.remove(childCoordinator: rootCoordinator) }) } else { - self.navigationRouter.setRootModule(rootCoordinator) { [weak self] in + navigationRouter.setRootModule(rootCoordinator) { [weak self] in self?.remove(childCoordinator: rootCoordinator) } } } func toPresentable() -> UIViewController { - return self.navigationRouter.toPresentable() + navigationRouter.toPresentable() } // MARK: - Private @@ -82,7 +80,7 @@ final class SpaceSettingsModalCoordinator: Coordinator { func pushScreen(with coordinator: Coordinator & Presentable) { add(childCoordinator: coordinator) - self.navigationRouter.push(coordinator, animated: true, popCompletion: { [weak self] in + navigationRouter.push(coordinator, animated: true, popCompletion: { [weak self] in self?.remove(childCoordinator: coordinator) }) @@ -110,12 +108,12 @@ final class SpaceSettingsModalCoordinator: Coordinator { switch optionType { case .rooms: Analytics.shared.viewRoomTrigger = .spaceSettings - exploreRooms(ofSpaceWithId: self.parameters.spaceId) + exploreRooms(ofSpaceWithId: parameters.spaceId) case .members: Analytics.shared.viewRoomTrigger = .spaceSettings - showMembers(ofSpaceWithId: self.parameters.spaceId) + showMembers(ofSpaceWithId: parameters.spaceId) case .visibility: - showAccess(ofSpaceWithId: self.parameters.spaceId) + showAccess(ofSpaceWithId: parameters.spaceId) } } @@ -124,7 +122,7 @@ final class SpaceSettingsModalCoordinator: Coordinator { coordinator.delegate = self add(childCoordinator: coordinator) coordinator.start() - self.navigationRouter.present(coordinator.toPresentable(), animated: true) + navigationRouter.present(coordinator.toPresentable(), animated: true) } private func showMembers(ofSpaceWithId spaceId: String) { @@ -132,7 +130,7 @@ final class SpaceSettingsModalCoordinator: Coordinator { coordinator.delegate = self add(childCoordinator: coordinator) coordinator.start() - self.navigationRouter.present(coordinator.toPresentable(), animated: true) + navigationRouter.present(coordinator.toPresentable(), animated: true) } private func showAccess(ofSpaceWithId spaceId: String) { @@ -158,23 +156,25 @@ final class SpaceSettingsModalCoordinator: Coordinator { } add(childCoordinator: coordinator) coordinator.start() - self.navigationRouter.present(coordinator.toPresentable(), animated: true) + navigationRouter.present(coordinator.toPresentable(), animated: true) } } // MARK: - ExploreRoomCoordinatorDelegate + extension SpaceSettingsModalCoordinator: ExploreRoomCoordinatorDelegate { func exploreRoomCoordinatorDidComplete(_ coordinator: ExploreRoomCoordinatorType) { - self.navigationRouter.dismissModule(animated: true, completion: { + navigationRouter.dismissModule(animated: true, completion: { self.remove(childCoordinator: coordinator) }) } } // MARK: - SpaceMembersCoordinatorDelegate + extension SpaceSettingsModalCoordinator: SpaceMembersCoordinatorDelegate { func spaceMembersCoordinatorDidCancel(_ coordinator: SpaceMembersCoordinatorType) { - self.navigationRouter.dismissModule(animated: true, completion: { + navigationRouter.dismissModule(animated: true, completion: { self.remove(childCoordinator: coordinator) }) } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinatorBridgePresenter.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinatorBridgePresenter.swift index 5614f2d08..c76f0aac7 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinatorBridgePresenter.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinatorBridgePresenter.swift @@ -27,7 +27,6 @@ import UIKit /// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. @objcMembers final class SpaceSettingsModalCoordinatorBridgePresenter: NSObject { - // MARK: - Properties // MARK: Private @@ -75,7 +74,7 @@ final class SpaceSettingsModalCoordinatorBridgePresenter: NSObject { } func dismiss(animated: Bool, completion: (() -> Void)?) { - guard let coordinator = self.coordinator else { + guard let coordinator = coordinator else { return } coordinator.toPresentable().dismiss(animated: animated) { @@ -91,9 +90,7 @@ final class SpaceSettingsModalCoordinatorBridgePresenter: NSObject { // MARK: - UIAdaptivePresentationControllerDelegate extension SpaceSettingsModalCoordinatorBridgePresenter: UIAdaptivePresentationControllerDelegate { - func roomNotificationSettingsCoordinatorDidComplete(_ presentationController: UIPresentationController) { - self.delegate?.spaceSettingsModalCoordinatorBridgePresenterDelegateDidCancel(self) + delegate?.spaceSettingsModalCoordinatorBridgePresenterDelegateDidCancel(self) } - } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinatorParameters.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinatorParameters.swift index 04778e3ed..85b3cf924 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/Coordinator/SpaceSettingsModalCoordinatorParameters.swift @@ -18,7 +18,6 @@ import Foundation /// SpaceSettingsModalCoordinator input parameters struct SpaceSettingsModalCoordinatorParameters { - /// The Matrix session let session: MXSession diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift index d2e451528..f5b69b6a5 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Coordinator/SpaceSettingsCoordinator.swift @@ -22,7 +22,6 @@ struct SpaceSettingsCoordinatorParameters { } final class SpaceSettingsCoordinator: Coordinator, Presentable { - // MARK: - Properties // MARK: Private @@ -77,9 +76,9 @@ final class SpaceSettingsCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.spaceSettingsHostingController - + spaceSettingsHostingController } + // MARK: - Private private func pickImage(from sourceRect: CGRect) { @@ -90,6 +89,7 @@ final class SpaceSettingsCoordinator: Coordinator, Presentable { } // MARK: - SingleImagePickerPresenterDelegate + extension SpaceSettingsCoordinator: SingleImagePickerPresenterDelegate { func singleImagePickerPresenter(_ presenter: SingleImagePickerPresenter, didSelectImageData imageData: Data, withUTI uti: MXKUTI?) { spaceSettingsViewModel.updateAvatarImage(with: UIImage(data: imageData)) diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift index 3a28e7f65..f18482638 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/MockSpaceSettingsScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -38,7 +38,7 @@ enum MockSpaceSettingsScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let service: MockSpaceSettingsService switch self { case .visibility(let visibility): @@ -53,7 +53,8 @@ enum MockSpaceSettingsScreenState: MockScreenState, CaseIterable { isNameEditable: true, isTopicEditable: true, isAddressEditable: true, - isAccessEditable: true) + isAccessEditable: true + ) service = MockSpaceSettingsService(roomProperties: roomProperties) case .notEditable: let roomProperties = SpaceSettingsRoomProperties( @@ -67,7 +68,8 @@ enum MockSpaceSettingsScreenState: MockScreenState, CaseIterable { isNameEditable: false, isTopicEditable: false, isAddressEditable: false, - isAccessEditable: false) + isAccessEditable: false + ) service = MockSpaceSettingsService(roomProperties: roomProperties) } let viewModel = SpaceSettingsViewModel.makeSpaceSettingsViewModel(service: service) diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/MatrixSDK/SpaceSettingsService.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/MatrixSDK/SpaceSettingsService.swift index 51028a11c..8b74bda67 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/MatrixSDK/SpaceSettingsService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/MatrixSDK/SpaceSettingsService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,12 +14,11 @@ // limitations under the License. // -import Foundation import Combine +import Foundation import MatrixSDK class SpaceSettingsService: SpaceSettingsServiceProtocol { - // MARK: - Properties var userDefinedAddress: String? { @@ -36,6 +35,7 @@ class SpaceSettingsService: SpaceSettingsServiceProtocol { updateRoomProperties() } } + private let room: MXRoom? private var roomEventListener: Any? @@ -88,12 +88,12 @@ class SpaceSettingsService: SpaceSettingsServiceProtocol { init(session: MXSession, spaceId: String) { self.session = session self.spaceId = spaceId - self.room = session.room(withRoomId: spaceId) - self.isLoadingSubject = CurrentValueSubject(false) - self.showPostProcessAlert = CurrentValueSubject(false) - self.roomPropertiesSubject = CurrentValueSubject(self.roomProperties) - self.addressValidationSubject = CurrentValueSubject(.none("#")) - self.defaultAddress = "" + room = session.room(withRoomId: spaceId) + isLoadingSubject = CurrentValueSubject(false) + showPostProcessAlert = CurrentValueSubject(false) + roomPropertiesSubject = CurrentValueSubject(roomProperties) + addressValidationSubject = CurrentValueSubject(.none("#")) + defaultAddress = "" readRoomState() } @@ -121,15 +121,15 @@ class SpaceSettingsService: SpaceSettingsServiceProtocol { private func readRoomState() { isLoadingSubject.send(true) - self.room?.state { [weak self] roomState in + room?.state { [weak self] roomState in self?.roomState = roomState self?.isLoadingSubject.send(false) } - roomEventListener = self.room?.listen(toEvents: { [weak self] event, direction, state in - self?.room?.state({ [weak self] roomState in + roomEventListener = room?.listen(toEvents: { [weak self] _, _, _ in + self?.room?.state { [weak self] roomState in self?.roomState = roomState - }) + } }) } @@ -164,7 +164,7 @@ class SpaceSettingsService: SpaceSettingsServiceProtocol { return false } - let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: self.session.myUserId) + let userPowerLevel = powerLevels.powerLevelOfUser(withUserID: session.myUserId) return userPowerLevel >= powerLevels.minimumPowerLevel(forNotifications: notification, defaultPower: powerLevels.stateDefault) } @@ -172,10 +172,10 @@ class SpaceSettingsService: SpaceSettingsServiceProtocol { addressValidationOperation?.cancel() addressValidationOperation = nil - guard let userDefinedAddress = self.userDefinedAddress, !userDefinedAddress.isEmpty else { + guard let userDefinedAddress = userDefinedAddress, !userDefinedAddress.isEmpty else { let fullAddress = MXTools.fullLocalAlias(from: defaultAddress, with: session) - if let publicAddress = self.publicAddress, !publicAddress.isEmpty { + if let publicAddress = publicAddress, !publicAddress.isEmpty { addressValidationSubject.send(.current(fullAddress)) } else if defaultAddress.isEmpty { addressValidationSubject.send(.none(fullAddress)) @@ -191,8 +191,8 @@ class SpaceSettingsService: SpaceSettingsServiceProtocol { private func validate(_ aliasLocalPart: String) { let fullAddress = MXTools.fullLocalAlias(from: aliasLocalPart, with: session) - if let publicAddress = self.publicAddress, publicAddress == aliasLocalPart { - self.addressValidationSubject.send(.current(fullAddress)) + if let publicAddress = publicAddress, publicAddress == aliasLocalPart { + addressValidationSubject.send(.current(fullAddress)) return } @@ -221,17 +221,17 @@ class SpaceSettingsService: SpaceSettingsServiceProtocol { if let canonicalAlias = roomState.canonicalAlias { let localAliasPart = MXTools.extractLocalAliasPart(from: canonicalAlias) - self.publicAddress = localAliasPart - self.defaultAddress = localAliasPart + publicAddress = localAliasPart + defaultAddress = localAliasPart } else { - self.publicAddress = nil - self.defaultAddress = MXTools.validAliasLocalPart(from: roomState.name) + publicAddress = nil + defaultAddress = MXTools.validAliasLocalPart(from: roomState.name) } - self.roomProperties = SpaceSettingsRoomProperties( + roomProperties = SpaceSettingsRoomProperties( name: roomState.name, topic: roomState.topic, - address: self.defaultAddress, + address: defaultAddress, avatarUrl: roomState.avatar, visibility: visibility(with: roomState), allowedParentIds: allowedParentIds(with: roomState), @@ -239,12 +239,13 @@ class SpaceSettingsService: SpaceSettingsServiceProtocol { isNameEditable: isField(ofType: kMXEventTypeStringRoomName, editableWith: roomState.powerLevels), isTopicEditable: isField(ofType: kMXEventTypeStringRoomTopic, editableWith: roomState.powerLevels), isAddressEditable: isField(ofType: kMXEventTypeStringRoomAliases, editableWith: roomState.powerLevels), - isAccessEditable: isField(ofType: kMXEventTypeStringRoomJoinRules, editableWith: roomState.powerLevels)) + isAccessEditable: isField(ofType: kMXEventTypeStringRoomJoinRules, editableWith: roomState.powerLevels) + ) } // MARK: - Post process - private var currentTaskIndex: Int = 0 + private var currentTaskIndex = 0 private var tasks: [PostProcessTask] = [] private var lastError: Error? private var completion: ((_ result: SpaceSettingsServiceCompletionResult) -> Void)? @@ -267,18 +268,18 @@ class SpaceSettingsService: SpaceSettingsServiceProtocol { let type: PostProcessTaskType var state: PostProcessTaskState = .none var isFinished: Bool { - return state == .failure || state == .success + state == .failure || state == .success } static func == (lhs: PostProcessTask, rhs: PostProcessTask) -> Bool { - return lhs.type == rhs.type && lhs.state == rhs.state + lhs.type == rhs.type && lhs.state == rhs.state } } func update(roomName: String, topic: String, address: String, avatar: UIImage?, completion: ((_ result: SpaceSettingsServiceCompletionResult) -> Void)?) { // First attempt - if self.tasks.isEmpty { + if tasks.isEmpty { var tasks: [PostProcessTask] = [] if roomProperties?.name ?? "" != roomName { tasks.append(PostProcessTask(type: .updateName(roomName))) @@ -295,17 +296,17 @@ class SpaceSettingsService: SpaceSettingsServiceProtocol { self.tasks = tasks } else { // Retry -> restart failed tasks - self.tasks = tasks.map({ task in + tasks = tasks.map { task in if task.state == .failure { return PostProcessTask(type: task.type, state: .none) } return task - }) + } } - self.isLoadingSubject.send(true) + isLoadingSubject.send(true) self.completion = completion - self.lastError = nil + lastError = nil currentTaskIndex = -1 runNextTask() } @@ -313,7 +314,7 @@ class SpaceSettingsService: SpaceSettingsServiceProtocol { private func runNextTask() { currentTaskIndex += 1 guard currentTaskIndex < tasks.count else { - self.isLoadingSubject.send(false) + isLoadingSubject.send(false) if let error = lastError { showPostProcessAlert.send(true) completion?(.failure(error)) @@ -395,7 +396,7 @@ class SpaceSettingsService: SpaceSettingsServiceProtocol { switch response { case .success: if let publicAddress = self.publicAddress { - self.currentOperation = self.room?.removeAlias(MXTools.fullLocalAlias(from: publicAddress, with: self.session), completion: { [weak self] response in + self.currentOperation = self.room?.removeAlias(MXTools.fullLocalAlias(from: publicAddress, with: self.session), completion: { [weak self] _ in guard let self = self else { return } self.setup(canonicalAlias: canonicalAlias) @@ -433,33 +434,33 @@ class SpaceSettingsService: SpaceSettingsServiceProtocol { let avatarUp = MXKTools.forceImageOrientationUp(avatar) mediaUploader.uploadData(avatarUp?.jpegData(compressionQuality: 0.5), filename: nil, mimeType: "image/jpeg", - success: { [weak self] (urlString) in - guard let self = self else { return } + success: { [weak self] urlString in + guard let self = self else { return } - guard let urlString = urlString else { - self.updateCurrentTaskState(with: .failure) - self.runNextTask() - return - } - guard let url = URL(string: urlString) else { - self.updateCurrentTaskState(with: .failure) - self.runNextTask() - return - } + guard let urlString = urlString else { + self.updateCurrentTaskState(with: .failure) + self.runNextTask() + return + } + guard let url = URL(string: urlString) else { + self.updateCurrentTaskState(with: .failure) + self.runNextTask() + return + } - self.setAvatar(withURL: url) + self.setAvatar(withURL: url) }, - failure: { [weak self] (error) in - guard let self = self else { return } + failure: { [weak self] error in + guard let self = self else { return } - self.lastError = error - self.updateCurrentTaskState(with: .failure) - self.runNextTask() + self.lastError = error + self.updateCurrentTaskState(with: .failure) + self.runNextTask() }) } private func setAvatar(withURL url: URL) { - currentOperation = room?.setAvatar(url: url) { [weak self] (response) in + currentOperation = room?.setAvatar(url: url) { [weak self] response in guard let self = self else { return } switch response { @@ -473,5 +474,4 @@ class SpaceSettingsService: SpaceSettingsServiceProtocol { self.runNextTask() } } - } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/Mock/MockSpaceSettingsService.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/Mock/MockSpaceSettingsService.swift index 96bc1f503..c9b7feb41 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/Mock/MockSpaceSettingsService.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/Mock/MockSpaceSettingsService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +14,10 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class MockSpaceSettingsService: SpaceSettingsServiceProtocol { - var spaceId: String var roomProperties: SpaceSettingsRoomProperties? private(set) var displayName: String? @@ -36,24 +35,19 @@ class MockSpaceSettingsService: SpaceSettingsServiceProtocol { self.spaceId = spaceId self.roomProperties = roomProperties self.displayName = displayName - self.isLoadingSubject = CurrentValueSubject(isLoading) + isLoadingSubject = CurrentValueSubject(isLoading) self.showPostProcessAlert = CurrentValueSubject(showPostProcessAlert) - self.roomPropertiesSubject = CurrentValueSubject(roomProperties) - self.addressValidationSubject = CurrentValueSubject(.none(spaceId)) + roomPropertiesSubject = CurrentValueSubject(roomProperties) + addressValidationSubject = CurrentValueSubject(.none(spaceId)) } - func update(roomName: String, topic: String, address: String, avatar: UIImage?, completion: ((SpaceSettingsServiceCompletionResult) -> Void)?) { - } + func update(roomName: String, topic: String, address: String, avatar: UIImage?, completion: ((SpaceSettingsServiceCompletionResult) -> Void)?) { } - func addressDidChange(_ newValue: String) { - - } + func addressDidChange(_ newValue: String) { } func simulateUpdate(addressValidationStatus: SpaceCreationSettingsAddressValidationStatus) { - self.addressValidationSubject.value = addressValidationStatus + addressValidationSubject.value = addressValidationStatus } - func trackSpace() { - - } + func trackSpace() { } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/SpaceSettingsServiceProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/SpaceSettingsServiceProtocol.swift index 4788c8e75..730a98bfe 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/SpaceSettingsServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Service/SpaceSettingsServiceProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation enum SpaceSettingsServiceCompletionResult { case success @@ -42,6 +42,7 @@ extension SpaceSettingsServiceProtocol { var mxContentUri: String? { roomProperties?.avatarUrl } + var matrixItemId: String { spaceId } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsModels.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsModels.swift index 6ca32eef4..2601d00d2 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsModels.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -81,15 +81,19 @@ struct SpaceSettingsViewState: BindableState { var isModified: Bool { userSelectedAvatar != nil || isRoomNameModified || isTopicModified || isAddressModified } + var isRoomNameModified: Bool { (roomProperties?.name ?? "") != bindings.name } + var isTopicModified: Bool { (roomProperties?.topic ?? "") != bindings.topic } + var isAddressModified: Bool { (roomProperties?.address ?? "") != bindings.address } + var bindings: SpaceSettingsViewModelBindings } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsViewModel.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsViewModel.swift index e98257b3f..c1ea68534 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsViewModel.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsViewModel.swift @@ -14,14 +14,12 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI + +typealias SpaceSettingsViewModelType = StateStoreViewModel -typealias SpaceSettingsViewModelType = StateStoreViewModel class SpaceSettingsViewModel: SpaceSettingsViewModelType, SpaceSettingsViewModelProtocol { - // MARK: - Properties private static let options: [SpaceSettingsOption] = [ @@ -40,7 +38,7 @@ class SpaceSettingsViewModel: SpaceSettingsViewModelType, SpaceSettingsViewModel // MARK: - Setup static func makeSpaceSettingsViewModel(service: SpaceSettingsServiceProtocol) -> SpaceSettingsViewModelProtocol { - return SpaceSettingsViewModel(service: service) + SpaceSettingsViewModel(service: service) } private init(service: SpaceSettingsServiceProtocol) { @@ -54,7 +52,8 @@ class SpaceSettingsViewModel: SpaceSettingsViewModelType, SpaceSettingsViewModel name: service.roomProperties?.name ?? "", topic: service.roomProperties?.topic ?? "", address: service.roomProperties?.address ?? "", - showPostProcessAlert: service.showPostProcessAlert.value) + showPostProcessAlert: service.showPostProcessAlert.value + ) return SpaceSettingsViewState( defaultAddress: service.roomProperties?.address ?? "", @@ -68,12 +67,13 @@ class SpaceSettingsViewModel: SpaceSettingsViewModelType, SpaceSettingsViewModel isLoading: service.isLoadingSubject.value, visibilityString: (service.roomProperties?.visibility ?? .private).stringValue, options: options, - bindings: bindings) + bindings: bindings + ) } private func setupObservers() { service.isLoadingSubject.sink { [weak self] isLoading in - self?.state.isLoading = isLoading + self?.state.isLoading = isLoading } .store(in: &cancellables) diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsViewModelProtocol.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsViewModelProtocol.swift index a2859c8a4..808265e83 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/SpaceSettingsViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol SpaceSettingsViewModelProtocol { - var completion: ((SpaceSettingsViewModelResult) -> Void)? { get set } static func makeSpaceSettingsViewModel(service: SpaceSettingsServiceProtocol) -> SpaceSettingsViewModelProtocol var context: SpaceSettingsViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift index abece45dd..583035c4c 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class SpaceSettingsUITests: MockScreenTestCase { // Tests to be implemented. diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/Unit/SpaceSettingsViewModelTests.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/Unit/SpaceSettingsViewModelTests.swift index 1e018fb85..102ba5669 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/Unit/SpaceSettingsViewModelTests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/Unit/SpaceSettingsViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI @@ -38,7 +38,8 @@ class SpaceSettingsViewModelTests: XCTestCase { isNameEditable: true, isTopicEditable: true, isAddressEditable: true, - isAccessEditable: true) + isAccessEditable: true + ) service = MockSpaceSettingsService(roomProperties: roomProperties, displayName: roomProperties.name, isLoading: false, showPostProcessAlert: false) viewModel = SpaceSettingsViewModel.makeSpaceSettingsViewModel(service: service) diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift index 42d8f71b3..8c1d31359 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettings.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct SpaceSettings: View { - // MARK: - Properties // MARK: Private @@ -32,7 +31,7 @@ struct SpaceSettings: View { ScrollView { VStack { avatarView - Spacer().frame(height:32) + Spacer().frame(height: 32) formView roomAccess options @@ -63,7 +62,7 @@ struct SpaceSettings: View { Alert(title: Text(VectorL10n.settingsTitle), message: Text(VectorL10n.spaceSettingsUpdateFailedMessage), primaryButton: .default(Text(VectorL10n.retry), action: { - updateSpace() + updateSpace() }), secondaryButton: .cancel()) }) @@ -80,7 +79,7 @@ struct SpaceSettings: View { GeometryReader { reader in ZStack { SpaceAvatarImage(mxContentUri: viewModel.viewState.avatar.mxContentUri, matrixItemId: viewModel.viewState.avatar.matrixItemId, displayName: viewModel.viewState.avatar.displayName, size: .xxLarge) - .padding(6) + .padding(6) if let image = viewModel.viewState.userSelectedAvatar { Image(uiImage: image) .resizable() @@ -89,13 +88,13 @@ struct SpaceSettings: View { .clipShape(RoundedRectangle(cornerRadius: 8)) } }.padding(10) - .onTapGesture { - guard viewModel.viewState.roomProperties?.isAvatarEditable == true else { - return + .onTapGesture { + guard viewModel.viewState.roomProperties?.isAvatarEditable == true else { + return + } + ResponderManager.resignFirstResponder() + viewModel.send(viewAction: .pickImage(reader.frame(in: .global))) } - ResponderManager.resignFirstResponder() - viewModel.send(viewAction: .pickImage(reader.frame(in: .global))) - } } if viewModel.viewState.roomProperties?.isAvatarEditable == true { Image(uiImage: Asset.Images.spaceCreationCamera.image) @@ -110,26 +109,28 @@ struct SpaceSettings: View { @ViewBuilder private var formView: some View { - VStack{ + VStack { RoundedBorderTextField( title: VectorL10n.createRoomPlaceholderName, placeHolder: "", text: $viewModel.name, footerText: viewModel.viewState.roomNameError, isError: true, - configuration: UIKitTextInputConfiguration( returnKeyType: .next)) - .padding(.horizontal, 2) - .padding(.bottom, 20) - .disabled(viewModel.viewState.roomProperties?.isNameEditable != true) + configuration: UIKitTextInputConfiguration(returnKeyType: .next) + ) + .padding(.horizontal, 2) + .padding(.bottom, 20) + .disabled(viewModel.viewState.roomProperties?.isNameEditable != true) RoundedBorderTextEditor( title: VectorL10n.spaceTopic, placeHolder: VectorL10n.spaceTopic, text: $viewModel.topic, textMaxHeight: 72, - error: nil) - .padding(.horizontal, 2) - .padding(.bottom, viewModel.viewState.showRoomAddress ? 20 : 3) - .disabled(viewModel.viewState.roomProperties?.isTopicEditable != true) + error: nil + ) + .padding(.horizontal, 2) + .padding(.bottom, viewModel.viewState.showRoomAddress ? 20 : 3) + .disabled(viewModel.viewState.roomProperties?.isTopicEditable != true) if viewModel.viewState.showRoomAddress { RoundedBorderTextField( title: VectorL10n.spacesCreationAddress, @@ -137,14 +138,15 @@ struct SpaceSettings: View { text: $viewModel.address, footerText: viewModel.viewState.addressMessage, isError: !viewModel.viewState.isAddressValid, - configuration: UIKitTextInputConfiguration(keyboardType: .URL, returnKeyType: .done, autocapitalizationType: .none), onTextChanged: { + configuration: UIKitTextInputConfiguration(keyboardType: .URL, returnKeyType: .done, autocapitalizationType: .none), onTextChanged: { newText in viewModel.send(viewAction: .addressChanged(newText)) - }) - .disabled(viewModel.viewState.roomProperties?.isAddressEditable != true) - .padding(.horizontal, 2) - .padding(.bottom, 3) - .accessibility(identifier: "addressTextField") + } + ) + .disabled(viewModel.viewState.roomProperties?.isAddressEditable != true) + .padding(.horizontal, 2) + .padding(.bottom, 3) + .accessibility(identifier: "addressTextField") } } .padding(.horizontal) @@ -153,7 +155,7 @@ struct SpaceSettings: View { @ViewBuilder private var roomAccess: some View { VStack(alignment: .leading) { - Spacer().frame(height:24) + Spacer().frame(height: 24) Text(VectorL10n.spaceSettingsAccessSection) .font(theme.fonts.footnote) .foregroundColor(theme.colors.secondaryContent) @@ -161,11 +163,12 @@ struct SpaceSettings: View { .padding(.bottom, 4) SpaceSettingsOptionListItem( title: VectorL10n.roomDetailsAccessRowTitle, - value: viewModel.viewState.visibilityString) { + value: viewModel.viewState.visibilityString + ) { ResponderManager.resignFirstResponder() viewModel.send(viewAction: .optionSelected(.visibility)) } - .disabled(viewModel.viewState.roomProperties?.isAccessEditable != true) + .disabled(viewModel.viewState.roomProperties?.isAccessEditable != true) } } @@ -182,11 +185,12 @@ struct SpaceSettings: View { SpaceSettingsOptionListItem( icon: option.icon, title: option.title, - value: option.value) { + value: option.value + ) { ResponderManager.resignFirstResponder() viewModel.send(viewAction: .optionSelected(option.id)) } - .disabled(!option.isEnabled) + .disabled(!option.isEnabled) } } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettingsOptionListItem.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettingsOptionListItem.swift index 6287ab24d..11b921ce7 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettingsOptionListItem.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/View/SpaceSettingsOptionListItem.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import SwiftUI struct SpaceSettingsOptionListItem: View { - // MARK: Private @Environment(\.theme) private var theme: ThemeSwiftUI @@ -85,7 +84,6 @@ struct SpaceSettingsOptionListItem: View { // MARK: - Previews struct SpaceSettingsOptionListItem_Previews: PreviewProvider { - static var previews: some View { sampleView.theme(.light).preferredColorScheme(.light) sampleView.theme(.dark).preferredColorScheme(.dark) @@ -100,5 +98,4 @@ struct SpaceSettingsOptionListItem_Previews: PreviewProvider { .disabled(true) } } - } diff --git a/RiotSwiftUI/Modules/Template/SimpleScreenExample/Coordinator/TemplateSimpleScreenCoordinator.swift b/RiotSwiftUI/Modules/Template/SimpleScreenExample/Coordinator/TemplateSimpleScreenCoordinator.swift index a794c88ab..4833d31f9 100644 --- a/RiotSwiftUI/Modules/Template/SimpleScreenExample/Coordinator/TemplateSimpleScreenCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/SimpleScreenExample/Coordinator/TemplateSimpleScreenCoordinator.swift @@ -14,34 +14,25 @@ // limitations under the License. // -import SwiftUI import CommonKit +import SwiftUI struct TemplateSimpleScreenCoordinatorParameters { let promptType: TemplateSimpleScreenPromptType } final class TemplateSimpleScreenCoordinator: Coordinator, Presentable { - - // MARK: - Properties - - // MARK: Private - private let parameters: TemplateSimpleScreenCoordinatorParameters private let templateSimpleScreenHostingController: UIViewController private var templateSimpleScreenViewModel: TemplateSimpleScreenViewModelProtocol private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var loadingIndicator: UserIndicator? - - // MARK: Public // Must be used only internally var childCoordinators: [Coordinator] = [] var completion: ((TemplateSimpleScreenViewModelResult) -> Void)? - // MARK: - Setup - init(parameters: TemplateSimpleScreenCoordinatorParameters) { self.parameters = parameters @@ -65,7 +56,7 @@ final class TemplateSimpleScreenCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.templateSimpleScreenHostingController + templateSimpleScreenHostingController } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift b/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift index 4a2d885cd..5db4451ec 100644 --- a/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift +++ b/RiotSwiftUI/Modules/Template/SimpleScreenExample/MockTemplateSimpleScreenScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,7 +37,7 @@ enum MockTemplateSimpleScreenScreenState: MockScreenState, CaseIterable { } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let promptType: TemplateSimpleScreenPromptType switch self { case .promptType(let type): diff --git a/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenModels.swift b/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenModels.swift index 2238fc709..d1755f432 100644 --- a/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenModels.swift +++ b/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenViewModel.swift b/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenViewModel.swift index 892e91a82..995faef6c 100644 --- a/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenViewModel.swift +++ b/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenViewModel.swift @@ -16,22 +16,11 @@ import SwiftUI -typealias TemplateSimpleScreenViewModelType = StateStoreViewModel +typealias TemplateSimpleScreenViewModelType = StateStoreViewModel class TemplateSimpleScreenViewModel: TemplateSimpleScreenViewModelType, TemplateSimpleScreenViewModelProtocol { - - // MARK: - Properties - - // MARK: Private - - // MARK: Public - var completion: ((TemplateSimpleScreenViewModelResult) -> Void)? - // MARK: - Setup - init(promptType: TemplateSimpleScreenPromptType, initialCount: Int = 0) { super.init(initialViewState: TemplateSimpleScreenViewState(promptType: promptType, count: 0)) } diff --git a/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenViewModelProtocol.swift b/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenViewModelProtocol.swift index 9d19bf528..cbab2a69a 100644 --- a/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol TemplateSimpleScreenViewModelProtocol { - var completion: ((TemplateSimpleScreenViewModelResult) -> Void)? { get set } var context: TemplateSimpleScreenViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/Template/SimpleScreenExample/Test/UI/TemplateSimpleScreenUITests.swift b/RiotSwiftUI/Modules/Template/SimpleScreenExample/Test/UI/TemplateSimpleScreenUITests.swift index 4b02918dd..4af3689ac 100644 --- a/RiotSwiftUI/Modules/Template/SimpleScreenExample/Test/UI/TemplateSimpleScreenUITests.swift +++ b/RiotSwiftUI/Modules/Template/SimpleScreenExample/Test/UI/TemplateSimpleScreenUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class TemplateSimpleScreenUITests: MockScreenTestCase { func testTemplateSimpleScreenPromptRegular() { diff --git a/RiotSwiftUI/Modules/Template/SimpleScreenExample/Test/Unit/TemplateSimpleScreenViewModelTests.swift b/RiotSwiftUI/Modules/Template/SimpleScreenExample/Test/Unit/TemplateSimpleScreenViewModelTests.swift index c3c0849a4..2ae36f733 100644 --- a/RiotSwiftUI/Modules/Template/SimpleScreenExample/Test/Unit/TemplateSimpleScreenViewModelTests.swift +++ b/RiotSwiftUI/Modules/Template/SimpleScreenExample/Test/Unit/TemplateSimpleScreenViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Template/SimpleScreenExample/View/TemplateSimpleScreen.swift b/RiotSwiftUI/Modules/Template/SimpleScreenExample/View/TemplateSimpleScreen.swift index fa43120b6..8d8ef892b 100644 --- a/RiotSwiftUI/Modules/Template/SimpleScreenExample/View/TemplateSimpleScreen.swift +++ b/RiotSwiftUI/Modules/Template/SimpleScreenExample/View/TemplateSimpleScreen.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,11 +17,6 @@ import SwiftUI struct TemplateSimpleScreen: View { - - // MARK: - Properties - - // MARK: Private - @Environment(\.theme) private var theme @Environment(\.horizontalSizeClass) private var horizontalSizeClass @@ -29,12 +24,8 @@ struct TemplateSimpleScreen: View { horizontalSizeClass == .regular ? 50 : 16 } - // MARK: Public - @ObservedObject var viewModel: TemplateSimpleScreenViewModel.Context - // MARK: Views - var body: some View { GeometryReader { geometry in VStack { @@ -64,10 +55,10 @@ struct TemplateSimpleScreen: View { Image(viewModel.viewState.promptType.image.name) .resizable() .scaledToFit() - .frame(width:100) + .frame(width: 100) .foregroundColor(theme.colors.accent) - HStack{ + HStack { Text("Counter: \(viewModel.viewState.count)") .foregroundColor(theme.colors.primaryContent) diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift index c2050e6ed..b3098629b 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Coordinator/TemplateUserProfileCoordinator.swift @@ -14,34 +14,25 @@ // limitations under the License. // -import SwiftUI import CommonKit +import SwiftUI struct TemplateUserProfileCoordinatorParameters { let session: MXSession } final class TemplateUserProfileCoordinator: Coordinator, Presentable { - - // MARK: - Properties - - // MARK: Private - private let parameters: TemplateUserProfileCoordinatorParameters private let templateUserProfileHostingController: UIViewController private var templateUserProfileViewModel: TemplateUserProfileViewModelProtocol private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var loadingIndicator: UserIndicator? - - // MARK: Public // Must be used only internally var childCoordinators: [Coordinator] = [] var completion: (() -> Void)? - // MARK: - Setup - init(parameters: TemplateUserProfileCoordinatorParameters) { self.parameters = parameters let viewModel = TemplateUserProfileViewModel.makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileService(session: parameters.session)) @@ -68,7 +59,7 @@ final class TemplateUserProfileCoordinator: Coordinator, Presentable { } func toPresentable() -> UIViewController { - return self.templateUserProfileHostingController + templateUserProfileHostingController } // MARK: - Private diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift index f43fb2557..08082aec5 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/MockTemplateUserProfileScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,12 +35,12 @@ enum MockTemplateUserProfileScreenState: MockScreenState, CaseIterable { static var allCases: [MockTemplateUserProfileScreenState] { // Each of the presence statuses TemplateUserProfilePresence.allCases.map(MockTemplateUserProfileScreenState.presence) - // A long display name - + [.longDisplayName("Somebody with a super long name we would like to test")] + // A long display name + + [.longDisplayName("Somebody with a super long name we would like to test")] } /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { + var screenView: ([Any], AnyView) { let service: MockTemplateUserProfileService switch self { case .presence(let presence): diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/MatrixSDK/TemplateUserProfileService.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/MatrixSDK/TemplateUserProfileService.swift index 77341c1b3..954bdbb3d 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/MatrixSDK/TemplateUserProfileService.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/MatrixSDK/TemplateUserProfileService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,20 +14,13 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class TemplateUserProfileService: TemplateUserProfileServiceProtocol { - - // MARK: - Properties - - // MARK: Private - private let session: MXSession private var listenerReference: Any? - // MARK: Public - var userId: String { session.myUser.userId } @@ -42,12 +35,10 @@ class TemplateUserProfileService: TemplateUserProfileServiceProtocol { private(set) var presenceSubject: CurrentValueSubject - // MARK: - Setup - init(session: MXSession) { self.session = session - self.presenceSubject = CurrentValueSubject(TemplateUserProfilePresence(mxPresence: session.myUser.presence)) - self.listenerReference = setupPresenceListener() + presenceSubject = CurrentValueSubject(TemplateUserProfilePresence(mxPresence: session.myUser.presence)) + listenerReference = setupPresenceListener() } deinit { @@ -70,8 +61,7 @@ class TemplateUserProfileService: TemplateUserProfileServiceProtocol { } } -fileprivate extension TemplateUserProfilePresence { - +private extension TemplateUserProfilePresence { init(mxPresence: MXPresence) { switch mxPresence { case .online: diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileService.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileService.swift index 86fe1c49a..4bd94dbd4 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileService.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class MockTemplateUserProfileService: TemplateUserProfileServiceProtocol { var presenceSubject: CurrentValueSubject @@ -23,19 +23,17 @@ class MockTemplateUserProfileService: TemplateUserProfileServiceProtocol { let userId: String let displayName: String? let avatarUrl: String? - init( - userId: String = "@alice:matrix.org", - displayName: String? = "Alice", - avatarUrl: String? = "mxc://matrix.org/VyNYAgahaiAzUoOeZETtQ", - presence: TemplateUserProfilePresence = .offline - ) { + init(userId: String = "@alice:matrix.org", + displayName: String? = "Alice", + avatarUrl: String? = "mxc://matrix.org/VyNYAgahaiAzUoOeZETtQ", + presence: TemplateUserProfilePresence = .offline) { self.userId = userId self.displayName = displayName self.avatarUrl = avatarUrl - self.presenceSubject = CurrentValueSubject(presence) + presenceSubject = CurrentValueSubject(presence) } func simulateUpdate(presence: TemplateUserProfilePresence) { - self.presenceSubject.value = presence + presenceSubject.value = presence } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/TemplateUserProfileServiceProtocol.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/TemplateUserProfileServiceProtocol.swift index a446d840f..d4317d2e3 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/TemplateUserProfileServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/TemplateUserProfileServiceProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation protocol TemplateUserProfileServiceProtocol: Avatarable { var userId: String { get } @@ -30,6 +30,7 @@ extension TemplateUserProfileServiceProtocol { var mxContentUri: String? { avatarUrl } + var matrixItemId: String { userId } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileModels.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileModels.swift index e57ce601e..6456e2b08 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileModels.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileViewModel.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileViewModel.swift index 3dfc27a9f..d827aed94 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileViewModel.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileViewModel.swift @@ -14,29 +14,18 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI -typealias TemplateUserProfileViewModelType = StateStoreViewModel +typealias TemplateUserProfileViewModelType = StateStoreViewModel class TemplateUserProfileViewModel: TemplateUserProfileViewModelType, TemplateUserProfileViewModelProtocol { - - // MARK: - Properties - - // MARK: Private - private let templateUserProfileService: TemplateUserProfileServiceProtocol - // MARK: Public - var completion: ((TemplateUserProfileViewModelResult) -> Void)? - // MARK: - Setup - static func makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewModelProtocol { - return TemplateUserProfileViewModel(templateUserProfileService: templateUserProfileService) + TemplateUserProfileViewModel(templateUserProfileService: templateUserProfileService) } private init(templateUserProfileService: TemplateUserProfileServiceProtocol) { @@ -46,7 +35,7 @@ class TemplateUserProfileViewModel: TemplateUserProfileViewModelType, TemplateUs } private static func defaultState(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewState { - return TemplateUserProfileViewState( + TemplateUserProfileViewState( avatar: templateUserProfileService.avatarData, displayName: templateUserProfileService.displayName, presence: templateUserProfileService.presenceSubject.value, diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileViewModelProtocol.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileViewModelProtocol.swift index 6862c6a20..6df29b71d 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol TemplateUserProfileViewModelProtocol { - var completion: ((TemplateUserProfileViewModelResult) -> Void)? { get set } static func makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewModelProtocol var context: TemplateUserProfileViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TemplateUserProfileUITests.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TemplateUserProfileUITests.swift index b23b4174e..feecd0e37 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TemplateUserProfileUITests.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TemplateUserProfileUITests.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class TemplateUserProfileUITests: MockScreenTestCase { func testTemplateUserProfilePresenceIdle() { @@ -53,5 +53,4 @@ class TemplateUserProfileUITests: MockScreenTestCase { XCTAssert(displayNameText.exists) XCTAssertEqual(displayNameText.label, name) } - } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift index be0420f8f..c3fffa429 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI @@ -24,6 +24,7 @@ class TemplateUserProfileViewModelTests: XCTestCase { static let presenceInitialValue: TemplateUserProfilePresence = .offline static let displayName = "Alice" } + var service: MockTemplateUserProfileService! var viewModel: TemplateUserProfileViewModelProtocol! var context: TemplateUserProfileViewModelType.Context! diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift index 073431f39..cf1b44d3d 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,15 +17,8 @@ import SwiftUI struct TemplateUserProfile: View { - - // MARK: - Properties - - // MARK: Private - @Environment(\.theme) private var theme: ThemeSwiftUI - // MARK: Public - @ObservedObject var viewModel: TemplateUserProfileViewModel.Context var body: some View { @@ -36,7 +29,7 @@ struct TemplateUserProfile: View { presence: viewModel.viewState.presence ) Divider() - HStack{ + HStack { Text("Counter: \(viewModel.viewState.count)") .font(theme.fonts.title2) .foregroundColor(theme.colors.secondaryContent) diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift index b67f6971b..a87e06605 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfileHeader.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,13 +17,8 @@ import SwiftUI struct TemplateUserProfileHeader: View { - - // MARK: - Properties - - // MARK: Private @Environment(\.theme) private var theme: ThemeSwiftUI - - // MARK: Public + let avatar: AvatarInputProtocol? let displayName: String? let presence: TemplateUserProfilePresence @@ -32,9 +27,9 @@ struct TemplateUserProfileHeader: View { VStack { if let avatar = avatar { AvatarImage(avatarData: avatar, size: .xxLarge) - .padding(.vertical) + .padding(.vertical) } - VStack(spacing: 8){ + VStack(spacing: 8) { Text(displayName ?? "") .font(theme.fonts.title3) .accessibility(identifier: "displayNameText") diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfilePresenceView.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfilePresenceView.swift index 0428474cf..7bc69abd1 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfilePresenceView.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfilePresenceView.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,10 +17,6 @@ import SwiftUI struct TemplateUserProfilePresenceView: View { - - // MARK: - Properties - - // MARK: Public let presence: TemplateUserProfilePresence var body: some View { @@ -54,7 +50,7 @@ struct TemplateUserProfilePresenceView: View { struct TemplateUserProfilePresenceView_Previews: PreviewProvider { static var previews: some View { - VStack(alignment:.leading){ + VStack(alignment: .leading) { Text("Presence") ForEach(TemplateUserProfilePresence.allCases) { presence in TemplateUserProfilePresenceView(presence: presence) diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift index fa6029248..5f5df52bb 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift @@ -18,59 +18,49 @@ import UIKit @objcMembers final class TemplateRoomsCoordinator: Coordinator, Presentable { - - // MARK: - Properties - - // MARK: Private - private let parameters: TemplateRoomsCoordinatorParameters private var navigationRouter: NavigationRouterType { - return self.parameters.navigationRouter + parameters.navigationRouter } - // MARK: Public - // Must be used only internally var childCoordinators: [Coordinator] = [] var callback: (() -> Void)? - // MARK: - Setup - init(parameters: TemplateRoomsCoordinatorParameters) { self.parameters = parameters - } + } // MARK: - Public - func start() { MXLog.debug("[TemplateRoomsCoordinator] did start.") - let rootCoordinator = self.createTemplateRoomListCoordinator() + let rootCoordinator = createTemplateRoomListCoordinator() rootCoordinator.start() - self.add(childCoordinator: rootCoordinator) + add(childCoordinator: rootCoordinator) - if self.navigationRouter.modules.isEmpty == false { - self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in + if navigationRouter.modules.isEmpty == false { + navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in self?.remove(childCoordinator: rootCoordinator) }) } else { - self.navigationRouter.setRootModule(rootCoordinator) { [weak self] in + navigationRouter.setRootModule(rootCoordinator) { [weak self] in self?.remove(childCoordinator: rootCoordinator) } } } func toPresentable() -> UIViewController { - return self.navigationRouter.toPresentable() + navigationRouter.toPresentable() } // MARK: - Private private func createTemplateRoomListCoordinator() -> TemplateRoomListCoordinator { - let coordinator: TemplateRoomListCoordinator = TemplateRoomListCoordinator(parameters: TemplateRoomListCoordinatorParameters(session: parameters.session)) + let coordinator = TemplateRoomListCoordinator(parameters: TemplateRoomListCoordinatorParameters(session: parameters.session)) coordinator.callback = { [weak self] result in MXLog.debug("[TemplateRoomsCoordinator] TemplateRoomListCoordinator did complete with result \(result).") @@ -86,7 +76,7 @@ final class TemplateRoomsCoordinator: Coordinator, Presentable { } private func createTemplateRoomChatCoordinator(room: MXRoom) -> TemplateRoomChatCoordinator { - let coordinator: TemplateRoomChatCoordinator = TemplateRoomChatCoordinator(parameters: TemplateRoomChatCoordinatorParameters(room: room)) + let coordinator = TemplateRoomChatCoordinator(parameters: TemplateRoomChatCoordinatorParameters(room: room)) return coordinator } @@ -99,7 +89,7 @@ final class TemplateRoomsCoordinator: Coordinator, Presentable { add(childCoordinator: templateRoomChatCoordinator) - self.navigationRouter.push(templateRoomChatCoordinator, animated: true, popCompletion: { [weak self] in + navigationRouter.push(templateRoomChatCoordinator, animated: true, popCompletion: { [weak self] in self?.remove(childCoordinator: templateRoomChatCoordinator) }) diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinatorParameters.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinatorParameters.swift index dd9cd4295..bce458e79 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinatorParameters.swift @@ -18,7 +18,6 @@ import Foundation /// TemplateRoomsCoordinator input parameters struct TemplateRoomsCoordinatorParameters { - /// The Matrix session let session: MXSession diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift index 5f1dcd5e6..bca98511f 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Coordinator/TemplateRoomChatCoordinator.swift @@ -21,23 +21,14 @@ struct TemplateRoomChatCoordinatorParameters { } final class TemplateRoomChatCoordinator: Coordinator, Presentable { - - // MARK: - Properties - - // MARK: Private - private let parameters: TemplateRoomChatCoordinatorParameters private let templateRoomChatHostingController: UIViewController private var templateRoomChatViewModel: TemplateRoomChatViewModelProtocol - - // MARK: Public // Must be used only internally var childCoordinators: [Coordinator] = [] var callback: (() -> Void)? - // MARK: - Setup - init(parameters: TemplateRoomChatCoordinatorParameters) { self.parameters = parameters let viewModel = TemplateRoomChatViewModel(templateRoomChatService: TemplateRoomChatService(room: parameters.room)) @@ -48,6 +39,7 @@ final class TemplateRoomChatCoordinator: Coordinator, Presentable { } // MARK: - Public + func start() { MXLog.debug("[TemplateRoomChatCoordinator] did start.") templateRoomChatViewModel.callback = { [weak self] result in @@ -56,12 +48,11 @@ final class TemplateRoomChatCoordinator: Coordinator, Presentable { switch result { case .done: self.callback?() - break } } } func toPresentable() -> UIViewController { - return self.templateRoomChatHostingController + templateRoomChatHostingController } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift index b755cc2fd..8ec8bced0 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/MockTemplateRoomChatScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -56,7 +56,7 @@ enum MockTemplateRoomChatScreenState: MockScreenState, CaseIterable { return ( [service, viewModel], AnyView(TemplateRoomChat(viewModel: viewModel.context) - .addDependency(MockAvatarService.example)) + .addDependency(MockAvatarService.example)) ) } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift index 6019c2535..364aa98f8 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,37 +14,29 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class TemplateRoomChatService: TemplateRoomChatServiceProtocol { - - // MARK: - Properties - - // MARK: Private - private let room: MXRoom private let eventFormatter: EventFormatter private var timeline: MXEventTimeline? private var eventBatch: [MXEvent] private var roomListenerReference: Any? - - - // MARK: Public + private(set) var chatMessagesSubject: CurrentValueSubject<[TemplateRoomChatMessage], Never> private(set) var roomInitializationStatus: CurrentValueSubject var roomName: String? { - self.room.summary.displayname + room.summary.displayname } - // MARK: - Setup init(room: MXRoom) { self.room = room - self.eventFormatter = EventFormatter(matrixSession: room.mxSession) - self.chatMessagesSubject = CurrentValueSubject([]) - self.roomInitializationStatus = CurrentValueSubject(.notInitialized) - self.eventBatch = [MXEvent]() + eventFormatter = EventFormatter(matrixSession: room.mxSession) + chatMessagesSubject = CurrentValueSubject([]) + roomInitializationStatus = CurrentValueSubject(.notInitialized) + eventBatch = [MXEvent]() initializeRoom() } @@ -54,14 +46,15 @@ class TemplateRoomChatService: TemplateRoomChatServiceProtocol { } // MARK: Public + func send(textMessage: String) { - var localEcho: MXEvent? = nil + var localEcho: MXEvent? room.sendTextMessage(textMessage, threadId: nil, localEcho: &localEcho, completion: { _ in }) } // MARK: Private - private func initializeRoom(){ + private func initializeRoom() { room.liveTimeline { [weak self] timeline in guard let self = self, let timeline = timeline @@ -70,21 +63,20 @@ class TemplateRoomChatService: TemplateRoomChatServiceProtocol { } self.timeline = timeline timeline.resetPagination() - self.roomListenerReference = timeline.listenToEvents([.roomMessage], { [weak self] event, direction, roomState in + self.roomListenerReference = timeline.listenToEvents([.roomMessage]) { [weak self] event, direction, _ in guard let self = self else { return } if direction == .backwards { self.eventBatch.append(event) } else { self.chatMessagesSubject.value += self.mapChatMessages(from: [event]) } - - }) + } timeline.paginate(200, direction: .backwards, onlyFromStore: false) { result in guard result.isSuccess else { self.roomInitializationStatus.value = .failedToInitialize return } - let sortedBatch = self.eventBatch.sorted(by: { $0.originServerTs < $1.originServerTs}) + let sortedBatch = self.eventBatch.sorted(by: { $0.originServerTs < $1.originServerTs }) self.chatMessagesSubject.value = self.mapChatMessages(from: sortedBatch) self.roomInitializationStatus.value = .initialized } @@ -92,15 +84,14 @@ class TemplateRoomChatService: TemplateRoomChatServiceProtocol { } private func mapChatMessages(from events: [MXEvent]) -> [TemplateRoomChatMessage] { - return events - .filter({ event in + events + .filter { event in event.type == kMXEventTypeStringRoomMessage && event.content[kMXMessageTypeKey] as? String == kMXMessageTypeText // TODO: New to our SwiftUI Template? Why not implement another message type like image? - - }) - .compactMap({ event -> TemplateRoomChatMessage? in + } + .compactMap { event -> TemplateRoomChatMessage? in guard let eventId = event.eventId, let body = event.content[kMXMessageBodyKey] as? String, let sender = senderForMessage(event: event) @@ -112,7 +103,7 @@ class TemplateRoomChatService: TemplateRoomChatServiceProtocol { sender: sender, timestamp: Date(timeIntervalSince1970: TimeInterval(event.originServerTs / 1000)) ) - }) + } } private func senderForMessage(event: MXEvent) -> TemplateRoomChatMember? { diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/Mock/MockTemplateRoomChatService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/Mock/MockTemplateRoomChatService.swift index d4da0a63e..c920b2a8f 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/Mock/MockTemplateRoomChatService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/Mock/MockTemplateRoomChatService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,39 +14,38 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class MockTemplateRoomChatService: TemplateRoomChatServiceProtocol { - let roomName: String? = "New Vector" static let amadine = TemplateRoomChatMember(id: "@amadine:matrix.org", avatarUrl: "!aaabaa:matrix.org", displayName: "Amadine") static let mathew = TemplateRoomChatMember(id: "@mathew:matrix.org", avatarUrl: "!bbabb:matrix.org", displayName: "Mathew") static let mockMessages = [ - TemplateRoomChatMessage(id: "!0:matrix.org", content: .text(TemplateRoomChatMessageTextContent(body: "Shall I put it live?")) , sender: amadine, timestamp: Date(timeIntervalSinceNow: 60 * -3)), + TemplateRoomChatMessage(id: "!0:matrix.org", content: .text(TemplateRoomChatMessageTextContent(body: "Shall I put it live?")), sender: amadine, timestamp: Date(timeIntervalSinceNow: 60 * -3)), TemplateRoomChatMessage(id: "!1:matrix.org", content: .text(TemplateRoomChatMessageTextContent(body: "Yea go for it! ...and then let's head to the pub")), sender: mathew, timestamp: Date(timeIntervalSinceNow: 60)), TemplateRoomChatMessage(id: "!2:matrix.org", content: .text(TemplateRoomChatMessageTextContent(body: "Deal.")), sender: amadine, timestamp: Date(timeIntervalSinceNow: 60 * -2)), - TemplateRoomChatMessage(id: "!3:matrix.org", content: .text(TemplateRoomChatMessageTextContent(body: "Ok, Done. 🍻")), sender: amadine, timestamp: Date(timeIntervalSinceNow: 60 * -1)), + TemplateRoomChatMessage(id: "!3:matrix.org", content: .text(TemplateRoomChatMessageTextContent(body: "Ok, Done. 🍻")), sender: amadine, timestamp: Date(timeIntervalSinceNow: 60 * -1)) ] var roomInitializationStatus: CurrentValueSubject var chatMessagesSubject: CurrentValueSubject<[TemplateRoomChatMessage], Never> init(messages: [TemplateRoomChatMessage] = mockMessages) { - self.roomInitializationStatus = CurrentValueSubject(.notInitialized) - self.chatMessagesSubject = CurrentValueSubject(messages) + roomInitializationStatus = CurrentValueSubject(.notInitialized) + chatMessagesSubject = CurrentValueSubject(messages) } func send(textMessage: String) { let newMessage = TemplateRoomChatMessage(id: "!\(chatMessagesSubject.value.count):matrix.org", content: .text(TemplateRoomChatMessageTextContent(body: textMessage)), sender: Self.amadine, timestamp: Date()) - self.chatMessagesSubject.value += [newMessage] + chatMessagesSubject.value += [newMessage] } func simulateUpdate(initializationStatus: TemplateRoomChatRoomInitializationStatus) { - self.roomInitializationStatus.value = initializationStatus + roomInitializationStatus.value = initializationStatus } func simulateUpdate(messages: [TemplateRoomChatMessage]) { - self.chatMessagesSubject.value = messages + chatMessagesSubject.value = messages } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/TemplateRoomChatServiceProtocol.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/TemplateRoomChatServiceProtocol.swift index 012c5ab85..a14ee05e3 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/TemplateRoomChatServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/TemplateRoomChatServiceProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation protocol TemplateRoomChatServiceProtocol { var roomInitializationStatus: CurrentValueSubject { get } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift index e9aa09141..84f09a068 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -88,7 +88,6 @@ enum TemplateRoomChatViewModelAction { case done } - // MARK: - View /// Actions send from the `View` to the `ViewModel`. diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatViewModel.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatViewModel.swift index 609d6a1f6..91da403b3 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatViewModel.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatViewModel.swift @@ -14,30 +14,20 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI -typealias TemplateRoomChatViewModelType = StateStoreViewModel +typealias TemplateRoomChatViewModelType = StateStoreViewModel class TemplateRoomChatViewModel: TemplateRoomChatViewModelType, TemplateRoomChatViewModelProtocol { - enum Constants { - static let maxTimeBeforeNewBubble: TimeInterval = 5*60 + static let maxTimeBeforeNewBubble: TimeInterval = 5 * 60 } - // MARK: - Properties - - // MARK: Private private let templateRoomChatService: TemplateRoomChatServiceProtocol - // MARK: Public - var callback: ((TemplateRoomChatViewModelAction) -> Void)? - // MARK: - Setup - init(templateRoomChatService: TemplateRoomChatServiceProtocol) { self.templateRoomChatService = templateRoomChatService super.init(initialViewState: Self.defaultState(templateRoomChatService: templateRoomChatService)) @@ -70,9 +60,8 @@ class TemplateRoomChatViewModel: TemplateRoomChatViewModelType, TemplateRoomChat } private static func makeBubbles(messages: [TemplateRoomChatMessage]) -> [TemplateRoomChatBubble] { - var bubbleOrder = [String]() - var bubbleMap = [String:TemplateRoomChatBubble]() + var bubbleMap = [String: TemplateRoomChatBubble]() messages.enumerated().forEach { i, message in // New message content @@ -85,9 +74,8 @@ class TemplateRoomChatViewModel: TemplateRoomChatViewModelType, TemplateRoomChat let lastBubbleId = bubbleOrder.last, var lastBubble = bubbleMap[lastBubbleId], lastBubble.sender.id == message.sender.id, - let interveningTime = lastBubble.items.last?.timestamp.timeIntervalSince(message.timestamp), - abs(interveningTime) < Constants.maxTimeBeforeNewBubble - { + let interveningTime = lastBubble.items.last?.timestamp.timeIntervalSince(message.timestamp), + abs(interveningTime) < Constants.maxTimeBeforeNewBubble { // if the last bubble's last message was within // the last 5 minutes append let item = TemplateRoomChatBubbleItem( @@ -108,7 +96,7 @@ class TemplateRoomChatViewModel: TemplateRoomChatViewModelType, TemplateRoomChat bubbleMap[bubble.id] = bubble } } - return bubbleOrder.compactMap({ bubbleMap[$0] }) + return bubbleOrder.compactMap { bubbleMap[$0] } } // MARK: - Public diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatViewModelProtocol.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatViewModelProtocol.swift index 3dd0ad937..4c2799351 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Test/UI/TemplateRoomChatUITests.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Test/UI/TemplateRoomChatUITests.swift index 449eddcc0..cf764cb2d 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Test/UI/TemplateRoomChatUITests.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Test/UI/TemplateRoomChatUITests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class TemplateRoomChatUITests: MockScreenTestCase { func testInitializingRoom() { @@ -44,12 +44,11 @@ class TemplateRoomChatUITests: MockScreenTestCase { // Verify bubble grouping with: // 3 bubbles - let bubbleCount = app.images.matching(identifier:"bubbleImage").count + let bubbleCount = app.images.matching(identifier: "bubbleImage").count XCTAssertEqual(bubbleCount, 3) // and 4 text items - let bubbleTextItemCount = app.staticTexts.matching(identifier:"bubbleTextContent").count + let bubbleTextItemCount = app.staticTexts.matching(identifier: "bubbleTextContent").count XCTAssertEqual(bubbleTextItemCount, 4) } - } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Test/Unit/TemplateRoomChatViewModelTests.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Test/Unit/TemplateRoomChatViewModelTests.swift index ea39fe0e8..5a99f0a58 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Test/Unit/TemplateRoomChatViewModelTests.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Test/Unit/TemplateRoomChatViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,13 +14,12 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI class TemplateRoomChatViewModelTests: XCTestCase { - var service: MockTemplateRoomChatService! var viewModel: TemplateRoomChatViewModel! var context: TemplateRoomChatViewModel.Context! @@ -39,18 +38,17 @@ class TemplateRoomChatViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.roomInitializationStatus, .initialized) } - func testSendMessageUpdatesReceived() throws { let bubblesPublisher: AnyPublisher<[[TemplateRoomChatBubble]], Never> = context.$viewState.map(\.bubbles).removeDuplicates().collect(2).first().eraseToAnyPublisher() let awaitDeferred = xcAwaitDeferred(bubblesPublisher) - let newMessage: String = "Let's Go" + let newMessage = "Let's Go" service.send(textMessage: newMessage) let result: [[TemplateRoomChatBubble]]? = try awaitDeferred() // Test that the update to the messages in turn updates the view's // the last bubble by appending another text item, asserting the body. - guard let item:TemplateRoomChatBubbleItem = result?.last?.last?.items.last, + guard let item: TemplateRoomChatBubbleItem = result?.last?.last?.items.last, case TemplateRoomChatBubbleItemContent.message(let message) = item.content, case let TemplateRoomChatMessageContent.text(text) = message else { XCTFail() diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift index fb70cf503..2e755636a 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChat.swift @@ -14,25 +14,18 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI struct TemplateRoomChat: View { - - // MARK: - Properties - - // MARK: Private - @Environment(\.theme) private var theme: ThemeSwiftUI - // MARK: Public - @ObservedObject var viewModel: TemplateRoomChatViewModel.Context var presentedModally = false var body: some View { VStack { - VStack{ + VStack { roomContent }.frame(maxHeight: .infinity) @@ -51,7 +44,6 @@ struct TemplateRoomChat: View { .animation(.easeOut(duration: 0.25)) .transition(.move(edge: .trailing)) .padding() - } .background(theme.colors.background) .navigationTitle(viewModel.viewState.roomName ?? "Chat") @@ -83,14 +75,14 @@ struct TemplateRoomChat: View { private var bubbleList: some View { ScrollViewReader { reader in - ScrollView{ + ScrollView { LazyVStack { ForEach(viewModel.viewState.bubbles) { bubble in TemplateRoomChatBubbleView(bubble: bubble) .id(bubble.id) } } - .onAppear{ + .onAppear { guard let lastBubbleId = viewModel.viewState.bubbles.last?.id else { return } reader.scrollTo(lastBubbleId, anchor: .bottom) @@ -116,7 +108,7 @@ struct TemplateRoomChat: View { } private var itemCount: Int { - return viewModel.viewState + viewModel.viewState .bubbles .map(\.items) .map(\.count) diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleContentView.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleContentView.swift index 59f4bac29..23e7c69cb 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleContentView.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleContentView.swift @@ -17,15 +17,8 @@ import SwiftUI struct TemplateRoomChatBubbleContentView: View { - - // MARK: - Properties - - // MARK: Private - @Environment(\.theme) private var theme: ThemeSwiftUI - // MARK: Public - let bubbleItem: TemplateRoomChatBubbleItem var body: some View { diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleImage.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleImage.swift index 9014e8209..f8e4edcb3 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleImage.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleImage.swift @@ -17,15 +17,8 @@ import SwiftUI struct TemplateRoomChatBubbleImage: View { - - // MARK: - Properties - - // MARK: Private - @Environment(\.theme) private var theme: ThemeSwiftUI - // MARK: Public - let imageContent: TemplateRoomChatMessageImageContent var body: some View { diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleMessage.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleMessage.swift index aa1cb44c6..b57f3026c 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleMessage.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleMessage.swift @@ -17,15 +17,8 @@ import SwiftUI struct TemplateRoomChatBubbleMessage: View { - - // MARK: - Properties - - // MARK: Private - @Environment(\.theme) private var theme: ThemeSwiftUI - // MARK: Public - let messageContent: TemplateRoomChatMessageTextContent var body: some View { diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift index f7cd4387b..70495bd25 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/View/TemplateRoomChatBubbleView.swift @@ -17,22 +17,15 @@ import SwiftUI struct TemplateRoomChatBubbleView: View { - - // MARK: - Properties - - // MARK: Private - @Environment(\.theme) private var theme: ThemeSwiftUI - // MARK: Public - let bubble: TemplateRoomChatBubble var body: some View { - HStack(alignment: .top){ + HStack(alignment: .top) { AvatarImage(avatarData: bubble.sender.avatarData, size: .xSmall) .accessibility(identifier: "bubbleImage") - VStack(alignment: .leading){ + VStack(alignment: .leading) { Text(bubble.sender.displayName ?? "") .foregroundColor(theme.userColor(for: bubble.sender.id)) .font(theme.fonts.bodySB) @@ -42,7 +35,7 @@ struct TemplateRoomChatBubbleView: View { } Spacer() } - //add to a style + // add to a style .padding(.horizontal) .padding(.vertical, 8) .frame(maxWidth: .infinity) diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift index 8d08f3de2..26bbbbb99 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Coordinator/TemplateRoomListCoordinator.swift @@ -21,23 +21,14 @@ struct TemplateRoomListCoordinatorParameters { } final class TemplateRoomListCoordinator: Coordinator, Presentable { - - // MARK: - Properties - - // MARK: Private - private let parameters: TemplateRoomListCoordinatorParameters private let templateRoomListHostingController: UIViewController private var templateRoomListViewModel: TemplateRoomListViewModelProtocol - - // MARK: Public // Must be used only internally var childCoordinators: [Coordinator] = [] var callback: ((TemplateRoomListCoordinatorAction) -> Void)? - // MARK: - Setup - init(parameters: TemplateRoomListCoordinatorParameters) { self.parameters = parameters let viewModel = TemplateRoomListViewModel(templateRoomListService: TemplateRoomListService(session: parameters.session)) @@ -59,12 +50,11 @@ final class TemplateRoomListCoordinator: Coordinator, Presentable { self.callback?(.didSelectRoom(roomId)) case .done: self.callback?(.done) - break } } } func toPresentable() -> UIViewController { - return self.templateRoomListHostingController + templateRoomListHostingController } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift index f0b6b1406..8b8c0e2a3 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/MockTemplateRoomListScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation import SwiftUI - /// Using an enum for the screen allows you define the different state cases with /// the relevant associated data for each case. enum MockTemplateRoomListScreenState: MockScreenState, CaseIterable { diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/MatrixSDK/TemplateRoomListService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/MatrixSDK/TemplateRoomListService.swift index 8678e6ddd..cd8b13649 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/MatrixSDK/TemplateRoomListService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/MatrixSDK/TemplateRoomListService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,36 +14,26 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class TemplateRoomListService: TemplateRoomListServiceProtocol { - - // MARK: - Properties - - // MARK: Private - private let session: MXSession private var listenerReference: Any? - - // MARK: Public + private(set) var roomsSubject: CurrentValueSubject<[TemplateRoomListRoom], Never> - // MARK: - Setup - init(session: MXSession) { self.session = session let unencryptedRooms = session.rooms - .filter({ !$0.summary.isEncrypted }) + .filter { !$0.summary.isEncrypted } .map(TemplateRoomListRoom.init(mxRoom:)) - self.roomsSubject = CurrentValueSubject(unencryptedRooms) + roomsSubject = CurrentValueSubject(unencryptedRooms) } - } -fileprivate extension TemplateRoomListRoom { - +private extension TemplateRoomListRoom { init(mxRoom: MXRoom) { self.init(id: mxRoom.roomId, avatar: mxRoom.avatarData, displayName: mxRoom.summary.displayname) } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/Mock/MockTemplateRoomListService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/Mock/MockTemplateRoomListService.swift index 1fcd53cb1..064884561 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/Mock/MockTemplateRoomListService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/Mock/MockTemplateRoomListService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +14,10 @@ // limitations under the License. // -import Foundation import Combine +import Foundation class MockTemplateRoomListService: TemplateRoomListServiceProtocol { - static let mockRooms = [ TemplateRoomListRoom(id: "!aaabaa:matrix.org", avatar: MockAvatarInput.example, displayName: "Matrix Discussion"), TemplateRoomListRoom(id: "!zzasds:matrix.org", avatar: MockAvatarInput.example, displayName: "Element Mobile"), @@ -31,6 +30,6 @@ class MockTemplateRoomListService: TemplateRoomListServiceProtocol { } func simulateUpdate(rooms: [TemplateRoomListRoom]) { - self.roomsSubject.send(rooms) + roomsSubject.send(rooms) } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/TemplateRoomListServiceProtocol.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/TemplateRoomListServiceProtocol.swift index ba4248446..2eed29bb5 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/TemplateRoomListServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Service/TemplateRoomListServiceProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import Foundation import Combine +import Foundation protocol TemplateRoomListServiceProtocol { var roomsSubject: CurrentValueSubject<[TemplateRoomListRoom], Never> { get } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListModels.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListModels.swift index 57452cc1c..6d1c67811 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListModels.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListViewModel.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListViewModel.swift index 14c84cc11..853abcf51 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListViewModel.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListViewModel.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,27 +14,16 @@ // limitations under the License. // -import SwiftUI import Combine +import SwiftUI -typealias TemplateRoomListViewModelType = StateStoreViewModel +typealias TemplateRoomListViewModelType = StateStoreViewModel class TemplateRoomListViewModel: TemplateRoomListViewModelType, TemplateRoomListViewModelProtocol { - - // MARK: - Properties - - // MARK: Private - private let templateRoomListService: TemplateRoomListServiceProtocol - // MARK: Public - var callback: ((TemplateRoomListViewModelAction) -> Void)? - // MARK: - Setup - init(templateRoomListService: TemplateRoomListServiceProtocol) { self.templateRoomListService = templateRoomListService super.init(initialViewState: Self.defaultState(templateRoomListService: templateRoomListService)) @@ -42,7 +31,7 @@ class TemplateRoomListViewModel: TemplateRoomListViewModelType, TemplateRoomList } private static func defaultState(templateRoomListService: TemplateRoomListServiceProtocol) -> TemplateRoomListViewState { - return TemplateRoomListViewState(rooms: templateRoomListService.roomsSubject.value) + TemplateRoomListViewState(rooms: templateRoomListService.roomsSubject.value) } private func startObservingRooms() { diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListViewModelProtocol.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListViewModelProtocol.swift index ee9e96060..217155848 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Test/UI/TemplateRoomListUITests.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Test/UI/TemplateRoomListUITests.swift index 8ee0012e2..9535644d4 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Test/UI/TemplateRoomListUITests.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Test/UI/TemplateRoomListUITests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,8 +14,8 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class TemplateRoomListUITests: MockScreenTestCase { func testTemplateRoomListNoRooms() { @@ -29,8 +29,7 @@ class TemplateRoomListUITests: MockScreenTestCase { func testTemplateRoomListRooms() { app.goToScreenWithIdentifier(MockTemplateRoomListScreenState.rooms.title) - let displayNameCount = app.buttons.matching(identifier:"roomNameText").count + let displayNameCount = app.buttons.matching(identifier: "roomNameText").count XCTAssertEqual(displayNameCount, 3) } - } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Test/Unit/TemplateRoomListViewModelTests.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Test/Unit/TemplateRoomListViewModelTests.swift index 8f69bb605..e39303007 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Test/Unit/TemplateRoomListViewModelTests.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/Test/Unit/TemplateRoomListViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,14 +14,14 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI class TemplateRoomListViewModelTests: XCTestCase { - private enum Constants { - } + private enum Constants { } + var service: MockTemplateRoomListService! var viewModel: TemplateRoomListViewModel! var context: TemplateRoomListViewModel.Context! @@ -46,7 +46,7 @@ class TemplateRoomListViewModelTests: XCTestCase { let updatedRooms = Array(MockTemplateRoomListService.mockRooms.dropLast()) let roomsPublisher = context.$viewState.map(\.rooms).removeDuplicates().collect(2).first() let awaitDeferred = xcAwaitDeferred(roomsPublisher) - service.simulateUpdate(rooms: updatedRooms) + service.simulateUpdate(rooms: updatedRooms) XCTAssertEqual(try awaitDeferred(), [MockTemplateRoomListService.mockRooms, updatedRooms]) } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomList.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomList.swift index 9ba36a2b4..721e2d27c 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomList.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomList.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,15 +17,8 @@ import SwiftUI struct TemplateRoomList: View { - - // MARK: - Properties - - // MARK: Private - @Environment(\.theme) private var theme: ThemeSwiftUI - // MARK: Public - @ObservedObject var viewModel: TemplateRoomListViewModelType.Context var body: some View { @@ -47,7 +40,7 @@ struct TemplateRoomList: View { .foregroundColor(theme.colors.primaryContent) .accessibility(identifier: "errorMessage") } else { - ScrollView{ + ScrollView { LazyVStack(spacing: 0) { ForEach(viewModel.viewState.rooms) { room in Button { @@ -66,7 +59,6 @@ struct TemplateRoomList: View { // MARK: - Previews struct TemplateRoomList_Previews: PreviewProvider { - static let stateRenderer = MockTemplateRoomListScreenState.stateRenderer static var previews: some View { stateRenderer.screenGroup(addNavigation: true) diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift index c7418e635..a8db7b8ba 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift @@ -17,27 +17,20 @@ import SwiftUI struct TemplateRoomListRow: View { - - // MARK: - Properties - - // MARK: Private - @Environment(\.theme) private var theme: ThemeSwiftUI - // MARK: Public - let avatar: AvatarInputProtocol let displayName: String? var body: some View { - HStack{ + HStack { AvatarImage(avatarData: avatar, size: .medium) Text(displayName ?? "") .foregroundColor(theme.colors.primaryContent) - .accessibility(identifier: "roomNameText") + .accessibility(identifier: "roomNameText") Spacer() } - //add to a style + // add to a style .padding(.horizontal) .padding(.vertical, 8) .frame(maxWidth: .infinity) diff --git a/RiotSwiftUI/Modules/UserSessions/DeviceType/DeviceType+Element.swift b/RiotSwiftUI/Modules/UserSessions/Common/DeviceType.swift similarity index 61% rename from RiotSwiftUI/Modules/UserSessions/DeviceType/DeviceType+Element.swift rename to RiotSwiftUI/Modules/UserSessions/Common/DeviceType.swift index ecbad3537..0f0685778 100644 --- a/RiotSwiftUI/Modules/UserSessions/DeviceType/DeviceType+Element.swift +++ b/RiotSwiftUI/Modules/UserSessions/Common/DeviceType.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,42 +17,38 @@ import Foundation import SwiftUI -extension DeviceType { +/// Client type +enum DeviceType { + case desktop + case web + case mobile + case unknown var image: Image { - - let image: Image - switch self { case .desktop: - image = Image(Asset.Images.deviceTypeDesktop.name) + return Image(Asset.Images.deviceTypeDesktop.name) case .web: - image = Image(Asset.Images.deviceTypeWeb.name) + return Image(Asset.Images.deviceTypeWeb.name) case .mobile: - image = Image(Asset.Images.deviceTypeMobile.name) + return Image(Asset.Images.deviceTypeMobile.name) case .unknown: - image = Image(Asset.Images.deviceTypeUnknown.name) + return Image(Asset.Images.deviceTypeUnknown.name) } - - return image } var name: String { - let name: String - let appName = AppInfo.current.displayName switch self { case .desktop: - name = VectorL10n.deviceNameDesktop(appName) + return VectorL10n.deviceNameDesktop(appName) case .web: - name = VectorL10n.deviceNameWeb(appName) + return VectorL10n.deviceNameWeb(appName) case .mobile: - name = VectorL10n.deviceNameMobile(appName) + return VectorL10n.deviceNameMobile(appName) case .unknown: - name = VectorL10n.deviceNameUnknown + return VectorL10n.deviceNameUnknown } - - return name } } diff --git a/RiotSwiftUI/Modules/UserSessions/Common/UserAgentParser.swift b/RiotSwiftUI/Modules/UserSessions/Common/UserAgentParser.swift new file mode 100644 index 000000000..2c0a10487 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/Common/UserAgentParser.swift @@ -0,0 +1,221 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct UserAgent { + let deviceType: DeviceType + let deviceModel: String? + let deviceOS: String? + let clientName: String? + let clientVersion: String? + + static let unknown = UserAgent(deviceType: .unknown, + deviceModel: nil, + deviceOS: nil, + clientName: nil, + clientVersion: nil) +} + +extension UserAgent: Equatable { } + +enum UserAgentParser { + private enum Constants { + static let deviceInfoRegexPattern = "\\((?:[^)(]+|\\((?:[^)(]+|\\([^)(]*\\))*\\))*\\)" + + static let androidKeyword = "; MatrixAndroidSdk2" + static let iosKeyword = "; iOS " + static let desktopKeyword = " Electron/" + static let webKeyword = "Mozilla/" + } + + static func parse(_ userAgent: String) -> UserAgent { + if userAgent.vc_caseInsensitiveContains(Constants.androidKeyword) { + return parseAndroid(userAgent) + } else if userAgent.vc_caseInsensitiveContains(Constants.iosKeyword) { + return parseIOS(userAgent) + } else if userAgent.vc_caseInsensitiveContains(Constants.desktopKeyword) { + return parseDesktop(userAgent) + } else if userAgent.vc_caseInsensitiveContains(Constants.webKeyword) { + return parseWeb(userAgent) + } + return .unknown + } + + // Legacy: Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0) + // New: Element dbg/1.5.0-dev (Xiaomi Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.0) + private static func parseAndroid(_ userAgent: String) -> UserAgent { + var deviceModel: String? + var deviceOS: String? + var clientName: String? + var clientVersion: String? + + let (beforeSlash, afterSlash) = userAgent.splitByFirst("/") + clientName = beforeSlash + if let afterSlash = afterSlash { + let (beforeSpace, afterSpace) = afterSlash.splitByFirst(" ") + clientVersion = beforeSpace + if let afterSpace = afterSpace { + if let deviceInfo = findFirstDeviceInfo(in: afterSpace) { + let deviceInfoComponents = deviceInfo.components(separatedBy: "; ") + let isLegacy = deviceInfoComponents[safe: 0] == "Linux" + if isLegacy { + // find the segment starting with "Android" + if let osSegmentIndex = deviceInfoComponents.firstIndex(where: { $0.hasPrefix("Android") }) { + deviceOS = deviceInfoComponents[safe: osSegmentIndex] + deviceModel = deviceInfoComponents[safe: osSegmentIndex + 1] + } + } else { + deviceModel = deviceInfoComponents[safe: 0] + deviceOS = deviceInfoComponents[safe: 1] + } + } + } + } + + return UserAgent(deviceType: .mobile, + deviceModel: deviceModel, + deviceOS: deviceOS, + clientName: clientName, + clientVersion: clientVersion) + } + + // Legacy: Riot/1.8.21 (iPhone; iOS 15.2; Scale/3.00) + // New: Riot/1.8.21 (iPhone X; iOS 15.2; Scale/3.00) + private static func parseIOS(_ userAgent: String) -> UserAgent { + var deviceModel: String? + var deviceOS: String? + var clientName: String? + var clientVersion: String? + + let (beforeSlash, afterSlash) = userAgent.splitByFirst("/") + clientName = beforeSlash + if let afterSlash = afterSlash { + let (beforeSpace, afterSpace) = afterSlash.splitByFirst(" ") + clientVersion = beforeSpace + if let afterSpace = afterSpace { + if let deviceInfo = findFirstDeviceInfo(in: afterSpace) { + let deviceInfoComponents = deviceInfo.components(separatedBy: "; ") + deviceModel = deviceInfoComponents[safe: 0] + deviceOS = deviceInfoComponents[safe: 1] + } + } + } + + return UserAgent(deviceType: .mobile, + deviceModel: deviceModel, + deviceOS: deviceOS, + clientName: clientName, + clientVersion: clientVersion) + } + + // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36 + private static func parseDesktop(_ userAgent: String) -> UserAgent { + var deviceOS: String? + let browserInfo = browserInfo(for: userAgent) + + if let deviceInfo = findFirstDeviceInfo(in: userAgent) { + let deviceInfoComponents = deviceInfo.components(separatedBy: "; ") + if deviceInfoComponents[safe: 1]?.hasPrefix("Android") == true { + deviceOS = deviceInfoComponents[safe: 1] + } else if deviceInfoComponents.first == "Macintosh" { + var osFull = deviceInfoComponents[safe: 1] + osFull = osFull?.replacingOccurrences(of: "Intel ", with: "") + osFull = osFull?.replacingOccurrences(of: "Mac OS X", with: "macOS") + osFull = osFull?.replacingOccurrences(of: "_", with: ".") + deviceOS = osFull + } else { + deviceOS = deviceInfoComponents.first + } + } + + return UserAgent(deviceType: .desktop, + deviceModel: nil, + deviceOS: deviceOS, + clientName: browserInfo.name, + clientVersion: browserInfo.version) + } + + // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 + private static func parseWeb(_ userAgent: String) -> UserAgent { + let desktopUserAgent = parseDesktop(userAgent) + + return UserAgent(deviceType: .web, + deviceModel: desktopUserAgent.deviceModel, + deviceOS: desktopUserAgent.deviceOS, + clientName: desktopUserAgent.clientName, + clientVersion: desktopUserAgent.clientVersion) + } + + private static func findFirstDeviceInfo(in string: String) -> String? { + guard let regex = try? NSRegularExpression(pattern: Constants.deviceInfoRegexPattern, + options: .caseInsensitive) else { + return nil + } + var range = regex.rangeOfFirstMatch(in: string, range: NSRange(string.startIndex..., in: string)) + if range.location != NSNotFound { + range.location += 1 + range.length -= 2 + return string[range] + } + return nil + } + + private static func browserInfo(for userAgent: String) -> (name: String?, version: String?) { + let components = userAgent.components(separatedBy: " ") + if components.last?.hasPrefix("Firefox") == true { + let version = components.last?.components(separatedBy: "/").last + return ("Firefox", version) + } else if components.last?.hasPrefix("Safari") == true, + components[safe: components.count - 2]?.hasPrefix("Mobile") == true { + // mobile browser + let possibleBrowserComponents = components[safe: components.count - 3] + if possibleBrowserComponents?.hasPrefix("Version") == true { + let version = possibleBrowserComponents?.components(separatedBy: "/").last + return ("Safari", version) + } else { + let components = possibleBrowserComponents?.components(separatedBy: "/") + return (components?.first, components?.last) + } + } else if components.last?.hasPrefix("Safari") == true, components[safe: components.count - 2]?.hasPrefix("Version") == true { + let version = components[safe: components.count - 2]?.components(separatedBy: "/").last + return ("Safari", version) + } else { + // regular browser + let browserComponent = components[safe: components.count - 2] + let components = browserComponent?.components(separatedBy: "/") + return (components?.first, components?[safe: 1]) + } + } +} + +private extension String { + subscript(_ range: NSRange) -> String { + let start = index(startIndex, offsetBy: range.lowerBound) + let end = index(startIndex, offsetBy: range.upperBound) + let subString = self[start.. (String?, String?) { + guard let delimiterIndex = firstIndex(of: delimiter) else { + return (nil, nil) + } + let before = String(prefix(upTo: delimiterIndex)) + let after = String(suffix(from: index(after: delimiterIndex))) + return (before, after) + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionInfo/UserSessionInfo.swift b/RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift similarity index 50% rename from RiotSwiftUI/Modules/UserSessions/UserSessionInfo/UserSessionInfo.swift rename to RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift index 6009bfcb2..79fe49ec2 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionInfo/UserSessionInfo.swift +++ b/RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,21 +18,11 @@ import Foundation /// Represents a user session information struct UserSessionInfo: Identifiable { - - /// Delay after which session is considered inactive, 90 days - static let inactiveSessionDurationTreshold: TimeInterval = 90 * 86400 - - // MARK: - Properties - - var id: String { - return sessionId - } - /// The session identifier - let sessionId: String + let id: String /// The session display name - let sessionName: String? + let name: String? /// The device type used by the session let deviceType: DeviceType @@ -45,34 +35,44 @@ struct UserSessionInfo: Identifiable { /// Last time the session was active let lastSeenTimestamp: TimeInterval? - - /// True to indicate that session has been used under `inactiveSessionDurationTreshold` value - let isSessionActive: Bool - - // MARK: - Setup - - init(sessionId: String, - sessionName: String?, - deviceType: DeviceType, - isVerified: Bool, - lastSeenIP: String?, - lastSeenTimestamp: TimeInterval?) { - - self.sessionId = sessionId - self.sessionName = sessionName - self.deviceType = deviceType - self.isVerified = isVerified - self.lastSeenIP = lastSeenIP - self.lastSeenTimestamp = lastSeenTimestamp - - if let lastSeenTimestamp = lastSeenTimestamp { - let elapsedTime = Date().timeIntervalSince1970 - lastSeenTimestamp - - let isSessionInactive = elapsedTime >= Self.inactiveSessionDurationTreshold - self.isSessionActive = !isSessionInactive - } else { - self.isSessionActive = true - } + // MARK: - Application Properties + + /// Application name used by the session + let applicationName: String? + + /// Application version used by the session + let applicationVersion: String? + + /// Application URL used by the session. Only applicable for web sessions. + let applicationURL: String? + + // MARK: - Device Properties + + /// Device model + let deviceModel: String? + + /// Device OS + let deviceOS: String? + + /// Last seen IP location + let lastSeenIPLocation: String? + + /// Client name + let clientName: String? + + /// Client version + let clientVersion: String? + + /// True to indicate that session has been used under `inactiveSessionDurationTreshold` value + let isActive: Bool + + /// True to indicate that this is current user session + let isCurrent: Bool +} + +extension UserSessionInfo: Equatable { + static func == (lhs: UserSessionInfo, rhs: UserSessionInfo) -> Bool { + lhs.id == rhs.id } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionFormatters/UserSessionLastActivityFormatter.swift b/RiotSwiftUI/Modules/UserSessions/Common/UserSessionLastActivityFormatter.swift similarity index 79% rename from RiotSwiftUI/Modules/UserSessions/UserSessionFormatters/UserSessionLastActivityFormatter.swift rename to RiotSwiftUI/Modules/UserSessions/Common/UserSessionLastActivityFormatter.swift index 891e0919b..0aa253ae7 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionFormatters/UserSessionLastActivityFormatter.swift +++ b/RiotSwiftUI/Modules/UserSessions/Common/UserSessionLastActivityFormatter.swift @@ -17,10 +17,7 @@ import Foundation /// Enables to build last activity date string -class UserSessionLastActivityFormatter { - - // MARK: - Constants - +enum UserSessionLastActivityFormatter { private static var lastActivityDateFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.locale = Locale.current @@ -30,13 +27,10 @@ class UserSessionLastActivityFormatter { return dateFormatter }() - // MARK: - Public - /// Session last activity string - func lastActivityDateString(from lastActivityTimestamp: TimeInterval) -> String { - + static func lastActivityDateString(from lastActivityTimestamp: TimeInterval) -> String { let date = Date(timeIntervalSince1970: lastActivityTimestamp) - return UserSessionLastActivityFormatter.lastActivityDateFormatter.string(from: date) + return Self.lastActivityDateFormatter.string(from: date) } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionFormatters/UserSessionNameFormatter.swift b/RiotSwiftUI/Modules/UserSessions/Common/UserSessionNameFormatter.swift similarity index 88% rename from RiotSwiftUI/Modules/UserSessions/UserSessionFormatters/UserSessionNameFormatter.swift rename to RiotSwiftUI/Modules/UserSessions/Common/UserSessionNameFormatter.swift index 0aed1082f..492e27226 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionFormatters/UserSessionNameFormatter.swift +++ b/RiotSwiftUI/Modules/UserSessions/Common/UserSessionNameFormatter.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,11 +17,9 @@ import Foundation /// Enables to build user session name -class UserSessionNameFormatter { - +enum UserSessionNameFormatter { /// Session name with client name and session display name - func sessionName(deviceType: DeviceType, sessionDisplayName: String?) -> String { - + static func sessionName(deviceType: DeviceType, sessionDisplayName: String?) -> String { let sessionName: String let clientName = deviceType.name diff --git a/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarView.swift similarity index 94% rename from RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift rename to RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarView.swift index a24567171..d9ee4961e 100644 --- a/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarView.swift +++ b/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarView.swift @@ -14,12 +14,11 @@ // limitations under the License. // -import SwiftUI import DesignKit +import SwiftUI /// Avatar view for device struct DeviceAvatarView: View { - @Environment(\.theme) var theme: ThemeSwiftUI var viewData: DeviceAvatarViewData @@ -29,7 +28,6 @@ struct DeviceAvatarView: View { var body: some View { ZStack(alignment: .bottomTrailing) { - // Device image VStack(alignment: .center) { viewData.deviceType.image @@ -41,7 +39,6 @@ struct DeviceAvatarView: View { // Verification badge if let isVerified = viewData.isVerified { - Image(isVerified ? Asset.Images.userSessionVerified.name : Asset.Images.userSessionUnverified.name) .frame(maxWidth: CGFloat(badgeSize), maxHeight: CGFloat(badgeSize)) .shapedBorder(color: theme.colors.system, borderWidth: 1, shape: Circle()) @@ -55,9 +52,8 @@ struct DeviceAvatarView: View { } struct DeviceAvatarViewListPreview: View { - var viewDataList: [DeviceAvatarViewData] { - return [ + [ DeviceAvatarViewData(deviceType: .desktop, isVerified: true), DeviceAvatarViewData(deviceType: .web, isVerified: true), DeviceAvatarViewData(deviceType: .mobile, isVerified: true), @@ -68,7 +64,7 @@ struct DeviceAvatarViewListPreview: View { var body: some View { HStack { VStack(alignment: .center, spacing: 20) { - DeviceAvatarView(viewData: DeviceAvatarViewData.init(deviceType: .web, isVerified: true)) + DeviceAvatarView(viewData: DeviceAvatarViewData(deviceType: .web, isVerified: true)) DeviceAvatarView(viewData: DeviceAvatarViewData(deviceType: .desktop, isVerified: false)) DeviceAvatarView(viewData: DeviceAvatarViewData(deviceType: .mobile, isVerified: true)) DeviceAvatarView(viewData: DeviceAvatarViewData(deviceType: .unknown, isVerified: false)) @@ -78,7 +74,6 @@ struct DeviceAvatarViewListPreview: View { } struct DeviceAvatarView_Previews: PreviewProvider { - static var previews: some View { Group { DeviceAvatarViewListPreview().theme(.light).preferredColorScheme(.light) diff --git a/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarViewData.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarViewData.swift similarity index 97% rename from RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarViewData.swift rename to RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarViewData.swift index 19c6cfd1d..397e945dc 100644 --- a/RiotSwiftUI/Modules/UserSessions/DeviceAvatar/DeviceAvatarViewData.swift +++ b/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarViewData.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,8 +19,6 @@ import SwiftUI /// View data for DeviceAvatarView struct DeviceAvatarViewData { - let deviceType: DeviceType - let isVerified: Bool? } diff --git a/RiotSwiftUI/Modules/UserSessions/Common/View/SeparatorLine.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/SeparatorLine.swift new file mode 100644 index 000000000..2f56761ef --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/Common/View/SeparatorLine.swift @@ -0,0 +1,28 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct SeparatorLine: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + var body: some View { + Rectangle() + .fill(theme.colors.quinaryContent) + .frame(maxWidth: .infinity) + .frame(height: 1.0) + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardView.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardView.swift new file mode 100644 index 000000000..8fa03b02c --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardView.swift @@ -0,0 +1,179 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import DesignKit +import SwiftUI + +struct UserSessionCardView: View { + @Environment(\.theme) var theme: ThemeSwiftUI + + var viewData: UserSessionCardViewData + + var onVerifyAction: ((String) -> Void)? + var onViewDetailsAction: ((String) -> Void)? + var onLearnMoreAction: (() -> Void)? + + private var verificationStatusImageName: String { + viewData.isVerified ? Asset.Images.userSessionVerified.name : Asset.Images.userSessionUnverified.name + } + + private var verificationStatusText: String { + viewData.isVerified ? VectorL10n.userSessionVerified : VectorL10n.userSessionUnverified + } + + private var verificationStatusColor: Color { + viewData.isVerified ? theme.colors.accent : theme.colors.alert + } + + private var verificationStatusAdditionalInfoText: String { + viewData.isVerified ? VectorL10n.userSessionVerifiedAdditionalInfo : VectorL10n.userSessionUnverifiedAdditionalInfo + } + + private var backgroundShape: RoundedRectangle { + RoundedRectangle(cornerRadius: 8) + } + + private var showExtraInformations: Bool { + viewData.isCurrentSessionDisplayMode == false && (viewData.lastActivityDateString.isEmptyOrNil == false || viewData.lastSeenIPInfo.isEmptyOrNil == false) + } + + var body: some View { + VStack(alignment: .center, spacing: 12) { + DeviceAvatarView(viewData: viewData.deviceAvatarViewData) + + Text(viewData.sessionName) + .font(theme.fonts.headline) + .foregroundColor(theme.colors.primaryContent) + .multilineTextAlignment(.center) + + HStack { + Image(verificationStatusImageName) + Text(verificationStatusText) + .font(theme.fonts.subheadline) + .foregroundColor(verificationStatusColor) + .multilineTextAlignment(.center) + } + + if viewData.isCurrentSessionDisplayMode { + Text(verificationStatusAdditionalInfoText) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .multilineTextAlignment(.center) + } else { + InlineTextButton(verificationStatusAdditionalInfoText + " %@", tappableText: VectorL10n.userSessionLearnMore) { + onLearnMoreAction?() + } + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .multilineTextAlignment(.center) + } + + if showExtraInformations { + VStack(spacing: 2) { + if let lastActivityDateString = viewData.lastActivityDateString, lastActivityDateString.isEmpty == false { + Text(lastActivityDateString) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .multilineTextAlignment(.center) + } + + if let lastSeenIPInfo = viewData.lastSeenIPInfo, lastSeenIPInfo.isEmpty == false { + Text(lastSeenIPInfo) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .multilineTextAlignment(.center) + } + } + } + + if viewData.isVerified == false { + Button { + onVerifyAction?(viewData.sessionId) + } label: { + Text(VectorL10n.userSessionVerifyAction) + .font(theme.fonts.body) + } + .buttonStyle(PrimaryActionButtonStyle()) + .padding(.top, 4) + .padding(.bottom, 3) + .accessibilityIdentifier("userSessionCardVerifyButton") + } + + if viewData.isCurrentSessionDisplayMode { + Text(VectorL10n.userSessionViewDetails) + .font(theme.fonts.body) + .foregroundColor(theme.colors.accent) + .accessibilityIdentifier("userSessionCardViewDetails") + } + } + .padding(24) + .frame(maxWidth: .infinity) + .background(theme.colors.background) + .clipShape(backgroundShape) + .shapedBorder(color: theme.colors.quinaryContent, borderWidth: 1.0, shape: backgroundShape) + .onTapGesture { + if viewData.isCurrentSessionDisplayMode { + onViewDetailsAction?(viewData.sessionId) + } + } + } +} + +struct UserSessionCardViewPreview: View { + @Environment(\.theme) var theme: ThemeSwiftUI + + let viewData: UserSessionCardViewData + + init(isCurrent: Bool = false) { + let sessionInfo = UserSessionInfo(id: "alice", + name: "iOS", + deviceType: .mobile, + isVerified: false, + lastSeenIP: "10.0.0.10", + lastSeenTimestamp: nil, + applicationName: "Element iOS", + applicationVersion: "1.0.0", + applicationURL: nil, + deviceModel: nil, + deviceOS: "iOS 15.5", + lastSeenIPLocation: nil, + clientName: "Element", + clientVersion: "1.0.0", + isActive: true, + isCurrent: isCurrent) + viewData = UserSessionCardViewData(sessionInfo: sessionInfo) + } + + var body: some View { + VStack { + UserSessionCardView(viewData: viewData) + } + .frame(maxWidth: .infinity) + .background(theme.colors.system) + .padding() + } +} + +struct UserSessionCardView_Previews: PreviewProvider { + static var previews: some View { + Group { + UserSessionCardViewPreview(isCurrent: true).theme(.light).preferredColorScheme(.light) + UserSessionCardViewPreview(isCurrent: true).theme(.dark).preferredColorScheme(.dark) + UserSessionCardViewPreview().theme(.light).preferredColorScheme(.light) + UserSessionCardViewPreview().theme(.dark).preferredColorScheme(.dark) + } + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardViewData.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardViewData.swift new file mode 100644 index 000000000..fff1c6564 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardViewData.swift @@ -0,0 +1,75 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// View data for UserSessionCardView +struct UserSessionCardViewData { + var id: String { + sessionId + } + + let sessionId: String + + let sessionName: String + + let isVerified: Bool + + let lastActivityDateString: String? + + let lastSeenIPInfo: String? + + let deviceAvatarViewData: DeviceAvatarViewData + + /// Indicate if the current user session is shown and to adpat the layout + let isCurrentSessionDisplayMode: Bool + + init(sessionId: String, + sessionDisplayName: String?, + deviceType: DeviceType, + isVerified: Bool, + lastActivityTimestamp: TimeInterval?, + lastSeenIP: String?, + isCurrentSessionDisplayMode: Bool = false) { + self.sessionId = sessionId + sessionName = UserSessionNameFormatter.sessionName(deviceType: deviceType, sessionDisplayName: sessionDisplayName) + self.isVerified = isVerified + + var lastActivityDateString: String? + + if let lastActivityTimestamp = lastActivityTimestamp { + lastActivityDateString = UserSessionLastActivityFormatter.lastActivityDateString(from: lastActivityTimestamp) + } + + self.lastActivityDateString = lastActivityDateString + lastSeenIPInfo = lastSeenIP + deviceAvatarViewData = DeviceAvatarViewData(deviceType: deviceType, isVerified: nil) + + self.isCurrentSessionDisplayMode = isCurrentSessionDisplayMode + } +} + +extension UserSessionCardViewData { + init(sessionInfo: UserSessionInfo) { + self.init(sessionId: sessionInfo.id, + sessionDisplayName: sessionInfo.name, + deviceType: sessionInfo.deviceType, + isVerified: sessionInfo.isVerified, + lastActivityTimestamp: sessionInfo.lastSeenTimestamp, + lastSeenIP: sessionInfo.lastSeenIP, + isCurrentSessionDisplayMode: sessionInfo.isCurrent) + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift new file mode 100644 index 000000000..83dc0361d --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift @@ -0,0 +1,116 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import CommonKit + +struct UserSessionsFlowCoordinatorParameters { + let session: MXSession + let router: NavigationRouterType? +} + +final class UserSessionsFlowCoordinator: Coordinator, Presentable { + private let parameters: UserSessionsFlowCoordinatorParameters + private let navigationRouter: NavigationRouterType + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: (() -> Void)? + + init(parameters: UserSessionsFlowCoordinatorParameters) { + self.parameters = parameters + navigationRouter = parameters.router ?? NavigationRouter(navigationController: RiotNavigationController()) + } + + // MARK: - Private + + private func pushScreen(with coordinator: Coordinator & Presentable) { + add(childCoordinator: coordinator) + + navigationRouter.push(coordinator, animated: true, popCompletion: { [weak self] in + self?.remove(childCoordinator: coordinator) + }) + + coordinator.start() + } + + private func createUserSessionsOverviewCoordinator() -> UserSessionsOverviewCoordinator { + let parameters = UserSessionsOverviewCoordinatorParameters(session: parameters.session) + + let coordinator = UserSessionsOverviewCoordinator(parameters: parameters) + coordinator.completion = { [weak self] result in + guard let self = self else { return } + switch result { + case let .openSessionOverview(sessionInfo: sessionInfo): + self.openSessionOverview(sessionInfo: sessionInfo) + } + } + return coordinator + } + + private func openSessionDetails(sessionInfo: UserSessionInfo) { + let coordinator = createUserSessionDetailsCoordinator(sessionInfo: sessionInfo) + pushScreen(with: coordinator) + } + + private func createUserSessionDetailsCoordinator(sessionInfo: UserSessionInfo) -> UserSessionDetailsCoordinator { + let parameters = UserSessionDetailsCoordinatorParameters(session: sessionInfo) + return UserSessionDetailsCoordinator(parameters: parameters) + } + + private func openSessionOverview(sessionInfo: UserSessionInfo) { + let coordinator = createUserSessionOverviewCoordinator(sessionInfo: sessionInfo) + coordinator.completion = { [weak self] result in + guard let self = self else { return } + switch result { + case let .openSessionDetails(sessionInfo: sessionInfo): + self.openSessionDetails(sessionInfo: sessionInfo) + } + } + pushScreen(with: coordinator) + } + + private func createUserSessionOverviewCoordinator(sessionInfo: UserSessionInfo) -> UserSessionOverviewCoordinator { + let parameters = UserSessionOverviewCoordinatorParameters(session: self.parameters.session, sessionInfo: sessionInfo) + return UserSessionOverviewCoordinator(parameters: parameters) + } + + // MARK: - Public + + func start() { + MXLog.debug("[UserSessionsFlowCoordinator] did start.") + + let rootCoordinator = createUserSessionsOverviewCoordinator() + rootCoordinator.start() + + add(childCoordinator: rootCoordinator) + + if navigationRouter.modules.isEmpty == false { + navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in + self?.remove(childCoordinator: rootCoordinator) + self?.completion?() + }) + } else { + navigationRouter.setRootModule(rootCoordinator) { [weak self] in + self?.remove(childCoordinator: rootCoordinator) + self?.completion?() + } + } + } + + func toPresentable() -> UIViewController { + navigationRouter.toPresentable() + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsFlow/Coordinator/UserSessionsFlowCoordinatorBridgePresenter.swift b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinatorBridgePresenter.swift similarity index 75% rename from RiotSwiftUI/Modules/UserSessions/UserSessionsFlow/Coordinator/UserSessionsFlowCoordinatorBridgePresenter.swift rename to RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinatorBridgePresenter.swift index d2a1a9ff0..a2b256177 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsFlow/Coordinator/UserSessionsFlowCoordinatorBridgePresenter.swift +++ b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinatorBridgePresenter.swift @@ -24,22 +24,11 @@ import UIKit /// Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. @objcMembers final class UserSessionsFlowCoordinatorBridgePresenter: NSObject { - - // MARK: - Constants - - // MARK: - Properties - - // MARK: Private - private let mxSession: MXSession private var coordinator: UserSessionsFlowCoordinator? - - // MARK: Public - + var completion: (() -> Void)? - // MARK: - Setup - init(mxSession: MXSession) { self.mxSession = mxSession super.init() @@ -48,26 +37,22 @@ final class UserSessionsFlowCoordinatorBridgePresenter: NSObject { // MARK: - Public func push(from navigationController: UINavigationController, animated: Bool) { - - self.startUserSessionsFlow(mxSession: self.mxSession, navigationController: navigationController) + startUserSessionsFlow(mxSession: mxSession, navigationController: navigationController) } // MARK: - Private private func startUserSessionsFlow(mxSession: MXSession, navigationController: UINavigationController?) { - var navigationRouter: NavigationRouterType? if let navigationController = navigationController { navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) } - let coordinatorParameters = UserSessionsFlowCoordinatorParameters(session: mxSession, router: navigationRouter) + let parameters = UserSessionsFlowCoordinatorParameters(session: mxSession, router: navigationRouter) + let coordinator = UserSessionsFlowCoordinator(parameters: parameters) - let userSessionsFlowCoordinator = UserSessionsFlowCoordinator(parameters: coordinatorParameters) - - userSessionsFlowCoordinator.completion = { [weak self] in - + coordinator.completion = { [weak self] in guard let self = self else { return } @@ -76,8 +61,8 @@ final class UserSessionsFlowCoordinatorBridgePresenter: NSObject { self.coordinator = nil } - userSessionsFlowCoordinator.start() + coordinator.start() - self.coordinator = userSessionsFlowCoordinator + self.coordinator = coordinator } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Coordinator/UserSessionDetailsCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Coordinator/UserSessionDetailsCoordinator.swift new file mode 100644 index 000000000..bffc0ed56 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Coordinator/UserSessionDetailsCoordinator.swift @@ -0,0 +1,68 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import CommonKit +import SwiftUI + +struct UserSessionDetailsCoordinatorParameters { + let session: UserSessionInfo +} + +final class UserSessionDetailsCoordinator: Coordinator, Presentable { + private let parameters: UserSessionDetailsCoordinatorParameters + private let userSessionDetailsHostingController: UIViewController + private var userSessionDetailsViewModel: UserSessionDetailsViewModelProtocol + + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var loadingIndicator: UserIndicator? + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: ((UserSessionDetailsViewModelResult) -> Void)? + + // MARK: - Setup + + init(parameters: UserSessionDetailsCoordinatorParameters) { + self.parameters = parameters + + let viewModel = UserSessionDetailsViewModel(session: parameters.session) + let view = UserSessionDetails(viewModel: viewModel.context) + userSessionDetailsViewModel = viewModel + userSessionDetailsHostingController = VectorHostingController(rootView: view) + + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: userSessionDetailsHostingController) + } + + // MARK: - Public + + func start() { + MXLog.debug("[UserSessionDetailsCoordinator] did start.") + userSessionDetailsViewModel.completion = { [weak self] result in + guard let self = self else { + return + } + + MXLog.debug("[UserSessionDetailsCoordinator] UserSessionDetailsViewModel did complete with result: \(result).") + self.completion?(result) + } + } + + func toPresentable() -> UIViewController { + userSessionDetailsHostingController + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/MockUserSessionDetailsScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/MockUserSessionDetailsScreenState.swift new file mode 100644 index 000000000..90fa84a36 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/MockUserSessionDetailsScreenState.swift @@ -0,0 +1,87 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// Using an enum for the screen allows you define the different state cases with +/// the relevant associated data for each case. +enum MockUserSessionDetailsScreenState: MockScreenState, CaseIterable { + // A case for each state you want to represent + // with specific, minimal associated data that will allow you + // mock that screen. + case allSections + case sessionSectionOnly + + /// The associated screen + var screenType: Any.Type { + UserSessionDetails.self + } + + /// A list of screen state definitions + static var allCases: [MockUserSessionDetailsScreenState] { + // Each of the presence statuses + [.allSections, .sessionSectionOnly] + } + + /// Generate the view struct for the screen state. + var screenView: ([Any], AnyView) { + let session: UserSessionInfo + switch self { + case .allSections: + session = UserSessionInfo(id: "alice", + name: "iOS", + deviceType: .mobile, + isVerified: false, + lastSeenIP: "10.0.0.10", + lastSeenTimestamp: nil, + applicationName: "Element iOS", + applicationVersion: "1.0.0", + applicationURL: nil, + deviceModel: nil, + deviceOS: "iOS 15.5", + lastSeenIPLocation: nil, + clientName: "Element", + clientVersion: "1.0.0", + isActive: true, + isCurrent: true) + case .sessionSectionOnly: + session = UserSessionInfo(id: "3", + name: "Android", + deviceType: .mobile, + isVerified: false, + lastSeenIP: "3.0.0.3", + lastSeenTimestamp: Date().timeIntervalSince1970 - 10, + applicationName: "Element Android", + applicationVersion: "1.0.0", + applicationURL: nil, + deviceModel: nil, + deviceOS: "Android 4.0", + lastSeenIPLocation: nil, + clientName: "Element", + clientVersion: "1.0.0", + isActive: true, + isCurrent: false) + } + let viewModel = UserSessionDetailsViewModel(session: session) + + // can simulate service and viewModel actions here if needs be. + + return ( + [session], + AnyView(UserSessionDetails(viewModel: viewModel.context)) + ) + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/UI/UserSessionDetailsUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/UI/UserSessionDetailsUITests.swift new file mode 100644 index 000000000..6eb573fe9 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/UI/UserSessionDetailsUITests.swift @@ -0,0 +1,35 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import RiotSwiftUI +import XCTest + +class UserSessionDetailsUITests: MockScreenTestCase { + func test_longPressDetailsCell_CopiesValueToClipboard() throws { + app.goToScreenWithIdentifier(MockUserSessionDetailsScreenState.allSections.title) + + UIPasteboard.general.string = "" + + let tables = app.tables + let sessionNameIosCell = tables.cells["Session name, iOS"] + sessionNameIosCell.press(forDuration: 0.5) + + app.buttons["Copy"].tap() + + let clipboard = try XCTUnwrap(UIPasteboard.general.string) + XCTAssertEqual(clipboard, "iOS") + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/Unit/UserSessionDetailsViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/Unit/UserSessionDetailsViewModelTests.swift new file mode 100644 index 000000000..b07751c10 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/Unit/UserSessionDetailsViewModelTests.swift @@ -0,0 +1,162 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import RiotSwiftUI + +class UserSessionDetailsViewModelTests: XCTestCase { + func test_whenSessionNameAndLastSeenIPNil_viewStateCorrect() { + let userSessionInfo = createUserSessionInfo(id: "session", + name: nil, + lastSeenIP: nil) + + let sessionItems = [ + sessionIdItem(sessionId: "session") + ] + + let sections = [ + UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(), + footer: VectorL10n.userSessionDetailsSessionSectionFooter, + items: sessionItems) + ] + + let expectedModel = UserSessionDetailsViewState(sections: sections) + let sut = UserSessionDetailsViewModel(session: userSessionInfo) + + XCTAssertEqual(sut.state, expectedModel) + } + + func test_whenSessionNameNotNilLastSeenIPNil_viewStateCorrect() { + let userSessionInfo = createUserSessionInfo(id: "session", + name: "session name", + lastSeenIP: nil) + + let sessionItems = [ + sessionNameItem(sessionName: "session name"), + sessionIdItem(sessionId: "session") + ] + + let sections = [ + UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(), + footer: VectorL10n.userSessionDetailsSessionSectionFooter, + items: sessionItems) + ] + + let expectedModel = UserSessionDetailsViewState(sections: sections) + let sut = UserSessionDetailsViewModel(session: userSessionInfo) + + XCTAssertEqual(sut.state, expectedModel) + } + + func test_whenUserSessionInfoContainsAllValues_viewStateCorrect() { + let userSessionInfo = createUserSessionInfo(id: "session", + name: "session name", + lastSeenIP: "0.0.0.0", + applicationName: "Element iOS", + applicationVersion: "1.0.0") + + let sessionItems = [ + sessionNameItem(sessionName: "session name"), + sessionIdItem(sessionId: "session") + ] + let appItems = [ + appNameItem(appName: "Element iOS"), + appVersionItem(appVersion: "1.0.0") + ] + let deviceItems = [ + ipAddressItem(ipAddress: "0.0.0.0") + ] + + let sections = [ + UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(), + footer: VectorL10n.userSessionDetailsSessionSectionFooter, + items: sessionItems), + UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsApplicationSectionHeader.uppercased(), + footer: nil, + items: appItems), + UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(), + footer: nil, + items: deviceItems) + ] + + let expectedModel = UserSessionDetailsViewState(sections: sections) + let sut = UserSessionDetailsViewModel(session: userSessionInfo) + + XCTAssertEqual(sut.state, expectedModel) + } + + // MARK: - Private + + private func createUserSessionInfo(id: String, + name: String?, + deviceType: DeviceType = .mobile, + isVerified: Bool = false, + lastSeenIP: String?, + lastSeenTimestamp: TimeInterval = Date().timeIntervalSince1970, + applicationName: String? = nil, + applicationVersion: String? = nil, + applicationURL: String? = nil, + deviceModel: String? = nil, + deviceOS: String? = nil, + lastSeenIPLocation: String? = nil, + clientName: String? = nil, + clientVersion: String? = nil, + isActive: Bool = true, + isCurrent: Bool = true) -> UserSessionInfo { + UserSessionInfo(id: id, + name: name, + deviceType: deviceType, + isVerified: isVerified, + lastSeenIP: lastSeenIP, + lastSeenTimestamp: lastSeenTimestamp, + applicationName: applicationName, + applicationVersion: applicationVersion, + applicationURL: applicationURL, + deviceModel: deviceModel, + deviceOS: deviceOS, + lastSeenIPLocation: lastSeenIPLocation, + clientName: clientName, + clientVersion: clientVersion, + isActive: isActive, + isCurrent: isCurrent) + } + + private func sessionNameItem(sessionName: String) -> UserSessionDetailsSectionItemViewData { + .init(title: VectorL10n.userSessionDetailsSessionName, + value: sessionName) + } + + private func sessionIdItem(sessionId: String) -> UserSessionDetailsSectionItemViewData { + .init(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle, + value: sessionId) + } + + private func appNameItem(appName: String) -> UserSessionDetailsSectionItemViewData { + .init(title: VectorL10n.userSessionDetailsApplicationName, + value: appName) + } + + private func appVersionItem(appVersion: String) -> UserSessionDetailsSectionItemViewData { + .init(title: VectorL10n.userSessionDetailsApplicationVersion, + value: appVersion) + } + + private func ipAddressItem(ipAddress: String) -> UserSessionDetailsSectionItemViewData { + .init(title: VectorL10n.userSessionDetailsDeviceIpAddress, + value: ipAddress) + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsModels.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsModels.swift new file mode 100644 index 000000000..456cd5e40 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsModels.swift @@ -0,0 +1,59 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// MARK: - Coordinator + +// MARK: View model + +enum UserSessionDetailsViewModelResult { } + +// MARK: View + +enum UserSessionDetailsViewAction { } + +struct UserSessionDetailsViewState: BindableState, Equatable { + let sections: [UserSessionDetailsSectionViewData] +} + +struct UserSessionDetailsSectionViewData: Identifiable { + let id = UUID() + let header: String + let footer: String? + let items: [UserSessionDetailsSectionItemViewData] +} + +struct UserSessionDetailsSectionItemViewData: Identifiable { + let id = UUID() + let title: String + let value: String +} + +extension UserSessionDetailsSectionViewData: Equatable { + static func == (lhs: UserSessionDetailsSectionViewData, rhs: UserSessionDetailsSectionViewData) -> Bool { + lhs.header == rhs.header && + lhs.footer == rhs.footer && + lhs.items == rhs.items + } +} + +extension UserSessionDetailsSectionItemViewData: Equatable { + static func == (lhs: UserSessionDetailsSectionItemViewData, rhs: UserSessionDetailsSectionItemViewData) -> Bool { + lhs.title == rhs.title && + lhs.value == rhs.value + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModel.swift new file mode 100644 index 000000000..f08ecdd8e --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModel.swift @@ -0,0 +1,121 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +typealias UserSessionDetailsViewModelType = StateStoreViewModel + +class UserSessionDetailsViewModel: UserSessionDetailsViewModelType, UserSessionDetailsViewModelProtocol { + var completion: ((UserSessionDetailsViewModelResult) -> Void)? + + init(session: UserSessionInfo) { + super.init(initialViewState: UserSessionDetailsViewState(sections: [])) + updateViewState(session: session) + } + + // MARK: - Public + + // MARK: - Private + + private func updateViewState(session: UserSessionInfo) { + var sections = [UserSessionDetailsSectionViewData]() + + sections.append(sessionSection(session: session)) + + if let applicationSection = applicationSection(session: session) { + sections.append(applicationSection) + } + + if let deviceSection = deviceSection(session: session) { + sections.append(deviceSection) + } + + state = UserSessionDetailsViewState(sections: sections) + } + + private func sessionSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData { + var sessionItems: [UserSessionDetailsSectionItemViewData] = [] + + if let sessionName = session.name { + sessionItems.append(.init(title: VectorL10n.userSessionDetailsSessionName, + value: sessionName)) + } + + sessionItems.append(.init(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle, + value: session.id)) + + return .init(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(), + footer: VectorL10n.userSessionDetailsSessionSectionFooter, + items: sessionItems) + } + + private func applicationSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData? { + var sessionItems: [UserSessionDetailsSectionItemViewData] = [] + + if let name = session.applicationName { + sessionItems.append(.init(title: VectorL10n.userSessionDetailsApplicationName, + value: name)) + } + if let version = session.applicationVersion { + sessionItems.append(.init(title: VectorL10n.userSessionDetailsApplicationVersion, + value: version)) + } + if let url = session.applicationURL { + sessionItems.append(.init(title: VectorL10n.userSessionDetailsApplicationUrl, + value: url)) + } + + guard !sessionItems.isEmpty else { + return nil + } + return .init(header: VectorL10n.userSessionDetailsApplicationSectionHeader.uppercased(), + footer: nil, + items: sessionItems) + } + + private func deviceSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData? { + var deviceSectionItems = [UserSessionDetailsSectionItemViewData]() + + if let model = session.deviceModel { + deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceModel, + value: model)) + } + if session.deviceType == .web, + let clientName = session.clientName, + let clientVersion = session.clientVersion { + deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceBrowser, + value: "\(clientName) \(clientVersion)")) + } + if let deviceOS = session.deviceOS { + deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceOs, + value: deviceOS)) + } + if let lastSeenIP = session.lastSeenIP { + deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceIpAddress, + value: lastSeenIP)) + } + if let lastSeenIPLocation = session.lastSeenIPLocation { + deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceIpLocation, + value: lastSeenIPLocation)) + } + if deviceSectionItems.count > 0 { + return .init(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(), + footer: nil, + items: deviceSectionItems) + } + return nil + } +} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsStateAction.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModelProtocol.swift similarity index 75% rename from RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsStateAction.swift rename to RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModelProtocol.swift index 3515e1250..3fcef2eba 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationRooms/Model/SpaceCreationRoomsStateAction.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModelProtocol.swift @@ -1,6 +1,4 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationRooms SpaceCreationRooms -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,5 +16,7 @@ import Foundation -enum SpaceCreationRoomsStateAction { +protocol UserSessionDetailsViewModelProtocol { + var completion: ((UserSessionDetailsViewModelResult) -> Void)? { get set } + var context: UserSessionDetailsViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/View/UserSessionDetails.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/View/UserSessionDetails.swift new file mode 100644 index 000000000..8801c9ef0 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/View/UserSessionDetails.swift @@ -0,0 +1,69 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct UserSessionDetails: View { + private enum LayoutConstants { + static let listItemHorizontalPadding: CGFloat = 20 + static let sectionVerticalPadding: CGFloat = 8 + } + + @Environment(\.theme) private var theme: ThemeSwiftUI + + @ObservedObject var viewModel: UserSessionDetailsViewModel.Context + + var body: some View { + List { + ForEach(viewModel.viewState.sections) { section in + SwiftUI.Section { + ForEach(section.items) { item in + UserSessionDetailsItem(viewData: item, horizontalPadding: LayoutConstants.listItemHorizontalPadding) + .listRowInsets(EdgeInsets()) + } + } header: { + Text(section.header) + .foregroundColor(theme.colors.secondaryContent) + .font(theme.fonts.footnote) + .padding([.leading, .trailing], LayoutConstants.listItemHorizontalPadding) + .padding(.top, 32) + .padding(.bottom, LayoutConstants.sectionVerticalPadding) + } footer: { + if let footer = section.footer { + Text(footer) + .foregroundColor(theme.colors.secondaryContent) + .font(theme.fonts.footnote) + .padding([.leading, .trailing], LayoutConstants.listItemHorizontalPadding) + .padding(.top, LayoutConstants.sectionVerticalPadding) + } + } + .listRowInsets(EdgeInsets()) + } + } + .listStyle(.grouped) + .navigationBarTitle(VectorL10n.userSessionDetailsTitle) + } +} + +// MARK: - Previews + +struct UserSessionDetails_Previews: PreviewProvider { + static let stateRenderer = MockUserSessionDetailsScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup(addNavigation: true).theme(.light).preferredColorScheme(.light) + stateRenderer.screenGroup(addNavigation: true).theme(.dark).preferredColorScheme(.dark) + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/View/UserSessionDetailsItem.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/View/UserSessionDetailsItem.swift new file mode 100644 index 000000000..80ff02dc5 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/View/UserSessionDetailsItem.swift @@ -0,0 +1,83 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct UserSessionDetailsItem: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + let viewData: UserSessionDetailsSectionItemViewData + let horizontalPadding: CGFloat + + init(viewData: UserSessionDetailsSectionItemViewData, horizontalPadding: CGFloat = 20) { + self.viewData = viewData + self.horizontalPadding = horizontalPadding + } + + var body: some View { + HStack { + Text(viewData.title) + .font(theme.fonts.subheadline) + .foregroundColor(theme.colors.secondaryContent) + .frame(maxWidth: .infinity, alignment: .leading) + .frame(maxHeight: .infinity, alignment: .top) + Text(viewData.value) + .font(theme.fonts.subheadline) + .foregroundColor(theme.colors.primaryContent) + .multilineTextAlignment(.trailing) + } + .contextMenu { + Button { + UIPasteboard.general.string = viewData.value + } label: { + Label(VectorL10n.copyButtonName, systemImage: "doc.on.doc") + } + } + .padding([.leading, .trailing], horizontalPadding) + .padding([.top, .bottom], 12) + } +} + +// MARK: - Previews + +struct UserSessionDetailsItem_Previews: PreviewProvider { + static var previews: some View { + Group { + List { + UserSessionDetailsItem(viewData: UserSessionDetailsSectionItemViewData(title: "Session name", + value: "Element Web: Firefox on macOS")) + .listRowInsets(EdgeInsets()) + UserSessionDetailsItem(viewData: UserSessionDetailsSectionItemViewData(title: "Session ID", + value: "76c95352559d-react-7c57680b93db-js-b64dbdce74b0")) + .listRowInsets(EdgeInsets()) + } + .preferredColorScheme(.light) + + .listStyle(.grouped) + List { + UserSessionDetailsItem(viewData: UserSessionDetailsSectionItemViewData(title: "Session name", + value: "Element Web: Firefox on macOS")) + .listRowInsets(EdgeInsets()) + UserSessionDetailsItem(viewData: UserSessionDetailsSectionItemViewData(title: "Session ID", + value: "76c95352559d-react-7c57680b93db-js-b64dbdce74b0")) + .listRowInsets(EdgeInsets()) + } + .preferredColorScheme(.dark) + .theme(.dark) + .listStyle(.grouped) + } + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Coordinator/UserSessionOverviewCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Coordinator/UserSessionOverviewCoordinator.swift new file mode 100644 index 000000000..cc6507213 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Coordinator/UserSessionOverviewCoordinator.swift @@ -0,0 +1,73 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import CommonKit +import SwiftUI + +struct UserSessionOverviewCoordinatorParameters { + let session: MXSession + let sessionInfo: UserSessionInfo +} + +final class UserSessionOverviewCoordinator: Coordinator, Presentable { + private let parameters: UserSessionOverviewCoordinatorParameters + private let hostingController: UIViewController + private var viewModel: UserSessionOverviewViewModelProtocol + + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var loadingIndicator: UserIndicator? + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: ((UserSessionOverviewCoordinatorResult) -> Void)? + + // MARK: - Setup + + init(parameters: UserSessionOverviewCoordinatorParameters) { + self.parameters = parameters + + let service = UserSessionOverviewService(session: parameters.session, sessionInfo: parameters.sessionInfo) + viewModel = UserSessionOverviewViewModel(sessionInfo: parameters.sessionInfo, service: service) + + hostingController = VectorHostingController(rootView: UserSessionOverview(viewModel: viewModel.context)) + + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingController) + } + + // MARK: - Public + + func start() { + MXLog.debug("[UserSessionOverviewCoordinator] did start.") + viewModel.completion = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[UserSessionOverviewCoordinator] UserSessionOverviewViewModel did complete with result: \(result).") + switch result { + case .verifyCurrentSession: + break // TODO: + case let .showSessionDetails(sessionInfo: sessionInfo): + self.completion?(.openSessionDetails(sessionInfo: sessionInfo)) + } + } + } + + func toPresentable() -> UIViewController { + hostingController + } + + // MARK: - Private +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/MockUserSessionOverviewScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/MockUserSessionOverviewScreenState.swift new file mode 100644 index 000000000..ebf382ad7 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/MockUserSessionOverviewScreenState.swift @@ -0,0 +1,128 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +/// Using an enum for the screen allows you define the different state cases with +/// the relevant associated data for each case. +enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable { + // A case for each state you want to represent + // with specific, minimal associated data that will allow you + // mock that screen. + case currentSession + case otherSession + case sessionWithPushNotifications(enabled: Bool) + case remotelyTogglingPushersNotAvailable + + /// The associated screen + var screenType: Any.Type { + UserSessionOverview.self + } + + /// A list of screen state definitions + static var allCases: [MockUserSessionOverviewScreenState] { + [.currentSession, + .otherSession, + .sessionWithPushNotifications(enabled: true), + .sessionWithPushNotifications(enabled: false), + .remotelyTogglingPushersNotAvailable] + } + + /// Generate the view struct for the screen state. + var screenView: ([Any], AnyView) { + let session: UserSessionInfo + let service: UserSessionOverviewServiceProtocol + switch self { + case .currentSession: + session = UserSessionInfo(id: "alice", + name: "iOS", + deviceType: .mobile, + isVerified: false, + lastSeenIP: "10.0.0.10", + lastSeenTimestamp: nil, + applicationName: "Element iOS", + applicationVersion: "1.0.0", + applicationURL: nil, + deviceModel: nil, + deviceOS: "iOS 15.5", + lastSeenIPLocation: nil, + clientName: "Element", + clientVersion: "1.0.0", + isActive: true, + isCurrent: true) + service = MockUserSessionOverviewService() + case .otherSession: + session = UserSessionInfo(id: "1", + name: "macOS", + deviceType: .desktop, + isVerified: true, + lastSeenIP: "1.0.0.1", + lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000, + applicationName: "Element MacOS", + applicationVersion: "1.0.0", + applicationURL: nil, + deviceModel: nil, + deviceOS: "macOS 12.5.1", + lastSeenIPLocation: nil, + clientName: "Electron", + clientVersion: "20.1.1", + isActive: false, + isCurrent: false) + service = MockUserSessionOverviewService() + case .sessionWithPushNotifications(let enabled): + session = UserSessionInfo(id: "1", + name: "macOS", + deviceType: .desktop, + isVerified: true, + lastSeenIP: "1.0.0.1", + lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000, + applicationName: "Element MacOS", + applicationVersion: "1.0.0", + applicationURL: nil, + deviceModel: nil, + deviceOS: "macOS 12.5.1", + lastSeenIPLocation: nil, + clientName: "My Mac", + clientVersion: "1.0.0", + isActive: false, + isCurrent: false) + service = MockUserSessionOverviewService(pusherEnabled: enabled) + case .remotelyTogglingPushersNotAvailable: + session = UserSessionInfo(id: "1", + name: "macOS", + deviceType: .desktop, + isVerified: true, + lastSeenIP: "1.0.0.1", + lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000, + applicationName: "Element MacOS", + applicationVersion: "1.0.0", + applicationURL: nil, + deviceModel: nil, + deviceOS: "macOS 12.5.1", + lastSeenIPLocation: nil, + clientName: "My Mac", + clientVersion: "1.0.0", + isActive: false, + isCurrent: false) + service = MockUserSessionOverviewService(pusherEnabled: true, remotelyTogglingPushersAvailable: false) + } + + let viewModel = UserSessionOverviewViewModel(sessionInfo: session, service: service) + // can simulate service and viewModel actions here if needs be. + return ([viewModel], AnyView(UserSessionOverview(viewModel: viewModel.context))) + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Service/MatrixSDK/UserSessionOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Service/MatrixSDK/UserSessionOverviewService.swift new file mode 100644 index 000000000..170e2ef2c --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Service/MatrixSDK/UserSessionOverviewService.swift @@ -0,0 +1,115 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import MatrixSDK + +class UserSessionOverviewService: UserSessionOverviewServiceProtocol { + + // MARK: - Members + + private(set) var pusherEnabledSubject: CurrentValueSubject + private(set) var remotelyTogglingPushersAvailableSubject: CurrentValueSubject + + // MARK: - Private + + private let session: MXSession + private let sessionInfo: UserSessionInfo + private var pusher: MXPusher? + + // MARK: - Setup + + init(session: MXSession, sessionInfo: UserSessionInfo) { + self.session = session + self.sessionInfo = sessionInfo + self.pusherEnabledSubject = CurrentValueSubject(nil) + self.remotelyTogglingPushersAvailableSubject = CurrentValueSubject(false) + + checkServerVersions { [weak self] in + self?.checkPusher() + } + } + + // MARK: - UserSessionOverviewServiceProtocol + + func togglePushNotifications() { + guard let pusher = pusher, let enabled = pusher.enabled?.boolValue, self.remotelyTogglingPushersAvailableSubject.value else { + return + } + + let data = pusher.data.jsonDictionary() as? [String: Any] ?? [:] + + self.session.matrixRestClient.setPusher(pushKey: pusher.pushkey, + kind: MXPusherKind(value: pusher.kind), + appId: pusher.appId, + appDisplayName:pusher.appDisplayName, + deviceDisplayName: pusher.deviceDisplayName, + profileTag: pusher.profileTag ?? "", + lang: pusher.lang, + data: data, + append: false, + enabled: !enabled) { [weak self] response in + guard let self = self else { return } + + switch response { + case .success: + self.checkPusher() + case .failure(let error): + MXLog.warning("[UserSessionOverviewService] togglePushNotifications failed due to error: \(error)") + self.pusherEnabledSubject.send(enabled) + } + } + } + + // MARK: - Private + + private func checkServerVersions(_ completion: @escaping () -> Void) { + session.supportedMatrixVersions { [weak self] response in + switch response { + case .success(let versions): + self?.remotelyTogglingPushersAvailableSubject.send(versions.supportsRemotelyTogglingPushNotifications) + case .failure(let error): + MXLog.warning("[UserSessionOverviewService] checkServerVersions failed due to error: \(error)") + } + completion() + } + } + + private func checkPusher() { + session.matrixRestClient.pushers { [weak self] response in + switch response { + case .success(let pushers): + self?.check(pushers: pushers) + case .failure(let error): + MXLog.warning("[UserSessionOverviewService] checkPusher failed due to error: \(error)") + } + } + } + + private func check(pushers: [MXPusher]) { + for pusher in pushers where pusher.deviceId == sessionInfo.id { + self.pusher = pusher + + guard let enabled = pusher.enabled else { + // For backwards compatibility, any pusher without an enabled field should be treated as if enabled is false + pusherEnabledSubject.send(false) + return + } + + pusherEnabledSubject.send(enabled.boolValue) + } + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Service/Mock/MockUserSessionOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Service/Mock/MockUserSessionOverviewService.swift new file mode 100644 index 000000000..ccd6f63dd --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Service/Mock/MockUserSessionOverviewService.swift @@ -0,0 +1,38 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation + +class MockUserSessionOverviewService: UserSessionOverviewServiceProtocol { + + + var pusherEnabledSubject: CurrentValueSubject + var remotelyTogglingPushersAvailableSubject: CurrentValueSubject + + init(pusherEnabled: Bool? = nil, remotelyTogglingPushersAvailable: Bool = true) { + self.pusherEnabledSubject = CurrentValueSubject(pusherEnabled) + self.remotelyTogglingPushersAvailableSubject = CurrentValueSubject(remotelyTogglingPushersAvailable) + } + + func togglePushNotifications() { + guard let enabled = pusherEnabledSubject.value, self.remotelyTogglingPushersAvailableSubject.value else { + return + } + + pusherEnabledSubject.send(!enabled) + } +} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesStateAction.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Service/UserSessionOverviewServiceProtocol.swift similarity index 65% rename from RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesStateAction.swift rename to RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Service/UserSessionOverviewServiceProtocol.swift index ead48ebf2..807f50c17 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationEmailInvites/Model/SpaceCreationEmailInvitesStateAction.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Service/UserSessionOverviewServiceProtocol.swift @@ -1,7 +1,5 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites -// -// Copyright 2021 New Vector Ltd +// +// 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. @@ -16,9 +14,12 @@ // limitations under the License. // +import Combine import Foundation -enum SpaceCreationEmailInvitesStateAction { - case updateEmailValidity(_ validity: [Bool]) - case updateLoading(_ loading: Bool) +protocol UserSessionOverviewServiceProtocol { + var pusherEnabledSubject: CurrentValueSubject { get } + var remotelyTogglingPushersAvailableSubject: CurrentValueSubject { get } + + func togglePushNotifications() } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift new file mode 100644 index 000000000..7dfb4d04f --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift @@ -0,0 +1,64 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import RiotSwiftUI +import XCTest + +class UserSessionOverviewUITests: MockScreenTestCase { + func test_whenCurrentSessionSelected_correctNavTittleDisplayed() { + app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.currentSession.title) + let navTitle = VectorL10n.userSessionOverviewCurrentSessionTitle + XCTAssertTrue(app.navigationBars[navTitle].staticTexts[navTitle].exists) + } + + func test_whenOtherSessionSelected_correctNavTittleDisplayed() { + app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.otherSession.title) + let navTitle = VectorL10n.userSessionOverviewSessionTitle + XCTAssertTrue(app.navigationBars[navTitle].staticTexts[navTitle].exists) + } + + func test_whenSessionOverviewPresented_sessionDetailsButtonExists() { + app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.currentSession.title) + XCTAssertTrue(app.buttons[VectorL10n.userSessionOverviewSessionDetailsButtonTitle].exists) + } + + func test_whenSessionOverviewPresented_pusherEnabledToggleExists() { + app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.sessionWithPushNotifications(enabled: true).title) + XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].exists) + XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].isOn) + XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].isEnabled) + XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotifications].exists) + XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotificationsMessage].exists) + } + + func test_whenSessionOverviewPresented_pusherDisabledToggleExists() { + app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.sessionWithPushNotifications(enabled: false).title) + XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].exists) + XCTAssertFalse(app.switches["UserSessionOverviewToggleCell"].isOn) + XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].isEnabled) + XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotifications].exists) + XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotificationsMessage].exists) + } + + func test_whenSessionOverviewPresented_pusherEnabledToggleExists_remotelyTogglingPushersAvailable() { + app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.remotelyTogglingPushersNotAvailable.title) + XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].exists) + XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].isOn) + XCTAssertFalse(app.switches["UserSessionOverviewToggleCell"].isEnabled) + XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotifications].exists) + XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotificationsMessage].exists) + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift new file mode 100644 index 000000000..a2d0e7807 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift @@ -0,0 +1,107 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import XCTest + +@testable import RiotSwiftUI + +class UserSessionOverviewViewModelTests: XCTestCase { + var sut: UserSessionOverviewViewModel! + + func test_whenVerifyCurrentSessionProcessed_completionWithVerifyCurrentSessionCalled() { + sut = UserSessionOverviewViewModel(sessionInfo: createUserSessionInfo(), service: MockUserSessionOverviewService()) + + XCTAssertEqual(sut.state.isPusherEnabled, nil) + var modelResult: UserSessionOverviewViewModelResult? + sut.completion = { result in + modelResult = result + } + sut.process(viewAction: .verifyCurrentSession) + XCTAssertEqual(modelResult, .verifyCurrentSession) + } + + func test_whenViewSessionDetailsProcessed_completionWithShowSessionDetailsCalled() { + let sessionInfo = createUserSessionInfo() + sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: MockUserSessionOverviewService()) + + XCTAssertEqual(sut.state.isPusherEnabled, nil) + var modelResult: UserSessionOverviewViewModelResult? + sut.completion = { result in + modelResult = result + } + sut.process(viewAction: .viewSessionDetails) + XCTAssertEqual(modelResult, .showSessionDetails(sessionInfo: sessionInfo)) + } + + func test_whenViewSessionDetailsProcessed_toggleAvailablePusher() { + let sessionInfo = createUserSessionInfo() + let service = MockUserSessionOverviewService(pusherEnabled: true) + sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: service) + + XCTAssertTrue(sut.state.remotelyTogglingPushersAvailable) + XCTAssertEqual(sut.state.isPusherEnabled, true) + sut.process(viewAction: .togglePushNotifications) + XCTAssertEqual(sut.state.isPusherEnabled, false) + sut.process(viewAction: .togglePushNotifications) + XCTAssertEqual(sut.state.isPusherEnabled, true) + } + + func test_whenViewSessionDetailsProcessed_toggleNoPusher() { + let sessionInfo = createUserSessionInfo() + let service = MockUserSessionOverviewService(pusherEnabled: nil) + sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: service) + + XCTAssertTrue(sut.state.remotelyTogglingPushersAvailable) + XCTAssertEqual(sut.state.isPusherEnabled, nil) + sut.process(viewAction: .togglePushNotifications) + XCTAssertEqual(sut.state.isPusherEnabled, nil) + sut.process(viewAction: .togglePushNotifications) + XCTAssertEqual(sut.state.isPusherEnabled, nil) + } + + func test_whenViewSessionDetailsProcessed_remotelyTogglingPushersNotAvailable() { + let sessionInfo = createUserSessionInfo() + let service = MockUserSessionOverviewService(pusherEnabled: true, remotelyTogglingPushersAvailable: false) + sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: service) + + XCTAssertFalse(sut.state.remotelyTogglingPushersAvailable) + XCTAssertEqual(sut.state.isPusherEnabled, true) + sut.process(viewAction: .togglePushNotifications) + XCTAssertEqual(sut.state.isPusherEnabled, true) + sut.process(viewAction: .togglePushNotifications) + XCTAssertEqual(sut.state.isPusherEnabled, true) + } + + private func createUserSessionInfo() -> UserSessionInfo { + UserSessionInfo(id: "session", + name: "iOS", + deviceType: .mobile, + isVerified: false, + lastSeenIP: "10.0.0.10", + lastSeenTimestamp: Date().timeIntervalSince1970 - 100, + applicationName: "Element iOS", + applicationVersion: "1.9.7", + applicationURL: nil, + deviceModel: "iPhone XS", + deviceOS: "iOS 15.5", + lastSeenIPLocation: nil, + clientName: "Element", + clientVersion: "1.9.7", + isActive: true, + isCurrent: true) + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewModels.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewModels.swift new file mode 100644 index 000000000..366da4f71 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewModels.swift @@ -0,0 +1,46 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// MARK: - Coordinator + +enum UserSessionOverviewCoordinatorResult { + case openSessionDetails(sessionInfo: UserSessionInfo) +} + +// MARK: View model + +enum UserSessionOverviewViewModelResult: Equatable { + case showSessionDetails(sessionInfo: UserSessionInfo) + case verifyCurrentSession +} + +// MARK: View + +struct UserSessionOverviewViewState: BindableState { + let cardViewData: UserSessionCardViewData + let isCurrentSession: Bool + var isPusherEnabled: Bool? + var remotelyTogglingPushersAvailable: Bool + var showLoadingIndicator: Bool +} + +enum UserSessionOverviewViewAction { + case verifyCurrentSession + case viewSessionDetails + case togglePushNotifications +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewViewModel.swift new file mode 100644 index 000000000..a5c8f7d99 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewViewModel.swift @@ -0,0 +1,74 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +typealias UserSessionOverviewViewModelType = StateStoreViewModel + +class UserSessionOverviewViewModel: UserSessionOverviewViewModelType, UserSessionOverviewViewModelProtocol { + private let sessionInfo: UserSessionInfo + private let service: UserSessionOverviewServiceProtocol + + var completion: ((UserSessionOverviewViewModelResult) -> Void)? + + // MARK: - Setup + + init(sessionInfo: UserSessionInfo, service: UserSessionOverviewServiceProtocol) { + self.sessionInfo = sessionInfo + self.service = service + + let cardViewData = UserSessionCardViewData(sessionInfo: sessionInfo) + let state = UserSessionOverviewViewState(cardViewData: cardViewData, + isCurrentSession: sessionInfo.isCurrent, + isPusherEnabled: service.pusherEnabledSubject.value, + remotelyTogglingPushersAvailable: service.remotelyTogglingPushersAvailableSubject.value, + showLoadingIndicator: false) + super.init(initialViewState: state) + + startObservingService() + } + + private func startObservingService() { + service + .pusherEnabledSubject + .sink(receiveValue: { [weak self] pushEnabled in + self?.state.showLoadingIndicator = false + self?.state.isPusherEnabled = pushEnabled + }) + .store(in: &cancellables) + + service + .remotelyTogglingPushersAvailableSubject + .sink(receiveValue: { [weak self] remotelyTogglingPushersAvailable in + self?.state.remotelyTogglingPushersAvailable = remotelyTogglingPushersAvailable + }) + .store(in: &cancellables) + } + + // MARK: - Public + + override func process(viewAction: UserSessionOverviewViewAction) { + switch viewAction { + case .verifyCurrentSession: + completion?(.verifyCurrentSession) + case .viewSessionDetails: + completion?(.showSessionDetails(sessionInfo: sessionInfo)) + case .togglePushNotifications: + self.state.showLoadingIndicator = true + service.togglePushNotifications() + } + } +} diff --git a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuStateAction.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewViewModelProtocol.swift similarity index 71% rename from RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuStateAction.swift rename to RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewViewModelProtocol.swift index 3109b348b..66eed3ba8 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceCreation/SpaceCreationMenu/Model/SpaceCreationMenuStateAction.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewViewModelProtocol.swift @@ -1,6 +1,4 @@ -// File created from TemplateAdvancedRoomsExample -// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +16,7 @@ import Foundation -/// Actions to be performed on the `ViewModel` State -enum SpaceCreationMenuStateAction { +protocol UserSessionOverviewViewModelProtocol { + var completion: ((UserSessionOverviewViewModelResult) -> Void)? { get set } + var context: UserSessionOverviewViewModelType.Context { get } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverview.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverview.swift new file mode 100644 index 000000000..b2170f0d2 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverview.swift @@ -0,0 +1,63 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct UserSessionOverview: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + @ObservedObject var viewModel: UserSessionOverviewViewModel.Context + + var body: some View { + ScrollView { + UserSessionCardView(viewData: viewModel.viewState.cardViewData, onVerifyAction: { _ in + viewModel.send(viewAction: .verifyCurrentSession) + }, + onViewDetailsAction: { _ in + viewModel.send(viewAction: .viewSessionDetails) + }) + .padding(16) + SwiftUI.Section { + UserSessionOverviewDisclosureCell(title: VectorL10n.userSessionOverviewSessionDetailsButtonTitle, onBackgroundTap: { + viewModel.send(viewAction: .viewSessionDetails) + }) + if let enabled = viewModel.viewState.isPusherEnabled { + UserSessionOverviewToggleCell(title: VectorL10n.userSessionPushNotifications, + message: VectorL10n.userSessionPushNotificationsMessage, + isOn: enabled, isEnabled: viewModel.viewState.remotelyTogglingPushersAvailable) { + viewModel.send(viewAction: .togglePushNotifications) + } + } + } + } + .background(theme.colors.system.ignoresSafeArea()) + .frame(maxHeight: .infinity) + .waitOverlay(show: viewModel.viewState.showLoadingIndicator, allowUserInteraction: false) + .navigationTitle(viewModel.viewState.isCurrentSession ? + VectorL10n.userSessionOverviewCurrentSessionTitle : + VectorL10n.userSessionOverviewSessionTitle) + } +} + +// MARK: - Previews + +struct UserSessionOverview_Previews: PreviewProvider { + static let stateRenderer = MockUserSessionOverviewScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup(addNavigation: true).theme(.light).preferredColorScheme(.light) + stateRenderer.screenGroup(addNavigation: true).theme(.dark).preferredColorScheme(.dark) + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverviewDisclosureCell.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverviewDisclosureCell.swift new file mode 100644 index 000000000..6fa8b00ca --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverviewDisclosureCell.swift @@ -0,0 +1,56 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct UserSessionOverviewDisclosureCell: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + let title: String + var onBackgroundTap: (() -> Void)? + + var body: some View { + Button(action: { onBackgroundTap?() }) { + VStack(spacing: 0) { + SeparatorLine() + HStack { + Text(title) + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + .frame(maxWidth: .infinity, alignment: .leading) + Image(Asset.Images.chevron.name) + } + .padding(.vertical, 15) + .padding(.horizontal, 16) + SeparatorLine() + } + .background(theme.colors.background) + } + } +} + +struct UserSessionOverviewDisclosureCell_Previews: PreviewProvider { + static var previews: some View { + Group { + UserSessionOverviewDisclosureCell(title: "Title") + .theme(.light) + .preferredColorScheme(.light) + UserSessionOverviewDisclosureCell(title: "Title") + .theme(.dark) + .preferredColorScheme(.dark) + } + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverviewToggleCell.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverviewToggleCell.swift new file mode 100644 index 000000000..b6f79b58f --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverviewToggleCell.swift @@ -0,0 +1,86 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct UserSessionOverviewToggleCell: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + let title: String + let message: String? + let isOn: Bool + let isEnabled: Bool + var onBackgroundTap: (() -> Void)? + + var body: some View { + VStack(spacing: 8) { + Button(action: { + guard isEnabled else { return } + onBackgroundTap?() + }) { + VStack(spacing: 0) { + SeparatorLine() + Toggle(isOn: .constant(isOn)) { + Text(title) + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + .opacity(isEnabled ? 1 : 0.3) + } + .disabled(!isEnabled) + .allowsHitTesting(false) + .padding(.vertical, 10) + .padding(.horizontal, 16) + .accessibilityIdentifier("UserSessionOverviewToggleCell") + SeparatorLine() + } + .background(theme.colors.background) + } + .disabled(!isEnabled) + if let message = message { + Text(message) + .multilineTextAlignment(.leading) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16) + } + } + } +} + +struct UserSessionOverviewToggleCell_Previews: PreviewProvider { + static var previews: some View { + Group { + preview + .theme(.light) + .preferredColorScheme(.light) + preview + .theme(.dark) + .preferredColorScheme(.dark) + } + } + + static var preview: some View { + VStack { + UserSessionOverviewToggleCell(title: "Title", message: nil, isOn: true, isEnabled: true) + UserSessionOverviewToggleCell(title: "Title", message: nil, isOn: false, isEnabled: true) + UserSessionOverviewToggleCell(title: "Title", message: "some very long message text in order to test the multine alignment", isOn: true, isEnabled: true) + UserSessionOverviewToggleCell(title: "Title", message: nil, isOn: true, isEnabled: false) + UserSessionOverviewToggleCell(title: "Title", message: nil, isOn: false, isEnabled: false) + UserSessionOverviewToggleCell(title: "Title", message: "some very long message text in order to test the multine alignment", isOn: true, isEnabled: false) + } + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsFlow/Coordinator/UserSessionsFlowCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsFlow/Coordinator/UserSessionsFlowCoordinator.swift deleted file mode 100644 index 42151e7cf..000000000 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsFlow/Coordinator/UserSessionsFlowCoordinator.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// Copyright 2022 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import CommonKit - -struct UserSessionsFlowCoordinatorParameters { - let session: MXSession - let router: NavigationRouterType? -} - -final class UserSessionsFlowCoordinator: Coordinator, Presentable { - - // MARK: - Properties - - // MARK: Private - - private let parameters: UserSessionsFlowCoordinatorParameters - private let navigationRouter: NavigationRouterType - - // MARK: Public - - // Must be used only internally - var childCoordinators: [Coordinator] = [] - var completion: (() -> Void)? - - // MARK: - Setup - - init(parameters: UserSessionsFlowCoordinatorParameters) { - self.parameters = parameters - - self.navigationRouter = parameters.router ?? NavigationRouter(navigationController: RiotNavigationController()) - } - - // MARK: - Public - - func start() { - MXLog.debug("[UserSessionsFlowCoordinator] did start.") - - let rootCoordinatorParameters = UserSessionsOverviewCoordinatorParameters(session: self.parameters.session) - - let rootCoordinator = UserSessionsOverviewCoordinator(parameters: rootCoordinatorParameters) - - rootCoordinator.start() - - self.add(childCoordinator: rootCoordinator) - - if self.navigationRouter.modules.isEmpty == false { - self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in - self?.remove(childCoordinator: rootCoordinator) - self?.completion?() - }) - } else { - self.navigationRouter.setRootModule(rootCoordinator) { [weak self] in - self?.remove(childCoordinator: rootCoordinator) - self?.completion?() - } - } - } - - func toPresentable() -> UIViewController { - return self.navigationRouter.toPresentable() - } -} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift index 1494eb7c2..1a72a45c6 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift @@ -14,44 +14,33 @@ // limitations under the License. // -import SwiftUI import CommonKit +import SwiftUI struct UserSessionsOverviewCoordinatorParameters { let session: MXSession } final class UserSessionsOverviewCoordinator: Coordinator, Presentable { - - // MARK: - Properties - - // MARK: Private - private let parameters: UserSessionsOverviewCoordinatorParameters - private let userSessionsOverviewHostingController: UIViewController - private var userSessionsOverviewViewModel: UserSessionsOverviewViewModelProtocol - + private let hostingViewController: UIViewController + private var viewModel: UserSessionsOverviewViewModelProtocol + private let service: UserSessionsOverviewService + private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var loadingIndicator: UserIndicator? - // MARK: Public - // Must be used only internally var childCoordinators: [Coordinator] = [] - var completion: (() -> Void)? - - // MARK: - Setup - + var completion: ((UserSessionsOverviewCoordinatorResult) -> Void)? + init(parameters: UserSessionsOverviewCoordinatorParameters) { self.parameters = parameters - let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: UserSessionsOverviewService(mxSession: parameters.session)) - let view = UserSessionsOverview(viewModel: viewModel.context) - userSessionsOverviewViewModel = viewModel - - let hostingViewController = VectorHostingController(rootView: view) - - userSessionsOverviewHostingController = hostingViewController + let dataProvider = UserSessionsDataProvider(session: parameters.session) + service = UserSessionsOverviewService(dataProvider: dataProvider) + viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service) + hostingViewController = VectorHostingController(rootView: UserSessionsOverview(viewModel: viewModel.context)) indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingViewController) } @@ -59,30 +48,29 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable { func start() { MXLog.debug("[UserSessionsOverviewCoordinator] did start.") - userSessionsOverviewViewModel.completion = { [weak self] result in + viewModel.completion = { [weak self] result in guard let self = self else { return } MXLog.debug("[UserSessionsOverviewCoordinator] UserSessionsOverviewViewModel did complete with result: \(result).") + switch result { - case .cancel: - self.completion?() case .showAllUnverifiedSessions: self.showAllUnverifiedSessions() case .showAllInactiveSessions: self.showAllInactiveSessions() case .verifyCurrentSession: self.startVerifyCurrentSession() - case .showCurrentSessionDetails: - self.showCurrentSessionDetails() + case let .showCurrentSessionOverview(sessionInfo): + self.showCurrentSessionOverview(sessionInfo: sessionInfo) case .showAllOtherSessions: self.showAllOtherSessions() - case .showUserSessionDetails(let sessionId): - self.showUserSessionDetails(sessionId: sessionId) + case let .showUserSessionOverview(sessionInfo): + self.showUserSessionOverview(sessionInfo: sessionInfo) } } } func toPresentable() -> UIViewController { - return self.userSessionsOverviewHostingController + hostingViewController } // MARK: - Private @@ -101,26 +89,26 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable { } private func showAllUnverifiedSessions() { - // TODO + // TODO: } private func showAllInactiveSessions() { - // TODO + // TODO: } private func startVerifyCurrentSession() { - // TODO + // TODO: } - private func showCurrentSessionDetails() { - // TODO + private func showCurrentSessionOverview(sessionInfo: UserSessionInfo) { + completion?(.openSessionOverview(sessionInfo: sessionInfo)) } - - private func showUserSessionDetails(sessionId: String) { - // TODO + + private func showUserSessionOverview(sessionInfo: UserSessionInfo) { + completion?(.openSessionOverview(sessionInfo: sessionInfo)) } private func showAllOtherSessions() { - // TODO + // TODO: } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift index a0b67dafc..626c8c07a 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/MockUserSessionsOverviewScreenState.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,34 +20,39 @@ import SwiftUI /// Using an enum for the screen allows you define the different state cases with /// the relevant associated data for each case. enum MockUserSessionsOverviewScreenState: MockScreenState, CaseIterable { - // A case for each state you want to represent - // with specific, minimal associated data that will allow you - // mock that screen. - case verifiedSession + case currentSessionUnverified + case currentSessionVerified + case onlyUnverifiedSessions + case onlyInactiveSessions + case noOtherSessions /// The associated screen var screenType: Any.Type { UserSessionsOverview.self } - /// A list of screen state definitions - static var allCases: [MockUserSessionsOverviewScreenState] { - // Each of the presence statuses - return [.verifiedSession] - } - /// Generate the view struct for the screen state. - var screenView: ([Any], AnyView) { - let service: MockUserSessionsOverviewService = MockUserSessionsOverviewService() + var screenView: ([Any], AnyView) { + var service: UserSessionsOverviewServiceProtocol? switch self { - case .verifiedSession: - break + case .currentSessionUnverified: + service = MockUserSessionsOverviewService(mode: .currentSessionUnverified) + case .currentSessionVerified: + service = MockUserSessionsOverviewService(mode: .currentSessionVerified) + case .onlyUnverifiedSessions: + service = MockUserSessionsOverviewService(mode: .onlyUnverifiedSessions) + case .onlyInactiveSessions: + service = MockUserSessionsOverviewService(mode: .onlyInactiveSessions) + case .noOtherSessions: + service = MockUserSessionsOverviewService(mode: .noOtherSessions) + } + + guard let service = service else { + fatalError() } let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service) - // can simulate service and viewModel actions here if needs be. - return ( [service, viewModel], AnyView(UserSessionsOverview(viewModel: viewModel.context) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProvider.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProvider.swift new file mode 100644 index 000000000..a8d36cc4e --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProvider.swift @@ -0,0 +1,50 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import MatrixSDK + +class UserSessionsDataProvider: UserSessionsDataProviderProtocol { + private let session: MXSession + + init(session: MXSession) { + self.session = session + } + + var myDeviceId: String { + session.myDeviceId + } + + var myUserId: String? { + session.myUserId + } + + var activeAccounts: [MXKAccount] { + MXKAccountManager.shared().activeAccounts + } + + func devices(completion: @escaping (MXResponse<[MXDevice]>) -> Void) { + session.matrixRestClient.devices(completion: completion) + } + + func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo? { + session.crypto.device(withDeviceId: deviceId, ofUser: userId) + } + + func accountData(for eventType: String) -> [AnyHashable: Any]? { + session.accountData.accountData(forEventType: eventType) + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProviderProtocol.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProviderProtocol.swift new file mode 100644 index 000000000..e97310a40 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProviderProtocol.swift @@ -0,0 +1,32 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import MatrixSDK + +protocol UserSessionsDataProviderProtocol { + var myDeviceId: String { get } + + var myUserId: String? { get } + + var activeAccounts: [MXKAccount] { get } + + func devices(completion: @escaping (MXResponse<[MXDevice]>) -> Void) + + func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo? + + func accountData(for eventType: String) -> [AnyHashable: Any]? +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift index 59ccf1e02..273072ea7 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,120 +18,134 @@ import Foundation import MatrixSDK class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol { + /// Delay after which session is considered inactive, 90 days + private static let inactiveSessionDurationTreshold: TimeInterval = 90 * 86400 - // MARK: - Constants + private let dataProvider: UserSessionsDataProviderProtocol - // MARK: - Properties + private(set) var overviewData: UserSessionsOverviewData - // MARK: Private - - private let mxSession: MXSession - - // MARK: Public + init(dataProvider: UserSessionsDataProviderProtocol) { + self.dataProvider = dataProvider - private(set) var lastOverviewData: UserSessionsOverviewData - - // MARK: - Setup - - init(mxSession: MXSession) { - self.mxSession = mxSession + overviewData = UserSessionsOverviewData(currentSession: nil, + unverifiedSessions: [], + inactiveSessions: [], + otherSessions: []) - self.lastOverviewData = UserSessionsOverviewData(currentSessionInfo: nil, unverifiedSessionsInfo: [], inactiveSessionsInfo: [], otherSessionsInfo: []) - - self.setupInitialOverviewData() + setupInitialOverviewData() } // MARK: - Public - func fetchUserSessionsOverviewData(completion: @escaping (Result) -> Void) { - self.mxSession.matrixRestClient.devices { response in + func updateOverviewData(completion: @escaping (Result) -> Void) { + dataProvider.devices { response in switch response { case .success(let devices): - let overviewData = self.userSessionsOverviewData(from: devices) - completion(.success(overviewData)) + self.overviewData = self.sessionsOverviewData(from: devices) + completion(.success(self.overviewData)) case .failure(let error): completion(.failure(error)) } } } + func sessionForIdentifier(_ sessionId: String) -> UserSessionInfo? { + if overviewData.currentSession?.id == sessionId { + return overviewData.currentSession + } + + return overviewData.otherSessions.first(where: { $0.id == sessionId }) + } + // MARK: - Private private func setupInitialOverviewData() { - let currentSessionInfo = self.getCurrentUserSessionInfoFromCache() + guard let currentSessionInfo = getCurrentSessionInfo() else { + return + } - self.lastOverviewData = UserSessionsOverviewData(currentSessionInfo: currentSessionInfo, unverifiedSessionsInfo: [], inactiveSessionsInfo: [], otherSessionsInfo: []) + overviewData = UserSessionsOverviewData(currentSession: currentSessionInfo, + unverifiedSessions: currentSessionInfo.isVerified ? [] : [currentSessionInfo], + inactiveSessions: currentSessionInfo.isActive ? [] : [currentSessionInfo], + otherSessions: []) } - private func getCurrentUserSessionInfoFromCache() -> UserSessionInfo? { - guard let mainAccount = MXKAccountManager.shared().activeAccounts.first, let device = mainAccount.device else { + private func getCurrentSessionInfo() -> UserSessionInfo? { + guard let mainAccount = dataProvider.activeAccounts.first, + let device = mainAccount.device else { return nil } - return self.userSessionInfo(from: device) + return sessionInfo(from: device, isCurrentSession: true) + } + + private func sessionsOverviewData(from devices: [MXDevice]) -> UserSessionsOverviewData { + let allSessions = devices + .sorted { $0.lastSeenTs > $1.lastSeenTs } + .map { sessionInfo(from: $0, isCurrentSession: $0.deviceId == dataProvider.myDeviceId) } + + return UserSessionsOverviewData(currentSession: allSessions.filter(\.isCurrent).first, + unverifiedSessions: allSessions.filter { !$0.isVerified }, + inactiveSessions: allSessions.filter { !$0.isActive }, + otherSessions: allSessions.filter { !$0.isCurrent }) } - private func userSessionInfo(from device: MXDevice) -> UserSessionInfo { - - let deviceInfo = self.getDeviceInfo(for: device.deviceId) - - let isSessionVerified = deviceInfo?.trustLevel.isVerified ?? false - - var lastSeenTs: TimeInterval? - + private func sessionInfo(from device: MXDevice, isCurrentSession: Bool) -> UserSessionInfo { + let isSessionVerified = deviceInfo(for: device.deviceId)?.trustLevel.isVerified ?? false + + let eventType = kMXAccountDataTypeClientInformation + "." + device.deviceId + let appData = dataProvider.accountData(for: eventType) + var userAgent: UserAgent? + var isSessionActive = true + + if let lastSeenUserAgent = device.lastSeenUserAgent { + userAgent = UserAgentParser.parse(lastSeenUserAgent) + } + if device.lastSeenTs > 0 { - lastSeenTs = TimeInterval(device.lastSeenTs / 1000) + let elapsedTime = Date().timeIntervalSince1970 - TimeInterval(device.lastSeenTs / 1000) + isSessionActive = elapsedTime < Self.inactiveSessionDurationTreshold } - - return UserSessionInfo(sessionId: device.deviceId, - sessionName: device.displayName, - deviceType: .unknown, - isVerified: isSessionVerified, - lastSeenIP: device.lastSeenIp, - lastSeenTimestamp: lastSeenTs) + + return UserSessionInfo(withDevice: device, + applicationData: appData as? [String: String], + userAgent: userAgent, + isSessionVerified: isSessionVerified, + isActive: isSessionActive, + isCurrent: isCurrentSession) } - private func getDeviceInfo(for deviceId: String) -> MXDeviceInfo? { - guard let userId = self.mxSession.myUserId else { + private func deviceInfo(for deviceId: String) -> MXDeviceInfo? { + guard let userId = dataProvider.myUserId else { return nil } - return self.mxSession.crypto.device(withDeviceId: deviceId, ofUser: userId) - } - - private func userSessionsOverviewData(from devices: [MXDevice]) -> UserSessionsOverviewData { - let sortedDevices = devices.sorted { device1, device2 in - device1.lastSeenTs > device2.lastSeenTs - } - - let allUserSessionInfo = sortedDevices.map { device in - return self.userSessionInfo(from: device) - } - - var currentSessionInfo: UserSessionInfo? - - var unverifiedSessionsInfo: [UserSessionInfo] = [] - var inactiveSessionsInfo: [UserSessionInfo] = [] - var otherSessionsInfo: [UserSessionInfo] = [] - - for userSessionInfo in allUserSessionInfo { - if userSessionInfo.sessionId == self.mxSession.myDeviceId { - currentSessionInfo = userSessionInfo - } else { - otherSessionsInfo.append(userSessionInfo) - - if userSessionInfo.isVerified == false { - unverifiedSessionsInfo.append(userSessionInfo) - } - - if userSessionInfo.isSessionActive == false { - inactiveSessionsInfo.append(userSessionInfo) - } - } - } - - return UserSessionsOverviewData(currentSessionInfo: currentSessionInfo, - unverifiedSessionsInfo: unverifiedSessionsInfo, - inactiveSessionsInfo: inactiveSessionsInfo, - otherSessionsInfo: otherSessionsInfo) + return dataProvider.device(withDeviceId: deviceId, ofUser: userId) + } +} + +extension UserSessionInfo { + init(withDevice device: MXDevice, + applicationData: [String: String]?, + userAgent: UserAgent?, + isSessionVerified: Bool, + isActive: Bool, + isCurrent: Bool) { + self.init(id: device.deviceId, + name: device.displayName, + deviceType: userAgent?.deviceType ?? .unknown, + isVerified: isSessionVerified, + lastSeenIP: device.lastSeenIp, + lastSeenTimestamp: device.lastSeenTs > 0 ? TimeInterval(device.lastSeenTs / 1000) : nil, + applicationName: applicationData?["name"], + applicationVersion: applicationData?["version"], + applicationURL: applicationData?["url"], + deviceModel: userAgent?.deviceModel, + deviceOS: userAgent?.deviceOS, + lastSeenIPLocation: nil, + clientName: userAgent?.clientName, + clientVersion: userAgent?.clientVersion, + isActive: isActive, + isCurrent: isCurrent) } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift index f0aa43861..a37607786 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,26 +17,132 @@ import Foundation class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol { - - var lastOverviewData: UserSessionsOverviewData - - func fetchUserSessionsOverviewData(completion: @escaping (Result) -> Void) { - completion(.success(self.lastOverviewData)) + enum Mode { + case currentSessionUnverified + case currentSessionVerified + case onlyUnverifiedSessions + case onlyInactiveSessions + case noOtherSessions } - init() { - let currentSessionInfo = UserSessionInfo(sessionId: "alice", sessionName: "iOS", deviceType: .mobile, isVerified: false, lastSeenIP: "10.0.0.10", lastSeenTimestamp: nil) + private let mode: Mode + + var overviewData: UserSessionsOverviewData + + init(mode: Mode = .currentSessionUnverified) { + self.mode = mode - let unverifiedSessionsInfo: [UserSessionInfo] = [] + overviewData = UserSessionsOverviewData(currentSession: nil, + unverifiedSessions: [], + inactiveSessions: [], + otherSessions: []) + } + + func updateOverviewData(completion: @escaping (Result) -> Void) { + let unverifiedSessions = buildSessions(verified: false, active: true) + let inactiveSessions = buildSessions(verified: true, active: false) - let inactiveSessionsInfo: [UserSessionInfo] = [] + switch mode { + case .noOtherSessions: + overviewData = UserSessionsOverviewData(currentSession: currentSession, + unverifiedSessions: [], + inactiveSessions: [], + otherSessions: []) + case .onlyUnverifiedSessions: + overviewData = UserSessionsOverviewData(currentSession: currentSession, + unverifiedSessions: unverifiedSessions + [currentSession], + inactiveSessions: [], + otherSessions: unverifiedSessions) + case .onlyInactiveSessions: + overviewData = UserSessionsOverviewData(currentSession: currentSession, + unverifiedSessions: [], + inactiveSessions: inactiveSessions, + otherSessions: inactiveSessions) + default: + let otherSessions = unverifiedSessions + inactiveSessions + buildSessions(verified: true, active: true) + + overviewData = UserSessionsOverviewData(currentSession: currentSession, + unverifiedSessions: unverifiedSessions, + inactiveSessions: inactiveSessions, + otherSessions: otherSessions) + } - let otherSessionsInfo: [UserSessionInfo] = [ - UserSessionInfo(sessionId: "1", sessionName: "macOS", deviceType: .desktop, isVerified: true, lastSeenIP: "1.0.0.1", lastSeenTimestamp: (Date().timeIntervalSince1970 - 130000)), - UserSessionInfo(sessionId: "2", sessionName: "Firefox on Windows", deviceType: .web, isVerified: true, lastSeenIP: "2.0.0.2", lastSeenTimestamp: (Date().timeIntervalSince1970 - 100)), - UserSessionInfo(sessionId: "3", sessionName: "Android", deviceType: .mobile, isVerified: false, lastSeenIP: "3.0.0.3", lastSeenTimestamp: (Date().timeIntervalSince1970 - 10)) - ] - - self.lastOverviewData = UserSessionsOverviewData(currentSessionInfo: currentSessionInfo, unverifiedSessionsInfo: unverifiedSessionsInfo, inactiveSessionsInfo: inactiveSessionsInfo, otherSessionsInfo: otherSessionsInfo) + completion(.success(overviewData)) + } + + func sessionForIdentifier(_ sessionId: String) -> UserSessionInfo? { + overviewData.otherSessions.first { $0.id == sessionId } + } + + // MARK: - Private + + private var currentSession: UserSessionInfo { + UserSessionInfo(id: "alice", + name: "iOS", + deviceType: .mobile, + isVerified: mode == .currentSessionVerified, + lastSeenIP: "10.0.0.10", + lastSeenTimestamp: nil, + applicationName: "Element iOS", + applicationVersion: "1.0.0", + applicationURL: nil, + deviceModel: nil, + deviceOS: "iOS 15.5", + lastSeenIPLocation: nil, + clientName: "Element", + clientVersion: "1.0.0", + isActive: true, + isCurrent: true) + } + + private func buildSessions(verified: Bool, active: Bool) -> [UserSessionInfo] { + [UserSessionInfo(id: "1 verified: \(verified) active: \(active)", + name: "macOS verified: \(verified) active: \(active)", + deviceType: .desktop, + isVerified: verified, + lastSeenIP: "1.0.0.1", + lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000, + applicationName: "Element MacOS", + applicationVersion: "1.0.0", + applicationURL: nil, + deviceModel: nil, + deviceOS: "macOS 12.5.1", + lastSeenIPLocation: nil, + clientName: "Electron", + clientVersion: "20.0.0", + isActive: active, + isCurrent: false), + UserSessionInfo(id: "2 verified: \(verified) active: \(active)", + name: "Firefox on Windows verified: \(verified) active: \(active)", + deviceType: .web, + isVerified: verified, + lastSeenIP: "2.0.0.2", + lastSeenTimestamp: Date().timeIntervalSince1970 - 100, + applicationName: "Element Web", + applicationVersion: "1.0.0", + applicationURL: nil, + deviceModel: nil, + deviceOS: "Windows 10", + lastSeenIPLocation: nil, + clientName: "Firefox", + clientVersion: "39.0", + isActive: active, + isCurrent: false), + UserSessionInfo(id: "3 verified: \(verified) active: \(active)", + name: "Android verified: \(verified) active: \(active)", + deviceType: .mobile, + isVerified: verified, + lastSeenIP: "3.0.0.3", + lastSeenTimestamp: Date().timeIntervalSince1970 - 10, + applicationName: "Element Android", + applicationVersion: "1.0.0", + applicationURL: nil, + deviceModel: nil, + deviceOS: "Android 4.0", + lastSeenIPLocation: nil, + clientName: "Element", + clientVersion: "1.0.0", + isActive: active, + isCurrent: false)] } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/UserSessionsOverviewServiceProtocol.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/UserSessionsOverviewServiceProtocol.swift index e7774be69..b1bc5f001 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/UserSessionsOverviewServiceProtocol.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/UserSessionsOverviewServiceProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,16 +17,16 @@ import Foundation struct UserSessionsOverviewData { - - let currentSessionInfo: UserSessionInfo? - let unverifiedSessionsInfo: [UserSessionInfo] - let inactiveSessionsInfo: [UserSessionInfo] - let otherSessionsInfo: [UserSessionInfo] + let currentSession: UserSessionInfo? + let unverifiedSessions: [UserSessionInfo] + let inactiveSessions: [UserSessionInfo] + let otherSessions: [UserSessionInfo] } protocol UserSessionsOverviewServiceProtocol { + var overviewData: UserSessionsOverviewData { get } - var lastOverviewData: UserSessionsOverviewData { get } - - func fetchUserSessionsOverviewData(completion: @escaping (Result) -> Void) -> Void + func updateOverviewData(completion: @escaping (Result) -> Void) -> Void + + func sessionForIdentifier(_ sessionId: String) -> UserSessionInfo? } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/UI/UserSessionsOverviewUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/UI/UserSessionsOverviewUITests.swift index 136372d89..f05203cb7 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/UI/UserSessionsOverviewUITests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/UI/UserSessionsOverviewUITests.swift @@ -14,9 +14,42 @@ // limitations under the License. // -import XCTest import RiotSwiftUI +import XCTest class UserSessionsOverviewUITests: MockScreenTestCase { - // TODO: + func testCurrentSessionUnverified() { + app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.currentSessionUnverified.title) + + XCTAssertTrue(app.buttons["userSessionCardVerifyButton"].exists) + XCTAssertTrue(app.staticTexts["userSessionCardViewDetails"].exists) + } + + func testCurrentSessionVerified() { + app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.currentSessionVerified.title) + + XCTAssertFalse(app.buttons["userSessionCardVerifyButton"].exists) + XCTAssertTrue(app.staticTexts["userSessionCardViewDetails"].exists) + } + + func testOnlyUnverifiedSessions() { + app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.onlyUnverifiedSessions.title) + + XCTAssertTrue(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists) + XCTAssertTrue(app.staticTexts["userSessionsOverviewOtherSection"].exists) + } + + func testOnlyInactiveSessions() { + app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.onlyInactiveSessions.title) + + XCTAssertTrue(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists) + XCTAssertTrue(app.staticTexts["userSessionsOverviewOtherSection"].exists) + } + + func testNoOtherSessions() { + app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.noOtherSessions.title) + + XCTAssertFalse(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists) + XCTAssertFalse(app.staticTexts["userSessionsOverviewOtherSection"].exists) + } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/Unit/UserSessionsOverviewViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/Unit/UserSessionsOverviewViewModelTests.swift index d45804590..21389eafd 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/Unit/UserSessionsOverviewViewModelTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Test/Unit/UserSessionsOverviewViewModelTests.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,20 +14,77 @@ // limitations under the License. // -import XCTest import Combine +import XCTest @testable import RiotSwiftUI class UserSessionsOverviewViewModelTests: XCTestCase { + func testInitialStateEmpty() { + let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService()) + + XCTAssertNil(viewModel.state.currentSessionViewData) + XCTAssertTrue(viewModel.state.unverifiedSessionsViewData.isEmpty) + XCTAssertTrue(viewModel.state.inactiveSessionsViewData.isEmpty) + XCTAssertTrue(viewModel.state.otherSessionsViewData.isEmpty) + } - var service: MockUserSessionsOverviewService! - var viewModel: UserSessionsOverviewViewModelProtocol! - var context: UserSessionsOverviewViewModelType.Context! + func testLoadOnDidAppear() { + let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService()) + viewModel.process(viewAction: .viewAppeared) + + XCTAssertNotNil(viewModel.state.currentSessionViewData) + XCTAssertFalse(viewModel.state.unverifiedSessionsViewData.isEmpty) + XCTAssertFalse(viewModel.state.inactiveSessionsViewData.isEmpty) + XCTAssertFalse(viewModel.state.otherSessionsViewData.isEmpty) + } - override func setUpWithError() throws { - service = MockUserSessionsOverviewService() - viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service) - context = viewModel.context + func testSimpleActionProcessing() { + let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService()) + + var result: UserSessionsOverviewViewModelResult? + viewModel.completion = { action in + result = action + } + + viewModel.process(viewAction: .verifyCurrentSession) + XCTAssertEqual(result, .verifyCurrentSession) + + viewModel.process(viewAction: .viewAllUnverifiedSessions) + XCTAssertEqual(result, .showAllUnverifiedSessions) + + viewModel.process(viewAction: .viewAllInactiveSessions) + XCTAssertEqual(result, .showAllInactiveSessions) + + viewModel.process(viewAction: .viewAllOtherSessions) + XCTAssertEqual(result, .showAllOtherSessions) + } + + func testShowSessionDetails() { + let service = MockUserSessionsOverviewService() + service.updateOverviewData { _ in } + + let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service) + + var result: UserSessionsOverviewViewModelResult? + viewModel.completion = { action in + result = action + } + + guard let currentSession = service.overviewData.currentSession else { + XCTFail("The current session should be valid at this point") + return + } + + viewModel.process(viewAction: .viewCurrentSessionDetails) + XCTAssertEqual(result, .showCurrentSessionOverview(session: currentSession)) + + guard let randomSession = service.overviewData.otherSessions.randomElement() else { + XCTFail("There should be other sessions") + return + } + + viewModel.process(viewAction: .tapUserSession(randomSession.id)) + XCTAssertEqual(result, .showUserSessionOverview(session: randomSession)) } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift index 5d01772fa..812554a3c 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,31 +18,33 @@ import Foundation // MARK: - Coordinator +enum UserSessionsOverviewCoordinatorResult { + case openSessionOverview(sessionInfo: UserSessionInfo) +} + // MARK: View model -enum UserSessionsOverviewViewModelResult { - case cancel +enum UserSessionsOverviewViewModelResult: Equatable { case showAllUnverifiedSessions case showAllInactiveSessions case verifyCurrentSession - case showCurrentSessionDetails + case showCurrentSessionOverview(session: UserSessionInfo) case showAllOtherSessions - case showUserSessionDetails(_ sessionId: String) + case showUserSessionOverview(session: UserSessionInfo) } // MARK: View struct UserSessionsOverviewViewState: BindableState { + var currentSessionViewData: UserSessionCardViewData? - var unverifiedSessionsViewData: [UserSessionListItemViewData] + var unverifiedSessionsViewData = [UserSessionListItemViewData]() - var inactiveSessionsViewData: [UserSessionListItemViewData] + var inactiveSessionsViewData = [UserSessionListItemViewData]() - var currentSessionViewData: UserSessionListItemViewData? + var otherSessionsViewData = [UserSessionListItemViewData]() - var otherSessionsViewData: [UserSessionListItemViewData] - - var showLoadingIndicator: Bool = false + var showLoadingIndicator = false } enum UserSessionsOverviewViewAction { diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift index 0edaf96bb..c44c2b6fa 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift @@ -16,88 +16,66 @@ import SwiftUI -typealias UserSessionsOverviewViewModelType = StateStoreViewModel +typealias UserSessionsOverviewViewModelType = StateStoreViewModel class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSessionsOverviewViewModelProtocol { - - // MARK: - Properties - - // MARK: Private - private let userSessionsOverviewService: UserSessionsOverviewServiceProtocol - // MARK: Public - var completion: ((UserSessionsOverviewViewModelResult) -> Void)? - // MARK: - Setup - init(userSessionsOverviewService: UserSessionsOverviewServiceProtocol) { self.userSessionsOverviewService = userSessionsOverviewService - let viewState = UserSessionsOverviewViewState(unverifiedSessionsViewData: [], inactiveSessionsViewData: [], currentSessionViewData: nil, otherSessionsViewData: []) + super.init(initialViewState: .init()) - super.init(initialViewState: viewState) - - self.updateViewState(with: userSessionsOverviewService.lastOverviewData) + updateViewState(with: userSessionsOverviewService.overviewData) } // MARK: - Public - + override func process(viewAction: UserSessionsOverviewViewAction) { switch viewAction { case .viewAppeared: - self.loadData() + loadData() case .verifyCurrentSession: - self.completion?(.verifyCurrentSession) + completion?(.verifyCurrentSession) case .viewCurrentSessionDetails: - self.completion?(.showCurrentSessionDetails) + guard let currentSessionInfo = userSessionsOverviewService.overviewData.currentSession else { + assertionFailure("Missing current session") + return + } + completion?(.showCurrentSessionOverview(session: currentSessionInfo)) case .viewAllUnverifiedSessions: - self.completion?(.showAllUnverifiedSessions) + completion?(.showAllUnverifiedSessions) case .viewAllInactiveSessions: - self.completion?(.showAllInactiveSessions) + completion?(.showAllInactiveSessions) case .viewAllOtherSessions: - self.completion?(.showAllOtherSessions) + completion?(.showAllOtherSessions) case .tapUserSession(let sessionId): - self.completion?(.showUserSessionDetails(sessionId)) + guard let session = userSessionsOverviewService.sessionForIdentifier(sessionId) else { + assertionFailure("Missing session info") + return + } + completion?(.showUserSessionOverview(session: session)) } } // MARK: - Private private func updateViewState(with userSessionsViewData: UserSessionsOverviewData) { + state.unverifiedSessionsViewData = userSessionsViewData.unverifiedSessions.asViewData() + state.inactiveSessionsViewData = userSessionsViewData.inactiveSessions.asViewData() + state.otherSessionsViewData = userSessionsViewData.otherSessions.asViewData() - let unverifiedSessionsViewData = self.userSessionListItemViewDataList(from: userSessionsViewData.unverifiedSessionsInfo) - let inactiveSessionsViewData = self.userSessionListItemViewDataList(from: userSessionsViewData.inactiveSessionsInfo) - - var currentSessionViewData: UserSessionListItemViewData? - - let otherSessionsViewData = self.userSessionListItemViewDataList(from: userSessionsViewData.otherSessionsInfo) - - - if let currentSessionInfo = userSessionsViewData.currentSessionInfo { - currentSessionViewData = UserSessionListItemViewData(userSessionInfo: currentSessionInfo) - } - - self.state.unverifiedSessionsViewData = unverifiedSessionsViewData - self.state.inactiveSessionsViewData = inactiveSessionsViewData - self.state.currentSessionViewData = currentSessionViewData - self.state.otherSessionsViewData = otherSessionsViewData - } - - private func userSessionListItemViewDataList(from userSessionInfoList: [UserSessionInfo]) -> [UserSessionListItemViewData] { - return userSessionInfoList.map { - return UserSessionListItemViewData(userSessionInfo: $0) + if let currentSessionInfo = userSessionsViewData.currentSession { + state.currentSessionViewData = UserSessionCardViewData(sessionInfo: currentSessionInfo) } } private func loadData() { + state.showLoadingIndicator = true - self.state.showLoadingIndicator = true - - self.userSessionsOverviewService.fetchUserSessionsOverviewData { [weak self] result in + userSessionsOverviewService.updateOverviewData { [weak self] result in guard let self = self else { return } @@ -108,9 +86,15 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess case .success(let overViewData): self.updateViewState(with: overViewData) case .failure(let error): - // TODO + // TODO: break } } } } + +private extension Collection where Element == UserSessionInfo { + func asViewData() -> [UserSessionListItemViewData] { + map { UserSessionListItemViewData(session: $0) } + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModelProtocol.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModelProtocol.swift index f416bb16a..01b4b45ba 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModelProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,6 @@ import Foundation protocol UserSessionsOverviewViewModelProtocol { - var completion: ((UserSessionsOverviewViewModelResult) -> Void)? { get set } var context: UserSessionsOverviewViewModelType.Context { get } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/SecurityRecommendationCard.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/SecurityRecommendationCard.swift new file mode 100644 index 000000000..090051154 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/SecurityRecommendationCard.swift @@ -0,0 +1,111 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import SwiftUI + +struct SecurityRecommendationCard: View { + enum Style { + case unverified + case inactive + } + + @Environment(\.theme) var theme: ThemeSwiftUI + + let style: SecurityRecommendationCard.Style + let sessionCount: Int + let action: () -> Void + + var body: some View { + HStack(alignment: .top) { + Image(iconName) + VStack(alignment: .leading, spacing: 16.0) { + VStack(alignment: .leading, spacing: 8.0) { + Text(title) + .font(theme.fonts.calloutSB) + .foregroundColor(theme.colors.primaryContent) + + Text(subtitle) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + } + + Button { + action() + } label: { + Text(buttonTitle) + .font(theme.fonts.body) + } + .foregroundColor(theme.colors.accent) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(16) + .background(theme.colors.background) + .clipShape(backgroundShape) + .shapedBorder(color: theme.colors.quinaryContent, borderWidth: 1.0, shape: backgroundShape) + } + + private var backgroundShape: RoundedRectangle { + RoundedRectangle(cornerRadius: 8) + } + + private var iconName: String { + switch style { + case .unverified: + return Asset.Images.userSessionsUnverified.name + case .inactive: + return Asset.Images.userSessionsInactive.name + } + } + + private var title: String { + switch style { + case .unverified: + return VectorL10n.userSessionsOverviewSecurityRecommendationsUnverifiedTitle + case .inactive: + return VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle + } + } + + private var subtitle: String { + switch style { + case .unverified: + return VectorL10n.userSessionsOverviewSecurityRecommendationsUnverifiedInfo + case .inactive: + return VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo + } + } + + private var buttonTitle: String { + VectorL10n.userSessionsViewAllAction(sessionCount) + } +} + +struct SecurityRecommendationCard_Previews: PreviewProvider { + static var previews: some View { + body.theme(.light).preferredColorScheme(.light) + body.theme(.dark).preferredColorScheme(.dark) + } + + @ViewBuilder + static var body: some View { + VStack { + SecurityRecommendationCard(style: .unverified, sessionCount: 4, action: { }) + SecurityRecommendationCard(style: .inactive, sessionCount: 100, action: { }) + } + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift index ae1b65893..51a698541 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift @@ -17,9 +17,6 @@ import SwiftUI struct UserSessionListItem: View { - - // MARK: - Constants - private enum LayoutConstants { static let horizontalPadding: CGFloat = 15 static let verticalPadding: CGFloat = 16 @@ -27,23 +24,16 @@ struct UserSessionListItem: View { static let avatarRightMargin: CGFloat = 18 } - // MARK: - Properties - - // MARK: Private - @Environment(\.theme) private var theme: ThemeSwiftUI - // MARK: Public - let viewData: UserSessionListItemViewData - var onBackgroundTap: ((String) -> (Void))? = nil - - // MARK: - Body + var onBackgroundTap: ((String) -> Void)? var body: some View { - Button(action: { onBackgroundTap?(self.viewData.sessionId) - }) { + Button { + onBackgroundTap?(viewData.sessionId) + } label: { VStack(alignment: .leading, spacing: LayoutConstants.verticalPadding) { HStack(spacing: LayoutConstants.avatarRightMargin) { DeviceAvatarView(viewData: viewData.deviceAvatarViewData) @@ -64,10 +54,7 @@ struct UserSessionListItem: View { // Separator // Note: Separator leading is matching the text leading, we could use alignment guide in the future - Rectangle() - .fill(theme.colors.quinaryContent) - .frame(maxWidth: .infinity, alignment: .trailing) - .frame(height: 1.0) + SeparatorLine() .padding(.leading, LayoutConstants.horizontalPadding + LayoutConstants.avatarRightMargin + LayoutConstants.avatarWidth) } .padding(.top, LayoutConstants.verticalPadding) @@ -77,15 +64,14 @@ struct UserSessionListItem: View { } struct UserSessionListPreview: View { - let userSessionsOverviewService: UserSessionsOverviewServiceProtocol = MockUserSessionsOverviewService() var body: some View { VStack(alignment: .leading, spacing: 0) { - ForEach(userSessionsOverviewService.lastOverviewData.otherSessionsInfo) { userSessionInfo in - let viewData = UserSessionListItemViewData(userSessionInfo: userSessionInfo) + ForEach(userSessionsOverviewService.overviewData.otherSessions) { userSessionInfo in + let viewData = UserSessionListItemViewData(session: userSessionInfo) - UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in + UserSessionListItem(viewData: viewData, onBackgroundTap: { _ in }) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift index 4acac069e..af13e572b 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift @@ -18,16 +18,8 @@ import Foundation /// View data for UserSessionListItem struct UserSessionListItemViewData: Identifiable { - - // MARK: - Constants - - private static let userSessionNameFormatter = UserSessionNameFormatter() - private static let lastActivityDateFormatter = UserSessionLastActivityFormatter() - - // MARK: - Properties - var id: String { - return sessionId + sessionId } let sessionId: String @@ -38,24 +30,20 @@ struct UserSessionListItemViewData: Identifiable { let deviceAvatarViewData: DeviceAvatarViewData - // MARK: - Setup - init(sessionId: String, sessionDisplayName: String?, deviceType: DeviceType, isVerified: Bool, lastActivityDate: TimeInterval?) { - self.sessionId = sessionId - self.sessionName = Self.userSessionNameFormatter.sessionName(deviceType: deviceType, sessionDisplayName: sessionDisplayName) - self.sessionDetails = Self.buildSessionDetails(isVerified: isVerified, lastActivityDate: lastActivityDate) - self.deviceAvatarViewData = DeviceAvatarViewData(deviceType: deviceType, isVerified: isVerified) + sessionName = UserSessionNameFormatter.sessionName(deviceType: deviceType, sessionDisplayName: sessionDisplayName) + sessionDetails = Self.buildSessionDetails(isVerified: isVerified, lastActivityDate: lastActivityDate) + deviceAvatarViewData = DeviceAvatarViewData(deviceType: deviceType, isVerified: isVerified) } // MARK: - Private private static func buildSessionDetails(isVerified: Bool, lastActivityDate: TimeInterval?) -> String { - let sessionDetailsString: String let sessionStatusText = isVerified ? VectorL10n.userSessionVerifiedShort : VectorL10n.userSessionUnverifiedShort @@ -63,7 +51,7 @@ struct UserSessionListItemViewData: Identifiable { var lastActivityDateString: String? if let lastActivityDate = lastActivityDate { - lastActivityDateString = Self.lastActivityDateFormatter.lastActivityDateString(from: lastActivityDate) + lastActivityDateString = UserSessionLastActivityFormatter.lastActivityDateString(from: lastActivityDate) } if let lastActivityDateString = lastActivityDateString, lastActivityDateString.isEmpty == false { @@ -77,8 +65,11 @@ struct UserSessionListItemViewData: Identifiable { } extension UserSessionListItemViewData { - - init(userSessionInfo: UserSessionInfo) { - self.init(sessionId: userSessionInfo.sessionId, sessionDisplayName: userSessionInfo.sessionName, deviceType: userSessionInfo.deviceType, isVerified: userSessionInfo.isVerified, lastActivityDate: userSessionInfo.lastSeenTimestamp) + init(session: UserSessionInfo) { + self.init(sessionId: session.id, + sessionDisplayName: session.name, + deviceType: session.deviceType, + isVerified: session.isVerified, + lastActivityDate: session.lastSeenTimestamp) } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift index f4b49c4ea..6823c9253 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,49 +17,94 @@ import SwiftUI struct UserSessionsOverview: View { - - // MARK: - Properties - - // MARK: Private - @Environment(\.theme) private var theme: ThemeSwiftUI - // MARK: Public - @ObservedObject var viewModel: UserSessionsOverviewViewModel.Context var body: some View { ScrollView { - - // Security recommendations section - if viewModel.viewState.unverifiedSessionsViewData.isEmpty == false || viewModel.viewState.inactiveSessionsViewData.isEmpty == false { - - // TODO: + if hasSecurityRecommendations { + securityRecommendationsSection } - // Current session section - if let currentSessionViewData = viewModel.viewState.currentSessionViewData { - // TODO: - } + currentSessionsSection - // Other sessions section - if viewModel.viewState.otherSessionsViewData.isEmpty == false { - self.otherSessionsSection + if !viewModel.viewState.otherSessionsViewData.isEmpty { + otherSessionsSection } } .background(theme.colors.system.ignoresSafeArea()) .frame(maxHeight: .infinity) .navigationTitle(VectorL10n.userSessionsOverviewTitle) .activityIndicator(show: viewModel.viewState.showLoadingIndicator) - .onAppear() { + .onAppear { viewModel.send(viewAction: .viewAppeared) } } - var otherSessionsSection: some View { - + private var securityRecommendationsSection: some View { + SwiftUI.Section { + if !viewModel.viewState.unverifiedSessionsViewData.isEmpty { + SecurityRecommendationCard(style: .unverified, + sessionCount: viewModel.viewState.unverifiedSessionsViewData.count) { + viewModel.send(viewAction: .viewAllUnverifiedSessions) + } + } + + if !viewModel.viewState.inactiveSessionsViewData.isEmpty { + SecurityRecommendationCard(style: .inactive, + sessionCount: viewModel.viewState.inactiveSessionsViewData.count) { + viewModel.send(viewAction: .viewAllInactiveSessions) + } + } + } header: { + VStack(alignment: .leading) { + Text(VectorL10n.userSessionsOverviewSecurityRecommendationsSectionTitle) + .textCase(.uppercase) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .padding(.bottom, 8.0) + + Text(VectorL10n.userSessionsOverviewSecurityRecommendationsSectionInfo) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .padding(.bottom, 12.0) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 24) + } + .padding(.horizontal, 16) + .accessibilityIdentifier("userSessionsOverviewSecurityRecommendationsSection") + } + + var hasSecurityRecommendations: Bool { + !viewModel.viewState.unverifiedSessionsViewData.isEmpty || !viewModel.viewState.inactiveSessionsViewData.isEmpty + } + + @ViewBuilder + private var currentSessionsSection: some View { + if let currentSessionViewData = viewModel.viewState.currentSessionViewData { + SwiftUI.Section { + UserSessionCardView(viewData: currentSessionViewData, onVerifyAction: { _ in + viewModel.send(viewAction: .verifyCurrentSession) + }, onViewDetailsAction: { _ in + viewModel.send(viewAction: .viewCurrentSessionDetails) + }) + } header: { + Text(VectorL10n.userSessionsOverviewCurrentSessionSectionTitle) + .textCase(.uppercase) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 12.0) + .padding(.top, 24.0) + } + .padding(.horizontal, 16) + } + } + + private var otherSessionsSection: some View { SwiftUI.Section { - // Device list LazyVStack(spacing: 0) { ForEach(viewModel.viewState.otherSessionsViewData) { viewData in UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in @@ -71,17 +116,21 @@ struct UserSessionsOverview: View { } header: { VStack(alignment: .leading) { Text(VectorL10n.userSessionsOverviewOtherSessionsSectionTitle) + .textCase(.uppercase) .font(theme.fonts.footnote) .foregroundColor(theme.colors.secondaryContent) - .padding(.bottom, 10) + .padding(.bottom, 8.0) Text(VectorL10n.userSessionsOverviewOtherSessionsSectionInfo) .font(theme.fonts.footnote) .foregroundColor(theme.colors.secondaryContent) - .padding(.bottom, 11) + .padding(.bottom, 12.0) } - .padding(.horizontal, 16) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16.0) + .padding(.top, 24.0) } + .accessibilityIdentifier("userSessionsOverviewOtherSection") } } diff --git a/RiotSwiftUI/RiotSwiftUIApp.swift b/RiotSwiftUI/RiotSwiftUIApp.swift index d63504e2f..04ed5e39c 100644 --- a/RiotSwiftUI/RiotSwiftUIApp.swift +++ b/RiotSwiftUI/RiotSwiftUIApp.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2021 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,7 +39,7 @@ struct RiotSwiftUIApp: App { } class RiotSwiftUIAppDelegate: NSObject, UIApplicationDelegate { - func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { if ProcessInfo.processInfo.environment["IS_RUNNING_UI_TESTS"] == "1" { UIView.setAnimationsEnabled(false) } diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml index 954a970eb..9086e890b 100644 --- a/RiotSwiftUI/target.yml +++ b/RiotSwiftUI/target.yml @@ -24,6 +24,9 @@ schemes: disableMainThreadChecker: true targets: - RiotSwiftUnitTests + gatherCoverageData: true + coverageTargets: + - RiotSwiftUI targets: RiotSwiftUI: @@ -67,3 +70,15 @@ targets: configFiles: Debug: Debug.xcconfig Release: Release.xcconfig + + postBuildScripts: + - name: 🧹 SwiftFormat + runOnlyWhenInstalling: false + shell: /bin/sh + script: | + export PATH="$PATH:/opt/homebrew/bin" + if which swiftformat >/dev/null; then + swiftformat --lint --lenient "$PROJECT_DIR" + else + echo "warning: SwiftFormat not installed, download from https://github.com/nicklockwood/SwiftFormat" + fi diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml index e52a2453b..1b23dab5c 100644 --- a/RiotSwiftUI/targetUITests.yml +++ b/RiotSwiftUI/targetUITests.yml @@ -24,6 +24,9 @@ schemes: disableMainThreadChecker: true targets: - RiotSwiftUITests + gatherCoverageData: true + coverageTargets: + - RiotSwiftUI targets: RiotSwiftUITests: diff --git a/RiotSwiftUI/targetUnitTests.yml b/RiotSwiftUI/targetUnitTests.yml index a0d596d55..6362bf447 100644 --- a/RiotSwiftUI/targetUnitTests.yml +++ b/RiotSwiftUI/targetUnitTests.yml @@ -24,6 +24,9 @@ schemes: disableMainThreadChecker: true targets: - RiotSwiftUnitTests + gatherCoverageData: true + coverageTargets: + - RiotSwiftUI targets: RiotSwiftUnitTests: diff --git a/RiotTests/MarkdownToHTMLRendererTests.swift b/RiotTests/MarkdownToHTMLRendererTests.swift index 58626c1e3..918b4d494 100644 --- a/RiotTests/MarkdownToHTMLRendererTests.swift +++ b/RiotTests/MarkdownToHTMLRendererTests.swift @@ -112,6 +112,20 @@ final class MarkdownToHTMLRendererTests: XCTestCase { testRenderHTML(input: input, expectedOutput: expectedOutput) } + func testRenderRepairedLinksWithCharactersRequiringPercentEncoding() { + let input = "Some link with special characters: " + + "https://matrix.to/#/#_oftc_#matrix-dev:matrix.org" + + " " + + "https://matrix.to/#/#?=+-_#_" + + "\n" + let expectedOutput = "

    Some link with special characters: " + + "https://matrix.to/#/#_oftc_#matrix-dev:matrix.org" + + " " + + "https://matrix.to/#/#?=+-_#_

    " + + "\n" + testRenderHTML(input: input, expectedOutput: expectedOutput) + } + /// Test links inside codeblocks. func testRenderLinksInCodeblock() { let input = "```" diff --git a/RiotTests/Modules/Authentication/Mocks/MockSessionCreator.swift b/RiotTests/Modules/Authentication/Mocks/MockSessionCreator.swift index e62578d37..2b018dae2 100644 --- a/RiotTests/Modules/Authentication/Mocks/MockSessionCreator.swift +++ b/RiotTests/Modules/Authentication/Mocks/MockSessionCreator.swift @@ -20,6 +20,7 @@ import Foundation struct MockSessionCreator: SessionCreatorProtocol { /// Returns a basic session created from the supplied credentials. This prevents the app from setting up the account during tests. + @MainActor func createSession(credentials: MXCredentials, client: AuthenticationRestClient, removeOtherAccounts: Bool) -> MXSession { let client = MXRestClient(credentials: credentials, unauthenticatedHandler: { _,_,_,_ in }) // The handler is expected if credentials are set. diff --git a/RiotTests/Modules/DeepLink/MXRoomAliasResolutionDeeplinkTests.swift b/RiotTests/Modules/DeepLink/MXRoomAliasResolutionDeeplinkTests.swift index 27116d070..03120ecbb 100644 --- a/RiotTests/Modules/DeepLink/MXRoomAliasResolutionDeeplinkTests.swift +++ b/RiotTests/Modules/DeepLink/MXRoomAliasResolutionDeeplinkTests.swift @@ -28,7 +28,7 @@ class MXRoomAliasResolutionDeeplinkTests: XCTestCase { let resolution = MXRoomAliasResolution() resolution.roomId = "!abc:matrix.org" - XCTAssertEqual(resolution.deeplinkFragment, "!abc%3Amatrix.org") + XCTAssertEqual(resolution.deeplinkFragment, "!abc:matrix.org") } func test_fragmentContainsSingleServer() { @@ -38,7 +38,7 @@ class MXRoomAliasResolutionDeeplinkTests: XCTestCase { "matrix.org" ] - XCTAssertEqual(resolution.deeplinkFragment, "xyz%3Aelement.io%3Fvia%3Dmatrix.org") + XCTAssertEqual(resolution.deeplinkFragment, "xyz:element.io?via=matrix.org") } func test_fragmentContainsMultipleSerivers() { @@ -51,6 +51,6 @@ class MXRoomAliasResolutionDeeplinkTests: XCTestCase { "matrix.org" ] - XCTAssertEqual(resolution.deeplinkFragment, "mno%3Aserver.com%3Fvia%3Dserver.com%26via%3Delement.io%26via%3Dwikipedia.org%26via%3Dmatrix.org") + XCTAssertEqual(resolution.deeplinkFragment, "mno:server.com?via=server.com&via=element.io&via=wikipedia.org&via=matrix.org") } } diff --git a/RiotTests/SessionCreatorTests.swift b/RiotTests/SessionCreatorTests.swift index f2003545e..dbc28c728 100644 --- a/RiotTests/SessionCreatorTests.swift +++ b/RiotTests/SessionCreatorTests.swift @@ -19,7 +19,7 @@ import XCTest class SessionCreatorTests: XCTestCase { - func testIdentityServer() throws { + func testIdentityServer() async throws { let sessionCreator = SessionCreator(withAccountManager: .mock) let mockIS = "mock_identity_server" @@ -29,7 +29,7 @@ class SessionCreatorTests: XCTestCase { accessToken: "mock_access_token") let client = MXRestClient(credentials: credentials) client.identityServer = mockIS - let session = sessionCreator.createSession(credentials: credentials, client: client, removeOtherAccounts: false) + let session = await sessionCreator.createSession(credentials: credentials, client: client, removeOtherAccounts: false) XCTAssertEqual(credentials.identityServer, mockIS) XCTAssertEqual(session.credentials.identityServer, mockIS) diff --git a/RiotTests/UserAgentParserTests.swift b/RiotTests/UserAgentParserTests.swift new file mode 100644 index 000000000..e268deda6 --- /dev/null +++ b/RiotTests/UserAgentParserTests.swift @@ -0,0 +1,204 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import Element + +class UserAgentParserTests: XCTestCase { + + func testAndroidUserAgents() throws { + let uaStrings = [ + // New User Agent Implementation + "Element dbg/1.5.0-dev (Xiaomi Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.2)", + "Element/1.5.0 (Samsung SM-G960F; Android 6.0.1; RKQ1.200826.002; Flavour FDroid; MatrixAndroidSdk2 1.5.2)", + "Element/1.5.0 (Google Nexus 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)", + // Legacy User Agent Implementation + "Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)", + "Element/1.0.0 (Linux; Android 7.0; SM-G610M Build/NRD90M; Flavour GPlay; MatrixAndroidSdk2 1.0)" + ] + let userAgents = uaStrings.map { UserAgentParser.parse($0) } + + let expected = [ + UserAgent(deviceType: .mobile, + deviceModel: "Xiaomi Mi 9T", + deviceOS: "Android 11", + clientName: "Element dbg", + clientVersion: "1.5.0-dev"), + UserAgent(deviceType: .mobile, + deviceModel: "Samsung SM-G960F", + deviceOS: "Android 6.0.1", + clientName: "Element", + clientVersion: "1.5.0"), + UserAgent(deviceType: .mobile, + deviceModel: "Google Nexus 5", + deviceOS: "Android 7.0", + clientName: "Element", + clientVersion: "1.5.0"), + UserAgent(deviceType: .mobile, + deviceModel: "SM-A510F Build/MMB29", + deviceOS: "Android 6.0.1", + clientName: "Element", + clientVersion: "1.0.0"), + UserAgent(deviceType: .mobile, + deviceModel: "SM-G610M Build/NRD90M", + deviceOS: "Android 7.0", + clientName: "Element", + clientVersion: "1.0.0") + ] + + XCTAssertEqual(userAgents, expected) + } + + func testIOSUserAgents() throws { + let uaStrings = [ + // New User Agent Implementation + "Element/1.9.8 (iPhone X; iOS 15.2; Scale/3.00)", + "Element/1.9.9 (iPhone XS; iOS 15.5; Scale/3.00)", + "Element/1.9.7 (iPad Pro (12.9-inch) (3rd generation); iOS 15.5; Scale/3.00)", + // Legacy User Agent Implementation + "Element/1.8.21 (iPhone; iOS 15.0; Scale/2.00)", + "Element/1.8.19 (iPhone; iOS 15.2; Scale/3.00)", + // Simulator User Agent + "Element/1.9.7 (Simulator (iPhone 13 Pro Max); iOS 15.5; Scale/3.00)" + ] + let userAgents = uaStrings.map { UserAgentParser.parse($0) } + + let expected = [ + UserAgent(deviceType: .mobile, + deviceModel: "iPhone X", + deviceOS: "iOS 15.2", + clientName: "Element", + clientVersion: "1.9.8"), + UserAgent(deviceType: .mobile, + deviceModel: "iPhone XS", + deviceOS: "iOS 15.5", + clientName: "Element", + clientVersion: "1.9.9"), + UserAgent(deviceType: .mobile, + deviceModel: "iPad Pro (12.9-inch) (3rd generation)", + deviceOS: "iOS 15.5", + clientName: "Element", + clientVersion: "1.9.7"), + UserAgent(deviceType: .mobile, + deviceModel: "iPhone", + deviceOS: "iOS 15.0", + clientName: "Element", + clientVersion: "1.8.21"), + UserAgent(deviceType: .mobile, + deviceModel: "iPhone", + deviceOS: "iOS 15.2", + clientName: "Element", + clientVersion: "1.8.19"), + UserAgent(deviceType: .mobile, + deviceModel: "Simulator (iPhone 13 Pro Max)", + deviceOS: "iOS 15.5", + clientName: "Element", + clientVersion: "1.9.7") + ] + + XCTAssertEqual(userAgents, expected) + } + + func testDesktopUserAgents() { + let uaStrings = [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36" + ] + let userAgents = uaStrings.map { UserAgentParser.parse($0) } + + let expected = [ + UserAgent(deviceType: .desktop, + deviceModel: nil, + deviceOS: "macOS 10.15.7", + clientName: "Electron", + clientVersion: "20.1.1"), + UserAgent(deviceType: .desktop, + deviceModel: nil, + deviceOS: "Windows NT 10.0", + clientName: "Electron", + clientVersion: "20.1.1") + ] + + XCTAssertEqual(userAgents, expected) + } + + func testWebUserAgents() throws { + let uaStrings = [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0) Gecko/20100101 Firefox/39.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 Safari/600.3.18", + "Mozilla/5.0 (Linux; Android 9; SM-G973U Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36" + ] + let userAgents = uaStrings.map { UserAgentParser.parse($0) } + + let expected = [ + UserAgent(deviceType: .web, + deviceModel: nil, + deviceOS: "macOS 10.15.7", + clientName: "Chrome", + clientVersion: "104.0.5112.102"), + UserAgent(deviceType: .web, + deviceModel: nil, + deviceOS: "Windows NT 10.0", + clientName: "Chrome", + clientVersion: "104.0.5112.102"), + UserAgent(deviceType: .web, + deviceModel: nil, + deviceOS: "macOS 10.10", + clientName: "Firefox", + clientVersion: "39.0"), + UserAgent(deviceType: .web, + deviceModel: nil, + deviceOS: "macOS 10.10.2", + clientName: "Safari", + clientVersion: "8.0.3"), + UserAgent(deviceType: .web, + deviceModel: nil, + deviceOS: "Android 9", + clientName: "Chrome", + clientVersion: "69.0.3497.100") + ] + + XCTAssertEqual(userAgents, expected) + } + + func testInvalidUserAgents() throws { + let uaStrings = [ + "Element (iPhone X; OS 15.2; 3.00)", + "Element/1.9.9; iOS", + "Element/1.9.7 Android", + "some random string", + "Element/1.9.9; iOS ", + ] + let userAgents = uaStrings.map { UserAgentParser.parse($0) } + + let expected = [ + .unknown, + .unknown, + .unknown, + .unknown, + UserAgent(deviceType: .mobile, + deviceModel: nil, + deviceOS: nil, + clientName: "Element", + clientVersion: "1.9.9;") + ] + + XCTAssertEqual(userAgents, expected) + } + +} diff --git a/RiotTests/UserSessionsOverviewServiceTests.swift b/RiotTests/UserSessionsOverviewServiceTests.swift new file mode 100644 index 000000000..d52ca31d9 --- /dev/null +++ b/RiotTests/UserSessionsOverviewServiceTests.swift @@ -0,0 +1,267 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import Element +@testable import MatrixSDK + +private let currentDeviceId = "deviceId" +private let unverifiedDeviceId = "unverifiedDeviceId" +private let currentUserId = "userId" + +class UserSessionsOverviewServiceTests: XCTestCase { + func testInitialSessionUnverified() { + let dataProvider = MockUserSessionsDataProvider(mode: .currentSessionUnverified) + let service = UserSessionsOverviewService(dataProvider: dataProvider) + + XCTAssertNotNil(service.overviewData.currentSession) + XCTAssertFalse(service.overviewData.currentSession?.isVerified ?? false) + XCTAssertTrue(service.overviewData.currentSession?.isActive ?? false) + XCTAssertFalse(service.overviewData.unverifiedSessions.isEmpty) + XCTAssertTrue(service.overviewData.inactiveSessions.isEmpty) + + XCTAssertEqual(service.sessionForIdentifier(currentDeviceId), service.overviewData.currentSession) + } + + func testInitialSessionVerified() { + let dataProvider = MockUserSessionsDataProvider(mode: .currentSessionVerified) + let service = UserSessionsOverviewService(dataProvider: dataProvider) + + XCTAssertNotNil(service.overviewData.currentSession) + XCTAssertTrue(service.overviewData.currentSession?.isVerified ?? false) + XCTAssertTrue(service.overviewData.currentSession?.isActive ?? false) + XCTAssertTrue(service.overviewData.unverifiedSessions.isEmpty) + XCTAssertTrue(service.overviewData.inactiveSessions.isEmpty) + } + + func testWithAllSessionsVerified() { + let service = setupServiceWithMode(.allOtherSessionsValid) + + XCTAssertNotNil(service.overviewData.currentSession) + XCTAssertTrue(service.overviewData.currentSession?.isVerified ?? false) + XCTAssertTrue(service.overviewData.currentSession?.isActive ?? false) + + XCTAssertTrue(service.overviewData.unverifiedSessions.isEmpty) + XCTAssertTrue(service.overviewData.inactiveSessions.isEmpty) + XCTAssertFalse(service.overviewData.otherSessions.isEmpty) + } + + func testWithSomeUnverifiedSessions() { + let service = setupServiceWithMode(.someUnverifiedSessions) + + XCTAssertNotNil(service.overviewData.currentSession) + XCTAssertTrue(service.overviewData.currentSession?.isVerified ?? false) + XCTAssertTrue(service.overviewData.currentSession?.isActive ?? false) + + XCTAssertFalse(service.overviewData.unverifiedSessions.isEmpty) + XCTAssertTrue(service.overviewData.inactiveSessions.isEmpty) + XCTAssertFalse(service.overviewData.otherSessions.isEmpty) + } + + func testWithSomeInactiveSessions() { + let service = setupServiceWithMode(.someInactiveSessions) + + XCTAssertNotNil(service.overviewData.currentSession) + XCTAssertTrue(service.overviewData.currentSession?.isVerified ?? false) + XCTAssertTrue(service.overviewData.currentSession?.isActive ?? false) + + XCTAssertTrue(service.overviewData.unverifiedSessions.isEmpty) + XCTAssertFalse(service.overviewData.inactiveSessions.isEmpty) + XCTAssertFalse(service.overviewData.otherSessions.isEmpty) + } + + func testWithSomeUnverifiedAndInactiveSessions() { + let service = setupServiceWithMode(.someUnverifiedAndInactiveSessions) + + XCTAssertNotNil(service.overviewData.currentSession) + XCTAssertTrue(service.overviewData.currentSession?.isVerified ?? false) + XCTAssertTrue(service.overviewData.currentSession?.isActive ?? false) + + XCTAssertFalse(service.overviewData.unverifiedSessions.isEmpty) + XCTAssertFalse(service.overviewData.inactiveSessions.isEmpty) + XCTAssertFalse(service.overviewData.otherSessions.isEmpty) + } + + // MARK: - Private + + private func setupServiceWithMode(_ mode: MockUserSessionsDataProvider.Mode) -> UserSessionsOverviewServiceProtocol { + let dataProvider = MockUserSessionsDataProvider(mode: mode) + let service = UserSessionsOverviewService(dataProvider: dataProvider) + + let expectation = expectation(description: "Wait for service update") + service.updateOverviewData { _ in + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + + return service + } +} + +private class MockUserSessionsDataProvider: UserSessionsDataProviderProtocol { + enum Mode { + case currentSessionUnverified + case currentSessionVerified + case allOtherSessionsValid + case someUnverifiedSessions + case someInactiveSessions + case someUnverifiedAndInactiveSessions + } + + private let mode: Mode + + var myDeviceId = currentDeviceId + + var myUserId: String? = currentUserId + + var activeAccounts: [MXKAccount] { + [MockAccount()] + } + + init(mode: Mode) { + self.mode = mode + } + + func devices(completion: @escaping (MXResponse<[MXDevice]>) -> Void) { + DispatchQueue.main.async { + switch self.mode { + case .currentSessionUnverified: + return + case .currentSessionVerified: + return + case .allOtherSessionsValid: + completion(.success(self.verifiedSessions)) + case .someUnverifiedSessions: + completion(.success(self.verifiedSessions + self.unverifiedSessions)) + case .someInactiveSessions: + completion(.success(self.verifiedSessions + self.inactiveSessions)) + case .someUnverifiedAndInactiveSessions: + completion(.success(self.verifiedSessions + self.unverifiedSessions + self.inactiveSessions)) + } + } + } + + func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo? { + guard deviceId == currentDeviceId else { + return MockDeviceInfo(verified: deviceId != unverifiedDeviceId) + } + + switch mode { + case .currentSessionUnverified: + return MockDeviceInfo(verified: false) + default: + return MockDeviceInfo(verified: true) + } + } + + func accountData(for eventType: String) -> [AnyHashable : Any]? { + [:] + } + + // MARK: - Private + + var verifiedSessions: [MXDevice] { + [MockDevice(identifier: currentDeviceId, sessionActive: true), + MockDevice(identifier: UUID().uuidString, sessionActive: true)] + } + + var unverifiedSessions: [MXDevice] { + [MockDevice(identifier: unverifiedDeviceId, sessionActive: true)] + } + + var inactiveSessions: [MXDevice] { + [MockDevice(identifier: UUID().uuidString, sessionActive: false)] + } +} + +private class MockAccount: MXKAccount { + override var device: MXDevice? { + MockDevice(identifier: currentDeviceId, sessionActive: true) + } +} + +private class MockDevice: MXDevice { + private let identifier: String + private let sessionActive: Bool + + init(identifier: String, sessionActive: Bool) { + self.identifier = identifier + self.sessionActive = sessionActive + super.init() + } + + required init?(coder: NSCoder) { + fatalError() + } + + override var deviceId: String { + get { + identifier + } + set { + + } + } + + override var lastSeenTs: UInt64 { + get { + if sessionActive { + return UInt64(Date().timeIntervalSince1970 * 1000) + } else { + let ninetyDays: Double = 90 * 86400 + return UInt64((Date().timeIntervalSince1970 - ninetyDays) * 1000) + } + } + set { + + } + } +} + +private class MockDeviceInfo: MXDeviceInfo { + private let verified: Bool + + init(verified: Bool) { + self.verified = verified + super.init() + } + + required init?(coder: NSCoder) { + fatalError() + } + + override var trustLevel: MXDeviceTrustLevel! { + MockDeviceTrustLevel(verified: verified) + } +} + +private class MockDeviceTrustLevel: MXDeviceTrustLevel { + private let verified: Bool + + init(verified: Bool) { + self.verified = verified + super.init() + } + + required init?(coder: NSCoder) { + fatalError() + } + + override var isVerified: Bool { + verified + } +} diff --git a/RiotTests/target.yml b/RiotTests/target.yml index 2465390e3..cec7a88ca 100644 --- a/RiotTests/target.yml +++ b/RiotTests/target.yml @@ -24,6 +24,9 @@ schemes: disableMainThreadChecker: true targets: - RiotTests + gatherCoverageData: true + coverageTargets: + - Riot targets: RiotTests: @@ -62,6 +65,8 @@ targets: - path: ../Config/AppConfiguration.swift - path: ../Config/CommonConfiguration.swift - path: ../Riot/Categories/Bundle.swift + - path: ../Riot/Managers/AppInfo/AppInfo.swift + - path: ../Riot/Managers/AppInfo/AppVersion.swift - path: ../Riot/Managers/Settings/RiotSettings.swift - path: ../Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift - path: ../bwi/SecureStorage/ diff --git a/SiriIntents/target.yml b/SiriIntents/target.yml index 0c9786fcf..21597f87f 100644 --- a/SiriIntents/target.yml +++ b/SiriIntents/target.yml @@ -33,6 +33,7 @@ targets: dependencies: - sdk: Intents.framework + - package: DeviceKit configFiles: Debug: Debug.xcconfig diff --git a/codecov.yml b/codecov.yml index 8af06f857..64701d167 100644 --- a/codecov.yml +++ b/codecov.yml @@ -6,14 +6,7 @@ coverage: # project coverage decrease by more than 1%: target: auto threshold: 1% - patch: - default: - # Be tolerant on slight code coverage diff on PRs to limit - # noisy red coverage status on github PRs. - # Note: The coverage stats are still uploaded - # to codecov so that PR reviewers can see uncovered lines - target: auto - threshold: 1% + patch: false ignore: - "Riot/Generated" # ignore the folder and all its contents \ No newline at end of file diff --git a/project.yml b/project.yml index f2043f39a..e8da8ff78 100644 --- a/project.yml +++ b/project.yml @@ -53,3 +53,6 @@ packages: SwiftOGG: url: https://github.com/vector-im/swift-ogg branch: main + DeviceKit: + url: https://github.com/devicekit/DeviceKit + majorVersion: 4.7.0