aboutsummaryrefslogtreecommitdiffstats
path: root/apps/mobile
diff options
context:
space:
mode:
Diffstat (limited to 'apps/mobile')
-rw-r--r--apps/mobile/components/bookmarks/BookmarkCard.tsx2
-rw-r--r--apps/mobile/components/ui/List.tsx478
-rw-r--r--apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx6
-rw-r--r--apps/mobile/package.json64
4 files changed, 36 insertions, 514 deletions
diff --git a/apps/mobile/components/bookmarks/BookmarkCard.tsx b/apps/mobile/components/bookmarks/BookmarkCard.tsx
index e2df31b9..6c3ef070 100644
--- a/apps/mobile/components/bookmarks/BookmarkCard.tsx
+++ b/apps/mobile/components/bookmarks/BookmarkCard.tsx
@@ -9,7 +9,7 @@ import {
View,
} from "react-native";
import * as Clipboard from "expo-clipboard";
-import * as FileSystem from "expo-file-system";
+import * as FileSystem from "expo-file-system/legacy";
import * as Haptics from "expo-haptics";
import { router, useRouter } from "expo-router";
import * as Sharing from "expo-sharing";
diff --git a/apps/mobile/components/ui/List.tsx b/apps/mobile/components/ui/List.tsx
deleted file mode 100644
index 67f0a9af..00000000
--- a/apps/mobile/components/ui/List.tsx
+++ /dev/null
@@ -1,478 +0,0 @@
-import type {
- FlashListProps,
- ListRenderItem as FlashListRenderItem,
- ListRenderItemInfo,
-} from "@shopify/flash-list";
-import * as React from "react";
-import {
- Platform,
- PressableProps,
- StyleProp,
- TextStyle,
- View,
- ViewProps,
- ViewStyle,
-} from "react-native";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { Button } from "@/components/ui/Button";
-import { Text, TextClassContext } from "@/components/ui/Text";
-import { cn } from "@/lib/utils";
-import { FlashList } from "@shopify/flash-list";
-import { cva } from "class-variance-authority";
-import { cssInterop } from "nativewind";
-
-cssInterop(FlashList, {
- className: "style",
- contentContainerClassName: "contentContainerStyle",
-});
-
-type ListDataItem = string | { title: string; subTitle?: string };
-type ListVariant = "insets" | "full-width";
-
-type ListRef<T extends ListDataItem> = React.Ref<
- React.ComponentRef<typeof FlashList<T>>
->;
-
-type ListRenderItemProps<T extends ListDataItem> = ListRenderItemInfo<T> & {
- variant?: ListVariant;
- isFirstInSection?: boolean;
- isLastInSection?: boolean;
- sectionHeaderAsGap?: boolean;
-};
-
-type ListProps<T extends ListDataItem> = Omit<
- FlashListProps<T>,
- "renderItem"
-> & {
- renderItem?: ListRenderItem<T>;
- variant?: ListVariant;
- sectionHeaderAsGap?: boolean;
- rootClassName?: string;
- rootStyle?: StyleProp<ViewStyle>;
-};
-type ListRenderItem<T extends ListDataItem> = (
- props: ListRenderItemProps<T>,
-) => ReturnType<FlashListRenderItem<T>>;
-
-const rootVariants = cva("min-h-2 flex-1", {
- variants: {
- variant: {
- insets: "ios:px-4",
- "full-width": "ios:bg-card ios:dark:bg-background",
- },
- sectionHeaderAsGap: {
- true: "",
- false: "",
- },
- },
- compoundVariants: [
- {
- variant: "full-width",
- sectionHeaderAsGap: true,
- className: "bg-card dark:bg-background",
- },
- ],
- defaultVariants: {
- variant: "full-width",
- sectionHeaderAsGap: false,
- },
-});
-
-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
- className={cn(
- rootVariants({
- variant,
- sectionHeaderAsGap,
- }),
- rootClassName,
- )}
- style={rootStyle}
- >
- <FlashList
- ref={ref}
- data={data}
- contentInsetAdjustmentBehavior={contentInsetAdjustmentBehavior}
- renderItem={renderItemWithVariant(
- renderItem,
- variant,
- data,
- sectionHeaderAsGap,
- )}
- contentContainerClassName={cn(
- variant === "insets" &&
- (!data || (typeof data?.[0] !== "string" && "pt-4")),
- contentContainerClassName,
- )}
- contentContainerStyle={{
- paddingBottom: Platform.select({
- ios:
- !contentInsetAdjustmentBehavior ||
- contentInsetAdjustmentBehavior === "never"
- ? insets.bottom + 16
- : 0,
- default: insets.bottom,
- }),
- }}
- getItemType={getItemType}
- showsVerticalScrollIndicator={false}
- {...props}
- />
- </View>
- );
-}
-
-function getItemType<T>(item: T) {
- return typeof item === "string" ? "sectioHeader" : "row";
-}
-
-function renderItemWithVariant<T extends ListDataItem>(
- renderItem: ListRenderItem<T> | null | undefined,
- variant: ListVariant,
- data: readonly T[] | null | undefined,
- sectionHeaderAsGap?: boolean,
-) {
- return (args: ListRenderItemProps<T>) => {
- const previousItem = data?.[args.index - 1];
- const nextItem = data?.[args.index + 1];
- return renderItem
- ? renderItem({
- ...args,
- variant,
- isFirstInSection: !previousItem || typeof previousItem === "string",
- isLastInSection: !nextItem || typeof nextItem === "string",
- sectionHeaderAsGap,
- })
- : null;
- };
-}
-
-const List = React.forwardRef(ListComponent) as <T extends ListDataItem>(
- props: ListProps<T> & { ref?: ListRef<T> },
-) => React.ReactElement;
-
-function isPressable(props: PressableProps) {
- return (
- ("onPress" in props && props.onPress) ||
- ("onLongPress" in props && props.onLongPress) ||
- ("onPressIn" in props && props.onPressIn) ||
- ("onPressOut" in props && props.onPressOut) ||
- ("onLongPress" in props && props.onLongPress)
- );
-}
-
-type ListItemProps<T extends ListDataItem> = PressableProps &
- ListRenderItemProps<T> & {
- androidRootClassName?: string;
- titleClassName?: string;
- titleStyle?: StyleProp<TextStyle>;
- textNumberOfLines?: number;
- subTitleClassName?: string;
- subTitleStyle?: StyleProp<TextStyle>;
- subTitleNumberOfLines?: number;
- textContentClassName?: string;
- leftView?: React.ReactNode;
- rightView?: React.ReactNode;
- removeSeparator?: boolean;
- };
-type ListItemRef = React.Ref<View>;
-
-const itemVariants = cva("ios:gap-0 flex-row gap-0 bg-card", {
- variants: {
- variant: {
- insets: "ios:bg-card bg-card/70",
- "full-width": "bg-card dark:bg-background",
- },
- sectionHeaderAsGap: {
- true: "",
- false: "",
- },
- isFirstItem: {
- true: "",
- false: "",
- },
- isFirstInSection: {
- true: "",
- false: "",
- },
- removeSeparator: {
- true: "",
- false: "",
- },
- isLastInSection: {
- true: "",
- false: "",
- },
- disabled: {
- true: "opacity-70",
- false: "opacity-100",
- },
- },
- compoundVariants: [
- {
- variant: "insets",
- sectionHeaderAsGap: true,
- className: "ios:dark:bg-card dark:bg-card/70",
- },
- {
- variant: "insets",
- isFirstInSection: true,
- className: "ios:rounded-t-[10px]",
- },
- {
- variant: "insets",
- isLastInSection: true,
- className: "ios:rounded-b-[10px]",
- },
- {
- removeSeparator: false,
- isLastInSection: true,
- className:
- "ios:border-b-0 border-b border-border/25 dark:border-border/80",
- },
- {
- variant: "insets",
- isFirstItem: true,
- className: "border-t border-border/40",
- },
- ],
- defaultVariants: {
- variant: "insets",
- sectionHeaderAsGap: false,
- isFirstInSection: false,
- isLastInSection: false,
- disabled: false,
- },
-});
-
-function ListItemComponent<T extends ListDataItem>(
- {
- item,
- isFirstInSection,
- isLastInSection,
- index: _index,
- variant,
- className,
- androidRootClassName,
- titleClassName,
- titleStyle,
- textNumberOfLines,
- subTitleStyle,
- subTitleClassName,
- subTitleNumberOfLines,
- textContentClassName,
- sectionHeaderAsGap,
- removeSeparator = false,
- leftView,
- rightView,
- disabled,
- ...props
- }: ListItemProps<T>,
- ref: ListItemRef,
-) {
- if (typeof item === "string") {
- console.log(
- "List.tsx",
- "ListItemComponent",
- "Invalid item of type 'string' was provided. Use ListSectionHeader instead.",
- );
- return null;
- }
- return (
- <>
- <Button
- disabled={disabled || !isPressable(props)}
- variant="plain"
- size="none"
- unstable_pressDelay={100}
- androidRootClassName={androidRootClassName}
- className={itemVariants({
- variant,
- sectionHeaderAsGap,
- isFirstInSection,
- isLastInSection,
- disabled,
- className,
- removeSeparator,
- })}
- {...props}
- ref={ref}
- >
- <TextClassContext.Provider value="font-normal leading-5">
- {!!leftView && <View>{leftView}</View>}
- <View
- className={cn(
- "h-full flex-1 flex-row pr-4",
- !item.subTitle ? "ios:py-3 py-[18px]" : "ios:py-2 py-2",
- !leftView && "ml-4",
- !removeSeparator &&
- (!isLastInSection || variant === "full-width") &&
- "ios:border-b ios:border-border/80",
- !removeSeparator &&
- isFirstInSection &&
- variant === "full-width" &&
- "ios:border-t ios:border-border/80",
- )}
- >
- <View className={cn("flex-1", textContentClassName)}>
- <Text
- numberOfLines={textNumberOfLines}
- style={titleStyle}
- className={cn("text-base", titleClassName)}
- >
- {item.title}
- </Text>
- {!!item.subTitle && (
- <Text
- numberOfLines={subTitleNumberOfLines}
- variant="subhead"
- style={subTitleStyle}
- className={cn("text-muted-foreground", subTitleClassName)}
- >
- {item.subTitle}
- </Text>
- )}
- </View>
- {!!rightView && (
- <View className="flex items-center justify-center">
- {rightView}
- </View>
- )}
- </View>
- </TextClassContext.Provider>
- </Button>
- {!removeSeparator && Platform.OS !== "ios" && !isLastInSection && (
- <View className={cn(variant === "insets" && "px-4")}>
- <View className="h-px bg-border/25 dark:bg-border/80" />
- </View>
- )}
- </>
- );
-}
-
-const ListItem = React.forwardRef(ListItemComponent) as <
- T extends ListDataItem,
->(
- props: ListItemProps<T> & { ref?: ListItemRef },
-) => React.ReactElement;
-
-type ListSectionHeaderProps<T extends ListDataItem> = ViewProps &
- ListRenderItemProps<T> & {
- textClassName?: string;
- };
-type ListSectionHeaderRef = React.Ref<View>;
-
-function ListSectionHeaderComponent<T extends ListDataItem>(
- {
- item,
- isFirstInSection: _isFirstInSection,
- isLastInSection: _isLastInSection,
- index: _index,
- variant,
- className,
- textClassName,
- sectionHeaderAsGap,
- ...props
- }: ListSectionHeaderProps<T>,
- ref: ListSectionHeaderRef,
-) {
- if (typeof item !== "string") {
- console.log(
- "List.tsx",
- "ListSectionHeaderComponent",
- "Invalid item provided. Expected type 'string'. Use ListItem instead.",
- );
- return null;
- }
-
- if (sectionHeaderAsGap) {
- return (
- <View
- className={cn(
- "bg-background",
- Platform.OS !== "ios" &&
- "border-b border-border/25 dark:border-border/80",
- className,
- )}
- {...props}
- ref={ref}
- >
- <View className="h-8" />
- </View>
- );
- }
- return (
- <View
- className={cn(
- "ios:pb-1 pb-4 pl-4 pt-4",
- Platform.OS !== "ios" &&
- "border-b border-border/25 dark:border-border/80",
- variant === "full-width"
- ? "bg-card dark:bg-background"
- : "bg-background",
- className,
- )}
- {...props}
- ref={ref}
- >
- <Text
- variant={Platform.select({ ios: "footnote", default: "body" })}
- className={cn("ios:uppercase ios:text-muted-foreground", textClassName)}
- >
- {item}
- </Text>
- </View>
- );
-}
-
-const ListSectionHeader = React.forwardRef(ListSectionHeaderComponent) as <
- T extends ListDataItem,
->(
- props: ListSectionHeaderProps<T> & { ref?: ListSectionHeaderRef },
-) => React.ReactElement;
-
-const ESTIMATED_ITEM_HEIGHT = {
- titleOnly: Platform.select({ ios: 45, default: 57 }),
- withSubTitle: 56,
-};
-
-function getStickyHeaderIndices<T extends ListDataItem>(data: T[]) {
- if (!data) return [];
- const indices: number[] = [];
- for (let i = 0; i < data.length; i++) {
- if (typeof data[i] === "string") {
- indices.push(i);
- }
- }
- return indices;
-}
-
-export {
- ESTIMATED_ITEM_HEIGHT,
- List,
- ListItem,
- ListSectionHeader,
- getStickyHeaderIndices,
-};
-export type {
- ListDataItem,
- ListItemProps,
- ListProps,
- ListRenderItemInfo,
- ListSectionHeaderProps,
-};
diff --git a/apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx b/apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx
index 0b1dd76c..1a767675 100644
--- a/apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx
+++ b/apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx
@@ -1,7 +1,3 @@
-import type {
- NativeSyntheticEvent,
- TextInputFocusEventData,
-} from "react-native";
import * as React from "react";
import { Pressable, TextInput, View, ViewStyle } from "react-native";
import Animated, {
@@ -119,7 +115,7 @@ const SearchInput = React.forwardRef<
onChangeText("");
}
- function onFocus(e: NativeSyntheticEvent<TextInputFocusEventData>) {
+ function onFocus(e: Parameters<NonNullable<typeof onFocusProp>>[0]) {
setShowCancel(true);
onFocusProp?.(e);
}
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 4fba01cb..94ebc0e1 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -15,52 +15,56 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
+ "@expo/metro-runtime": "~6.1.2",
"@karakeep/shared": "workspace:^0.1.0",
"@karakeep/shared-react": "workspace:^0.1.0",
"@karakeep/trpc": "workspace:^0.1.0",
- "@react-native-async-storage/async-storage": "1.23.1",
- "@react-native-menu/menu": "^1.2.4",
+ "@react-native-async-storage/async-storage": "2.2.0",
+ "@react-native-menu/menu": "^2.0.0",
+ "@react-navigation/native": "^7.1.8",
"@rn-primitives/hooks": "^1.3.0",
"@rn-primitives/slot": "^1.2.0",
- "@shopify/flash-list": "^2.0.3",
+ "@shopify/flash-list": "2.0.2",
"@tanstack/react-query": "5.90.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
- "expo": "~53.0.19",
- "expo-build-properties": "^0.14.6",
- "expo-checkbox": "^4.1.4",
- "expo-clipboard": "^7.1.4",
- "expo-constants": "~17.1.6",
- "expo-dev-client": "^5.2.0",
- "expo-file-system": "~18.1.11",
- "expo-haptics": "^14.1.4",
- "expo-image": "^2.4.0",
- "expo-image-picker": "^16.1.4",
- "expo-linking": "~7.1.5",
- "expo-navigation-bar": "^4.2.5",
- "expo-router": "~5.0.7",
- "expo-secure-store": "^14.2.3",
- "expo-share-intent": "^4.0.0",
- "expo-sharing": "~13.0.1",
- "expo-status-bar": "~2.2.3",
- "expo-system-ui": "^5.0.8",
- "expo-web-browser": "^14.1.6",
+ "expo": "~54.0.31",
+ "expo-build-properties": "~1.0.10",
+ "expo-checkbox": "~5.0.8",
+ "expo-clipboard": "~8.0.8",
+ "expo-constants": "~18.0.13",
+ "expo-dev-client": "~6.0.20",
+ "expo-file-system": "~19.0.21",
+ "expo-haptics": "~15.0.8",
+ "expo-image": "~3.0.11",
+ "expo-image-picker": "~17.0.10",
+ "expo-linking": "~8.0.11",
+ "expo-navigation-bar": "~5.0.10",
+ "expo-router": "~6.0.21",
+ "expo-secure-store": "~15.0.8",
+ "expo-share-intent": "^5.1.1",
+ "expo-sharing": "~14.0.8",
+ "expo-status-bar": "~3.0.9",
+ "expo-system-ui": "~6.0.9",
+ "expo-web-browser": "~15.0.10",
"lucide-react-native": "^0.513.0",
- "nativewind": "^4.1.23",
+ "nativewind": "^4.2.1",
"react": "^19.2.1",
- "react-native": "0.79.5",
+ "react-native": "0.81.5",
"react-native-awesome-slider": "^2.5.3",
"react-native-blob-util": "^0.21.2",
- "react-native-gesture-handler": "~2.24.0",
+ "react-native-css-interop": "0.2.1",
+ "react-native-gesture-handler": "~2.28.0",
"react-native-image-viewing": "^0.2.2",
"react-native-keyboard-controller": "^1.18.5",
"react-native-markdown-display": "^7.0.2",
"react-native-pdf": "7.0.3",
- "react-native-reanimated": "^3.17.5",
- "react-native-safe-area-context": "5.4.0",
- "react-native-screens": "~4.11.1",
- "react-native-svg": "^15.11.2",
- "react-native-webview": "^13.13.5",
+ "react-native-reanimated": "~4.1.1",
+ "react-native-safe-area-context": "~5.6.0",
+ "react-native-screens": "~4.16.0",
+ "react-native-svg": "15.12.1",
+ "react-native-webview": "13.15.0",
+ "react-native-worklets": "0.5.1",
"sonner-native": "^0.22.2",
"tailwind-merge": "^2.2.1",
"zod": "^3.24.2",