diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_inactive.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_inactive.imageset/Contents.json
new file mode 100644
index 000000000..7261e4ae4
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_inactive.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "user_sessions_inactive.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_inactive.imageset/user_sessions_inactive.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_inactive.imageset/user_sessions_inactive.svg
new file mode 100644
index 000000000..d7baa4786
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_inactive.imageset/user_sessions_inactive.svg
@@ -0,0 +1,11 @@
+
diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_unverified.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_unverified.imageset/Contents.json
new file mode 100644
index 000000000..2c0d97214
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_unverified.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "user_sessions_unverified.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_unverified.imageset/user_sessions_unverified.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_unverified.imageset/user_sessions_unverified.svg
new file mode 100644
index 000000000..4db2abe67
--- /dev/null
+++ b/Riot/Assets/Images.xcassets/DeviceManager/user_sessions_unverified.imageset/user_sessions_unverified.svg
@@ -0,0 +1,5 @@
+
diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings
index 7165bdcd2..79cf31ba0 100644
--- a/Riot/Assets/en.lproj/Vector.strings
+++ b/Riot/Assets/en.lproj/Vector.strings
@@ -2358,10 +2358,21 @@ To enable access, tap Settings> Location and select Always";
"user_sessions_overview_title" = "Sessions";
-"user_sessions_overview_other_sessions_section_title" = "OTHER SESSIONS";
+"user_sessions_overview_security_recommendations_section_title" = "Security recommendations";
+"user_sessions_overview_security_recommendations_section_info" = "Improve your account security by following these recommendations.";
+
+"user_sessions_overview_security_recommendations_unverified_title" = "Unverified sessions";
+"user_sessions_overview_security_recommendations_unverified_info" = "Verify or sign out from unverified sessions.";
+
+"user_sessions_overview_security_recommendations_inactive_title" = "Inactive sessions";
+"user_sessions_overview_security_recommendations_inactive_info" = "Consider signing out from old sessions (90 days or older) you don’t use anymore.";
+
+"user_sessions_overview_other_sessions_section_title" = "Other sessions";
"user_sessions_overview_other_sessions_section_info" = "For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore.";
-"user_sessions_overview_current_session_section_title" = "CURRENT SESSION";
+"user_sessions_overview_current_session_section_title" = "Current session";
+
+"user_sessions_view_all_action" = "View all (%d)";
"user_session_verified" = "Verified session";
"user_session_unverified" = "Unverified session";
@@ -2374,6 +2385,7 @@ To enable access, tap Settings> Location and select Always";
"user_session_verified_additional_info" = "Your current session is ready for secure messaging.";
"user_session_unverified_additional_info" = "Verify your current session for enhanced secure messaging.";
+
// First item is client name and second item is session display name
"user_session_name" = "%@: %@";
@@ -2385,8 +2397,8 @@ To enable access, tap Settings> Location and select Always";
"device_name_unknown" = "Unknown client";
"user_session_details_title" = "Session details";
-"user_session_details_session_section_header" = "SESSION";
-"user_session_details_device_section_header" = "DEVICE";
+"user_session_details_session_section_header" = "Session";
+"user_session_details_device_section_header" = "Device";
"user_session_details_session_name" = "Session name";
"user_session_details_session_id" = "Session ID";
"user_session_details_session_section_footer" = "Copy any data by tapping on it and holding it down.";
diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift
index 6fc13773b..9dc652238 100644
--- a/Riot/Generated/Images.swift
+++ b/Riot/Generated/Images.swift
@@ -107,6 +107,8 @@ internal class Asset: NSObject {
internal static let deviceTypeWeb = ImageAsset(name: "device_type_web")
internal static let userSessionUnverified = ImageAsset(name: "user_session_unverified")
internal static let userSessionVerified = ImageAsset(name: "user_session_verified")
+ internal static let userSessionsInactive = ImageAsset(name: "user_sessions_inactive")
+ internal static let userSessionsUnverified = ImageAsset(name: "user_sessions_unverified")
internal static let e2eBlocked = ImageAsset(name: "e2e_blocked")
internal static let e2eUnencrypted = ImageAsset(name: "e2e_unencrypted")
internal static let e2eWarning = ImageAsset(name: "e2e_warning")
diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift
index 00249e843..31d85976d 100644
--- a/Riot/Generated/Strings.swift
+++ b/Riot/Generated/Strings.swift
@@ -8471,7 +8471,7 @@ public class VectorL10n: NSObject {
public static var userSessionDetailsDeviceIpAddress: String {
return VectorL10n.tr("Vector", "user_session_details_device_ip_address")
}
- /// DEVICE
+ /// Device
public static var userSessionDetailsDeviceSectionHeader: String {
return VectorL10n.tr("Vector", "user_session_details_device_section_header")
}
@@ -8487,7 +8487,7 @@ public class VectorL10n: NSObject {
public static var userSessionDetailsSessionSectionFooter: String {
return VectorL10n.tr("Vector", "user_session_details_session_section_footer")
}
- /// SESSION
+ /// Session
public static var userSessionDetailsSessionSectionHeader: String {
return VectorL10n.tr("Vector", "user_session_details_session_section_header")
}
@@ -8551,7 +8551,7 @@ public class VectorL10n: NSObject {
public static var userSessionViewDetails: String {
return VectorL10n.tr("Vector", "user_session_view_details")
}
- /// CURRENT SESSION
+ /// Current session
public static var userSessionsOverviewCurrentSessionSectionTitle: String {
return VectorL10n.tr("Vector", "user_sessions_overview_current_session_section_title")
}
@@ -8559,10 +8559,34 @@ public class VectorL10n: NSObject {
public static var userSessionsOverviewOtherSessionsSectionInfo: String {
return VectorL10n.tr("Vector", "user_sessions_overview_other_sessions_section_info")
}
- /// OTHER SESSIONS
+ /// Other sessions
public static var userSessionsOverviewOtherSessionsSectionTitle: String {
return VectorL10n.tr("Vector", "user_sessions_overview_other_sessions_section_title")
}
+ /// Consider signing out from old sessions (90 days or older) you don’t use anymore.
+ public static var userSessionsOverviewSecurityRecommendationsInactiveInfo: String {
+ return VectorL10n.tr("Vector", "user_sessions_overview_security_recommendations_inactive_info")
+ }
+ /// Inactive sessions
+ public static var userSessionsOverviewSecurityRecommendationsInactiveTitle: String {
+ return VectorL10n.tr("Vector", "user_sessions_overview_security_recommendations_inactive_title")
+ }
+ /// Improve your account security by following these recommendations.
+ public static var userSessionsOverviewSecurityRecommendationsSectionInfo: String {
+ return VectorL10n.tr("Vector", "user_sessions_overview_security_recommendations_section_info")
+ }
+ /// Security recommendations
+ public static var userSessionsOverviewSecurityRecommendationsSectionTitle: String {
+ return VectorL10n.tr("Vector", "user_sessions_overview_security_recommendations_section_title")
+ }
+ /// Verify or sign out from unverified sessions.
+ public static var userSessionsOverviewSecurityRecommendationsUnverifiedInfo: String {
+ return VectorL10n.tr("Vector", "user_sessions_overview_security_recommendations_unverified_info")
+ }
+ /// Unverified sessions
+ public static var userSessionsOverviewSecurityRecommendationsUnverifiedTitle: String {
+ return VectorL10n.tr("Vector", "user_sessions_overview_security_recommendations_unverified_title")
+ }
/// Sessions
public static var userSessionsOverviewTitle: String {
return VectorL10n.tr("Vector", "user_sessions_overview_title")
@@ -8571,6 +8595,10 @@ public class VectorL10n: NSObject {
public static var userSessionsSettings: String {
return VectorL10n.tr("Vector", "user_sessions_settings")
}
+ /// View all (%d)
+ public static func userSessionsViewAllAction(_ p1: Int) -> String {
+ return VectorL10n.tr("Vector", "user_sessions_view_all_action", p1)
+ }
/// If you didn’t sign in to this session, your account may be compromised.
public static var userVerificationSessionDetailsAdditionalInformationUntrustedCurrentUser: String {
return VectorL10n.tr("Vector", "user_verification_session_details_additional_information_untrusted_current_user")
diff --git a/RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift b/RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift
index a63491e5a..6fba28616 100644
--- a/RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift
+++ b/RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift
@@ -18,20 +18,11 @@ import Foundation
/// Represents a user session information
struct UserSessionInfo: Identifiable {
- /// Delay after which session is considered inactive, 90 days
- static let inactiveSessionDurationTreshold: TimeInterval = 90 * 86400
-
- // MARK: - Properties
-
- var id: String {
- sessionId
- }
-
/// The session identifier
- let sessionId: String
+ let id: String
/// The session display name
- let sessionName: String?
+ let name: String?
/// The device type used by the session
let deviceType: DeviceType
@@ -46,35 +37,10 @@ struct UserSessionInfo: Identifiable {
let lastSeenTimestamp: TimeInterval?
/// True to indicate that session has been used under `inactiveSessionDurationTreshold` value
- let isSessionActive: Bool
+ let isActive: Bool
/// True to indicate that this is current user session
- let isCurrentSession: Bool
-
- // MARK: - Setup
-
- init(sessionId: String,
- sessionName: String?,
- deviceType: DeviceType,
- isVerified: Bool,
- lastSeenIP: String?,
- lastSeenTimestamp: TimeInterval?,
- isCurrentSession: Bool) {
- self.sessionId = sessionId
- self.sessionName = sessionName
- self.deviceType = deviceType
- self.isVerified = isVerified
- self.lastSeenIP = lastSeenIP
- self.lastSeenTimestamp = lastSeenTimestamp
-
- if let lastSeenTimestamp = lastSeenTimestamp {
- let elapsedTime = Date().timeIntervalSince1970 - lastSeenTimestamp
- isSessionActive = elapsedTime < Self.inactiveSessionDurationTreshold
- } else {
- isSessionActive = true
- }
- self.isCurrentSession = isCurrentSession
- }
+ let isCurrent: Bool
}
extension UserSessionInfo: Equatable {
diff --git a/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardView.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardView.swift
index a54915e23..bf49cedb8 100644
--- a/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardView.swift
+++ b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardView.swift
@@ -136,13 +136,14 @@ struct UserSessionCardViewPreview: View {
let viewData: UserSessionCardViewData
init(isCurrentSessionInfo: Bool = false) {
- let session = UserSessionInfo(sessionId: "alice",
- sessionName: "iOS",
+ let session = UserSessionInfo(id: "alice",
+ name: "iOS",
deviceType: .mobile,
isVerified: false,
lastSeenIP: "10.0.0.10",
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
- isCurrentSession: isCurrentSessionInfo)
+ isActive: true,
+ isCurrent: isCurrentSessionInfo)
viewData = UserSessionCardViewData(session: session)
}
diff --git a/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardViewData.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardViewData.swift
index 3fc86306e..339de5806 100644
--- a/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardViewData.swift
+++ b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardViewData.swift
@@ -67,12 +67,12 @@ struct UserSessionCardViewData {
extension UserSessionCardViewData {
init(session: UserSessionInfo) {
- self.init(sessionId: session.sessionId,
- sessionDisplayName: session.sessionName,
+ self.init(sessionId: session.id,
+ sessionDisplayName: session.name,
deviceType: session.deviceType,
isVerified: session.isVerified,
lastActivityTimestamp: session.lastSeenTimestamp,
lastSeenIP: session.lastSeenIP,
- isCurrentSessionDisplayMode: session.isCurrentSession)
+ isCurrentSessionDisplayMode: session.isCurrent)
}
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/MockUserSessionDetailsScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/MockUserSessionDetailsScreenState.swift
index dcb31faef..22b263543 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/MockUserSessionDetailsScreenState.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/MockUserSessionDetailsScreenState.swift
@@ -41,21 +41,23 @@ enum MockUserSessionDetailsScreenState: MockScreenState, CaseIterable {
let session: UserSessionInfo
switch self {
case .allSections:
- session = UserSessionInfo(sessionId: "session",
- sessionName: "iOS",
+ session = UserSessionInfo(id: "session",
+ name: "iOS",
deviceType: .mobile,
isVerified: false,
lastSeenIP: "10.0.0.10",
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
- isCurrentSession: true)
+ isActive: true,
+ isCurrent: true)
case .sessionSectionOnly:
- session = UserSessionInfo(sessionId: "session",
- sessionName: "iOS",
+ session = UserSessionInfo(id: "session",
+ name: "iOS",
deviceType: .mobile,
isVerified: false,
lastSeenIP: nil,
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
- isCurrentSession: false)
+ isActive: true,
+ isCurrent: false)
}
let viewModel = UserSessionDetailsViewModel(session: session)
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/Unit/UserSessionDetailsViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/Unit/UserSessionDetailsViewModelTests.swift
index 20debe96b..cf4ddf43c 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/Unit/UserSessionDetailsViewModelTests.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/Unit/UserSessionDetailsViewModelTests.swift
@@ -28,7 +28,7 @@ class UserSessionDetailsViewModelTests: XCTestCase {
sessionItems.append(sessionIdItem(sessionId: userSessionInfo.sessionId))
var sections = [UserSessionDetailsSectionViewData]()
- sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader,
+ sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
items: sessionItems))
let expectedModel = UserSessionDetailsViewState(sections: sections)
@@ -47,7 +47,7 @@ class UserSessionDetailsViewModelTests: XCTestCase {
sessionItems.append(sessionIdItem(sessionId: userSessionInfo.sessionId))
var sections = [UserSessionDetailsSectionViewData]()
- sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader,
+ sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
items: sessionItems))
@@ -67,14 +67,14 @@ class UserSessionDetailsViewModelTests: XCTestCase {
sessionItems.append(sessionIdItem(sessionId: userSessionInfo.sessionId))
var sections = [UserSessionDetailsSectionViewData]()
- sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader,
+ sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
items: sessionItems))
var deviceSectionItems = [UserSessionDetailsSectionItemViewData]()
deviceSectionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsDeviceIpAddress,
value: "0.0.0.0"))
- sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsDeviceSectionHeader,
+ sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
footer: nil,
items: deviceSectionItems))
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModel.swift
index 66a5d7559..9fe47469d 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModel.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModel.swift
@@ -45,15 +45,15 @@ class UserSessionDetailsViewModel: UserSessionDetailsViewModelType, UserSessionD
private func sessionSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData {
var sessionItems = [UserSessionDetailsSectionItemViewData]()
- if let sessionName = session.sessionName {
+ if let sessionName = session.name {
sessionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsSessionName,
value: sessionName))
}
sessionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
- value: session.sessionId))
+ value: session.id))
- return UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader,
+ return UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
items: sessionItems)
}
@@ -65,7 +65,7 @@ class UserSessionDetailsViewModel: UserSessionDetailsViewModelType, UserSessionD
value: lastSeenIP))
}
if deviceSectionItems.count > 0 {
- return UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsDeviceSectionHeader,
+ return UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
footer: nil,
items: deviceSectionItems)
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/MockUserSessionOverviewScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/MockUserSessionOverviewScreenState.swift
index 4830859d0..0cfa0b7a4 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/MockUserSessionOverviewScreenState.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/MockUserSessionOverviewScreenState.swift
@@ -41,22 +41,24 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable {
let viewModel: UserSessionOverviewViewModel
switch self {
case .currentSession:
- let session = UserSessionInfo(sessionId: "session",
- sessionName: "iOS",
+ let session = UserSessionInfo(id: "session",
+ name: "iOS",
deviceType: .mobile,
isVerified: false,
lastSeenIP: "10.0.0.10",
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
- isCurrentSession: true)
+ isActive: true,
+ isCurrent: true)
viewModel = UserSessionOverviewViewModel(session: session)
case .otherSession:
- let session = UserSessionInfo(sessionId: "session",
- sessionName: "Mac",
+ let session = UserSessionInfo(id: "session",
+ name: "Mac",
deviceType: .desktop,
isVerified: true,
lastSeenIP: "10.0.0.10",
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
- isCurrentSession: false)
+ isActive: true,
+ isCurrent: false)
viewModel = UserSessionOverviewViewModel(session: session)
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewViewModel.swift
index 95261d7c8..42eaaa6c7 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewViewModel.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewViewModel.swift
@@ -27,7 +27,7 @@ class UserSessionOverviewViewModel: UserSessionOverviewViewModelType, UserSessio
self.session = session
let cardViewData = UserSessionCardViewData(session: session)
- let state = UserSessionOverviewViewState(cardViewData: cardViewData, isCurrentSession: session.isCurrentSession)
+ let state = UserSessionOverviewViewState(cardViewData: cardViewData, isCurrentSession: session.isCurrent)
super.init(initialViewState: state)
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift
index da4e30ba8..da0127e83 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift
@@ -18,6 +18,9 @@ import Foundation
import MatrixSDK
class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
+ /// Delay after which session is considered inactive, 90 days
+ static private let inactiveSessionDurationTreshold: TimeInterval = 90 * 86400
+
private let mxSession: MXSession
private(set) var overviewData: UserSessionsOverviewData
@@ -52,7 +55,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
return overviewData.currentSession
}
- return overviewData.otherSessions.first(where: { $0.sessionId == sessionId })
+ return overviewData.otherSessions.first(where: { $0.id == sessionId })
}
// MARK: - Private
@@ -85,7 +88,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
var otherSessions: [UserSessionInfo] = []
for session in allSessions {
- if session.isCurrentSession {
+ if session.isCurrent {
currentSession = session
} else {
otherSessions.append(session)
@@ -94,7 +97,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
unverifiedSessions.append(session)
}
- if session.isSessionActive == false {
+ if session.isActive == false {
inactiveSessions.append(session)
}
}
@@ -114,13 +117,20 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
lastSeenTs = TimeInterval(device.lastSeenTs / 1000)
}
- return UserSessionInfo(sessionId: device.deviceId,
- sessionName: device.displayName,
+ var isSessionActive = true
+ if let lastSeenTimestamp = lastSeenTs {
+ let elapsedTime = Date().timeIntervalSince1970 - lastSeenTimestamp
+ isSessionActive = elapsedTime < Self.inactiveSessionDurationTreshold
+ }
+
+ return UserSessionInfo(id: device.deviceId,
+ name: device.displayName,
deviceType: .unknown,
isVerified: isSessionVerified,
lastSeenIP: device.lastSeenIp,
lastSeenTimestamp: lastSeenTs,
- isCurrentSession: isCurrentSession)
+ isActive: isSessionActive,
+ isCurrent: isCurrentSession)
}
private func deviceInfo(for deviceId: String) -> MXDeviceInfo? {
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift
index 13167d350..ac863f5ce 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift
@@ -28,21 +28,49 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
}
init() {
- let currentSessionInfo = UserSessionInfo(sessionId: "alice", sessionName: "iOS", deviceType: .mobile, isVerified: false, lastSeenIP: "10.0.0.10", lastSeenTimestamp: nil, isCurrentSession: true)
+ let currentSession = UserSessionInfo(id: "alice",
+ name: "iOS",
+ deviceType: .mobile,
+ isVerified: false,
+ lastSeenIP: "10.0.0.10",
+ lastSeenTimestamp: nil,
+ isActive: true,
+ isCurrent: true)
- let unverifiedSessionsInfo: [UserSessionInfo] = []
-
- let inactiveSessionsInfo: [UserSessionInfo] = []
-
- let otherSessionsInfo: [UserSessionInfo] = [
- UserSessionInfo(sessionId: "1", sessionName: "macOS", deviceType: .desktop, isVerified: true, lastSeenIP: "1.0.0.1", lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000, isCurrentSession: false),
- UserSessionInfo(sessionId: "2", sessionName: "Firefox on Windows", deviceType: .web, isVerified: true, lastSeenIP: "2.0.0.2", lastSeenTimestamp: Date().timeIntervalSince1970 - 100, isCurrentSession: false),
- UserSessionInfo(sessionId: "3", sessionName: "Android", deviceType: .mobile, isVerified: false, lastSeenIP: "3.0.0.3", lastSeenTimestamp: Date().timeIntervalSince1970 - 10, isCurrentSession: false)
+ let otherSessions: [UserSessionInfo] = [
+ UserSessionInfo(id: "1",
+ name: "macOS",
+ deviceType: .desktop,
+ isVerified: true,
+ lastSeenIP: "1.0.0.1",
+ lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000,
+ isActive: false,
+ isCurrent: false),
+ UserSessionInfo(id: "2",
+ name: "Firefox on Windows",
+ deviceType: .web,
+ isVerified: true,
+ lastSeenIP: "2.0.0.2",
+ lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
+ isActive: true,
+ isCurrent: false),
+ UserSessionInfo(id: "3",
+ name: "Android",
+ deviceType: .mobile,
+ isVerified: false,
+ lastSeenIP: "3.0.0.3",
+ lastSeenTimestamp: Date().timeIntervalSince1970 - 10,
+ isActive: true,
+ isCurrent: false)
]
- overviewData = UserSessionsOverviewData(currentSession: currentSessionInfo,
- unverifiedSessions: unverifiedSessionsInfo,
- inactiveSessions: inactiveSessionsInfo,
- otherSessions: otherSessionsInfo)
+ let unverifiedSessions: [UserSessionInfo] = otherSessions.filter { !$0.isVerified }
+
+ let inactiveSessions: [UserSessionInfo] = otherSessions.filter { !$0.isActive }
+
+ overviewData = UserSessionsOverviewData(currentSession: currentSession,
+ unverifiedSessions: unverifiedSessions,
+ inactiveSessions: inactiveSessions,
+ otherSessions: otherSessions)
}
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/SecurityRecommendationCard.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/SecurityRecommendationCard.swift
new file mode 100644
index 000000000..090051154
--- /dev/null
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/SecurityRecommendationCard.swift
@@ -0,0 +1,111 @@
+//
+// Copyright 2022 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import SwiftUI
+
+struct SecurityRecommendationCard: View {
+ enum Style {
+ case unverified
+ case inactive
+ }
+
+ @Environment(\.theme) var theme: ThemeSwiftUI
+
+ let style: SecurityRecommendationCard.Style
+ let sessionCount: Int
+ let action: () -> Void
+
+ var body: some View {
+ HStack(alignment: .top) {
+ Image(iconName)
+ VStack(alignment: .leading, spacing: 16.0) {
+ VStack(alignment: .leading, spacing: 8.0) {
+ Text(title)
+ .font(theme.fonts.calloutSB)
+ .foregroundColor(theme.colors.primaryContent)
+
+ Text(subtitle)
+ .font(theme.fonts.footnote)
+ .foregroundColor(theme.colors.secondaryContent)
+ }
+
+ Button {
+ action()
+ } label: {
+ Text(buttonTitle)
+ .font(theme.fonts.body)
+ }
+ .foregroundColor(theme.colors.accent)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ .padding(16)
+ .background(theme.colors.background)
+ .clipShape(backgroundShape)
+ .shapedBorder(color: theme.colors.quinaryContent, borderWidth: 1.0, shape: backgroundShape)
+ }
+
+ private var backgroundShape: RoundedRectangle {
+ RoundedRectangle(cornerRadius: 8)
+ }
+
+ private var iconName: String {
+ switch style {
+ case .unverified:
+ return Asset.Images.userSessionsUnverified.name
+ case .inactive:
+ return Asset.Images.userSessionsInactive.name
+ }
+ }
+
+ private var title: String {
+ switch style {
+ case .unverified:
+ return VectorL10n.userSessionsOverviewSecurityRecommendationsUnverifiedTitle
+ case .inactive:
+ return VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle
+ }
+ }
+
+ private var subtitle: String {
+ switch style {
+ case .unverified:
+ return VectorL10n.userSessionsOverviewSecurityRecommendationsUnverifiedInfo
+ case .inactive:
+ return VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo
+ }
+ }
+
+ private var buttonTitle: String {
+ VectorL10n.userSessionsViewAllAction(sessionCount)
+ }
+}
+
+struct SecurityRecommendationCard_Previews: PreviewProvider {
+ static var previews: some View {
+ body.theme(.light).preferredColorScheme(.light)
+ body.theme(.dark).preferredColorScheme(.dark)
+ }
+
+ @ViewBuilder
+ static var body: some View {
+ VStack {
+ SecurityRecommendationCard(style: .unverified, sessionCount: 4, action: { })
+ SecurityRecommendationCard(style: .inactive, sessionCount: 100, action: { })
+ }
+ }
+}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift
index b72301eb0..56bccbbad 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift
@@ -69,6 +69,10 @@ struct UserSessionListItemViewData: Identifiable {
extension UserSessionListItemViewData {
init(session: UserSessionInfo) {
- self.init(sessionId: session.sessionId, sessionDisplayName: session.sessionName, deviceType: session.deviceType, isVerified: session.isVerified, lastActivityDate: session.lastSeenTimestamp)
+ self.init(sessionId: session.id,
+ sessionDisplayName: session.name,
+ deviceType: session.deviceType,
+ isVerified: session.isVerified,
+ lastActivityDate: session.lastSeenTimestamp)
}
}
diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift
index 0d2c5cabe..925ab82b7 100644
--- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift
+++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift
@@ -19,43 +19,14 @@ import SwiftUI
struct UserSessionsOverview: View {
@Environment(\.theme) private var theme: ThemeSwiftUI
- @ViewBuilder
- private var currentSessionsSection: some View {
- if let currentSessionViewData = viewModel.viewState.currentSessionViewData {
- SwiftUI.Section {
- UserSessionCardView(viewData: currentSessionViewData, onVerifyAction: { _ in
- viewModel.send(viewAction: .verifyCurrentSession)
- }, onViewDetailsAction: { _ in
- viewModel.send(viewAction: .viewCurrentSessionDetails)
- })
- .padding(.horizontal, 16)
- } header: {
- Text(VectorL10n.userSessionsOverviewCurrentSessionSectionTitle)
- .font(theme.fonts.footnote)
- .foregroundColor(theme.colors.secondaryContent)
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding(.horizontal, 16)
- .padding(.top, 24)
- .padding(.bottom, 11)
- }
- }
- }
-
- // MARK: Public
-
@ObservedObject var viewModel: UserSessionsOverviewViewModel.Context
var body: some View {
ScrollView {
- // Security recommendations section
- if viewModel.viewState.unverifiedSessionsViewData.isEmpty == false || viewModel.viewState.inactiveSessionsViewData.isEmpty == false {
- // TODO:
- }
+ securityRecommendationsSection
- // Current session section
currentSessionsSection
- // Other sessions section
if viewModel.viewState.otherSessionsViewData.isEmpty == false {
otherSessionsSection
}
@@ -68,6 +39,69 @@ struct UserSessionsOverview: View {
viewModel.send(viewAction: .viewAppeared)
}
}
+
+ @ViewBuilder
+ private var securityRecommendationsSection: some View {
+ if hasSecurityRecommendations {
+ SwiftUI.Section {
+ if !viewModel.viewState.unverifiedSessionsViewData.isEmpty {
+ SecurityRecommendationCard(style: .unverified,
+ sessionCount: viewModel.viewState.unverifiedSessionsViewData.count) {
+ viewModel.send(viewAction: .viewAllUnverifiedSessions)
+ }
+ }
+
+ 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)
+ }
+ }
+
+ var hasSecurityRecommendations: Bool {
+ !viewModel.viewState.unverifiedSessionsViewData.isEmpty || !viewModel.viewState.inactiveSessionsViewData.isEmpty
+ }
+
+ @ViewBuilder
+ private var currentSessionsSection: some View {
+ if let currentSessionViewData = viewModel.viewState.currentSessionViewData {
+ SwiftUI.Section {
+ UserSessionCardView(viewData: currentSessionViewData, onVerifyAction: { _ in
+ viewModel.send(viewAction: .verifyCurrentSession)
+ }, onViewDetailsAction: { _ in
+ viewModel.send(viewAction: .viewCurrentSessionDetails)
+ })
+ } header: {
+ Text(VectorL10n.userSessionsOverviewCurrentSessionSectionTitle)
+ .textCase(.uppercase)
+ .font(theme.fonts.footnote)
+ .foregroundColor(theme.colors.secondaryContent)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.bottom, 12.0)
+ .padding(.top, 24.0)
+ }
+ .padding(.horizontal, 16)
+ }
+ }
private var otherSessionsSection: some View {
SwiftUI.Section {
@@ -83,17 +117,19 @@ struct UserSessionsOverview: View {
} header: {
VStack(alignment: .leading) {
Text(VectorL10n.userSessionsOverviewOtherSessionsSectionTitle)
+ .textCase(.uppercase)
.font(theme.fonts.footnote)
.foregroundColor(theme.colors.secondaryContent)
- .padding(.bottom, 10)
+ .padding(.bottom, 8.0)
Text(VectorL10n.userSessionsOverviewOtherSessionsSectionInfo)
.font(theme.fonts.footnote)
.foregroundColor(theme.colors.secondaryContent)
- .padding(.bottom, 11)
+ .padding(.bottom, 12.0)
}
- .padding(.horizontal, 16)
- .padding(.top, 24)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .padding(.horizontal, 16.0)
+ .padding(.top, 24.0)
}
}
}