aboutsummaryrefslogtreecommitdiffstats
path: root/apps/mobile
diff options
context:
space:
mode:
Diffstat (limited to 'apps/mobile')
-rw-r--r--apps/mobile/app/dashboard/(tabs)/settings.tsx180
-rw-r--r--apps/mobile/components/settings/UserProfileHeader.tsx27
-rw-r--r--apps/mobile/components/ui/Avatar.tsx88
-rw-r--r--apps/mobile/components/ui/CustomSafeAreaView.tsx1
-rw-r--r--apps/mobile/components/ui/List.tsx41
5 files changed, 234 insertions, 103 deletions
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 (
<CustomSafeAreaView>
- <PageTitle title="Settings" />
+ <UserProfileHeader
+ image={data?.image}
+ name={data?.name}
+ email={data?.email}
+ />
<View className="flex h-full w-full items-center gap-3 px-4 py-2">
- <View className="flex w-full gap-3 rounded-lg bg-card px-4 py-2">
- <Text>{isSettingsLoading ? "Loading ..." : settings.address}</Text>
- <Divider orientation="horizontal" />
- <Text>{isLoading ? "Loading ..." : data?.email}</Text>
- </View>
- <Text className="w-full p-1 text-2xl font-bold text-foreground">
- App Settings
- </Text>
- <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-card px-4 py-2">
- <Link asChild href="/dashboard/settings/theme" className="flex-1">
- <Pressable className="flex flex-row justify-between">
- <Text>Theme</Text>
- <View className="flex flex-row items-center gap-2">
- <Text className="text-muted-foreground">
- {
- { light: "Light", dark: "Dark", system: "System" }[
- settings.theme
- ]
- }
- </Text>
- <ChevronRight />
- </View>
- </Pressable>
- </Link>
- </View>
- <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-card px-4 py-2">
- <Link
- asChild
- href="/dashboard/settings/bookmark-default-view"
- className="flex-1"
- >
- <Pressable className="flex flex-row justify-between">
- <Text>Default Bookmark View</Text>
- <View className="flex flex-row items-center gap-2">
- {isSettingsLoading ? (
- <ActivityIndicator size="small" />
- ) : (
+ <View className="w-full rounded-xl bg-card py-2">
+ <View className="flex flex-row items-center justify-between gap-8 px-4 py-1">
+ <Link asChild href="/dashboard/settings/theme" className="flex-1">
+ <Pressable className="flex flex-row justify-between">
+ <Text>Theme</Text>
+ <View className="flex flex-row items-center gap-2">
<Text className="text-muted-foreground">
- {settings.defaultBookmarkView === "reader"
- ? "Reader"
- : "Browser"}
+ {
+ { light: "Light", dark: "Dark", system: "System" }[
+ settings.theme
+ ]
+ }
</Text>
- )}
+ <ChevronRight />
+ </View>
+ </Pressable>
+ </Link>
+ </View>
+ <Divider orientation="horizontal" className="mx-6 my-1" />
+ <View className="flex flex-row items-center justify-between gap-8 px-4 py-1">
+ <Link
+ asChild
+ href="/dashboard/settings/bookmark-default-view"
+ className="flex-1"
+ >
+ <Pressable className="flex flex-row justify-between">
+ <Text>Default Bookmark View</Text>
+ <View className="flex flex-row items-center gap-2">
+ {isSettingsLoading ? (
+ <ActivityIndicator size="small" />
+ ) : (
+ <Text className="text-muted-foreground">
+ {settings.defaultBookmarkView === "reader"
+ ? "Reader"
+ : "Browser"}
+ </Text>
+ )}
+ <ChevronRight />
+ </View>
+ </Pressable>
+ </Link>
+ </View>
+ <Divider orientation="horizontal" className="mx-6 my-1" />
+ <View className="flex flex-row items-center justify-between gap-8 px-4 py-1">
+ <Link
+ asChild
+ href="/dashboard/settings/reader-settings"
+ className="flex-1"
+ >
+ <Pressable className="flex flex-row justify-between">
+ <Text>Reader Text Settings</Text>
<ChevronRight />
- </View>
- </Pressable>
- </Link>
- </View>
- <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-card px-4 py-2">
- <Link
- asChild
- href="/dashboard/settings/reader-settings"
- className="flex-1"
- >
- <Pressable className="flex flex-row justify-between">
- <Text>Reader Text Settings</Text>
- <ChevronRight />
- </Pressable>
- </Link>
- </View>
- <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-card px-4 py-2">
- <Text>Show note preview in bookmark</Text>
- <Switch
- value={settings.showNotes}
- onValueChange={(value) =>
- setSettings({
- ...settings,
- showNotes: value,
- })
- }
- />
- </View>
- <Text className="w-full p-1 text-2xl font-bold text-foreground">
- Upload Settings
- </Text>
- <View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-card px-4 py-2">
- <Text>Image Quality</Text>
- <View className="flex flex-1 flex-row items-center justify-center gap-2">
- <Text className="text-foreground">
- {Math.round(settings.imageQuality * 100)}%
+ </Pressable>
+ </Link>
+ </View>
+ <Divider orientation="horizontal" className="mx-6 my-1" />
+ <View className="flex flex-row items-center justify-between gap-8 px-4 py-1">
+ <Text className="flex-1" numberOfLines={1}>
+ Show notes in bookmark card
</Text>
- <Slider
- onSlidingComplete={(value) =>
+ <Switch
+ className="shrink-0"
+ value={settings.showNotes}
+ onValueChange={(value) =>
setSettings({
...settings,
- imageQuality: Math.round(value) / 100,
+ showNotes: value,
})
}
- progress={imageQuality}
- minimumValue={imageQualityMin}
- maximumValue={imageQualityMax}
/>
</View>
</View>
- <Divider orientation="horizontal" />
+
+ <View className="w-full rounded-xl bg-card py-2">
+ <View className="flex w-full flex-row items-center justify-between gap-8 px-4 py-1">
+ <Text>Upload Image Quality</Text>
+ <View className="flex flex-1 flex-row items-center justify-center gap-2">
+ <Text className="text-foreground">
+ {Math.round(settings.imageQuality * 100)}%
+ </Text>
+ <Slider
+ onSlidingComplete={(value) =>
+ setSettings({
+ ...settings,
+ imageQuality: Math.round(value) / 100,
+ })
+ }
+ progress={imageQuality}
+ minimumValue={imageQualityMin}
+ maximumValue={imageQualityMax}
+ />
+ </View>
+ </View>
+ </View>
<Button
androidRootClassName="w-full"
onPress={logout}
@@ -150,6 +153,9 @@ export default function Dashboard() {
</Button>
<View className="mt-4 w-full gap-1">
<Text className="text-center text-sm text-muted-foreground">
+ {isSettingsLoading ? "Loading..." : settings.address}
+ </Text>
+ <Text className="text-center text-sm text-muted-foreground">
App Version: {Constants.expoConfig?.version ?? "unknown"}
</Text>
<Text className="text-center text-sm text-muted-foreground">
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 (
+ <View className="w-full items-center gap-2 py-6">
+ <Avatar image={image} name={name} size={88} />
+ <View className="items-center gap-1">
+ <Text className="text-xl font-semibold">{name || "User"}</Text>
+ {email && (
+ <Text className="text-sm text-muted-foreground">{email}</Text>
+ )}
+ </View>
+ </View>
+ );
+}
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 (
+ <View
+ className={cn("overflow-hidden bg-black", className)}
+ style={{
+ width: size,
+ height: size,
+ borderRadius: size / 2,
+ }}
+ >
+ {showFallback ? (
+ <View
+ className={cn(
+ "flex h-full w-full items-center justify-center bg-black",
+ fallbackClassName,
+ )}
+ >
+ <Text
+ className="text-white"
+ style={{
+ fontSize: size * 0.4,
+ lineHeight: size * 0.4,
+ textAlign: "center",
+ }}
+ >
+ {initials}
+ </Text>
+ </View>
+ ) : (
+ <Image
+ source={imageUrl}
+ className={cn("h-full w-full", imageClassName)}
+ style={{ resizeMode: "cover" }}
+ onError={() => setImageError(true)}
+ />
+ )}
+ </View>
+ );
+}
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 (
<SafeAreaView
style={{
+ flex: 1,
paddingTop:
// Some ugly hacks to make the app look the same on both android and ios
Platform.OS == "android" && edges.includes("top")
diff --git a/apps/mobile/components/ui/List.tsx b/apps/mobile/components/ui/List.tsx
index 52ff5779..67f0a9af 100644
--- a/apps/mobile/components/ui/List.tsx
+++ b/apps/mobile/components/ui/List.tsx
@@ -29,7 +29,9 @@ cssInterop(FlashList, {
type ListDataItem = string | { title: string; subTitle?: string };
type ListVariant = "insets" | "full-width";
-type ListRef<T extends ListDataItem> = React.Ref<typeof FlashList<T>>;
+type ListRef<T extends ListDataItem> = React.Ref<
+ React.ComponentRef<typeof FlashList<T>>
+>;
type ListRenderItemProps<T extends ListDataItem> = ListRenderItemInfo<T> & {
variant?: ListVariant;
@@ -76,17 +78,20 @@ const rootVariants = cva("min-h-2 flex-1", {
},
});
-function ListComponent<T extends ListDataItem>({
- variant = "full-width",
- rootClassName,
- rootStyle,
- contentContainerClassName,
- renderItem,
- data,
- sectionHeaderAsGap = false,
- contentInsetAdjustmentBehavior = "automatic",
- ...props
-}: ListProps<T>) {
+function ListComponent<T extends ListDataItem>(
+ {
+ variant = "full-width",
+ rootClassName,
+ rootStyle,
+ contentContainerClassName,
+ renderItem,
+ data,
+ sectionHeaderAsGap = false,
+ contentInsetAdjustmentBehavior = "automatic",
+ ...props
+ }: ListProps<T>,
+ ref: ListRef<T>,
+) {
const insets = useSafeAreaInsets();
return (
<View
@@ -100,6 +105,7 @@ function ListComponent<T extends ListDataItem>({
style={rootStyle}
>
<FlashList
+ ref={ref}
data={data}
contentInsetAdjustmentBehavior={contentInsetAdjustmentBehavior}
renderItem={renderItemWithVariant(
@@ -311,10 +317,9 @@ function ListItemComponent<T extends ListDataItem>(
{!!leftView && <View>{leftView}</View>}
<View
className={cn(
- "h-full flex-1 flex-row",
+ "h-full flex-1 flex-row pr-4",
!item.subTitle ? "ios:py-3 py-[18px]" : "ios:py-2 py-2",
!leftView && "ml-4",
- !rightView && "pr-4",
!removeSeparator &&
(!isLastInSection || variant === "full-width") &&
"ios:border-b ios:border-border/80",
@@ -328,7 +333,7 @@ function ListItemComponent<T extends ListDataItem>(
<Text
numberOfLines={textNumberOfLines}
style={titleStyle}
- className={titleClassName}
+ className={cn("text-base", titleClassName)}
>
{item.title}
</Text>
@@ -343,7 +348,11 @@ function ListItemComponent<T extends ListDataItem>(
</Text>
)}
</View>
- {!!rightView && <View>{rightView}</View>}
+ {!!rightView && (
+ <View className="flex items-center justify-center">
+ {rightView}
+ </View>
+ )}
</View>
</TextClassContext.Provider>
</Button>