diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 3b6a8b386..0dc7a2ef1 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -17,6 +17,7 @@ import Combine import Foundation import SwiftState +import UserNotifications enum RoomFlowCoordinatorAction: Equatable { case presentedRoom(String) @@ -149,6 +150,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { return .mapNavigator(roomID: roomID) case (.dismissMapNavigator, .mapNavigator(let roomID)): return .room(roomID: roomID) + + case (.presentNotificationSettingsScreen, .roomDetails(let roomID, _)): + return .notificationSettingsScreen(roomID: roomID) + case (.dismissNotificationSettingsScreen, .notificationSettingsScreen(let roomID)): + return .roomDetails(roomID: roomID, isRoot: false) default: return nil } @@ -215,6 +221,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { presentMapNavigator(interactionMode: mode) case (.mapNavigator, .dismissMapNavigator, .room): break + + case (.roomDetails, .presentNotificationSettingsScreen, .notificationSettingsScreen): + asyncPresentNotificationSettingsScreen(animated: animated) + case (.notificationSettingsScreen, .dismissNotificationSettingsScreen, .roomDetails): + break + default: fatalError("Unknown transition: \(context)") } @@ -372,12 +384,15 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { userIndicatorController: userIndicatorController, notificationSettings: userSession.clientProxy.notificationSettings()) let coordinator = RoomDetailsScreenCoordinator(parameters: params) - coordinator.callback = { [weak self] action in + coordinator.actions.sink { [weak self] action in switch action { case .leftRoom: self?.dismissRoom(animated: animated) + case .presentNotificationSettingsScreen: + self?.stateMachine.tryEvent(.presentNotificationSettingsScreen) } } + .store(in: &cancellables) if isRoot { navigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self] in @@ -610,6 +625,32 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.presentRoom(roomID: roomID), userInfo: EventUserInfo(animated: true, destinationRoomProxy: targetRoomProxy)) } + + private func asyncPresentNotificationSettingsScreen(animated: Bool) { + Task { + await presentNotificationSettingsScreen(animated: animated) + } + } + + private func presentNotificationSettingsScreen(animated: Bool) async { + let navigationCoordinator = NavigationStackCoordinator() + let parameters = await NotificationSettingsScreenCoordinatorParameters(userNotificationCenter: UNUserNotificationCenter.current(), + notificationSettings: userSession.clientProxy.notificationSettings(), + isModallyPresented: true) + let coordinator = NotificationSettingsScreenCoordinator(parameters: parameters) + coordinator.actions.sink { [weak self] action in + switch action { + case .close: + self?.navigationStackCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + navigationCoordinator.setRootCoordinator(coordinator) + navigationStackCoordinator.setSheetCoordinator(navigationCoordinator) { [weak self] in + self?.stateMachine.tryEvent(.dismissNotificationSettingsScreen) + } + } } private extension RoomFlowCoordinator { @@ -636,6 +677,7 @@ private extension RoomFlowCoordinator { case mapNavigator(roomID: String) case roomMemberDetails(roomID: String, member: HashableRoomMemberWrapper) case messageForwarding(roomID: String, itemID: TimelineItemIdentifier) + case notificationSettingsScreen(roomID: String) } struct EventUserInfo { @@ -670,6 +712,9 @@ private extension RoomFlowCoordinator { case presentMessageForwarding(itemID: TimelineItemIdentifier) case dismissMessageForwarding + + case presentNotificationSettingsScreen + case dismissNotificationSettingsScreen } } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift index 4173e4872..62c3c7619 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift @@ -29,19 +29,24 @@ struct RoomDetailsScreenCoordinatorParameters { enum RoomDetailsScreenCoordinatorAction { case leftRoom + case presentNotificationSettingsScreen } final class RoomDetailsScreenCoordinator: CoordinatorProtocol { private let parameters: RoomDetailsScreenCoordinatorParameters private var viewModel: RoomDetailsScreenViewModelProtocol - private var cancellables: Set = .init() private let selectedUsers: CurrentValueSubject<[UserProfileProxy], Never> = .init([]) private var navigationStackCoordinator: NavigationStackCoordinator? { parameters.navigationStackCoordinator } - var callback: ((RoomDetailsScreenCoordinatorAction) -> Void)? - + private let actionsSubject: PassthroughSubject = .init() + private var cancellables: Set = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + init(parameters: RoomDetailsScreenCoordinatorParameters) { self.parameters = parameters @@ -65,7 +70,7 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { case .requestInvitePeoplePresentation: self.presentInviteUsersScreen() case .leftRoom: - self.callback?(.leftRoom) + self.actionsSubject.send(.leftRoom) case .requestEditDetailsPresentation(let accountOwner): self.presentRoomDetailsEditScreen(accountOwner: accountOwner) case .requestNotificationSettingsPresentation: @@ -189,9 +194,17 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { } private func presentNotificationSettings() { - let roomNotificationSettingsParameters = RoomNotificationSettingsScreenCoordinatorParameters(notificationSettingsProxy: parameters.notificationSettings, + let roomNotificationSettingsParameters = RoomNotificationSettingsScreenCoordinatorParameters(navigationStackCoordinator: parameters.navigationStackCoordinator, + notificationSettingsProxy: parameters.notificationSettings, roomProxy: parameters.roomProxy) let roomNotificationSettingsCoordinator = RoomNotificationSettingsScreenCoordinator(parameters: roomNotificationSettingsParameters) + roomNotificationSettingsCoordinator.actions.sink { [weak self] actions in + switch actions { + case .presentNotificationSettingsScreen: + self?.actionsSubject.send(.presentNotificationSettingsScreen) + } + } + .store(in: &cancellables) navigationStackCoordinator?.push(roomNotificationSettingsCoordinator) } } diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift index ced098959..bb8ebb0a6 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift @@ -18,13 +18,29 @@ import Combine import SwiftUI struct RoomNotificationSettingsScreenCoordinatorParameters { + weak var navigationStackCoordinator: NavigationStackCoordinator? let notificationSettingsProxy: NotificationSettingsProxyProtocol let roomProxy: RoomProxyProtocol } +enum RoomNotificationSettingsScreenCoordinatorAction { + case presentNotificationSettingsScreen +} + final class RoomNotificationSettingsScreenCoordinator: CoordinatorProtocol { private let parameters: RoomNotificationSettingsScreenCoordinatorParameters private var viewModel: RoomNotificationSettingsScreenViewModelProtocol + + private let actionsSubject: PassthroughSubject = .init() + private var cancellables: Set = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + private var navigationStackCoordinator: NavigationStackCoordinator? { + parameters.navigationStackCoordinator + } init(parameters: RoomNotificationSettingsScreenCoordinatorParameters) { self.parameters = parameters @@ -32,7 +48,14 @@ final class RoomNotificationSettingsScreenCoordinator: CoordinatorProtocol { roomProxy: parameters.roomProxy) } - func start() { } + func start() { + viewModel.actions.sink { [weak self] action in + switch action { + case .openGlobalSettings: + self?.actionsSubject.send(.presentNotificationSettingsScreen) + } + }.store(in: &cancellables) + } func toPresentable() -> AnyView { AnyView(RoomNotificationSettingsScreen(context: viewModel.context)) diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenModels.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenModels.swift index a01e03f11..231243fd2 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenModels.swift @@ -16,7 +16,9 @@ import Foundation -enum RoomNotificationSettingsScreenViewModelAction { } +enum RoomNotificationSettingsScreenViewModelAction { + case openGlobalSettings +} enum RoomNotificationSettingsState { case loading @@ -66,15 +68,20 @@ struct RoomNotificationSettingsScreenViewStateBindings { enum RoomNotificationSettingsScreenViewAction { case changedAllowCustomSettings case setCustomMode(RoomNotificationModeProxy) + case customSettingFootnoteLinkTapped } struct RoomNotificationSettingsScreenStrings { let customSettingFootnote: AttributedString + let customSettingFootnoteLink: URL? init() { + customSettingFootnoteLink = URL(string: "element://openGlobalSettings") + let linkPlaceholder = "{link}" var customSettingFootnote = AttributedString(L10n.screenRoomNotificationSettingsDefaultSettingFootnote(linkPlaceholder)) var linkString = AttributedString(L10n.screenRoomNotificationSettingsDefaultSettingFootnoteContentLink) + linkString.link = customSettingFootnoteLink linkString.bold() customSettingFootnote.replace(linkPlaceholder, with: linkString) diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift index d177f6c18..5515a0d23 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift @@ -48,6 +48,8 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie toogleCustomSetting() case .setCustomMode(let mode): setCustomMode(mode) + case .customSettingFootnoteLinkTapped: + actionsSubject.send(.openGlobalSettings) } } diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift index faea5df38..28a65ab24 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift @@ -71,6 +71,11 @@ struct RoomNotificationSettingsScreen: View { .compoundFormSectionHeader() } footer: { Text(context.viewState.strings.customSettingFootnote) + .environment(\.openURL, OpenURLAction { url in + guard url == context.viewState.strings.customSettingFootnoteLink else { return .discarded } + context.send(viewAction: .customSettingFootnoteLinkTapped) + return .handled + }) .compoundFormSectionFooter() } .compoundFormSection() diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift index 7a27db386..c24ab7ca7 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift @@ -20,16 +20,19 @@ import SwiftUI struct NotificationSettingsScreenCoordinatorParameters { let userNotificationCenter: UserNotificationCenterProtocol let notificationSettings: NotificationSettingsProxyProtocol + let isModallyPresented: Bool } -enum NotificationSettingsScreenCoordinatorAction { } +enum NotificationSettingsScreenCoordinatorAction { + case close +} final class NotificationSettingsScreenCoordinator: CoordinatorProtocol { private let parameters: NotificationSettingsScreenCoordinatorParameters private var viewModel: NotificationSettingsScreenViewModelProtocol private let actionsSubject: PassthroughSubject = .init() private var cancellables: Set = .init() - + var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } @@ -39,11 +42,20 @@ final class NotificationSettingsScreenCoordinator: CoordinatorProtocol { viewModel = NotificationSettingsScreenViewModel(appSettings: ServiceLocator.shared.settings, userNotificationCenter: parameters.userNotificationCenter, - notificationSettingsProxy: parameters.notificationSettings) + notificationSettingsProxy: parameters.notificationSettings, + isModallyPresented: parameters.isModallyPresented) } func start() { viewModel.fetchInitialContent() + + viewModel.actions.sink { [weak self] action in + switch action { + case .close: + self?.actionsSubject.send(.close) + } + } + .store(in: &cancellables) } func toPresentable() -> AnyView { diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift index 2d6d8d2e1..3d760cfe6 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift @@ -17,11 +17,14 @@ import Foundation import UIKit -enum NotificationSettingsScreenViewModelAction { } +enum NotificationSettingsScreenViewModelAction { + case close +} struct NotificationSettingsScreenViewState: BindableState { var bindings: NotificationSettingsScreenViewStateBindings var strings = NotificationSettingsScreenStrings() + let isModallyPresented: Bool var isUserPermissionGranted: Bool? var allowedNotificationModes: [RoomNotificationModeProxy] = [.allMessages, .mentionsAndKeywordsOnly] @@ -82,6 +85,7 @@ enum NotificationSettingsScreenViewAction { case directChatsTapped case roomMentionChanged case callsChanged + case close } enum NotificationSettingsScreenErrorType: Hashable { diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift index 464d2a1aa..0ef77b33c 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift @@ -30,13 +30,13 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy actionsSubject.eraseToAnyPublisher() } - init(appSettings: AppSettings, userNotificationCenter: UserNotificationCenterProtocol, notificationSettingsProxy: NotificationSettingsProxyProtocol) { + init(appSettings: AppSettings, userNotificationCenter: UserNotificationCenterProtocol, notificationSettingsProxy: NotificationSettingsProxyProtocol, isModallyPresented: Bool) { self.appSettings = appSettings self.userNotificationCenter = userNotificationCenter self.notificationSettingsProxy = notificationSettingsProxy let bindings = NotificationSettingsScreenViewStateBindings(enableNotifications: appSettings.enableNotifications) - super.init(initialViewState: NotificationSettingsScreenViewState(bindings: bindings)) + super.init(initialViewState: NotificationSettingsScreenViewState(bindings: bindings, isModallyPresented: isModallyPresented)) // Listen for changes to AppSettings.enableNotifications appSettings.$enableNotifications @@ -73,6 +73,8 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy return } Task { await enableCalls(state.bindings.callsEnabled) } + case .close: + actionsSubject.send(.close) } } diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift index 9efe10265..fe7127a16 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift @@ -37,12 +37,25 @@ struct NotificationSettingsScreen: View { } .compoundForm() .navigationTitle(L10n.screenNotificationSettingsTitle) + .navigationBarTitleDisplayMode(.inline) + .toolbar { toolbar } .alert(item: $context.alertInfo) .track(screen: .settingsNotifications) } // MARK: - Private - + + @ToolbarContentBuilder + private var toolbar: some ToolbarContent { + if context.viewState.isModallyPresented { + ToolbarItem(placement: .confirmationAction) { + Button(L10n.actionClose) { + context.send(viewAction: .close) + } + } + } + } + private var userPermissionSection: some View { Section { HStack(alignment: .firstTextBaseline, spacing: 13) { @@ -183,7 +196,8 @@ struct NotificationSettingsScreen_Previews: PreviewProvider { var viewModel = NotificationSettingsScreenViewModel(appSettings: appSettings, userNotificationCenter: notificationCenter, - notificationSettingsProxy: notificationSettingsProxy) + notificationSettingsProxy: notificationSettingsProxy, + isModallyPresented: true) viewModel.fetchInitialContent() return viewModel }() diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift index 919fa8c07..df31c5bd2 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift @@ -142,7 +142,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { private func presentNotificationSettings() { let notificationParameters = NotificationSettingsScreenCoordinatorParameters(userNotificationCenter: UNUserNotificationCenter.current(), - notificationSettings: parameters.notificationSettings) + notificationSettings: parameters.notificationSettings, + isModallyPresented: false) let coordinator = NotificationSettingsScreenCoordinator(parameters: notificationParameters) parameters.navigationStackCoordinator?.push(coordinator) } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 14eaf0591..ae08da674 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -195,7 +195,8 @@ class MockScreen: Identifiable { let userNotificationCenter = UserNotificationCenterMock() userNotificationCenter.authorizationStatusReturnValue = .denied let parameters = NotificationSettingsScreenCoordinatorParameters(userNotificationCenter: userNotificationCenter, - notificationSettings: NotificationSettingsProxyMock(with: .init())) + notificationSettings: NotificationSettingsProxyMock(with: .init()), + isModallyPresented: false) return NotificationSettingsScreenCoordinator(parameters: parameters) case .onboarding: return OnboardingCoordinator() @@ -481,14 +482,16 @@ class MockScreen: Identifiable { case .roomNotificationSettingsDefaultSetting: let navigationStackCoordinator = NavigationStackCoordinator() let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie] - let coordinator = RoomNotificationSettingsScreenCoordinator(parameters: .init(notificationSettingsProxy: NotificationSettingsProxyMock(with: .init(defaultRoomMode: .allMessages, roomMode: .allMessages)), + let coordinator = RoomNotificationSettingsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, + notificationSettingsProxy: NotificationSettingsProxyMock(with: .init(defaultRoomMode: .allMessages, roomMode: .allMessages)), roomProxy: RoomProxyMock(with: .init(displayName: "test", members: members)))) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomNotificationSettingsCustomSetting: let navigationStackCoordinator = NavigationStackCoordinator() let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie] - let coordinator = RoomNotificationSettingsScreenCoordinator(parameters: .init(notificationSettingsProxy: NotificationSettingsProxyMock(with: .init(defaultRoomMode: .allMessages, roomMode: .mentionsAndKeywordsOnly)), + let coordinator = RoomNotificationSettingsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, + notificationSettingsProxy: NotificationSettingsProxyMock(with: .init(defaultRoomMode: .allMessages, roomMode: .mentionsAndKeywordsOnly)), roomProxy: RoomProxyMock(with: .init(displayName: "test", members: members)))) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator diff --git a/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift index ee75a3e78..4faa4d1b8 100644 --- a/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift @@ -40,7 +40,8 @@ class NotificationSettingsScreenViewModelTests: XCTestCase { viewModel = NotificationSettingsScreenViewModel(appSettings: appSettings, userNotificationCenter: userNotificationCenter, - notificationSettingsProxy: notificationSettingsProxy) + notificationSettingsProxy: notificationSettingsProxy, + isModallyPresented: false) context = viewModel.context }