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:
@@ -127,6 +127,9 @@ struct SettingsView: View {
|
|||||||
private var profileDetail: some View {
|
private var profileDetail: some View {
|
||||||
VStack(alignment: .leading, spacing: 24) {
|
VStack(alignment: .leading, spacing: 24) {
|
||||||
sectionHeader
|
sectionHeader
|
||||||
|
if !isEffectivelyOnline {
|
||||||
|
offlineSettingsHint
|
||||||
|
}
|
||||||
avatarRow
|
avatarRow
|
||||||
Divider().frame(maxWidth: 420)
|
Divider().frame(maxWidth: 420)
|
||||||
nameRow
|
nameRow
|
||||||
@@ -184,6 +187,7 @@ struct SettingsView: View {
|
|||||||
ProgressView().controlSize(.small)
|
ProgressView().controlSize(.small)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
if let avatarErrorMessage {
|
if let avatarErrorMessage {
|
||||||
Text(avatarErrorMessage)
|
Text(avatarErrorMessage)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
@@ -212,6 +216,7 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
if let nameErrorMessage {
|
if let nameErrorMessage {
|
||||||
Text(nameErrorMessage)
|
Text(nameErrorMessage)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
@@ -248,8 +253,13 @@ struct SettingsView: View {
|
|||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
if !isEffectivelyOnline {
|
||||||
|
offlineSettingsHint
|
||||||
|
}
|
||||||
|
|
||||||
preferencesSubsection("Display") {
|
preferencesSubsection("Display") {
|
||||||
languageRow
|
languageRow
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
Divider()
|
Divider()
|
||||||
appearanceRow
|
appearanceRow
|
||||||
Divider()
|
Divider()
|
||||||
@@ -259,6 +269,7 @@ struct SettingsView: View {
|
|||||||
isOn: session.userPreferences?.useCursorPointer ?? false,
|
isOn: session.userPreferences?.useCursorPointer ?? false,
|
||||||
onChange: { newValue in Task { await savePreference { $0.useCursorPointer = newValue } } }
|
onChange: { newValue in Task { await savePreference { $0.useCursorPointer = newValue } } }
|
||||||
)
|
)
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
Divider()
|
Divider()
|
||||||
preferenceToggleRow(
|
preferenceToggleRow(
|
||||||
"Show line numbers",
|
"Show line numbers",
|
||||||
@@ -266,6 +277,7 @@ struct SettingsView: View {
|
|||||||
isOn: session.userPreferences?.codeBlockLineNumbers ?? false,
|
isOn: session.userPreferences?.codeBlockLineNumbers ?? false,
|
||||||
onChange: { newValue in Task { await savePreference { $0.codeBlockLineNumbers = newValue } } }
|
onChange: { newValue in Task { await savePreference { $0.codeBlockLineNumbers = newValue } } }
|
||||||
)
|
)
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
Divider()
|
Divider()
|
||||||
preferenceToggleRow(
|
preferenceToggleRow(
|
||||||
"Show comment marker",
|
"Show comment marker",
|
||||||
@@ -273,6 +285,7 @@ struct SettingsView: View {
|
|||||||
isOn: session.userPreferences?.showCommentMarker ?? false,
|
isOn: session.userPreferences?.showCommentMarker ?? false,
|
||||||
onChange: { newValue in Task { await savePreference { $0.showCommentMarker = newValue } } }
|
onChange: { newValue in Task { await savePreference { $0.showCommentMarker = newValue } } }
|
||||||
)
|
)
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
}
|
}
|
||||||
|
|
||||||
preferencesSubsection("Behavior") {
|
preferencesSubsection("Behavior") {
|
||||||
@@ -299,6 +312,7 @@ struct SettingsView: View {
|
|||||||
Divider()
|
Divider()
|
||||||
notificationBadgeRow
|
notificationBadgeRow
|
||||||
}
|
}
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
|
|
||||||
if let preferencesErrorMessage {
|
if let preferencesErrorMessage {
|
||||||
Text(preferencesErrorMessage)
|
Text(preferencesErrorMessage)
|
||||||
@@ -309,6 +323,7 @@ struct SettingsView: View {
|
|||||||
preferencesSubsection("Danger") {
|
preferencesSubsection("Danger") {
|
||||||
deleteAccountRow
|
deleteAccountRow
|
||||||
}
|
}
|
||||||
|
.disabled(!isEffectivelyOnline)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: 480, alignment: .leading)
|
.frame(maxWidth: 480, alignment: .leading)
|
||||||
.task { await refreshProfile() }
|
.task { await refreshProfile() }
|
||||||
@@ -778,6 +793,20 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
// MARK: - Helpers
|
// 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 {
|
private func labeledRow(_ label: String, _ value: String) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
Text(label)
|
Text(label)
|
||||||
|
|||||||
Reference in New Issue
Block a user