aboutsummaryrefslogtreecommitdiffstats
path: root/apps/mobile
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-10-21 15:23:46 +0100
committerMohamedBassem <me@mbassem.com>2024-10-21 15:23:46 +0100
commit2ce42a8978163470b33085bbfd93172ce01a8d69 (patch)
tree9c2ad3222ab3261a1773f20edca6f9d2755ee9b8 /apps/mobile
parent019b5d2f5ea0a78cb6c44be26b1eba60b2a4e88d (diff)
downloadkarakeep-2ce42a8978163470b33085bbfd93172ce01a8d69.tar.zst
feature(mobile): Use inline WebView for expanding bookmarks
Diffstat (limited to 'apps/mobile')
-rw-r--r--apps/mobile/app/dashboard/bookmarks/[slug].tsx293
-rw-r--r--apps/mobile/components/bookmarks/BookmarkCard.tsx21
-rw-r--r--apps/mobile/components/bookmarks/ViewBookmarkModal.tsx144
-rw-r--r--apps/mobile/package.json1
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",