feat(settings): Notifications section skeleton + All-notifications toggle
Marks .notifications implemented, adds the shared setNotifications save path (per-event subscribe/unsubscribe, sequential calls for toggles that group more than one wire event type) and the master "All notifications" row. Individual event toggles land in follow-up commits.
This commit is contained in:
@@ -39,6 +39,8 @@ struct SettingsView: View {
|
||||
@State private var isShowingDeleteAccountConfirmation = false
|
||||
@State private var isDeletingAccount = false
|
||||
@State private var deleteAccountErrorMessage: String?
|
||||
@State private var isSavingNotifications = false
|
||||
@State private var notificationsErrorMessage: String?
|
||||
|
||||
/// Full Local Sync and cache-clearing both need a real connection to be
|
||||
/// safe — clearing while offline (or letting Full Local Sync think it
|
||||
@@ -79,6 +81,7 @@ struct SettingsView: View {
|
||||
case .appearance: appearanceDetail
|
||||
case .profile: profileDetail
|
||||
case .preferences: preferencesDetail
|
||||
case .notifications: notificationsDetail
|
||||
case .offlineSync: offlineSyncDetail
|
||||
case .advanced: advancedDetail
|
||||
case .about: aboutDetail
|
||||
@@ -544,6 +547,84 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
private var notificationsDetail: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
sectionHeader
|
||||
Text("Manage when and where you receive email notifications.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if !isEffectivelyOnline {
|
||||
offlineSettingsHint
|
||||
}
|
||||
|
||||
allNotificationsRow
|
||||
Divider()
|
||||
|
||||
if let notificationsErrorMessage {
|
||||
Text(notificationsErrorMessage)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
.disabled(!isEffectivelyOnline)
|
||||
.frame(maxWidth: 480, alignment: .leading)
|
||||
.task { await refreshProfile() }
|
||||
}
|
||||
|
||||
private var allNotificationsRow: some View {
|
||||
let isOn = NotificationEventType.allCases.allSatisfy { session.userNotificationSettings?[$0.rawValue] ?? false }
|
||||
return notificationToggleRow(
|
||||
"All notifications",
|
||||
description: nil,
|
||||
isOn: isOn,
|
||||
onChange: { newValue in Task { await setNotifications(nil, subscribed: newValue) } }
|
||||
)
|
||||
}
|
||||
|
||||
private func notificationToggleRow(
|
||||
_ title: String,
|
||||
description: String?,
|
||||
isOn: Bool,
|
||||
onChange: @escaping (Bool) -> Void
|
||||
) -> some View {
|
||||
Toggle(isOn: Binding(get: { isOn }, set: onChange)) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
if let description {
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `eventTypes` groups more than one wire event under a single visible
|
||||
/// toggle (e.g. "Mentioned" covers both `comments.mentioned` and
|
||||
/// `documents.mentioned`) — confirmed as the right grouping against
|
||||
/// Outline's own settings copy, applied with one subscribe/unsubscribe
|
||||
/// call per underlying event type, sequentially.
|
||||
private func setNotifications(_ eventTypes: [NotificationEventType]?, subscribed: Bool) async {
|
||||
guard let apiClient = session.apiClient else { return }
|
||||
isSavingNotifications = true
|
||||
defer { isSavingNotifications = false }
|
||||
let targets: [NotificationEventType?] = eventTypes ?? [nil]
|
||||
do {
|
||||
for target in targets {
|
||||
let updated = subscribed
|
||||
? try await apiClient.subscribeToNotifications(eventType: target)
|
||||
: try await apiClient.unsubscribeFromNotifications(eventType: target)
|
||||
session.applyUpdatedProfile(updated)
|
||||
}
|
||||
notificationsErrorMessage = nil
|
||||
} catch {
|
||||
notificationsErrorMessage = outlineErrorMessage(error, fallback: "Couldn't update your notification settings.")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Offline & Sync
|
||||
|
||||
private var offlineSyncDetail: some View {
|
||||
|
||||
@@ -122,7 +122,7 @@ enum SettingsSection: String, CaseIterable, Identifiable, Hashable {
|
||||
/// placeholder until its content is specified and built.
|
||||
var isImplemented: Bool {
|
||||
switch self {
|
||||
case .appearance, .offlineSync, .advanced, .about, .profile, .preferences:
|
||||
case .appearance, .offlineSync, .advanced, .about, .profile, .preferences, .notifications:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user