700 lines · 24.5 KB · TypeScript
RawDownload
import { useAuth, useUser } from "@clerk/expo";import { useAtomSet, useAtomValue } from "@effect/atom-react";import Constants from "expo-constants";import * as Notifications from "expo-notifications";import { useNavigation } from "@react-navigation/native";import { NativeStackScreenOptions } from "../../native/StackHeader";import { SymbolView } from "../../components/AppSymbol";import * as Effect from "effect/Effect";import { AsyncResult } from "effect/unstable/reactivity";import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";import { Alert, Linking, Platform, Pressable, ScrollView, View } from "react-native";import { useSafeAreaInsets } from "react-native-safe-area-context"; import {  isAtomCommandInterrupted,  reportAtomCommandResult,  settleAsyncResult,  settlePromise,  squashAtomCommandFailure,} from "@t3tools/client-runtime/state/runtime";import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";import { AppText as Text } from "../../components/AppText";import { supportsAgentAwarenessPush } from "../agent-awareness/capabilities";import { setLiveActivityUpdatesEnabled } from "../agent-awareness/liveActivityPreferences";import { requestAgentNotificationPermission } from "../agent-awareness/notificationPermissions";import {  getAgentAwarenessRegistrationStatus,  refreshAgentAwarenessRegistration,  subscribeAgentAwarenessRegistrationStatus,} from "../agent-awareness/remoteRegistration";import { refreshManagedRelayEnvironments } from "../cloud/managedRelayState";import { hasCloudPublicConfig, resolveRelayClerkTokenOptions } from "../cloud/publicConfig";import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items";import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar";import { runtime } from "../../lib/runtime";import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";import {  type AppUpdateCheckState,  isAppUpdateCheckAvailable,  registerHiddenUpdateTap,  runAppUpdateCheck,} from "../updates/app-updates";import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";import { SettingsRow } from "./components/SettingsRow";import { SettingsSection } from "./components/SettingsSection";import { SettingsSwitchRow } from "./components/SettingsSwitchRow";import { resolveAgentAwarenessPlatformPresentation } from "./SettingsRouteScreen.logic"; type NotificationStatus = "checking" | "enabled" | "disabled" | "unsupported";type LiveActivityStatus = "checking" | "enabled" | "disabled" | "signed-out" | "linking"; // Reflects whether the relay actually accepted this device's registration.// The notification and Live Activity switches are gated on this so they can// never read as enabled when the device cannot receive anything (e.g. the// registration request timed out).function useDeviceRegistered(): boolean {  const status = useSyncExternalStore(    subscribeAgentAwarenessRegistrationStatus,    getAgentAwarenessRegistrationStatus,    () => "unknown" as const,  );  return status === "registered";} export function SettingsRouteScreen() {  const navigation = useNavigation();   return (    <>      <WorkspaceSidebarToolbar />      {Platform.OS === "android" ? (        <>          {/* Android renders its own in-screen header instead of the native bar. */}          <NativeStackScreenOptions options={{ headerShown: false }} />          <AndroidScreenHeader title="Settings" onBack={() => navigation.goBack()} />        </>      ) : (        <NativeStackScreenOptions          options={{            unstable_headerRightItems:              Platform.OS === "ios"                ? () => [                    withNativeGlassHeaderItem({                      accessibilityLabel: "Close settings",                      icon: { name: "xmark", type: "sfSymbol" } as const,                      identifier: "settings-close",                      label: "",                      onPress: () => navigation.goBack(),                      type: "button",                    }),                  ]                : undefined,          }}        />      )}      {hasCloudPublicConfig() ? <ConfiguredSettingsRouteScreen /> : <LocalSettingsRouteScreen />}    </>  );} function LocalSettingsRouteScreen() {  const insets = useSafeAreaInsets();  const { savedConnectionsById } = useSavedRemoteConnections();  const environmentCount = Object.keys(savedConnectionsById).length;   return (    <View collapsable={false} className="flex-1 bg-sheet">      <ScrollView        contentInsetAdjustmentBehavior="automatic"        showsVerticalScrollIndicator={false}        className="flex-1"        contentContainerClassName="gap-6 px-5 pt-4"        contentContainerStyle={{          paddingBottom: Math.max(insets.bottom, 18) + 18,        }}      >        <SettingsSection title="Configuration">          <SettingsRow            icon="desktopcomputer"            label="Environments"            value={`${environmentCount}`}            target="SettingsEnvironments"          />        </SettingsSection>         <GeneralSettingsSection />         <SettingsSection title="Appearance">          <SettingsRow icon="paintbrush" label="Appearance" target="SettingsAppearance" />        </SettingsSection>         <LegacySettingsSection />         <ArchivedThreadsSettingsSection />         <AppSettingsSection />      </ScrollView>    </View>  );} function ConfiguredSettingsRouteScreen() {  const preferencesResult = useAtomValue(mobilePreferencesAtom);  const savePreferences = useAtomSet(updateMobilePreferencesAtom);  const agentAwarenessPushAvailable = supportsAgentAwarenessPush();  const agentAwarenessPlatform = resolveAgentAwarenessPlatformPresentation(Platform.OS);  const insets = useSafeAreaInsets();  const navigation = useNavigation();  const { getToken, isLoaded, isSignedIn } = useAuth({ treatPendingAsSignedOut: false });  const { user } = useUser();  const { savedConnectionsById } = useSavedRemoteConnections();  const [notificationStatus, setNotificationStatus] = useState<NotificationStatus>("checking");  const [liveActivityStatus, setLiveActivityStatus] = useState<LiveActivityStatus>("checking");  const deviceRegistered = useDeviceRegistered();  const liveActivitiesPreferenceEnabled = AsyncResult.isSuccess(preferencesResult)    ? preferencesResult.value.liveActivitiesEnabled !== false    : true;   const connections = useMemo(() => Object.values(savedConnectionsById), [savedConnectionsById]);  const environmentCount = connections.length;  const accountLabel = useMemo(() => {    if (!isLoaded) return "Checking";    if (!isSignedIn) return "Sign in";    return user?.primaryEmailAddress?.emailAddress ?? "Signed in";  }, [isLoaded, isSignedIn, user?.primaryEmailAddress?.emailAddress]);   const refreshNotifications = useCallback(async () => {    if (process.env.EXPO_OS !== "ios") {      setNotificationStatus("unsupported");      return;    }    const result = await settlePromise(() => Notifications.getPermissionsAsync());    if (result._tag === "Failure") {      reportAtomCommandResult(result, { label: "notification permission refresh" });      setNotificationStatus("disabled");      return;    }    setNotificationStatus(result.value.granted ? "enabled" : "disabled");  }, []);   useEffect(() => {    void refreshNotifications();  }, [refreshNotifications]);   useEffect(() => {    if (!isLoaded) {      setLiveActivityStatus("checking");      return;    }    if (!isSignedIn) {      setLiveActivityStatus("signed-out");      return;    }    if (!AsyncResult.isSuccess(preferencesResult)) {      if (AsyncResult.isFailure(preferencesResult)) {        reportAtomCommandResult(preferencesResult, { label: "live activity preference load" });        setLiveActivityStatus("enabled");      } else {        setLiveActivityStatus("checking");      }      return;    }    setLiveActivityStatus(      preferencesResult.value.liveActivitiesEnabled === false ? "disabled" : "enabled",    );  }, [isLoaded, isSignedIn, preferencesResult]);   const requestNotifications = useCallback(async () => {    const result = await settleAsyncResult(() =>      runtime.runPromiseExit(        requestAgentNotificationPermission.pipe(          Effect.tap((permission) =>            permission.type === "granted" ? refreshAgentAwarenessRegistration() : Effect.void,          ),        ),      ),    );    if (result._tag === "Failure") {      if (!isAtomCommandInterrupted(result)) {        const error = squashAtomCommandFailure(result);        Alert.alert(          "Notifications unavailable",          error instanceof Error ? error.message : "Could not request notification permission.",        );      }      return;    }    if (result.value.type === "granted") {      setNotificationStatus("enabled");      // Permission alone is not enough: the switch stays off until the relay      // registration succeeds, so tell the user the truth about which happened.      if (getAgentAwarenessRegistrationStatus() === "registered") {        Alert.alert(          "Notifications enabled",          "Live Activity notifications are enabled for this device.",        );      } else {        Alert.alert(          "Couldn't finish enabling notifications",          "Notification access was granted, but this device could not be registered with T3 Connect. Notifications will start once registration succeeds.",        );      }      return;    }    if (result.value.type === "unsupported") {      setNotificationStatus("unsupported");      Alert.alert(        "Notifications unavailable",        "Live Activity notifications are only available on iOS.",      );      return;    }    setNotificationStatus("disabled");    if (result.value.canAskAgain) {      Alert.alert("Notifications disabled", "Notifications were not enabled.");      return;    }    Alert.alert(      "Notifications disabled",      "Notifications were denied for this app. Open Settings to enable them.",      [        { text: "Cancel", style: "cancel" },        { text: "Open Settings", onPress: () => void Linking.openSettings() },      ],    );  }, []);   const promptSignIn = useCallback(() => {    Alert.alert(      "Sign in to T3 Connect",      "Live Activity updates require T3 Connect so relay can deliver updates to this device.",      [        { text: "Cancel", style: "cancel" },        {          text: "Continue",          onPress: () => navigation.navigate("SettingsSheet", { screen: "SettingsAuth" }),        },      ],    );  }, [navigation]);   const linkEnvironments = useCallback(async () => {    if (!isSignedIn) {      promptSignIn();      return;    }     setLiveActivityStatus("linking");    const tokenResult = await settlePromise(() => getToken(resolveRelayClerkTokenOptions()));    if (tokenResult._tag === "Failure") {      setLiveActivityStatus("disabled");      const error = squashAtomCommandFailure(tokenResult);      Alert.alert(        "Live Activities unavailable",        error instanceof Error ? error.message : "Could not enable Live Activity updates.",      );      return;    }    if (!tokenResult.value) {      promptSignIn();      setLiveActivityStatus("signed-out");      return;    }     const updateResult = await settleAsyncResult(() =>      runtime.runPromiseExit(        setLiveActivityUpdatesEnabled({          enabled: true,          previousEnabled: liveActivitiesPreferenceEnabled,          clerkToken: tokenResult.value,          connections,        }),      ),    );    if (updateResult._tag === "Failure") {      setLiveActivityStatus("disabled");      if (!isAtomCommandInterrupted(updateResult)) {        const error = squashAtomCommandFailure(updateResult);        Alert.alert(          "Live Activities unavailable",          error instanceof Error ? error.message : "Could not enable Live Activity updates.",        );      }      return;    }     savePreferences({ liveActivitiesEnabled: true });    refreshManagedRelayEnvironments();    setLiveActivityStatus("enabled");    // The environment link can succeed while this device's own registration    // (the push-to-start token the relay needs) has not — don't claim Live    // Activities are live until the device is actually registered.    if (getAgentAwarenessRegistrationStatus() === "registered") {      Alert.alert(        "Live Activities enabled",        environmentCount > 0          ? `${environmentCount} environment${environmentCount === 1 ? "" : "s"} linked for Live Activity updates.`          : "Live Activity updates are enabled. Add an environment to start receiving updates.",      );    } else {      Alert.alert(        "Couldn't finish enabling Live Activities",        "This device could not be registered with T3 Connect, so Live Activities won't appear yet. They'll start once registration succeeds.",      );    }  }, [    connections,    environmentCount,    getToken,    isSignedIn,    liveActivitiesPreferenceEnabled,    promptSignIn,    savePreferences,  ]);   const handleDeviceNotificationsChange = useCallback(    (enabled: boolean) => {      if (enabled) {        void requestNotifications();        return;      }       Alert.alert(        "Disable notifications",        "Notification permission is controlled by iOS. Open Settings to disable notifications for T3 Code.",        [          { text: "Cancel", style: "cancel" },          { text: "Open Settings", onPress: () => void Linking.openSettings() },        ],      );    },    [requestNotifications],  );   const handleLiveActivitiesChange = useCallback(    (enabled: boolean) => {      if (!enabled) {        setLiveActivityStatus("disabled");        void (async () => {          let token: string | null = null;          if (isSignedIn) {            const tokenResult = await settlePromise(() =>              getToken(resolveRelayClerkTokenOptions()),            );            if (tokenResult._tag === "Failure") {              reportAtomCommandResult(tokenResult, {                label: "live activity disable token lookup",              });              return;            }            token = tokenResult.value;          }           const updateResult = await settleAsyncResult(() =>            runtime.runPromiseExit(              setLiveActivityUpdatesEnabled({                enabled: false,                previousEnabled: liveActivitiesPreferenceEnabled,                clerkToken: token,                connections,              }),            ),          );          if (updateResult._tag === "Failure") {            setLiveActivityStatus("enabled");            reportAtomCommandResult(updateResult, {              label: "live activity disable",            });            return;          }          savePreferences({ liveActivitiesEnabled: false });          refreshManagedRelayEnvironments();        })();        return;      }       if (!isSignedIn) {        promptSignIn();        return;      }       void linkEnvironments();    },    [      connections,      getToken,      isSignedIn,      linkEnvironments,      liveActivitiesPreferenceEnabled,      promptSignIn,      savePreferences,    ],  );   const openAccount = useCallback(() => {    if (!isLoaded) return;    navigation.navigate("SettingsSheet", { screen: "SettingsAuth" });  }, [isLoaded, navigation]);   return (    <View collapsable={false} className="flex-1 bg-sheet">      <ScrollView        contentInsetAdjustmentBehavior="automatic"        showsVerticalScrollIndicator={false}        className="flex-1"        contentContainerClassName="gap-6 px-5 pt-4"        contentContainerStyle={{          paddingBottom: Math.max(insets.bottom, 18) + 18,        }}      >        <View className="gap-3">          <SettingsSection title="Account">            <SettingsRow              icon="person.crop.circle"              label="T3 Account"              value={accountLabel}              onPress={openAccount}            />          </SettingsSection>          <Text className="px-2 text-sm text-foreground-muted">            T3 Code works locally without signing in. Cloud features are optional.          </Text>        </View>         <SettingsSection title="Configuration">          <SettingsRow            icon="desktopcomputer"            label="Environments"            value={`${environmentCount}`}            target="SettingsEnvironments"          />          <SettingsSwitchRow            icon="bell.badge"            label="Device Notifications"            disabled={              !agentAwarenessPlatform.supported ||              !agentAwarenessPushAvailable ||              notificationStatus === "checking" ||              notificationStatus === "unsupported"            }            subtitle={agentAwarenessPlatform.subtitle}            // Only reads as on when this device is actually registered with the            // relay; otherwise notifications cannot be delivered regardless of            // the local iOS permission.            value={              agentAwarenessPushAvailable && notificationStatus === "enabled" && deviceRegistered            }            onValueChange={handleDeviceNotificationsChange}          />          <SettingsSwitchRow            disabled={              !agentAwarenessPlatform.supported ||              !agentAwarenessPushAvailable ||              !isLoaded ||              liveActivityStatus === "checking" ||              liveActivityStatus === "linking"            }            icon="bolt.circle"            label="Live Activity Updates"

Lines 1–500 of 700.