Merge pull request #6789 from vector-im/gil/6787-Add_support_for_MSC3881

Added support for MSC3881
This commit is contained in:
Gil Eluard 2022-10-04 07:20:14 +02:00 committed by GitHub
commit c8b1657e99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 489 additions and 59 deletions

View file

@ -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" = "%@: %@";

View file

@ -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")

View file

@ -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) {
}

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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))
}
}
}

View file

@ -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)))
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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",

View file

@ -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
}

View file

@ -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()
}
}
}

View file

@ -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)

View file

@ -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()
}

View file

@ -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)
}
}
}

View file

@ -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() {

View file

@ -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)
}

View file

@ -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]?
}

View file

@ -19,7 +19,7 @@ import Foundation
// MARK: - Coordinator
enum UserSessionsOverviewCoordinatorResult {
case openSessionOverview(session: UserSessionInfo)
case openSessionOverview(sessionInfo: UserSessionInfo)
}
// MARK: View model

View file

@ -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)
}
}

View file

@ -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
View file

@ -0,0 +1 @@
User sessions: Add support for MSC3881