Improve server URL input UX on native app launch (#9)

This commit is contained in:
jake authored and GitHub committed 2026-03-13 10:32:38 -04:00
1 parent 83839a0cd7
commit 3a57de2ed6
2 files changed
+58 -41

No files matched your search

+23 -11
View File
@@ -63,8 +63,6 @@ export default function ServerUrlScreen() {
const destructiveColor = useCSSVariable("--color-destructive") as string; const destructiveColor = useCSSVariable("--color-destructive") as string;
const mutedFgColor = useCSSVariable("--color-muted-foreground") as string; const mutedFgColor = useCSSVariable("--color-muted-foreground") as string;
const [isFirstLaunch] = useState(() => !hasStoredServerUrl());
// Icon pulse animation // Icon pulse animation
const iconOpacity = useSharedValue(1); const iconOpacity = useSharedValue(1);
const iconAnimatedStyle = useAnimatedStyle(() => ({ const iconAnimatedStyle = useAnimatedStyle(() => ({
@@ -94,14 +92,6 @@ export default function ServerUrlScreen() {
}; };
}, []); }, []);
// Auto-focus on first launch
useEffect(() => {
if (isFirstLaunch) {
const timer = setTimeout(() => inputRef.current?.focus(), 500);
return () => clearTimeout(timer);
}
}, [isFirstLaunch]);
const handleChangeText = (text: string) => { const handleChangeText = (text: string) => {
setUrl(text); setUrl(text);
if (connection.phase === "error") { if (connection.phase === "error") {
@@ -114,6 +104,14 @@ export default function ServerUrlScreen() {
if (!trimmed) return; if (!trimmed) return;
const fullUrl = normalizeUrl(trimmed); const fullUrl = normalizeUrl(trimmed);
try {
new URL(fullUrl);
} catch {
setConnection({ phase: "error", error: "invalid_url" });
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
return;
}
setConnection({ phase: "connecting" }); setConnection({ phase: "connecting" });
const result = await validateServerUrl(fullUrl); const result = await validateServerUrl(fullUrl);
@@ -133,6 +131,16 @@ export default function ServerUrlScreen() {
const isConnecting = connection.phase === "connecting"; const isConnecting = connection.phase === "connecting";
const isSuccess = connection.phase === "success"; const isSuccess = connection.phase === "success";
const trimmedUrl = url.trim().replace(/\/+$/, "");
const isValidUrl = (() => {
if (!trimmedUrl) return false;
try {
new URL(normalizeUrl(trimmedUrl));
return true;
} catch {
return false;
}
})();
const isDisabled = isConnecting || isSuccess; const isDisabled = isConnecting || isSuccess;
return ( return (
@@ -187,7 +195,11 @@ export default function ServerUrlScreen() {
</Text> </Text>
</View> </View>
) : ( ) : (
<Button onPress={handleConnect} className="bg-primary"> <Button
onPress={handleConnect}
disabled={!isValidUrl}
className="bg-primary"
>
<ButtonLabel>Connect</ButtonLabel> <ButtonLabel>Connect</ButtonLabel>
</Button> </Button>
)} )}
+35 -30
View File
@@ -1,6 +1,7 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import type { StyleProp, ViewStyle } from "react-native"; import type { StyleProp, ViewStyle } from "react-native";
import { ScrollView } from "react-native"; import { ScrollView } from "react-native";
import { KeyboardAvoidingView } from "react-native-keyboard-controller";
import Animated, { FadeIn } from "react-native-reanimated"; import Animated, { FadeIn } from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { SofaLogo } from "@/components/ui/sofa-logo"; import { SofaLogo } from "@/components/ui/sofa-logo";
@@ -22,37 +23,41 @@ export function AuthScreen({
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
return ( return (
<ScrollView <KeyboardAvoidingView behavior="padding" style={{ flex: 1 }}>
contentContainerStyle={{ <ScrollView
flexGrow: 1, contentContainerStyle={{
justifyContent: "center", flexGrow: 1,
paddingHorizontal: 24, justifyContent: "center",
paddingTop: insets.top, paddingHorizontal: 24,
paddingBottom: insets.bottom, paddingTop: insets.top + 24,
}} paddingBottom: insets.bottom,
keyboardShouldPersistTaps="handled" }}
bounces={false} keyboardShouldPersistTaps="handled"
className="bg-background" bounces={false}
> className="bg-background"
<Animated.View
entering={FadeIn.duration(400)}
className="mb-4 items-center"
> >
{logoStyle ? ( <Animated.View
<Animated.View style={logoStyle}> entering={FadeIn.duration(400)}
className="mb-4 items-center"
>
{logoStyle ? (
<Animated.View style={logoStyle}>
<SofaLogo size={48} />
</Animated.View>
) : (
<SofaLogo size={48} /> <SofaLogo size={48} />
</Animated.View> )}
) : ( <Text className="mt-1 font-display text-[32px] text-foreground">
<SofaLogo size={48} /> {title}
)} </Text>
<Text className="mt-1 font-display text-[32px] text-foreground"> {subtitle && (
{title} <Text className="mt-2 text-muted-foreground text-sm">
</Text> {subtitle}
{subtitle && ( </Text>
<Text className="mt-2 text-muted-foreground text-sm">{subtitle}</Text> )}
)} </Animated.View>
</Animated.View> {children}
{children} </ScrollView>
</ScrollView> </KeyboardAvoidingView>
); );
} }