From 6ee48ffb9d628a04c487b73b222be76241ff3ec4 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Mon, 29 Dec 2025 10:23:36 +0200 Subject: feat(mobile): make the settings menu look more native (#2307) * feat(mobile): make the settings menu look more native * more fixes * review comments --- apps/mobile/app/dashboard/(tabs)/settings.tsx | 180 +++++++++++---------- .../components/settings/UserProfileHeader.tsx | 27 ++++ apps/mobile/components/ui/Avatar.tsx | 88 ++++++++++ apps/mobile/components/ui/CustomSafeAreaView.tsx | 1 + apps/mobile/components/ui/List.tsx | 41 +++-- 5 files changed, 234 insertions(+), 103 deletions(-) create mode 100644 apps/mobile/components/settings/UserProfileHeader.tsx create mode 100644 apps/mobile/components/ui/Avatar.tsx (limited to 'apps') diff --git a/apps/mobile/app/dashboard/(tabs)/settings.tsx b/apps/mobile/app/dashboard/(tabs)/settings.tsx index db19b6fe..106baec5 100644 --- a/apps/mobile/app/dashboard/(tabs)/settings.tsx +++ b/apps/mobile/app/dashboard/(tabs)/settings.tsx @@ -4,11 +4,11 @@ import { Slider } from "react-native-awesome-slider"; import { useSharedValue } from "react-native-reanimated"; import Constants from "expo-constants"; import { Link } from "expo-router"; +import { UserProfileHeader } from "@/components/settings/UserProfileHeader"; import { Button } from "@/components/ui/Button"; import ChevronRight from "@/components/ui/ChevronRight"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import { Divider } from "@/components/ui/Divider"; -import PageTitle from "@/components/ui/PageTitle"; import { Text } from "@/components/ui/Text"; import { useServerVersion } from "@/lib/hooks"; import { useSession } from "@/lib/session"; @@ -31,7 +31,7 @@ export default function Dashboard() { imageQuality.value = settings.imageQuality * 100; }, [settings]); - const { data, error, isLoading } = api.users.whoami.useQuery(); + const { data, error } = api.users.whoami.useQuery(); const { data: serverVersion, isLoading: isServerVersionLoading, @@ -44,103 +44,106 @@ export default function Dashboard() { return ( - + - - {isSettingsLoading ? "Loading ..." : settings.address} - - {isLoading ? "Loading ..." : data?.email} - - - App Settings - - - - - Theme - - - { - { light: "Light", dark: "Dark", system: "System" }[ - settings.theme - ] - } - - - - - - - - - - Default Bookmark View - - {isSettingsLoading ? ( - - ) : ( + + + + + Theme + - {settings.defaultBookmarkView === "reader" - ? "Reader" - : "Browser"} + { + { light: "Light", dark: "Dark", system: "System" }[ + settings.theme + ] + } - )} + + + + + + + + + + Default Bookmark View + + {isSettingsLoading ? ( + + ) : ( + + {settings.defaultBookmarkView === "reader" + ? "Reader" + : "Browser"} + + )} + + + + + + + + + + Reader Text Settings - - - - - - - - Reader Text Settings - - - - - - Show note preview in bookmark - - setSettings({ - ...settings, - showNotes: value, - }) - } - /> - - - Upload Settings - - - Image Quality - - - {Math.round(settings.imageQuality * 100)}% + + + + + + + Show notes in bookmark card - + setSettings({ ...settings, - imageQuality: Math.round(value) / 100, + showNotes: value, }) } - progress={imageQuality} - minimumValue={imageQualityMin} - maximumValue={imageQualityMax} /> - + + + + Upload Image Quality + + + {Math.round(settings.imageQuality * 100)}% + + + setSettings({ + ...settings, + imageQuality: Math.round(value) / 100, + }) + } + progress={imageQuality} + minimumValue={imageQualityMin} + maximumValue={imageQualityMax} + /> + + + + + {isSettingsLoading ? "Loading..." : settings.address} + App Version: {Constants.expoConfig?.version ?? "unknown"} diff --git a/apps/mobile/components/settings/UserProfileHeader.tsx b/apps/mobile/components/settings/UserProfileHeader.tsx new file mode 100644 index 00000000..6e389877 --- /dev/null +++ b/apps/mobile/components/settings/UserProfileHeader.tsx @@ -0,0 +1,27 @@ +import { View } from "react-native"; +import { Avatar } from "@/components/ui/Avatar"; +import { Text } from "@/components/ui/Text"; + +interface UserProfileHeaderProps { + image?: string | null; + name?: string | null; + email?: string | null; +} + +export function UserProfileHeader({ + image, + name, + email, +}: UserProfileHeaderProps) { + return ( + + + + {name || "User"} + {email && ( + {email} + )} + + + ); +} diff --git a/apps/mobile/components/ui/Avatar.tsx b/apps/mobile/components/ui/Avatar.tsx new file mode 100644 index 00000000..923c634e --- /dev/null +++ b/apps/mobile/components/ui/Avatar.tsx @@ -0,0 +1,88 @@ +import * as React from "react"; +import { Image, View } from "react-native"; +import { Text } from "@/components/ui/Text"; +import { useAssetUrl } from "@/lib/hooks"; +import { cn } from "@/lib/utils"; + +interface AvatarProps { + image?: string | null; + name?: string | null; + size?: number; + className?: string; + imageClassName?: string; + fallbackClassName?: string; +} + +function isExternalUrl(url: string) { + return url.startsWith("http://") || url.startsWith("https://"); +} + +export function Avatar({ + image, + name, + size = 40, + className, + imageClassName, + fallbackClassName, +}: AvatarProps) { + const [imageError, setImageError] = React.useState(false); + const assetUrl = useAssetUrl(image ?? ""); + + const imageUrl = React.useMemo(() => { + if (!image) return null; + return isExternalUrl(image) + ? { + uri: image, + } + : assetUrl; + }, [image]); + + React.useEffect(() => { + setImageError(false); + }, [image]); + + const initials = React.useMemo(() => { + if (!name) return "U"; + return name.charAt(0).toUpperCase(); + }, [name]); + + const showFallback = !imageUrl || imageError; + + return ( + + {showFallback ? ( + + + {initials} + + + ) : ( + setImageError(true)} + /> + )} + + ); +} diff --git a/apps/mobile/components/ui/CustomSafeAreaView.tsx b/apps/mobile/components/ui/CustomSafeAreaView.tsx index fdf6520d..840ea058 100644 --- a/apps/mobile/components/ui/CustomSafeAreaView.tsx +++ b/apps/mobile/components/ui/CustomSafeAreaView.tsx @@ -15,6 +15,7 @@ export default function CustomSafeAreaView({ return ( = React.Ref>; +type ListRef = React.Ref< + React.ComponentRef> +>; type ListRenderItemProps = ListRenderItemInfo & { variant?: ListVariant; @@ -76,17 +78,20 @@ const rootVariants = cva("min-h-2 flex-1", { }, }); -function ListComponent({ - variant = "full-width", - rootClassName, - rootStyle, - contentContainerClassName, - renderItem, - data, - sectionHeaderAsGap = false, - contentInsetAdjustmentBehavior = "automatic", - ...props -}: ListProps) { +function ListComponent( + { + variant = "full-width", + rootClassName, + rootStyle, + contentContainerClassName, + renderItem, + data, + sectionHeaderAsGap = false, + contentInsetAdjustmentBehavior = "automatic", + ...props + }: ListProps, + ref: ListRef, +) { const insets = useSafeAreaInsets(); return ( ({ style={rootStyle} > ( {!!leftView && {leftView}} ( {item.title} @@ -343,7 +348,11 @@ function ListItemComponent( )} - {!!rightView && {rightView}} + {!!rightView && ( + + {rightView} + + )} -- cgit v1.2.3-70-g09d2