mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-28 20:26:57 +02:00
Extended device info (PSG-772) (#6766)
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct UserAgent {
|
||||
let deviceType: DeviceType
|
||||
let deviceModel: String?
|
||||
let deviceOS: String?
|
||||
let clientName: String?
|
||||
let clientVersion: String?
|
||||
|
||||
static let unknown = UserAgent(deviceType: .unknown,
|
||||
deviceModel: nil,
|
||||
deviceOS: nil,
|
||||
clientName: nil,
|
||||
clientVersion: nil)
|
||||
}
|
||||
|
||||
extension UserAgent: Equatable { }
|
||||
|
||||
enum UserAgentParser {
|
||||
private enum Constants {
|
||||
static let deviceInfoRegexPattern = "\\((?:[^)(]+|\\((?:[^)(]+|\\([^)(]*\\))*\\))*\\)"
|
||||
|
||||
static let androidKeyword = "; MatrixAndroidSdk2"
|
||||
static let iosKeyword = "; iOS "
|
||||
static let desktopKeyword = " Electron/"
|
||||
static let webKeyword = "Mozilla/"
|
||||
}
|
||||
|
||||
static func parse(_ userAgent: String) -> UserAgent {
|
||||
if userAgent.vc_caseInsensitiveContains(Constants.androidKeyword) {
|
||||
return parseAndroid(userAgent)
|
||||
} else if userAgent.vc_caseInsensitiveContains(Constants.iosKeyword) {
|
||||
return parseIOS(userAgent)
|
||||
} else if userAgent.vc_caseInsensitiveContains(Constants.desktopKeyword) {
|
||||
return parseDesktop(userAgent)
|
||||
} else if userAgent.vc_caseInsensitiveContains(Constants.webKeyword) {
|
||||
return parseWeb(userAgent)
|
||||
}
|
||||
return .unknown
|
||||
}
|
||||
|
||||
// Legacy: Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)
|
||||
// New: Element dbg/1.5.0-dev (Xiaomi Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.0)
|
||||
private static func parseAndroid(_ userAgent: String) -> UserAgent {
|
||||
var deviceModel: String?
|
||||
var deviceOS: String?
|
||||
var clientName: String?
|
||||
var clientVersion: String?
|
||||
|
||||
let (beforeSlash, afterSlash) = userAgent.splitByFirst("/")
|
||||
clientName = beforeSlash
|
||||
if let afterSlash = afterSlash {
|
||||
let (beforeSpace, afterSpace) = afterSlash.splitByFirst(" ")
|
||||
clientVersion = beforeSpace
|
||||
if let afterSpace = afterSpace {
|
||||
if let deviceInfo = findFirstDeviceInfo(in: afterSpace) {
|
||||
let deviceInfoComponents = deviceInfo.components(separatedBy: "; ")
|
||||
let isLegacy = deviceInfoComponents[safe: 0] == "Linux"
|
||||
if isLegacy {
|
||||
// find the segment starting with "Android"
|
||||
if let osSegmentIndex = deviceInfoComponents.firstIndex(where: { $0.hasPrefix("Android") }) {
|
||||
deviceOS = deviceInfoComponents[safe: osSegmentIndex]
|
||||
deviceModel = deviceInfoComponents[safe: osSegmentIndex + 1]
|
||||
}
|
||||
} else {
|
||||
deviceModel = deviceInfoComponents[safe: 0]
|
||||
deviceOS = deviceInfoComponents[safe: 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return UserAgent(deviceType: .mobile,
|
||||
deviceModel: deviceModel,
|
||||
deviceOS: deviceOS,
|
||||
clientName: clientName,
|
||||
clientVersion: clientVersion)
|
||||
}
|
||||
|
||||
// Legacy: Riot/1.8.21 (iPhone; iOS 15.2; Scale/3.00)
|
||||
// New: Riot/1.8.21 (iPhone X; iOS 15.2; Scale/3.00)
|
||||
private static func parseIOS(_ userAgent: String) -> UserAgent {
|
||||
var deviceModel: String?
|
||||
var deviceOS: String?
|
||||
var clientName: String?
|
||||
var clientVersion: String?
|
||||
|
||||
let (beforeSlash, afterSlash) = userAgent.splitByFirst("/")
|
||||
clientName = beforeSlash
|
||||
if let afterSlash = afterSlash {
|
||||
let (beforeSpace, afterSpace) = afterSlash.splitByFirst(" ")
|
||||
clientVersion = beforeSpace
|
||||
if let afterSpace = afterSpace {
|
||||
if let deviceInfo = findFirstDeviceInfo(in: afterSpace) {
|
||||
let deviceInfoComponents = deviceInfo.components(separatedBy: "; ")
|
||||
deviceModel = deviceInfoComponents[safe: 0]
|
||||
deviceOS = deviceInfoComponents[safe: 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return UserAgent(deviceType: .mobile,
|
||||
deviceModel: deviceModel,
|
||||
deviceOS: deviceOS,
|
||||
clientName: clientName,
|
||||
clientVersion: clientVersion)
|
||||
}
|
||||
|
||||
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36
|
||||
private static func parseDesktop(_ userAgent: String) -> UserAgent {
|
||||
var deviceOS: String?
|
||||
let browserName = browserName(for: userAgent)
|
||||
|
||||
if let deviceInfo = findFirstDeviceInfo(in: userAgent) {
|
||||
let deviceInfoComponents = deviceInfo.components(separatedBy: "; ")
|
||||
deviceOS = deviceInfoComponents[safe: 1]?.hasPrefix("Android") == true ? deviceInfoComponents[safe: 1] : deviceInfoComponents.first
|
||||
}
|
||||
|
||||
return UserAgent(deviceType: .desktop,
|
||||
deviceModel: browserName,
|
||||
deviceOS: deviceOS,
|
||||
clientName: nil,
|
||||
clientVersion: nil)
|
||||
}
|
||||
|
||||
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
|
||||
private static func parseWeb(_ userAgent: String) -> UserAgent {
|
||||
let desktopUserAgent = parseDesktop(userAgent)
|
||||
|
||||
return UserAgent(deviceType: .web,
|
||||
deviceModel: desktopUserAgent.deviceModel,
|
||||
deviceOS: desktopUserAgent.deviceOS,
|
||||
clientName: desktopUserAgent.clientName,
|
||||
clientVersion: desktopUserAgent.clientVersion)
|
||||
}
|
||||
|
||||
private static func findFirstDeviceInfo(in string: String) -> String? {
|
||||
guard let regex = try? NSRegularExpression(pattern: Constants.deviceInfoRegexPattern,
|
||||
options: .caseInsensitive) else {
|
||||
return nil
|
||||
}
|
||||
var range = regex.rangeOfFirstMatch(in: string, range: NSRange(string.startIndex..., in: string))
|
||||
if range.location != NSNotFound {
|
||||
range.location += 1
|
||||
range.length -= 2
|
||||
return string[range]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func browserName(for userAgent: String) -> String? {
|
||||
let components = userAgent.components(separatedBy: " ")
|
||||
if components.last?.hasPrefix("Firefox") == true {
|
||||
return "Firefox"
|
||||
} else if components.last?.hasPrefix("Safari") == true
|
||||
&& components[safe:components.count - 2]?.hasPrefix("Mobile") == true {
|
||||
// mobile browser
|
||||
let possibleBrowserName = components[safe:components.count - 3]?.components(separatedBy: "/").first
|
||||
return possibleBrowserName == "Version" ? "Safari" : possibleBrowserName
|
||||
} else if components.last?.hasPrefix("Safari") == true && components[safe:components.count - 2]?.hasPrefix("Version") == true {
|
||||
return "Safari"
|
||||
} else {
|
||||
// regular browser
|
||||
return components[safe:components.count - 2]?.components(separatedBy: "/").first
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
subscript(_ range: NSRange) -> String {
|
||||
let start = index(startIndex, offsetBy: range.lowerBound)
|
||||
let end = index(startIndex, offsetBy: range.upperBound)
|
||||
let subString = self[start..<end]
|
||||
return String(subString)
|
||||
}
|
||||
|
||||
func splitByFirst(_ delimiter: Character) -> (String?, String?) {
|
||||
guard let delimiterIndex = firstIndex(of: delimiter) else {
|
||||
return (nil, nil)
|
||||
}
|
||||
let before = String(prefix(upTo: delimiterIndex))
|
||||
let after = String(suffix(from: index(after: delimiterIndex)))
|
||||
return (before, after)
|
||||
}
|
||||
}
|
||||
@@ -35,16 +35,41 @@ struct UserSessionInfo: Identifiable {
|
||||
|
||||
/// Last time the session was active
|
||||
let lastSeenTimestamp: TimeInterval?
|
||||
|
||||
|
||||
// MARK: - Application Properties
|
||||
|
||||
/// Application name used by the session
|
||||
let applicationName: String?
|
||||
|
||||
/// Application version used by the session
|
||||
let applicationVersion: String?
|
||||
|
||||
/// Application URL used by the session. Only applicable for web sessions.
|
||||
let applicationURL: String?
|
||||
|
||||
// MARK: - Device Properties
|
||||
|
||||
/// Device model
|
||||
let deviceModel: String?
|
||||
|
||||
/// Device OS
|
||||
let deviceOS: String?
|
||||
|
||||
/// Last seen IP location
|
||||
let lastSeenIPLocation: String?
|
||||
|
||||
/// Device name
|
||||
let deviceName: String?
|
||||
|
||||
/// True to indicate that session has been used under `inactiveSessionDurationTreshold` value
|
||||
let isActive: Bool
|
||||
|
||||
|
||||
/// True to indicate that this is current user session
|
||||
let isCurrent: Bool
|
||||
}
|
||||
|
||||
extension UserSessionInfo: Equatable {
|
||||
static func == (lhs: UserSessionInfo, rhs: UserSessionInfo) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,17 +134,23 @@ struct UserSessionCardViewPreview: View {
|
||||
@Environment(\.theme) var theme: ThemeSwiftUI
|
||||
|
||||
let viewData: UserSessionCardViewData
|
||||
|
||||
init(isCurrentSessionInfo: Bool = false) {
|
||||
|
||||
init(isCurrent: Bool = false) {
|
||||
let session = UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My iPhone",
|
||||
isActive: true,
|
||||
isCurrent: isCurrentSessionInfo)
|
||||
|
||||
isCurrent: isCurrent)
|
||||
viewData = UserSessionCardViewData(session: session)
|
||||
}
|
||||
|
||||
@@ -161,8 +167,8 @@ struct UserSessionCardViewPreview: View {
|
||||
struct UserSessionCardView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
UserSessionCardViewPreview(isCurrentSessionInfo: true).theme(.light).preferredColorScheme(.light)
|
||||
UserSessionCardViewPreview(isCurrentSessionInfo: true).theme(.dark).preferredColorScheme(.dark)
|
||||
UserSessionCardViewPreview(isCurrent: true).theme(.light).preferredColorScheme(.light)
|
||||
UserSessionCardViewPreview(isCurrent: true).theme(.dark).preferredColorScheme(.dark)
|
||||
UserSessionCardViewPreview().theme(.light).preferredColorScheme(.light)
|
||||
UserSessionCardViewPreview().theme(.dark).preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
+20
-6
@@ -41,21 +41,35 @@ enum MockUserSessionDetailsScreenState: MockScreenState, CaseIterable {
|
||||
let session: UserSessionInfo
|
||||
switch self {
|
||||
case .allSections:
|
||||
session = UserSessionInfo(id: "session",
|
||||
session = UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My iPhone",
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
case .sessionSectionOnly:
|
||||
session = UserSessionInfo(id: "session",
|
||||
name: "iOS",
|
||||
session = UserSessionInfo(id: "3",
|
||||
name: "Android",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: nil,
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
lastSeenIP: "3.0.0.3",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10,
|
||||
applicationName: "Element Android",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "Android 4.0",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My Phone",
|
||||
isActive: true,
|
||||
isCurrent: false)
|
||||
}
|
||||
|
||||
+96
-49
@@ -20,17 +20,20 @@ import XCTest
|
||||
|
||||
class UserSessionDetailsViewModelTests: XCTestCase {
|
||||
func test_whenSessionNameAndLastSeenIPNil_viewStateCorrect() {
|
||||
let userSessionInfo = createUserSessionInfo(sessionId: "session",
|
||||
sessionName: nil,
|
||||
let userSessionInfo = createUserSessionInfo(id: "session",
|
||||
name: nil,
|
||||
lastSeenIP: nil)
|
||||
|
||||
let sessionItems = [
|
||||
sessionIdItem(sessionId: "session")
|
||||
]
|
||||
|
||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
sessionItems.append(sessionIdItem(sessionId: userSessionInfo.id))
|
||||
|
||||
var sections = [UserSessionDetailsSectionViewData]()
|
||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems))
|
||||
let sections = [
|
||||
UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems)
|
||||
]
|
||||
|
||||
let expectedModel = UserSessionDetailsViewState(sections: sections)
|
||||
let sut = UserSessionDetailsViewModel(session: userSessionInfo)
|
||||
|
||||
@@ -38,18 +41,20 @@ class UserSessionDetailsViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
func test_whenSessionNameNotNilLastSeenIPNil_viewStateCorrect() {
|
||||
let userSessionInfo = createUserSessionInfo(sessionId: "session",
|
||||
sessionName: "session name",
|
||||
let userSessionInfo = createUserSessionInfo(id: "session",
|
||||
name: "session name",
|
||||
lastSeenIP: nil)
|
||||
|
||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
sessionItems.append(sessionNameItem(sessionName: "session name"))
|
||||
sessionItems.append(sessionIdItem(sessionId: userSessionInfo.id))
|
||||
|
||||
var sections = [UserSessionDetailsSectionViewData]()
|
||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems))
|
||||
|
||||
let sessionItems = [
|
||||
sessionNameItem(sessionName: "session name"),
|
||||
sessionIdItem(sessionId: "session")
|
||||
]
|
||||
|
||||
let sections = [
|
||||
UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems)
|
||||
]
|
||||
|
||||
let expectedModel = UserSessionDetailsViewState(sections: sections)
|
||||
let sut = UserSessionDetailsViewModel(session: userSessionInfo)
|
||||
@@ -58,56 +63,98 @@ class UserSessionDetailsViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
func test_whenUserSessionInfoContainsAllValues_viewStateCorrect() {
|
||||
let userSessionInfo = createUserSessionInfo(sessionId: "session",
|
||||
sessionName: "session name",
|
||||
lastSeenIP: "0.0.0.0")
|
||||
let userSessionInfo = createUserSessionInfo(id: "session",
|
||||
name: "session name",
|
||||
lastSeenIP: "0.0.0.0",
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0")
|
||||
|
||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
sessionItems.append(sessionNameItem(sessionName: "session name"))
|
||||
sessionItems.append(sessionIdItem(sessionId: userSessionInfo.id))
|
||||
|
||||
var sections = [UserSessionDetailsSectionViewData]()
|
||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems))
|
||||
|
||||
var deviceSectionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
deviceSectionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsDeviceIpAddress,
|
||||
value: "0.0.0.0"))
|
||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
|
||||
footer: nil,
|
||||
items: deviceSectionItems))
|
||||
let sessionItems = [
|
||||
sessionNameItem(sessionName: "session name"),
|
||||
sessionIdItem(sessionId: "session")
|
||||
]
|
||||
let appItems = [
|
||||
appNameItem(appName: "Element iOS"),
|
||||
appVersionItem(appVersion: "1.0.0")
|
||||
]
|
||||
let deviceItems = [
|
||||
ipAddressItem(ipAddress: "0.0.0.0")
|
||||
]
|
||||
|
||||
let sections = [
|
||||
UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems),
|
||||
UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsApplicationSectionHeader.uppercased(),
|
||||
footer: nil,
|
||||
items: appItems),
|
||||
UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
|
||||
footer: nil,
|
||||
items: deviceItems)
|
||||
]
|
||||
|
||||
let expectedModel = UserSessionDetailsViewState(sections: sections)
|
||||
let sut = UserSessionDetailsViewModel(session: userSessionInfo)
|
||||
|
||||
XCTAssertEqual(sut.state, expectedModel)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func createUserSessionInfo(sessionId: String,
|
||||
sessionName: String?,
|
||||
private func createUserSessionInfo(id: String,
|
||||
name: String?,
|
||||
deviceType: DeviceType = .mobile,
|
||||
isVerified: Bool = false,
|
||||
lastSeenIP: String?,
|
||||
lastSeenTimestamp: TimeInterval = Date().timeIntervalSince1970,
|
||||
isCurrentSession: Bool = true) -> UserSessionInfo {
|
||||
UserSessionInfo(id: sessionId,
|
||||
name: sessionName,
|
||||
applicationName: String? = nil,
|
||||
applicationVersion: String? = nil,
|
||||
applicationURL: String? = nil,
|
||||
deviceModel: String? = nil,
|
||||
deviceOS: String? = nil,
|
||||
lastSeenIPLocation: String? = nil,
|
||||
deviceName: String? = nil,
|
||||
isActive: Bool = true,
|
||||
isCurrent: Bool = true) -> UserSessionInfo {
|
||||
UserSessionInfo(id: id,
|
||||
name: name,
|
||||
deviceType: deviceType,
|
||||
isVerified: isVerified,
|
||||
lastSeenIP: lastSeenIP,
|
||||
lastSeenTimestamp: lastSeenTimestamp,
|
||||
isActive: true,
|
||||
isCurrent: isCurrentSession)
|
||||
applicationName: applicationName,
|
||||
applicationVersion: applicationVersion,
|
||||
applicationURL: applicationURL,
|
||||
deviceModel: deviceModel,
|
||||
deviceOS: deviceOS,
|
||||
lastSeenIPLocation: lastSeenIPLocation,
|
||||
deviceName: deviceName,
|
||||
isActive: isActive,
|
||||
isCurrent: isCurrent)
|
||||
}
|
||||
|
||||
private func sessionNameItem(sessionName: String) -> UserSessionDetailsSectionItemViewData {
|
||||
UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsSessionName,
|
||||
value: sessionName)
|
||||
.init(title: VectorL10n.userSessionDetailsSessionName,
|
||||
value: sessionName)
|
||||
}
|
||||
|
||||
private func sessionIdItem(sessionId: String) -> UserSessionDetailsSectionItemViewData {
|
||||
UserSessionDetailsSectionItemViewData(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
|
||||
value: sessionId)
|
||||
.init(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
|
||||
value: sessionId)
|
||||
}
|
||||
|
||||
private func appNameItem(appName: String) -> UserSessionDetailsSectionItemViewData {
|
||||
.init(title: VectorL10n.userSessionDetailsApplicationName,
|
||||
value: appName)
|
||||
}
|
||||
|
||||
private func appVersionItem(appVersion: String) -> UserSessionDetailsSectionItemViewData {
|
||||
.init(title: VectorL10n.userSessionDetailsApplicationVersion,
|
||||
value: appVersion)
|
||||
}
|
||||
|
||||
private func ipAddressItem(ipAddress: String) -> UserSessionDetailsSectionItemViewData {
|
||||
.init(title: VectorL10n.userSessionDetailsDeviceIpAddress,
|
||||
value: ipAddress)
|
||||
}
|
||||
}
|
||||
|
||||
+56
-15
@@ -32,8 +32,12 @@ class UserSessionDetailsViewModel: UserSessionDetailsViewModelType, UserSessionD
|
||||
|
||||
private func updateViewState(session: UserSessionInfo) {
|
||||
var sections = [UserSessionDetailsSectionViewData]()
|
||||
|
||||
|
||||
sections.append(sessionSection(session: session))
|
||||
|
||||
if let applicationSection = applicationSection(session: session) {
|
||||
sections.append(applicationSection)
|
||||
}
|
||||
|
||||
if let deviceSection = deviceSection(session: session) {
|
||||
sections.append(deviceSection)
|
||||
@@ -43,31 +47,68 @@ class UserSessionDetailsViewModel: UserSessionDetailsViewModelType, UserSessionD
|
||||
}
|
||||
|
||||
private func sessionSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData {
|
||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
|
||||
var sessionItems: [UserSessionDetailsSectionItemViewData] = []
|
||||
|
||||
if let sessionName = session.name {
|
||||
sessionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsSessionName,
|
||||
value: sessionName))
|
||||
sessionItems.append(.init(title: VectorL10n.userSessionDetailsSessionName,
|
||||
value: sessionName))
|
||||
}
|
||||
|
||||
sessionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
|
||||
value: session.id))
|
||||
sessionItems.append(.init(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
|
||||
value: session.id))
|
||||
|
||||
return UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems)
|
||||
return .init(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems)
|
||||
}
|
||||
|
||||
private func applicationSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData? {
|
||||
var sessionItems: [UserSessionDetailsSectionItemViewData] = []
|
||||
|
||||
if let name = session.applicationName {
|
||||
sessionItems.append(.init(title: VectorL10n.userSessionDetailsApplicationName,
|
||||
value: name))
|
||||
}
|
||||
if let version = session.applicationVersion {
|
||||
sessionItems.append(.init(title: VectorL10n.userSessionDetailsApplicationVersion,
|
||||
value: version))
|
||||
}
|
||||
if let url = session.applicationURL {
|
||||
sessionItems.append(.init(title: VectorL10n.userSessionDetailsApplicationUrl,
|
||||
value: url))
|
||||
}
|
||||
|
||||
guard !sessionItems.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return .init(header: VectorL10n.userSessionDetailsApplicationSectionHeader.uppercased(),
|
||||
footer: nil,
|
||||
items: sessionItems)
|
||||
}
|
||||
|
||||
private func deviceSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData? {
|
||||
var deviceSectionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
|
||||
if let model = session.deviceModel {
|
||||
deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceModel,
|
||||
value: model))
|
||||
}
|
||||
if let deviceOS = session.deviceOS {
|
||||
deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceOs,
|
||||
value: deviceOS))
|
||||
}
|
||||
if let lastSeenIP = session.lastSeenIP {
|
||||
deviceSectionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsDeviceIpAddress,
|
||||
value: lastSeenIP))
|
||||
deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceIpAddress,
|
||||
value: lastSeenIP))
|
||||
}
|
||||
if let lastSeenIPLocation = session.lastSeenIPLocation {
|
||||
deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceIpLocation,
|
||||
value: lastSeenIPLocation))
|
||||
}
|
||||
if deviceSectionItems.count > 0 {
|
||||
return UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
|
||||
footer: nil,
|
||||
items: deviceSectionItems)
|
||||
return .init(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
|
||||
footer: nil,
|
||||
items: deviceSectionItems)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+33
-20
@@ -38,30 +38,43 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable {
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
let viewModel: UserSessionOverviewViewModel
|
||||
let session: UserSessionInfo
|
||||
switch self {
|
||||
case .currentSession:
|
||||
let session = UserSessionInfo(id: "session",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
viewModel = UserSessionOverviewViewModel(session: session)
|
||||
session = UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My iPhone",
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
case .otherSession:
|
||||
let session = UserSessionInfo(id: "session",
|
||||
name: "Mac",
|
||||
deviceType: .desktop,
|
||||
isVerified: true,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
isActive: true,
|
||||
isCurrent: false)
|
||||
viewModel = UserSessionOverviewViewModel(session: session)
|
||||
session = UserSessionInfo(id: "1",
|
||||
name: "macOS",
|
||||
deviceType: .desktop,
|
||||
isVerified: true,
|
||||
lastSeenIP: "1.0.0.1",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000,
|
||||
applicationName: "Element MacOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "macOS 12.5.1",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My Mac",
|
||||
isActive: false,
|
||||
isCurrent: false)
|
||||
}
|
||||
|
||||
|
||||
let viewModel = UserSessionOverviewViewModel(session: session)
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
return ([viewModel], AnyView(UserSessionOverview(viewModel: viewModel.context)))
|
||||
}
|
||||
|
||||
+7
@@ -52,6 +52,13 @@ class UserSessionOverviewViewModelTests: XCTestCase {
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
applicationName: "Element",
|
||||
applicationVersion: "1.9.7",
|
||||
applicationURL: nil,
|
||||
deviceModel: "iPhone XS",
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "Mobile",
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ struct UserSessionOverview: View {
|
||||
UserSessionCardView(viewData: viewModel.viewState.cardViewData, onVerifyAction: { _ in
|
||||
viewModel.send(viewAction: .verifyCurrentSession)
|
||||
},
|
||||
onViewDetailsAction: { _ in
|
||||
onViewDetailsAction: { _ in
|
||||
viewModel.send(viewAction: .viewSessionDetails)
|
||||
})
|
||||
.padding(16)
|
||||
@@ -39,8 +39,8 @@ struct UserSessionOverview: View {
|
||||
.background(theme.colors.system.ignoresSafeArea())
|
||||
.frame(maxHeight: .infinity)
|
||||
.navigationTitle(viewModel.viewState.isCurrentSession ?
|
||||
VectorL10n.userSessionOverviewCurrentSessionTitle :
|
||||
VectorL10n.userSessionOverviewSessionTitle)
|
||||
VectorL10n.userSessionOverviewCurrentSessionTitle :
|
||||
VectorL10n.userSessionOverviewSessionTitle)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
||||
private func showCurrentSessionOverview(session: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(session: session))
|
||||
}
|
||||
|
||||
|
||||
private func showUserSessionOverview(session: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(session: session))
|
||||
}
|
||||
|
||||
+42
-16
@@ -76,7 +76,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
}
|
||||
return sessionInfo(from: device, isCurrentSession: true)
|
||||
}
|
||||
|
||||
|
||||
private func sessionsOverviewData(from devices: [MXDevice]) -> UserSessionsOverviewData {
|
||||
let allSessions = devices
|
||||
.sorted { $0.lastSeenTs > $1.lastSeenTs }
|
||||
@@ -90,24 +90,25 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
|
||||
private func sessionInfo(from device: MXDevice, isCurrentSession: Bool) -> UserSessionInfo {
|
||||
let isSessionVerified = deviceInfo(for: device.deviceId)?.trustLevel.isVerified ?? false
|
||||
|
||||
var lastSeenTs: TimeInterval?
|
||||
if device.lastSeenTs > 0 {
|
||||
lastSeenTs = TimeInterval(device.lastSeenTs / 1000)
|
||||
}
|
||||
|
||||
|
||||
let eventType = kMXAccountDataTypeClientInformation + "." + device.deviceId
|
||||
let appData = mxSession.accountData.accountData(forEventType: eventType)
|
||||
var userAgent: UserAgent?
|
||||
var isSessionActive = true
|
||||
if let lastSeenTimestamp = lastSeenTs {
|
||||
let elapsedTime = Date().timeIntervalSince1970 - lastSeenTimestamp
|
||||
|
||||
if let lastSeenUserAgent = device.lastSeenUserAgent {
|
||||
userAgent = UserAgentParser.parse(lastSeenUserAgent)
|
||||
}
|
||||
|
||||
if device.lastSeenTs > 0 {
|
||||
let elapsedTime = Date().timeIntervalSince1970 - TimeInterval(device.lastSeenTs / 1000)
|
||||
isSessionActive = elapsedTime < Self.inactiveSessionDurationTreshold
|
||||
}
|
||||
|
||||
return UserSessionInfo(id: device.deviceId,
|
||||
name: device.displayName,
|
||||
deviceType: .unknown,
|
||||
isVerified: isSessionVerified,
|
||||
lastSeenIP: device.lastSeenIp,
|
||||
lastSeenTimestamp: lastSeenTs,
|
||||
|
||||
return UserSessionInfo(withDevice: device,
|
||||
applicationData: appData as? [String: String],
|
||||
userAgent: userAgent,
|
||||
isSessionVerified: isSessionVerified,
|
||||
isActive: isSessionActive,
|
||||
isCurrent: isCurrentSession)
|
||||
}
|
||||
@@ -120,3 +121,28 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
return mxSession.crypto.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)
|
||||
}
|
||||
}
|
||||
|
||||
+63
-35
@@ -17,7 +17,7 @@
|
||||
import Foundation
|
||||
|
||||
class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
var overviewData: UserSessionsOverviewData
|
||||
let overviewData: UserSessionsOverviewData
|
||||
|
||||
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) {
|
||||
completion(.success(overviewData))
|
||||
@@ -34,38 +34,66 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
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,
|
||||
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)]
|
||||
}()
|
||||
static let allSessions = [
|
||||
UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My iPhone",
|
||||
isActive: true,
|
||||
isCurrent: true),
|
||||
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),
|
||||
UserSessionInfo(id: "2",
|
||||
name: "Firefox on Windows",
|
||||
deviceType: .web,
|
||||
isVerified: true,
|
||||
lastSeenIP: "2.0.0.2",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
applicationName: "Element Web",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "Windows 10",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My Windows",
|
||||
isActive: true,
|
||||
isCurrent: false),
|
||||
UserSessionInfo(id: "3",
|
||||
name: "Android",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "3.0.0.3",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10,
|
||||
applicationName: "Element Android",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "Android 4.0",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My Phone",
|
||||
isActive: true,
|
||||
isCurrent: false)
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user