diff options
| author | MohamedBassem <me@mbassem.com> | 2024-10-21 15:23:46 +0100 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-10-21 15:23:46 +0100 |
| commit | 2ce42a8978163470b33085bbfd93172ce01a8d69 (patch) | |
| tree | 9c2ad3222ab3261a1773f20edca6f9d2755ee9b8 /apps | |
| parent | 019b5d2f5ea0a78cb6c44be26b1eba60b2a4e88d (diff) | |
| download | karakeep-2ce42a8978163470b33085bbfd93172ce01a8d69.tar.zst | |
feature(mobile): Use inline WebView for expanding bookmarks
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/[slug].tsx | 293 | ||||
| -rw-r--r-- | apps/mobile/components/bookmarks/BookmarkCard.tsx | 21 | ||||
| -rw-r--r-- | apps/mobile/components/bookmarks/ViewBookmarkModal.tsx | 144 | ||||
| -rw-r--r-- | apps/mobile/package.json | 1 |
4 files changed, 308 insertions, 151 deletions
diff --git a/apps/mobile/app/dashboard/bookmarks/[slug].tsx b/apps/mobile/app/dashboard/bookmarks/[slug].tsx new file mode 100644 index 00000000..bb6570a7 --- /dev/null +++ b/apps/mobile/app/dashboard/bookmarks/[slug].tsx @@ -0,0 +1,293 @@ +import React, { useRef, useState } from "react"; +import { + Alert, + Keyboard, + Linking, + Pressable, + ScrollView, + View, +} from "react-native"; +import ImageView from "react-native-image-viewing"; +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 { Button } from "@/components/ui/Button"; +import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import FullPageSpinner from "@/components/ui/FullPageSpinner"; +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 { + useDeleteBookmark, + useUpdateBookmarkText, +} from "@hoarder/shared-react/hooks/bookmarks"; +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: () => { + router.back(); + toast({ + message: "The bookmark has been deleted!", + showProgress: false, + }); + }, + onError: () => { + toast({ + message: "Something went wrong", + variant: "destructive", + showProgress: false, + }); + }, + }); + + const deleteBookmarkAlert = () => + Alert.alert( + "Delete bookmark?", + "Are you sure you want to delete this bookmark?", + [ + { text: "Cancel", style: "cancel" }, + { + text: "Delete", + onPress: () => deleteBookmark({ bookmarkId: bookmark.id }), + style: "destructive", + }, + ], + ); + const actions = [ + { + id: "lists", + icon: <ClipboardList />, + shouldRender: true, + onClick: () => manageListsSheetRef.current?.present(), + disabled: false, + }, + { + id: "open", + icon: <ArrowUpFromLine />, + shouldRender: true, + onClick: () => viewBookmarkModal.current?.present(), + disabled: false, + }, + { + id: "delete", + icon: <Trash2 />, + shouldRender: true, + onClick: deleteBookmarkAlert, + disabled: isDeletionPending, + }, + { + id: "browser", + icon: <Globe />, + shouldRender: bookmark.content.type == BookmarkTypes.LINK, + onClick: () => + bookmark.content.type == BookmarkTypes.LINK && + Linking.openURL(bookmark.content.url), + disabled: false, + }, + ]; + 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) => + a.shouldRender && ( + <Pressable + disabled={a.disabled} + key={a.id} + onPress={a.onClick} + className="py-auto" + > + {a.icon} + </Pressable> + ), + )} + </View> + </View> + ); +} + +function BookmarkLinkView({ bookmark }: { bookmark: ZBookmark }) { + if (bookmark.content.type !== BookmarkTypes.LINK) { + throw new Error("Wrong content type rendered"); + } + return ( + <WebView + startInLoadingState={true} + mediaPlaybackRequiresUserAction={true} + source={{ uri: bookmark.content.url }} + /> + ); +} + +function BookmarkTextView({ bookmark }: { bookmark: ZBookmark }) { + if (bookmark.content.type !== BookmarkTypes.TEXT) { + throw new Error("Wrong content type rendered"); + } + const { toast } = useToast(); + + const [isEditing, setIsEditing] = useState(false); + const initialText = bookmark.content.text; + const [content, setContent] = useState(initialText); + + const { mutate, isPending } = useUpdateBookmarkText({ + onError: () => { + toast({ + message: "Something went wrong", + variant: "destructive", + }); + }, + onSuccess: () => { + setIsEditing(false); + }, + }); + + return ( + <View className="flex-1"> + {isEditing && ( + <View className="absolute right-0 top-0 z-10 m-4 flex flex-row gap-1"> + <Button label="Save" variant="default" onPress={Keyboard.dismiss} /> + <Button + label="Discard" + variant="destructive" + onPress={() => { + setContent(initialText); + setIsEditing(false); + }} + /> + </View> + )} + <ScrollView className="flex bg-background p-2"> + {isEditing ? ( + <Input + loading={isPending} + editable={!isPending} + onBlur={() => + mutate({ + bookmarkId: bookmark.id, + text: content, + }) + } + value={content} + onChangeText={setContent} + multiline + autoFocus + /> + ) : ( + <Pressable onPress={() => setIsEditing(true)}> + <View className="mb-4 rounded-xl border border-accent p-2"> + <BookmarkTextMarkdown text={content} /> + </View> + </Pressable> + )} + </ScrollView> + </View> + ); +} + +function BookmarkAssetView({ bookmark }: { bookmark: ZBookmark }) { + const [imageZoom, setImageZoom] = useState(false); + if (bookmark.content.type !== BookmarkTypes.ASSET) { + throw new Error("Wrong content type rendered"); + } + const assetSource = useAssetUrl(bookmark.content.assetId); + return ( + <View className="flex flex-1 gap-2"> + <ImageView + visible={imageZoom} + imageIndex={0} + onRequestClose={() => setImageZoom(false)} + doubleTapToZoomEnabled={true} + images={[assetSource]} + /> + + <Pressable onPress={() => setImageZoom(true)}> + <BookmarkAssetImage + assetId={bookmark.content.assetId} + className="h-56 min-h-56 w-full object-cover" + /> + </Pressable> + </View> + ); +} + +export default function ListView() { + const { slug } = useLocalSearchParams(); + if (typeof slug !== "string") { + throw new Error("Unexpected param type"); + } + + const { + data: bookmark, + error, + refetch, + } = api.bookmarks.getBookmark.useQuery({ bookmarkId: slug }); + + if (error) { + return <FullPageError error={error.message} onRetry={refetch} />; + } + + if (!bookmark) { + return <FullPageSpinner />; + } + + let comp; + let title = null; + switch (bookmark.content.type) { + case BookmarkTypes.LINK: + title = bookmark.title ?? bookmark.content.title; + comp = <BookmarkLinkView bookmark={bookmark} />; + break; + case BookmarkTypes.TEXT: + title = bookmark.title; + comp = <BookmarkTextView bookmark={bookmark} />; + break; + case BookmarkTypes.ASSET: + title = bookmark.title ?? bookmark.content.fileName; + comp = <BookmarkAssetView bookmark={bookmark} />; + break; + } + return ( + <CustomSafeAreaView> + <Stack.Screen + options={{ + headerTitle: title ?? "", + headerBackTitle: "Back", + headerTransparent: false, + }} + /> + <View className="flex h-full"> + {comp} + <BottomActions bookmark={bookmark} /> + </View> + </CustomSafeAreaView> + ); +} diff --git a/apps/mobile/components/bookmarks/BookmarkCard.tsx b/apps/mobile/components/bookmarks/BookmarkCard.tsx index 14eb3cf3..5d84ee6f 100644 --- a/apps/mobile/components/bookmarks/BookmarkCard.tsx +++ b/apps/mobile/components/bookmarks/BookmarkCard.tsx @@ -9,6 +9,7 @@ import { View, } from "react-native"; import * as Haptics from "expo-haptics"; +import { useRouter } from "expo-router"; import useAppSettings from "@/lib/settings"; import { api } from "@/lib/trpc"; import { BottomSheetModal } from "@gorhom/bottom-sheet"; @@ -34,7 +35,6 @@ import BookmarkAssetImage from "./BookmarkAssetImage"; import BookmarkTextMarkdown from "./BookmarkTextMarkdown"; import ListPickerModal from "./ListPickerModal"; import TagPill from "./TagPill"; -import ViewBookmarkModal from "./ViewBookmarkModal"; function ActionBar({ bookmark }: { bookmark: ZBookmark }) { const { toast } = useToast(); @@ -341,7 +341,7 @@ export default function BookmarkCard({ }, ); - const viewBookmarkModal = useRef<BottomSheetModal>(null); + const router = useRouter(); let comp; switch (bookmark.content.type) { @@ -349,7 +349,9 @@ export default function BookmarkCard({ comp = ( <LinkCard bookmark={bookmark} - onOpenBookmark={() => viewBookmarkModal.current?.present()} + onOpenBookmark={() => + router.push(`/dashboard/bookmarks/${bookmark.id}`) + } /> ); break; @@ -357,7 +359,9 @@ export default function BookmarkCard({ comp = ( <TextCard bookmark={bookmark} - onOpenBookmark={() => viewBookmarkModal.current?.present()} + onOpenBookmark={() => + router.push(`/dashboard/bookmarks/${bookmark.id}`) + } /> ); break; @@ -365,7 +369,9 @@ export default function BookmarkCard({ comp = ( <AssetCard bookmark={bookmark} - onOpenBookmark={() => viewBookmarkModal.current?.present()} + onOpenBookmark={() => + router.push(`/dashboard/bookmarks/${bookmark.id}`) + } /> ); break; @@ -373,11 +379,6 @@ export default function BookmarkCard({ return ( <View className="overflow-hidden rounded-xl border-b border-accent bg-background"> - <ViewBookmarkModal - bookmark={bookmark} - ref={viewBookmarkModal} - snapPoints={["95%"]} - /> {comp} </View> ); diff --git a/apps/mobile/components/bookmarks/ViewBookmarkModal.tsx b/apps/mobile/components/bookmarks/ViewBookmarkModal.tsx index 059b990e..df513a89 100644 --- a/apps/mobile/components/bookmarks/ViewBookmarkModal.tsx +++ b/apps/mobile/components/bookmarks/ViewBookmarkModal.tsx @@ -1,9 +1,5 @@ -import React, { useState } from "react"; -import { Keyboard, Pressable, Text } from "react-native"; -import ImageView from "react-native-image-viewing"; -import * as WebBrowser from "expo-web-browser"; -import { useAssetUrl } from "@/lib/hooks"; -import { cn } from "@/lib/utils"; +import React from "react"; +import { Keyboard, Text } from "react-native"; import { BottomSheetBackdrop, BottomSheetModal, @@ -12,23 +8,14 @@ import { BottomSheetView, TouchableWithoutFeedback, } from "@gorhom/bottom-sheet"; -import { ExternalLink } from "lucide-react-native"; -import { - useUpdateBookmark, - useUpdateBookmarkText, -} from "@hoarder/shared-react/hooks/bookmarks"; +import { useUpdateBookmark } from "@hoarder/shared-react/hooks/bookmarks"; import { isBookmarkStillTagging } from "@hoarder/shared-react/utils/bookmarkUtils"; import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; -import { TailwindResolver } from "../TailwindResolver"; -import { buttonVariants } from "../ui/Button"; import { Input } from "../ui/Input"; import PageTitle from "../ui/PageTitle"; import { Skeleton } from "../ui/Skeleton"; -import { useToast } from "../ui/Toast"; -import BookmarkAssetImage from "./BookmarkAssetImage"; -import BookmarkTextMarkdown from "./BookmarkTextMarkdown"; import TagPill from "./TagPill"; function TagList({ bookmark }: { bookmark: ZBookmark }) { @@ -79,126 +66,6 @@ function NotesEditor({ bookmark }: { bookmark: ZBookmark }) { ); } -function BookmarkLinkView({ bookmark }: { bookmark: ZBookmark }) { - const [imageZoom, setImageZoom] = useState(false); - if (bookmark.content.type !== BookmarkTypes.LINK) { - throw new Error("Wrong content type rendered"); - } - const url = new URL(bookmark.content.url); - - const imageAssetId = - bookmark.content.imageAssetId ?? bookmark.content.screenshotAssetId ?? ""; - const assetSource = useAssetUrl(imageAssetId); - return ( - <BottomSheetView className="flex gap-2"> - <Pressable - className={cn( - buttonVariants({ variant: "default" }), - "flex w-fit flex-row items-center gap-2", - )} - onPress={() => WebBrowser.openBrowserAsync(url.toString())} - > - <Text className="text-background">{url.host}</Text> - <TailwindResolver - className="color-background" - comp={(styles) => ( - <ExternalLink size={20} color={styles?.color?.toString()} /> - )} - /> - </Pressable> - <ImageView - visible={imageZoom} - imageIndex={0} - onRequestClose={() => setImageZoom(false)} - doubleTapToZoomEnabled={true} - images={[assetSource]} - /> - - <Pressable onPress={() => setImageZoom(true)}> - <BookmarkAssetImage - assetId={imageAssetId} - className="h-56 min-h-56 w-full object-cover" - /> - </Pressable> - </BottomSheetView> - ); -} - -function BookmarkTextView({ bookmark }: { bookmark: ZBookmark }) { - if (bookmark.content.type !== BookmarkTypes.TEXT) { - throw new Error("Wrong content type rendered"); - } - const { toast } = useToast(); - - const [isEditing, setIsEditing] = useState(false); - const [content, setContent] = useState(bookmark.content.text); - - const { mutate, isPending } = useUpdateBookmarkText({ - onError: () => { - toast({ - message: "Something went wrong", - variant: "destructive", - }); - }, - onSuccess: () => { - setIsEditing(false); - }, - }); - - return ( - <BottomSheetView> - {isEditing ? ( - <Input - loading={isPending} - editable={!isPending} - onBlur={() => - mutate({ - bookmarkId: bookmark.id, - text: content, - }) - } - value={content} - onChangeText={setContent} - multiline - autoFocus - /> - ) : ( - <Pressable onPress={() => setIsEditing(true)}> - <BottomSheetView className="rounded-xl border border-accent p-2"> - <BookmarkTextMarkdown text={content} /> - </BottomSheetView> - </Pressable> - )} - </BottomSheetView> - ); -} - -function BookmarkAssetView({ bookmark }: { bookmark: ZBookmark }) { - const [imageZoom, setImageZoom] = useState(false); - if (bookmark.content.type !== BookmarkTypes.ASSET) { - throw new Error("Wrong content type rendered"); - } - const assetSource = useAssetUrl(bookmark.content.assetId); - return ( - <BottomSheetView className="flex gap-2"> - <ImageView - visible={imageZoom} - imageIndex={0} - onRequestClose={() => setImageZoom(false)} - doubleTapToZoomEnabled={true} - images={[assetSource]} - /> - - <Pressable onPress={() => setImageZoom(true)}> - <BookmarkAssetImage - assetId={bookmark.content.assetId} - className="h-56 min-h-56 w-full object-cover" - /> - </Pressable> - </BottomSheetView> - ); -} - const ViewBookmarkModal = React.forwardRef< BottomSheetModal, Omit< @@ -208,20 +75,16 @@ const ViewBookmarkModal = React.forwardRef< bookmark: ZBookmark; } >(({ bookmark, ...props }, ref) => { - let comp; let title = null; switch (bookmark.content.type) { case BookmarkTypes.LINK: title = bookmark.title ?? bookmark.content.title; - comp = <BookmarkLinkView bookmark={bookmark} />; break; case BookmarkTypes.TEXT: title = bookmark.title; - comp = <BookmarkTextView bookmark={bookmark} />; break; case BookmarkTypes.ASSET: title = bookmark.title ?? bookmark.content.fileName; - comp = <BookmarkAssetView bookmark={bookmark} />; break; } return ( @@ -241,7 +104,6 @@ const ViewBookmarkModal = React.forwardRef< <BottomSheetView className="flex flex-1"> <PageTitle title={title ?? "Untitled"} className="line-clamp-2" /> <BottomSheetView className="gap-4 px-4"> - {comp} <TagList bookmark={bookmark} /> <NotesEditor bookmark={bookmark} /> </BottomSheetView> diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 0383fc96..3a1322e1 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -51,6 +51,7 @@ "react-native-safe-area-context": "4.8.2", "react-native-screens": "~3.29.0", "react-native-svg": "^15.1.0", + "react-native-webview": "^13.12.3", "tailwind-merge": "^2.2.1", "use-debounce": "^10.0.0", "zod": "^3.22.4", |
