mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-30 21:26:57 +02:00
Release 2.0.0
This commit is contained in:
+16
@@ -44,7 +44,23 @@ class UserSessionsDataProvider: UserSessionsDataProviderProtocol {
|
||||
session.crypto.device(withDeviceId: deviceId, ofUser: userId)
|
||||
}
|
||||
|
||||
func verificationState(for deviceInfo: MXDeviceInfo?) -> UserSessionInfo.VerificationState {
|
||||
guard let deviceInfo = deviceInfo else { return .unknown }
|
||||
|
||||
guard session.crypto?.crossSigning.canCrossSign == true else {
|
||||
return deviceInfo.deviceId == session.myDeviceId ? .unverified : .unknown
|
||||
}
|
||||
|
||||
return deviceInfo.trustLevel.isVerified ? .verified : .unverified
|
||||
}
|
||||
|
||||
func accountData(for eventType: String) -> [AnyHashable: Any]? {
|
||||
session.accountData.accountData(forEventType: eventType)
|
||||
}
|
||||
|
||||
func qrLoginAvailable() async throws -> Bool {
|
||||
let service = QRLoginService(client: session.matrixRestClient,
|
||||
mode: .authenticated)
|
||||
return try await service.isServiceAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -28,5 +28,9 @@ protocol UserSessionsDataProviderProtocol {
|
||||
|
||||
func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo?
|
||||
|
||||
func verificationState(for deviceInfo: MXDeviceInfo?) -> UserSessionInfo.VerificationState
|
||||
|
||||
func accountData(for eventType: String) -> [AnyHashable: Any]?
|
||||
|
||||
func qrLoginAvailable() async throws -> Bool
|
||||
}
|
||||
|
||||
+63
-29
@@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import MatrixSDK
|
||||
|
||||
class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
@@ -22,18 +22,22 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
private static let inactiveSessionDurationTreshold: TimeInterval = 90 * 86400
|
||||
|
||||
private let dataProvider: UserSessionsDataProviderProtocol
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
private(set) var overviewData: UserSessionsOverviewData
|
||||
private(set) var overviewDataPublisher: CurrentValueSubject<UserSessionsOverviewData, Never>
|
||||
private(set) var sessionInfos: [UserSessionInfo]
|
||||
|
||||
init(dataProvider: UserSessionsDataProviderProtocol) {
|
||||
self.dataProvider = dataProvider
|
||||
|
||||
overviewData = UserSessionsOverviewData(currentSession: nil,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: [],
|
||||
otherSessions: [])
|
||||
|
||||
overviewDataPublisher = .init(UserSessionsOverviewData(currentSession: nil,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: [],
|
||||
otherSessions: [],
|
||||
linkDeviceEnabled: false))
|
||||
sessionInfos = []
|
||||
setupInitialOverviewData()
|
||||
listenForSessionUpdates()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -42,8 +46,14 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
dataProvider.devices { response in
|
||||
switch response {
|
||||
case .success(let devices):
|
||||
self.overviewData = self.sessionsOverviewData(from: devices)
|
||||
completion(.success(self.overviewData))
|
||||
self.sessionInfos = self.sortedSessionInfos(from: devices)
|
||||
Task { @MainActor in
|
||||
let linkDeviceEnabled = try? await self.dataProvider.qrLoginAvailable()
|
||||
let overviewData = self.sessionsOverviewData(from: self.sessionInfos,
|
||||
linkDeviceEnabled: linkDeviceEnabled ?? false)
|
||||
self.overviewDataPublisher.send(overviewData)
|
||||
completion(.success(overviewData))
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
@@ -51,24 +61,43 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
}
|
||||
|
||||
func sessionForIdentifier(_ sessionId: String) -> UserSessionInfo? {
|
||||
if overviewData.currentSession?.id == sessionId {
|
||||
return overviewData.currentSession
|
||||
if currentSession?.id == sessionId {
|
||||
return currentSession
|
||||
}
|
||||
|
||||
return overviewData.otherSessions.first(where: { $0.id == sessionId })
|
||||
return otherSessions.first(where: { $0.id == sessionId })
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func listenForSessionUpdates() {
|
||||
NotificationCenter.default.publisher(for: .MXDeviceInfoTrustLevelDidChange)
|
||||
.sink { [weak self] _ in
|
||||
self?.updateOverviewData { _ in }
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
NotificationCenter.default.publisher(for: .MXDeviceListDidUpdateUsersDevices)
|
||||
.sink { [weak self] _ in
|
||||
self?.updateOverviewData { _ in }
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
NotificationCenter.default.publisher(for: .MXCrossSigningInfoTrustLevelDidChange)
|
||||
.sink { [weak self] _ in
|
||||
self?.updateOverviewData { _ in }
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func setupInitialOverviewData() {
|
||||
guard let currentSessionInfo = getCurrentSessionInfo() else {
|
||||
return
|
||||
}
|
||||
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSessionInfo,
|
||||
unverifiedSessions: currentSessionInfo.isVerified ? [] : [currentSessionInfo],
|
||||
inactiveSessions: currentSessionInfo.isActive ? [] : [currentSessionInfo],
|
||||
otherSessions: [])
|
||||
overviewDataPublisher = .init(UserSessionsOverviewData(currentSession: currentSessionInfo,
|
||||
unverifiedSessions: currentSessionInfo.verificationState == .verified ? [] : [currentSessionInfo],
|
||||
inactiveSessions: currentSessionInfo.isActive ? [] : [currentSessionInfo],
|
||||
otherSessions: [],
|
||||
linkDeviceEnabled: false))
|
||||
}
|
||||
|
||||
private func getCurrentSessionInfo() -> UserSessionInfo? {
|
||||
@@ -78,20 +107,25 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
}
|
||||
return sessionInfo(from: device, isCurrentSession: true)
|
||||
}
|
||||
|
||||
private func sessionsOverviewData(from devices: [MXDevice]) -> UserSessionsOverviewData {
|
||||
let allSessions = devices
|
||||
|
||||
private func sortedSessionInfos(from devices: [MXDevice]) -> [UserSessionInfo] {
|
||||
devices
|
||||
.sorted { $0.lastSeenTs > $1.lastSeenTs }
|
||||
.map { sessionInfo(from: $0, isCurrentSession: $0.deviceId == dataProvider.myDeviceId) }
|
||||
|
||||
return UserSessionsOverviewData(currentSession: allSessions.filter(\.isCurrent).first,
|
||||
unverifiedSessions: allSessions.filter { !$0.isVerified },
|
||||
inactiveSessions: allSessions.filter { !$0.isActive },
|
||||
otherSessions: allSessions.filter { !$0.isCurrent })
|
||||
}
|
||||
|
||||
private func sessionsOverviewData(from allSessions: [UserSessionInfo],
|
||||
linkDeviceEnabled: Bool) -> UserSessionsOverviewData {
|
||||
UserSessionsOverviewData(currentSession: allSessions.filter(\.isCurrent).first,
|
||||
unverifiedSessions: allSessions.filter { $0.verificationState == .unverified && !$0.isCurrent },
|
||||
inactiveSessions: allSessions.filter { !$0.isActive },
|
||||
otherSessions: allSessions.filter { !$0.isCurrent },
|
||||
linkDeviceEnabled: linkDeviceEnabled)
|
||||
}
|
||||
|
||||
private func sessionInfo(from device: MXDevice, isCurrentSession: Bool) -> UserSessionInfo {
|
||||
let isSessionVerified = deviceInfo(for: device.deviceId)?.trustLevel.isVerified ?? false
|
||||
let deviceInfo = deviceInfo(for: device.deviceId)
|
||||
let verificationState = dataProvider.verificationState(for: deviceInfo)
|
||||
|
||||
let eventType = kMXAccountDataTypeClientInformation + "." + device.deviceId
|
||||
let appData = dataProvider.accountData(for: eventType)
|
||||
@@ -110,7 +144,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
return UserSessionInfo(withDevice: device,
|
||||
applicationData: appData as? [String: String],
|
||||
userAgent: userAgent,
|
||||
isSessionVerified: isSessionVerified,
|
||||
verificationState: verificationState,
|
||||
isActive: isSessionActive,
|
||||
isCurrent: isCurrentSession)
|
||||
}
|
||||
@@ -128,13 +162,13 @@ extension UserSessionInfo {
|
||||
init(withDevice device: MXDevice,
|
||||
applicationData: [String: String]?,
|
||||
userAgent: UserAgent?,
|
||||
isSessionVerified: Bool,
|
||||
verificationState: VerificationState,
|
||||
isActive: Bool,
|
||||
isCurrent: Bool) {
|
||||
self.init(id: device.deviceId,
|
||||
name: device.displayName,
|
||||
deviceType: userAgent?.deviceType ?? .unknown,
|
||||
isVerified: isSessionVerified,
|
||||
verificationState: verificationState,
|
||||
lastSeenIP: device.lastSeenIp,
|
||||
lastSeenTimestamp: device.lastSeenTs > 0 ? TimeInterval(device.lastSeenTs / 1000) : nil,
|
||||
applicationName: applicationData?["name"],
|
||||
|
||||
+39
-33
@@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
enum Mode {
|
||||
@@ -27,15 +27,17 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
|
||||
private let mode: Mode
|
||||
|
||||
var overviewData: UserSessionsOverviewData
|
||||
var overviewDataPublisher: CurrentValueSubject<UserSessionsOverviewData, Never>
|
||||
var sessionInfos = [UserSessionInfo]()
|
||||
|
||||
init(mode: Mode = .currentSessionUnverified) {
|
||||
self.mode = mode
|
||||
|
||||
overviewData = UserSessionsOverviewData(currentSession: nil,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: [],
|
||||
otherSessions: [])
|
||||
overviewDataPublisher = .init(UserSessionsOverviewData(currentSession: nil,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: [],
|
||||
otherSessions: [],
|
||||
linkDeviceEnabled: false))
|
||||
}
|
||||
|
||||
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) {
|
||||
@@ -44,43 +46,47 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
|
||||
switch mode {
|
||||
case .noOtherSessions:
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: [],
|
||||
otherSessions: [])
|
||||
overviewDataPublisher.send(UserSessionsOverviewData(currentSession: mockCurrentSession,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: [],
|
||||
otherSessions: [],
|
||||
linkDeviceEnabled: false))
|
||||
case .onlyUnverifiedSessions:
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||
unverifiedSessions: unverifiedSessions + [currentSession],
|
||||
inactiveSessions: [],
|
||||
otherSessions: unverifiedSessions)
|
||||
overviewDataPublisher.send(UserSessionsOverviewData(currentSession: mockCurrentSession,
|
||||
unverifiedSessions: unverifiedSessions + [mockCurrentSession],
|
||||
inactiveSessions: [],
|
||||
otherSessions: unverifiedSessions,
|
||||
linkDeviceEnabled: false))
|
||||
case .onlyInactiveSessions:
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: inactiveSessions,
|
||||
otherSessions: inactiveSessions)
|
||||
overviewDataPublisher.send(UserSessionsOverviewData(currentSession: mockCurrentSession,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: inactiveSessions,
|
||||
otherSessions: inactiveSessions,
|
||||
linkDeviceEnabled: false))
|
||||
default:
|
||||
let otherSessions = unverifiedSessions + inactiveSessions + buildSessions(verified: true, active: true)
|
||||
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||
unverifiedSessions: unverifiedSessions,
|
||||
inactiveSessions: inactiveSessions,
|
||||
otherSessions: otherSessions)
|
||||
overviewDataPublisher.send(UserSessionsOverviewData(currentSession: mockCurrentSession,
|
||||
unverifiedSessions: unverifiedSessions,
|
||||
inactiveSessions: inactiveSessions,
|
||||
otherSessions: otherSessions,
|
||||
linkDeviceEnabled: true))
|
||||
}
|
||||
|
||||
completion(.success(overviewData))
|
||||
completion(.success(overviewDataPublisher.value))
|
||||
}
|
||||
|
||||
func sessionForIdentifier(_ sessionId: String) -> UserSessionInfo? {
|
||||
overviewData.otherSessions.first { $0.id == sessionId }
|
||||
otherSessions.first { $0.id == sessionId }
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var currentSession: UserSessionInfo {
|
||||
private var mockCurrentSession: UserSessionInfo {
|
||||
UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: mode == .currentSessionVerified,
|
||||
verificationState: mode == .currentSessionVerified ? .verified : .unverified,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
@@ -99,14 +105,14 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
[UserSessionInfo(id: "1 verified: \(verified) active: \(active)",
|
||||
name: "macOS verified: \(verified) active: \(active)",
|
||||
deviceType: .desktop,
|
||||
isVerified: verified,
|
||||
verificationState: verified ? .verified : .unverified,
|
||||
lastSeenIP: "1.0.0.1",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000,
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 8_000_000,
|
||||
applicationName: "Element MacOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "macOS 12.5.1",
|
||||
deviceOS: "macOS",
|
||||
lastSeenIPLocation: nil,
|
||||
clientName: "Electron",
|
||||
clientVersion: "20.0.0",
|
||||
@@ -115,14 +121,14 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
UserSessionInfo(id: "2 verified: \(verified) active: \(active)",
|
||||
name: "Firefox on Windows verified: \(verified) active: \(active)",
|
||||
deviceType: .web,
|
||||
isVerified: verified,
|
||||
verificationState: verified ? .verified : .unverified,
|
||||
lastSeenIP: "2.0.0.2",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
applicationName: "Element Web",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "Windows 10",
|
||||
deviceOS: "Windows",
|
||||
lastSeenIPLocation: nil,
|
||||
clientName: "Firefox",
|
||||
clientVersion: "39.0",
|
||||
@@ -131,7 +137,7 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
UserSessionInfo(id: "3 verified: \(verified) active: \(active)",
|
||||
name: "Android verified: \(verified) active: \(active)",
|
||||
deviceType: .mobile,
|
||||
isVerified: verified,
|
||||
verificationState: verified ? .verified : .unverified,
|
||||
lastSeenIP: "3.0.0.3",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10,
|
||||
applicationName: "Element Android",
|
||||
|
||||
+17
-2
@@ -14,19 +14,34 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
struct UserSessionsOverviewData {
|
||||
let currentSession: UserSessionInfo?
|
||||
let unverifiedSessions: [UserSessionInfo]
|
||||
let inactiveSessions: [UserSessionInfo]
|
||||
let otherSessions: [UserSessionInfo]
|
||||
let linkDeviceEnabled: Bool
|
||||
}
|
||||
|
||||
protocol UserSessionsOverviewServiceProtocol {
|
||||
var overviewData: UserSessionsOverviewData { get }
|
||||
var overviewDataPublisher: CurrentValueSubject<UserSessionsOverviewData, Never> { get }
|
||||
var sessionInfos: [UserSessionInfo] { get }
|
||||
|
||||
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) -> Void
|
||||
|
||||
func sessionForIdentifier(_ sessionId: String) -> UserSessionInfo?
|
||||
}
|
||||
|
||||
extension UserSessionsOverviewServiceProtocol {
|
||||
/// The user's current session.
|
||||
var currentSession: UserSessionInfo? { overviewDataPublisher.value.currentSession }
|
||||
/// Any unverified sessions on the user's account.
|
||||
var unverifiedSessions: [UserSessionInfo] { overviewDataPublisher.value.unverifiedSessions }
|
||||
/// Any inactive sessions on the user's account (not seen for a while).
|
||||
var inactiveSessions: [UserSessionInfo] { overviewDataPublisher.value.inactiveSessions }
|
||||
/// Any sessions that are verified and have been seen recently.
|
||||
var otherSessions: [UserSessionInfo] { overviewDataPublisher.value.otherSessions }
|
||||
/// Whether it is possible to link a new device via a QR code.
|
||||
var linkDeviceEnabled: Bool { overviewDataPublisher.value.linkDeviceEnabled }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user