summaryrefslogtreecommitdiff
path: root/app/(tabs)/controls.tsx
diff options
context:
space:
mode:
authorJustZvan <justzvan@justzvan.xyz>2026-02-06 13:22:33 +0100
committerJustZvan <justzvan@justzvan.xyz>2026-02-06 13:22:33 +0100
commit7eb8ccae48b0cc18a9dcaa9c3626a02df8e6d919 (patch)
tree57b7dd06ac9aa7053c671d916f7183e3b4fa9410 /app/(tabs)/controls.tsx
feat: initial commit!
Diffstat (limited to 'app/(tabs)/controls.tsx')
-rw-r--r--app/(tabs)/controls.tsx270
1 files changed, 270 insertions, 0 deletions
diff --git a/app/(tabs)/controls.tsx b/app/(tabs)/controls.tsx
new file mode 100644
index 0000000..ac44f1b
--- /dev/null
+++ b/app/(tabs)/controls.tsx
@@ -0,0 +1,270 @@
+import { useEffect, useState } from "react";
+import { Pressable, Switch, View } from "react-native";
+import { ControlsData, getControlsData, updateSafetyControl } from "../../api";
+import { useDevice } from "../../lib/device";
+import { t } from "../../lib/locales";
+import { colors } from "../../lib/theme";
+import {
+ Card,
+ ConfirmDialog,
+ DeviceSelector,
+ Divider,
+ H2,
+ Muted,
+ Row,
+ Screen,
+ SelectDialog,
+} from "../../lib/ui";
+
+export default function ControlsScreen() {
+ const [data, setData] = useState<ControlsData | null>(null);
+ const [state, setState] = useState<Record<string, boolean>>({});
+ const [communication, setCommunication] = useState<"scan" | "block">("scan");
+ const [communicationModalVisible, setCommunicationModalVisible] =
+ useState(false);
+ const [confirmModalVisible, setConfirmModalVisible] = useState(false);
+ const [pendingSelection, setPendingSelection] = useState<
+ "scan" | "block" | null
+ >(null);
+
+ const {
+ devices,
+ selectedDevice,
+ selectDevice,
+ isLoading: isDeviceLoading,
+ } = useDevice();
+
+ useEffect(() => {
+ if (selectedDevice) {
+ getControlsData().then((d) => {
+ setData(d);
+ setState(
+ Object.fromEntries(
+ d.safetyControls.map((c) => [c.key, c.defaultValue]),
+ ),
+ );
+ const blockDefault = d.safetyControls.find(
+ (c) => c.key === "block_strangers",
+ )?.defaultValue;
+ setCommunication(blockDefault ? "block" : "scan");
+ });
+ }
+ }, [selectedDevice]);
+
+ const handleToggle = (key: string, value: boolean) => {
+ setState((prev) => ({ ...prev, [key]: value }));
+ updateSafetyControl(key, value);
+ };
+
+ const openCommunicationOptions = () => {
+ setCommunicationModalVisible(true);
+ };
+
+ const chooseCommunication = (choice: "scan" | "block") => {
+ if (choice === "scan") {
+ setCommunication("scan");
+ handleToggle("block_strangers", false);
+ setCommunicationModalVisible(false);
+ } else {
+ // block chosen - show confirmation
+ setPendingSelection("block");
+ setConfirmModalVisible(true);
+ setCommunicationModalVisible(false);
+ }
+ };
+
+ const confirmBlock = () => {
+ setCommunication("block");
+ handleToggle("block_strangers", true);
+ setConfirmModalVisible(false);
+ setPendingSelection(null);
+ };
+
+ const cancelConfirm = () => {
+ setConfirmModalVisible(false);
+ setPendingSelection(null);
+ };
+
+ if (!selectedDevice && !isDeviceLoading)
+ return (
+ <Screen>
+ <DeviceSelector
+ devices={devices}
+ selectedDevice={selectedDevice}
+ onSelectDevice={selectDevice}
+ isLoading={isDeviceLoading}
+ />
+ <Muted>{t("noDeviceSelected")}</Muted>
+ </Screen>
+ );
+
+ if (!data)
+ return (
+ <Screen>
+ <DeviceSelector
+ devices={devices}
+ selectedDevice={selectedDevice}
+ onSelectDevice={selectDevice}
+ isLoading={isDeviceLoading}
+ />
+ </Screen>
+ );
+
+ return (
+ <Screen>
+ <DeviceSelector
+ devices={devices}
+ selectedDevice={selectedDevice}
+ onSelectDevice={selectDevice}
+ isLoading={isDeviceLoading}
+ />
+
+ <Card>
+ <H2>{t("disableBuddy")}</H2>
+ <Divider />
+ <Row
+ left={
+ <View style={{ gap: 4 }}>
+ <H2>{t("disableBuddy")}</H2>
+ <Muted>{t("temporarilyDisablesBuddy")}</Muted>
+ </View>
+ }
+ right={
+ <Switch
+ value={!!state["disable_buddy"]}
+ onValueChange={(value) => handleToggle("disable_buddy", value)}
+ trackColor={{ false: colors.outline, true: colors.primary }}
+ thumbColor={colors.onPrimary}
+ />
+ }
+ />
+ </Card>
+
+ <Card>
+ <H2>{t("contentBlocking")}</H2>
+ <Divider />
+ <Row
+ left={
+ <View style={{ gap: 4 }}>
+ <H2>{t("adultSites")}</H2>
+ <Muted>{t("blockAdultWebsites")}</Muted>
+ </View>
+ }
+ right={
+ <Switch
+ value={!!state["adult_sites"]}
+ onValueChange={(value) => handleToggle("adult_sites", value)}
+ trackColor={{ false: colors.outline, true: colors.primary }}
+ thumbColor={colors.onPrimary}
+ />
+ }
+ />
+ </Card>
+
+ <Card>
+ <H2>{t("familyLink")}</H2>
+ <Divider />
+ <Row
+ left={
+ <View style={{ gap: 4 }}>
+ <H2>{t("antiCircumvention")}</H2>
+ <Muted>{t("preventFamilyLinkBypasses")}</Muted>
+ </View>
+ }
+ right={
+ <Switch
+ value={!!state["family_link_anti_circumvention"]}
+ onValueChange={(value) =>
+ handleToggle("family_link_anti_circumvention", value)
+ }
+ trackColor={{ false: colors.outline, true: colors.primary }}
+ thumbColor={colors.onPrimary}
+ />
+ }
+ />
+ </Card>
+
+ <Card>
+ <H2>{t("communication")}</H2>
+ <Divider />
+ <Row
+ left={<Muted>{t("communicationWithStrangers")}</Muted>}
+ right={
+ <Pressable onPress={openCommunicationOptions}>
+ <Muted>
+ {communication === "block"
+ ? t("blockAllCommunications")
+ : t("scanCommunicationsWithAI")}
+ </Muted>
+ </Pressable>
+ }
+ />
+ <Muted style={{ marginTop: 6 }}>
+ {t("chooseHowBuddyShouldHandleStrangers")}
+ </Muted>
+
+ <SelectDialog
+ visible={communicationModalVisible}
+ title={t("communicationWithStrangersTitle")}
+ options={[
+ {
+ label: t("scanCommunicationsWithAI"),
+ onPress: () => chooseCommunication("scan"),
+ },
+ {
+ label: t("blockAllCommunications"),
+ onPress: () => chooseCommunication("block"),
+ destructive: true,
+ },
+ ]}
+ onClose={() => setCommunicationModalVisible(false)}
+ />
+
+ <ConfirmDialog
+ visible={confirmModalVisible}
+ title={t("blockAllCommunications")}
+ message={t("blockAllCommunicationsConfirm")}
+ confirmLabel={t("okay")}
+ cancelLabel={t("cancel")}
+ onConfirm={confirmBlock}
+ onCancel={cancelConfirm}
+ destructive
+ />
+ </Card>
+
+ <Card>
+ <H2>{t("notifications")}</H2>
+ <Divider />
+ <Row
+ left={<Muted>{t("dangerousMessages")}</Muted>}
+ right={
+ <Switch
+ value={!!state["notify_dangerous_messages"]}
+ onValueChange={(value) =>
+ handleToggle("notify_dangerous_messages", value)
+ }
+ trackColor={{ false: colors.outline, true: colors.primary }}
+ thumbColor={colors.onPrimary}
+ />
+ }
+ />
+ <View style={{ paddingVertical: 12 }}>
+ <Divider />
+ </View>
+ <Row
+ left={<Muted>{t("newContactAdded")}</Muted>}
+ right={
+ <Switch
+ value={!!state["notify_new_contact_added"]}
+ onValueChange={(value) =>
+ handleToggle("notify_new_contact_added", value)
+ }
+ trackColor={{ false: colors.outline, true: colors.primary }}
+ thumbColor={colors.onPrimary}
+ />
+ }
+ />
+ </Card>
+ </Screen>
+ );
+}