From ece68ed078be3f6d66b5dcd7de8ba9853d48be27 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Mon, 22 Dec 2025 16:36:23 +0200 Subject: feat(mobile): Convert server address editing to modal in mobile app (#2290) * feat: Convert server address editing to modal in mobile app Changed the server address editing experience from an inline button to a modal dialog. This improves UX by forcing users to explicitly save or cancel their changes rather than forgetting to click a save button. Changes: - Created ServerAddressModal component following the CustomHeadersModal pattern - Updated signin page to use the modal instead of inline editing - Enhanced settings page to allow changing server address (was previously read-only) - Added validation and error handling within the modal - Made the settings page server address clickable with visual feedback This resolves the issue where users forget to click the save button after editing the server address. * refactor: Convert server address to screen modal Changed from React Native Modal to Expo Router screen modal presentation. This provides a better native experience with proper navigation stack integration. Changes: - Created server-address.tsx as a screen route with modal presentation - Registered the route in root _layout.tsx - Updated signin.tsx to navigate to the screen modal instead of opening RN modal - Reverted settings page to original (no server address editing from settings) - Removed ServerAddressModal component (no longer needed) Benefits: - Native modal presentation with proper animations - Better integration with the navigation stack - Cleaner separation of concerns * merge the custom headers inside the server-add screen * fix the look of the address UI --------- Co-authored-by: Claude --- apps/mobile/app/_layout.tsx | 8 ++ apps/mobile/app/server-address.tsx | 230 +++++++++++++++++++++++++++++++++++++ apps/mobile/app/signin.tsx | 103 +++-------------- 3 files changed, 257 insertions(+), 84 deletions(-) create mode 100644 apps/mobile/app/server-address.tsx (limited to 'apps/mobile') diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index 1e6128c7..3f9e5575 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -63,6 +63,14 @@ export default function RootLayout() { }} /> + (); + + // Custom headers state + const [headers, setHeaders] = useState<{ key: string; value: string }[]>( + Object.entries(settings.customHeaders || {}).map(([key, value]) => ({ + key, + value, + })), + ); + const [newHeaderKey, setNewHeaderKey] = useState(""); + const [newHeaderValue, setNewHeaderValue] = useState(""); + + const handleAddHeader = () => { + if (!newHeaderKey.trim() || !newHeaderValue.trim()) { + return; + } + + // Check if header already exists + const existingIndex = headers.findIndex((h) => h.key === newHeaderKey); + if (existingIndex >= 0) { + // Update existing header + const updatedHeaders = [...headers]; + updatedHeaders[existingIndex].value = newHeaderValue; + setHeaders(updatedHeaders); + } else { + // Add new header + setHeaders([...headers, { key: newHeaderKey, value: newHeaderValue }]); + } + + setNewHeaderKey(""); + setNewHeaderValue(""); + }; + + const handleRemoveHeader = (index: number) => { + setHeaders(headers.filter((_, i) => i !== index)); + }; + + const handleSave = () => { + // Validate the address + if (!address.trim()) { + setError("Server address is required"); + return; + } + + if (!address.startsWith("http://") && !address.startsWith("https://")) { + setError("Server address must start with http:// or https://"); + return; + } + + // Convert headers array to object + const headersObject = headers.reduce( + (acc, { key, value }) => { + if (key.trim() && value.trim()) { + acc[key] = value; + } + return acc; + }, + {} as Record, + ); + + // Remove trailing slash and save + const cleanedAddress = address.trim().replace(/\/$/, ""); + setSettings({ + ...settings, + address: cleanedAddress, + customHeaders: headersObject, + }); + router.back(); + }; + + return ( + + ( + + + Save + + + ), + }} + /> + + + {/* Error Message */} + {error && ( + + + {error} + + + )} + + {/* Server Address Section */} + + + Server URL + + + + Enter the URL of your Karakeep server + + { + setAddress(text); + setError(undefined); + }} + autoCapitalize="none" + keyboardType="url" + autoFocus + inputClasses="bg-background" + /> + + Must start with http:// or https:// + + + + + {/* Custom Headers Section */} + + + Custom Headers + {headers.length > 0 && ( + ({headers.length}) + )} + + + + Add custom HTTP headers for API requests + + + {/* Existing Headers List */} + {headers.length === 0 ? ( + + + No custom headers configured + + + ) : ( + + {headers.map((header, index) => ( + + + + {header.key} + + + {header.value} + + + handleRemoveHeader(index)} + className="rounded-md p-2" + hitSlop={8} + > + + + + ))} + + )} + + {/* Add New Header Form */} + + Add New Header + + + + + + + + + ); +} diff --git a/apps/mobile/app/signin.tsx b/apps/mobile/app/signin.tsx index 6a554f89..03cbba5a 100644 --- a/apps/mobile/app/signin.tsx +++ b/apps/mobile/app/signin.tsx @@ -7,7 +7,6 @@ import { View, } from "react-native"; import { Redirect, useRouter } from "expo-router"; -import { CustomHeadersModal } from "@/components/CustomHeadersModal"; import Logo from "@/components/Logo"; import { TailwindResolver } from "@/components/TailwindResolver"; import { Button } from "@/components/ui/Button"; @@ -15,7 +14,7 @@ import { Input } from "@/components/ui/Input"; import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; import { api } from "@/lib/trpc"; -import { Bug, Check, Edit3 } from "lucide-react-native"; +import { Bug, Edit3 } from "lucide-react-native"; enum LoginType { Password, @@ -28,12 +27,6 @@ export default function Signin() { const [error, setError] = useState(); const [loginType, setLoginType] = useState(LoginType.Password); - const [isEditingServerAddress, setIsEditingServerAddress] = useState(false); - const [tempServerAddress, setTempServerAddress] = useState( - settings.address ?? "https://cloud.karakeep.app", - ); - const [isCustomHeadersModalVisible, setIsCustomHeadersModalVisible] = - useState(false); const emailRef = useRef(""); const passwordRef = useRef(""); @@ -82,19 +75,15 @@ export default function Signin() { return ; } - const handleSaveCustomHeaders = (headers: Record) => { - setSettings({ ...settings, customHeaders: headers }); - }; - const onSignin = () => { - if (!tempServerAddress) { + if (!settings.address) { setError("Server address is required"); return; } if ( - !tempServerAddress.startsWith("http://") && - !tempServerAddress.startsWith("https://") + !settings.address.startsWith("http://") && + !settings.address.startsWith("https://") ) { setError("Server address must start with http:// or https://"); return; @@ -137,71 +126,23 @@ export default function Signin() { )} Server Address - {!isEditingServerAddress ? ( - - - {tempServerAddress} - - + + + {settings.address ?? "https://cloud.karakeep.app"} - ) : ( - - router.push("/server-address")} + > + ( + + )} + className="color-foreground" /> - - - )} - setIsCustomHeadersModalVisible(true)} - className="mt-1" - > - - Configure Custom Headers{" "} - {settings.customHeaders && - Object.keys(settings.customHeaders).length > 0 && - `(${Object.keys(settings.customHeaders).length})`} - - + + {loginType === LoginType.Password && ( <> @@ -282,12 +223,6 @@ export default function Signin() { - setIsCustomHeadersModalVisible(false)} - onSave={handleSaveCustomHeaders} - /> ); } -- cgit v1.2.3-70-g09d2