diff --git a/Outpost/Features/Account/SettingsView.swift b/Outpost/Features/Account/SettingsView.swift index 917297a..45c01d2 100644 --- a/Outpost/Features/Account/SettingsView.swift +++ b/Outpost/Features/Account/SettingsView.swift @@ -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 { diff --git a/Outpost/Root/AppNavigation.swift b/Outpost/Root/AppNavigation.swift index 47f47bc..ea7f0b2 100644 --- a/Outpost/Root/AppNavigation.swift +++ b/Outpost/Root/AppNavigation.swift @@ -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