Update files

This commit is contained in:
Mohamed Boudra
2026-02-05 12:17:50 +07:00
parent ae27a14e3b
commit 45869e699f
5 changed files with 142 additions and 101 deletions

View File

@@ -1,11 +1,13 @@
// App name and package ID are controlled by Gradle product flavors (dev/production)
// See plugins/with-android-product-flavors.js for flavor configuration
const pkg = require("./package.json");
export default {
expo: {
name: "Paseo",
slug: "voice-mobile",
version: "0.1.0",
version: pkg.version,
orientation: "portrait",
icon: "./assets/images/icon.png",
scheme: "paseo",

View File

@@ -13,7 +13,7 @@ import { router, useLocalSearchParams } from "expo-router";
import Constants from "expo-constants";
import { useSafeAreaInsets } from "react-native-safe-area-context";
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 { useAppSettings, type AppSettings } from "@/hooks/use-settings";
import { useDaemonRegistry, type HostProfile } from "@/contexts/daemon-registry-context";
@@ -126,16 +126,6 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.base,
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: {
color: theme.colors.palette.red[300],
fontSize: theme.fontSize.xs,
@@ -158,6 +148,24 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs,
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: {
width: 36,
height: 32,
@@ -390,6 +398,8 @@ export default function SettingsScreen() {
const [addConnectionTargetServerId, setAddConnectionTargetServerId] = useState<string | null>(null);
const [pendingEditReopenServerId, setPendingEditReopenServerId] = useState<string | 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 [editLabel, setEditLabel] = useState("");
const [isSavingEdit, setIsSavingEdit] = useState(false);
@@ -441,6 +451,19 @@ export default function SettingsScreen() {
setEditLabel("");
}, [isSavingEdit]);
const closeAddConnectionFlow = useCallback(() => {
setIsAddHostMethodVisible(false);
setIsDirectHostVisible(false);
setIsPasteLinkVisible(false);
setAddConnectionTargetServerId(null);
}, []);
const goBackToAddConnectionMethods = useCallback(() => {
setIsDirectHostVisible(false);
setIsPasteLinkVisible(false);
setIsAddHostMethodVisible(true);
}, []);
useEffect(() => {
const editHost = typeof params.editHost === "string" ? params.editHost.trim() : "";
if (!editHost) return;
@@ -498,45 +521,9 @@ export default function SettingsScreen() {
[removeConnection]
);
const handleRemoveDaemon = useCallback(
(profile: HostProfile) => {
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 handleRemoveDaemon = useCallback((profile: HostProfile) => {
setPendingRemoveHost(profile);
}, []);
const handleThemeChange = useCallback(
(newTheme: AppSettings["theme"]) => {
@@ -641,13 +628,20 @@ export default function SettingsScreen() {
<AddHostMethodModal
visible={isAddHostMethodVisible}
onClose={() => setIsAddHostMethodVisible(false)}
onDirectConnection={() => setIsDirectHostVisible(true)}
onPasteLink={() => setIsPasteLinkVisible(true)}
onClose={closeAddConnectionFlow}
onDirectConnection={() => {
setIsAddHostMethodVisible(false);
setIsDirectHostVisible(true);
}}
onPasteLink={() => {
setIsAddHostMethodVisible(false);
setIsPasteLinkVisible(true);
}}
onScanQr={() => {
const target = addConnectionTargetServerId;
const source = target ? "editHost" : "settings";
const qs = target ? `?source=${source}&targetServerId=${encodeURIComponent(target)}` : `?source=${source}`;
closeAddConnectionFlow();
router.push(`/pair-scan${qs}`);
}}
/>
@@ -655,7 +649,8 @@ export default function SettingsScreen() {
<AddHostModal
visible={isDirectHostVisible}
targetServerId={addConnectionTargetServerId ?? undefined}
onClose={() => setIsDirectHostVisible(false)}
onClose={closeAddConnectionFlow}
onCancel={goBackToAddConnectionMethods}
onSaved={({ serverId, hostname, isNewHost }) => {
if (isNewHost) {
setPendingNameHost({ serverId, hostname });
@@ -666,7 +661,8 @@ export default function SettingsScreen() {
<PairLinkModal
visible={isPasteLinkVisible}
targetServerId={addConnectionTargetServerId ?? undefined}
onClose={() => setIsPasteLinkVisible(false)}
onClose={closeAddConnectionFlow}
onCancel={goBackToAddConnectionMethods}
onSaved={({ serverId, hostname, isNewHost }) => {
if (isNewHost) {
setPendingNameHost({ serverId, hostname });
@@ -688,6 +684,51 @@ export default function SettingsScreen() {
/>
) : 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
title="Edit host"
visible={Boolean(editingDaemon)}
@@ -995,13 +1036,15 @@ function DaemonCard({
: statusTone === "error"
? "rgba(248, 113, 113, 0.1)"
: "rgba(161, 161, 170, 0.1)";
const connectionsSummary = (() => {
const parts = daemon.connections.map((conn) => {
if (conn.type === "relay") return `relay:${conn.relayEndpoint}`;
return `direct:${conn.endpoint}`;
});
return parts.join(" • ");
const connectionBadge = (() => {
if (!activeConnection) return null;
if (activeConnection.type === "relay") {
return { icon: <Globe size={12} color={theme.colors.foregroundMuted} />, text: "Relay" };
}
return {
icon: <Monitor size={12} color={theme.colors.foregroundMuted} />,
text: activeConnection.display,
};
})();
return (
@@ -1015,6 +1058,15 @@ function DaemonCard({
<Text style={[styles.statusText, { color: statusColor }]}>{badgeText}</Text>
</View>
{connectionBadge ? (
<View style={styles.connectionPill}>
{connectionBadge.icon}
<Text style={styles.connectionText} numberOfLines={1}>
{connectionBadge.text}
</Text>
</View>
) : null}
<DropdownMenu>
<DropdownMenuTrigger
testID={`daemon-menu-trigger-${daemon.serverId}`}
@@ -1050,32 +1102,6 @@ function DaemonCard({
</DropdownMenu>
</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}
</View>
</View>

View File

@@ -48,19 +48,16 @@ export function AddHostMethodModal({
const { theme } = useUnistyles();
const handleDirect = useCallback(() => {
onClose();
onDirectConnection();
}, [onClose, onDirectConnection]);
}, [onDirectConnection]);
const handleScan = useCallback(() => {
onClose();
onScanQr();
}, [onClose, onScanQr]);
}, [onScanQr]);
const handlePaste = useCallback(() => {
onClose();
onPasteLink();
}, [onClose, onPasteLink]);
}, [onPasteLink]);
return (
<AdaptiveModalSheet title="Add connection" visible={visible} onClose={onClose} testID="add-host-method-modal">

View File

@@ -147,10 +147,11 @@ export interface AddHostModalProps {
visible: boolean;
onClose: () => void;
targetServerId?: string;
onCancel?: () => 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 { daemons, upsertDirectConnection } = useDaemonRegistry();
const isMobile =
@@ -170,6 +171,13 @@ export function AddHostModal({ visible, onClose, onSaved, targetServerId }: AddH
onClose();
}, [isSaving, onClose]);
const handleCancel = useCallback(() => {
if (isSaving) return;
setEndpointRaw("");
setErrorMessage("");
(onCancel ?? onClose)();
}, [isSaving, onCancel, onClose]);
const handleSave = useCallback(async () => {
if (isSaving) return;
@@ -256,8 +264,8 @@ export function AddHostModal({ visible, onClose, onSaved, targetServerId }: AddH
</View>
<View style={styles.actions}>
<Pressable style={styles.button} onPress={handleClose} disabled={isSaving}>
<Text style={styles.buttonText}>Cancel</Text>
<Pressable style={styles.button} onPress={handleCancel} disabled={isSaving}>
<Text style={styles.buttonText}>{onCancel ? "Back" : "Cancel"}</Text>
</Pressable>
<Pressable
style={[styles.button, styles.primaryButton, isSaving ? { opacity: 0.7 } : null]}

View File

@@ -69,10 +69,11 @@ export interface PairLinkModalProps {
visible: boolean;
onClose: () => void;
targetServerId?: string;
onCancel?: () => 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 { daemons, upsertDaemonFromOfferUrl } = useDaemonRegistry();
const isMobile =
@@ -90,6 +91,13 @@ export function PairLinkModal({ visible, onClose, onSaved, targetServerId }: Pai
onClose();
}, [isSaving, onClose]);
const handleCancel = useCallback(() => {
if (isSaving) return;
setOfferUrl("");
setErrorMessage("");
(onCancel ?? onClose)();
}, [isSaving, onCancel, onClose]);
const handleSave = useCallback(async () => {
if (isSaving) return;
const raw = offerUrl.trim();
@@ -178,14 +186,14 @@ export function PairLinkModal({ visible, onClose, onSaved, targetServerId }: Pai
<View style={styles.actions}>
<Pressable
style={styles.button}
onPress={handleClose}
onPress={handleCancel}
disabled={isSaving}
testID="pair-link-cancel"
accessible
accessibilityRole="button"
accessibilityLabel="Cancel"
>
<Text style={styles.buttonText}>Cancel</Text>
<Text style={styles.buttonText}>{onCancel ? "Back" : "Cancel"}</Text>
</Pressable>
<Pressable
style={[styles.button, styles.primaryButton, isSaving ? { opacity: 0.7 } : null]}