Update files
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
// App name and package ID are controlled by Gradle product flavors (dev/production)
|
// App name and package ID are controlled by Gradle product flavors (dev/production)
|
||||||
// See plugins/with-android-product-flavors.js for flavor configuration
|
// See plugins/with-android-product-flavors.js for flavor configuration
|
||||||
|
|
||||||
|
const pkg = require("./package.json");
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
expo: {
|
expo: {
|
||||||
name: "Paseo",
|
name: "Paseo",
|
||||||
slug: "voice-mobile",
|
slug: "voice-mobile",
|
||||||
version: "0.1.0",
|
version: pkg.version,
|
||||||
orientation: "portrait",
|
orientation: "portrait",
|
||||||
icon: "./assets/images/icon.png",
|
icon: "./assets/images/icon.png",
|
||||||
scheme: "paseo",
|
scheme: "paseo",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { router, useLocalSearchParams } from "expo-router";
|
|||||||
import Constants from "expo-constants";
|
import Constants from "expo-constants";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
import { Sun, Moon, Monitor, MoreVertical } from "lucide-react-native";
|
import { Sun, Moon, Monitor, MoreVertical, Globe } from "lucide-react-native";
|
||||||
import { Fonts } from "@/constants/theme";
|
import { Fonts } from "@/constants/theme";
|
||||||
import { useAppSettings, type AppSettings } from "@/hooks/use-settings";
|
import { useAppSettings, type AppSettings } from "@/hooks/use-settings";
|
||||||
import { useDaemonRegistry, type HostProfile } from "@/contexts/daemon-registry-context";
|
import { useDaemonRegistry, type HostProfile } from "@/contexts/daemon-registry-context";
|
||||||
@@ -126,16 +126,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.semibold,
|
||||||
},
|
},
|
||||||
hostUrl: {
|
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
fontSize: theme.fontSize.sm,
|
|
||||||
fontFamily: Fonts.mono,
|
|
||||||
},
|
|
||||||
hostConnections: {
|
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
fontFamily: Fonts.mono,
|
|
||||||
},
|
|
||||||
hostError: {
|
hostError: {
|
||||||
color: theme.colors.palette.red[300],
|
color: theme.colors.palette.red[300],
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
@@ -158,6 +148,24 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.semibold,
|
||||||
},
|
},
|
||||||
|
connectionPill: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
paddingHorizontal: theme.spacing[2],
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: theme.borderRadius.full,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
backgroundColor: theme.colors.surface3,
|
||||||
|
maxWidth: 170,
|
||||||
|
},
|
||||||
|
connectionText: {
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
fontWeight: theme.fontWeight.semibold,
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
flexShrink: 1,
|
||||||
|
},
|
||||||
menuButton: {
|
menuButton: {
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 32,
|
height: 32,
|
||||||
@@ -390,6 +398,8 @@ export default function SettingsScreen() {
|
|||||||
const [addConnectionTargetServerId, setAddConnectionTargetServerId] = useState<string | null>(null);
|
const [addConnectionTargetServerId, setAddConnectionTargetServerId] = useState<string | null>(null);
|
||||||
const [pendingEditReopenServerId, setPendingEditReopenServerId] = useState<string | null>(null);
|
const [pendingEditReopenServerId, setPendingEditReopenServerId] = useState<string | null>(null);
|
||||||
const [pendingNameHost, setPendingNameHost] = useState<{ serverId: string; hostname: string | null } | null>(null);
|
const [pendingNameHost, setPendingNameHost] = useState<{ serverId: string; hostname: string | null } | null>(null);
|
||||||
|
const [pendingRemoveHost, setPendingRemoveHost] = useState<HostProfile | null>(null);
|
||||||
|
const [isRemovingHost, setIsRemovingHost] = useState(false);
|
||||||
const [editingDaemon, setEditingDaemon] = useState<HostProfile | null>(null);
|
const [editingDaemon, setEditingDaemon] = useState<HostProfile | null>(null);
|
||||||
const [editLabel, setEditLabel] = useState("");
|
const [editLabel, setEditLabel] = useState("");
|
||||||
const [isSavingEdit, setIsSavingEdit] = useState(false);
|
const [isSavingEdit, setIsSavingEdit] = useState(false);
|
||||||
@@ -441,6 +451,19 @@ export default function SettingsScreen() {
|
|||||||
setEditLabel("");
|
setEditLabel("");
|
||||||
}, [isSavingEdit]);
|
}, [isSavingEdit]);
|
||||||
|
|
||||||
|
const closeAddConnectionFlow = useCallback(() => {
|
||||||
|
setIsAddHostMethodVisible(false);
|
||||||
|
setIsDirectHostVisible(false);
|
||||||
|
setIsPasteLinkVisible(false);
|
||||||
|
setAddConnectionTargetServerId(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const goBackToAddConnectionMethods = useCallback(() => {
|
||||||
|
setIsDirectHostVisible(false);
|
||||||
|
setIsPasteLinkVisible(false);
|
||||||
|
setIsAddHostMethodVisible(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const editHost = typeof params.editHost === "string" ? params.editHost.trim() : "";
|
const editHost = typeof params.editHost === "string" ? params.editHost.trim() : "";
|
||||||
if (!editHost) return;
|
if (!editHost) return;
|
||||||
@@ -498,45 +521,9 @@ export default function SettingsScreen() {
|
|||||||
[removeConnection]
|
[removeConnection]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRemoveDaemon = useCallback(
|
const handleRemoveDaemon = useCallback((profile: HostProfile) => {
|
||||||
(profile: HostProfile) => {
|
setPendingRemoveHost(profile);
|
||||||
if (Platform.OS === "web") {
|
}, []);
|
||||||
const hasBrowserConfirm =
|
|
||||||
typeof globalThis !== "undefined" &&
|
|
||||||
typeof (globalThis as any).confirm === "function";
|
|
||||||
|
|
||||||
const confirmed = hasBrowserConfirm ? (globalThis as any).confirm(`Remove ${profile.label}?`) : true;
|
|
||||||
if (!confirmed) return;
|
|
||||||
|
|
||||||
void removeHost(profile.serverId).catch((error) => {
|
|
||||||
console.error("[Settings] Failed to remove daemon", error);
|
|
||||||
Alert.alert("Error", "Unable to remove host");
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Alert.alert(
|
|
||||||
"Remove host",
|
|
||||||
`Remove ${profile.label}?`,
|
|
||||||
[
|
|
||||||
{ text: "Cancel", style: "cancel" },
|
|
||||||
{
|
|
||||||
text: "Remove",
|
|
||||||
style: "destructive",
|
|
||||||
onPress: async () => {
|
|
||||||
try {
|
|
||||||
await removeHost(profile.serverId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Settings] Failed to remove daemon", error);
|
|
||||||
Alert.alert("Error", "Unable to remove host");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[removeHost]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleThemeChange = useCallback(
|
const handleThemeChange = useCallback(
|
||||||
(newTheme: AppSettings["theme"]) => {
|
(newTheme: AppSettings["theme"]) => {
|
||||||
@@ -641,13 +628,20 @@ export default function SettingsScreen() {
|
|||||||
|
|
||||||
<AddHostMethodModal
|
<AddHostMethodModal
|
||||||
visible={isAddHostMethodVisible}
|
visible={isAddHostMethodVisible}
|
||||||
onClose={() => setIsAddHostMethodVisible(false)}
|
onClose={closeAddConnectionFlow}
|
||||||
onDirectConnection={() => setIsDirectHostVisible(true)}
|
onDirectConnection={() => {
|
||||||
onPasteLink={() => setIsPasteLinkVisible(true)}
|
setIsAddHostMethodVisible(false);
|
||||||
|
setIsDirectHostVisible(true);
|
||||||
|
}}
|
||||||
|
onPasteLink={() => {
|
||||||
|
setIsAddHostMethodVisible(false);
|
||||||
|
setIsPasteLinkVisible(true);
|
||||||
|
}}
|
||||||
onScanQr={() => {
|
onScanQr={() => {
|
||||||
const target = addConnectionTargetServerId;
|
const target = addConnectionTargetServerId;
|
||||||
const source = target ? "editHost" : "settings";
|
const source = target ? "editHost" : "settings";
|
||||||
const qs = target ? `?source=${source}&targetServerId=${encodeURIComponent(target)}` : `?source=${source}`;
|
const qs = target ? `?source=${source}&targetServerId=${encodeURIComponent(target)}` : `?source=${source}`;
|
||||||
|
closeAddConnectionFlow();
|
||||||
router.push(`/pair-scan${qs}`);
|
router.push(`/pair-scan${qs}`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -655,7 +649,8 @@ export default function SettingsScreen() {
|
|||||||
<AddHostModal
|
<AddHostModal
|
||||||
visible={isDirectHostVisible}
|
visible={isDirectHostVisible}
|
||||||
targetServerId={addConnectionTargetServerId ?? undefined}
|
targetServerId={addConnectionTargetServerId ?? undefined}
|
||||||
onClose={() => setIsDirectHostVisible(false)}
|
onClose={closeAddConnectionFlow}
|
||||||
|
onCancel={goBackToAddConnectionMethods}
|
||||||
onSaved={({ serverId, hostname, isNewHost }) => {
|
onSaved={({ serverId, hostname, isNewHost }) => {
|
||||||
if (isNewHost) {
|
if (isNewHost) {
|
||||||
setPendingNameHost({ serverId, hostname });
|
setPendingNameHost({ serverId, hostname });
|
||||||
@@ -666,7 +661,8 @@ export default function SettingsScreen() {
|
|||||||
<PairLinkModal
|
<PairLinkModal
|
||||||
visible={isPasteLinkVisible}
|
visible={isPasteLinkVisible}
|
||||||
targetServerId={addConnectionTargetServerId ?? undefined}
|
targetServerId={addConnectionTargetServerId ?? undefined}
|
||||||
onClose={() => setIsPasteLinkVisible(false)}
|
onClose={closeAddConnectionFlow}
|
||||||
|
onCancel={goBackToAddConnectionMethods}
|
||||||
onSaved={({ serverId, hostname, isNewHost }) => {
|
onSaved={({ serverId, hostname, isNewHost }) => {
|
||||||
if (isNewHost) {
|
if (isNewHost) {
|
||||||
setPendingNameHost({ serverId, hostname });
|
setPendingNameHost({ serverId, hostname });
|
||||||
@@ -688,6 +684,51 @@ export default function SettingsScreen() {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{pendingRemoveHost ? (
|
||||||
|
<AdaptiveModalSheet
|
||||||
|
title="Remove host"
|
||||||
|
visible
|
||||||
|
onClose={() => {
|
||||||
|
if (isRemovingHost) return;
|
||||||
|
setPendingRemoveHost(null);
|
||||||
|
}}
|
||||||
|
testID="remove-host-confirm-modal"
|
||||||
|
>
|
||||||
|
<Text style={{ color: theme.colors.foregroundMuted, fontSize: 14 }}>
|
||||||
|
Remove {pendingRemoveHost.label}? This will delete its saved connections.
|
||||||
|
</Text>
|
||||||
|
<View style={[styles.formActionsRow, { marginTop: theme.spacing[4] }]}>
|
||||||
|
<Pressable
|
||||||
|
style={[styles.formButton, isRemovingHost && styles.disabled]}
|
||||||
|
onPress={() => setPendingRemoveHost(null)}
|
||||||
|
disabled={isRemovingHost}
|
||||||
|
>
|
||||||
|
<Text style={styles.formButtonText}>Cancel</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
style={[styles.formButton, isRemovingHost && styles.disabled, { backgroundColor: theme.colors.destructive }]}
|
||||||
|
onPress={() => {
|
||||||
|
const serverId = pendingRemoveHost.serverId;
|
||||||
|
setIsRemovingHost(true);
|
||||||
|
void removeHost(serverId)
|
||||||
|
.then(() => setPendingRemoveHost(null))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("[Settings] Failed to remove host", error);
|
||||||
|
Alert.alert("Error", "Unable to remove host");
|
||||||
|
})
|
||||||
|
.finally(() => setIsRemovingHost(false));
|
||||||
|
}}
|
||||||
|
disabled={isRemovingHost}
|
||||||
|
testID="remove-host-confirm"
|
||||||
|
>
|
||||||
|
<Text style={[styles.formButtonText, { color: theme.colors.palette.white }]}>
|
||||||
|
Remove
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</AdaptiveModalSheet>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<AdaptiveModalSheet
|
<AdaptiveModalSheet
|
||||||
title="Edit host"
|
title="Edit host"
|
||||||
visible={Boolean(editingDaemon)}
|
visible={Boolean(editingDaemon)}
|
||||||
@@ -995,13 +1036,15 @@ function DaemonCard({
|
|||||||
: statusTone === "error"
|
: statusTone === "error"
|
||||||
? "rgba(248, 113, 113, 0.1)"
|
? "rgba(248, 113, 113, 0.1)"
|
||||||
: "rgba(161, 161, 170, 0.1)";
|
: "rgba(161, 161, 170, 0.1)";
|
||||||
|
const connectionBadge = (() => {
|
||||||
const connectionsSummary = (() => {
|
if (!activeConnection) return null;
|
||||||
const parts = daemon.connections.map((conn) => {
|
if (activeConnection.type === "relay") {
|
||||||
if (conn.type === "relay") return `relay:${conn.relayEndpoint}`;
|
return { icon: <Globe size={12} color={theme.colors.foregroundMuted} />, text: "Relay" };
|
||||||
return `direct:${conn.endpoint}`;
|
}
|
||||||
});
|
return {
|
||||||
return parts.join(" • ");
|
icon: <Monitor size={12} color={theme.colors.foregroundMuted} />,
|
||||||
|
text: activeConnection.display,
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1015,6 +1058,15 @@ function DaemonCard({
|
|||||||
<Text style={[styles.statusText, { color: statusColor }]}>{badgeText}</Text>
|
<Text style={[styles.statusText, { color: statusColor }]}>{badgeText}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{connectionBadge ? (
|
||||||
|
<View style={styles.connectionPill}>
|
||||||
|
{connectionBadge.icon}
|
||||||
|
<Text style={styles.connectionText} numberOfLines={1}>
|
||||||
|
{connectionBadge.text}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
testID={`daemon-menu-trigger-${daemon.serverId}`}
|
testID={`daemon-menu-trigger-${daemon.serverId}`}
|
||||||
@@ -1050,32 +1102,6 @@ function DaemonCard({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.hostUrl}>
|
|
||||||
{(() => {
|
|
||||||
if (connectionStatus === "online") {
|
|
||||||
if (activeConnection?.type === "relay") return "Connected via relay";
|
|
||||||
if (activeConnection?.type === "direct") return `Connected via ${activeConnection.display}`;
|
|
||||||
return "Connected";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectionStatus === "connecting" && activeConnection) {
|
|
||||||
if (activeConnection.type === "relay") return "Trying relay";
|
|
||||||
return `Trying ${activeConnection.display}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((connectionStatus === "offline" || connectionStatus === "error") && activeConnection) {
|
|
||||||
if (activeConnection.type === "relay") return "Last tried: relay";
|
|
||||||
return `Last tried: ${activeConnection.display}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relay = daemon.connections.find((c) => c.type === "relay");
|
|
||||||
const direct = daemon.connections.find((c) => c.type === "direct");
|
|
||||||
if (direct) return direct.endpoint;
|
|
||||||
if (relay) return "Relay";
|
|
||||||
return "";
|
|
||||||
})()}
|
|
||||||
</Text>
|
|
||||||
{connectionsSummary ? <Text style={styles.hostConnections}>{connectionsSummary}</Text> : null}
|
|
||||||
{connectionError ? <Text style={styles.hostError}>{connectionError}</Text> : null}
|
{connectionError ? <Text style={styles.hostError}>{connectionError}</Text> : null}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -48,19 +48,16 @@ export function AddHostMethodModal({
|
|||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
|
|
||||||
const handleDirect = useCallback(() => {
|
const handleDirect = useCallback(() => {
|
||||||
onClose();
|
|
||||||
onDirectConnection();
|
onDirectConnection();
|
||||||
}, [onClose, onDirectConnection]);
|
}, [onDirectConnection]);
|
||||||
|
|
||||||
const handleScan = useCallback(() => {
|
const handleScan = useCallback(() => {
|
||||||
onClose();
|
|
||||||
onScanQr();
|
onScanQr();
|
||||||
}, [onClose, onScanQr]);
|
}, [onScanQr]);
|
||||||
|
|
||||||
const handlePaste = useCallback(() => {
|
const handlePaste = useCallback(() => {
|
||||||
onClose();
|
|
||||||
onPasteLink();
|
onPasteLink();
|
||||||
}, [onClose, onPasteLink]);
|
}, [onPasteLink]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdaptiveModalSheet title="Add connection" visible={visible} onClose={onClose} testID="add-host-method-modal">
|
<AdaptiveModalSheet title="Add connection" visible={visible} onClose={onClose} testID="add-host-method-modal">
|
||||||
|
|||||||
@@ -147,10 +147,11 @@ export interface AddHostModalProps {
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
targetServerId?: string;
|
targetServerId?: string;
|
||||||
|
onCancel?: () => void;
|
||||||
onSaved?: (result: { profile: HostProfile; serverId: string; hostname: string | null; isNewHost: boolean }) => void;
|
onSaved?: (result: { profile: HostProfile; serverId: string; hostname: string | null; isNewHost: boolean }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AddHostModal({ visible, onClose, onSaved, targetServerId }: AddHostModalProps) {
|
export function AddHostModal({ visible, onClose, onCancel, onSaved, targetServerId }: AddHostModalProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const { daemons, upsertDirectConnection } = useDaemonRegistry();
|
const { daemons, upsertDirectConnection } = useDaemonRegistry();
|
||||||
const isMobile =
|
const isMobile =
|
||||||
@@ -170,6 +171,13 @@ export function AddHostModal({ visible, onClose, onSaved, targetServerId }: AddH
|
|||||||
onClose();
|
onClose();
|
||||||
}, [isSaving, onClose]);
|
}, [isSaving, onClose]);
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
if (isSaving) return;
|
||||||
|
setEndpointRaw("");
|
||||||
|
setErrorMessage("");
|
||||||
|
(onCancel ?? onClose)();
|
||||||
|
}, [isSaving, onCancel, onClose]);
|
||||||
|
|
||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
if (isSaving) return;
|
if (isSaving) return;
|
||||||
|
|
||||||
@@ -256,8 +264,8 @@ export function AddHostModal({ visible, onClose, onSaved, targetServerId }: AddH
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
<Pressable style={styles.button} onPress={handleClose} disabled={isSaving}>
|
<Pressable style={styles.button} onPress={handleCancel} disabled={isSaving}>
|
||||||
<Text style={styles.buttonText}>Cancel</Text>
|
<Text style={styles.buttonText}>{onCancel ? "Back" : "Cancel"}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[styles.button, styles.primaryButton, isSaving ? { opacity: 0.7 } : null]}
|
style={[styles.button, styles.primaryButton, isSaving ? { opacity: 0.7 } : null]}
|
||||||
|
|||||||
@@ -69,10 +69,11 @@ export interface PairLinkModalProps {
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
targetServerId?: string;
|
targetServerId?: string;
|
||||||
|
onCancel?: () => void;
|
||||||
onSaved?: (result: { profile: HostProfile; serverId: string; hostname: string | null; isNewHost: boolean }) => void;
|
onSaved?: (result: { profile: HostProfile; serverId: string; hostname: string | null; isNewHost: boolean }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PairLinkModal({ visible, onClose, onSaved, targetServerId }: PairLinkModalProps) {
|
export function PairLinkModal({ visible, onClose, onCancel, onSaved, targetServerId }: PairLinkModalProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const { daemons, upsertDaemonFromOfferUrl } = useDaemonRegistry();
|
const { daemons, upsertDaemonFromOfferUrl } = useDaemonRegistry();
|
||||||
const isMobile =
|
const isMobile =
|
||||||
@@ -90,6 +91,13 @@ export function PairLinkModal({ visible, onClose, onSaved, targetServerId }: Pai
|
|||||||
onClose();
|
onClose();
|
||||||
}, [isSaving, onClose]);
|
}, [isSaving, onClose]);
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
if (isSaving) return;
|
||||||
|
setOfferUrl("");
|
||||||
|
setErrorMessage("");
|
||||||
|
(onCancel ?? onClose)();
|
||||||
|
}, [isSaving, onCancel, onClose]);
|
||||||
|
|
||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
if (isSaving) return;
|
if (isSaving) return;
|
||||||
const raw = offerUrl.trim();
|
const raw = offerUrl.trim();
|
||||||
@@ -178,14 +186,14 @@ export function PairLinkModal({ visible, onClose, onSaved, targetServerId }: Pai
|
|||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={handleClose}
|
onPress={handleCancel}
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
testID="pair-link-cancel"
|
testID="pair-link-cancel"
|
||||||
accessible
|
accessible
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Cancel"
|
accessibilityLabel="Cancel"
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>Cancel</Text>
|
<Text style={styles.buttonText}>{onCancel ? "Back" : "Cancel"}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[styles.button, styles.primaryButton, isSaving ? { opacity: 0.7 } : null]}
|
style={[styles.button, styles.primaryButton, isSaving ? { opacity: 0.7 } : null]}
|
||||||
|
|||||||
Reference in New Issue
Block a user