1import { offersLinkedEnvironments } from "@t3tools/client-runtime/link-request";
2import { peerLinkHealth } from "@t3tools/client-runtime/state/peerLinks";
3import { useFocusEffect } from "@react-navigation/native";
4import { AuthAccessReadScope, type EnvironmentId } from "@t3tools/contracts";
5import * as DateTime from "effect/DateTime";
6import { useCallback, useState } from "react";
7import { AppState, View } from "react-native";
8
9import { AppText as Text } from "../../components/ui/AppText";
10import { useEnvironmentServerConfig } from "../../state/entities";
11import { peerLinkEnvironment } from "../../state/peerLinks";
12import { useEnvironmentQuery } from "../../state/query";
13import { useEnvironmentScope } from "../../state/session";
14import { SettingsSection } from "../settings/components/SettingsSection";
15
16export function LinkedEnvironmentsSection(props: {
17 readonly environmentId: EnvironmentId;
18 readonly environmentLabel: string;
19}) {
20 const config = useEnvironmentServerConfig(props.environmentId);
21 const canRead = useEnvironmentScope(props.environmentId, AuthAccessReadScope);
22 const supported = canRead && offersLinkedEnvironments(config);
23 const links = useEnvironmentQuery(
24 supported ? peerLinkEnvironment.list({ environmentId: props.environmentId, input: {} }) : null,
25 );
26 const [nowMs, setNowMs] = useState(Date.now);
27 const hasLinks = supported && (links.data?.links.length ?? 0) > 0;
28 useFocusEffect(
29 useCallback(() => {
30 if (!hasLinks) return;
31 const updateNow = () => setNowMs(Date.now());
32 updateNow();
33 const timer = setInterval(updateNow, 60_000);
34 const subscription = AppState.addEventListener("change", (state) => {
35 if (state === "active") updateNow();
36 });
37 return () => {
38 clearInterval(timer);
39 subscription.remove();
40 };
41 }, [hasLinks]),
42 );
43 if (!supported || links.error || !links.data || links.data.links.length === 0) return null;
44 const now = DateTime.makeUnsafe(nowMs);
45 return (
46 <View className="mt-5 gap-3">
47 <SettingsSection title={`Linked from ${props.environmentLabel}`}>
48 {links.data.links.map((link) => {
49 const health = peerLinkHealth(link, now);
50 const status =
51 health.kind === "expired"
52 ? "Expired. Link it again with a new pairing code."
53 : health.kind === "unreachable"
54 ? (health.detail ?? "Not answering right now.")
55 : health.kind === "failing"
56 ? health.detail
57 : health.expiresInDays !== null
58 ? `Expires in ${health.expiresInDays} ${health.expiresInDays === 1 ? "day" : "days"}`
59 : "Reachable";
60 return (
61 <View key={link.environmentId} className="gap-0.5 p-4">
62 <Text className="text-base font-t3-bold text-foreground">{link.label}</Text>
63 <Text
64 className={
65 health.kind === "reachable" && health.expiresInDays === null
66 ? "text-xs text-foreground-muted"
67 : "text-xs text-danger-foreground"
68 }
69 numberOfLines={2}
70 >
71 {status}
72 </Text>
73 </View>
74 );
75 })}
76 </SettingsSection>
77 </View>
78 );
79}