diff --git a/app.json b/app.json index 2ea1169..e0223b4 100644 --- a/app.json +++ b/app.json @@ -73,6 +73,7 @@ "./plugins/withIOSNetworkSecurity.js", "./plugins/withNetworkSecurityConfig.js", "./plugins/withAndroidLocalNetworkSsl.js", + "./plugins/withTermixWidgets.js", "expo-dev-client", "expo-secure-store", "expo-web-browser" @@ -89,9 +90,7 @@ "projectId": "cf86f530-ca4b-44bf-bb68-4d178b264910" } }, - "assetBundlePatterns": [ - "assets/**/*" - ], + "assetBundlePatterns": ["assets/**/*"], "owner": "termix" } } diff --git a/app/AppContext.tsx b/app/AppContext.tsx index 707b18a..1312240 100644 --- a/app/AppContext.tsx +++ b/app/AppContext.tsx @@ -17,6 +17,7 @@ import { getCurrentServerUrl, } from "./main-axios"; import Constants from "expo-constants"; +import { publishSignedOutSnapshot } from "@/app/widgets"; interface Server { name: string; @@ -183,6 +184,14 @@ export const AppProvider: React.FC = ({ children }) => { }); }, []); + // Losing authentication (sign-out elsewhere, expired token, server change) + // must also empty the home-screen widgets — they would otherwise keep showing + // host names the user can no longer reach. + useEffect(() => { + if (isLoading || isAuthenticated) return; + void publishSignedOutSnapshot(); + }, [isAuthenticated, isLoading]); + const lastValidationTimeRef = useRef(0); const validationInProgressRef = useRef(false); diff --git a/app/_layout.tsx b/app/_layout.tsx index a26d336..4d3f021 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -21,6 +21,7 @@ import { useFonts } from "expo-font"; import { FONT_MAP, MONO_FONT, MONO_FONT_BOLD } from "./constants/fonts"; import "../global.css"; import UpdateRequired from "@/app/authentication/UpdateRequired"; +import { WidgetDeepLinkHost } from "@/app/widgets"; function RootLayoutContent() { const { @@ -71,6 +72,10 @@ function RootLayoutContent() { + {/* Home-screen widget taps: session navigation and the snippet run + sheet. Mounted here so it is active for the whole session, including + cold starts from a widget. */} + {authFlowVisible ? ( diff --git a/app/tabs/hosts/Hosts.tsx b/app/tabs/hosts/Hosts.tsx index bc23c40..a9d5b53 100644 --- a/app/tabs/hosts/Hosts.tsx +++ b/app/tabs/hosts/Hosts.tsx @@ -7,6 +7,7 @@ import { } from "react-native"; import { useState, useCallback, useRef, useMemo, useEffect } from "react"; import { useFocusEffect } from "@react-navigation/native"; +import { useRouter } from "expo-router"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { RefreshCw, @@ -18,6 +19,7 @@ import { Zap, Plus, KeyRound, + FileText, } from "lucide-react-native"; import HostTree from "@/app/tabs/hosts/navigation/Folder"; import type { HostMetrics } from "@/app/tabs/hosts/navigation/Host"; @@ -34,8 +36,10 @@ import { getCurrentServerUrl, deleteSSHHost, createSSHHost, + getSnippets, } from "@/app/main-axios"; -import { SSHHost, ServerStatus } from "@/types"; +import { SSHHost, ServerStatus, Snippet } from "@/types"; +import { publishHostSnapshot } from "@/app/widgets"; import { Screen } from "@/app/components/Screen"; import { Text, @@ -115,6 +119,7 @@ const STORAGE_EXPANDED = "hostExpandedFolders"; export default function Hosts() { const color = useThemeColor(); + const router = useRouter(); const [hosts, setHosts] = useState([]); const [folderColors, setFolderColors] = useState< Record @@ -126,6 +131,10 @@ export default function Hosts() { Record >({}); const [metrics, setMetrics] = useState>({}); + // Snippets are fetched here purely to feed the home-screen widget; the list + // itself lives under Settings. `null` means "not fetched yet", which keeps a + // failed fetch from wiping snippets the Snippets screen already published. + const [snippets, setSnippets] = useState(null); const [sortKey, setSortKey] = useState("default"); const [filterState, setFilterState] = useState(DEFAULT_FILTERS); const [expandedPaths, setExpandedPaths] = useState>(new Set()); @@ -215,10 +224,12 @@ export default function Hosts() { return; } - const [hostsResult, statusesResult] = await Promise.allSettled([ - getSSHHosts(), - getAllServerStatuses(), - ]); + const [hostsResult, statusesResult, snippetsResult] = + await Promise.allSettled([ + getSSHHosts(), + getAllServerStatuses(), + getSnippets(), + ]); if (hostsResult.status !== "fulfilled") throw hostsResult.reason; @@ -248,6 +259,13 @@ export default function Hosts() { setFolderColors(colors); setServerStatuses(statuses); + if ( + snippetsResult.status === "fulfilled" && + Array.isArray(snippetsResult.value) + ) { + setSnippets(snippetsResult.value as Snippet[]); + } + // Best-effort live metrics for online hosts only. void fetchMetrics(hostList, statuses); } catch (error: any) { @@ -276,6 +294,21 @@ export default function Hosts() { }, [fetchData]), ); + // Keep the home-screen widgets in sync with whatever this screen shows. The + // publisher throttles and de-dupes, so re-running on every data change is + // cheap. This screen only renders for an authenticated user. + useEffect(() => { + if (loading) return; + void publishHostSnapshot({ + hosts, + statuses: serverStatuses, + metrics, + snippets: snippets ?? undefined, + serverUrl: getCurrentServerUrl(), + authenticated: true, + }); + }, [loading, hosts, serverStatuses, metrics, snippets]); + // --- Build, sort, and filter the tree. const tree = useMemo( () => buildHostTree(hosts, folderColors), @@ -437,54 +470,35 @@ export default function Hosts() {