diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 74c7cf2a7..a7cf6aa3c 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -5613,7 +5613,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = 1.1.12; + version = 1.1.13; }; }; 821C67C9A7F8CC3FD41B28B4 /* XCRemoteSwiftPackageReference "emojibase-bindings" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 444ab3268..219f12f85 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -129,8 +129,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-rust-components-swift", "state" : { - "revision" : "8c2307180e468fc40ab72e6a5e8b31a4fbbea4d5", - "version" : "1.1.12" + "revision" : "482c8c04019e6e6ac9638ae792adae9d67b08114", + "version" : "1.1.13" } }, { diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 33a0cb15c..3e2e9c26e 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -35,6 +35,8 @@ "action_learn_more" = "Learn more"; "action_leave" = "Leave"; "action_leave_room" = "Leave room"; +"action_manage_account" = "Manage account"; +"action_manage_devices" = "Manage devices"; "action_next" = "Next"; "action_no" = "No"; "action_not_now" = "Not now"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 7acb09baf..cb8fead33 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -86,6 +86,10 @@ public enum L10n { public static var actionLeave: String { return L10n.tr("Localizable", "action_leave") } /// Leave room public static var actionLeaveRoom: String { return L10n.tr("Localizable", "action_leave_room") } + /// Manage account + public static var actionManageAccount: String { return L10n.tr("Localizable", "action_manage_account") } + /// Manage devices + public static var actionManageDevices: String { return L10n.tr("Localizable", "action_manage_devices") } /// Next public static var actionNext: String { return L10n.tr("Localizable", "action_next") } /// No diff --git a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index bdc102006..d0b8d0602 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -33,19 +33,27 @@ class SDKClientMock: SDKClientProtocol { } //MARK: - accountUrl - public var accountUrlCallsCount = 0 - public var accountUrlCalled: Bool { - return accountUrlCallsCount > 0 + public var accountUrlActionThrowableError: Error? + public var accountUrlActionCallsCount = 0 + public var accountUrlActionCalled: Bool { + return accountUrlActionCallsCount > 0 } - public var accountUrlReturnValue: String? - public var accountUrlClosure: (() -> String?)? + public var accountUrlActionReceivedAction: AccountManagementAction? + public var accountUrlActionReceivedInvocations: [AccountManagementAction?] = [] + public var accountUrlActionReturnValue: String? + public var accountUrlActionClosure: ((AccountManagementAction?) throws -> String?)? - public func accountUrl() -> String? { - accountUrlCallsCount += 1 - if let accountUrlClosure = accountUrlClosure { - return accountUrlClosure() + public func accountUrl(action: AccountManagementAction?) throws -> String? { + if let error = accountUrlActionThrowableError { + throw error + } + accountUrlActionCallsCount += 1 + accountUrlActionReceivedAction = action + accountUrlActionReceivedInvocations.append(action) + if let accountUrlActionClosure = accountUrlActionClosure { + return try accountUrlActionClosure(action) } else { - return accountUrlReturnValue + return accountUrlActionReturnValue } } //MARK: - avatarUrl diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift index 7100dd620..1aa5b91bc 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift @@ -48,23 +48,25 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { switch action { case .close: - self.callback?(.dismiss) - case .account: - self.presentAccountSettings() + callback?(.dismiss) + case .accountProfile: + presentAccountProfileURL() case .analytics: - self.presentAnalyticsScreen() + presentAnalyticsScreen() case .reportBug: - self.presentBugReportScreen() + presentBugReportScreen() case .about: - self.presentLegalInformationScreen() + presentLegalInformationScreen() case .sessionVerification: - self.verifySession() + verifySession() + case .accountSessionsList: + presentAccountSessionsListURL() case .developerOptions: - self.presentDeveloperOptions() + presentDeveloperOptions() case .logout: - self.callback?(.logout) + callback?(.logout) case .notifications: - self.presentNotificationSettings() + presentNotificationSettings() } } } @@ -75,15 +77,26 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { AnyView(SettingsScreen(context: viewModel.context)) } - // MARK: - Private + // MARK: - OIDC Account Management - private var accountSettingsPresenter: OIDCAccountSettingsPresenter? - private func presentAccountSettings() { - guard let accountURL = viewModel.context.viewState.accountURL else { + private func presentAccountProfileURL() { + guard let url = viewModel.context.viewState.accountProfileURL else { MXLog.error("Account URL is missing.") return } - + presentAccountManagementURL(url) + } + + private func presentAccountSessionsListURL() { + guard let url = viewModel.context.viewState.accountSessionsListURL else { + MXLog.error("Account URL is missing.") + return + } + presentAccountManagementURL(url) + } + + private var accountSettingsPresenter: OIDCAccountSettingsPresenter? + private func presentAccountManagementURL(_ url: URL) { guard let window = viewModel.context.viewState.window else { MXLog.error("The window is missing.") return @@ -91,10 +104,12 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { // Note to anyone in the future if you come back here to make this open in Safari instead of a WAS. // As of iOS 16, there is an issue on the simulator with accessing the cookie but it works on a device. 🤷‍♂️ - accountSettingsPresenter = OIDCAccountSettingsPresenter(accountURL: accountURL, presentationAnchor: window) + accountSettingsPresenter = OIDCAccountSettingsPresenter(accountURL: url, presentationAnchor: window) accountSettingsPresenter?.start() } + // MARK: - Private + private func presentAnalyticsScreen() { let coordinator = AnalyticsSettingsScreenCoordinator(parameters: .init(appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics)) diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift index d33ce906d..b3ee7d6e9 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift @@ -19,11 +19,12 @@ import UIKit enum SettingsScreenViewModelAction { case close - case account + case accountProfile case analytics case reportBug case about case sessionVerification + case accountSessionsList case developerOptions case notifications case logout @@ -33,7 +34,8 @@ struct SettingsScreenViewState: BindableState { var bindings: SettingsScreenViewStateBindings var deviceID: String? var userID: String - var accountURL: URL? + var accountProfileURL: URL? + var accountSessionsListURL: URL? var userAvatarURL: URL? var userDisplayName: String? var showSessionVerificationSection: Bool @@ -49,13 +51,14 @@ struct SettingsScreenViewStateBindings { enum SettingsScreenViewAction { case close - case account + case accountProfile case analytics case reportBug case about case sessionVerification case logout case changedTimelineStyle + case accountSessionsList case developerOptions case notifications diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift index 88d52757a..cd39f006b 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift @@ -39,7 +39,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo super.init(initialViewState: .init(bindings: bindings, deviceID: userSession.deviceID, userID: userSession.userID, - accountURL: userSession.clientProxy.accountURL, + accountProfileURL: userSession.clientProxy.accountURL(action: .profile), + accountSessionsListURL: userSession.clientProxy.accountURL(action: .sessionsList), showSessionVerificationSection: showSessionVerificationSection, showDeveloperOptions: appSettings.canShowDeveloperOptions), imageProvider: userSession.mediaProvider) @@ -81,8 +82,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo switch viewAction { case .close: callback?(.close) - case .account: - callback?(.account) + case .accountProfile: + callback?(.accountProfile) case .analytics: callback?(.analytics) case .reportBug: @@ -97,6 +98,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo appSettings.timelineStyle = state.bindings.timelineStyle case .notifications: callback?(.notifications) + case .accountSessionsList: + callback?(.accountSessionsList) case .developerOptions: callback?(.developerOptions) diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift index 8afca5fd8..20ece2b5f 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift @@ -15,6 +15,7 @@ // import Compound +import SFSafeSymbols import SwiftUI struct SettingsScreen: View { @@ -30,7 +31,11 @@ struct SettingsScreen: View { sessionVerificationSection } - simplifiedSection + mainSection + + if context.viewState.accountSessionsListURL != nil { + manageSessionsSection + } if context.viewState.showDeveloperOptions { developerOptionsSection @@ -41,20 +46,12 @@ struct SettingsScreen: View { .compoundList() .navigationTitle(L10n.commonSettings) .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .confirmationAction) { - doneButton - } - } + .toolbar { toolbar } .introspect(.window, on: .iOS(.v16, .v17)) { window in context.send(viewAction: .updateWindow(window)) } } - private var versionText: Text { - Text(L10n.settingsVersionNumber(InfoPlistReader.main.bundleShortVersionString, InfoPlistReader.main.bundleVersion)) - } - private var userSection: some View { Section { ListRow(kind: .custom { @@ -90,25 +87,13 @@ struct SettingsScreen: View { } } - private var developerOptionsSection: some View { - Section { - ListRow(label: .default(title: L10n.commonDeveloperOptions, - systemIcon: .hammerCircle), - kind: .navigationLink { - context.send(viewAction: .developerOptions) - }) - .accessibilityIdentifier(A11yIdentifiers.settingsScreen.developerOptions) - } - } - - private var simplifiedSection: some View { + private var mainSection: some View { Section { // Account - if context.viewState.accountURL != nil { - ListRow(label: .default(title: L10n.screenSettingsOidcAccount, - systemIcon: .person), + if context.viewState.accountProfileURL != nil { + ListRow(label: .default(title: L10n.actionManageAccount, systemIcon: .person), kind: .button { - context.send(viewAction: .account) + context.send(viewAction: .accountProfile) }) .accessibilityIdentifier(A11yIdentifiers.settingsScreen.account) } @@ -157,6 +142,36 @@ struct SettingsScreen: View { } } + private var manageSessionsSection: some View { + Section { + ListRow(label: .default(title: L10n.actionManageDevices, systemIcon: deviceIcon), + kind: .button { + context.send(viewAction: .accountSessionsList) + }) + } + } + + private var deviceIcon: SFSymbol { + if ProcessInfo.processInfo.isiOSAppOnMac { + return .macbookAndIphone + } else if UIDevice.current.userInterfaceIdiom == .pad { + return .ipad + } else { + return .iphone + } + } + + private var developerOptionsSection: some View { + Section { + ListRow(label: .default(title: L10n.commonDeveloperOptions, + systemIcon: .hammerCircle), + kind: .navigationLink { + context.send(viewAction: .developerOptions) + }) + .accessibilityIdentifier(A11yIdentifiers.settingsScreen.developerOptions) + } + } + private var signOutSection: some View { Section { ListRow(label: .default(title: L10n.screenSignoutPreferenceItem, @@ -166,9 +181,9 @@ struct SettingsScreen: View { }) .accessibilityIdentifier(A11yIdentifiers.settingsScreen.logout) .alert(L10n.screenSignoutConfirmationDialogTitle, isPresented: $showingLogoutConfirmation) { - Button(L10n.screenSignoutConfirmationDialogSubmit, - role: .destructive, - action: logout) + Button(L10n.screenSignoutConfirmationDialogSubmit, role: .destructive) { + context.send(viewAction: .logout) + } } message: { Text(L10n.screenSignoutConfirmationDialogContent) } @@ -186,18 +201,16 @@ struct SettingsScreen: View { .padding(.top, 24) } } - - private var doneButton: some View { - Button(L10n.actionDone, action: close) - .accessibilityIdentifier(A11yIdentifiers.settingsScreen.done) + + private var versionText: Text { + Text(L10n.settingsVersionNumber(InfoPlistReader.main.bundleShortVersionString, InfoPlistReader.main.bundleVersion)) } - - private func close() { - context.send(viewAction: .close) - } - - private func logout() { - context.send(viewAction: .logout) + + private var toolbar: some ToolbarContent { + ToolbarItem(placement: .confirmationAction) { + Button(L10n.actionDone) { context.send(viewAction: .close) } + .accessibilityIdentifier(A11yIdentifiers.settingsScreen.done) + } } } @@ -220,8 +233,7 @@ struct SettingsScreen_Previews: PreviewProvider { verificationController.isVerified = false let userSession = MockUserSession(sessionVerificationController: verificationController, clientProxy: MockClientProxy(userID: "@userid:example.com", - deviceID: "AAAAAAAAAAA", - accountURL: "https://matrix.org/account"), + deviceID: "AAAAAAAAAAA"), mediaProvider: MockMediaProvider()) return SettingsScreenViewModel(userSession: userSession, appSettings: ServiceLocator.shared.settings) diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index ffb7e6913..06b367aee 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -125,10 +125,6 @@ class ClientProxy: ClientProxyProtocol { return nil } } - - var accountURL: URL? { - client.accountUrl().flatMap(URL.init(string:)) - } func startSync() { guard !hasEncounteredAuthError else { @@ -155,6 +151,10 @@ class ClientProxy: ClientProxyProtocol { } } + func accountURL(action: AccountManagementAction) -> URL? { + try? client.accountUrl(action: action).flatMap(URL.init(string:)) + } + func directRoomForUserID(_ userID: String) async -> Result { await Task.dispatch(on: clientQueue) { do { diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index dca66ccee..7cd797978 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -83,8 +83,6 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { var restorationToken: RestorationToken? { get } - var accountURL: URL? { get } - var roomSummaryProvider: RoomSummaryProviderProtocol? { get } var inviteSummaryProvider: RoomSummaryProviderProtocol? { get } @@ -95,6 +93,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { func stopSync() + func accountURL(action: AccountManagementAction) -> URL? + func directRoomForUserID(_ userID: String) async -> Result func createDirectRoom(with userID: String, expectedRoomName: String?) async -> Result diff --git a/ElementX/Sources/Services/Client/MockClientProxy.swift b/ElementX/Sources/Services/Client/MockClientProxy.swift index 33df497aa..8446ddaa7 100644 --- a/ElementX/Sources/Services/Client/MockClientProxy.swift +++ b/ElementX/Sources/Services/Client/MockClientProxy.swift @@ -27,7 +27,6 @@ class MockClientProxy: ClientProxyProtocol { let deviceID: String? let homeserver = "" let restorationToken: RestorationToken? = nil - let accountURL: URL? var roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider() @@ -37,21 +36,22 @@ class MockClientProxy: ClientProxyProtocol { var notificationSettings: NotificationSettingsProxyProtocol = NotificationSettingsProxyMock(with: .init()) - init(userID: String, deviceID: String? = nil, accountURL: URL? = nil, roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()) { + init(userID: String, deviceID: String? = nil, roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()) { self.userID = userID self.deviceID = deviceID - self.accountURL = accountURL self.roomSummaryProvider = roomSummaryProvider } func loadUserAvatarURL() async { } func startSync() { } - - func stopSync(completionHandler: () -> Void) { } func stopSync() { } + func accountURL(action: AccountManagementAction) -> URL? { + "https://matrix.org/account" + } + func directRoomForUserID(_ userID: String) async -> Result { .failure(.failedRetrievingDirectRoom) } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 0abf40858..ecb372955 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -170,7 +170,7 @@ class MockScreen: Identifiable { return navigationStackCoordinator case .settings: let navigationStackCoordinator = NavigationStackCoordinator() - let clientProxy = MockClientProxy(userID: "@mock:client.com", accountURL: "https://matrix.org/account") + let clientProxy = MockClientProxy(userID: "@mock:client.com") let coordinator = SettingsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, userIndicatorController: nil, userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()), diff --git a/changelog.d/pr-1698.change b/changelog.d/pr-1698.change new file mode 100644 index 000000000..e1a8bd82f --- /dev/null +++ b/changelog.d/pr-1698.change @@ -0,0 +1 @@ +Separate Manage account from Manage devices \ No newline at end of file diff --git a/project.yml b/project.yml index 8191e17f7..eaacd8629 100644 --- a/project.yml +++ b/project.yml @@ -46,7 +46,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/matrix-org/matrix-rust-components-swift - exactVersion: 1.1.12 + exactVersion: 1.1.13 # path: ../matrix-rust-sdk DesignKit: path: DesignKit