From a11be29ba9104cc3d9f3592cb21b5b0440dd17a8 Mon Sep 17 00:00:00 2001 From: psmattas Date: Mon, 17 Aug 2026 21:49:11 +0100 Subject: [PATCH] feat(settings): grey out server-synced settings while offline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Profile's name/avatar and every Preferences toggle (except Appearance, which is local-only) now disable + show a hint when isEffectivelyOnline is false, instead of letting a save silently fail. Matches how Share/ Permissions/Search already behave — these are a "needs a real connection" category, not queued through the offline write queue (infrequent writes, not worth a second offline-sync path for). --- Outpost/Features/Account/SettingsView.swift | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Outpost/Features/Account/SettingsView.swift b/Outpost/Features/Account/SettingsView.swift index 7ebd559..917297a 100644 --- a/Outpost/Features/Account/SettingsView.swift +++ b/Outpost/Features/Account/SettingsView.swift @@ -127,6 +127,9 @@ struct SettingsView: View { private var profileDetail: some View { VStack(alignment: .leading, spacing: 24) { sectionHeader + if !isEffectivelyOnline { + offlineSettingsHint + } avatarRow Divider().frame(maxWidth: 420) nameRow @@ -184,6 +187,7 @@ struct SettingsView: View { ProgressView().controlSize(.small) } } + .disabled(!isEffectivelyOnline) if let avatarErrorMessage { Text(avatarErrorMessage) .font(.caption) @@ -212,6 +216,7 @@ struct SettingsView: View { } } } + .disabled(!isEffectivelyOnline) if let nameErrorMessage { Text(nameErrorMessage) .font(.caption) @@ -248,8 +253,13 @@ struct SettingsView: View { .font(.subheadline) .foregroundStyle(.secondary) + if !isEffectivelyOnline { + offlineSettingsHint + } + preferencesSubsection("Display") { languageRow + .disabled(!isEffectivelyOnline) Divider() appearanceRow Divider() @@ -259,6 +269,7 @@ struct SettingsView: View { isOn: session.userPreferences?.useCursorPointer ?? false, onChange: { newValue in Task { await savePreference { $0.useCursorPointer = newValue } } } ) + .disabled(!isEffectivelyOnline) Divider() preferenceToggleRow( "Show line numbers", @@ -266,6 +277,7 @@ struct SettingsView: View { isOn: session.userPreferences?.codeBlockLineNumbers ?? false, onChange: { newValue in Task { await savePreference { $0.codeBlockLineNumbers = newValue } } } ) + .disabled(!isEffectivelyOnline) Divider() preferenceToggleRow( "Show comment marker", @@ -273,6 +285,7 @@ struct SettingsView: View { isOn: session.userPreferences?.showCommentMarker ?? false, onChange: { newValue in Task { await savePreference { $0.showCommentMarker = newValue } } } ) + .disabled(!isEffectivelyOnline) } preferencesSubsection("Behavior") { @@ -299,6 +312,7 @@ struct SettingsView: View { Divider() notificationBadgeRow } + .disabled(!isEffectivelyOnline) if let preferencesErrorMessage { Text(preferencesErrorMessage) @@ -309,6 +323,7 @@ struct SettingsView: View { preferencesSubsection("Danger") { deleteAccountRow } + .disabled(!isEffectivelyOnline) } .frame(maxWidth: 480, alignment: .leading) .task { await refreshProfile() } @@ -778,6 +793,20 @@ struct SettingsView: View { // MARK: - Helpers + /// Everything backed by a live `users.update`/`users.notifications*` + /// call (Profile's name/avatar, all of Preferences, all of + /// Notifications) shows this and disables its controls while offline — + /// there's nothing to optimistically apply here the way document edits + /// can be, and no offline queue for it (see `TODO.local.md`'s own + /// "things that actually require an internet connection" framing, + /// already applied to Share/Permissions/Search). Local-only settings + /// (Appearance, Offline Mode itself) are deliberately left enabled. + private var offlineSettingsHint: some View { + Text("You're offline — these settings need a connection to change.") + .font(.caption) + .foregroundStyle(.orange) + } + private func labeledRow(_ label: String, _ value: String) -> some View { HStack { Text(label)