feat(settings): grey out server-synced settings while offline

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).
This commit is contained in:
2026-08-17 21:49:11 +01:00
parent 564cce0637
commit a11be29ba9
@@ -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)