mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Merge pull request #6789 from vector-im/gil/6787-Add_support_for_MSC3881
Added support for MSC3881
This commit is contained in:
commit
c8b1657e99
25 changed files with 489 additions and 59 deletions
|
@ -2388,6 +2388,8 @@ To enable access, tap Settings> Location and select Always";
|
|||
"user_session_verified_additional_info" = "Your current session is ready for secure messaging.";
|
||||
"user_session_unverified_additional_info" = "Verify your current session for enhanced secure messaging.";
|
||||
|
||||
"user_session_push_notifications" = "Push notifications";
|
||||
"user_session_push_notifications_message" = "When turned on, this session will receive push notifications.";
|
||||
|
||||
// First item is client name and second item is session display name
|
||||
"user_session_name" = "%@: %@";
|
||||
|
|
|
@ -8563,6 +8563,14 @@ public class VectorL10n: NSObject {
|
|||
public static var userSessionOverviewSessionTitle: String {
|
||||
return VectorL10n.tr("Vector", "user_session_overview_session_title")
|
||||
}
|
||||
/// Push notifications
|
||||
public static var userSessionPushNotifications: String {
|
||||
return VectorL10n.tr("Vector", "user_session_push_notifications")
|
||||
}
|
||||
/// When turned on, this session will receive push notifications.
|
||||
public static var userSessionPushNotificationsMessage: String {
|
||||
return VectorL10n.tr("Vector", "user_session_push_notifications_message")
|
||||
}
|
||||
/// Unverified session
|
||||
public static var userSessionUnverified: String {
|
||||
return VectorL10n.tr("Vector", "user_session_unverified")
|
||||
|
|
|
@ -496,12 +496,12 @@ class CallPresenter: NSObject {
|
|||
#if canImport(JitsiMeetSDK)
|
||||
JMCallKitProxy.removeListener(self)
|
||||
|
||||
guard let session = sessions.first else {
|
||||
guard let sessionInfo = sessions.first else {
|
||||
return
|
||||
}
|
||||
|
||||
if let widgetEventsListener = widgetEventsListener {
|
||||
session.removeListener(widgetEventsListener)
|
||||
sessionInfo.removeListener(widgetEventsListener)
|
||||
}
|
||||
widgetEventsListener = nil
|
||||
#endif
|
||||
|
@ -872,11 +872,11 @@ extension CallPresenter: JMCallKitListener {
|
|||
|
||||
}
|
||||
|
||||
func providerDidActivateAudioSession(session: AVAudioSession) {
|
||||
func providerDidActivateAudioSession(sessionInfo: AVAudioSession) {
|
||||
|
||||
}
|
||||
|
||||
func providerDidDeactivateAudioSession(session: AVAudioSession) {
|
||||
func providerDidDeactivateAudioSession(sessionInfo: AVAudioSession) {
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -138,7 +138,7 @@ struct UserSessionCardViewPreview: View {
|
|||
let viewData: UserSessionCardViewData
|
||||
|
||||
init(isCurrent: Bool = false) {
|
||||
let session = UserSessionInfo(id: "alice",
|
||||
let sessionInfo = UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
|
@ -153,7 +153,7 @@ struct UserSessionCardViewPreview: View {
|
|||
deviceName: "My iPhone",
|
||||
isActive: true,
|
||||
isCurrent: isCurrent)
|
||||
viewData = UserSessionCardViewData(session: session)
|
||||
viewData = UserSessionCardViewData(sessionInfo: sessionInfo)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
|
|
@ -63,13 +63,13 @@ struct UserSessionCardViewData {
|
|||
}
|
||||
|
||||
extension UserSessionCardViewData {
|
||||
init(session: UserSessionInfo) {
|
||||
self.init(sessionId: session.id,
|
||||
sessionDisplayName: session.name,
|
||||
deviceType: session.deviceType,
|
||||
isVerified: session.isVerified,
|
||||
lastActivityTimestamp: session.lastSeenTimestamp,
|
||||
lastSeenIP: session.lastSeenIP,
|
||||
isCurrentSessionDisplayMode: session.isCurrent)
|
||||
init(sessionInfo: UserSessionInfo) {
|
||||
self.init(sessionId: sessionInfo.id,
|
||||
sessionDisplayName: sessionInfo.name,
|
||||
deviceType: sessionInfo.deviceType,
|
||||
isVerified: sessionInfo.isVerified,
|
||||
lastActivityTimestamp: sessionInfo.lastSeenTimestamp,
|
||||
lastSeenIP: sessionInfo.lastSeenIP,
|
||||
isCurrentSessionDisplayMode: sessionInfo.isCurrent)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,37 +53,37 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable {
|
|||
coordinator.completion = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case let .openSessionOverview(session: session):
|
||||
self.openSessionOverview(session: session)
|
||||
case let .openSessionOverview(sessionInfo: sessionInfo):
|
||||
self.openSessionOverview(sessionInfo: sessionInfo)
|
||||
}
|
||||
}
|
||||
return coordinator
|
||||
}
|
||||
|
||||
private func openSessionDetails(session: UserSessionInfo) {
|
||||
let coordinator = createUserSessionDetailsCoordinator(session: session)
|
||||
private func openSessionDetails(sessionInfo: UserSessionInfo) {
|
||||
let coordinator = createUserSessionDetailsCoordinator(sessionInfo: sessionInfo)
|
||||
pushScreen(with: coordinator)
|
||||
}
|
||||
|
||||
private func createUserSessionDetailsCoordinator(session: UserSessionInfo) -> UserSessionDetailsCoordinator {
|
||||
let parameters = UserSessionDetailsCoordinatorParameters(session: session)
|
||||
private func createUserSessionDetailsCoordinator(sessionInfo: UserSessionInfo) -> UserSessionDetailsCoordinator {
|
||||
let parameters = UserSessionDetailsCoordinatorParameters(session: sessionInfo)
|
||||
return UserSessionDetailsCoordinator(parameters: parameters)
|
||||
}
|
||||
|
||||
private func openSessionOverview(session: UserSessionInfo) {
|
||||
let coordinator = createUserSessionOverviewCoordinator(session: session)
|
||||
private func openSessionOverview(sessionInfo: UserSessionInfo) {
|
||||
let coordinator = createUserSessionOverviewCoordinator(sessionInfo: sessionInfo)
|
||||
coordinator.completion = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case let .openSessionDetails(session: session):
|
||||
self.openSessionDetails(session: session)
|
||||
case let .openSessionDetails(sessionInfo: sessionInfo):
|
||||
self.openSessionDetails(sessionInfo: sessionInfo)
|
||||
}
|
||||
}
|
||||
pushScreen(with: coordinator)
|
||||
}
|
||||
|
||||
private func createUserSessionOverviewCoordinator(session: UserSessionInfo) -> UserSessionOverviewCoordinator {
|
||||
let parameters = UserSessionOverviewCoordinatorParameters(session: session)
|
||||
private func createUserSessionOverviewCoordinator(sessionInfo: UserSessionInfo) -> UserSessionOverviewCoordinator {
|
||||
let parameters = UserSessionOverviewCoordinatorParameters(session: self.parameters.session, sessionInfo: sessionInfo)
|
||||
return UserSessionOverviewCoordinator(parameters: parameters)
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,8 @@ import CommonKit
|
|||
import SwiftUI
|
||||
|
||||
struct UserSessionOverviewCoordinatorParameters {
|
||||
let session: UserSessionInfo
|
||||
let session: MXSession
|
||||
let sessionInfo: UserSessionInfo
|
||||
}
|
||||
|
||||
final class UserSessionOverviewCoordinator: Coordinator, Presentable {
|
||||
|
@ -40,7 +41,8 @@ final class UserSessionOverviewCoordinator: Coordinator, Presentable {
|
|||
init(parameters: UserSessionOverviewCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = UserSessionOverviewViewModel(session: parameters.session)
|
||||
let service = UserSessionOverviewService(session: parameters.session, sessionInfo: parameters.sessionInfo)
|
||||
viewModel = UserSessionOverviewViewModel(sessionInfo: parameters.sessionInfo, service: service)
|
||||
|
||||
hostingController = VectorHostingController(rootView: UserSessionOverview(viewModel: viewModel.context))
|
||||
|
||||
|
@ -57,8 +59,8 @@ final class UserSessionOverviewCoordinator: Coordinator, Presentable {
|
|||
switch result {
|
||||
case .verifyCurrentSession:
|
||||
break // TODO:
|
||||
case let .showSessionDetails(session: session):
|
||||
self.completion?(.openSessionDetails(session: session))
|
||||
case let .showSessionDetails(sessionInfo: sessionInfo):
|
||||
self.completion?(.openSessionDetails(sessionInfo: sessionInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,9 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable {
|
|||
// mock that screen.
|
||||
case currentSession
|
||||
case otherSession
|
||||
|
||||
case sessionWithPushNotifications(enabled: Bool)
|
||||
case remotelyTogglingPushersNotAvailable
|
||||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
UserSessionOverview.self
|
||||
|
@ -33,12 +35,17 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable {
|
|||
|
||||
/// A list of screen state definitions
|
||||
static var allCases: [MockUserSessionOverviewScreenState] {
|
||||
[.currentSession, .otherSession]
|
||||
[.currentSession,
|
||||
.otherSession,
|
||||
.sessionWithPushNotifications(enabled: true),
|
||||
.sessionWithPushNotifications(enabled: false),
|
||||
.remotelyTogglingPushersNotAvailable]
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
let session: UserSessionInfo
|
||||
let service: UserSessionOverviewServiceProtocol
|
||||
switch self {
|
||||
case .currentSession:
|
||||
session = UserSessionInfo(id: "alice",
|
||||
|
@ -56,6 +63,7 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable {
|
|||
deviceName: "My iPhone",
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
service = MockUserSessionOverviewService()
|
||||
case .otherSession:
|
||||
session = UserSessionInfo(id: "1",
|
||||
name: "macOS",
|
||||
|
@ -72,9 +80,44 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable {
|
|||
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(session: session)
|
||||
let viewModel = UserSessionOverviewViewModel(sessionInfo: session, service: service)
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
return ([viewModel], AnyView(UserSessionOverview(viewModel: viewModel.context)))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import MatrixSDK
|
||||
|
||||
class UserSessionOverviewService: UserSessionOverviewServiceProtocol {
|
||||
|
||||
// MARK: - Members
|
||||
|
||||
private(set) var pusherEnabledSubject: CurrentValueSubject<Bool?, Never>
|
||||
private(set) var remotelyTogglingPushersAvailableSubject: CurrentValueSubject<Bool, Never>
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private let session: MXSession
|
||||
private let sessionInfo: UserSessionInfo
|
||||
private var pusher: MXPusher?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(session: MXSession, sessionInfo: UserSessionInfo) {
|
||||
self.session = session
|
||||
self.sessionInfo = sessionInfo
|
||||
self.pusherEnabledSubject = CurrentValueSubject(nil)
|
||||
self.remotelyTogglingPushersAvailableSubject = CurrentValueSubject(false)
|
||||
|
||||
checkServerVersions { [weak self] in
|
||||
self?.checkPusher()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UserSessionOverviewServiceProtocol
|
||||
|
||||
func togglePushNotifications() {
|
||||
guard let pusher = pusher, let enabled = pusher.enabled?.boolValue, self.remotelyTogglingPushersAvailableSubject.value else {
|
||||
return
|
||||
}
|
||||
|
||||
let data = pusher.data.jsonDictionary() as? [String: Any] ?? [:]
|
||||
|
||||
self.session.matrixRestClient.setPusher(pushKey: pusher.pushkey,
|
||||
kind: MXPusherKind(value: pusher.kind),
|
||||
appId: pusher.appId,
|
||||
appDisplayName:pusher.appDisplayName,
|
||||
deviceDisplayName: pusher.deviceDisplayName,
|
||||
profileTag: pusher.profileTag ?? "",
|
||||
lang: pusher.lang,
|
||||
data: data,
|
||||
append: false,
|
||||
enabled: !enabled) { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch response {
|
||||
case .success:
|
||||
self.checkPusher()
|
||||
case .failure(let error):
|
||||
MXLog.warning("[UserSessionOverviewService] togglePushNotifications failed due to error: \(error)")
|
||||
self.pusherEnabledSubject.send(enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func checkServerVersions(_ completion: @escaping () -> Void) {
|
||||
session.supportedMatrixVersions { [weak self] response in
|
||||
switch response {
|
||||
case .success(let versions):
|
||||
self?.remotelyTogglingPushersAvailableSubject.send(versions.supportsRemotelyTogglingPushNotifications)
|
||||
case .failure(let error):
|
||||
MXLog.warning("[UserSessionOverviewService] checkServerVersions failed due to error: \(error)")
|
||||
}
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
private func checkPusher() {
|
||||
session.matrixRestClient.pushers { [weak self] response in
|
||||
switch response {
|
||||
case .success(let pushers):
|
||||
self?.check(pushers: pushers)
|
||||
case .failure(let error):
|
||||
MXLog.warning("[UserSessionOverviewService] checkPusher failed due to error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func check(pushers: [MXPusher]) {
|
||||
for pusher in pushers where pusher.deviceId == sessionInfo.id {
|
||||
self.pusher = pusher
|
||||
|
||||
guard let enabled = pusher.enabled else {
|
||||
// For backwards compatibility, any pusher without an enabled field should be treated as if enabled is false
|
||||
pusherEnabledSubject.send(false)
|
||||
return
|
||||
}
|
||||
|
||||
pusherEnabledSubject.send(enabled.boolValue)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
class MockUserSessionOverviewService: UserSessionOverviewServiceProtocol {
|
||||
|
||||
|
||||
var pusherEnabledSubject: CurrentValueSubject<Bool?, Never>
|
||||
var remotelyTogglingPushersAvailableSubject: CurrentValueSubject<Bool, Never>
|
||||
|
||||
init(pusherEnabled: Bool? = nil, remotelyTogglingPushersAvailable: Bool = true) {
|
||||
self.pusherEnabledSubject = CurrentValueSubject(pusherEnabled)
|
||||
self.remotelyTogglingPushersAvailableSubject = CurrentValueSubject(remotelyTogglingPushersAvailable)
|
||||
}
|
||||
|
||||
func togglePushNotifications() {
|
||||
guard let enabled = pusherEnabledSubject.value, self.remotelyTogglingPushersAvailableSubject.value else {
|
||||
return
|
||||
}
|
||||
|
||||
pusherEnabledSubject.send(!enabled)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
protocol UserSessionOverviewServiceProtocol {
|
||||
var pusherEnabledSubject: CurrentValueSubject<Bool?, Never> { get }
|
||||
var remotelyTogglingPushersAvailableSubject: CurrentValueSubject<Bool, Never> { get }
|
||||
|
||||
func togglePushNotifications()
|
||||
}
|
|
@ -34,4 +34,31 @@ class UserSessionOverviewUITests: MockScreenTestCase {
|
|||
app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.currentSession.title)
|
||||
XCTAssertTrue(app.buttons[VectorL10n.userSessionOverviewSessionDetailsButtonTitle].exists)
|
||||
}
|
||||
|
||||
func test_whenSessionOverviewPresented_pusherEnabledToggleExists() {
|
||||
app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.sessionWithPushNotifications(enabled: true).title)
|
||||
XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].exists)
|
||||
XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].isOn)
|
||||
XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].isEnabled)
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotifications].exists)
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotificationsMessage].exists)
|
||||
}
|
||||
|
||||
func test_whenSessionOverviewPresented_pusherDisabledToggleExists() {
|
||||
app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.sessionWithPushNotifications(enabled: false).title)
|
||||
XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].exists)
|
||||
XCTAssertFalse(app.switches["UserSessionOverviewToggleCell"].isOn)
|
||||
XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].isEnabled)
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotifications].exists)
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotificationsMessage].exists)
|
||||
}
|
||||
|
||||
func test_whenSessionOverviewPresented_pusherEnabledToggleExists_remotelyTogglingPushersAvailable() {
|
||||
app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.remotelyTogglingPushersNotAvailable.title)
|
||||
XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].exists)
|
||||
XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].isOn)
|
||||
XCTAssertFalse(app.switches["UserSessionOverviewToggleCell"].isEnabled)
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotifications].exists)
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotificationsMessage].exists)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,8 +23,9 @@ class UserSessionOverviewViewModelTests: XCTestCase {
|
|||
var sut: UserSessionOverviewViewModel!
|
||||
|
||||
func test_whenVerifyCurrentSessionProcessed_completionWithVerifyCurrentSessionCalled() {
|
||||
sut = UserSessionOverviewViewModel(session: createUserSessionInfo())
|
||||
sut = UserSessionOverviewViewModel(sessionInfo: createUserSessionInfo(), service: MockUserSessionOverviewService())
|
||||
|
||||
XCTAssertEqual(sut.state.isPusherEnabled, nil)
|
||||
var modelResult: UserSessionOverviewViewModelResult?
|
||||
sut.completion = { result in
|
||||
modelResult = result
|
||||
|
@ -34,17 +35,57 @@ class UserSessionOverviewViewModelTests: XCTestCase {
|
|||
}
|
||||
|
||||
func test_whenViewSessionDetailsProcessed_completionWithShowSessionDetailsCalled() {
|
||||
let session = createUserSessionInfo()
|
||||
sut = UserSessionOverviewViewModel(session: session)
|
||||
let sessionInfo = createUserSessionInfo()
|
||||
sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: MockUserSessionOverviewService())
|
||||
|
||||
XCTAssertEqual(sut.state.isPusherEnabled, nil)
|
||||
var modelResult: UserSessionOverviewViewModelResult?
|
||||
sut.completion = { result in
|
||||
modelResult = result
|
||||
}
|
||||
sut.process(viewAction: .viewSessionDetails)
|
||||
XCTAssertEqual(modelResult, .showSessionDetails(session: session))
|
||||
XCTAssertEqual(modelResult, .showSessionDetails(sessionInfo: sessionInfo))
|
||||
}
|
||||
|
||||
func test_whenViewSessionDetailsProcessed_toggleAvailablePusher() {
|
||||
let sessionInfo = createUserSessionInfo()
|
||||
let service = MockUserSessionOverviewService(pusherEnabled: true)
|
||||
sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: service)
|
||||
|
||||
XCTAssertTrue(sut.state.remotelyTogglingPushersAvailable)
|
||||
XCTAssertEqual(sut.state.isPusherEnabled, true)
|
||||
sut.process(viewAction: .togglePushNotifications)
|
||||
XCTAssertEqual(sut.state.isPusherEnabled, false)
|
||||
sut.process(viewAction: .togglePushNotifications)
|
||||
XCTAssertEqual(sut.state.isPusherEnabled, true)
|
||||
}
|
||||
|
||||
func test_whenViewSessionDetailsProcessed_toggleNoPusher() {
|
||||
let sessionInfo = createUserSessionInfo()
|
||||
let service = MockUserSessionOverviewService(pusherEnabled: nil)
|
||||
sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: service)
|
||||
|
||||
XCTAssertTrue(sut.state.remotelyTogglingPushersAvailable)
|
||||
XCTAssertEqual(sut.state.isPusherEnabled, nil)
|
||||
sut.process(viewAction: .togglePushNotifications)
|
||||
XCTAssertEqual(sut.state.isPusherEnabled, nil)
|
||||
sut.process(viewAction: .togglePushNotifications)
|
||||
XCTAssertEqual(sut.state.isPusherEnabled, nil)
|
||||
}
|
||||
|
||||
func test_whenViewSessionDetailsProcessed_remotelyTogglingPushersNotAvailable() {
|
||||
let sessionInfo = createUserSessionInfo()
|
||||
let service = MockUserSessionOverviewService(pusherEnabled: true, remotelyTogglingPushersAvailable: false)
|
||||
sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: service)
|
||||
|
||||
XCTAssertFalse(sut.state.remotelyTogglingPushersAvailable)
|
||||
XCTAssertEqual(sut.state.isPusherEnabled, true)
|
||||
sut.process(viewAction: .togglePushNotifications)
|
||||
XCTAssertEqual(sut.state.isPusherEnabled, true)
|
||||
sut.process(viewAction: .togglePushNotifications)
|
||||
XCTAssertEqual(sut.state.isPusherEnabled, true)
|
||||
}
|
||||
|
||||
private func createUserSessionInfo() -> UserSessionInfo {
|
||||
UserSessionInfo(id: "session",
|
||||
name: "iOS",
|
||||
|
|
|
@ -19,13 +19,13 @@ import Foundation
|
|||
// MARK: - Coordinator
|
||||
|
||||
enum UserSessionOverviewCoordinatorResult {
|
||||
case openSessionDetails(session: UserSessionInfo)
|
||||
case openSessionDetails(sessionInfo: UserSessionInfo)
|
||||
}
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum UserSessionOverviewViewModelResult: Equatable {
|
||||
case showSessionDetails(session: UserSessionInfo)
|
||||
case showSessionDetails(sessionInfo: UserSessionInfo)
|
||||
case verifyCurrentSession
|
||||
}
|
||||
|
||||
|
@ -34,9 +34,13 @@ enum UserSessionOverviewViewModelResult: Equatable {
|
|||
struct UserSessionOverviewViewState: BindableState {
|
||||
let cardViewData: UserSessionCardViewData
|
||||
let isCurrentSession: Bool
|
||||
var isPusherEnabled: Bool?
|
||||
var remotelyTogglingPushersAvailable: Bool
|
||||
var showLoadingIndicator: Bool
|
||||
}
|
||||
|
||||
enum UserSessionOverviewViewAction {
|
||||
case verifyCurrentSession
|
||||
case viewSessionDetails
|
||||
case togglePushNotifications
|
||||
}
|
||||
|
|
|
@ -19,18 +19,45 @@ import SwiftUI
|
|||
typealias UserSessionOverviewViewModelType = StateStoreViewModel<UserSessionOverviewViewState, UserSessionOverviewViewAction>
|
||||
|
||||
class UserSessionOverviewViewModel: UserSessionOverviewViewModelType, UserSessionOverviewViewModelProtocol {
|
||||
private let session: UserSessionInfo
|
||||
private let sessionInfo: UserSessionInfo
|
||||
private let service: UserSessionOverviewServiceProtocol
|
||||
|
||||
var completion: ((UserSessionOverviewViewModelResult) -> Void)?
|
||||
|
||||
init(session: UserSessionInfo) {
|
||||
self.session = session
|
||||
// MARK: - Setup
|
||||
|
||||
init(sessionInfo: UserSessionInfo, service: UserSessionOverviewServiceProtocol) {
|
||||
self.sessionInfo = sessionInfo
|
||||
self.service = service
|
||||
|
||||
let cardViewData = UserSessionCardViewData(session: session)
|
||||
let state = UserSessionOverviewViewState(cardViewData: cardViewData, isCurrentSession: session.isCurrent)
|
||||
let cardViewData = UserSessionCardViewData(sessionInfo: sessionInfo)
|
||||
let state = UserSessionOverviewViewState(cardViewData: cardViewData,
|
||||
isCurrentSession: sessionInfo.isCurrent,
|
||||
isPusherEnabled: service.pusherEnabledSubject.value,
|
||||
remotelyTogglingPushersAvailable: service.remotelyTogglingPushersAvailableSubject.value,
|
||||
showLoadingIndicator: false)
|
||||
super.init(initialViewState: state)
|
||||
|
||||
startObservingService()
|
||||
}
|
||||
|
||||
private func startObservingService() {
|
||||
service
|
||||
.pusherEnabledSubject
|
||||
.sink(receiveValue: { [weak self] pushEnabled in
|
||||
self?.state.showLoadingIndicator = false
|
||||
self?.state.isPusherEnabled = pushEnabled
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
|
||||
service
|
||||
.remotelyTogglingPushersAvailableSubject
|
||||
.sink(receiveValue: { [weak self] remotelyTogglingPushersAvailable in
|
||||
self?.state.remotelyTogglingPushersAvailable = remotelyTogglingPushersAvailable
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: UserSessionOverviewViewAction) {
|
||||
|
@ -38,7 +65,10 @@ class UserSessionOverviewViewModel: UserSessionOverviewViewModelType, UserSessio
|
|||
case .verifyCurrentSession:
|
||||
completion?(.verifyCurrentSession)
|
||||
case .viewSessionDetails:
|
||||
completion?(.showSessionDetails(session: session))
|
||||
completion?(.showSessionDetails(sessionInfo: sessionInfo))
|
||||
case .togglePushNotifications:
|
||||
self.state.showLoadingIndicator = true
|
||||
service.togglePushNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,10 +34,18 @@ struct UserSessionOverview: View {
|
|||
UserSessionOverviewDisclosureCell(title: VectorL10n.userSessionOverviewSessionDetailsButtonTitle, onBackgroundTap: {
|
||||
viewModel.send(viewAction: .viewSessionDetails)
|
||||
})
|
||||
if let enabled = viewModel.viewState.isPusherEnabled {
|
||||
UserSessionOverviewToggleCell(title: VectorL10n.userSessionPushNotifications,
|
||||
message: VectorL10n.userSessionPushNotificationsMessage,
|
||||
isOn: enabled, isEnabled: viewModel.viewState.remotelyTogglingPushersAvailable) {
|
||||
viewModel.send(viewAction: .togglePushNotifications)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(theme.colors.system.ignoresSafeArea())
|
||||
.frame(maxHeight: .infinity)
|
||||
.waitOverlay(show: viewModel.viewState.showLoadingIndicator, allowUserInteraction: false)
|
||||
.navigationTitle(viewModel.viewState.isCurrentSession ?
|
||||
VectorL10n.userSessionOverviewCurrentSessionTitle :
|
||||
VectorL10n.userSessionOverviewSessionTitle)
|
||||
|
|
|
@ -33,7 +33,7 @@ struct UserSessionOverviewDisclosureCell: View {
|
|||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Image(Asset.Images.chevron.name)
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.vertical, 15)
|
||||
.padding(.horizontal, 16)
|
||||
SeparatorLine()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct UserSessionOverviewToggleCell: View {
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
let title: String
|
||||
let message: String?
|
||||
let isOn: Bool
|
||||
let isEnabled: Bool
|
||||
var onBackgroundTap: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
Button(action: {
|
||||
guard isEnabled else { return }
|
||||
onBackgroundTap?()
|
||||
}) {
|
||||
VStack(spacing: 0) {
|
||||
SeparatorLine()
|
||||
Toggle(isOn: .constant(isOn)) {
|
||||
Text(title)
|
||||
.font(theme.fonts.body)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.opacity(isEnabled ? 1 : 0.3)
|
||||
}
|
||||
.disabled(!isEnabled)
|
||||
.allowsHitTesting(false)
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 16)
|
||||
.accessibilityIdentifier("UserSessionOverviewToggleCell")
|
||||
SeparatorLine()
|
||||
}
|
||||
.background(theme.colors.background)
|
||||
}
|
||||
.disabled(!isEnabled)
|
||||
if let message = message {
|
||||
Text(message)
|
||||
.multilineTextAlignment(.leading)
|
||||
.font(theme.fonts.footnote)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserSessionOverviewToggleCell_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
preview
|
||||
.theme(.light)
|
||||
.preferredColorScheme(.light)
|
||||
preview
|
||||
.theme(.dark)
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
||||
|
||||
static var preview: some View {
|
||||
VStack {
|
||||
UserSessionOverviewToggleCell(title: "Title", message: nil, isOn: true, isEnabled: true)
|
||||
UserSessionOverviewToggleCell(title: "Title", message: nil, isOn: false, isEnabled: true)
|
||||
UserSessionOverviewToggleCell(title: "Title", message: "some very long message text in order to test the multine alignment", isOn: true, isEnabled: true)
|
||||
UserSessionOverviewToggleCell(title: "Title", message: nil, isOn: true, isEnabled: false)
|
||||
UserSessionOverviewToggleCell(title: "Title", message: nil, isOn: false, isEnabled: false)
|
||||
UserSessionOverviewToggleCell(title: "Title", message: "some very long message text in order to test the multine alignment", isOn: true, isEnabled: false)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -59,12 +59,12 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
|||
self.showAllInactiveSessions()
|
||||
case .verifyCurrentSession:
|
||||
self.startVerifyCurrentSession()
|
||||
case let .showCurrentSessionOverview(session):
|
||||
self.showCurrentSessionOverview(session: session)
|
||||
case let .showCurrentSessionOverview(sessionInfo):
|
||||
self.showCurrentSessionOverview(sessionInfo: sessionInfo)
|
||||
case .showAllOtherSessions:
|
||||
self.showAllOtherSessions()
|
||||
case let .showUserSessionOverview(session):
|
||||
self.showUserSessionOverview(session: session)
|
||||
case let .showUserSessionOverview(sessionInfo):
|
||||
self.showUserSessionOverview(sessionInfo: sessionInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,12 +100,12 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
|||
// TODO:
|
||||
}
|
||||
|
||||
private func showCurrentSessionOverview(session: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(session: session))
|
||||
private func showCurrentSessionOverview(sessionInfo: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(sessionInfo: sessionInfo))
|
||||
}
|
||||
|
||||
private func showUserSessionOverview(session: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(session: session))
|
||||
private func showUserSessionOverview(sessionInfo: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(sessionInfo: sessionInfo))
|
||||
}
|
||||
|
||||
private func showAllOtherSessions() {
|
||||
|
|
|
@ -40,7 +40,7 @@ class UserSessionsDataProvider: UserSessionsDataProviderProtocol {
|
|||
session.matrixRestClient.devices(completion: completion)
|
||||
}
|
||||
|
||||
func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo {
|
||||
func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo? {
|
||||
session.crypto.device(withDeviceId: deviceId, ofUser: userId)
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ protocol UserSessionsDataProviderProtocol {
|
|||
|
||||
func devices(completion: @escaping (MXResponse<[MXDevice]>) -> Void)
|
||||
|
||||
func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo
|
||||
func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo?
|
||||
|
||||
func accountData(for eventType: String) -> [AnyHashable: Any]?
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import Foundation
|
|||
// MARK: - Coordinator
|
||||
|
||||
enum UserSessionsOverviewCoordinatorResult {
|
||||
case openSessionOverview(session: UserSessionInfo)
|
||||
case openSessionOverview(sessionInfo: UserSessionInfo)
|
||||
}
|
||||
|
||||
// MARK: View model
|
||||
|
|
|
@ -68,7 +68,7 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
|
|||
state.otherSessionsViewData = userSessionsViewData.otherSessions.asViewData()
|
||||
|
||||
if let currentSessionInfo = userSessionsViewData.currentSession {
|
||||
state.currentSessionViewData = UserSessionCardViewData(session: currentSessionInfo)
|
||||
state.currentSessionViewData = UserSessionCardViewData(sessionInfo: currentSessionInfo)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -155,7 +155,7 @@ private class MockUserSessionsDataProvider: UserSessionsDataProviderProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo {
|
||||
func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo? {
|
||||
guard deviceId == currentDeviceId else {
|
||||
return MockDeviceInfo(verified: deviceId != unverifiedDeviceId)
|
||||
}
|
||||
|
|
1
changelog.d/6787.change
Normal file
1
changelog.d/6787.change
Normal file
|
@ -0,0 +1 @@
|
|||
User sessions: Add support for MSC3881
|
Loading…
Reference in a new issue