Skip to content

Commit

Permalink
Notification settings screen (#1414)
Browse files Browse the repository at this point in the history
  • Loading branch information
nimau authored Jul 31, 2023
1 parent 62f78fb commit d38b6f7
Show file tree
Hide file tree
Showing 13 changed files with 521 additions and 28 deletions.
9 changes: 9 additions & 0 deletions ElementX/Resources/Localizations/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,16 @@
"screen_media_upload_preview_error_failed_sending" = "Failed uploading media, please try again.";
"screen_migration_message" = "This is a one time process, thanks for waiting.";
"screen_migration_title" = "Setting up your account.";
"screen_notification_settings_additional_settings_section_title" = "Additional settings";
"screen_notification_settings_calls_label" = "Audio and video calls";
"screen_notification_settings_direct_chats" = "Direct chats";
"screen_notification_settings_enable_notifications" = "Enable notifications on this device";
"screen_notification_settings_group_chats" = "Group chats";
"screen_notification_settings_mentions_section_title" = "Mentions";
"screen_notification_settings_mode_all" = "All";
"screen_notification_settings_mode_mentions" = "Mentions";
"screen_notification_settings_notification_section_title" = "Notify me for";
"screen_notification_settings_room_mention_label" = "Notify me on @room";
"screen_notification_settings_system_notifications_action_required" = "To receive notifications, please change your %1$@.";
"screen_notification_settings_system_notifications_action_required_content_link" = "system settings";
"screen_notification_settings_system_notifications_turned_off" = "System notifications turned off";
Expand Down
15 changes: 11 additions & 4 deletions ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,14 +299,21 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
// MARK: Settings

private func presentSettingsScreen(animated: Bool) {
Task {
await asyncPresentSettingsScreen(animated: animated)
}
}

private func asyncPresentSettingsScreen(animated: Bool) async {
let settingsNavigationStackCoordinator = NavigationStackCoordinator()

let userIndicatorController = UserIndicatorController(rootCoordinator: settingsNavigationStackCoordinator)

let parameters = SettingsScreenCoordinatorParameters(navigationStackCoordinator: settingsNavigationStackCoordinator,
userIndicatorController: userIndicatorController,
userSession: userSession,
bugReportService: bugReportService)
let parameters = await SettingsScreenCoordinatorParameters(navigationStackCoordinator: settingsNavigationStackCoordinator,
userIndicatorController: userIndicatorController,
userSession: userSession,
bugReportService: bugReportService,
notificationSettings: userSession.clientProxy.notificationSettings())
let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: parameters)
settingsScreenCoordinator.callback = { [weak self] action in
guard let self else { return }
Expand Down
18 changes: 18 additions & 0 deletions ElementX/Sources/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -672,8 +672,26 @@ public enum L10n {
public static var screenMigrationMessage: String { return L10n.tr("Localizable", "screen_migration_message") }
/// Setting up your account.
public static var screenMigrationTitle: String { return L10n.tr("Localizable", "screen_migration_title") }
/// Additional settings
public static var screenNotificationSettingsAdditionalSettingsSectionTitle: String { return L10n.tr("Localizable", "screen_notification_settings_additional_settings_section_title") }
/// Audio and video calls
public static var screenNotificationSettingsCallsLabel: String { return L10n.tr("Localizable", "screen_notification_settings_calls_label") }
/// Direct chats
public static var screenNotificationSettingsDirectChats: String { return L10n.tr("Localizable", "screen_notification_settings_direct_chats") }
/// Enable notifications on this device
public static var screenNotificationSettingsEnableNotifications: String { return L10n.tr("Localizable", "screen_notification_settings_enable_notifications") }
/// Group chats
public static var screenNotificationSettingsGroupChats: String { return L10n.tr("Localizable", "screen_notification_settings_group_chats") }
/// Mentions
public static var screenNotificationSettingsMentionsSectionTitle: String { return L10n.tr("Localizable", "screen_notification_settings_mentions_section_title") }
/// All
public static var screenNotificationSettingsModeAll: String { return L10n.tr("Localizable", "screen_notification_settings_mode_all") }
/// Mentions
public static var screenNotificationSettingsModeMentions: String { return L10n.tr("Localizable", "screen_notification_settings_mode_mentions") }
/// Notify me for
public static var screenNotificationSettingsNotificationSectionTitle: String { return L10n.tr("Localizable", "screen_notification_settings_notification_section_title") }
/// Notify me on @room
public static var screenNotificationSettingsRoomMentionLabel: String { return L10n.tr("Localizable", "screen_notification_settings_room_mention_label") }
/// To receive notifications, please change your %1$@.
public static func screenNotificationSettingsSystemNotificationsActionRequired(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_notification_settings_system_notifications_action_required", String(describing: p1))
Expand Down
14 changes: 14 additions & 0 deletions ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,19 @@ extension NotificationSettingsProxyMock {
self.callbacks.send(.settingsDidChange)
}
}
setRoomMentionEnabledEnabledClosure = { [weak self] enabled in
guard let self else { return }
self.isRoomMentionEnabledReturnValue = enabled
Task {
self.callbacks.send(.settingsDidChange)
}
}
setCallEnabledEnabledClosure = { [weak self] enabled in
guard let self else { return }
self.isCallEnabledReturnValue = enabled
Task {
self.callbacks.send(.settingsDidChange)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import SwiftUI

struct NotificationSettingsScreenCoordinatorParameters {
let userNotificationCenter: UserNotificationCenterProtocol
let notificationSettings: NotificationSettingsProxyProtocol
}

enum NotificationSettingsScreenCoordinatorAction { }
Expand All @@ -37,10 +38,13 @@ final class NotificationSettingsScreenCoordinator: CoordinatorProtocol {
self.parameters = parameters

viewModel = NotificationSettingsScreenViewModel(appSettings: ServiceLocator.shared.settings,
userNotificationCenter: parameters.userNotificationCenter)
userNotificationCenter: parameters.userNotificationCenter,
notificationSettingsProxy: parameters.notificationSettings)
}

func start() { }
func start() {
viewModel.fetchInitialContent()
}

func toPresentable() -> AnyView {
AnyView(NotificationSettingsScreen(context: viewModel.context))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,31 @@ struct NotificationSettingsScreenViewState: BindableState {
var bindings: NotificationSettingsScreenViewStateBindings
var strings = NotificationSettingsScreenStrings()
var isUserPermissionGranted: Bool?
var allowedNotificationModes: [RoomNotificationModeProxy] = [.allMessages, .mentionsAndKeywordsOnly]

var showSystemNotificationsAlert: Bool {
bindings.enableNotifications && isUserPermissionGranted == false
}

var settings: NotificationSettingsScreenSettings?
var applyingChange = false
}

struct NotificationSettingsScreenViewStateBindings {
var enableNotifications = false
var roomMentionsEnabled = false
var callsEnabled = false
var alertInfo: AlertInfo<NotificationSettingsScreenErrorType>?
}

struct NotificationSettingsScreenSettings {
let groupChatsMode: RoomNotificationModeProxy
let directChatsMode: RoomNotificationModeProxy
let roomMentionsEnabled: Bool?
let callsEnabled: Bool?
// Old clients were having specific settings for encrypted and unencrypted rooms,
// so it's possible for `group chats` and `direct chats` settings to be inconsistent (e.g. encrypted `direct chats` can have a different mode that unencrypted `direct chats`)
let inconsistentSettings: Bool
}

struct NotificationSettingsScreenStrings {
Expand All @@ -45,9 +62,29 @@ struct NotificationSettingsScreenStrings {

return text
}()

func string(for mode: RoomNotificationModeProxy) -> String {
switch mode {
case .allMessages:
return L10n.screenNotificationSettingsModeAll
case .mentionsAndKeywordsOnly:
return L10n.screenNotificationSettingsModeMentions
case .mute:
return L10n.commonMute
}
}
}

enum NotificationSettingsScreenViewAction {
case linkClicked(url: URL)
case changedEnableNotifications
case groupChatsTapped
case directChatsTapped
case roomMentionChanged
case callsChanged
}

enum NotificationSettingsScreenErrorType: Hashable {
/// A specific error message shown in an alert.
case alert
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy
private var actionsSubject: PassthroughSubject<NotificationSettingsScreenViewModelAction, Never> = .init()
private let appSettings: AppSettings
private let userNotificationCenter: UserNotificationCenterProtocol
private let notificationSettingsProxy: NotificationSettingsProxyProtocol
@CancellableTask private var fetchSettingsTask: Task<Void, Error>?

var actions: AnyPublisher<NotificationSettingsScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}

init(appSettings: AppSettings, userNotificationCenter: UserNotificationCenterProtocol) {
init(appSettings: AppSettings, userNotificationCenter: UserNotificationCenterProtocol, notificationSettingsProxy: NotificationSettingsProxyProtocol) {
self.appSettings = appSettings
self.userNotificationCenter = userNotificationCenter
self.notificationSettingsProxy = notificationSettingsProxy

let bindings = NotificationSettingsScreenViewStateBindings(enableNotifications: appSettings.enableNotifications)
super.init(initialViewState: NotificationSettingsScreenViewState(bindings: bindings))

Expand All @@ -39,18 +43,12 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy
.weakAssign(to: \.state.bindings.enableNotifications, on: self)
.store(in: &cancellables)

// Refresh authorization status uppon UIApplication.didBecomeActiveNotification notification
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
.sink { [weak self] _ in
Task {
await self?.readSystemAuthorizationStatus()
}
}
.store(in: &cancellables)

Task {
await readSystemAuthorizationStatus()
}
setupDidBecomeActiveSubscription()
setupNotificationSettingsSubscription()
}

func fetchInitialContent() {
fetchSettings()
}

// MARK: - Public
Expand All @@ -61,16 +59,129 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy
MXLog.warning("Link clicked: \(url)")
case .changedEnableNotifications:
toggleNotifications()
case .groupChatsTapped:
break
case .directChatsTapped:
break
case .roomMentionChanged:
guard let settings = state.settings, settings.roomMentionsEnabled != state.bindings.roomMentionsEnabled else {
return
}
Task { await enableRoomMention(state.bindings.roomMentionsEnabled) }
case .callsChanged:
guard let settings = state.settings, settings.callsEnabled != state.bindings.callsEnabled else {
return
}
Task { await enableCalls(state.bindings.callsEnabled) }
}
}

// MARK: - Private

func readSystemAuthorizationStatus() async {
state.isUserPermissionGranted = await userNotificationCenter.authorizationStatus() == .authorized
}

func toggleNotifications() {
appSettings.enableNotifications.toggle()
}

private func setupDidBecomeActiveSubscription() {
// Refresh authorization status uppon UIApplication.didBecomeActiveNotification notification
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
.sink { [weak self] _ in
Task {
await self?.readSystemAuthorizationStatus()
}
}
.store(in: &cancellables)

Task {
await readSystemAuthorizationStatus()
}
}

private func setupNotificationSettingsSubscription() {
notificationSettingsProxy.callbacks
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
guard let self else { return }

switch callback {
case .settingsDidChange:
self.fetchSettings()
}
}
.store(in: &cancellables)
}

private func fetchSettings() {
fetchSettingsTask = Task {
// Group chats
// A group chat is a chat having more than 2 active members
let groupChatActiveMembers: UInt64 = 3
var groupChatsMode = await notificationSettingsProxy.getDefaultNotificationRoomMode(isEncrypted: false, activeMembersCount: groupChatActiveMembers)
let encryptedGroupChatsMode = await notificationSettingsProxy.getDefaultNotificationRoomMode(isEncrypted: true, activeMembersCount: groupChatActiveMembers)

// Direct chats
// A direct chat is a chat having exactly 2 active members
let directChatActiveMembers: UInt64 = 2
var directChatsMode = await notificationSettingsProxy.getDefaultNotificationRoomMode(isEncrypted: false, activeMembersCount: directChatActiveMembers)
let encryptedDirectChatsMode = await notificationSettingsProxy.getDefaultNotificationRoomMode(isEncrypted: true, activeMembersCount: directChatActiveMembers)

// Old clients were having specific settings for encrypted and unencrypted rooms,
// so it's possible for `group chats` and `direct chats` settings to be inconsistent (e.g. encrypted `direct chats` can have a different mode that unencrypted `direct chats`)
var inconsistencyDetected = false
if groupChatsMode != encryptedGroupChatsMode {
groupChatsMode = .allMessages
inconsistencyDetected = true
}
if directChatsMode != encryptedDirectChatsMode {
directChatsMode = .allMessages
inconsistencyDetected = true
}

// The following calls may fail if the associated push rule doesn't exist
let roomMentionsEnabled = try? await notificationSettingsProxy.isRoomMentionEnabled()
let callEnabled = try? await notificationSettingsProxy.isCallEnabled()

guard !Task.isCancelled else { return }

let notificationSettings = NotificationSettingsScreenSettings(groupChatsMode: groupChatsMode,
directChatsMode: directChatsMode,
roomMentionsEnabled: roomMentionsEnabled,
callsEnabled: callEnabled,
inconsistentSettings: inconsistencyDetected)

state.settings = notificationSettings
state.bindings.roomMentionsEnabled = notificationSettings.roomMentionsEnabled ?? false
state.bindings.callsEnabled = notificationSettings.callsEnabled ?? false
}
}

private func enableRoomMention(_ enable: Bool) async {
guard let notificationSettings = state.settings else { return }
do {
state.applyingChange = true
MXLog.info("[NotificationSettingsScreenViewMode] setRoomMentionEnabled(\(enable))")
try await notificationSettingsProxy.setRoomMentionEnabled(enabled: enable)
} catch {
state.bindings.alertInfo = AlertInfo(id: .alert)
state.bindings.roomMentionsEnabled = notificationSettings.roomMentionsEnabled ?? false
}
state.applyingChange = false
}

func enableCalls(_ enable: Bool) async {
guard let notificationSettings = state.settings else { return }
do {
state.applyingChange = true
MXLog.info("[NotificationSettingsScreenViewMode] setCallEnabled(\(enable))")
try await notificationSettingsProxy.setCallEnabled(enabled: enable)
} catch {
state.bindings.alertInfo = AlertInfo(id: .alert)
state.bindings.callsEnabled = notificationSettings.callsEnabled ?? false
}
state.applyingChange = false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ import Combine
protocol NotificationSettingsScreenViewModelProtocol {
var actions: AnyPublisher<NotificationSettingsScreenViewModelAction, Never> { get }
var context: NotificationSettingsScreenViewModelType.Context { get }

func fetchInitialContent()
}
Loading

0 comments on commit d38b6f7

Please sign in to comment.