diff --git a/plugins/rose-plugs/src/ui/PluginDetailSheet.tsx b/plugins/rose-plugs/src/ui/PluginDetailSheet.tsx index c1672f8..064d83a 100644 --- a/plugins/rose-plugs/src/ui/PluginDetailSheet.tsx +++ b/plugins/rose-plugs/src/ui/PluginDetailSheet.tsx @@ -6,21 +6,28 @@ import { NexusPlugin } from "../lib/nexusApi"; const { ScrollView, Text } = ReactNative; +// Confirmed live: this could get stuck on "Installing..." forever - the timeout below is a safety +// net so a hung installPlugin() call can't leave the button stuck indefinitely. +function withTimeout(promise: Promise, ms: number): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error("Timed out")), ms); + promise.then( + (v) => { clearTimeout(timer); resolve(v); }, + (e) => { clearTimeout(timer); reject(e); }, + ); + }); +} + export default function PluginDetailSheet({ plugin }: { plugin: NexusPlugin }) { const [installing, setInstalling] = React.useState(false); const alreadyInstalled = plugin.installUrl in installedPlugins; const onInstall = React.useCallback(() => { setInstalling(true); - installPlugin(plugin.installUrl) - .then(() => { - setInstalling(false); - showToast(`${plugin.name} installed`); - }) - .catch((e: any) => { - setInstalling(false); - showToast(`Install failed: ${e?.message ?? e}`); - }); + withTimeout(installPlugin(plugin.installUrl), 20000) + .then(() => showToast(`${plugin.name} installed`)) + .catch((e: any) => showToast(`Install failed: ${e?.message ?? e}`)) + .finally(() => setInstalling(false)); }, [plugin.installUrl]); return ( diff --git a/plugins/rose-plugs/src/ui/PluginsBrowser.tsx b/plugins/rose-plugs/src/ui/PluginsBrowser.tsx index 349497c..20adc21 100644 --- a/plugins/rose-plugs/src/ui/PluginsBrowser.tsx +++ b/plugins/rose-plugs/src/ui/PluginsBrowser.tsx @@ -1,6 +1,7 @@ import { React, ReactNative } from "@vendetta/metro/common"; -import { plugins as installedPlugins } from "@vendetta/plugins"; +import { plugins as installedPlugins, installPlugin } from "@vendetta/plugins"; import { showCustomAlert } from "@vendetta/ui/alerts"; +import { showToast } from "@vendetta/ui/toasts"; import SettingsScaffold from "@shared/ui/SettingsScaffold"; import NoteBox from "@shared/ui/NoteBox"; import PrimaryButton from "@shared/ui/PrimaryButton"; @@ -8,7 +9,7 @@ import { TableRow, TableRowGroup } from "@shared/ui/table"; import { fetchNexusPlugins, NexusPlugin } from "../lib/nexusApi"; import PluginDetailSheet from "./PluginDetailSheet"; -const { Text } = ReactNative; +const { Text, View, TouchableOpacity } = ReactNative; interface BrowserState { loading: boolean; @@ -16,8 +17,22 @@ interface BrowserState { plugins: NexusPlugin[]; } +// Confirmed live: installing from the detail sheet could get stuck on "Installing..." forever - +// the promise from installPlugin wasn't reliably settling. This timeout is a safety net so a hung +// call can't leave a row stuck indefinitely, on top of moving the action to an inline button. +function withTimeout(promise: Promise, ms: number): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error("Timed out")), ms); + promise.then( + (v) => { clearTimeout(timer); resolve(v); }, + (e) => { clearTimeout(timer); reject(e); }, + ); + }); +} + export default function PluginsBrowser() { const [state, setState] = React.useState({ loading: true, error: null, plugins: [] }); + const [installingId, setInstallingId] = React.useState(null); const load = React.useCallback(() => { setState((s) => ({ ...s, loading: true, error: null })); @@ -30,6 +45,14 @@ export default function PluginsBrowser() { load(); }, [load]); + const onInstall = React.useCallback((plugin: NexusPlugin) => { + setInstallingId(plugin.id); + withTimeout(installPlugin(plugin.installUrl), 20000) + .then(() => showToast(`${plugin.name} installed`)) + .catch((e: any) => showToast(`Install failed: ${e?.message ?? e}`)) + .finally(() => setInstallingId(null)); + }, []); + if (state.loading) { return ( @@ -60,19 +83,39 @@ export default function PluginsBrowser() { {plugins.map((plugin) => { const installed = plugin.installUrl in installedPlugins; + const installing = installingId === plugin.id; return ( - {plugin.status.toUpperCase()} - - ) : installed ? ( - ✓ - ) : undefined + + {plugin.status && plugin.status !== "default" && ( + + {plugin.status.toUpperCase()} + + )} + {installed ? ( + ✓ + ) : ( + onInstall(plugin)} + style={{ + backgroundColor: "#5865F2", + borderRadius: 6, + paddingHorizontal: 10, + paddingVertical: 5, + opacity: installing ? 0.6 : 1, + }} + > + + {installing ? "Installing…" : "Install"} + + + )} + } onPress={() => showCustomAlert(PluginDetailSheet, { plugin })} />