diff options
| author | Mohamed Bassem <me@mbassem.com> | 2024-11-23 20:59:34 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-11-23 20:59:34 +0000 |
| commit | 5522e20104da6afe2e4667cf45dbbbbc0e838865 (patch) | |
| tree | 72f416fa83c97a8533eea431e25bd63bda1e7d81 /apps/mobile/app/dashboard | |
| parent | 4bb74872fd518008afea16a136292037baf5b024 (diff) | |
| download | karakeep-5522e20104da6afe2e4667cf45dbbbbc0e838865.tar.zst | |
ui(mobile): Replace bottom sheet with native screens (#690)
* Remove bottom sheet from bookmark info page
* Remove bottom sheet from manage lists page
* Remove bottom sheet from new list page
* Remove bottom sheet from new bookmark page
* Drop bottom-sheets
* Improve the look of the modals
* Make the search page fade from bottom
Diffstat (limited to 'apps/mobile/app/dashboard')
| -rw-r--r-- | apps/mobile/app/dashboard/(tabs)/index.tsx | 10 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/(tabs)/lists.tsx | 10 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/_layout.tsx | 45 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/add-link.tsx | 64 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx (renamed from apps/mobile/app/dashboard/bookmarks/[slug].tsx) | 33 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx | 115 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx | 105 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/new.tsx | 76 | ||||
| -rw-r--r-- | apps/mobile/app/dashboard/lists/new.tsx | 56 |
9 files changed, 409 insertions, 105 deletions
diff --git a/apps/mobile/app/dashboard/(tabs)/index.tsx b/apps/mobile/app/dashboard/(tabs)/index.tsx index b9ab7d11..f70474a9 100644 --- a/apps/mobile/app/dashboard/(tabs)/index.tsx +++ b/apps/mobile/app/dashboard/(tabs)/index.tsx @@ -1,9 +1,7 @@ -import { useRef } from "react"; import { Platform, Pressable, Text, View } from "react-native"; import * as Haptics from "expo-haptics"; import * as ImagePicker from "expo-image-picker"; import { router } from "expo-router"; -import NoteEditorModal from "@/components/bookmarks/NewBookmarkModal"; import UpdatingBookmarkList from "@/components/bookmarks/UpdatingBookmarkList"; import { TailwindResolver } from "@/components/TailwindResolver"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; @@ -11,7 +9,6 @@ import PageTitle from "@/components/ui/PageTitle"; import { useToast } from "@/components/ui/Toast"; import useAppSettings from "@/lib/settings"; import { useUploadAsset } from "@/lib/upload"; -import { BottomSheetModal } from "@gorhom/bottom-sheet"; import { MenuView } from "@react-native-menu/menu"; import { Plus, Search } from "lucide-react-native"; @@ -77,11 +74,8 @@ function HeaderRight({ } export default function Home() { - const newBookmarkModal = useRef<BottomSheetModal>(null); - return ( <CustomSafeAreaView> - <NoteEditorModal ref={newBookmarkModal} snapPoints={["90%", "60%"]} /> <UpdatingBookmarkList query={{ archived: false }} header={ @@ -89,7 +83,9 @@ export default function Home() { <View className="flex flex-row justify-between"> <PageTitle title="Home" className="pb-2" /> <HeaderRight - openNewBookmarkModal={() => newBookmarkModal.current?.present()} + openNewBookmarkModal={() => + router.push("/dashboard/bookmarks/new") + } /> </View> <Pressable diff --git a/apps/mobile/app/dashboard/(tabs)/lists.tsx b/apps/mobile/app/dashboard/(tabs)/lists.tsx index fa97f67a..9cc49cd4 100644 --- a/apps/mobile/app/dashboard/(tabs)/lists.tsx +++ b/apps/mobile/app/dashboard/(tabs)/lists.tsx @@ -1,15 +1,13 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { FlatList, Pressable, Text, View } from "react-native"; import * as Haptics from "expo-haptics"; -import { Link } from "expo-router"; +import { Link, router } from "expo-router"; import FullPageError from "@/components/FullPageError"; -import NewListModal from "@/components/lists/NewListModal"; import { TailwindResolver } from "@/components/TailwindResolver"; import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import PageTitle from "@/components/ui/PageTitle"; import { api } from "@/lib/trpc"; -import { BottomSheetModal } from "@gorhom/bottom-sheet"; import { ChevronRight, Plus } from "lucide-react-native"; import { useBookmarkLists } from "@hoarder/shared-react/hooks/lists"; @@ -72,7 +70,6 @@ export default function Lists() { {}, ); const apiUtils = api.useUtils(); - const newListModal = useRef<BottomSheetModal>(null); useEffect(() => { setRefreshing(isPending); @@ -117,14 +114,13 @@ export default function Lists() { return ( <CustomSafeAreaView> - <NewListModal ref={newListModal} snapPoints={["90%"]} /> <FlatList className="h-full" ListHeaderComponent={ <View className="flex flex-row justify-between"> <PageTitle title="Lists" /> <HeaderRight - openNewListModal={() => newListModal.current?.present()} + openNewListModal={() => router.push("/dashboard/lists/new")} /> </View> } diff --git a/apps/mobile/app/dashboard/_layout.tsx b/apps/mobile/app/dashboard/_layout.tsx index 609f06f5..bc743c7a 100644 --- a/apps/mobile/app/dashboard/_layout.tsx +++ b/apps/mobile/app/dashboard/_layout.tsx @@ -50,6 +50,49 @@ export default function Dashboard() { }} /> <Stack.Screen + name="bookmarks/[slug]/index" + options={{ + headerTitle: "", + headerBackTitle: "Back", + headerTransparent: true, + }} + /> + <Stack.Screen + name="bookmarks/new" + options={{ + headerTitle: "New Bookmark", + headerBackTitle: "Back", + headerTransparent: true, + presentation: "modal", + }} + /> + <Stack.Screen + name="bookmarks/[slug]/manage_lists" + options={{ + headerTitle: "Manage Lists", + headerBackTitle: "Back", + headerTransparent: true, + presentation: "modal", + }} + /> + <Stack.Screen + name="bookmarks/[slug]/info" + options={{ + headerBackTitle: "Back", + headerTransparent: true, + presentation: "modal", + }} + /> + <Stack.Screen + name="lists/new" + options={{ + headerTitle: "New List", + headerBackTitle: "Back", + headerTransparent: true, + presentation: "modal", + }} + /> + <Stack.Screen name="archive" options={{ headerTitle: "", @@ -64,6 +107,8 @@ export default function Dashboard() { headerBackTitle: "", headerTransparent: true, headerShown: false, + animation: "fade_from_bottom", + animationDuration: 100, }} /> <Stack.Screen diff --git a/apps/mobile/app/dashboard/add-link.tsx b/apps/mobile/app/dashboard/add-link.tsx deleted file mode 100644 index d9773fb4..00000000 --- a/apps/mobile/app/dashboard/add-link.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useState } from "react"; -import { Text, View } from "react-native"; -import { useRouter } from "expo-router"; -import { Button } from "@/components/ui/Button"; -import { Input } from "@/components/ui/Input"; -import { useToast } from "@/components/ui/Toast"; -import { api } from "@/lib/trpc"; - -import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; - -export default function AddNote() { - const [text, setText] = useState(""); - const [error, setError] = useState<string | undefined>(); - const { toast } = useToast(); - const router = useRouter(); - const invalidateAllBookmarks = - api.useUtils().bookmarks.getBookmarks.invalidate; - - const { mutate } = api.bookmarks.createBookmark.useMutation({ - onSuccess: (resp) => { - if (resp.alreadyExists) { - toast({ - message: "Bookmark already exists", - }); - } - invalidateAllBookmarks(); - if (router.canGoBack()) { - router.replace("../"); - } else { - router.replace("dashboard"); - } - }, - onError: (e) => { - let message; - if (e.data?.zodError) { - const zodError = e.data.zodError; - message = JSON.stringify(zodError); - } else { - message = `Something went wrong: ${e.message}`; - } - setError(message); - }, - }); - - return ( - <View className="flex gap-2 p-4"> - {error && ( - <Text className="w-full text-center text-red-500">{error}</Text> - )} - <Input - value={text} - onChangeText={setText} - placeholder="Link" - autoCapitalize="none" - inputMode="url" - autoFocus - /> - <Button - onPress={() => mutate({ type: BookmarkTypes.LINK, url: text })} - label="Add Link" - /> - </View> - ); -} diff --git a/apps/mobile/app/dashboard/bookmarks/[slug].tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx index 9459488a..87330a88 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug].tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from "react"; +import React, { useState } from "react"; import { Alert, Keyboard, @@ -12,8 +12,6 @@ import WebView from "react-native-webview"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import BookmarkAssetImage from "@/components/bookmarks/BookmarkAssetImage"; import BookmarkTextMarkdown from "@/components/bookmarks/BookmarkTextMarkdown"; -import ListPickerModal from "@/components/bookmarks/ListPickerModal"; -import ViewBookmarkModal from "@/components/bookmarks/ViewBookmarkModal"; import FullPageError from "@/components/FullPageError"; import { TailwindResolver } from "@/components/TailwindResolver"; import { Button } from "@/components/ui/Button"; @@ -23,13 +21,7 @@ import { Input } from "@/components/ui/Input"; import { useToast } from "@/components/ui/Toast"; import { useAssetUrl } from "@/lib/hooks"; import { api } from "@/lib/trpc"; -import { BottomSheetModal } from "@gorhom/bottom-sheet"; -import { - ArrowUpFromLine, - ClipboardList, - Globe, - Trash2, -} from "lucide-react-native"; +import { ClipboardList, Globe, Info, Trash2 } from "lucide-react-native"; import { useDeleteBookmark, @@ -40,8 +32,6 @@ import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; function BottomActions({ bookmark }: { bookmark: ZBookmark }) { const { toast } = useToast(); const router = useRouter(); - const viewBookmarkModal = useRef<BottomSheetModal>(null); - const manageListsSheetRef = useRef<BottomSheetModal>(null); const { mutate: deleteBookmark, isPending: isDeletionPending } = useDeleteBookmark({ onSuccess: () => { @@ -84,7 +74,8 @@ function BottomActions({ bookmark }: { bookmark: ZBookmark }) { /> ), shouldRender: true, - onClick: () => manageListsSheetRef.current?.present(), + onClick: () => + router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`), disabled: false, }, { @@ -92,13 +83,11 @@ function BottomActions({ bookmark }: { bookmark: ZBookmark }) { icon: ( <TailwindResolver className="text-foreground" - comp={(styles) => ( - <ArrowUpFromLine color={styles?.color?.toString()} /> - )} + comp={(styles) => <Info color={styles?.color?.toString()} />} /> ), shouldRender: true, - onClick: () => viewBookmarkModal.current?.present(), + onClick: () => router.push(`/dashboard/bookmarks/${bookmark.id}/info`), disabled: false, }, { @@ -130,16 +119,6 @@ function BottomActions({ bookmark }: { bookmark: ZBookmark }) { ]; return ( <View> - <ViewBookmarkModal - bookmark={bookmark} - ref={viewBookmarkModal} - snapPoints={["95%"]} - /> - <ListPickerModal - ref={manageListsSheetRef} - snapPoints={["50%", "90%"]} - bookmarkId={bookmark.id} - /> <View className="flex flex-row items-center justify-between px-10 pb-2 pt-4"> {actions.map( (a) => diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx new file mode 100644 index 00000000..5d15ab6b --- /dev/null +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx @@ -0,0 +1,115 @@ +import React from "react"; +import { Text, View } from "react-native"; +import { Stack, useLocalSearchParams } from "expo-router"; +import TagPill from "@/components/bookmarks/TagPill"; +import FullPageError from "@/components/FullPageError"; +import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import FullPageSpinner from "@/components/ui/FullPageSpinner"; +import { Input } from "@/components/ui/Input"; +import { Skeleton } from "@/components/ui/Skeleton"; +import { api } from "@/lib/trpc"; + +import { useUpdateBookmark } from "@hoarder/shared-react/hooks/bookmarks"; +import { isBookmarkStillTagging } from "@hoarder/shared-react/utils/bookmarkUtils"; +import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; + +function TagList({ bookmark }: { bookmark: ZBookmark }) { + return ( + <View className="flex flex-row items-center gap-4"> + <Text className="text-foreground">Tags</Text> + {isBookmarkStillTagging(bookmark) ? ( + <> + <Skeleton className="h-4 w-full" /> + <Skeleton className="h-4 w-full" /> + </> + ) : bookmark.tags.length > 0 ? ( + <View className="flex flex-row flex-wrap gap-2"> + {bookmark.tags.map((t) => ( + <TagPill key={t.id} tag={t} /> + ))} + </View> + ) : ( + <Text className="text-foreground">No tags</Text> + )} + </View> + ); +} + +function NotesEditor({ bookmark }: { bookmark: ZBookmark }) { + const { mutate, isPending } = useUpdateBookmark(); + return ( + <View className="flex flex-row items-center gap-4"> + <Text className="text-foreground">Notes</Text> + + <Input + className="flex-1" + editable={!isPending} + multiline={true} + numberOfLines={3} + loading={isPending} + placeholder="Notes" + textAlignVertical="top" + onEndEditing={(ev) => + mutate({ + bookmarkId: bookmark.id, + note: ev.nativeEvent.text, + }) + } + defaultValue={bookmark.note ?? ""} + /> + </View> + ); +} + +const ViewBookmarkPage = () => { + const { slug } = useLocalSearchParams(); + if (typeof slug !== "string") { + throw new Error("Unexpected param type"); + } + const { + data: bookmark, + isPending, + refetch, + } = api.bookmarks.getBookmark.useQuery({ bookmarkId: slug }); + + if (isPending) { + return <FullPageSpinner />; + } + + if (!bookmark) { + return ( + <FullPageError error="Bookmark not found" onRetry={() => refetch()} /> + ); + } + + let title = null; + switch (bookmark.content.type) { + case BookmarkTypes.LINK: + title = bookmark.title ?? bookmark.content.title; + break; + case BookmarkTypes.TEXT: + title = bookmark.title; + break; + case BookmarkTypes.ASSET: + title = bookmark.title ?? bookmark.content.fileName; + break; + } + return ( + <CustomSafeAreaView> + <Stack.Screen + options={{ + headerShown: true, + headerTitle: title ?? "Untitled", + }} + /> + <View className="w-full p-4"> + <View className="gap-4 px-4"> + <TagList bookmark={bookmark} /> + <NotesEditor bookmark={bookmark} /> + </View> + </View> + </CustomSafeAreaView> + ); +}; + +export default ViewBookmarkPage; diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx new file mode 100644 index 00000000..b38261df --- /dev/null +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx @@ -0,0 +1,105 @@ +import React from "react"; +import { FlatList, Pressable, Text, View } from "react-native"; +import Checkbox from "expo-checkbox"; +import { useLocalSearchParams } from "expo-router"; +import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import { useToast } from "@/components/ui/Toast"; + +import { + useAddBookmarkToList, + useBookmarkLists, + useRemoveBookmarkFromList, +} from "@hoarder/shared-react/hooks/lists"; +import { api } from "@hoarder/shared-react/trpc"; + +const ListPickerPage = () => { + const { slug: bookmarkId } = useLocalSearchParams(); + if (typeof bookmarkId !== "string") { + throw new Error("Unexpected param type"); + } + 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 ( + <CustomSafeAreaView> + <FlatList + 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} + /> + </CustomSafeAreaView> + ); +}; + +export default ListPickerPage; diff --git a/apps/mobile/app/dashboard/bookmarks/new.tsx b/apps/mobile/app/dashboard/bookmarks/new.tsx new file mode 100644 index 00000000..06a16a40 --- /dev/null +++ b/apps/mobile/app/dashboard/bookmarks/new.tsx @@ -0,0 +1,76 @@ +import React, { useState } from "react"; +import { Text, View } from "react-native"; +import { router } from "expo-router"; +import { Button } from "@/components/ui/Button"; +import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import { Input } from "@/components/ui/Input"; +import { useToast } from "@/components/ui/Toast"; + +import { useCreateBookmark } from "@hoarder/shared-react/hooks/bookmarks"; +import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; + +const NoteEditorPage = () => { + const dismiss = () => { + router.back(); + }; + + const [text, setText] = useState(""); + const [error, setError] = useState<string | undefined>(); + const { toast } = useToast(); + + const { mutate: createBookmark } = useCreateBookmark({ + onSuccess: (resp) => { + if (resp.alreadyExists) { + toast({ + message: "Bookmark already exists", + }); + } + setText(""); + dismiss(); + }, + onError: (e) => { + let message; + if (e.data?.zodError) { + const zodError = e.data.zodError; + message = JSON.stringify(zodError); + } else { + message = `Something went wrong: ${e.message}`; + } + setError(message); + }, + }); + + const onSubmit = () => { + const data = text.trim(); + try { + const url = new URL(data); + if (url.protocol != "http:" && url.protocol != "https:") { + throw new Error(`Unsupported URL protocol: ${url.protocol}`); + } + createBookmark({ type: BookmarkTypes.LINK, url: data }); + } catch (e: unknown) { + createBookmark({ type: BookmarkTypes.TEXT, text: data }); + } + }; + + return ( + <CustomSafeAreaView> + <View className="gap-2 px-4"> + {error && ( + <Text className="w-full text-center text-red-500">{error}</Text> + )} + <Input + onChangeText={setText} + multiline + placeholder="What's on your mind?" + autoFocus + autoCapitalize={"none"} + textAlignVertical="top" + /> + <Button onPress={onSubmit} label="Save" /> + </View> + </CustomSafeAreaView> + ); +}; + +export default NoteEditorPage; diff --git a/apps/mobile/app/dashboard/lists/new.tsx b/apps/mobile/app/dashboard/lists/new.tsx new file mode 100644 index 00000000..998638aa --- /dev/null +++ b/apps/mobile/app/dashboard/lists/new.tsx @@ -0,0 +1,56 @@ +import React, { useState } from "react"; +import { Text, View } from "react-native"; +import { router } from "expo-router"; +import { Button } from "@/components/ui/Button"; +import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import { Input } from "@/components/ui/Input"; +import { useToast } from "@/components/ui/Toast"; + +import { useCreateBookmarkList } from "@hoarder/shared-react/hooks/lists"; + +const NewListPage = () => { + const dismiss = () => { + router.back(); + }; + const { toast } = useToast(); + const [text, setText] = useState(""); + + const { mutate, isPending } = useCreateBookmarkList({ + onSuccess: () => { + dismiss(); + }, + onError: () => { + toast({ + message: "Something went wrong", + variant: "destructive", + }); + }, + }); + + const onSubmit = () => { + mutate({ + name: text, + icon: "🚀", + }); + }; + + return ( + <CustomSafeAreaView> + <View className="gap-2 px-4"> + <View className="flex flex-row items-center gap-1"> + <Text className="shrink p-2">🚀</Text> + <Input + className="flex-1" + onChangeText={setText} + placeholder="List Name" + autoFocus + autoCapitalize={"none"} + /> + </View> + <Button disabled={isPending} onPress={onSubmit} label="Save" /> + </View> + </CustomSafeAreaView> + ); +}; + +export default NewListPage; |
