1import { useAtomValue } from "@effect/atom-react";
2import { AsyncResult } from "effect/reactivity";
3import { useState } from "react";
4import { View } from "react-native";
5import type { EnvironmentId, UiSettingsProfile } from "@t3tools/contracts";
6import { AppText as Text } from "../../components/AppText";
7import { useEnvironments } from "../../state/environments";
8import { useAtomCommand } from "../../state/use-atom-command";
9import {
10 uiSettingsSyncAtoms,
11 useUiSettingsSync,
12 useUiSettingsSyncActions,
13} from "../../state/uiSettingsSync";
14import { SettingsSection } from "./components/SettingsSection";
15import { SettingsActionRow } from "./components/SettingsActionRow";
16import { SettingsChoiceRow } from "./components/SettingsChoiceRow";
17
18export 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}