import type {
NativeStackHeaderItem,
NativeStackNavigationOptions,
} from "@react-navigation/native-stack";
import { useEffect, useRef, useState, type ReactNode } from "react";
import { ActivityIndicator, Animated, Platform, Pressable, View } from "react-native";
import { SymbolView } from "../../components/AppSymbol";
import { AppText as Text } from "../../components/AppText";
import { brandTitleOffset, CompactBrandTitle } from "../../components/CompactBrandTitle";
import { useThemeColor } from "../../lib/useThemeColor";
import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
import { useWorkspaceState } from "../../state/workspace";
import {
workspaceConnectionStatusPresentation,
type WorkspaceConnectionStatusPresentation,
} from "./workspace-connection-status";
/**
* Delay before a connection interruption surfaces in the title slot. Sub-second
* blips (the common reconnect case) resolve without any UI at all.
*/
const STATUS_SHOW_DELAY_MS = 800;
const FADE_IN_MS = 250;
/**
* Connection status presentation, debounced for display: null until the
* workspace has been in a non-connected state for STATUS_SHOW_DELAY_MS,
* then live-updating until the workspace reconnects (null again immediately).
*/
function useDelayedConnectionStatus(): WorkspaceConnectionStatusPresentation | null {
const { state } = useWorkspaceState();
const presentation = workspaceConnectionStatusPresentation(state);
const hasStatus = presentation !== null;
const [visible, setVisible] = useState(false);
useEffect(() => {
if (!hasStatus) {
setVisible(false);
return;
}
const timer = setTimeout(() => setVisible(true), STATUS_SHOW_DELAY_MS);
return () => clearTimeout(timer);
}, [hasStatus]);
return visible ? presentation : null;
}
/**
* One-shot entrance fade for the status label. Deliberately JS-driven: this can
* mount inside a native header item (RNSScreenStackHeaderSubview), where
* native-driver animated nodes blank the re-hosted view entirely. The JS driver
* updates opacity through the ordinary style path, which those subviews handle.
*/
function StatusFadeIn(props: { readonly children: ReactNode; readonly grow?: boolean }) {
const opacity = useRef(new Animated.Value(0)).current;
useEffect(() => {
const animation = Animated.timing(opacity, {
duration: FADE_IN_MS,
toValue: 1,
useNativeDriver: false,
});
animation.start();
return () => animation.stop();
}, [opacity]);
return (
{props.children}
);
}
/**
* Renders the brand/title slot of a thread-list surface, swapping the brand
* for the workspace connection status while an environment is unavailable.
*
* Both states occupy the same slot, so connection changes never shift the
* layout below. While connected the brand renders untouched — no wrapper —
* keeping the native header item on the exact element tree that predates the
* status swap. Replaces the old WorkspaceConnectionStatus pill, which inserted
* a row above the thread list.
*/
export function WorkspaceConnectionTitle(props: {
/** Content shown while connected (brand lockup or a screen title). */
readonly brand: ReactNode;
/** Opens environment settings. Status is not pressable when omitted. */
readonly onPress?: () => void;
/** Fill the available row width (in-flow headers) instead of hugging content (native title slots). */
readonly grow?: boolean;
readonly size?: "navbar" | "pageTitle";
/** Horizontal correction so the status aligns with the brand in native title slots. */
readonly statusOffset?: number;
}) {
const iconColor = String(useThemeColor("--color-icon-muted"));
const status = useDelayedConnectionStatus();
const size = props.size ?? "navbar";
if (status === null) {
return props.grow ? (
{props.brand}
) : (
<>{props.brand}>
);
}
return (
{status.showsProgress ? (
) : (
)}
{status.label}
);
}
/**
* getCompactBrandHeaderOptions with the brand slot upgraded to the
* connection-status swap. Screens with an environment-settings callback apply
* this over the static brand options at mount.
*/
export function getConnectionAwareBrandHeaderOptions(opts: {
readonly onOpenEnvironments: () => void;
readonly fallbackTitleStyle?: NativeStackNavigationOptions["headerTitleStyle"];
}): NativeStackNavigationOptions {
if (Platform.OS === "ios" && NATIVE_LIQUID_GLASS_SUPPORTED) {
return {
headerTitle: "Threads",
headerTitleStyle: { color: "transparent", fontSize: 18, fontWeight: "800" },
title: "Threads",
unstable_headerLeftItems: (): NativeStackHeaderItem[] => [
{
element: (
}
onPress={opts.onOpenEnvironments}
statusOffset={brandTitleOffset(true)}
/>
),
hidesSharedBackground: true,
type: "custom",
},
],
};
}
return {
headerTitle: () => (
}
onPress={opts.onOpenEnvironments}
statusOffset={brandTitleOffset(false)}
/>
),
headerTitleStyle: opts.fallbackTitleStyle,
title: "Threads",
};
}