| 1 | import { useAtomValue } from "@effect/atom-react"; |
| 2 | import { AsyncResult } from "effect/reactivity"; |
| 3 | import { useState } from "react"; |
| 4 | import { View } from "react-native"; |
| 5 | import type { EnvironmentId, UiSettingsProfile } from "@t3tools/contracts"; |
| 6 | import { AppText as Text } from "../../components/AppText"; |
| 7 | import { useEnvironments } from "../../state/environments"; |
| 8 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 9 | import { |
| 10 | uiSettingsSyncAtoms, |
| 11 | useUiSettingsSync, |
| 12 | useUiSettingsSyncActions, |
| 13 | } from "../../state/uiSettingsSync"; |
| 14 | import { SettingsSection } from "./components/SettingsSection"; |
| 15 | import { SettingsActionRow } from "./components/SettingsActionRow"; |
| 16 | import { SettingsChoiceRow } from "./components/SettingsChoiceRow"; |
| 17 | |
| 18 | export function UiSettingsSyncSection() { |
| 19 | const { environments } = useEnvironments(); |
| 20 | const sync = useUiSettingsSync(); |
| 21 | const actions = useUiSettingsSyncActions(); |
| 22 | const [selected, setSelected] = useState<EnvironmentId | null>(null); |
| 23 | const [saved, setSaved] = useState<UiSettingsProfile | null>(null); |
| 24 | const [busy, setBusy] = useState(false); |
| 25 | const [error, setError] = useState<string | null>(null); |
| 26 | const read = useAtomCommand(uiSettingsSyncAtoms.read, { reportFailure: false }); |
| 27 | const permission = useAtomValue(uiSettingsSyncAtoms.patch.permissionAtom(selected)); |
| 28 | const eligible = environments.filter( |
| 29 | (entry) => |
| 30 | entry.connection.phase === "connected" && |
| 31 | entry.serverConfig?.environment.capabilities.uiSettingsSync === true, |
| 32 | ); |
| 33 | const active = environments.find((entry) => entry.environmentId === sync.environmentId); |
| 34 | |
| 35 | async function inspect() { |
| 36 | if (!selected) return; |
| 37 | setBusy(true); |
| 38 | setError(null); |
| 39 | const result = await read({ environmentId: selected, input: {} }); |
| 40 | if (AsyncResult.isSuccess(result)) setSaved(result.value); |
| 41 | else setError("Unable to read the profile. Reconnect to this environment and try again."); |
| 42 | setBusy(false); |
| 43 | } |
| 44 | async function enable(mode: "adopt" | "publish") { |
| 45 | if (!selected || !saved) return; |
| 46 | setBusy(true); |
| 47 | setError(null); |
| 48 | try { |
| 49 | await actions.enable(selected, saved, mode); |
| 50 | setSaved(null); |
| 51 | setSelected(null); |
| 52 | } catch { |
| 53 | setError("Unable to enable sync. Check device storage and try again."); |
| 54 | } finally { |
| 55 | setBusy(false); |
| 56 | } |
| 57 | } |
| 58 | return ( |
| 59 | <View className="gap-3"> |
| 60 | <SettingsSection |
| 61 | title="UI settings sync" |
| 62 | footer="Sync follow-ups, project grouping, and the working shelf. Native themes and text sizes stay on this device." |
| 63 | > |
| 64 | {eligible.map((entry, index) => ( |
| 65 | <SettingsChoiceRow |
| 66 | key={entry.environmentId} |
| 67 | label={entry.label} |
| 68 | selected={selected === entry.environmentId} |
| 69 | separated={index > 0} |
| 70 | disabled={busy} |
| 71 | onPress={() => { |
| 72 | setSelected(entry.environmentId); |
| 73 | setSaved(null); |
| 74 | setError(null); |
| 75 | }} |
| 76 | /> |
| 77 | ))} |
| 78 | <SettingsActionRow |
| 79 | icon="arrow.clockwise" |
| 80 | label="Read profile" |
| 81 | disabled={!selected || !permission || busy} |
| 82 | loading={busy} |
| 83 | onPress={() => void inspect()} |
| 84 | /> |
| 85 | {saved && saved.revision > 0 ? ( |
| 86 | <SettingsActionRow |
| 87 | icon="checkmark" |
| 88 | label="Use saved" |
| 89 | disabled={busy} |
| 90 | onPress={() => void enable("adopt")} |
| 91 | /> |
| 92 | ) : null} |
| 93 | {saved ? ( |
| 94 | <SettingsActionRow |
| 95 | icon="arrow.up" |
| 96 | label="Publish current" |
| 97 | disabled={busy || !permission} |
| 98 | onPress={() => void enable("publish")} |
| 99 | /> |
| 100 | ) : null} |
| 101 | {saved ? ( |
| 102 | <SettingsActionRow |
| 103 | icon="xmark" |
| 104 | label="Cancel" |
| 105 | disabled={busy} |
| 106 | onPress={() => setSaved(null)} |
| 107 | /> |
| 108 | ) : null} |
| 109 | {sync.environmentId ? ( |
| 110 | <SettingsActionRow |
| 111 | icon="xmark" |
| 112 | label="Disable sync" |
| 113 | disabled={busy} |
| 114 | onPress={() => { |
| 115 | void actions |
| 116 | .disable() |
| 117 | .catch(() => |
| 118 | setError("Unable to disable sync. Check device storage and try again."), |
| 119 | ); |
| 120 | }} |
| 121 | /> |
| 122 | ) : null} |
| 123 | </SettingsSection> |
| 124 | <Text accessibilityLiveRegion="polite" className="px-2 text-sm text-foreground-muted"> |
| 125 | {sync.environmentId === null |
| 126 | ? "Off" |
| 127 | : sync.status === "synced" |
| 128 | ? `Following ${active?.label ?? "your saved environment"}` |
| 129 | : sync.status === "syncing" |
| 130 | ? "Syncing…" |
| 131 | : "Sync paused. Changes stay on this device until the profile environment reconnects."} |
| 132 | </Text> |
| 133 | {saved ? ( |
| 134 | <Text className="px-2 text-sm text-foreground-muted"> |
| 135 | {saved.revision === 0 |
| 136 | ? "No profile saved. Publish this device’s current preferences to start." |
| 137 | : "Use the saved profile, or publish this device’s shared preferences for other participating devices."} |
| 138 | </Text> |
| 139 | ) : null} |
| 140 | {eligible.length === 0 ? ( |
| 141 | <Text className="px-2 text-sm text-foreground-muted"> |
| 142 | Connect to an updated private environment to set up sync. |
| 143 | </Text> |
| 144 | ) : null} |
| 145 | {selected && !permission ? ( |
| 146 | <Text className="px-2 text-sm text-foreground-muted"> |
| 147 | Settings access is required. Update this connection’s permissions in Connections. |
| 148 | </Text> |
| 149 | ) : null} |
| 150 | {sync.conflicts.length > 0 ? ( |
| 151 | <Text accessibilityLiveRegion="polite" className="px-2 text-sm text-foreground-muted"> |
| 152 | Newer saved preferences replaced conflicting edits. Change a preference again to use your |
| 153 | choice. |
| 154 | </Text> |
| 155 | ) : null} |
| 156 | {error ? ( |
| 157 | <Text accessibilityRole="alert" className="px-2 text-sm text-danger-foreground"> |
| 158 | {error} |
| 159 | </Text> |
| 160 | ) : null} |
| 161 | </View> |
| 162 | ); |
| 163 | } |