mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +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)
|
## Changes in 1.9.6 (2022-09-20)
|
||||||
|
|
||||||
🙌 Improvements
|
🙌 Improvements
|
||||||
|
|
|
@ -15,5 +15,5 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
// Version
|
// Version
|
||||||
MARKETING_VERSION = 1.9.7
|
MARKETING_VERSION = 1.9.8
|
||||||
CURRENT_PROJECT_VERSION = 1.9.7
|
CURRENT_PROJECT_VERSION = 1.9.8
|
||||||
|
|
|
@ -419,9 +419,5 @@ final class BuildSettings: NSObject {
|
||||||
static let syncLocalContacts: Bool = false
|
static let syncLocalContacts: Bool = false
|
||||||
|
|
||||||
// MARK: - New App Layout
|
// MARK: - New App Layout
|
||||||
static let newAppLayoutEnabled = true
|
static let newAppLayoutEnabled = true
|
||||||
|
|
||||||
// MARK: - Device manager
|
|
||||||
|
|
||||||
static let deviceManagerEnabled = false
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,8 @@ class CommonConfiguration: NSObject, Configurable {
|
||||||
sdkOptions.authEnableRefreshTokens = BuildSettings.authEnableRefreshTokens
|
sdkOptions.authEnableRefreshTokens = BuildSettings.authEnableRefreshTokens
|
||||||
// Configure key provider delegate
|
// Configure key provider delegate
|
||||||
MXKeyProvider.sharedInstance().delegate = EncryptionKeyManager.shared
|
MXKeyProvider.sharedInstance().delegate = EncryptionKeyManager.shared
|
||||||
|
|
||||||
|
sdkOptions.enableNewClientInformationFeature = RiotSettings.shared.enableClientInformationFeature
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeASCIIUserAgent() -> String? {
|
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
|
# - `{ :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
|
# 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 = :local
|
||||||
# $matrixSDKVersion = { :branch => 'develop'}
|
# $matrixSDKVersion = { :branch => 'develop'}
|
||||||
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }
|
# $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } }
|
||||||
|
|
18
Podfile.lock
18
Podfile.lock
|
@ -56,9 +56,9 @@ PODS:
|
||||||
- LoggerAPI (1.9.200):
|
- LoggerAPI (1.9.200):
|
||||||
- Logging (~> 1.1)
|
- Logging (~> 1.1)
|
||||||
- Logging (1.4.0)
|
- Logging (1.4.0)
|
||||||
- MatrixSDK (0.23.18):
|
- MatrixSDK (0.23.19):
|
||||||
- MatrixSDK/Core (= 0.23.18)
|
- MatrixSDK/Core (= 0.23.19)
|
||||||
- MatrixSDK/Core (0.23.18):
|
- MatrixSDK/Core (0.23.19):
|
||||||
- AFNetworking (~> 4.0.0)
|
- AFNetworking (~> 4.0.0)
|
||||||
- GZIP (~> 1.3.0)
|
- GZIP (~> 1.3.0)
|
||||||
- libbase58 (~> 0.1.4)
|
- libbase58 (~> 0.1.4)
|
||||||
|
@ -66,9 +66,9 @@ PODS:
|
||||||
- OLMKit (~> 3.2.5)
|
- OLMKit (~> 3.2.5)
|
||||||
- Realm (= 10.27.0)
|
- Realm (= 10.27.0)
|
||||||
- SwiftyBeaver (= 1.9.5)
|
- SwiftyBeaver (= 1.9.5)
|
||||||
- MatrixSDK/CryptoSDK (0.23.18):
|
- MatrixSDK/CryptoSDK (0.23.19):
|
||||||
- MatrixSDKCrypto (= 0.1.0)
|
- MatrixSDKCrypto (= 0.1.0)
|
||||||
- MatrixSDK/JingleCallStack (0.23.18):
|
- MatrixSDK/JingleCallStack (0.23.19):
|
||||||
- JitsiMeetSDK (= 5.0.2)
|
- JitsiMeetSDK (= 5.0.2)
|
||||||
- MatrixSDK/Core
|
- MatrixSDK/Core
|
||||||
- MatrixSDKCrypto (0.1.0)
|
- MatrixSDKCrypto (0.1.0)
|
||||||
|
@ -123,8 +123,8 @@ DEPENDENCIES:
|
||||||
- KeychainAccess (~> 4.2.2)
|
- KeychainAccess (~> 4.2.2)
|
||||||
- KTCenterFlowLayout (~> 1.3.1)
|
- KTCenterFlowLayout (~> 1.3.1)
|
||||||
- libPhoneNumber-iOS (~> 0.9.13)
|
- libPhoneNumber-iOS (~> 0.9.13)
|
||||||
- MatrixSDK (= 0.23.18)
|
- MatrixSDK (= 0.23.19)
|
||||||
- MatrixSDK/JingleCallStack (= 0.23.18)
|
- MatrixSDK/JingleCallStack (= 0.23.19)
|
||||||
- OLMKit
|
- OLMKit
|
||||||
- PostHog (~> 1.4.4)
|
- PostHog (~> 1.4.4)
|
||||||
- ReadMoreTextView (~> 3.0.1)
|
- ReadMoreTextView (~> 3.0.1)
|
||||||
|
@ -221,7 +221,7 @@ SPEC CHECKSUMS:
|
||||||
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
|
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
|
||||||
LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d
|
LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d
|
||||||
Logging: beeb016c9c80cf77042d62e83495816847ef108b
|
Logging: beeb016c9c80cf77042d62e83495816847ef108b
|
||||||
MatrixSDK: 26da2e3a9f3b02fc6ea67f5bc311d30f06f9ffba
|
MatrixSDK: a60a00635006c539dce654253e8f0544ea996111
|
||||||
MatrixSDKCrypto: 4b9146d5ef484550341be056a164c6930038028e
|
MatrixSDKCrypto: 4b9146d5ef484550341be056a164c6930038028e
|
||||||
OLMKit: da115f16582e47626616874e20f7bb92222c7a51
|
OLMKit: da115f16582e47626616874e20f7bb92222c7a51
|
||||||
PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f
|
PostHog: 4b6321b521569092d4ef3a02238d9435dbaeb99f
|
||||||
|
@ -241,6 +241,6 @@ SPEC CHECKSUMS:
|
||||||
zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c
|
zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c
|
||||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||||
|
|
||||||
PODFILE CHECKSUM: 45176df406c18b0c23321a308f58535fbe425a93
|
PODFILE CHECKSUM: 400334cf1580361b831a632dcc025f2029e56b6e
|
||||||
|
|
||||||
COCOAPODS: 1.11.2
|
COCOAPODS: 1.11.2
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"info" : {
|
"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_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_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_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_labs_enable_new_app_layout" = "New Application Layout";
|
||||||
|
|
||||||
"settings_version" = "Version %@";
|
"settings_version" = "Version %@";
|
||||||
|
@ -2193,6 +2195,7 @@ Tap the + to start adding people.";
|
||||||
|
|
||||||
"room_recents_recently_viewed_section" = "Recently viewed";
|
"room_recents_recently_viewed_section" = "Recently viewed";
|
||||||
|
|
||||||
|
"all_chats_user_menu_accessibility_label" = "User menu";
|
||||||
"all_chats_user_menu_settings" = "User settings";
|
"all_chats_user_menu_settings" = "User settings";
|
||||||
|
|
||||||
"all_chats_edit_menu_leave_space" = "Leave %@";
|
"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_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_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";
|
"user_other_session_security_recommendation_title" = "Security recommendation";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// First item is client name and second item is session display name
|
// First item is client name and second item is session display name
|
||||||
"user_session_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_title" = "Session details";
|
||||||
"user_session_details_session_section_header" = "Session";
|
"user_session_details_session_section_header" = "Session";
|
||||||
|
"user_session_details_application_section_header" = "Application";
|
||||||
"user_session_details_device_section_header" = "Device";
|
"user_session_details_device_section_header" = "Device";
|
||||||
"user_session_details_session_name" = "Session name";
|
"user_session_details_session_name" = "Session name";
|
||||||
"user_session_details_session_id" = "Session ID";
|
"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_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_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_current_session_title" = "Current session";
|
||||||
"user_session_overview_session_title" = "Session";
|
"user_session_overview_session_title" = "Session";
|
||||||
"user_session_overview_session_details_button_title" = "Session details";
|
"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_unverify" = "Unverify";
|
||||||
"room_event_encryption_info_block" = "Blacklist";
|
"room_event_encryption_info_block" = "Blacklist";
|
||||||
"room_event_encryption_info_unblock" = "Unblacklist";
|
"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_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_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";
|
"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 e2eWarning = ImageAsset(name: "e2e_warning")
|
||||||
internal static let encryptionNormal = ImageAsset(name: "encryption_normal")
|
internal static let encryptionNormal = ImageAsset(name: "encryption_normal")
|
||||||
internal static let encryptionTrusted = ImageAsset(name: "encryption_trusted")
|
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 encryptionWarning = ImageAsset(name: "encryption_warning")
|
||||||
internal static let favouritesEmptyScreenArtwork = ImageAsset(name: "favourites_empty_screen_artwork")
|
internal static let favouritesEmptyScreenArtwork = ImageAsset(name: "favourites_empty_screen_artwork")
|
||||||
internal static let favouritesEmptyScreenArtworkDark = ImageAsset(name: "favourites_empty_screen_artwork_dark")
|
internal static let favouritesEmptyScreenArtworkDark = ImageAsset(name: "favourites_empty_screen_artwork_dark")
|
||||||
|
|
|
@ -247,6 +247,10 @@ public class VectorL10n: NSObject {
|
||||||
public static var allChatsTitle: String {
|
public static var allChatsTitle: String {
|
||||||
return VectorL10n.tr("Vector", "all_chats_title")
|
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
|
/// User settings
|
||||||
public static var allChatsUserMenuSettings: String {
|
public static var allChatsUserMenuSettings: String {
|
||||||
return VectorL10n.tr("Vector", "all_chats_user_menu_settings")
|
return VectorL10n.tr("Vector", "all_chats_user_menu_settings")
|
||||||
|
@ -5623,6 +5627,10 @@ public class VectorL10n: NSObject {
|
||||||
public static var roomEventEncryptionInfoEventUserId: String {
|
public static var roomEventEncryptionInfoEventUserId: String {
|
||||||
return VectorL10n.tr("Vector", "room_event_encryption_info_event_user_id")
|
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
|
/// End-to-end encryption information\n\n
|
||||||
public static var roomEventEncryptionInfoTitle: String {
|
public static var roomEventEncryptionInfoTitle: String {
|
||||||
return VectorL10n.tr("Vector", "room_event_encryption_info_title")
|
return VectorL10n.tr("Vector", "room_event_encryption_info_title")
|
||||||
|
@ -7375,6 +7383,14 @@ public class VectorL10n: NSObject {
|
||||||
public static var settingsLabsEnableNewAppLayout: String {
|
public static var settingsLabsEnableNewAppLayout: String {
|
||||||
return VectorL10n.tr("Vector", "settings_labs_enable_new_app_layout")
|
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
|
/// Ring for group calls
|
||||||
public static var settingsLabsEnableRingingForGroupCalls: String {
|
public static var settingsLabsEnableRingingForGroupCalls: String {
|
||||||
return VectorL10n.tr("Vector", "settings_labs_enable_ringing_for_group_calls")
|
return VectorL10n.tr("Vector", "settings_labs_enable_ringing_for_group_calls")
|
||||||
|
@ -8479,10 +8495,38 @@ public class VectorL10n: NSObject {
|
||||||
public static var userOtherSessionSecurityRecommendationTitle: String {
|
public static var userOtherSessionSecurityRecommendationTitle: String {
|
||||||
return VectorL10n.tr("Vector", "user_other_session_security_recommendation_title")
|
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
|
/// IP address
|
||||||
public static var userSessionDetailsDeviceIpAddress: String {
|
public static var userSessionDetailsDeviceIpAddress: String {
|
||||||
return VectorL10n.tr("Vector", "user_session_details_device_ip_address")
|
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
|
/// Device
|
||||||
public static var userSessionDetailsDeviceSectionHeader: String {
|
public static var userSessionDetailsDeviceSectionHeader: String {
|
||||||
return VectorL10n.tr("Vector", "user_session_details_device_section_header")
|
return VectorL10n.tr("Vector", "user_session_details_device_section_header")
|
||||||
|
@ -8531,6 +8575,14 @@ public class VectorL10n: NSObject {
|
||||||
public static var userSessionOverviewSessionTitle: String {
|
public static var userSessionOverviewSessionTitle: String {
|
||||||
return VectorL10n.tr("Vector", "user_session_overview_session_title")
|
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
|
/// Unverified session
|
||||||
public static var userSessionUnverified: String {
|
public static var userSessionUnverified: String {
|
||||||
return VectorL10n.tr("Vector", "user_session_unverified")
|
return VectorL10n.tr("Vector", "user_session_unverified")
|
||||||
|
|
|
@ -496,12 +496,12 @@ class CallPresenter: NSObject {
|
||||||
#if canImport(JitsiMeetSDK)
|
#if canImport(JitsiMeetSDK)
|
||||||
JMCallKitProxy.removeListener(self)
|
JMCallKitProxy.removeListener(self)
|
||||||
|
|
||||||
guard let session = sessions.first else {
|
guard let sessionInfo = sessions.first else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let widgetEventsListener = widgetEventsListener {
|
if let widgetEventsListener = widgetEventsListener {
|
||||||
session.removeListener(widgetEventsListener)
|
sessionInfo.removeListener(widgetEventsListener)
|
||||||
}
|
}
|
||||||
widgetEventsListener = nil
|
widgetEventsListener = nil
|
||||||
#endif
|
#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)
|
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
|
// MARK: Calls
|
||||||
|
|
||||||
/// Indicate if `allowStunServerFallback` settings has been set once.
|
/// Indicate if `allowStunServerFallback` settings has been set once.
|
||||||
|
|
|
@ -370,6 +370,7 @@ class AllChatsCoordinator: NSObject, SplitViewMasterCoordinatorProtocol {
|
||||||
button.menu = menu
|
button.menu = menu
|
||||||
button.showsMenuAsPrimaryAction = true
|
button.showsMenuAsPrimaryAction = true
|
||||||
button.autoresizingMask = [.flexibleHeight, .flexibleWidth]
|
button.autoresizingMask = [.flexibleHeight, .flexibleWidth]
|
||||||
|
button.accessibilityLabel = VectorL10n.allChatsUserMenuAccessibilityLabel
|
||||||
view.addSubview(button)
|
view.addSubview(button)
|
||||||
self.avatarMenuButton = button
|
self.avatarMenuButton = button
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,23 @@ class AllChatsViewController: HomeViewController {
|
||||||
|
|
||||||
private var currentAlert: UIAlertController?
|
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
|
// MARK: - SplitViewMasterViewControllerProtocol
|
||||||
|
|
||||||
// References on the currently selected room
|
// References on the currently selected room
|
||||||
|
@ -91,6 +108,8 @@ class AllChatsViewController: HomeViewController {
|
||||||
|
|
||||||
// Tell whether the onboarding screen is preparing.
|
// Tell whether the onboarding screen is preparing.
|
||||||
private(set) var isOnboardingInProgress: Bool = false
|
private(set) var isOnboardingInProgress: Bool = false
|
||||||
|
|
||||||
|
private var toolbarHeight: CGFloat = 0
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
|
@ -107,6 +126,8 @@ class AllChatsViewController: HomeViewController {
|
||||||
recentsTableView.register(RecentsInvitesTableViewCell.nib, forCellReuseIdentifier: RecentsInvitesTableViewCell.reuseIdentifier)
|
recentsTableView.register(RecentsInvitesTableViewCell.nib, forCellReuseIdentifier: RecentsInvitesTableViewCell.reuseIdentifier)
|
||||||
recentsTableView.contentInsetAdjustmentBehavior = .automatic
|
recentsTableView.contentInsetAdjustmentBehavior = .automatic
|
||||||
|
|
||||||
|
toolbarHeight = toolbar.frame.height
|
||||||
|
|
||||||
updateUI()
|
updateUI()
|
||||||
|
|
||||||
navigationItem.largeTitleDisplayMode = .automatic
|
navigationItem.largeTitleDisplayMode = .automatic
|
||||||
|
@ -122,8 +143,7 @@ class AllChatsViewController: HomeViewController {
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
self.navigationController?.isToolbarHidden = false
|
self.toolbar.tintColor = ThemeService.shared().theme.colors.accent
|
||||||
self.navigationController?.toolbar.tintColor = ThemeService.shared().theme.colors.accent
|
|
||||||
if self.navigationItem.searchController == nil {
|
if self.navigationItem.searchController == nil {
|
||||||
self.navigationItem.searchController = searchController
|
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) {
|
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||||
super.viewWillTransition(to: size, with: coordinator)
|
super.viewWillTransition(to: size, with: coordinator)
|
||||||
|
|
||||||
|
@ -372,8 +386,8 @@ class AllChatsViewController: HomeViewController {
|
||||||
|
|
||||||
let scrollPosition = scrollPosition(of: scrollView)
|
let scrollPosition = scrollPosition(of: scrollView)
|
||||||
|
|
||||||
if !self.recentsTableView.isDragging && scrollPosition == 0 && self.navigationController?.isToolbarHidden == true {
|
if !self.recentsTableView.isDragging && scrollPosition == 0 && self.isToolbarHidden == true {
|
||||||
self.navigationController?.setToolbarHidden(false, animated: true)
|
self.setToolbarHidden(false, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard self.recentsTableView.isDragging else {
|
guard self.recentsTableView.isDragging else {
|
||||||
|
@ -385,8 +399,8 @@ class AllChatsViewController: HomeViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
let isToolBarHidden: Bool = scrollPosition - initialScrollPosition > 0
|
let isToolBarHidden: Bool = scrollPosition - initialScrollPosition > 0
|
||||||
if isToolBarHidden != self.navigationController?.isToolbarHidden {
|
if isToolBarHidden != self.isToolbarHidden {
|
||||||
self.navigationController?.setToolbarHidden(isToolBarHidden, animated: true)
|
self.setToolbarHidden(isToolBarHidden, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -494,13 +508,21 @@ class AllChatsViewController: HomeViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateToolbar(with menu: UIMenu) {
|
private func updateToolbar(with menu: UIMenu) {
|
||||||
self.navigationController?.isToolbarHidden = false
|
guard isViewLoaded else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isToolbarHidden = false
|
||||||
self.update(with: ThemeService.shared().theme)
|
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.flexibleSpace(),
|
||||||
UIBarButtonItem(image: Asset.Images.allChatsEditIcon.image, menu: menu)
|
UIBarButtonItem(image: Asset.Images.allChatsEditIcon.image, menu: menu)
|
||||||
], animated: true)
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showCreateSpace(parentSpaceId: String?) {
|
private func showCreateSpace(parentSpaceId: String?) {
|
||||||
|
|
|
@ -11,8 +11,7 @@
|
||||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AllChatsViewController" customModule="Element" customModuleProvider="target">
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AllChatsViewController" customModule="Element" customModuleProvider="target">
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="recentsTableView" destination="orV-HH-88x" id="lgA-2k-pXJ"/>
|
<outlet property="recentsTableView" destination="orV-HH-88x" id="lgA-2k-pXJ"/>
|
||||||
<outlet property="stickyHeadersBottomContainer" destination="EXH-mK-0eB" id="95Y-KP-bwF"/>
|
<outlet property="toolbar" destination="osO-1z-CZg" id="xRm-cJ-bNi"/>
|
||||||
<outlet property="stickyHeadersBottomContainerHeightConstraint" destination="SNq-Js-N7s" id="vom-iM-s6W"/>
|
|
||||||
<outlet property="view" destination="iN0-l3-epB" id="NUQ-LI-M61"/>
|
<outlet property="view" destination="iN0-l3-epB" id="NUQ-LI-M61"/>
|
||||||
</connections>
|
</connections>
|
||||||
</placeholder>
|
</placeholder>
|
||||||
|
@ -25,25 +24,24 @@
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||||
</tableView>
|
</tableView>
|
||||||
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="EXH-mK-0eB">
|
<toolbar opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="osO-1z-CZg">
|
||||||
<rect key="frame" x="0.0" y="667" width="375" height="0.0"/>
|
<rect key="frame" x="0.0" y="623" width="375" height="44"/>
|
||||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
|
||||||
<accessibility key="accessibilityConfiguration" identifier="RecentsVCStickyHeadersBottomContainer"/>
|
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" id="SNq-Js-N7s"/>
|
<constraint firstAttribute="height" constant="44" id="7k8-Ot-vpH"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
<items/>
|
||||||
|
</toolbar>
|
||||||
</subviews>
|
</subviews>
|
||||||
<viewLayoutGuide key="safeArea" id="4qf-KS-Fc9"/>
|
<viewLayoutGuide key="safeArea" id="4qf-KS-Fc9"/>
|
||||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="trailing" secondItem="orV-HH-88x" secondAttribute="trailing" id="3Np-64-AUe"/>
|
<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="osO-1z-CZg" firstAttribute="trailing" secondItem="4qf-KS-Fc9" secondAttribute="trailing" id="T2o-XM-BgN"/>
|
||||||
<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="orV-HH-88x" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="dTn-zC-Axs"/>
|
<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="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>
|
</constraints>
|
||||||
<point key="canvasLocation" x="140" y="137.18140929535232"/>
|
<point key="canvasLocation" x="140" y="137.18140929535232"/>
|
||||||
</view>
|
</view>
|
||||||
|
|
|
@ -209,6 +209,7 @@ extension UserVerificationCoordinator: KeyVerificationManuallyVerifyCoordinatorD
|
||||||
self.presenter.toPresentable().dismiss(animated: true) {
|
self.presenter.toPresentable().dismiss(animated: true) {
|
||||||
self.remove(childCoordinator: coordinator)
|
self.remove(childCoordinator: coordinator)
|
||||||
}
|
}
|
||||||
|
delegate?.userVerificationCoordinatorDidComplete(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func keyVerificationManuallyVerifyCoordinatorDidCancel(_ coordinator: KeyVerificationManuallyVerifyCoordinatorType) {
|
func keyVerificationManuallyVerifyCoordinatorDidCancel(_ coordinator: KeyVerificationManuallyVerifyCoordinatorType) {
|
||||||
|
|
|
@ -77,8 +77,14 @@ final class UserVerificationCoordinatorBridgePresenter: NSObject {
|
||||||
} else {
|
} else {
|
||||||
userVerificationCoordinator = UserVerificationCoordinator(presenter: self.presenter, session: self.session, userId: self.userId, userDisplayName: self.userDisplayName)
|
userVerificationCoordinator = UserVerificationCoordinator(presenter: self.presenter, session: self.session, userId: self.userId, userDisplayName: self.userDisplayName)
|
||||||
}
|
}
|
||||||
|
userVerificationCoordinator.delegate = self
|
||||||
userVerificationCoordinator.start()
|
userVerificationCoordinator.start()
|
||||||
self.coordinator = userVerificationCoordinator
|
self.coordinator = userVerificationCoordinator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension UserVerificationCoordinatorBridgePresenter: UserVerificationCoordinatorDelegate {
|
||||||
|
func userVerificationCoordinatorDidComplete(_ coordinator: UserVerificationCoordinatorType) {
|
||||||
|
delegate?.userVerificationCoordinatorBridgePresenterDelegateDidComplete(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -920,7 +920,7 @@
|
||||||
{
|
{
|
||||||
for (MXKRoomBubbleComponent *component in bubbleComponents)
|
for (MXKRoomBubbleComponent *component in bubbleComponents)
|
||||||
{
|
{
|
||||||
if (component.showEncryptionBadge)
|
if (component.encryptionDecoration != EventEncryptionDecorationNone)
|
||||||
{
|
{
|
||||||
containsBubbleComponentWithEncryptionBadge = YES;
|
containsBubbleComponentWithEncryptionBadge = YES;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
#import "MXKEventFormatter.h"
|
#import "MXKEventFormatter.h"
|
||||||
#import "MXKURLPreviewDataProtocol.h"
|
#import "MXKURLPreviewDataProtocol.h"
|
||||||
|
#import "EventEncryptionDecoration.h"
|
||||||
|
|
||||||
@protocol MXThreadProtocol;
|
@protocol MXThreadProtocol;
|
||||||
|
|
||||||
|
@ -101,9 +102,9 @@ typedef enum : NSUInteger {
|
||||||
@property (nonatomic) MXEventScan *eventScan;
|
@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.
|
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];
|
[self updateLinkWithRoomState:roomState];
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@
|
||||||
andLatestRoomState:latestRoomState
|
andLatestRoomState:latestRoomState
|
||||||
error:&error];
|
error:&error];
|
||||||
|
|
||||||
_showEncryptionBadge = [self shouldShowWarningBadgeForEvent:event roomState:roomState session:session];
|
_encryptionDecoration = [self encryptionDecorationForEvent:event roomState:roomState session:session];
|
||||||
|
|
||||||
[self updateLinkWithRoomState:roomState];
|
[self updateLinkWithRoomState:roomState];
|
||||||
}
|
}
|
||||||
|
@ -167,24 +167,24 @@
|
||||||
self.link = url;
|
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
|
// Warning badges are unnecessary in unencrypted rooms
|
||||||
if (!roomState.isEncrypted)
|
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.
|
// 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)
|
if (event.eventType != MXEventTypeRoomMessage && !event.isMediaAttachment)
|
||||||
{
|
{
|
||||||
return NO;
|
return EventEncryptionDecorationNone;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always show a warning badge if there was a decryption error.
|
// Always show a warning badge if there was a decryption error.
|
||||||
if (event.decryptionError)
|
if (event.decryptionError)
|
||||||
{
|
{
|
||||||
return YES;
|
return EventEncryptionDecorationDecryptionError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unencrypted message events should show a warning unless they're pending local echoes
|
// Unencrypted message events should show a warning unless they're pending local echoes
|
||||||
|
@ -193,10 +193,10 @@
|
||||||
if (event.isLocalEvent
|
if (event.isLocalEvent
|
||||||
|| event.contentHasBeenEdited) // Local echo for an edit is clear but uses a true event id, the one of the edited event
|
|| 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.
|
// The encryption is in a good state.
|
||||||
|
@ -208,12 +208,17 @@
|
||||||
|
|
||||||
if (userTrustLevel.isVerified && !deviceInfo.trustLevel.isVerified)
|
if (userTrustLevel.isVerified && !deviceInfo.trustLevel.isVerified)
|
||||||
{
|
{
|
||||||
return YES;
|
return EventEncryptionDecorationUntrustedDevice;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.isUntrusted)
|
||||||
|
{
|
||||||
|
return EventEncryptionDecorationUnsafeKey;
|
||||||
|
}
|
||||||
|
|
||||||
// Everything was fine
|
// Everything was fine
|
||||||
return NO;
|
return EventEncryptionDecorationNone;
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -192,7 +192,7 @@ static NSAttributedString *verticalWhitespace = nil;
|
||||||
NSString *claimedKey = _mxEvent.keysClaimed[@"ed25519"];
|
NSString *claimedKey = _mxEvent.keysClaimed[@"ed25519"];
|
||||||
NSString *algorithm = _mxEvent.wireContent[@"algorithm"];
|
NSString *algorithm = _mxEvent.wireContent[@"algorithm"];
|
||||||
NSString *sessionId = _mxEvent.wireContent[@"session_id"];
|
NSString *sessionId = _mxEvent.wireContent[@"session_id"];
|
||||||
NSString *untrusted = _mxEvent.isUntrusted ? [VectorL10n userVerificationSessionsListSessionUntrusted] : [VectorL10n userVerificationSessionsListSessionTrusted];
|
NSString *untrusted = _mxEvent.isUntrusted ? [VectorL10n roomEventEncryptionInfoKeyAuthenticityNotGuaranteed] : [VectorL10n userVerificationSessionsListSessionTrusted];
|
||||||
|
|
||||||
NSString *decryptionError;
|
NSString *decryptionError;
|
||||||
if (_mxEvent.decryptionError)
|
if (_mxEvent.decryptionError)
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
#define TABLEVIEW_ROW_CELL_HEIGHT 46
|
#define TABLEVIEW_ROW_CELL_HEIGHT 46
|
||||||
#define TABLEVIEW_SECTION_HEADER_HEIGHT 28
|
#define TABLEVIEW_SECTION_HEADER_HEIGHT 28
|
||||||
|
|
||||||
@interface RoomMemberDetailsViewController () <UIGestureRecognizerDelegate, DeviceTableViewCellDelegate, RoomMemberTitleViewDelegate, KeyVerificationCoordinatorBridgePresenterDelegate>
|
@interface RoomMemberDetailsViewController () <UIGestureRecognizerDelegate, DeviceTableViewCellDelegate, RoomMemberTitleViewDelegate, KeyVerificationCoordinatorBridgePresenterDelegate, UserVerificationCoordinatorBridgePresenterDelegate>
|
||||||
{
|
{
|
||||||
RoomMemberTitleView* memberTitleView;
|
RoomMemberTitleView* memberTitleView;
|
||||||
|
|
||||||
|
@ -449,6 +449,7 @@
|
||||||
session:self.mxRoom.mxSession
|
session:self.mxRoom.mxSession
|
||||||
userId:self.mxRoomMember.userId
|
userId:self.mxRoomMember.userId
|
||||||
userDisplayName:self.mxRoomMember.displayname];
|
userDisplayName:self.mxRoomMember.displayname];
|
||||||
|
userVerificationCoordinatorBridgePresenter.delegate = self;
|
||||||
[userVerificationCoordinatorBridgePresenter start];
|
[userVerificationCoordinatorBridgePresenter start];
|
||||||
self.userVerificationCoordinatorBridgePresenter = userVerificationCoordinatorBridgePresenter;
|
self.userVerificationCoordinatorBridgePresenter = userVerificationCoordinatorBridgePresenter;
|
||||||
}
|
}
|
||||||
|
@ -1345,4 +1346,11 @@
|
||||||
keyVerificationCoordinatorBridgePresenter = nil;
|
keyVerificationCoordinatorBridgePresenter = nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - UserVerificationCoordinatorBridgePresenterDelegate
|
||||||
|
|
||||||
|
- (void)userVerificationCoordinatorBridgePresenterDelegateDidComplete:(UserVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
||||||
|
{
|
||||||
|
[self refreshUserEncryptionTrustLevel];
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@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
|
+ (UIImage*)encryptionIconForBubbleComponent:(MXKRoomBubbleComponent *)bubbleComponent
|
||||||
{
|
{
|
||||||
if (!bubbleComponent.showEncryptionBadge)
|
switch (bubbleComponent.encryptionDecoration) {
|
||||||
{
|
case EventEncryptionDecorationNone:
|
||||||
return nil;
|
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
|
+ (void)addEncryptionStatusFromBubbleData:(MXKRoomBubbleCellData *)bubbleData inContainerView:(UIView *)containerView
|
||||||
|
|
|
@ -71,7 +71,15 @@ final class PinCodePreferences: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
var isBiometricsAvailable: Bool {
|
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
|
/// Allowed number of PIN trials before showing forgot help alert
|
||||||
|
|
|
@ -45,7 +45,7 @@ enum {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@interface ManageSessionViewController ()
|
@interface ManageSessionViewController () <UserVerificationCoordinatorBridgePresenterDelegate>
|
||||||
{
|
{
|
||||||
// The device to display
|
// The device to display
|
||||||
MXDevice *device;
|
MXDevice *device;
|
||||||
|
@ -649,6 +649,7 @@ enum {
|
||||||
userId:self.mainSession.myUser.userId
|
userId:self.mainSession.myUser.userId
|
||||||
userDisplayName:nil
|
userDisplayName:nil
|
||||||
deviceId:device.deviceId];
|
deviceId:device.deviceId];
|
||||||
|
userVerificationCoordinatorBridgePresenter.delegate = self;
|
||||||
[userVerificationCoordinatorBridgePresenter start];
|
[userVerificationCoordinatorBridgePresenter start];
|
||||||
self.userVerificationCoordinatorBridgePresenter = userVerificationCoordinatorBridgePresenter;
|
self.userVerificationCoordinatorBridgePresenter = userVerificationCoordinatorBridgePresenter;
|
||||||
}
|
}
|
||||||
|
@ -701,4 +702,11 @@ enum {
|
||||||
self.reauthenticationCoordinatorBridgePresenter = reauthenticationPresenter;
|
self.reauthenticationCoordinatorBridgePresenter = reauthenticationPresenter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma mark - UserVerificationCoordinatorBridgePresenterDelegate
|
||||||
|
|
||||||
|
- (void)userVerificationCoordinatorBridgePresenterDelegateDidComplete:(UserVerificationCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
||||||
|
{
|
||||||
|
[self reloadDeviceWithCompletion:^{}];
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -172,7 +172,9 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE)
|
||||||
LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0,
|
LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0,
|
||||||
LABS_ENABLE_THREADS_INDEX,
|
LABS_ENABLE_THREADS_INDEX,
|
||||||
LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS,
|
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)
|
typedef NS_ENUM(NSUInteger, SECURITY)
|
||||||
|
@ -403,7 +405,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
|
||||||
Section *sectionSecurity = [Section sectionWithTag:SECTION_TAG_SECURITY];
|
Section *sectionSecurity = [Section sectionWithTag:SECTION_TAG_SECURITY];
|
||||||
[sectionSecurity addRowWithTag:SECURITY_BUTTON_INDEX];
|
[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
|
// NOTE: Add device manager entry point in the security section atm for debug purpose
|
||||||
[sectionSecurity addRowWithTag:DEVICE_MANAGER_INDEX];
|
[sectionSecurity addRowWithTag:DEVICE_MANAGER_INDEX];
|
||||||
|
@ -595,6 +597,8 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
|
||||||
{
|
{
|
||||||
[sectionLabs addRowWithTag:LABS_ENABLE_LIVE_LOCATION_SHARING];
|
[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];
|
sectionLabs.headerTitle = [VectorL10n settingsLabs];
|
||||||
if (sectionLabs.hasAnyRows)
|
if (sectionLabs.hasAnyRows)
|
||||||
{
|
{
|
||||||
|
@ -2532,6 +2536,30 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
|
||||||
{
|
{
|
||||||
cell = [self buildLiveLocationSharingCellForTableView:tableView atIndexPath:indexPath];
|
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)
|
else if (section == SECTION_TAG_SECURITY)
|
||||||
{
|
{
|
||||||
|
@ -3277,6 +3305,19 @@ ChangePasswordCoordinatorBridgePresenterDelegate>
|
||||||
[[AppDelegate theDelegate] restoreEmptyDetailsViewController];
|
[[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
|
- (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender
|
||||||
{
|
{
|
||||||
RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn;
|
RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn;
|
||||||
|
|
|
@ -461,8 +461,8 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm";
|
||||||
{
|
{
|
||||||
calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
|
calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
|
||||||
|
|
||||||
// Use the secondary bg color to set the background color in the default CSS.
|
// Use the selected bg color to set the code block background color in the default CSS.
|
||||||
NSUInteger bgColor = [MXKTools rgbValueWithColor:ThemeService.shared.theme.headerBackgroundColor];
|
NSUInteger bgColor = [MXKTools rgbValueWithColor:ThemeService.shared.theme.selectedBackgroundColor];
|
||||||
self.defaultCSS = [NSString stringWithFormat:@" \
|
self.defaultCSS = [NSString stringWithFormat:@" \
|
||||||
pre,code { \
|
pre,code { \
|
||||||
background-color: #%06lX; \
|
background-color: #%06lX; \
|
||||||
|
|
|
@ -24,6 +24,9 @@ schemes:
|
||||||
disableMainThreadChecker: true
|
disableMainThreadChecker: true
|
||||||
targets:
|
targets:
|
||||||
- RiotTests
|
- RiotTests
|
||||||
|
gatherCoverageData: true
|
||||||
|
coverageTargets:
|
||||||
|
- Riot
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
Riot:
|
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
|
return dateFormatter
|
||||||
}()
|
}()
|
||||||
|
|
||||||
func lastActivityDateString(from lastActivityTimestamp: TimeInterval) -> String {
|
static func lastActivityDateString(from lastActivityTimestamp: TimeInterval) -> String {
|
||||||
let date = Date(timeIntervalSince1970: lastActivityTimestamp)
|
let date = Date(timeIntervalSince1970: lastActivityTimestamp)
|
||||||
|
|
||||||
return InactiveUserSessionLastActivityFormatter.dateFormatter.string(from: date)
|
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
|
/// Last time the session was active
|
||||||
let lastSeenTimestamp: TimeInterval?
|
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
|
/// True to indicate that session has been used under `inactiveSessionDurationTreshold` value
|
||||||
let isActive: Bool
|
let isActive: Bool
|
||||||
|
|
||||||
/// True to indicate that this is current user session
|
/// True to indicate that this is current user session
|
||||||
let isCurrent: Bool
|
let isCurrent: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UserSessionInfo: Equatable {
|
extension UserSessionInfo: Equatable {
|
||||||
static func == (lhs: UserSessionInfo, rhs: UserSessionInfo) -> Bool {
|
static func == (lhs: UserSessionInfo, rhs: UserSessionInfo) -> Bool {
|
||||||
return lhs.id == rhs.id
|
lhs.id == rhs.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Enables to build last activity date string
|
/// Enables to build last activity date string
|
||||||
class UserSessionLastActivityFormatter {
|
enum UserSessionLastActivityFormatter {
|
||||||
private static var lastActivityDateFormatter: DateFormatter = {
|
private static var lastActivityDateFormatter: DateFormatter = {
|
||||||
let dateFormatter = DateFormatter()
|
let dateFormatter = DateFormatter()
|
||||||
dateFormatter.locale = Locale.current
|
dateFormatter.locale = Locale.current
|
||||||
|
@ -28,9 +28,9 @@ class UserSessionLastActivityFormatter {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
/// Session last activity string
|
/// Session last activity string
|
||||||
func lastActivityDateString(from lastActivityTimestamp: TimeInterval) -> String {
|
static func lastActivityDateString(from lastActivityTimestamp: TimeInterval) -> String {
|
||||||
let date = Date(timeIntervalSince1970: lastActivityTimestamp)
|
let date = Date(timeIntervalSince1970: lastActivityTimestamp)
|
||||||
|
|
||||||
return UserSessionLastActivityFormatter.lastActivityDateFormatter.string(from: date)
|
return Self.lastActivityDateFormatter.string(from: date)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,9 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Enables to build user session name
|
/// Enables to build user session name
|
||||||
class UserSessionNameFormatter {
|
enum UserSessionNameFormatter {
|
||||||
/// Session name with client name and session display name
|
/// 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 sessionName: String
|
||||||
|
|
||||||
let clientName = deviceType.name
|
let clientName = deviceType.name
|
||||||
|
|
|
@ -109,12 +109,14 @@ struct UserSessionCardView: View {
|
||||||
.buttonStyle(PrimaryActionButtonStyle())
|
.buttonStyle(PrimaryActionButtonStyle())
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
.padding(.bottom, 3)
|
.padding(.bottom, 3)
|
||||||
|
.accessibilityIdentifier("userSessionCardVerifyButton")
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewData.isCurrentSessionDisplayMode {
|
if viewData.isCurrentSessionDisplayMode {
|
||||||
Text(VectorL10n.userSessionViewDetails)
|
Text(VectorL10n.userSessionViewDetails)
|
||||||
.font(theme.fonts.body)
|
.font(theme.fonts.body)
|
||||||
.foregroundColor(theme.colors.accent)
|
.foregroundColor(theme.colors.accent)
|
||||||
|
.accessibilityIdentifier("userSessionCardViewDetails")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(24)
|
.padding(24)
|
||||||
|
@ -134,18 +136,24 @@ struct UserSessionCardViewPreview: View {
|
||||||
@Environment(\.theme) var theme: ThemeSwiftUI
|
@Environment(\.theme) var theme: ThemeSwiftUI
|
||||||
|
|
||||||
let viewData: UserSessionCardViewData
|
let viewData: UserSessionCardViewData
|
||||||
|
|
||||||
init(isCurrentSessionInfo: Bool = false) {
|
init(isCurrent: Bool = false) {
|
||||||
let session = UserSessionInfo(id: "alice",
|
let sessionInfo = UserSessionInfo(id: "alice",
|
||||||
name: "iOS",
|
name: "iOS",
|
||||||
deviceType: .mobile,
|
deviceType: .mobile,
|
||||||
isVerified: false,
|
isVerified: false,
|
||||||
lastSeenIP: "10.0.0.10",
|
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,
|
isActive: true,
|
||||||
isCurrent: isCurrentSessionInfo)
|
isCurrent: isCurrent)
|
||||||
|
viewData = UserSessionCardViewData(sessionInfo: sessionInfo)
|
||||||
viewData = UserSessionCardViewData(session: session)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -161,8 +169,8 @@ struct UserSessionCardViewPreview: View {
|
||||||
struct UserSessionCardView_Previews: PreviewProvider {
|
struct UserSessionCardView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
Group {
|
Group {
|
||||||
UserSessionCardViewPreview(isCurrentSessionInfo: true).theme(.light).preferredColorScheme(.light)
|
UserSessionCardViewPreview(isCurrent: true).theme(.light).preferredColorScheme(.light)
|
||||||
UserSessionCardViewPreview(isCurrentSessionInfo: true).theme(.dark).preferredColorScheme(.dark)
|
UserSessionCardViewPreview(isCurrent: true).theme(.dark).preferredColorScheme(.dark)
|
||||||
UserSessionCardViewPreview().theme(.light).preferredColorScheme(.light)
|
UserSessionCardViewPreview().theme(.light).preferredColorScheme(.light)
|
||||||
UserSessionCardViewPreview().theme(.dark).preferredColorScheme(.dark)
|
UserSessionCardViewPreview().theme(.dark).preferredColorScheme(.dark)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,6 @@ import Foundation
|
||||||
|
|
||||||
/// View data for UserSessionCardView
|
/// View data for UserSessionCardView
|
||||||
struct UserSessionCardViewData {
|
struct UserSessionCardViewData {
|
||||||
private static let userSessionNameFormatter = UserSessionNameFormatter()
|
|
||||||
private static let lastActivityDateFormatter = UserSessionLastActivityFormatter()
|
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
sessionId
|
sessionId
|
||||||
}
|
}
|
||||||
|
@ -48,13 +45,13 @@ struct UserSessionCardViewData {
|
||||||
lastSeenIP: String?,
|
lastSeenIP: String?,
|
||||||
isCurrentSessionDisplayMode: Bool = false) {
|
isCurrentSessionDisplayMode: Bool = false) {
|
||||||
self.sessionId = sessionId
|
self.sessionId = sessionId
|
||||||
sessionName = Self.userSessionNameFormatter.sessionName(deviceType: deviceType, sessionDisplayName: sessionDisplayName)
|
sessionName = UserSessionNameFormatter.sessionName(deviceType: deviceType, sessionDisplayName: sessionDisplayName)
|
||||||
self.isVerified = isVerified
|
self.isVerified = isVerified
|
||||||
|
|
||||||
var lastActivityDateString: String?
|
var lastActivityDateString: String?
|
||||||
|
|
||||||
if let lastActivityTimestamp = lastActivityTimestamp {
|
if let lastActivityTimestamp = lastActivityTimestamp {
|
||||||
lastActivityDateString = Self.lastActivityDateFormatter.lastActivityDateString(from: lastActivityTimestamp)
|
lastActivityDateString = UserSessionLastActivityFormatter.lastActivityDateString(from: lastActivityTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.lastActivityDateString = lastActivityDateString
|
self.lastActivityDateString = lastActivityDateString
|
||||||
|
@ -66,13 +63,13 @@ struct UserSessionCardViewData {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UserSessionCardViewData {
|
extension UserSessionCardViewData {
|
||||||
init(session: UserSessionInfo) {
|
init(sessionInfo: UserSessionInfo) {
|
||||||
self.init(sessionId: session.id,
|
self.init(sessionId: sessionInfo.id,
|
||||||
sessionDisplayName: session.name,
|
sessionDisplayName: sessionInfo.name,
|
||||||
deviceType: session.deviceType,
|
deviceType: sessionInfo.deviceType,
|
||||||
isVerified: session.isVerified,
|
isVerified: sessionInfo.isVerified,
|
||||||
lastActivityTimestamp: session.lastSeenTimestamp,
|
lastActivityTimestamp: sessionInfo.lastSeenTimestamp,
|
||||||
lastSeenIP: session.lastSeenIP,
|
lastSeenIP: sessionInfo.lastSeenIP,
|
||||||
isCurrentSessionDisplayMode: session.isCurrent)
|
isCurrentSessionDisplayMode: sessionInfo.isCurrent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,58 +53,58 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable {
|
||||||
coordinator.completion = { [weak self] result in
|
coordinator.completion = { [weak self] result in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
switch result {
|
switch result {
|
||||||
case let .openSessionOverview(session: session):
|
case let .openSessionOverview(sessionInfo: sessionInfo):
|
||||||
self.openSessionOverview(session: session)
|
self.openSessionOverview(sessionsInfo: sessionInfo)
|
||||||
case let .openOtherSessions(sessions: sessions, filter: filter):
|
case let .openOtherSessions(sessionsInfo: sessionsInfo, filter: filter):
|
||||||
self.openOtherSessions(sessions: sessions, filterBy: filter, title: VectorL10n.userOtherSessionSecurityRecommendationTitle)
|
self.openOtherSessions(sessionsInfo: sessionsInfo, filterBy: filter, title: VectorL10n.userOtherSessionSecurityRecommendationTitle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return coordinator
|
return coordinator
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openSessionDetails(session: UserSessionInfo) {
|
private func openSessionDetails(sessionInfo: UserSessionInfo) {
|
||||||
let coordinator = createUserSessionDetailsCoordinator(session: session)
|
let coordinator = createUserSessionDetailsCoordinator(sessionInfo: sessionInfo)
|
||||||
pushScreen(with: coordinator)
|
pushScreen(with: coordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createUserSessionDetailsCoordinator(session: UserSessionInfo) -> UserSessionDetailsCoordinator {
|
private func createUserSessionDetailsCoordinator(sessionInfo: UserSessionInfo) -> UserSessionDetailsCoordinator {
|
||||||
let parameters = UserSessionDetailsCoordinatorParameters(session: session)
|
let parameters = UserSessionDetailsCoordinatorParameters(sessionInfo: sessionInfo)
|
||||||
return UserSessionDetailsCoordinator(parameters: parameters)
|
return UserSessionDetailsCoordinator(parameters: parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openSessionOverview(session: UserSessionInfo) {
|
private func openSessionOverview(sessionsInfo: UserSessionInfo) {
|
||||||
let coordinator = createUserSessionOverviewCoordinator(session: session)
|
let coordinator = createUserSessionOverviewCoordinator(sessionsInfo: sessionsInfo)
|
||||||
coordinator.completion = { [weak self] result in
|
coordinator.completion = { [weak self] result in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
switch result {
|
switch result {
|
||||||
case let .openSessionDetails(session: session):
|
case let .openSessionDetails(sessionInfo: session):
|
||||||
self.openSessionDetails(session: session)
|
self.openSessionDetails(sessionInfo: session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pushScreen(with: coordinator)
|
pushScreen(with: coordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createUserSessionOverviewCoordinator(session: UserSessionInfo) -> UserSessionOverviewCoordinator {
|
private func createUserSessionOverviewCoordinator(sessionsInfo: UserSessionInfo) -> UserSessionOverviewCoordinator {
|
||||||
let parameters = UserSessionOverviewCoordinatorParameters(session: session)
|
let parameters = UserSessionOverviewCoordinatorParameters(session: parameters.session, sessionInfo: sessionsInfo)
|
||||||
return UserSessionOverviewCoordinator(parameters: parameters)
|
return UserSessionOverviewCoordinator(parameters: parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openOtherSessions(sessions: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter, title: String) {
|
private func openOtherSessions(sessionsInfo: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter, title: String) {
|
||||||
let coordinator = createOtherSessionsCoordinator(sessions: sessions,
|
let coordinator = createOtherSessionsCoordinator(sessionsInfo: sessionsInfo,
|
||||||
filterBy: filter,
|
filterBy: filter,
|
||||||
title: title)
|
title: title)
|
||||||
coordinator.completion = { [weak self] result in
|
coordinator.completion = { [weak self] result in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
switch result {
|
switch result {
|
||||||
case let .openSessionDetails(session: session):
|
case let .openSessionDetails(sessionInfo: session):
|
||||||
self.openSessionDetails(session: session)
|
self.openSessionDetails(sessionInfo: session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pushScreen(with: coordinator)
|
pushScreen(with: coordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createOtherSessionsCoordinator(sessions: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter, title: String) -> UserOtherSessionsCoordinator {
|
private func createOtherSessionsCoordinator(sessionsInfo: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter, title: String) -> UserOtherSessionsCoordinator {
|
||||||
let parameters = UserOtherSessionsCoordinatorParameters(sessions: sessions,
|
let parameters = UserOtherSessionsCoordinatorParameters(sessionsInfo: sessionsInfo,
|
||||||
filter: filter,
|
filter: filter,
|
||||||
title: title)
|
title: title)
|
||||||
return UserOtherSessionsCoordinator(parameters: parameters)
|
return UserOtherSessionsCoordinator(parameters: parameters)
|
||||||
|
|
|
@ -18,7 +18,7 @@ import CommonKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct UserOtherSessionsCoordinatorParameters {
|
struct UserOtherSessionsCoordinatorParameters {
|
||||||
let sessions: [UserSessionInfo]
|
let sessionsInfo: [UserSessionInfo]
|
||||||
let filter: OtherUserSessionsFilter
|
let filter: OtherUserSessionsFilter
|
||||||
let title: String
|
let title: String
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ final class UserOtherSessionsCoordinator: Coordinator, Presentable {
|
||||||
init(parameters: UserOtherSessionsCoordinatorParameters) {
|
init(parameters: UserOtherSessionsCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
let viewModel = UserOtherSessionsViewModel(sessions: parameters.sessions,
|
let viewModel = UserOtherSessionsViewModel(sessionsInfo: parameters.sessionsInfo,
|
||||||
filter: parameters.filter,
|
filter: parameters.filter,
|
||||||
title: parameters.title)
|
title: parameters.title)
|
||||||
let view = UserOtherSessions(viewModel: viewModel.context)
|
let view = UserOtherSessions(viewModel: viewModel.context)
|
||||||
|
@ -55,8 +55,8 @@ final class UserOtherSessionsCoordinator: Coordinator, Presentable {
|
||||||
userOtherSessionsViewModel.completion = { [weak self] result in
|
userOtherSessionsViewModel.completion = { [weak self] result in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
switch result {
|
switch result {
|
||||||
case let .showUserSessionOverview(session: session):
|
case let .showUserSessionOverview(sessionInfo: session):
|
||||||
self.completion?(.openSessionDetails(session: session))
|
self.completion?(.openSessionDetails(sessionInfo: session))
|
||||||
}
|
}
|
||||||
MXLog.debug("[UserOtherSessionsCoordinator] UserOtherSessionsViewModel did complete with result: \(result).")
|
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.
|
/// Generate the view struct for the screen state.
|
||||||
var screenView: ([Any], AnyView) {
|
var screenView: ([Any], AnyView) {
|
||||||
|
|
||||||
let viewModel = UserOtherSessionsViewModel(sessions: inactiveSessions(),
|
let viewModel = UserOtherSessionsViewModel(sessionsInfo: inactiveSessions(),
|
||||||
filter: .inactive,
|
filter: .inactive,
|
||||||
title: VectorL10n.userOtherSessionSecurityRecommendationTitle)
|
title: VectorL10n.userOtherSessionSecurityRecommendationTitle)
|
||||||
|
|
||||||
|
@ -53,12 +53,19 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func inactiveSessions() -> [UserSessionInfo] {
|
private func inactiveSessions() -> [UserSessionInfo] {
|
||||||
[UserSessionInfo(id: "alice",
|
[UserSessionInfo(id: "0",
|
||||||
name: "iOS",
|
name: "iOS",
|
||||||
deviceType: .mobile,
|
deviceType: .mobile,
|
||||||
isVerified: false,
|
isVerified: false,
|
||||||
lastSeenIP: "10.0.0.10",
|
lastSeenIP: "10.0.0.10",
|
||||||
lastSeenTimestamp: nil,
|
lastSeenTimestamp: nil,
|
||||||
|
applicationName: nil,
|
||||||
|
applicationVersion: nil,
|
||||||
|
applicationURL: nil,
|
||||||
|
deviceModel: nil,
|
||||||
|
deviceOS: nil,
|
||||||
|
lastSeenIPLocation: nil,
|
||||||
|
deviceName: "",
|
||||||
isActive: false,
|
isActive: false,
|
||||||
isCurrent: true),
|
isCurrent: true),
|
||||||
UserSessionInfo(id: "1",
|
UserSessionInfo(id: "1",
|
||||||
|
@ -67,6 +74,13 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
||||||
isVerified: true,
|
isVerified: true,
|
||||||
lastSeenIP: "1.0.0.1",
|
lastSeenIP: "1.0.0.1",
|
||||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 8000000,
|
lastSeenTimestamp: Date().timeIntervalSince1970 - 8000000,
|
||||||
|
applicationName: nil,
|
||||||
|
applicationVersion: nil,
|
||||||
|
applicationURL: nil,
|
||||||
|
deviceModel: nil,
|
||||||
|
deviceOS: nil,
|
||||||
|
lastSeenIPLocation: nil,
|
||||||
|
deviceName: "",
|
||||||
isActive: false,
|
isActive: false,
|
||||||
isCurrent: false),
|
isCurrent: false),
|
||||||
UserSessionInfo(id: "2",
|
UserSessionInfo(id: "2",
|
||||||
|
@ -75,6 +89,13 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
||||||
isVerified: true,
|
isVerified: true,
|
||||||
lastSeenIP: "2.0.0.2",
|
lastSeenIP: "2.0.0.2",
|
||||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 9000000,
|
lastSeenTimestamp: Date().timeIntervalSince1970 - 9000000,
|
||||||
|
applicationName: nil,
|
||||||
|
applicationVersion: nil,
|
||||||
|
applicationURL: nil,
|
||||||
|
deviceModel: nil,
|
||||||
|
deviceOS: nil,
|
||||||
|
lastSeenIPLocation: nil,
|
||||||
|
deviceName: "",
|
||||||
isActive: false,
|
isActive: false,
|
||||||
isCurrent: false),
|
isCurrent: false),
|
||||||
UserSessionInfo(id: "3",
|
UserSessionInfo(id: "3",
|
||||||
|
@ -83,6 +104,13 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
||||||
isVerified: false,
|
isVerified: false,
|
||||||
lastSeenIP: "3.0.0.3",
|
lastSeenIP: "3.0.0.3",
|
||||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10000000,
|
lastSeenTimestamp: Date().timeIntervalSince1970 - 10000000,
|
||||||
|
applicationName: nil,
|
||||||
|
applicationVersion: nil,
|
||||||
|
applicationURL: nil,
|
||||||
|
deviceModel: nil,
|
||||||
|
deviceOS: nil,
|
||||||
|
lastSeenIPLocation: nil,
|
||||||
|
deviceName: "",
|
||||||
isActive: false,
|
isActive: false,
|
||||||
isCurrent: false)]
|
isCurrent: false)]
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,13 +18,13 @@ import Foundation
|
||||||
|
|
||||||
// MARK: - Coordinator
|
// MARK: - Coordinator
|
||||||
enum UserOtherSessionsCoordinatorResult {
|
enum UserOtherSessionsCoordinatorResult {
|
||||||
case openSessionDetails(session: UserSessionInfo)
|
case openSessionDetails(sessionInfo: UserSessionInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: View model
|
// MARK: View model
|
||||||
|
|
||||||
enum UserOtherSessionsViewModelResult {
|
enum UserOtherSessionsViewModelResult {
|
||||||
case showUserSessionOverview(session: UserSessionInfo)
|
case showUserSessionOverview(sessionInfo: UserSessionInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: View
|
// MARK: View
|
||||||
|
|
|
@ -20,14 +20,14 @@ typealias UserOtherSessionsViewModelType = StateStoreViewModel<UserOtherSessions
|
||||||
|
|
||||||
class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessionsViewModelProtocol {
|
class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessionsViewModelProtocol {
|
||||||
var completion: ((UserOtherSessionsViewModelResult) -> Void)?
|
var completion: ((UserOtherSessionsViewModelResult) -> Void)?
|
||||||
private let sessions: [UserSessionInfo]
|
private let sessionsInfo: [UserSessionInfo]
|
||||||
|
|
||||||
init(sessions: [UserSessionInfo],
|
init(sessionsInfo: [UserSessionInfo],
|
||||||
filter: OtherUserSessionsFilter,
|
filter: OtherUserSessionsFilter,
|
||||||
title: String) {
|
title: String) {
|
||||||
self.sessions = sessions
|
self.sessionsInfo = sessionsInfo
|
||||||
super.init(initialViewState: UserOtherSessionsViewState(title: title, sections: []))
|
super.init(initialViewState: UserOtherSessionsViewState(title: title, sections: []))
|
||||||
updateViewState(sessions: sessions, filter: filter)
|
updateViewState(sessionsInfo: sessionsInfo, filter: filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
|
@ -35,30 +35,30 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi
|
||||||
override func process(viewAction: UserOtherSessionsViewAction) {
|
override func process(viewAction: UserOtherSessionsViewAction) {
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case let .userOtherSessionSelected(sessionId: sessionId):
|
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.")
|
assertionFailure("Session should exist in the array.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
completion?(.showUserSessionOverview(session: session))
|
completion?(.showUserSessionOverview(sessionInfo: session))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func updateViewState(sessions: [UserSessionInfo], filter: OtherUserSessionsFilter) {
|
private func updateViewState(sessionsInfo: [UserSessionInfo], filter: OtherUserSessionsFilter) {
|
||||||
let sectionItems = filterSessions(sessions: sessions, by: filter).asViewData()
|
let sectionItems = filterSessions(sessionsInfo: sessionsInfo, by: filter).asViewData()
|
||||||
let sectionHeader = createHeaderData(filter: filter)
|
let sectionHeader = createHeaderData(filter: filter)
|
||||||
state.sections = [.sessionItems(header: sectionHeader, items: sectionItems)]
|
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 {
|
switch filter {
|
||||||
case .all:
|
case .all:
|
||||||
return sessions.filter { !$0.isCurrent }
|
return sessionsInfo.filter { !$0.isCurrent }
|
||||||
case .inactive:
|
case .inactive:
|
||||||
return sessions.filter { !$0.isActive }
|
return sessionsInfo.filter { !$0.isActive }
|
||||||
case .unverified:
|
case .unverified:
|
||||||
return sessions.filter { !$0.isVerified }
|
return sessionsInfo.filter { !$0.isVerified }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ import CommonKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct UserSessionDetailsCoordinatorParameters {
|
struct UserSessionDetailsCoordinatorParameters {
|
||||||
let session: UserSessionInfo
|
let sessionInfo: UserSessionInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
final class UserSessionDetailsCoordinator: Coordinator, Presentable {
|
final class UserSessionDetailsCoordinator: Coordinator, Presentable {
|
||||||
|
@ -40,7 +40,7 @@ final class UserSessionDetailsCoordinator: Coordinator, Presentable {
|
||||||
init(parameters: UserSessionDetailsCoordinatorParameters) {
|
init(parameters: UserSessionDetailsCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
let viewModel = UserSessionDetailsViewModel(session: parameters.session)
|
let viewModel = UserSessionDetailsViewModel(session: parameters.sessionInfo)
|
||||||
let view = UserSessionDetails(viewModel: viewModel.context)
|
let view = UserSessionDetails(viewModel: viewModel.context)
|
||||||
userSessionDetailsViewModel = viewModel
|
userSessionDetailsViewModel = viewModel
|
||||||
userSessionDetailsHostingController = VectorHostingController(rootView: view)
|
userSessionDetailsHostingController = VectorHostingController(rootView: view)
|
||||||
|
|
|
@ -41,21 +41,35 @@ enum MockUserSessionDetailsScreenState: MockScreenState, CaseIterable {
|
||||||
let session: UserSessionInfo
|
let session: UserSessionInfo
|
||||||
switch self {
|
switch self {
|
||||||
case .allSections:
|
case .allSections:
|
||||||
session = UserSessionInfo(id: "session",
|
session = UserSessionInfo(id: "alice",
|
||||||
name: "iOS",
|
name: "iOS",
|
||||||
deviceType: .mobile,
|
deviceType: .mobile,
|
||||||
isVerified: false,
|
isVerified: false,
|
||||||
lastSeenIP: "10.0.0.10",
|
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,
|
isActive: true,
|
||||||
isCurrent: true)
|
isCurrent: true)
|
||||||
case .sessionSectionOnly:
|
case .sessionSectionOnly:
|
||||||
session = UserSessionInfo(id: "session",
|
session = UserSessionInfo(id: "3",
|
||||||
name: "iOS",
|
name: "Android",
|
||||||
deviceType: .mobile,
|
deviceType: .mobile,
|
||||||
isVerified: false,
|
isVerified: false,
|
||||||
lastSeenIP: nil,
|
lastSeenIP: "3.0.0.3",
|
||||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
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,
|
isActive: true,
|
||||||
isCurrent: false)
|
isCurrent: false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,17 +20,20 @@ import XCTest
|
||||||
|
|
||||||
class UserSessionDetailsViewModelTests: XCTestCase {
|
class UserSessionDetailsViewModelTests: XCTestCase {
|
||||||
func test_whenSessionNameAndLastSeenIPNil_viewStateCorrect() {
|
func test_whenSessionNameAndLastSeenIPNil_viewStateCorrect() {
|
||||||
let userSessionInfo = createUserSessionInfo(sessionId: "session",
|
let userSessionInfo = createUserSessionInfo(id: "session",
|
||||||
sessionName: nil,
|
name: nil,
|
||||||
lastSeenIP: nil)
|
lastSeenIP: nil)
|
||||||
|
|
||||||
|
let sessionItems = [
|
||||||
|
sessionIdItem(sessionId: "session")
|
||||||
|
]
|
||||||
|
|
||||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
let sections = [
|
||||||
sessionItems.append(sessionIdItem(sessionId: userSessionInfo.id))
|
UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||||
|
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||||
var sections = [UserSessionDetailsSectionViewData]()
|
items: sessionItems)
|
||||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
]
|
||||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
|
||||||
items: sessionItems))
|
|
||||||
let expectedModel = UserSessionDetailsViewState(sections: sections)
|
let expectedModel = UserSessionDetailsViewState(sections: sections)
|
||||||
let sut = UserSessionDetailsViewModel(session: userSessionInfo)
|
let sut = UserSessionDetailsViewModel(session: userSessionInfo)
|
||||||
|
|
||||||
|
@ -38,18 +41,20 @@ class UserSessionDetailsViewModelTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_whenSessionNameNotNilLastSeenIPNil_viewStateCorrect() {
|
func test_whenSessionNameNotNilLastSeenIPNil_viewStateCorrect() {
|
||||||
let userSessionInfo = createUserSessionInfo(sessionId: "session",
|
let userSessionInfo = createUserSessionInfo(id: "session",
|
||||||
sessionName: "session name",
|
name: "session name",
|
||||||
lastSeenIP: nil)
|
lastSeenIP: nil)
|
||||||
|
|
||||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
let sessionItems = [
|
||||||
sessionItems.append(sessionNameItem(sessionName: "session name"))
|
sessionNameItem(sessionName: "session name"),
|
||||||
sessionItems.append(sessionIdItem(sessionId: userSessionInfo.id))
|
sessionIdItem(sessionId: "session")
|
||||||
|
]
|
||||||
var sections = [UserSessionDetailsSectionViewData]()
|
|
||||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
let sections = [
|
||||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||||
items: sessionItems))
|
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||||
|
items: sessionItems)
|
||||||
|
]
|
||||||
|
|
||||||
let expectedModel = UserSessionDetailsViewState(sections: sections)
|
let expectedModel = UserSessionDetailsViewState(sections: sections)
|
||||||
let sut = UserSessionDetailsViewModel(session: userSessionInfo)
|
let sut = UserSessionDetailsViewModel(session: userSessionInfo)
|
||||||
|
@ -58,56 +63,98 @@ class UserSessionDetailsViewModelTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_whenUserSessionInfoContainsAllValues_viewStateCorrect() {
|
func test_whenUserSessionInfoContainsAllValues_viewStateCorrect() {
|
||||||
let userSessionInfo = createUserSessionInfo(sessionId: "session",
|
let userSessionInfo = createUserSessionInfo(id: "session",
|
||||||
sessionName: "session name",
|
name: "session name",
|
||||||
lastSeenIP: "0.0.0.0")
|
lastSeenIP: "0.0.0.0",
|
||||||
|
applicationName: "Element iOS",
|
||||||
|
applicationVersion: "1.0.0")
|
||||||
|
|
||||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
let sessionItems = [
|
||||||
sessionItems.append(sessionNameItem(sessionName: "session name"))
|
sessionNameItem(sessionName: "session name"),
|
||||||
sessionItems.append(sessionIdItem(sessionId: userSessionInfo.id))
|
sessionIdItem(sessionId: "session")
|
||||||
|
]
|
||||||
var sections = [UserSessionDetailsSectionViewData]()
|
let appItems = [
|
||||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
appNameItem(appName: "Element iOS"),
|
||||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
appVersionItem(appVersion: "1.0.0")
|
||||||
items: sessionItems))
|
]
|
||||||
|
let deviceItems = [
|
||||||
var deviceSectionItems = [UserSessionDetailsSectionItemViewData]()
|
ipAddressItem(ipAddress: "0.0.0.0")
|
||||||
deviceSectionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsDeviceIpAddress,
|
]
|
||||||
value: "0.0.0.0"))
|
|
||||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
|
let sections = [
|
||||||
footer: nil,
|
UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||||
items: deviceSectionItems))
|
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 expectedModel = UserSessionDetailsViewState(sections: sections)
|
||||||
let sut = UserSessionDetailsViewModel(session: userSessionInfo)
|
let sut = UserSessionDetailsViewModel(session: userSessionInfo)
|
||||||
|
|
||||||
XCTAssertEqual(sut.state, expectedModel)
|
XCTAssertEqual(sut.state, expectedModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
private func createUserSessionInfo(sessionId: String,
|
private func createUserSessionInfo(id: String,
|
||||||
sessionName: String?,
|
name: String?,
|
||||||
deviceType: DeviceType = .mobile,
|
deviceType: DeviceType = .mobile,
|
||||||
isVerified: Bool = false,
|
isVerified: Bool = false,
|
||||||
lastSeenIP: String?,
|
lastSeenIP: String?,
|
||||||
lastSeenTimestamp: TimeInterval = Date().timeIntervalSince1970,
|
lastSeenTimestamp: TimeInterval = Date().timeIntervalSince1970,
|
||||||
isCurrentSession: Bool = true) -> UserSessionInfo {
|
applicationName: String? = nil,
|
||||||
UserSessionInfo(id: sessionId,
|
applicationVersion: String? = nil,
|
||||||
name: sessionName,
|
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,
|
deviceType: deviceType,
|
||||||
isVerified: isVerified,
|
isVerified: isVerified,
|
||||||
lastSeenIP: lastSeenIP,
|
lastSeenIP: lastSeenIP,
|
||||||
lastSeenTimestamp: lastSeenTimestamp,
|
lastSeenTimestamp: lastSeenTimestamp,
|
||||||
isActive: true,
|
applicationName: applicationName,
|
||||||
isCurrent: isCurrentSession)
|
applicationVersion: applicationVersion,
|
||||||
|
applicationURL: applicationURL,
|
||||||
|
deviceModel: deviceModel,
|
||||||
|
deviceOS: deviceOS,
|
||||||
|
lastSeenIPLocation: lastSeenIPLocation,
|
||||||
|
deviceName: deviceName,
|
||||||
|
isActive: isActive,
|
||||||
|
isCurrent: isCurrent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sessionNameItem(sessionName: String) -> UserSessionDetailsSectionItemViewData {
|
private func sessionNameItem(sessionName: String) -> UserSessionDetailsSectionItemViewData {
|
||||||
UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsSessionName,
|
.init(title: VectorL10n.userSessionDetailsSessionName,
|
||||||
value: sessionName)
|
value: sessionName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sessionIdItem(sessionId: String) -> UserSessionDetailsSectionItemViewData {
|
private func sessionIdItem(sessionId: String) -> UserSessionDetailsSectionItemViewData {
|
||||||
UserSessionDetailsSectionItemViewData(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
|
.init(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
|
||||||
value: sessionId)
|
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) {
|
private func updateViewState(session: UserSessionInfo) {
|
||||||
var sections = [UserSessionDetailsSectionViewData]()
|
var sections = [UserSessionDetailsSectionViewData]()
|
||||||
|
|
||||||
sections.append(sessionSection(session: session))
|
sections.append(sessionSection(session: session))
|
||||||
|
|
||||||
|
if let applicationSection = applicationSection(session: session) {
|
||||||
|
sections.append(applicationSection)
|
||||||
|
}
|
||||||
|
|
||||||
if let deviceSection = deviceSection(session: session) {
|
if let deviceSection = deviceSection(session: session) {
|
||||||
sections.append(deviceSection)
|
sections.append(deviceSection)
|
||||||
|
@ -43,31 +47,68 @@ class UserSessionDetailsViewModel: UserSessionDetailsViewModelType, UserSessionD
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sessionSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData {
|
private func sessionSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData {
|
||||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
var sessionItems: [UserSessionDetailsSectionItemViewData] = []
|
||||||
|
|
||||||
if let sessionName = session.name {
|
if let sessionName = session.name {
|
||||||
sessionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsSessionName,
|
sessionItems.append(.init(title: VectorL10n.userSessionDetailsSessionName,
|
||||||
value: sessionName))
|
value: sessionName))
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
|
sessionItems.append(.init(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
|
||||||
value: session.id))
|
value: session.id))
|
||||||
|
|
||||||
return UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
return .init(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||||
items: sessionItems)
|
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? {
|
private func deviceSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData? {
|
||||||
var deviceSectionItems = [UserSessionDetailsSectionItemViewData]()
|
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 {
|
if let lastSeenIP = session.lastSeenIP {
|
||||||
deviceSectionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsDeviceIpAddress,
|
deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceIpAddress,
|
||||||
value: lastSeenIP))
|
value: lastSeenIP))
|
||||||
|
}
|
||||||
|
if let lastSeenIPLocation = session.lastSeenIPLocation {
|
||||||
|
deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceIpLocation,
|
||||||
|
value: lastSeenIPLocation))
|
||||||
}
|
}
|
||||||
if deviceSectionItems.count > 0 {
|
if deviceSectionItems.count > 0 {
|
||||||
return UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
|
return .init(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
|
||||||
footer: nil,
|
footer: nil,
|
||||||
items: deviceSectionItems)
|
items: deviceSectionItems)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,8 @@ import CommonKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct UserSessionOverviewCoordinatorParameters {
|
struct UserSessionOverviewCoordinatorParameters {
|
||||||
let session: UserSessionInfo
|
let session: MXSession
|
||||||
|
let sessionInfo: UserSessionInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
final class UserSessionOverviewCoordinator: Coordinator, Presentable {
|
final class UserSessionOverviewCoordinator: Coordinator, Presentable {
|
||||||
|
@ -40,7 +41,8 @@ final class UserSessionOverviewCoordinator: Coordinator, Presentable {
|
||||||
init(parameters: UserSessionOverviewCoordinatorParameters) {
|
init(parameters: UserSessionOverviewCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
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))
|
hostingController = VectorHostingController(rootView: UserSessionOverview(viewModel: viewModel.context))
|
||||||
|
|
||||||
|
@ -57,8 +59,8 @@ final class UserSessionOverviewCoordinator: Coordinator, Presentable {
|
||||||
switch result {
|
switch result {
|
||||||
case .verifyCurrentSession:
|
case .verifyCurrentSession:
|
||||||
break // TODO:
|
break // TODO:
|
||||||
case let .showSessionDetails(session: session):
|
case let .showSessionDetails(sessionInfo: sessionInfo):
|
||||||
self.completion?(.openSessionDetails(session: session))
|
self.completion?(.openSessionDetails(sessionInfo: sessionInfo))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,9 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable {
|
||||||
// mock that screen.
|
// mock that screen.
|
||||||
case currentSession
|
case currentSession
|
||||||
case otherSession
|
case otherSession
|
||||||
|
case sessionWithPushNotifications(enabled: Bool)
|
||||||
|
case remotelyTogglingPushersNotAvailable
|
||||||
|
|
||||||
/// The associated screen
|
/// The associated screen
|
||||||
var screenType: Any.Type {
|
var screenType: Any.Type {
|
||||||
UserSessionOverview.self
|
UserSessionOverview.self
|
||||||
|
@ -33,35 +35,89 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable {
|
||||||
|
|
||||||
/// A list of screen state definitions
|
/// A list of screen state definitions
|
||||||
static var allCases: [MockUserSessionOverviewScreenState] {
|
static var allCases: [MockUserSessionOverviewScreenState] {
|
||||||
[.currentSession, .otherSession]
|
[.currentSession,
|
||||||
|
.otherSession,
|
||||||
|
.sessionWithPushNotifications(enabled: true),
|
||||||
|
.sessionWithPushNotifications(enabled: false),
|
||||||
|
.remotelyTogglingPushersNotAvailable]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the view struct for the screen state.
|
/// Generate the view struct for the screen state.
|
||||||
var screenView: ([Any], AnyView) {
|
var screenView: ([Any], AnyView) {
|
||||||
let viewModel: UserSessionOverviewViewModel
|
let session: UserSessionInfo
|
||||||
|
let service: UserSessionOverviewServiceProtocol
|
||||||
switch self {
|
switch self {
|
||||||
case .currentSession:
|
case .currentSession:
|
||||||
let session = UserSessionInfo(id: "session",
|
session = UserSessionInfo(id: "alice",
|
||||||
name: "iOS",
|
name: "iOS",
|
||||||
deviceType: .mobile,
|
deviceType: .mobile,
|
||||||
isVerified: false,
|
isVerified: false,
|
||||||
lastSeenIP: "10.0.0.10",
|
lastSeenIP: "10.0.0.10",
|
||||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
lastSeenTimestamp: nil,
|
||||||
isActive: true,
|
applicationName: "Element iOS",
|
||||||
isCurrent: true)
|
applicationVersion: "1.0.0",
|
||||||
viewModel = UserSessionOverviewViewModel(session: session)
|
applicationURL: nil,
|
||||||
|
deviceModel: nil,
|
||||||
|
deviceOS: "iOS 15.5",
|
||||||
|
lastSeenIPLocation: nil,
|
||||||
|
deviceName: "My iPhone",
|
||||||
|
isActive: true,
|
||||||
|
isCurrent: true)
|
||||||
|
service = MockUserSessionOverviewService()
|
||||||
case .otherSession:
|
case .otherSession:
|
||||||
let session = UserSessionInfo(id: "session",
|
session = UserSessionInfo(id: "1",
|
||||||
name: "Mac",
|
name: "macOS",
|
||||||
deviceType: .desktop,
|
deviceType: .desktop,
|
||||||
isVerified: true,
|
isVerified: true,
|
||||||
lastSeenIP: "10.0.0.10",
|
lastSeenIP: "1.0.0.1",
|
||||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000,
|
||||||
isActive: true,
|
applicationName: "Element MacOS",
|
||||||
isCurrent: false)
|
applicationVersion: "1.0.0",
|
||||||
viewModel = UserSessionOverviewViewModel(session: session)
|
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.
|
// can simulate service and viewModel actions here if needs be.
|
||||||
return ([viewModel], AnyView(UserSessionOverview(viewModel: viewModel.context)))
|
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)
|
app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.currentSession.title)
|
||||||
XCTAssertTrue(app.buttons[VectorL10n.userSessionOverviewSessionDetailsButtonTitle].exists)
|
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!
|
var sut: UserSessionOverviewViewModel!
|
||||||
|
|
||||||
func test_whenVerifyCurrentSessionProcessed_completionWithVerifyCurrentSessionCalled() {
|
func test_whenVerifyCurrentSessionProcessed_completionWithVerifyCurrentSessionCalled() {
|
||||||
sut = UserSessionOverviewViewModel(session: createUserSessionInfo())
|
sut = UserSessionOverviewViewModel(sessionInfo: createUserSessionInfo(), service: MockUserSessionOverviewService())
|
||||||
|
|
||||||
|
XCTAssertEqual(sut.state.isPusherEnabled, nil)
|
||||||
var modelResult: UserSessionOverviewViewModelResult?
|
var modelResult: UserSessionOverviewViewModelResult?
|
||||||
sut.completion = { result in
|
sut.completion = { result in
|
||||||
modelResult = result
|
modelResult = result
|
||||||
|
@ -34,17 +35,57 @@ class UserSessionOverviewViewModelTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func test_whenViewSessionDetailsProcessed_completionWithShowSessionDetailsCalled() {
|
func test_whenViewSessionDetailsProcessed_completionWithShowSessionDetailsCalled() {
|
||||||
let session = createUserSessionInfo()
|
let sessionInfo = createUserSessionInfo()
|
||||||
sut = UserSessionOverviewViewModel(session: session)
|
sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: MockUserSessionOverviewService())
|
||||||
|
|
||||||
|
XCTAssertEqual(sut.state.isPusherEnabled, nil)
|
||||||
var modelResult: UserSessionOverviewViewModelResult?
|
var modelResult: UserSessionOverviewViewModelResult?
|
||||||
sut.completion = { result in
|
sut.completion = { result in
|
||||||
modelResult = result
|
modelResult = result
|
||||||
}
|
}
|
||||||
sut.process(viewAction: .viewSessionDetails)
|
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 {
|
private func createUserSessionInfo() -> UserSessionInfo {
|
||||||
UserSessionInfo(id: "session",
|
UserSessionInfo(id: "session",
|
||||||
name: "iOS",
|
name: "iOS",
|
||||||
|
@ -52,6 +93,13 @@ class UserSessionOverviewViewModelTests: XCTestCase {
|
||||||
isVerified: false,
|
isVerified: false,
|
||||||
lastSeenIP: "10.0.0.10",
|
lastSeenIP: "10.0.0.10",
|
||||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
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,
|
isActive: true,
|
||||||
isCurrent: true)
|
isCurrent: true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,13 +19,13 @@ import Foundation
|
||||||
// MARK: - Coordinator
|
// MARK: - Coordinator
|
||||||
|
|
||||||
enum UserSessionOverviewCoordinatorResult {
|
enum UserSessionOverviewCoordinatorResult {
|
||||||
case openSessionDetails(session: UserSessionInfo)
|
case openSessionDetails(sessionInfo: UserSessionInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: View model
|
// MARK: View model
|
||||||
|
|
||||||
enum UserSessionOverviewViewModelResult: Equatable {
|
enum UserSessionOverviewViewModelResult: Equatable {
|
||||||
case showSessionDetails(session: UserSessionInfo)
|
case showSessionDetails(sessionInfo: UserSessionInfo)
|
||||||
case verifyCurrentSession
|
case verifyCurrentSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,9 +34,13 @@ enum UserSessionOverviewViewModelResult: Equatable {
|
||||||
struct UserSessionOverviewViewState: BindableState {
|
struct UserSessionOverviewViewState: BindableState {
|
||||||
let cardViewData: UserSessionCardViewData
|
let cardViewData: UserSessionCardViewData
|
||||||
let isCurrentSession: Bool
|
let isCurrentSession: Bool
|
||||||
|
var isPusherEnabled: Bool?
|
||||||
|
var remotelyTogglingPushersAvailable: Bool
|
||||||
|
var showLoadingIndicator: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UserSessionOverviewViewAction {
|
enum UserSessionOverviewViewAction {
|
||||||
case verifyCurrentSession
|
case verifyCurrentSession
|
||||||
case viewSessionDetails
|
case viewSessionDetails
|
||||||
|
case togglePushNotifications
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,18 +19,45 @@ import SwiftUI
|
||||||
typealias UserSessionOverviewViewModelType = StateStoreViewModel<UserSessionOverviewViewState, UserSessionOverviewViewAction>
|
typealias UserSessionOverviewViewModelType = StateStoreViewModel<UserSessionOverviewViewState, UserSessionOverviewViewAction>
|
||||||
|
|
||||||
class UserSessionOverviewViewModel: UserSessionOverviewViewModelType, UserSessionOverviewViewModelProtocol {
|
class UserSessionOverviewViewModel: UserSessionOverviewViewModelType, UserSessionOverviewViewModelProtocol {
|
||||||
private let session: UserSessionInfo
|
private let sessionInfo: UserSessionInfo
|
||||||
|
private let service: UserSessionOverviewServiceProtocol
|
||||||
|
|
||||||
var completion: ((UserSessionOverviewViewModelResult) -> Void)?
|
var completion: ((UserSessionOverviewViewModelResult) -> Void)?
|
||||||
|
|
||||||
init(session: UserSessionInfo) {
|
// MARK: - Setup
|
||||||
self.session = session
|
|
||||||
|
init(sessionInfo: UserSessionInfo, service: UserSessionOverviewServiceProtocol) {
|
||||||
|
self.sessionInfo = sessionInfo
|
||||||
|
self.service = service
|
||||||
|
|
||||||
let cardViewData = UserSessionCardViewData(session: session)
|
let cardViewData = UserSessionCardViewData(sessionInfo: sessionInfo)
|
||||||
let state = UserSessionOverviewViewState(cardViewData: cardViewData, isCurrentSession: session.isCurrent)
|
let state = UserSessionOverviewViewState(cardViewData: cardViewData,
|
||||||
|
isCurrentSession: sessionInfo.isCurrent,
|
||||||
|
isPusherEnabled: service.pusherEnabledSubject.value,
|
||||||
|
remotelyTogglingPushersAvailable: service.remotelyTogglingPushersAvailableSubject.value,
|
||||||
|
showLoadingIndicator: false)
|
||||||
super.init(initialViewState: state)
|
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
|
// MARK: - Public
|
||||||
|
|
||||||
override func process(viewAction: UserSessionOverviewViewAction) {
|
override func process(viewAction: UserSessionOverviewViewAction) {
|
||||||
|
@ -38,7 +65,10 @@ class UserSessionOverviewViewModel: UserSessionOverviewViewModelType, UserSessio
|
||||||
case .verifyCurrentSession:
|
case .verifyCurrentSession:
|
||||||
completion?(.verifyCurrentSession)
|
completion?(.verifyCurrentSession)
|
||||||
case .viewSessionDetails:
|
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
|
UserSessionCardView(viewData: viewModel.viewState.cardViewData, onVerifyAction: { _ in
|
||||||
viewModel.send(viewAction: .verifyCurrentSession)
|
viewModel.send(viewAction: .verifyCurrentSession)
|
||||||
},
|
},
|
||||||
onViewDetailsAction: { _ in
|
onViewDetailsAction: { _ in
|
||||||
viewModel.send(viewAction: .viewSessionDetails)
|
viewModel.send(viewAction: .viewSessionDetails)
|
||||||
})
|
})
|
||||||
.padding(16)
|
.padding(16)
|
||||||
|
@ -34,13 +34,21 @@ struct UserSessionOverview: View {
|
||||||
UserSessionOverviewDisclosureCell(title: VectorL10n.userSessionOverviewSessionDetailsButtonTitle, onBackgroundTap: {
|
UserSessionOverviewDisclosureCell(title: VectorL10n.userSessionOverviewSessionDetailsButtonTitle, onBackgroundTap: {
|
||||||
viewModel.send(viewAction: .viewSessionDetails)
|
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())
|
.background(theme.colors.system.ignoresSafeArea())
|
||||||
.frame(maxHeight: .infinity)
|
.frame(maxHeight: .infinity)
|
||||||
|
.waitOverlay(show: viewModel.viewState.showLoadingIndicator, allowUserInteraction: false)
|
||||||
.navigationTitle(viewModel.viewState.isCurrentSession ?
|
.navigationTitle(viewModel.viewState.isCurrentSession ?
|
||||||
VectorL10n.userSessionOverviewCurrentSessionTitle :
|
VectorL10n.userSessionOverviewCurrentSessionTitle :
|
||||||
VectorL10n.userSessionOverviewSessionTitle)
|
VectorL10n.userSessionOverviewSessionTitle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ struct UserSessionOverviewDisclosureCell: View {
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
Image(Asset.Images.chevron.name)
|
Image(Asset.Images.chevron.name)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 15)
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
SeparatorLine()
|
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) {
|
init(parameters: UserSessionsOverviewCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
service = UserSessionsOverviewService(mxSession: parameters.session)
|
let dataProvider = UserSessionsDataProvider(session: parameters.session)
|
||||||
|
service = UserSessionsOverviewService(dataProvider: dataProvider)
|
||||||
viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
|
viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
|
||||||
hostingViewController = VectorHostingController(rootView: UserSessionsOverview(viewModel: viewModel.context))
|
hostingViewController = VectorHostingController(rootView: UserSessionsOverview(viewModel: viewModel.context))
|
||||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingViewController)
|
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingViewController)
|
||||||
|
@ -52,14 +53,14 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
||||||
MXLog.debug("[UserSessionsOverviewCoordinator] UserSessionsOverviewViewModel did complete with result: \(result).")
|
MXLog.debug("[UserSessionsOverviewCoordinator] UserSessionsOverviewViewModel did complete with result: \(result).")
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case let .showOtherSessions(sessions: sessions, filter: filter):
|
case let .showOtherSessions(sessionsInfo: sessionsInfo, filter: filter):
|
||||||
self.showOtherSessions(sessions: sessions, filterBy: filter)
|
self.showOtherSessions(sessionsInfo: sessionsInfo, filterBy: filter)
|
||||||
case .verifyCurrentSession:
|
case .verifyCurrentSession:
|
||||||
self.startVerifyCurrentSession()
|
self.startVerifyCurrentSession()
|
||||||
case let .showCurrentSessionOverview(session):
|
case let .showCurrentSessionOverview(sessionInfo):
|
||||||
self.showCurrentSessionOverview(session: session)
|
self.showCurrentSessionOverview(sessionInfo: sessionInfo)
|
||||||
case let .showUserSessionOverview(session):
|
case let .showUserSessionOverview(sessionInfo):
|
||||||
self.showUserSessionOverview(session: session)
|
self.showUserSessionOverview(sessionInfo: sessionInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,20 +84,20 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
||||||
loadingIndicator = nil
|
loadingIndicator = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showOtherSessions(sessions: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter) {
|
private func showOtherSessions(sessionsInfo: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter) {
|
||||||
completion?(.openOtherSessions(sessions: sessions, filter: filter))
|
completion?(.openOtherSessions(sessionsInfo: sessionsInfo, filter: filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startVerifyCurrentSession() {
|
private func startVerifyCurrentSession() {
|
||||||
// TODO:
|
// TODO:
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showCurrentSessionOverview(session: UserSessionInfo) {
|
private func showCurrentSessionOverview(sessionInfo: UserSessionInfo) {
|
||||||
completion?(.openSessionOverview(session: session))
|
completion?(.openSessionOverview(sessionInfo: sessionInfo))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showUserSessionOverview(session: UserSessionInfo) {
|
private func showUserSessionOverview(sessionInfo: UserSessionInfo) {
|
||||||
completion?(.openSessionOverview(session: session))
|
completion?(.openSessionOverview(sessionInfo: sessionInfo))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,31 +20,39 @@ import SwiftUI
|
||||||
/// Using an enum for the screen allows you define the different state cases with
|
/// Using an enum for the screen allows you define the different state cases with
|
||||||
/// the relevant associated data for each case.
|
/// the relevant associated data for each case.
|
||||||
enum MockUserSessionsOverviewScreenState: MockScreenState, CaseIterable {
|
enum MockUserSessionsOverviewScreenState: MockScreenState, CaseIterable {
|
||||||
case verifiedSession
|
case currentSessionUnverified
|
||||||
|
case currentSessionVerified
|
||||||
|
case onlyUnverifiedSessions
|
||||||
|
case onlyInactiveSessions
|
||||||
|
case noOtherSessions
|
||||||
|
|
||||||
/// The associated screen
|
/// The associated screen
|
||||||
var screenType: Any.Type {
|
var screenType: Any.Type {
|
||||||
UserSessionsOverview.self
|
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.
|
/// Generate the view struct for the screen state.
|
||||||
var screenView: ([Any], AnyView) {
|
var screenView: ([Any], AnyView) {
|
||||||
let service = MockUserSessionsOverviewService()
|
var service: UserSessionsOverviewServiceProtocol?
|
||||||
switch self {
|
switch self {
|
||||||
case .verifiedSession:
|
case .currentSessionUnverified:
|
||||||
break
|
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)
|
let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
|
||||||
|
|
||||||
// can simulate service and viewModel actions here if needs be.
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
[service, viewModel],
|
[service, viewModel],
|
||||||
AnyView(UserSessionsOverview(viewModel: viewModel.context)
|
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
|
/// Delay after which session is considered inactive, 90 days
|
||||||
private static let inactiveSessionDurationTreshold: TimeInterval = 90 * 86400
|
private static let inactiveSessionDurationTreshold: TimeInterval = 90 * 86400
|
||||||
|
|
||||||
private let mxSession: MXSession
|
private let dataProvider: UserSessionsDataProviderProtocol
|
||||||
|
|
||||||
private(set) var overviewData: UserSessionsOverviewData
|
private(set) var overviewData: UserSessionsOverviewData
|
||||||
|
|
||||||
init(mxSession: MXSession) {
|
init(dataProvider: UserSessionsDataProviderProtocol) {
|
||||||
self.mxSession = mxSession
|
self.dataProvider = dataProvider
|
||||||
|
|
||||||
overviewData = UserSessionsOverviewData(currentSession: nil,
|
overviewData = UserSessionsOverviewData(currentSession: nil,
|
||||||
unverifiedSessions: [],
|
unverifiedSessions: [],
|
||||||
|
@ -39,7 +39,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
|
|
||||||
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) {
|
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) {
|
||||||
mxSession.matrixRestClient.devices { response in
|
dataProvider.devices { response in
|
||||||
switch response {
|
switch response {
|
||||||
case .success(let devices):
|
case .success(let devices):
|
||||||
self.overviewData = self.sessionsOverviewData(from: devices)
|
self.overviewData = self.sessionsOverviewData(from: devices)
|
||||||
|
@ -61,26 +61,28 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func setupInitialOverviewData() {
|
private func setupInitialOverviewData() {
|
||||||
let currentSessionInfo = currentSessionInfo()
|
guard let currentSessionInfo = getCurrentSessionInfo() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
overviewData = UserSessionsOverviewData(currentSession: currentSessionInfo,
|
overviewData = UserSessionsOverviewData(currentSession: currentSessionInfo,
|
||||||
unverifiedSessions: [],
|
unverifiedSessions: currentSessionInfo.isVerified ? [] : [currentSessionInfo],
|
||||||
inactiveSessions: [],
|
inactiveSessions: currentSessionInfo.isActive ? [] : [currentSessionInfo],
|
||||||
otherSessions: [])
|
otherSessions: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
private func currentSessionInfo() -> UserSessionInfo? {
|
private func getCurrentSessionInfo() -> UserSessionInfo? {
|
||||||
guard let mainAccount = MXKAccountManager.shared().activeAccounts.first,
|
guard let mainAccount = dataProvider.activeAccounts.first,
|
||||||
let device = mainAccount.device else {
|
let device = mainAccount.device else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return sessionInfo(from: device, isCurrentSession: true)
|
return sessionInfo(from: device, isCurrentSession: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sessionsOverviewData(from devices: [MXDevice]) -> UserSessionsOverviewData {
|
private func sessionsOverviewData(from devices: [MXDevice]) -> UserSessionsOverviewData {
|
||||||
let allSessions = devices
|
let allSessions = devices
|
||||||
.sorted { $0.lastSeenTs > $1.lastSeenTs }
|
.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,
|
return UserSessionsOverviewData(currentSession: allSessions.filter(\.isCurrent).first,
|
||||||
unverifiedSessions: allSessions.filter { !$0.isVerified },
|
unverifiedSessions: allSessions.filter { !$0.isVerified },
|
||||||
|
@ -90,33 +92,59 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||||
|
|
||||||
private func sessionInfo(from device: MXDevice, isCurrentSession: Bool) -> UserSessionInfo {
|
private func sessionInfo(from device: MXDevice, isCurrentSession: Bool) -> UserSessionInfo {
|
||||||
let isSessionVerified = deviceInfo(for: device.deviceId)?.trustLevel.isVerified ?? false
|
let isSessionVerified = deviceInfo(for: device.deviceId)?.trustLevel.isVerified ?? false
|
||||||
|
|
||||||
var lastSeenTs: TimeInterval?
|
let eventType = kMXAccountDataTypeClientInformation + "." + device.deviceId
|
||||||
if device.lastSeenTs > 0 {
|
let appData = dataProvider.accountData(for: eventType)
|
||||||
lastSeenTs = TimeInterval(device.lastSeenTs / 1000)
|
var userAgent: UserAgent?
|
||||||
}
|
|
||||||
|
|
||||||
var isSessionActive = true
|
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
|
isSessionActive = elapsedTime < Self.inactiveSessionDurationTreshold
|
||||||
}
|
}
|
||||||
|
|
||||||
return UserSessionInfo(id: device.deviceId,
|
return UserSessionInfo(withDevice: device,
|
||||||
name: device.displayName,
|
applicationData: appData as? [String: String],
|
||||||
deviceType: .unknown,
|
userAgent: userAgent,
|
||||||
isVerified: isSessionVerified,
|
isSessionVerified: isSessionVerified,
|
||||||
lastSeenIP: device.lastSeenIp,
|
|
||||||
lastSeenTimestamp: lastSeenTs,
|
|
||||||
isActive: isSessionActive,
|
isActive: isSessionActive,
|
||||||
isCurrent: isCurrentSession)
|
isCurrent: isCurrentSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deviceInfo(for deviceId: String) -> MXDeviceInfo? {
|
private func deviceInfo(for deviceId: String) -> MXDeviceInfo? {
|
||||||
guard let userId = mxSession.myUserId else {
|
guard let userId = dataProvider.myUserId else {
|
||||||
return nil
|
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
|
import Foundation
|
||||||
|
|
||||||
class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||||
|
enum Mode {
|
||||||
|
case currentSessionUnverified
|
||||||
|
case currentSessionVerified
|
||||||
|
case onlyUnverifiedSessions
|
||||||
|
case onlyInactiveSessions
|
||||||
|
case noOtherSessions
|
||||||
|
}
|
||||||
|
|
||||||
|
private let mode: Mode
|
||||||
|
|
||||||
var overviewData: UserSessionsOverviewData
|
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) {
|
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))
|
completion(.success(overviewData))
|
||||||
}
|
}
|
||||||
|
|
||||||
func sessionForIdentifier(_ sessionId: String) -> UserSessionInfo? {
|
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() {
|
private func buildSessions(verified: Bool, active: Bool) -> [UserSessionInfo] {
|
||||||
overviewData = UserSessionsOverviewData(currentSession: Self.allSessions.filter(\.isCurrent).first,
|
[UserSessionInfo(id: "1 verified: \(verified) active: \(active)",
|
||||||
unverifiedSessions: Self.allSessions.filter { !$0.isVerified },
|
name: "macOS verified: \(verified) active: \(active)",
|
||||||
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",
|
|
||||||
deviceType: .desktop,
|
deviceType: .desktop,
|
||||||
isVerified: true,
|
isVerified: verified,
|
||||||
lastSeenIP: "1.0.0.1",
|
lastSeenIP: "1.0.0.1",
|
||||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 8000000,
|
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),
|
isCurrent: false),
|
||||||
UserSessionInfo(id: "2",
|
UserSessionInfo(id: "2 verified: \(verified) active: \(active)",
|
||||||
name: "Firefox on Windows",
|
name: "Firefox on Windows verified: \(verified) active: \(active)",
|
||||||
deviceType: .web,
|
deviceType: .web,
|
||||||
isVerified: true,
|
isVerified: verified,
|
||||||
lastSeenIP: "2.0.0.2",
|
lastSeenIP: "2.0.0.2",
|
||||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
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),
|
isCurrent: false),
|
||||||
UserSessionInfo(id: "3",
|
UserSessionInfo(id: "3 verified: \(verified) active: \(active)",
|
||||||
name: "Android",
|
name: "Android verified: \(verified) active: \(active)",
|
||||||
deviceType: .mobile,
|
deviceType: .mobile,
|
||||||
isVerified: false,
|
isVerified: verified,
|
||||||
lastSeenIP: "3.0.0.3",
|
lastSeenIP: "3.0.0.3",
|
||||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10,
|
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)]
|
isCurrent: false)]
|
||||||
}()
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,5 +18,38 @@ import RiotSwiftUI
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
class UserSessionsOverviewUITests: MockScreenTestCase {
|
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
|
@testable import RiotSwiftUI
|
||||||
|
|
||||||
class UserSessionsOverviewViewModelTests: XCTestCase {
|
class UserSessionsOverviewViewModelTests: XCTestCase {
|
||||||
var service: MockUserSessionsOverviewService!
|
func testInitialStateEmpty() {
|
||||||
var viewModel: UserSessionsOverviewViewModelProtocol!
|
let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService())
|
||||||
var context: UserSessionsOverviewViewModelType.Context!
|
|
||||||
|
XCTAssertNil(viewModel.state.currentSessionViewData)
|
||||||
|
XCTAssertTrue(viewModel.state.unverifiedSessionsViewData.isEmpty)
|
||||||
|
XCTAssertTrue(viewModel.state.inactiveSessionsViewData.isEmpty)
|
||||||
|
XCTAssertTrue(viewModel.state.otherSessionsViewData.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
func testLoadOnDidAppear() {
|
||||||
service = MockUserSessionsOverviewService()
|
let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService())
|
||||||
viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
|
viewModel.process(viewAction: .viewAppeared)
|
||||||
context = viewModel.context
|
|
||||||
|
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
|
// MARK: - Coordinator
|
||||||
|
|
||||||
enum UserSessionsOverviewCoordinatorResult {
|
enum UserSessionsOverviewCoordinatorResult {
|
||||||
case openSessionOverview(session: UserSessionInfo)
|
case openSessionOverview(sessionInfo: UserSessionInfo)
|
||||||
case openOtherSessions(sessions: [UserSessionInfo], filter: OtherUserSessionsFilter)
|
case openOtherSessions(sessionsInfo: [UserSessionInfo], filter: OtherUserSessionsFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: View model
|
// MARK: View model
|
||||||
|
|
||||||
enum UserSessionsOverviewViewModelResult {
|
enum UserSessionsOverviewViewModelResult: Equatable {
|
||||||
case showOtherSessions(sessions: [UserSessionInfo], filter: OtherUserSessionsFilter)
|
case showOtherSessions(sessionsInfo: [UserSessionInfo], filter: OtherUserSessionsFilter)
|
||||||
case verifyCurrentSession
|
case verifyCurrentSession
|
||||||
case showCurrentSessionOverview(session: UserSessionInfo)
|
case showCurrentSessionOverview(sessionInfo: UserSessionInfo)
|
||||||
case showUserSessionOverview(session: UserSessionInfo)
|
case showUserSessionOverview(sessionInfo: UserSessionInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: View
|
// MARK: View
|
||||||
|
|
|
@ -44,19 +44,21 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
|
||||||
assertionFailure("Missing current session")
|
assertionFailure("Missing current session")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
completion?(.showCurrentSessionOverview(session: currentSessionInfo))
|
completion?(.showCurrentSessionOverview(sessionInfo: currentSessionInfo))
|
||||||
case .viewAllUnverifiedSessions:
|
case .viewAllUnverifiedSessions:
|
||||||
showSessions(filteredBy: .unverified)
|
// TODO: showSessions(filteredBy: .unverified)
|
||||||
|
break
|
||||||
case .viewAllInactiveSessions:
|
case .viewAllInactiveSessions:
|
||||||
showSessions(filteredBy: .inactive)
|
showSessions(filteredBy: .inactive)
|
||||||
case .viewAllOtherSessions:
|
case .viewAllOtherSessions:
|
||||||
showSessions(filteredBy: .all)
|
// TODO: showSessions(filteredBy: .all)
|
||||||
|
break
|
||||||
case .tapUserSession(let sessionId):
|
case .tapUserSession(let sessionId):
|
||||||
guard let session = userSessionsOverviewService.sessionForIdentifier(sessionId) else {
|
guard let session = userSessionsOverviewService.sessionForIdentifier(sessionId) else {
|
||||||
assertionFailure("Missing session info")
|
assertionFailure("Missing session info")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
completion?(.showUserSessionOverview(session: session))
|
completion?(.showUserSessionOverview(sessionInfo: session))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +70,7 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
|
||||||
state.otherSessionsViewData = userSessionsViewData.otherSessions.asViewData()
|
state.otherSessionsViewData = userSessionsViewData.otherSessions.asViewData()
|
||||||
|
|
||||||
if let currentSessionInfo = userSessionsViewData.currentSession {
|
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) {
|
private func showSessions(filteredBy filter: OtherUserSessionsFilter) {
|
||||||
completion?(.showOtherSessions(sessions: userSessionsOverviewService.overviewData.otherSessions,
|
completion?(.showOtherSessions(sessionsInfo: userSessionsOverviewService.overviewData.otherSessions,
|
||||||
filter: filter))
|
filter: filter))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import Foundation
|
||||||
|
|
||||||
/// View data for UserSessionListItem
|
/// View data for UserSessionListItem
|
||||||
struct UserSessionListItemViewData: Identifiable, Hashable {
|
struct UserSessionListItemViewData: Identifiable, Hashable {
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
sessionId
|
sessionId
|
||||||
}
|
}
|
||||||
|
@ -29,6 +30,6 @@ struct UserSessionListItemViewData: Identifiable, Hashable {
|
||||||
let sessionDetails: String
|
let sessionDetails: String
|
||||||
|
|
||||||
let deviceAvatarViewData: DeviceAvatarViewData
|
let deviceAvatarViewData: DeviceAvatarViewData
|
||||||
|
|
||||||
let sessionDetailsIcon: String?
|
let sessionDetailsIcon: String?
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,13 +18,9 @@ import Foundation
|
||||||
|
|
||||||
struct UserSessionListItemViewDataFactory {
|
struct UserSessionListItemViewDataFactory {
|
||||||
|
|
||||||
private static let userSessionNameFormatter = UserSessionNameFormatter()
|
|
||||||
private static let lastActivityDateFormatter = UserSessionLastActivityFormatter()
|
|
||||||
private static let inactiveSessionDateFormatter = InactiveUserSessionLastActivityFormatter()
|
|
||||||
|
|
||||||
func create(from session: UserSessionInfo) -> UserSessionListItemViewData {
|
func create(from session: UserSessionInfo) -> UserSessionListItemViewData {
|
||||||
let sessionName = UserSessionListItemViewDataFactory.userSessionNameFormatter.sessionName(deviceType: session.deviceType,
|
let sessionName = UserSessionNameFormatter.sessionName(deviceType: session.deviceType,
|
||||||
sessionDisplayName: session.name)
|
sessionDisplayName: session.name)
|
||||||
let sessionDetails = buildSessionDetails(isVerified: session.isVerified,
|
let sessionDetails = buildSessionDetails(isVerified: session.isVerified,
|
||||||
lastActivityDate: session.lastSeenTimestamp,
|
lastActivityDate: session.lastSeenTimestamp,
|
||||||
isActive: session.isActive)
|
isActive: session.isActive)
|
||||||
|
@ -47,7 +43,7 @@ struct UserSessionListItemViewDataFactory {
|
||||||
|
|
||||||
private func inactiveSessionDetails(lastActivityDate: TimeInterval?) -> String {
|
private func inactiveSessionDetails(lastActivityDate: TimeInterval?) -> String {
|
||||||
if let lastActivityDate = lastActivityDate {
|
if let lastActivityDate = lastActivityDate {
|
||||||
let lastActivityDateString = Self.inactiveSessionDateFormatter.lastActivityDateString(from: lastActivityDate)
|
let lastActivityDateString = InactiveUserSessionLastActivityFormatter.lastActivityDateString(from: lastActivityDate)
|
||||||
return VectorL10n.userInactiveSessionItemWithDate(lastActivityDateString)
|
return VectorL10n.userInactiveSessionItemWithDate(lastActivityDateString)
|
||||||
}
|
}
|
||||||
return VectorL10n.userInactiveSessionItem
|
return VectorL10n.userInactiveSessionItem
|
||||||
|
@ -61,7 +57,7 @@ struct UserSessionListItemViewDataFactory {
|
||||||
var lastActivityDateString: String?
|
var lastActivityDateString: String?
|
||||||
|
|
||||||
if let lastActivityDate = lastActivityDate {
|
if let lastActivityDate = lastActivityDate {
|
||||||
lastActivityDateString = Self.lastActivityDateFormatter.lastActivityDateString(from: lastActivityDate)
|
lastActivityDateString = UserSessionLastActivityFormatter.lastActivityDateString(from: lastActivityDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let lastActivityDateString = lastActivityDateString, lastActivityDateString.isEmpty == false {
|
if let lastActivityDateString = lastActivityDateString, lastActivityDateString.isEmpty == false {
|
||||||
|
|
|
@ -23,11 +23,13 @@ struct UserSessionsOverview: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
securityRecommendationsSection
|
if hasSecurityRecommendations {
|
||||||
|
securityRecommendationsSection
|
||||||
|
}
|
||||||
|
|
||||||
currentSessionsSection
|
currentSessionsSection
|
||||||
|
|
||||||
if viewModel.viewState.otherSessionsViewData.isEmpty == false {
|
if !viewModel.viewState.otherSessionsViewData.isEmpty {
|
||||||
otherSessionsSection
|
otherSessionsSection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,41 +42,39 @@ struct UserSessionsOverview: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var securityRecommendationsSection: some View {
|
private var securityRecommendationsSection: some View {
|
||||||
if hasSecurityRecommendations {
|
SwiftUI.Section {
|
||||||
SwiftUI.Section {
|
if !viewModel.viewState.unverifiedSessionsViewData.isEmpty {
|
||||||
if !viewModel.viewState.unverifiedSessionsViewData.isEmpty {
|
SecurityRecommendationCard(style: .unverified,
|
||||||
SecurityRecommendationCard(style: .unverified,
|
sessionCount: viewModel.viewState.unverifiedSessionsViewData.count) {
|
||||||
sessionCount: viewModel.viewState.unverifiedSessionsViewData.count) {
|
viewModel.send(viewAction: .viewAllUnverifiedSessions)
|
||||||
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 {
|
var hasSecurityRecommendations: Bool {
|
||||||
|
@ -102,10 +102,9 @@ struct UserSessionsOverview: View {
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var otherSessionsSection: some View {
|
private var otherSessionsSection: some View {
|
||||||
SwiftUI.Section {
|
SwiftUI.Section {
|
||||||
// Device list
|
|
||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
ForEach(viewModel.viewState.otherSessionsViewData) { viewData in
|
ForEach(viewModel.viewState.otherSessionsViewData) { viewData in
|
||||||
UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in
|
UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in
|
||||||
|
@ -131,6 +130,7 @@ struct UserSessionsOverview: View {
|
||||||
.padding(.horizontal, 16.0)
|
.padding(.horizontal, 16.0)
|
||||||
.padding(.top, 24.0)
|
.padding(.top, 24.0)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("userSessionsOverviewOtherSection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,9 @@ schemes:
|
||||||
disableMainThreadChecker: true
|
disableMainThreadChecker: true
|
||||||
targets:
|
targets:
|
||||||
- RiotSwiftUnitTests
|
- RiotSwiftUnitTests
|
||||||
|
gatherCoverageData: true
|
||||||
|
coverageTargets:
|
||||||
|
- RiotSwiftUI
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
RiotSwiftUI:
|
RiotSwiftUI:
|
||||||
|
|
|
@ -24,6 +24,9 @@ schemes:
|
||||||
disableMainThreadChecker: true
|
disableMainThreadChecker: true
|
||||||
targets:
|
targets:
|
||||||
- RiotSwiftUITests
|
- RiotSwiftUITests
|
||||||
|
gatherCoverageData: true
|
||||||
|
coverageTargets:
|
||||||
|
- RiotSwiftUI
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
RiotSwiftUITests:
|
RiotSwiftUITests:
|
||||||
|
|
|
@ -24,6 +24,9 @@ schemes:
|
||||||
disableMainThreadChecker: true
|
disableMainThreadChecker: true
|
||||||
targets:
|
targets:
|
||||||
- RiotSwiftUnitTests
|
- RiotSwiftUnitTests
|
||||||
|
gatherCoverageData: true
|
||||||
|
coverageTargets:
|
||||||
|
- RiotSwiftUI
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
RiotSwiftUnitTests:
|
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
|
disableMainThreadChecker: true
|
||||||
targets:
|
targets:
|
||||||
- RiotTests
|
- RiotTests
|
||||||
|
gatherCoverageData: true
|
||||||
|
coverageTargets:
|
||||||
|
- Riot
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
RiotTests:
|
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