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 isShowingDeleteAccountConfirmation = false
|
||||||
@State private var isDeletingAccount = false
|
@State private var isDeletingAccount = false
|
||||||
@State private var deleteAccountErrorMessage: String?
|
@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
|
/// Full Local Sync and cache-clearing both need a real connection to be
|
||||||
/// safe — clearing while offline (or letting Full Local Sync think it
|
/// safe — clearing while offline (or letting Full Local Sync think it
|
||||||
@@ -79,6 +81,7 @@ struct SettingsView: View {
|
|||||||
case .appearance: appearanceDetail
|
case .appearance: appearanceDetail
|
||||||
case .profile: profileDetail
|
case .profile: profileDetail
|
||||||
case .preferences: preferencesDetail
|
case .preferences: preferencesDetail
|
||||||
|
case .notifications: notificationsDetail
|
||||||
case .offlineSync: offlineSyncDetail
|
case .offlineSync: offlineSyncDetail
|
||||||
case .advanced: advancedDetail
|
case .advanced: advancedDetail
|
||||||
case .about: aboutDetail
|
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
|
// MARK: - Offline & Sync
|
||||||
|
|
||||||
private var offlineSyncDetail: some View {
|
private var offlineSyncDetail: some View {
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ enum SettingsSection: String, CaseIterable, Identifiable, Hashable {
|
|||||||
/// placeholder until its content is specified and built.
|
/// placeholder until its content is specified and built.
|
||||||
var isImplemented: Bool {
|
var isImplemented: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .appearance, .offlineSync, .advanced, .about, .profile, .preferences:
|
case .appearance, .offlineSync, .advanced, .about, .profile, .preferences, .notifications:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
Reference in New Issue
Block a user