mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 15:22:39 +00:00
Merge branch 'develop' into aleksandrs/6786_inactive_sessions_screen
# Conflicts: # Riot/Assets/en.lproj/Vector.strings # Riot/Generated/Strings.swift # RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift # RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift # RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift # RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift # RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift # RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift
This commit is contained in:
commit
3deee90005
88 changed files with 2246 additions and 415 deletions
12
CHANGES.md
12
CHANGES.md
|
@ -1,3 +1,15 @@
|
|||
## 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
|
||||
|
|
|
@ -15,5 +15,5 @@
|
|||
//
|
||||
|
||||
// Version
|
||||
MARKETING_VERSION = 1.9.7
|
||||
CURRENT_PROJECT_VERSION = 1.9.7
|
||||
MARKETING_VERSION = 1.9.8
|
||||
CURRENT_PROJECT_VERSION = 1.9.8
|
||||
|
|
|
@ -419,9 +419,5 @@ final class BuildSettings: NSObject {
|
|||
static let syncLocalContacts: Bool = false
|
||||
|
||||
// MARK: - New App Layout
|
||||
static let newAppLayoutEnabled = true
|
||||
|
||||
// MARK: - Device manager
|
||||
|
||||
static let deviceManagerEnabled = false
|
||||
static let newAppLayoutEnabled = true
|
||||
}
|
||||
|
|
|
@ -89,6 +89,8 @@ class CommonConfiguration: NSObject, Configurable {
|
|||
sdkOptions.authEnableRefreshTokens = BuildSettings.authEnableRefreshTokens
|
||||
// Configure key provider delegate
|
||||
MXKeyProvider.sharedInstance().delegate = EncryptionKeyManager.shared
|
||||
|
||||
sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature
|
||||
}
|
||||
|
||||
private func makeASCIIUserAgent() -> String? {
|
||||
|
|
2
Podfile
2
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.23.19'
|
||||
# $matrixSDKVersion = :local
|
||||
# $matrixSDKVersion = { :branch => 'develop'}
|
||||
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }
|
||||
|
|
18
Podfile.lock
18
Podfile.lock
|
@ -56,9 +56,9 @@ PODS:
|
|||
- LoggerAPI (1.9.200):
|
||||
- Logging (~> 1.1)
|
||||
- Logging (1.4.0)
|
||||
- MatrixSDK (0.23.18):
|
||||
- MatrixSDK/Core (= 0.23.18)
|
||||
- MatrixSDK/Core (0.23.18):
|
||||
- MatrixSDK (0.23.19):
|
||||
- MatrixSDK/Core (= 0.23.19)
|
||||
- MatrixSDK/Core (0.23.19):
|
||||
- AFNetworking (~> 4.0.0)
|
||||
- GZIP (~> 1.3.0)
|
||||
- libbase58 (~> 0.1.4)
|
||||
|
@ -66,9 +66,9 @@ PODS:
|
|||
- OLMKit (~> 3.2.5)
|
||||
- Realm (= 10.27.0)
|
||||
- SwiftyBeaver (= 1.9.5)
|
||||
- MatrixSDK/CryptoSDK (0.23.18):
|
||||
- MatrixSDK/CryptoSDK (0.23.19):
|
||||
- MatrixSDKCrypto (= 0.1.0)
|
||||
- MatrixSDK/JingleCallStack (0.23.18):
|
||||
- MatrixSDK/JingleCallStack (0.23.19):
|
||||
- JitsiMeetSDK (= 5.0.2)
|
||||
- MatrixSDK/Core
|
||||
- MatrixSDKCrypto (0.1.0)
|
||||
|
@ -123,8 +123,8 @@ DEPENDENCIES:
|
|||
- KeychainAccess (~> 4.2.2)
|
||||
- KTCenterFlowLayout (~> 1.3.1)
|
||||
- libPhoneNumber-iOS (~> 0.9.13)
|
||||
- MatrixSDK (= 0.23.18)
|
||||
- MatrixSDK/JingleCallStack (= 0.23.18)
|
||||
- MatrixSDK (= 0.23.19)
|
||||
- MatrixSDK/JingleCallStack (= 0.23.19)
|
||||
- OLMKit
|
||||
- PostHog (~> 1.4.4)
|
||||
- ReadMoreTextView (~> 3.0.1)
|
||||
|
@ -221,7 +221,7 @@ SPEC CHECKSUMS:
|
|||
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
|
||||
LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d
|
||||
Logging: beeb016c9c80cf77042d62e83495816847ef108b
|
||||
MatrixSDK: 26da2e3a9f3b02fc6ea67f5bc311d30f06f9ffba
|
||||
MatrixSDK: a60a00635006c539dce654253e8f0544ea996111
|
||||
MatrixSDKCrypto: 4b9146d5ef484550341be056a164c6930038028e
|
||||
OLMKit: da115f16582e47626616874e20f7bb92222c7a51
|
||||
PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f
|
||||
|
@ -241,6 +241,6 @@ SPEC CHECKSUMS:
|
|||
zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: 45176df406c18b0c23321a308f58535fbe425a93
|
||||
PODFILE CHECKSUM: 400334cf1580361b831a632dcc025f2029e56b6e
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
23
Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/Contents.json
vendored
Normal file
23
Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/Contents.json
vendored
Normal file
|
@ -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
|
||||
}
|
||||
}
|
BIN
Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/encryption_untrusted.png
vendored
Normal file
BIN
Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/encryption_untrusted.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 890 B |
BIN
Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/encryption_untrusted@2x.png
vendored
Normal file
BIN
Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/encryption_untrusted@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/encryption_untrusted@3x.png
vendored
Normal file
BIN
Riot/Assets/Images.xcassets/Encryption/encryption_untrusted.imageset/encryption_untrusted@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
|
@ -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 %@";
|
||||
|
@ -2193,6 +2195,7 @@ Tap the + to start adding people.";
|
|||
|
||||
"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 %@";
|
||||
|
@ -2385,10 +2388,11 @@ To enable access, tap Settings> Location and select Always";
|
|||
"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.";
|
||||
|
||||
"user_other_session_security_recommendation_title" = "Security recommendation";
|
||||
|
||||
|
||||
|
||||
// First item is client name and second item is session display name
|
||||
"user_session_name" = "%@: %@";
|
||||
|
||||
|
@ -2403,12 +2407,18 @@ To enable access, tap Settings> Location and select Always";
|
|||
|
||||
"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_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";
|
||||
|
@ -2601,6 +2611,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";
|
||||
|
|
|
@ -116,6 +116,7 @@ internal class Asset: NSObject {
|
|||
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")
|
||||
|
|
|
@ -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")
|
||||
|
@ -8479,10 +8495,38 @@ public class VectorL10n: NSObject {
|
|||
public static var userOtherSessionSecurityRecommendationTitle: String {
|
||||
return VectorL10n.tr("Vector", "user_other_session_security_recommendation_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")
|
||||
}
|
||||
/// 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")
|
||||
|
@ -8531,6 +8575,14 @@ public class VectorL10n: NSObject {
|
|||
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")
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -163,7 +163,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.
|
||||
|
|
|
@ -370,6 +370,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
|
||||
|
||||
|
|
|
@ -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,8 @@ class AllChatsViewController: HomeViewController {
|
|||
recentsTableView.register(RecentsInvitesTableViewCell.nib, forCellReuseIdentifier: RecentsInvitesTableViewCell.reuseIdentifier)
|
||||
recentsTableView.contentInsetAdjustmentBehavior = .automatic
|
||||
|
||||
toolbarHeight = toolbar.frame.height
|
||||
|
||||
updateUI()
|
||||
|
||||
navigationItem.largeTitleDisplayMode = .automatic
|
||||
|
@ -122,8 +143,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 +184,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)
|
||||
|
||||
|
@ -372,8 +386,8 @@ class AllChatsViewController: HomeViewController {
|
|||
|
||||
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 {
|
||||
|
@ -385,8 +399,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -494,13 +508,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?) {
|
||||
|
|
|
@ -11,8 +11,7 @@
|
|||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AllChatsViewController" customModule="Element" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="recentsTableView" destination="orV-HH-88x" id="lgA-2k-pXJ"/>
|
||||
<outlet property="stickyHeadersBottomContainer" destination="EXH-mK-0eB" id="95Y-KP-bwF"/>
|
||||
<outlet property="stickyHeadersBottomContainerHeightConstraint" destination="SNq-Js-N7s" id="vom-iM-s6W"/>
|
||||
<outlet property="toolbar" destination="osO-1z-CZg" id="xRm-cJ-bNi"/>
|
||||
<outlet property="view" destination="iN0-l3-epB" id="NUQ-LI-M61"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
|
@ -25,25 +24,24 @@
|
|||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</tableView>
|
||||
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="EXH-mK-0eB">
|
||||
<rect key="frame" x="0.0" y="667" width="375" height="0.0"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<accessibility key="accessibilityConfiguration" identifier="RecentsVCStickyHeadersBottomContainer"/>
|
||||
<toolbar opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="osO-1z-CZg">
|
||||
<rect key="frame" x="0.0" y="623" width="375" height="44"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" id="SNq-Js-N7s"/>
|
||||
<constraint firstAttribute="height" constant="44" id="7k8-Ot-vpH"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<items/>
|
||||
</toolbar>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="4qf-KS-Fc9"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="orV-HH-88x" secondAttribute="trailing" id="3Np-64-AUe"/>
|
||||
<constraint firstItem="4qf-KS-Fc9" firstAttribute="bottom" secondItem="orV-HH-88x" secondAttribute="bottom" id="Bka-Zz-CEr"/>
|
||||
<constraint firstItem="EXH-mK-0eB" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="bottom" id="Kmg-aC-GOO"/>
|
||||
<constraint firstItem="EXH-mK-0eB" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="S3i-DW-PUB"/>
|
||||
<constraint firstAttribute="trailing" secondItem="EXH-mK-0eB" secondAttribute="trailing" id="bPP-yu-FTa"/>
|
||||
<constraint firstItem="osO-1z-CZg" firstAttribute="trailing" secondItem="4qf-KS-Fc9" secondAttribute="trailing" id="T2o-XM-BgN"/>
|
||||
<constraint firstItem="orV-HH-88x" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="dTn-zC-Axs"/>
|
||||
<constraint firstItem="osO-1z-CZg" firstAttribute="leading" secondItem="4qf-KS-Fc9" secondAttribute="leading" id="mq4-BS-JFN"/>
|
||||
<constraint firstItem="osO-1z-CZg" firstAttribute="bottom" secondItem="4qf-KS-Fc9" secondAttribute="bottom" id="ups-Ek-9Zs"/>
|
||||
<constraint firstItem="orV-HH-88x" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="xMc-15-1wS"/>
|
||||
<constraint firstItem="4qf-KS-Fc9" firstAttribute="bottom" secondItem="orV-HH-88x" secondAttribute="bottom" id="zCp-Ju-dvr"/>
|
||||
</constraints>
|
||||
<point key="canvasLocation" x="140" y="137.18140929535232"/>
|
||||
</view>
|
||||
|
|
|
@ -209,6 +209,7 @@ extension UserVerificationCoordinator: KeyVerificationManuallyVerifyCoordinatorD
|
|||
self.presenter.toPresentable().dismiss(animated: true) {
|
||||
self.remove(childCoordinator: coordinator)
|
||||
}
|
||||
delegate?.userVerificationCoordinatorDidComplete(self)
|
||||
}
|
||||
|
||||
func keyVerificationManuallyVerifyCoordinatorDidCancel(_ coordinator: KeyVerificationManuallyVerifyCoordinatorType) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -920,7 +920,7 @@
|
|||
{
|
||||
for (MXKRoomBubbleComponent *component in bubbleComponents)
|
||||
{
|
||||
if (component.showEncryptionBadge)
|
||||
if (component.encryptionDecoration != EventEncryptionDecorationNone)
|
||||
{
|
||||
containsBubbleComponentWithEncryptionBadge = YES;
|
||||
break;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
#define TABLEVIEW_ROW_CELL_HEIGHT 46
|
||||
#define TABLEVIEW_SECTION_HEADER_HEIGHT 28
|
||||
|
||||
@interface RoomMemberDetailsViewController () <UIGestureRecognizerDelegate, DeviceTableViewCellDelegate, RoomMemberTitleViewDelegate, KeyVerificationCoordinatorBridgePresenterDelegate>
|
||||
@interface RoomMemberDetailsViewController () <UIGestureRecognizerDelegate, DeviceTableViewCellDelegate, RoomMemberTitleViewDelegate, KeyVerificationCoordinatorBridgePresenterDelegate, UserVerificationCoordinatorBridgePresenterDelegate>
|
||||
{
|
||||
RoomMemberTitleView* memberTitleView;
|
||||
|
||||
|
@ -449,6 +449,7 @@
|
|||
session:self.mxRoom.mxSession
|
||||
userId:self.mxRoomMember.userId
|
||||
userDisplayName:self.mxRoomMember.displayname];
|
||||
userVerificationCoordinatorBridgePresenter.delegate = self;
|
||||
[userVerificationCoordinatorBridgePresenter start];
|
||||
self.userVerificationCoordinatorBridgePresenter = userVerificationCoordinatorBridgePresenter;
|
||||
}
|
||||
|
@ -1345,4 +1346,11 @@
|
|||
keyVerificationCoordinatorBridgePresenter = nil;
|
||||
}
|
||||
|
||||
#pragma mark - UserVerificationCoordinatorBridgePresenterDelegate
|
||||
|
||||
- (void)userVerificationCoordinatorBridgePresenterDelegateDidComplete:(UserVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
||||
{
|
||||
[self refreshUserEncryptionTrustLevel];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -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 */
|
|
@ -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
|
||||
|
|
|
@ -71,7 +71,15 @@ final class PinCodePreferences: NSObject {
|
|||
}
|
||||
|
||||
var isBiometricsAvailable: Bool {
|
||||
return LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
|
||||
var error: NSError?
|
||||
let result = LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
|
||||
|
||||
// While in lockout they're still techincally available
|
||||
if error?.code == LAError.Code.biometryLockout.rawValue {
|
||||
return true
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/// Allowed number of PIN trials before showing forgot help alert
|
||||
|
|
|
@ -45,7 +45,7 @@ enum {
|
|||
};
|
||||
|
||||
|
||||
@interface ManageSessionViewController ()
|
||||
@interface ManageSessionViewController () <UserVerificationCoordinatorBridgePresenterDelegate>
|
||||
{
|
||||
// The device to display
|
||||
MXDevice *device;
|
||||
|
@ -649,6 +649,7 @@ enum {
|
|||
userId:self.mainSession.myUser.userId
|
||||
userDisplayName:nil
|
||||
deviceId:device.deviceId];
|
||||
userVerificationCoordinatorBridgePresenter.delegate = self;
|
||||
[userVerificationCoordinatorBridgePresenter start];
|
||||
self.userVerificationCoordinatorBridgePresenter = userVerificationCoordinatorBridgePresenter;
|
||||
}
|
||||
|
@ -701,4 +702,11 @@ enum {
|
|||
self.reauthenticationCoordinatorBridgePresenter = reauthenticationPresenter;
|
||||
}
|
||||
|
||||
#pragma mark - UserVerificationCoordinatorBridgePresenterDelegate
|
||||
|
||||
- (void)userVerificationCoordinatorBridgePresenterDelegateDidComplete:(UserVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
||||
{
|
||||
[self reloadDeviceWithCompletion:^{}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -172,7 +172,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)
|
||||
|
@ -403,7 +405,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];
|
||||
|
@ -595,6 +597,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)
|
||||
{
|
||||
|
@ -2532,6 +2536,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)
|
||||
{
|
||||
|
@ -3277,6 +3305,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;
|
||||
|
|
|
@ -461,8 +461,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; \
|
||||
|
|
|
@ -24,6 +24,9 @@ schemes:
|
|||
disableMainThreadChecker: true
|
||||
targets:
|
||||
- RiotTests
|
||||
gatherCoverageData: true
|
||||
coverageTargets:
|
||||
- Riot
|
||||
|
||||
targets:
|
||||
Riot:
|
||||
|
|
24
RiotSwiftUI/Modules/Common/Extensions/Collection.swift
Normal file
24
RiotSwiftUI/Modules/Common/Extensions/Collection.swift
Normal file
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ class InactiveUserSessionLastActivityFormatter {
|
|||
return dateFormatter
|
||||
}()
|
||||
|
||||
func lastActivityDateString(from lastActivityTimestamp: TimeInterval) -> String {
|
||||
static func lastActivityDateString(from lastActivityTimestamp: TimeInterval) -> String {
|
||||
let date = Date(timeIntervalSince1970: lastActivityTimestamp)
|
||||
|
||||
return InactiveUserSessionLastActivityFormatter.dateFormatter.string(from: date)
|
||||
|
|
201
RiotSwiftUI/Modules/UserSessions/Common/UserAgentParser.swift
Normal file
201
RiotSwiftUI/Modules/UserSessions/Common/UserAgentParser.swift
Normal file
|
@ -0,0 +1,201 @@
|
|||
//
|
||||
// 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 browserName = browserName(for: userAgent)
|
||||
|
||||
if let deviceInfo = findFirstDeviceInfo(in: userAgent) {
|
||||
let deviceInfoComponents = deviceInfo.components(separatedBy: "; ")
|
||||
deviceOS = deviceInfoComponents[safe: 1]?.hasPrefix("Android") == true ? deviceInfoComponents[safe: 1] : deviceInfoComponents.first
|
||||
}
|
||||
|
||||
return UserAgent(deviceType: .desktop,
|
||||
deviceModel: browserName,
|
||||
deviceOS: deviceOS,
|
||||
clientName: nil,
|
||||
clientVersion: nil)
|
||||
}
|
||||
|
||||
// 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 browserName(for userAgent: String) -> String? {
|
||||
let components = userAgent.components(separatedBy: " ")
|
||||
if components.last?.hasPrefix("Firefox") == true {
|
||||
return "Firefox"
|
||||
} else if components.last?.hasPrefix("Safari") == true
|
||||
&& components[safe:components.count - 2]?.hasPrefix("Mobile") == true {
|
||||
// mobile browser
|
||||
let possibleBrowserName = components[safe:components.count - 3]?.components(separatedBy: "/").first
|
||||
return possibleBrowserName == "Version" ? "Safari" : possibleBrowserName
|
||||
} else if components.last?.hasPrefix("Safari") == true && components[safe:components.count - 2]?.hasPrefix("Version") == true {
|
||||
return "Safari"
|
||||
} else {
|
||||
// regular browser
|
||||
return components[safe:components.count - 2]?.components(separatedBy: "/").first
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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..<end]
|
||||
return String(subString)
|
||||
}
|
||||
|
||||
func splitByFirst(_ delimiter: Character) -> (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)
|
||||
}
|
||||
}
|
|
@ -35,16 +35,41 @@ struct UserSessionInfo: Identifiable {
|
|||
|
||||
/// Last time the session was active
|
||||
let lastSeenTimestamp: TimeInterval?
|
||||
|
||||
|
||||
// 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?
|
||||
|
||||
/// Device name
|
||||
let deviceName: 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 {
|
||||
return lhs.id == rhs.id
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
import Foundation
|
||||
|
||||
/// Enables to build last activity date string
|
||||
class UserSessionLastActivityFormatter {
|
||||
enum UserSessionLastActivityFormatter {
|
||||
private static var lastActivityDateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.locale = Locale.current
|
||||
|
@ -28,9 +28,9 @@ class UserSessionLastActivityFormatter {
|
|||
}()
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +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
|
||||
|
|
|
@ -109,12 +109,14 @@ struct UserSessionCardView: View {
|
|||
.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)
|
||||
|
@ -134,18 +136,24 @@ struct UserSessionCardViewPreview: View {
|
|||
@Environment(\.theme) var theme: ThemeSwiftUI
|
||||
|
||||
let viewData: UserSessionCardViewData
|
||||
|
||||
init(isCurrentSessionInfo: Bool = false) {
|
||||
let session = UserSessionInfo(id: "alice",
|
||||
|
||||
init(isCurrent: Bool = false) {
|
||||
let sessionInfo = UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My iPhone",
|
||||
isActive: true,
|
||||
isCurrent: isCurrentSessionInfo)
|
||||
|
||||
viewData = UserSessionCardViewData(session: session)
|
||||
isCurrent: isCurrent)
|
||||
viewData = UserSessionCardViewData(sessionInfo: sessionInfo)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
@ -161,8 +169,8 @@ struct UserSessionCardViewPreview: View {
|
|||
struct UserSessionCardView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
UserSessionCardViewPreview(isCurrentSessionInfo: true).theme(.light).preferredColorScheme(.light)
|
||||
UserSessionCardViewPreview(isCurrentSessionInfo: true).theme(.dark).preferredColorScheme(.dark)
|
||||
UserSessionCardViewPreview(isCurrent: true).theme(.light).preferredColorScheme(.light)
|
||||
UserSessionCardViewPreview(isCurrent: true).theme(.dark).preferredColorScheme(.dark)
|
||||
UserSessionCardViewPreview().theme(.light).preferredColorScheme(.light)
|
||||
UserSessionCardViewPreview().theme(.dark).preferredColorScheme(.dark)
|
||||
}
|
||||
|
|
|
@ -18,9 +18,6 @@ import Foundation
|
|||
|
||||
/// View data for UserSessionCardView
|
||||
struct UserSessionCardViewData {
|
||||
private static let userSessionNameFormatter = UserSessionNameFormatter()
|
||||
private static let lastActivityDateFormatter = UserSessionLastActivityFormatter()
|
||||
|
||||
var id: String {
|
||||
sessionId
|
||||
}
|
||||
|
@ -48,13 +45,13 @@ struct UserSessionCardViewData {
|
|||
lastSeenIP: String?,
|
||||
isCurrentSessionDisplayMode: Bool = false) {
|
||||
self.sessionId = sessionId
|
||||
sessionName = Self.userSessionNameFormatter.sessionName(deviceType: deviceType, sessionDisplayName: sessionDisplayName)
|
||||
sessionName = UserSessionNameFormatter.sessionName(deviceType: deviceType, sessionDisplayName: sessionDisplayName)
|
||||
self.isVerified = isVerified
|
||||
|
||||
var lastActivityDateString: String?
|
||||
|
||||
if let lastActivityTimestamp = lastActivityTimestamp {
|
||||
lastActivityDateString = Self.lastActivityDateFormatter.lastActivityDateString(from: lastActivityTimestamp)
|
||||
lastActivityDateString = UserSessionLastActivityFormatter.lastActivityDateString(from: lastActivityTimestamp)
|
||||
}
|
||||
|
||||
self.lastActivityDateString = lastActivityDateString
|
||||
|
@ -66,13 +63,13 @@ struct UserSessionCardViewData {
|
|||
}
|
||||
|
||||
extension UserSessionCardViewData {
|
||||
init(session: UserSessionInfo) {
|
||||
self.init(sessionId: session.id,
|
||||
sessionDisplayName: session.name,
|
||||
deviceType: session.deviceType,
|
||||
isVerified: session.isVerified,
|
||||
lastActivityTimestamp: session.lastSeenTimestamp,
|
||||
lastSeenIP: session.lastSeenIP,
|
||||
isCurrentSessionDisplayMode: session.isCurrent)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,58 +53,58 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable {
|
|||
coordinator.completion = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case let .openSessionOverview(session: session):
|
||||
self.openSessionOverview(session: session)
|
||||
case let .openOtherSessions(sessions: sessions, filter: filter):
|
||||
self.openOtherSessions(sessions: sessions, filterBy: filter, title: VectorL10n.userOtherSessionSecurityRecommendationTitle)
|
||||
case let .openSessionOverview(sessionInfo: sessionInfo):
|
||||
self.openSessionOverview(sessionsInfo: sessionInfo)
|
||||
case let .openOtherSessions(sessionsInfo: sessionsInfo, filter: filter):
|
||||
self.openOtherSessions(sessionsInfo: sessionsInfo, filterBy: filter, title: VectorL10n.userOtherSessionSecurityRecommendationTitle)
|
||||
}
|
||||
}
|
||||
return coordinator
|
||||
}
|
||||
|
||||
private func openSessionDetails(session: UserSessionInfo) {
|
||||
let coordinator = createUserSessionDetailsCoordinator(session: session)
|
||||
private func openSessionDetails(sessionInfo: UserSessionInfo) {
|
||||
let coordinator = createUserSessionDetailsCoordinator(sessionInfo: sessionInfo)
|
||||
pushScreen(with: coordinator)
|
||||
}
|
||||
|
||||
private func createUserSessionDetailsCoordinator(session: UserSessionInfo) -> UserSessionDetailsCoordinator {
|
||||
let parameters = UserSessionDetailsCoordinatorParameters(session: session)
|
||||
private func createUserSessionDetailsCoordinator(sessionInfo: UserSessionInfo) -> UserSessionDetailsCoordinator {
|
||||
let parameters = UserSessionDetailsCoordinatorParameters(sessionInfo: sessionInfo)
|
||||
return UserSessionDetailsCoordinator(parameters: parameters)
|
||||
}
|
||||
|
||||
private func openSessionOverview(session: UserSessionInfo) {
|
||||
let coordinator = createUserSessionOverviewCoordinator(session: session)
|
||||
private func openSessionOverview(sessionsInfo: UserSessionInfo) {
|
||||
let coordinator = createUserSessionOverviewCoordinator(sessionsInfo: sessionsInfo)
|
||||
coordinator.completion = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case let .openSessionDetails(session: session):
|
||||
self.openSessionDetails(session: session)
|
||||
case let .openSessionDetails(sessionInfo: session):
|
||||
self.openSessionDetails(sessionInfo: session)
|
||||
}
|
||||
}
|
||||
pushScreen(with: coordinator)
|
||||
}
|
||||
|
||||
private func createUserSessionOverviewCoordinator(session: UserSessionInfo) -> UserSessionOverviewCoordinator {
|
||||
let parameters = UserSessionOverviewCoordinatorParameters(session: session)
|
||||
private func createUserSessionOverviewCoordinator(sessionsInfo: UserSessionInfo) -> UserSessionOverviewCoordinator {
|
||||
let parameters = UserSessionOverviewCoordinatorParameters(session: parameters.session, sessionInfo: sessionsInfo)
|
||||
return UserSessionOverviewCoordinator(parameters: parameters)
|
||||
}
|
||||
|
||||
private func openOtherSessions(sessions: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter, title: String) {
|
||||
let coordinator = createOtherSessionsCoordinator(sessions: sessions,
|
||||
private func openOtherSessions(sessionsInfo: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter, title: String) {
|
||||
let coordinator = createOtherSessionsCoordinator(sessionsInfo: sessionsInfo,
|
||||
filterBy: filter,
|
||||
title: title)
|
||||
coordinator.completion = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case let .openSessionDetails(session: session):
|
||||
self.openSessionDetails(session: session)
|
||||
case let .openSessionDetails(sessionInfo: session):
|
||||
self.openSessionDetails(sessionInfo: session)
|
||||
}
|
||||
}
|
||||
pushScreen(with: coordinator)
|
||||
}
|
||||
|
||||
private func createOtherSessionsCoordinator(sessions: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter, title: String) -> UserOtherSessionsCoordinator {
|
||||
let parameters = UserOtherSessionsCoordinatorParameters(sessions: sessions,
|
||||
private func createOtherSessionsCoordinator(sessionsInfo: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter, title: String) -> UserOtherSessionsCoordinator {
|
||||
let parameters = UserOtherSessionsCoordinatorParameters(sessionsInfo: sessionsInfo,
|
||||
filter: filter,
|
||||
title: title)
|
||||
return UserOtherSessionsCoordinator(parameters: parameters)
|
||||
|
|
|
@ -18,7 +18,7 @@ import CommonKit
|
|||
import SwiftUI
|
||||
|
||||
struct UserOtherSessionsCoordinatorParameters {
|
||||
let sessions: [UserSessionInfo]
|
||||
let sessionsInfo: [UserSessionInfo]
|
||||
let filter: OtherUserSessionsFilter
|
||||
let title: String
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ final class UserOtherSessionsCoordinator: Coordinator, Presentable {
|
|||
init(parameters: UserOtherSessionsCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = UserOtherSessionsViewModel(sessions: parameters.sessions,
|
||||
let viewModel = UserOtherSessionsViewModel(sessionsInfo: parameters.sessionsInfo,
|
||||
filter: parameters.filter,
|
||||
title: parameters.title)
|
||||
let view = UserOtherSessions(viewModel: viewModel.context)
|
||||
|
@ -55,8 +55,8 @@ final class UserOtherSessionsCoordinator: Coordinator, Presentable {
|
|||
userOtherSessionsViewModel.completion = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case let .showUserSessionOverview(session: session):
|
||||
self.completion?(.openSessionDetails(session: session))
|
||||
case let .showUserSessionOverview(sessionInfo: session):
|
||||
self.completion?(.openSessionDetails(sessionInfo: session))
|
||||
}
|
||||
MXLog.debug("[UserOtherSessionsCoordinator] UserOtherSessionsViewModel did complete with result: \(result).")
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
|||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
|
||||
let viewModel = UserOtherSessionsViewModel(sessions: inactiveSessions(),
|
||||
let viewModel = UserOtherSessionsViewModel(sessionsInfo: inactiveSessions(),
|
||||
filter: .inactive,
|
||||
title: VectorL10n.userOtherSessionSecurityRecommendationTitle)
|
||||
|
||||
|
@ -53,12 +53,19 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
|||
}
|
||||
|
||||
private func inactiveSessions() -> [UserSessionInfo] {
|
||||
[UserSessionInfo(id: "alice",
|
||||
[UserSessionInfo(id: "0",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: nil,
|
||||
applicationVersion: nil,
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: nil,
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "",
|
||||
isActive: false,
|
||||
isCurrent: true),
|
||||
UserSessionInfo(id: "1",
|
||||
|
@ -67,6 +74,13 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
|||
isVerified: true,
|
||||
lastSeenIP: "1.0.0.1",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 8000000,
|
||||
applicationName: nil,
|
||||
applicationVersion: nil,
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: nil,
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "",
|
||||
isActive: false,
|
||||
isCurrent: false),
|
||||
UserSessionInfo(id: "2",
|
||||
|
@ -75,6 +89,13 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
|||
isVerified: true,
|
||||
lastSeenIP: "2.0.0.2",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 9000000,
|
||||
applicationName: nil,
|
||||
applicationVersion: nil,
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: nil,
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "",
|
||||
isActive: false,
|
||||
isCurrent: false),
|
||||
UserSessionInfo(id: "3",
|
||||
|
@ -83,6 +104,13 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
|||
isVerified: false,
|
||||
lastSeenIP: "3.0.0.3",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10000000,
|
||||
applicationName: nil,
|
||||
applicationVersion: nil,
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: nil,
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "",
|
||||
isActive: false,
|
||||
isCurrent: false)]
|
||||
}
|
||||
|
|
|
@ -18,13 +18,13 @@ import Foundation
|
|||
|
||||
// MARK: - Coordinator
|
||||
enum UserOtherSessionsCoordinatorResult {
|
||||
case openSessionDetails(session: UserSessionInfo)
|
||||
case openSessionDetails(sessionInfo: UserSessionInfo)
|
||||
}
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum UserOtherSessionsViewModelResult {
|
||||
case showUserSessionOverview(session: UserSessionInfo)
|
||||
case showUserSessionOverview(sessionInfo: UserSessionInfo)
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
|
|
@ -20,14 +20,14 @@ typealias UserOtherSessionsViewModelType = StateStoreViewModel<UserOtherSessions
|
|||
|
||||
class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessionsViewModelProtocol {
|
||||
var completion: ((UserOtherSessionsViewModelResult) -> Void)?
|
||||
private let sessions: [UserSessionInfo]
|
||||
private let sessionsInfo: [UserSessionInfo]
|
||||
|
||||
init(sessions: [UserSessionInfo],
|
||||
init(sessionsInfo: [UserSessionInfo],
|
||||
filter: OtherUserSessionsFilter,
|
||||
title: String) {
|
||||
self.sessions = sessions
|
||||
self.sessionsInfo = sessionsInfo
|
||||
super.init(initialViewState: UserOtherSessionsViewState(title: title, sections: []))
|
||||
updateViewState(sessions: sessions, filter: filter)
|
||||
updateViewState(sessionsInfo: sessionsInfo, filter: filter)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
@ -35,30 +35,30 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi
|
|||
override func process(viewAction: UserOtherSessionsViewAction) {
|
||||
switch viewAction {
|
||||
case let .userOtherSessionSelected(sessionId: sessionId):
|
||||
guard let session = sessions.first(where: {$0.id == sessionId}) else {
|
||||
guard let session = sessionsInfo.first(where: {$0.id == sessionId}) else {
|
||||
assertionFailure("Session should exist in the array.")
|
||||
return
|
||||
}
|
||||
completion?(.showUserSessionOverview(session: session))
|
||||
completion?(.showUserSessionOverview(sessionInfo: session))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func updateViewState(sessions: [UserSessionInfo], filter: OtherUserSessionsFilter) {
|
||||
let sectionItems = filterSessions(sessions: sessions, by: filter).asViewData()
|
||||
private func updateViewState(sessionsInfo: [UserSessionInfo], filter: OtherUserSessionsFilter) {
|
||||
let sectionItems = filterSessions(sessionsInfo: sessionsInfo, by: filter).asViewData()
|
||||
let sectionHeader = createHeaderData(filter: filter)
|
||||
state.sections = [.sessionItems(header: sectionHeader, items: sectionItems)]
|
||||
}
|
||||
|
||||
private func filterSessions(sessions: [UserSessionInfo], by filter: OtherUserSessionsFilter) -> [UserSessionInfo] {
|
||||
private func filterSessions(sessionsInfo: [UserSessionInfo], by filter: OtherUserSessionsFilter) -> [UserSessionInfo] {
|
||||
switch filter {
|
||||
case .all:
|
||||
return sessions.filter { !$0.isCurrent }
|
||||
return sessionsInfo.filter { !$0.isCurrent }
|
||||
case .inactive:
|
||||
return sessions.filter { !$0.isActive }
|
||||
return sessionsInfo.filter { !$0.isActive }
|
||||
case .unverified:
|
||||
return sessions.filter { !$0.isVerified }
|
||||
return sessionsInfo.filter { !$0.isVerified }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import CommonKit
|
|||
import SwiftUI
|
||||
|
||||
struct UserSessionDetailsCoordinatorParameters {
|
||||
let session: UserSessionInfo
|
||||
let sessionInfo: UserSessionInfo
|
||||
}
|
||||
|
||||
final class UserSessionDetailsCoordinator: Coordinator, Presentable {
|
||||
|
@ -40,7 +40,7 @@ final class UserSessionDetailsCoordinator: Coordinator, Presentable {
|
|||
init(parameters: UserSessionDetailsCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = UserSessionDetailsViewModel(session: parameters.session)
|
||||
let viewModel = UserSessionDetailsViewModel(session: parameters.sessionInfo)
|
||||
let view = UserSessionDetails(viewModel: viewModel.context)
|
||||
userSessionDetailsViewModel = viewModel
|
||||
userSessionDetailsHostingController = VectorHostingController(rootView: view)
|
||||
|
|
|
@ -41,21 +41,35 @@ enum MockUserSessionDetailsScreenState: MockScreenState, CaseIterable {
|
|||
let session: UserSessionInfo
|
||||
switch self {
|
||||
case .allSections:
|
||||
session = UserSessionInfo(id: "session",
|
||||
session = UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My iPhone",
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
case .sessionSectionOnly:
|
||||
session = UserSessionInfo(id: "session",
|
||||
name: "iOS",
|
||||
session = UserSessionInfo(id: "3",
|
||||
name: "Android",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: nil,
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
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,
|
||||
deviceName: "My Phone",
|
||||
isActive: true,
|
||||
isCurrent: false)
|
||||
}
|
||||
|
|
|
@ -20,17 +20,20 @@ import XCTest
|
|||
|
||||
class UserSessionDetailsViewModelTests: XCTestCase {
|
||||
func test_whenSessionNameAndLastSeenIPNil_viewStateCorrect() {
|
||||
let userSessionInfo = createUserSessionInfo(sessionId: "session",
|
||||
sessionName: nil,
|
||||
let userSessionInfo = createUserSessionInfo(id: "session",
|
||||
name: nil,
|
||||
lastSeenIP: nil)
|
||||
|
||||
let sessionItems = [
|
||||
sessionIdItem(sessionId: "session")
|
||||
]
|
||||
|
||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
sessionItems.append(sessionIdItem(sessionId: userSessionInfo.id))
|
||||
|
||||
var sections = [UserSessionDetailsSectionViewData]()
|
||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems))
|
||||
let sections = [
|
||||
UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems)
|
||||
]
|
||||
|
||||
let expectedModel = UserSessionDetailsViewState(sections: sections)
|
||||
let sut = UserSessionDetailsViewModel(session: userSessionInfo)
|
||||
|
||||
|
@ -38,18 +41,20 @@ class UserSessionDetailsViewModelTests: XCTestCase {
|
|||
}
|
||||
|
||||
func test_whenSessionNameNotNilLastSeenIPNil_viewStateCorrect() {
|
||||
let userSessionInfo = createUserSessionInfo(sessionId: "session",
|
||||
sessionName: "session name",
|
||||
let userSessionInfo = createUserSessionInfo(id: "session",
|
||||
name: "session name",
|
||||
lastSeenIP: nil)
|
||||
|
||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
sessionItems.append(sessionNameItem(sessionName: "session name"))
|
||||
sessionItems.append(sessionIdItem(sessionId: userSessionInfo.id))
|
||||
|
||||
var sections = [UserSessionDetailsSectionViewData]()
|
||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems))
|
||||
|
||||
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)
|
||||
|
@ -58,56 +63,98 @@ class UserSessionDetailsViewModelTests: XCTestCase {
|
|||
}
|
||||
|
||||
func test_whenUserSessionInfoContainsAllValues_viewStateCorrect() {
|
||||
let userSessionInfo = createUserSessionInfo(sessionId: "session",
|
||||
sessionName: "session name",
|
||||
lastSeenIP: "0.0.0.0")
|
||||
let userSessionInfo = createUserSessionInfo(id: "session",
|
||||
name: "session name",
|
||||
lastSeenIP: "0.0.0.0",
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0")
|
||||
|
||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
sessionItems.append(sessionNameItem(sessionName: "session name"))
|
||||
sessionItems.append(sessionIdItem(sessionId: userSessionInfo.id))
|
||||
|
||||
var sections = [UserSessionDetailsSectionViewData]()
|
||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems))
|
||||
|
||||
var deviceSectionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
deviceSectionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsDeviceIpAddress,
|
||||
value: "0.0.0.0"))
|
||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
|
||||
footer: nil,
|
||||
items: deviceSectionItems))
|
||||
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(sessionId: String,
|
||||
sessionName: String?,
|
||||
private func createUserSessionInfo(id: String,
|
||||
name: String?,
|
||||
deviceType: DeviceType = .mobile,
|
||||
isVerified: Bool = false,
|
||||
lastSeenIP: String?,
|
||||
lastSeenTimestamp: TimeInterval = Date().timeIntervalSince1970,
|
||||
isCurrentSession: Bool = true) -> UserSessionInfo {
|
||||
UserSessionInfo(id: sessionId,
|
||||
name: sessionName,
|
||||
applicationName: String? = nil,
|
||||
applicationVersion: String? = nil,
|
||||
applicationURL: String? = nil,
|
||||
deviceModel: String? = nil,
|
||||
deviceOS: String? = nil,
|
||||
lastSeenIPLocation: String? = nil,
|
||||
deviceName: String? = nil,
|
||||
isActive: Bool = true,
|
||||
isCurrent: Bool = true) -> UserSessionInfo {
|
||||
UserSessionInfo(id: id,
|
||||
name: name,
|
||||
deviceType: deviceType,
|
||||
isVerified: isVerified,
|
||||
lastSeenIP: lastSeenIP,
|
||||
lastSeenTimestamp: lastSeenTimestamp,
|
||||
isActive: true,
|
||||
isCurrent: isCurrentSession)
|
||||
applicationName: applicationName,
|
||||
applicationVersion: applicationVersion,
|
||||
applicationURL: applicationURL,
|
||||
deviceModel: deviceModel,
|
||||
deviceOS: deviceOS,
|
||||
lastSeenIPLocation: lastSeenIPLocation,
|
||||
deviceName: deviceName,
|
||||
isActive: isActive,
|
||||
isCurrent: isCurrent)
|
||||
}
|
||||
|
||||
private func sessionNameItem(sessionName: String) -> UserSessionDetailsSectionItemViewData {
|
||||
UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsSessionName,
|
||||
value: sessionName)
|
||||
.init(title: VectorL10n.userSessionDetailsSessionName,
|
||||
value: sessionName)
|
||||
}
|
||||
|
||||
private func sessionIdItem(sessionId: String) -> UserSessionDetailsSectionItemViewData {
|
||||
UserSessionDetailsSectionItemViewData(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
|
||||
value: sessionId)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,12 @@ class UserSessionDetailsViewModel: UserSessionDetailsViewModelType, UserSessionD
|
|||
|
||||
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)
|
||||
|
@ -43,31 +47,68 @@ class UserSessionDetailsViewModel: UserSessionDetailsViewModelType, UserSessionD
|
|||
}
|
||||
|
||||
private func sessionSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData {
|
||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
|
||||
var sessionItems: [UserSessionDetailsSectionItemViewData] = []
|
||||
|
||||
if let sessionName = session.name {
|
||||
sessionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsSessionName,
|
||||
value: sessionName))
|
||||
sessionItems.append(.init(title: VectorL10n.userSessionDetailsSessionName,
|
||||
value: sessionName))
|
||||
}
|
||||
|
||||
sessionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
|
||||
value: session.id))
|
||||
sessionItems.append(.init(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
|
||||
value: session.id))
|
||||
|
||||
return UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems)
|
||||
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 let deviceOS = session.deviceOS {
|
||||
deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceOs,
|
||||
value: deviceOS))
|
||||
}
|
||||
if let lastSeenIP = session.lastSeenIP {
|
||||
deviceSectionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsDeviceIpAddress,
|
||||
value: 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 UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
|
||||
footer: nil,
|
||||
items: deviceSectionItems)
|
||||
return .init(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
|
||||
footer: nil,
|
||||
items: deviceSectionItems)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -18,7 +18,8 @@ import CommonKit
|
|||
import SwiftUI
|
||||
|
||||
struct UserSessionOverviewCoordinatorParameters {
|
||||
let session: UserSessionInfo
|
||||
let session: MXSession
|
||||
let sessionInfo: UserSessionInfo
|
||||
}
|
||||
|
||||
final class UserSessionOverviewCoordinator: Coordinator, Presentable {
|
||||
|
@ -40,7 +41,8 @@ final class UserSessionOverviewCoordinator: Coordinator, Presentable {
|
|||
init(parameters: UserSessionOverviewCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = UserSessionOverviewViewModel(session: parameters.session)
|
||||
let service = UserSessionOverviewService(session: parameters.session, sessionInfo: parameters.sessionInfo)
|
||||
viewModel = UserSessionOverviewViewModel(sessionInfo: parameters.sessionInfo, service: service)
|
||||
|
||||
hostingController = VectorHostingController(rootView: UserSessionOverview(viewModel: viewModel.context))
|
||||
|
||||
|
@ -57,8 +59,8 @@ final class UserSessionOverviewCoordinator: Coordinator, Presentable {
|
|||
switch result {
|
||||
case .verifyCurrentSession:
|
||||
break // TODO:
|
||||
case let .showSessionDetails(session: session):
|
||||
self.completion?(.openSessionDetails(session: session))
|
||||
case let .showSessionDetails(sessionInfo: sessionInfo):
|
||||
self.completion?(.openSessionDetails(sessionInfo: sessionInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,9 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable {
|
|||
// mock that screen.
|
||||
case currentSession
|
||||
case otherSession
|
||||
|
||||
case sessionWithPushNotifications(enabled: Bool)
|
||||
case remotelyTogglingPushersNotAvailable
|
||||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
UserSessionOverview.self
|
||||
|
@ -33,35 +35,89 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable {
|
|||
|
||||
/// A list of screen state definitions
|
||||
static var allCases: [MockUserSessionOverviewScreenState] {
|
||||
[.currentSession, .otherSession]
|
||||
[.currentSession,
|
||||
.otherSession,
|
||||
.sessionWithPushNotifications(enabled: true),
|
||||
.sessionWithPushNotifications(enabled: false),
|
||||
.remotelyTogglingPushersNotAvailable]
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
let viewModel: UserSessionOverviewViewModel
|
||||
let session: UserSessionInfo
|
||||
let service: UserSessionOverviewServiceProtocol
|
||||
switch self {
|
||||
case .currentSession:
|
||||
let session = UserSessionInfo(id: "session",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
viewModel = UserSessionOverviewViewModel(session: session)
|
||||
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,
|
||||
deviceName: "My iPhone",
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
service = MockUserSessionOverviewService()
|
||||
case .otherSession:
|
||||
let session = UserSessionInfo(id: "session",
|
||||
name: "Mac",
|
||||
deviceType: .desktop,
|
||||
isVerified: true,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
isActive: true,
|
||||
isCurrent: false)
|
||||
viewModel = UserSessionOverviewViewModel(session: session)
|
||||
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,
|
||||
deviceName: "My Mac",
|
||||
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,
|
||||
deviceName: "My Mac",
|
||||
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,
|
||||
deviceName: "My Mac",
|
||||
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)))
|
||||
}
|
||||
|
|
|
@ -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<Bool?, Never>
|
||||
private(set) var remotelyTogglingPushersAvailableSubject: CurrentValueSubject<Bool, Never>
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Bool?, Never>
|
||||
var remotelyTogglingPushersAvailableSubject: CurrentValueSubject<Bool, Never>
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
protocol UserSessionOverviewServiceProtocol {
|
||||
var pusherEnabledSubject: CurrentValueSubject<Bool?, Never> { get }
|
||||
var remotelyTogglingPushersAvailableSubject: CurrentValueSubject<Bool, Never> { get }
|
||||
|
||||
func togglePushNotifications()
|
||||
}
|
|
@ -34,4 +34,31 @@ class UserSessionOverviewUITests: MockScreenTestCase {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,8 +23,9 @@ class UserSessionOverviewViewModelTests: XCTestCase {
|
|||
var sut: UserSessionOverviewViewModel!
|
||||
|
||||
func test_whenVerifyCurrentSessionProcessed_completionWithVerifyCurrentSessionCalled() {
|
||||
sut = UserSessionOverviewViewModel(session: createUserSessionInfo())
|
||||
sut = UserSessionOverviewViewModel(sessionInfo: createUserSessionInfo(), service: MockUserSessionOverviewService())
|
||||
|
||||
XCTAssertEqual(sut.state.isPusherEnabled, nil)
|
||||
var modelResult: UserSessionOverviewViewModelResult?
|
||||
sut.completion = { result in
|
||||
modelResult = result
|
||||
|
@ -34,17 +35,57 @@ class UserSessionOverviewViewModelTests: XCTestCase {
|
|||
}
|
||||
|
||||
func test_whenViewSessionDetailsProcessed_completionWithShowSessionDetailsCalled() {
|
||||
let session = createUserSessionInfo()
|
||||
sut = UserSessionOverviewViewModel(session: session)
|
||||
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(session: session))
|
||||
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",
|
||||
|
@ -52,6 +93,13 @@ class UserSessionOverviewViewModelTests: XCTestCase {
|
|||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
applicationName: "Element",
|
||||
applicationVersion: "1.9.7",
|
||||
applicationURL: nil,
|
||||
deviceModel: "iPhone XS",
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "Mobile",
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
}
|
||||
|
|
|
@ -19,13 +19,13 @@ import Foundation
|
|||
// MARK: - Coordinator
|
||||
|
||||
enum UserSessionOverviewCoordinatorResult {
|
||||
case openSessionDetails(session: UserSessionInfo)
|
||||
case openSessionDetails(sessionInfo: UserSessionInfo)
|
||||
}
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum UserSessionOverviewViewModelResult: Equatable {
|
||||
case showSessionDetails(session: UserSessionInfo)
|
||||
case showSessionDetails(sessionInfo: UserSessionInfo)
|
||||
case verifyCurrentSession
|
||||
}
|
||||
|
||||
|
@ -34,9 +34,13 @@ enum UserSessionOverviewViewModelResult: Equatable {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -19,18 +19,45 @@ import SwiftUI
|
|||
typealias UserSessionOverviewViewModelType = StateStoreViewModel<UserSessionOverviewViewState, UserSessionOverviewViewAction>
|
||||
|
||||
class UserSessionOverviewViewModel: UserSessionOverviewViewModelType, UserSessionOverviewViewModelProtocol {
|
||||
private let session: UserSessionInfo
|
||||
private let sessionInfo: UserSessionInfo
|
||||
private let service: UserSessionOverviewServiceProtocol
|
||||
|
||||
var completion: ((UserSessionOverviewViewModelResult) -> Void)?
|
||||
|
||||
init(session: UserSessionInfo) {
|
||||
self.session = session
|
||||
// MARK: - Setup
|
||||
|
||||
init(sessionInfo: UserSessionInfo, service: UserSessionOverviewServiceProtocol) {
|
||||
self.sessionInfo = sessionInfo
|
||||
self.service = service
|
||||
|
||||
let cardViewData = UserSessionCardViewData(session: session)
|
||||
let state = UserSessionOverviewViewState(cardViewData: cardViewData, isCurrentSession: session.isCurrent)
|
||||
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) {
|
||||
|
@ -38,7 +65,10 @@ class UserSessionOverviewViewModel: UserSessionOverviewViewModelType, UserSessio
|
|||
case .verifyCurrentSession:
|
||||
completion?(.verifyCurrentSession)
|
||||
case .viewSessionDetails:
|
||||
completion?(.showSessionDetails(session: session))
|
||||
completion?(.showSessionDetails(sessionInfo: sessionInfo))
|
||||
case .togglePushNotifications:
|
||||
self.state.showLoadingIndicator = true
|
||||
service.togglePushNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ struct UserSessionOverview: View {
|
|||
UserSessionCardView(viewData: viewModel.viewState.cardViewData, onVerifyAction: { _ in
|
||||
viewModel.send(viewAction: .verifyCurrentSession)
|
||||
},
|
||||
onViewDetailsAction: { _ in
|
||||
onViewDetailsAction: { _ in
|
||||
viewModel.send(viewAction: .viewSessionDetails)
|
||||
})
|
||||
.padding(16)
|
||||
|
@ -34,13 +34,21 @@ struct UserSessionOverview: View {
|
|||
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)
|
||||
VectorL10n.userSessionOverviewCurrentSessionTitle :
|
||||
VectorL10n.userSessionOverviewSessionTitle)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ struct UserSessionOverviewDisclosureCell: View {
|
|||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Image(Asset.Images.chevron.name)
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.vertical, 15)
|
||||
.padding(.horizontal, 16)
|
||||
SeparatorLine()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,7 +37,8 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
|||
init(parameters: UserSessionsOverviewCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
service = UserSessionsOverviewService(mxSession: parameters.session)
|
||||
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)
|
||||
|
@ -52,14 +53,14 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
|||
MXLog.debug("[UserSessionsOverviewCoordinator] UserSessionsOverviewViewModel did complete with result: \(result).")
|
||||
|
||||
switch result {
|
||||
case let .showOtherSessions(sessions: sessions, filter: filter):
|
||||
self.showOtherSessions(sessions: sessions, filterBy: filter)
|
||||
case let .showOtherSessions(sessionsInfo: sessionsInfo, filter: filter):
|
||||
self.showOtherSessions(sessionsInfo: sessionsInfo, filterBy: filter)
|
||||
case .verifyCurrentSession:
|
||||
self.startVerifyCurrentSession()
|
||||
case let .showCurrentSessionOverview(session):
|
||||
self.showCurrentSessionOverview(session: session)
|
||||
case let .showUserSessionOverview(session):
|
||||
self.showUserSessionOverview(session: session)
|
||||
case let .showCurrentSessionOverview(sessionInfo):
|
||||
self.showCurrentSessionOverview(sessionInfo: sessionInfo)
|
||||
case let .showUserSessionOverview(sessionInfo):
|
||||
self.showUserSessionOverview(sessionInfo: sessionInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,20 +84,20 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
|||
loadingIndicator = nil
|
||||
}
|
||||
|
||||
private func showOtherSessions(sessions: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter) {
|
||||
completion?(.openOtherSessions(sessions: sessions, filter: filter))
|
||||
private func showOtherSessions(sessionsInfo: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter) {
|
||||
completion?(.openOtherSessions(sessionsInfo: sessionsInfo, filter: filter))
|
||||
}
|
||||
|
||||
private func startVerifyCurrentSession() {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
private func showCurrentSessionOverview(session: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(session: session))
|
||||
private func showCurrentSessionOverview(sessionInfo: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(sessionInfo: sessionInfo))
|
||||
}
|
||||
|
||||
private func showUserSessionOverview(session: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(session: session))
|
||||
private func showUserSessionOverview(sessionInfo: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(sessionInfo: sessionInfo))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,31 +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 {
|
||||
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
|
||||
[.verifiedSession]
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
let service = MockUserSessionsOverviewService()
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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]?
|
||||
}
|
|
@ -21,12 +21,12 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
|||
/// Delay after which session is considered inactive, 90 days
|
||||
private static let inactiveSessionDurationTreshold: TimeInterval = 90 * 86400
|
||||
|
||||
private let mxSession: MXSession
|
||||
private let dataProvider: UserSessionsDataProviderProtocol
|
||||
|
||||
private(set) var overviewData: UserSessionsOverviewData
|
||||
|
||||
init(mxSession: MXSession) {
|
||||
self.mxSession = mxSession
|
||||
init(dataProvider: UserSessionsDataProviderProtocol) {
|
||||
self.dataProvider = dataProvider
|
||||
|
||||
overviewData = UserSessionsOverviewData(currentSession: nil,
|
||||
unverifiedSessions: [],
|
||||
|
@ -39,7 +39,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
|||
// MARK: - Public
|
||||
|
||||
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) {
|
||||
mxSession.matrixRestClient.devices { response in
|
||||
dataProvider.devices { response in
|
||||
switch response {
|
||||
case .success(let devices):
|
||||
self.overviewData = self.sessionsOverviewData(from: devices)
|
||||
|
@ -61,26 +61,28 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
|||
// MARK: - Private
|
||||
|
||||
private func setupInitialOverviewData() {
|
||||
let currentSessionInfo = currentSessionInfo()
|
||||
guard let currentSessionInfo = getCurrentSessionInfo() else {
|
||||
return
|
||||
}
|
||||
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSessionInfo,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: [],
|
||||
unverifiedSessions: currentSessionInfo.isVerified ? [] : [currentSessionInfo],
|
||||
inactiveSessions: currentSessionInfo.isActive ? [] : [currentSessionInfo],
|
||||
otherSessions: [])
|
||||
}
|
||||
|
||||
private func currentSessionInfo() -> UserSessionInfo? {
|
||||
guard let mainAccount = MXKAccountManager.shared().activeAccounts.first,
|
||||
private func getCurrentSessionInfo() -> UserSessionInfo? {
|
||||
guard let mainAccount = dataProvider.activeAccounts.first,
|
||||
let device = mainAccount.device else {
|
||||
return nil
|
||||
}
|
||||
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 == mxSession.myDeviceId) }
|
||||
.map { sessionInfo(from: $0, isCurrentSession: $0.deviceId == dataProvider.myDeviceId) }
|
||||
|
||||
return UserSessionsOverviewData(currentSession: allSessions.filter(\.isCurrent).first,
|
||||
unverifiedSessions: allSessions.filter { !$0.isVerified },
|
||||
|
@ -90,33 +92,59 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
|||
|
||||
private func sessionInfo(from device: MXDevice, isCurrentSession: Bool) -> UserSessionInfo {
|
||||
let isSessionVerified = deviceInfo(for: device.deviceId)?.trustLevel.isVerified ?? false
|
||||
|
||||
var lastSeenTs: TimeInterval?
|
||||
if device.lastSeenTs > 0 {
|
||||
lastSeenTs = TimeInterval(device.lastSeenTs / 1000)
|
||||
}
|
||||
|
||||
|
||||
let eventType = kMXAccountDataTypeClientInformation + "." + device.deviceId
|
||||
let appData = dataProvider.accountData(for: eventType)
|
||||
var userAgent: UserAgent?
|
||||
var isSessionActive = true
|
||||
if let lastSeenTimestamp = lastSeenTs {
|
||||
let elapsedTime = Date().timeIntervalSince1970 - lastSeenTimestamp
|
||||
|
||||
if let lastSeenUserAgent = device.lastSeenUserAgent {
|
||||
userAgent = UserAgentParser.parse(lastSeenUserAgent)
|
||||
}
|
||||
|
||||
if device.lastSeenTs > 0 {
|
||||
let elapsedTime = Date().timeIntervalSince1970 - TimeInterval(device.lastSeenTs / 1000)
|
||||
isSessionActive = elapsedTime < Self.inactiveSessionDurationTreshold
|
||||
}
|
||||
|
||||
return UserSessionInfo(id: device.deviceId,
|
||||
name: 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 deviceInfo(for deviceId: String) -> MXDeviceInfo? {
|
||||
guard let userId = mxSession.myUserId else {
|
||||
guard let userId = dataProvider.myUserId else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return mxSession.crypto.device(withDeviceId: deviceId, ofUser: userId)
|
||||
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,
|
||||
deviceName: userAgent?.clientName,
|
||||
isActive: isActive,
|
||||
isCurrent: isCurrent)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,55 +17,128 @@
|
|||
import Foundation
|
||||
|
||||
class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
enum Mode {
|
||||
case currentSessionUnverified
|
||||
case currentSessionVerified
|
||||
case onlyUnverifiedSessions
|
||||
case onlyInactiveSessions
|
||||
case noOtherSessions
|
||||
}
|
||||
|
||||
private let mode: Mode
|
||||
|
||||
var overviewData: UserSessionsOverviewData
|
||||
|
||||
init(mode: Mode = .currentSessionUnverified) {
|
||||
self.mode = mode
|
||||
|
||||
overviewData = UserSessionsOverviewData(currentSession: nil,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: [],
|
||||
otherSessions: [])
|
||||
}
|
||||
|
||||
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) {
|
||||
let unverifiedSessions = buildSessions(verified: false, active: true)
|
||||
let inactiveSessions = buildSessions(verified: true, active: false)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
completion(.success(overviewData))
|
||||
}
|
||||
|
||||
func sessionForIdentifier(_ sessionId: String) -> UserSessionInfo? {
|
||||
nil
|
||||
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,
|
||||
deviceName: "My iPhone",
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
}
|
||||
|
||||
init() {
|
||||
overviewData = UserSessionsOverviewData(currentSession: Self.allSessions.filter(\.isCurrent).first,
|
||||
unverifiedSessions: Self.allSessions.filter { !$0.isVerified },
|
||||
inactiveSessions: Self.allSessions.filter { !$0.isActive },
|
||||
otherSessions: Self.allSessions.filter { !$0.isCurrent })
|
||||
}
|
||||
|
||||
static var allSessions: [UserSessionInfo] = {
|
||||
[UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: nil,
|
||||
isActive: true,
|
||||
isCurrent: true),
|
||||
UserSessionInfo(id: "1",
|
||||
name: "macOS",
|
||||
private func buildSessions(verified: Bool, active: Bool) -> [UserSessionInfo] {
|
||||
[UserSessionInfo(id: "1 verified: \(verified) active: \(active)",
|
||||
name: "macOS verified: \(verified) active: \(active)",
|
||||
deviceType: .desktop,
|
||||
isVerified: true,
|
||||
isVerified: verified,
|
||||
lastSeenIP: "1.0.0.1",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 8000000,
|
||||
isActive: false,
|
||||
applicationName: "Element MacOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "macOS 12.5.1",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My Mac",
|
||||
isActive: active,
|
||||
isCurrent: false),
|
||||
UserSessionInfo(id: "2",
|
||||
name: "Firefox on Windows",
|
||||
UserSessionInfo(id: "2 verified: \(verified) active: \(active)",
|
||||
name: "Firefox on Windows verified: \(verified) active: \(active)",
|
||||
deviceType: .web,
|
||||
isVerified: true,
|
||||
isVerified: verified,
|
||||
lastSeenIP: "2.0.0.2",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
isActive: true,
|
||||
applicationName: "Element Web",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "Windows 10",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My Windows",
|
||||
isActive: active,
|
||||
isCurrent: false),
|
||||
UserSessionInfo(id: "3",
|
||||
name: "Android",
|
||||
UserSessionInfo(id: "3 verified: \(verified) active: \(active)",
|
||||
name: "Android verified: \(verified) active: \(active)",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
isVerified: verified,
|
||||
lastSeenIP: "3.0.0.3",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10,
|
||||
isActive: true,
|
||||
applicationName: "Element Android",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "Android 4.0",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My Phone",
|
||||
isActive: active,
|
||||
isCurrent: false)]
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,5 +18,38 @@ 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,13 +20,65 @@ import XCTest
|
|||
@testable import RiotSwiftUI
|
||||
|
||||
class UserSessionsOverviewViewModelTests: XCTestCase {
|
||||
var service: MockUserSessionsOverviewService!
|
||||
var viewModel: UserSessionsOverviewViewModelProtocol!
|
||||
var context: UserSessionsOverviewViewModelType.Context!
|
||||
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)
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
service = MockUserSessionsOverviewService()
|
||||
viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
|
||||
context = viewModel.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)
|
||||
}
|
||||
|
||||
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: .viewAllInactiveSessions)
|
||||
XCTAssertEqual(result, .showOtherSessions(sessionsInfo: [], filter: .inactive))
|
||||
}
|
||||
|
||||
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(sessionInfo: 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(sessionInfo: randomSession))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,17 +19,17 @@ import Foundation
|
|||
// MARK: - Coordinator
|
||||
|
||||
enum UserSessionsOverviewCoordinatorResult {
|
||||
case openSessionOverview(session: UserSessionInfo)
|
||||
case openOtherSessions(sessions: [UserSessionInfo], filter: OtherUserSessionsFilter)
|
||||
case openSessionOverview(sessionInfo: UserSessionInfo)
|
||||
case openOtherSessions(sessionsInfo: [UserSessionInfo], filter: OtherUserSessionsFilter)
|
||||
}
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum UserSessionsOverviewViewModelResult {
|
||||
case showOtherSessions(sessions: [UserSessionInfo], filter: OtherUserSessionsFilter)
|
||||
enum UserSessionsOverviewViewModelResult: Equatable {
|
||||
case showOtherSessions(sessionsInfo: [UserSessionInfo], filter: OtherUserSessionsFilter)
|
||||
case verifyCurrentSession
|
||||
case showCurrentSessionOverview(session: UserSessionInfo)
|
||||
case showUserSessionOverview(session: UserSessionInfo)
|
||||
case showCurrentSessionOverview(sessionInfo: UserSessionInfo)
|
||||
case showUserSessionOverview(sessionInfo: UserSessionInfo)
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
|
|
@ -44,19 +44,21 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
|
|||
assertionFailure("Missing current session")
|
||||
return
|
||||
}
|
||||
completion?(.showCurrentSessionOverview(session: currentSessionInfo))
|
||||
completion?(.showCurrentSessionOverview(sessionInfo: currentSessionInfo))
|
||||
case .viewAllUnverifiedSessions:
|
||||
showSessions(filteredBy: .unverified)
|
||||
// TODO: showSessions(filteredBy: .unverified)
|
||||
break
|
||||
case .viewAllInactiveSessions:
|
||||
showSessions(filteredBy: .inactive)
|
||||
case .viewAllOtherSessions:
|
||||
showSessions(filteredBy: .all)
|
||||
// TODO: showSessions(filteredBy: .all)
|
||||
break
|
||||
case .tapUserSession(let sessionId):
|
||||
guard let session = userSessionsOverviewService.sessionForIdentifier(sessionId) else {
|
||||
assertionFailure("Missing session info")
|
||||
return
|
||||
}
|
||||
completion?(.showUserSessionOverview(session: session))
|
||||
completion?(.showUserSessionOverview(sessionInfo: session))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,7 +70,7 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
|
|||
state.otherSessionsViewData = userSessionsViewData.otherSessions.asViewData()
|
||||
|
||||
if let currentSessionInfo = userSessionsViewData.currentSession {
|
||||
state.currentSessionViewData = UserSessionCardViewData(session: currentSessionInfo)
|
||||
state.currentSessionViewData = UserSessionCardViewData(sessionInfo: currentSessionInfo)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,7 +95,7 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
|
|||
}
|
||||
|
||||
private func showSessions(filteredBy filter: OtherUserSessionsFilter) {
|
||||
completion?(.showOtherSessions(sessions: userSessionsOverviewService.overviewData.otherSessions,
|
||||
completion?(.showOtherSessions(sessionsInfo: userSessionsOverviewService.overviewData.otherSessions,
|
||||
filter: filter))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import Foundation
|
|||
|
||||
/// View data for UserSessionListItem
|
||||
struct UserSessionListItemViewData: Identifiable, Hashable {
|
||||
|
||||
var id: String {
|
||||
sessionId
|
||||
}
|
||||
|
@ -29,6 +30,6 @@ struct UserSessionListItemViewData: Identifiable, Hashable {
|
|||
let sessionDetails: String
|
||||
|
||||
let deviceAvatarViewData: DeviceAvatarViewData
|
||||
|
||||
|
||||
let sessionDetailsIcon: String?
|
||||
}
|
||||
|
|
|
@ -18,13 +18,9 @@ import Foundation
|
|||
|
||||
struct UserSessionListItemViewDataFactory {
|
||||
|
||||
private static let userSessionNameFormatter = UserSessionNameFormatter()
|
||||
private static let lastActivityDateFormatter = UserSessionLastActivityFormatter()
|
||||
private static let inactiveSessionDateFormatter = InactiveUserSessionLastActivityFormatter()
|
||||
|
||||
func create(from session: UserSessionInfo) -> UserSessionListItemViewData {
|
||||
let sessionName = UserSessionListItemViewDataFactory.userSessionNameFormatter.sessionName(deviceType: session.deviceType,
|
||||
sessionDisplayName: session.name)
|
||||
let sessionName = UserSessionNameFormatter.sessionName(deviceType: session.deviceType,
|
||||
sessionDisplayName: session.name)
|
||||
let sessionDetails = buildSessionDetails(isVerified: session.isVerified,
|
||||
lastActivityDate: session.lastSeenTimestamp,
|
||||
isActive: session.isActive)
|
||||
|
@ -47,7 +43,7 @@ struct UserSessionListItemViewDataFactory {
|
|||
|
||||
private func inactiveSessionDetails(lastActivityDate: TimeInterval?) -> String {
|
||||
if let lastActivityDate = lastActivityDate {
|
||||
let lastActivityDateString = Self.inactiveSessionDateFormatter.lastActivityDateString(from: lastActivityDate)
|
||||
let lastActivityDateString = InactiveUserSessionLastActivityFormatter.lastActivityDateString(from: lastActivityDate)
|
||||
return VectorL10n.userInactiveSessionItemWithDate(lastActivityDateString)
|
||||
}
|
||||
return VectorL10n.userInactiveSessionItem
|
||||
|
@ -61,7 +57,7 @@ struct UserSessionListItemViewDataFactory {
|
|||
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 {
|
||||
|
|
|
@ -23,11 +23,13 @@ struct UserSessionsOverview: View {
|
|||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
securityRecommendationsSection
|
||||
if hasSecurityRecommendations {
|
||||
securityRecommendationsSection
|
||||
}
|
||||
|
||||
currentSessionsSection
|
||||
|
||||
if viewModel.viewState.otherSessionsViewData.isEmpty == false {
|
||||
if !viewModel.viewState.otherSessionsViewData.isEmpty {
|
||||
otherSessionsSection
|
||||
}
|
||||
}
|
||||
|
@ -40,41 +42,39 @@ struct UserSessionsOverview: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var securityRecommendationsSection: some View {
|
||||
if hasSecurityRecommendations {
|
||||
SwiftUI.Section {
|
||||
if !viewModel.viewState.unverifiedSessionsViewData.isEmpty {
|
||||
SecurityRecommendationCard(style: .unverified,
|
||||
sessionCount: viewModel.viewState.unverifiedSessionsViewData.count) {
|
||||
viewModel.send(viewAction: .viewAllUnverifiedSessions)
|
||||
}
|
||||
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)
|
||||
|
||||
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 {
|
||||
|
@ -102,10 +102,9 @@ struct UserSessionsOverview: View {
|
|||
.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
|
||||
|
@ -131,6 +130,7 @@ struct UserSessionsOverview: View {
|
|||
.padding(.horizontal, 16.0)
|
||||
.padding(.top, 24.0)
|
||||
}
|
||||
.accessibilityIdentifier("userSessionsOverviewOtherSection")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,9 @@ schemes:
|
|||
disableMainThreadChecker: true
|
||||
targets:
|
||||
- RiotSwiftUnitTests
|
||||
gatherCoverageData: true
|
||||
coverageTargets:
|
||||
- RiotSwiftUI
|
||||
|
||||
targets:
|
||||
RiotSwiftUI:
|
||||
|
|
|
@ -24,6 +24,9 @@ schemes:
|
|||
disableMainThreadChecker: true
|
||||
targets:
|
||||
- RiotSwiftUITests
|
||||
gatherCoverageData: true
|
||||
coverageTargets:
|
||||
- RiotSwiftUI
|
||||
|
||||
targets:
|
||||
RiotSwiftUITests:
|
||||
|
|
|
@ -24,6 +24,9 @@ schemes:
|
|||
disableMainThreadChecker: true
|
||||
targets:
|
||||
- RiotSwiftUnitTests
|
||||
gatherCoverageData: true
|
||||
coverageTargets:
|
||||
- RiotSwiftUI
|
||||
|
||||
targets:
|
||||
RiotSwiftUnitTests:
|
||||
|
|
202
RiotTests/UserAgentParserTests.swift
Normal file
202
RiotTests/UserAgentParserTests.swift
Normal file
|
@ -0,0 +1,202 @@
|
|||
//
|
||||
// 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: "Electron",
|
||||
deviceOS: "Macintosh",
|
||||
clientName: nil,
|
||||
clientVersion: nil),
|
||||
UserAgent(deviceType: .desktop,
|
||||
deviceModel: "Electron",
|
||||
deviceOS: "Windows NT 10.0",
|
||||
clientName: nil,
|
||||
clientVersion: nil)
|
||||
]
|
||||
|
||||
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: "Chrome",
|
||||
deviceOS: "Macintosh",
|
||||
clientName: nil,
|
||||
clientVersion: nil),
|
||||
UserAgent(deviceType: .web,
|
||||
deviceModel: "Chrome",
|
||||
deviceOS: "Windows NT 10.0",
|
||||
clientName: nil,
|
||||
clientVersion: nil),
|
||||
UserAgent(deviceType: .web,
|
||||
deviceModel: "Firefox",
|
||||
deviceOS: "Macintosh",
|
||||
clientName: nil,
|
||||
clientVersion: nil),
|
||||
UserAgent(deviceType: .web,
|
||||
deviceModel: "Safari",
|
||||
deviceOS: "Macintosh",
|
||||
clientName: nil,
|
||||
clientVersion: nil),
|
||||
UserAgent(deviceType: .web,
|
||||
deviceModel: "Chrome",
|
||||
deviceOS: "Android 9",
|
||||
clientName: nil,
|
||||
clientVersion: nil)
|
||||
]
|
||||
|
||||
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",
|
||||
"Element/1.9.9; iOS "
|
||||
]
|
||||
let userAgents = uaStrings.map { UserAgentParser.parse($0) }
|
||||
|
||||
let expected = [
|
||||
.unknown,
|
||||
.unknown,
|
||||
.unknown,
|
||||
UserAgent(deviceType: .mobile,
|
||||
deviceModel: nil,
|
||||
deviceOS: nil,
|
||||
clientName: "Element",
|
||||
clientVersion: "1.9.9;")
|
||||
]
|
||||
|
||||
XCTAssertEqual(userAgents, expected)
|
||||
}
|
||||
|
||||
}
|
267
RiotTests/UserSessionsOverviewServiceTests.swift
Normal file
267
RiotTests/UserSessionsOverviewServiceTests.swift
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -24,6 +24,9 @@ schemes:
|
|||
disableMainThreadChecker: true
|
||||
targets:
|
||||
- RiotTests
|
||||
gatherCoverageData: true
|
||||
coverageTargets:
|
||||
- Riot
|
||||
|
||||
targets:
|
||||
RiotTests:
|
||||
|
|
1
changelog.d/6580.bugfix
Normal file
1
changelog.d/6580.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Voiceover: Add labels to User Menu and My Spaces buttons on the All Chats view.
|
1
changelog.d/6755.bugfix
Normal file
1
changelog.d/6755.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
App Layout: Unable to send message after filtering for room
|
1
changelog.d/6778.bugfix
Normal file
1
changelog.d/6778.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Fix code block background colour
|
1
changelog.d/6781.change
Normal file
1
changelog.d/6781.change
Normal file
|
@ -0,0 +1 @@
|
|||
CryptoV2: Manual device verification
|
1
changelog.d/6787.change
Normal file
1
changelog.d/6787.change
Normal file
|
@ -0,0 +1 @@
|
|||
User sessions: Add support for MSC3881
|
1
changelog.d/pr-6766.change
Normal file
1
changelog.d/pr-6766.change
Normal file
|
@ -0,0 +1 @@
|
|||
UserSessions: Extended device information (PSG-772).
|
1
changelog.d/pr-6780.change
Normal file
1
changelog.d/pr-6780.change
Normal file
|
@ -0,0 +1 @@
|
|||
Settings: Add labs flags for new session manager (PSG-792, PSG-799).
|
Loading…
Reference in a new issue