aboutsummaryrefslogtreecommitdiffstats
path: root/apps/mobile
diff options
context:
space:
mode:
Diffstat (limited to 'apps/mobile')
-rw-r--r--apps/mobile/app/_layout.tsx50
-rw-r--r--apps/mobile/app/sharing.tsx40
-rw-r--r--apps/mobile/components/bookmarks/BookmarkCard.tsx20
-rw-r--r--apps/mobile/components/bookmarks/ListPickerModal.tsx117
-rw-r--r--apps/mobile/package.json5
5 files changed, 201 insertions, 31 deletions
diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx
index a5aafb8c..f56aa810 100644
--- a/apps/mobile/app/_layout.tsx
+++ b/apps/mobile/app/_layout.tsx
@@ -3,6 +3,7 @@ import "expo-dev-client";
import { useEffect } from "react";
import { View } from "react-native";
+import { GestureHandlerRootView } from "react-native-gesture-handler";
import { useRouter } from "expo-router";
import { Stack } from "expo-router/stack";
import { ShareIntentProvider, useShareIntent } from "expo-share-intent";
@@ -10,6 +11,7 @@ import { StatusBar } from "expo-status-bar";
import { StyledStack } from "@/components/navigation/stack";
import { Providers } from "@/lib/providers";
import { cn } from "@/lib/utils";
+import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import { useColorScheme } from "nativewind";
export default function RootLayout() {
@@ -28,28 +30,32 @@ export default function RootLayout() {
return (
<ShareIntentProvider>
<Providers>
- <View
- className={cn(
- "w-full flex-1 bg-gray-100 text-foreground dark:bg-background",
- colorScheme == "dark" ? "dark" : "light",
- )}
- >
- <StyledStack
- contentClassName="bg-gray-100 dark:bg-background"
- screenOptions={{
- headerShown: false,
- }}
- >
- <Stack.Screen name="index" />
- <Stack.Screen
- name="sharing"
- options={{
- presentation: "modal",
- }}
- />
- </StyledStack>
- <StatusBar style="auto" />
- </View>
+ <GestureHandlerRootView style={{ flex: 1 }}>
+ <BottomSheetModalProvider>
+ <View
+ className={cn(
+ "w-full flex-1 bg-gray-100 text-foreground dark:bg-background",
+ colorScheme == "dark" ? "dark" : "light",
+ )}
+ >
+ <StyledStack
+ contentClassName="bg-gray-100 dark:bg-background"
+ screenOptions={{
+ headerShown: false,
+ }}
+ >
+ <Stack.Screen name="index" />
+ <Stack.Screen
+ name="sharing"
+ options={{
+ presentation: "modal",
+ }}
+ />
+ </StyledStack>
+ <StatusBar style="auto" />
+ </View>
+ </BottomSheetModalProvider>
+ </GestureHandlerRootView>
</Providers>
</ShareIntentProvider>
);
diff --git a/apps/mobile/app/sharing.tsx b/apps/mobile/app/sharing.tsx
index d1d39e5b..ee7bd609 100644
--- a/apps/mobile/app/sharing.tsx
+++ b/apps/mobile/app/sharing.tsx
@@ -1,10 +1,13 @@
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
import { useRouter } from "expo-router";
import { useShareIntentContext } from "expo-share-intent";
+import ListPickerModal from "@/components/bookmarks/ListPickerModal";
+import { Button } from "@/components/ui/Button";
import useAppSettings from "@/lib/settings";
import { api } from "@/lib/trpc";
import { useUploadAsset } from "@/lib/upload";
+import { BottomSheetModal } from "@gorhom/bottom-sheet";
import { z } from "zod";
import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks";
@@ -80,18 +83,39 @@ export default function Sharing() {
const router = useRouter();
const [mode, setMode] = useState<Mode>({ type: "idle" });
+ let autoCloseTimeoutId: NodeJS.Timeout | null = null;
+ const addToListSheetRef = useRef<BottomSheetModal>(null);
+
let comp;
switch (mode.type) {
case "idle": {
comp = <SaveBookmark setMode={setMode} />;
break;
}
+ case "alreadyExists":
case "success": {
- comp = <Text className="text-4xl text-foreground">Hoarded!</Text>;
- break;
- }
- case "alreadyExists": {
- comp = <Text className="text-4xl text-foreground">Already Hoarded!</Text>;
+ comp = (
+ <View className="items-center gap-4">
+ <ListPickerModal
+ ref={addToListSheetRef}
+ snapPoints={["90%"]}
+ bookmarkId={mode.bookmarkId}
+ onDismiss={() => router.replace("dashboard")}
+ />
+ <Text className="text-4xl text-foreground">
+ {mode.type === "alreadyExists" ? "Already Hoarded!" : "Hoarded!"}
+ </Text>
+ <Button
+ label="Add to List"
+ onPress={() => {
+ addToListSheetRef.current?.present();
+ if (autoCloseTimeoutId) {
+ clearTimeout(autoCloseTimeoutId);
+ }
+ }}
+ />
+ </View>
+ );
break;
}
case "error": {
@@ -106,11 +130,11 @@ export default function Sharing() {
return;
}
- const timeoutId = setTimeout(() => {
+ autoCloseTimeoutId = setTimeout(() => {
router.replace("dashboard");
}, 2000);
- return () => clearTimeout(timeoutId);
+ return () => clearTimeout(autoCloseTimeoutId!);
}, [mode.type]);
return (
diff --git a/apps/mobile/components/bookmarks/BookmarkCard.tsx b/apps/mobile/components/bookmarks/BookmarkCard.tsx
index 8faa8618..3be1f9a0 100644
--- a/apps/mobile/components/bookmarks/BookmarkCard.tsx
+++ b/apps/mobile/components/bookmarks/BookmarkCard.tsx
@@ -1,3 +1,4 @@
+import { useRef } from "react";
import {
ActivityIndicator,
Image,
@@ -13,6 +14,7 @@ import { Link } from "expo-router";
import * as WebBrowser from "expo-web-browser";
import useAppSettings from "@/lib/settings";
import { api } from "@/lib/trpc";
+import { BottomSheetModal } from "@gorhom/bottom-sheet";
import { MenuView } from "@react-native-menu/menu";
import { Ellipsis, Star } from "lucide-react-native";
@@ -32,6 +34,7 @@ import { TailwindResolver } from "../TailwindResolver";
import { Divider } from "../ui/Divider";
import { Skeleton } from "../ui/Skeleton";
import { useToast } from "../ui/Toast";
+import ListPickerModal from "./ListPickerModal";
function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
const { toast } = useToast();
@@ -70,6 +73,8 @@ function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
onError,
});
+ const manageListsSheetRef = useRef<BottomSheetModal>(null);
+
return (
<View className="flex flex-row gap-4">
{(isArchivePending || isDeletionPending) && <ActivityIndicator />}
@@ -89,6 +94,12 @@ function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
)}
</Pressable>
+ <ListPickerModal
+ ref={manageListsSheetRef}
+ snapPoints={["50%", "90%"]}
+ bookmarkId={bookmark.id}
+ />
+
<MenuView
onPressAction={({ nativeEvent }) => {
Haptics.selectionAsync();
@@ -101,6 +112,8 @@ function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
bookmarkId: bookmark.id,
archived: !bookmark.archived,
});
+ } else if (nativeEvent.event === "manage_list") {
+ manageListsSheetRef?.current?.present();
}
}}
actions={[
@@ -121,6 +134,13 @@ function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
ios: "trash",
}),
},
+ {
+ id: "manage_list",
+ title: "Manage Lists",
+ image: Platform.select({
+ ios: "list",
+ }),
+ },
]}
shouldOpenOnLongPress={false}
>
diff --git a/apps/mobile/components/bookmarks/ListPickerModal.tsx b/apps/mobile/components/bookmarks/ListPickerModal.tsx
new file mode 100644
index 00000000..6079e53d
--- /dev/null
+++ b/apps/mobile/components/bookmarks/ListPickerModal.tsx
@@ -0,0 +1,117 @@
+import React from "react";
+import { Pressable, Text, View } from "react-native";
+import Checkbox from "expo-checkbox";
+import {
+ BottomSheetFlatList,
+ BottomSheetModal,
+ BottomSheetModalProps,
+} from "@gorhom/bottom-sheet";
+
+import {
+ useAddBookmarkToList,
+ useBookmarkLists,
+ useRemoveBookmarkFromList,
+} from "@hoarder/shared-react/hooks/lists";
+import { api } from "@hoarder/shared-react/trpc";
+
+import PageTitle from "../ui/PageTitle";
+import { useToast } from "../ui/Toast";
+
+const ListPickerModal = React.forwardRef<
+ BottomSheetModal,
+ Omit<BottomSheetModalProps, "children"> & {
+ bookmarkId: string;
+ }
+>(({ bookmarkId, ...props }, ref) => {
+ const { toast } = useToast();
+ const onError = () => {
+ toast({
+ message: "Something went wrong",
+ variant: "destructive",
+ showProgress: false,
+ });
+ };
+ const { data: existingLists } = api.lists.getListsOfBookmark.useQuery(
+ {
+ bookmarkId,
+ },
+ {
+ select: (data) => new Set(data.lists.map((l) => l.id)),
+ },
+ );
+ const { data } = useBookmarkLists();
+
+ const { mutate: addToList } = useAddBookmarkToList({
+ onSuccess: () => {
+ toast({
+ message: `The bookmark has been added to the list!`,
+ showProgress: false,
+ });
+ },
+ onError,
+ });
+
+ const { mutate: removeToList } = useRemoveBookmarkFromList({
+ onSuccess: () => {
+ toast({
+ message: `The bookmark has been removed from the list!`,
+ showProgress: false,
+ });
+ },
+ onError,
+ });
+
+ const toggleList = (listId: string) => {
+ if (!existingLists) {
+ return;
+ }
+ if (existingLists.has(listId)) {
+ removeToList({ bookmarkId, listId });
+ } else {
+ addToList({ bookmarkId, listId });
+ }
+ };
+
+ const { allPaths } = data ?? {};
+ return (
+ <View>
+ <BottomSheetModal ref={ref} {...props}>
+ <BottomSheetFlatList
+ ListHeaderComponent={<PageTitle title="Manage Lists" />}
+ className="h-full"
+ contentContainerStyle={{
+ gap: 5,
+ }}
+ renderItem={(l) => (
+ <View className="mx-2 flex flex-row items-center rounded-xl border border-input bg-white px-4 py-2 dark:bg-accent">
+ <Pressable
+ key={l.item[l.item.length - 1].id}
+ onPress={() => toggleList(l.item[l.item.length - 1].id)}
+ className="flex w-full flex-row justify-between"
+ >
+ <Text className="text-lg text-accent-foreground">
+ {l.item
+ .map((item) => `${item.icon} ${item.name}`)
+ .join(" / ")}
+ </Text>
+ <Checkbox
+ value={
+ existingLists &&
+ existingLists.has(l.item[l.item.length - 1].id)
+ }
+ onValueChange={() => {
+ toggleList(l.item[l.item.length - 1].id);
+ }}
+ />
+ </Pressable>
+ </View>
+ )}
+ data={allPaths}
+ />
+ </BottomSheetModal>
+ </View>
+ );
+});
+ListPickerModal.displayName = "ListPickerModal";
+
+export default ListPickerModal;
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index ef19bfbf..da8ef432 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -13,15 +13,17 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
- "@hoarder/trpc": "workspace:^0.1.0",
+ "@gorhom/bottom-sheet": "^4.6.3",
"@hoarder/shared": "workspace:^0.1.0",
"@hoarder/shared-react": "workspace:^0.1.0",
+ "@hoarder/trpc": "workspace:^0.1.0",
"@react-native-menu/menu": "^0.9.1",
"@tanstack/react-query": "^5.24.8",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"expo": "~50.0.11",
"expo-build-properties": "^0.11.1",
+ "expo-checkbox": "^3.0.0",
"expo-config-plugin-ios-share-extension": "^0.0.4",
"expo-constants": "~15.4.5",
"expo-dev-client": "^3.3.9",
@@ -40,6 +42,7 @@
"nativewind": "^4.0.1",
"react": "^18.2.0",
"react-native": "0.73.4",
+ "react-native-gesture-handler": "~2.14.0",
"react-native-markdown-display": "^7.0.2",
"react-native-reanimated": "^3.8.0",
"react-native-safe-area-context": "4.8.2",