aboutsummaryrefslogtreecommitdiffstats
path: root/apps/mobile/components/bookmarks
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-08-26 13:41:13 +0300
committerMohamedBassem <me@mbassem.com>2024-08-26 16:17:35 +0300
commitb094b2cecb0da1bcdf4c63dd081638d87793c53c (patch)
tree332db48228a83a43e3336aa8e818c70e58b4f85d /apps/mobile/components/bookmarks
parent8410a6d3c125cf27daa4e3abeb4c4a4d228e2cfd (diff)
downloadkarakeep-b094b2cecb0da1bcdf4c63dd081638d87793c53c.tar.zst
feature(mobile): Change the view bookmark page to be a modal and add tags and
notes
Diffstat (limited to 'apps/mobile/components/bookmarks')
-rw-r--r--apps/mobile/components/bookmarks/BookmarkCard.tsx100
-rw-r--r--apps/mobile/components/bookmarks/NewBookmarkModal.tsx71
-rw-r--r--apps/mobile/components/bookmarks/TagPill.tsx17
-rw-r--r--apps/mobile/components/bookmarks/ViewBookmarkModal.tsx192
4 files changed, 273 insertions, 107 deletions
diff --git a/apps/mobile/components/bookmarks/BookmarkCard.tsx b/apps/mobile/components/bookmarks/BookmarkCard.tsx
index 8c582d59..0b787372 100644
--- a/apps/mobile/components/bookmarks/BookmarkCard.tsx
+++ b/apps/mobile/components/bookmarks/BookmarkCard.tsx
@@ -9,7 +9,6 @@ import {
View,
} from "react-native";
import * as Haptics from "expo-haptics";
-import { Link, router } from "expo-router";
import * as WebBrowser from "expo-web-browser";
import useAppSettings from "@/lib/settings";
import { api } from "@/lib/trpc";
@@ -35,7 +34,8 @@ import { useToast } from "../ui/Toast";
import BookmarkAssetImage from "./BookmarkAssetImage";
import BookmarkTextMarkdown from "./BookmarkTextMarkdown";
import ListPickerModal from "./ListPickerModal";
-import NoteEditorModal from "./NewBookmarkModal";
+import TagPill from "./TagPill";
+import ViewBookmarkModal from "./ViewBookmarkModal";
function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
const { toast } = useToast();
@@ -75,7 +75,6 @@ function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
});
const manageListsSheetRef = useRef<BottomSheetModal>(null);
- const editBookmarkModal = useRef<BottomSheetModal>(null);
return (
<View className="flex flex-row gap-4">
@@ -101,13 +100,6 @@ function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
snapPoints={["50%", "90%"]}
bookmarkId={bookmark.id}
/>
- {bookmark.content.type === BookmarkTypes.TEXT && (
- <NoteEditorModal
- ref={editBookmarkModal}
- bookmark={bookmark}
- snapPoints={["90%", "60%"]}
- />
- )}
<MenuView
onPressAction={({ nativeEvent }) => {
@@ -123,22 +115,10 @@ function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
});
} else if (nativeEvent.event === "manage_list") {
manageListsSheetRef?.current?.present();
- } else if (nativeEvent.event === "edit") {
- editBookmarkModal.current?.present();
}
}}
actions={[
{
- id: "edit",
- title: "Edit",
- image: Platform.select({
- ios: "edit",
- }),
- attributes: {
- hidden: bookmark.content.type !== BookmarkTypes.TEXT,
- },
- },
- {
id: "archive",
title: bookmark.archived ? "Un-archive" : "Archive",
image: Platform.select({
@@ -187,21 +167,19 @@ function TagList({ bookmark }: { bookmark: ZBookmark }) {
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View className="flex flex-row gap-2">
{tags.map((t) => (
- <View
- key={t.id}
- className="rounded-full border border-accent px-2.5 py-0.5 text-xs font-semibold"
- >
- <Link className="text-foreground" href={`dashboard/tags/${t.id}`}>
- {t.name}
- </Link>
- </View>
+ <TagPill key={t.id} tag={t} />
))}
</View>
</ScrollView>
);
}
-function LinkCard({ bookmark }: { bookmark: ZBookmark }) {
+function LinkCard({
+ bookmark,
+}: {
+ bookmark: ZBookmark;
+ onOpenBookmark: () => void;
+}) {
const { settings } = useAppSettings();
if (bookmark.content.type !== BookmarkTypes.LINK) {
throw new Error("Wrong content type rendered");
@@ -264,16 +242,20 @@ function LinkCard({ bookmark }: { bookmark: ZBookmark }) {
);
}
-function TextCard({ bookmark }: { bookmark: ZBookmark }) {
+function TextCard({
+ bookmark,
+ onOpenBookmark,
+}: {
+ bookmark: ZBookmark;
+ onOpenBookmark: () => void;
+}) {
if (bookmark.content.type !== BookmarkTypes.TEXT) {
throw new Error("Wrong content type rendered");
}
const content = bookmark.content.text;
return (
<View className="flex max-h-96 gap-2 p-2">
- <Pressable
- onPress={() => router.push(`/dashboard/bookmarks/${bookmark.id}`)}
- >
+ <Pressable onPress={onOpenBookmark}>
{bookmark.title && (
<Text className="line-clamp-2 text-xl font-bold text-foreground">
{bookmark.title}
@@ -281,9 +263,7 @@ function TextCard({ bookmark }: { bookmark: ZBookmark }) {
)}
</Pressable>
<View className="max-h-56 overflow-hidden p-2 text-foreground">
- <Pressable
- onPress={() => router.push(`/dashboard/bookmarks/${bookmark.id}`)}
- >
+ <Pressable onPress={onOpenBookmark}>
<BookmarkTextMarkdown text={content} />
</Pressable>
</View>
@@ -297,7 +277,13 @@ function TextCard({ bookmark }: { bookmark: ZBookmark }) {
);
}
-function AssetCard({ bookmark }: { bookmark: ZBookmark }) {
+function AssetCard({
+ bookmark,
+ onOpenBookmark,
+}: {
+ bookmark: ZBookmark;
+ onOpenBookmark: () => void;
+}) {
if (bookmark.content.type !== BookmarkTypes.ASSET) {
throw new Error("Wrong content type rendered");
}
@@ -305,18 +291,14 @@ function AssetCard({ bookmark }: { bookmark: ZBookmark }) {
return (
<View className="flex gap-2">
- <Pressable
- onPress={() => router.push(`/dashboard/bookmarks/${bookmark.id}`)}
- >
+ <Pressable onPress={onOpenBookmark}>
<BookmarkAssetImage
assetId={bookmark.content.assetId}
className="h-56 min-h-56 w-full object-cover"
/>
</Pressable>
<View className="flex gap-2 p-2">
- <Pressable
- onPress={() => router.push(`/dashboard/bookmarks/${bookmark.id}`)}
- >
+ <Pressable onPress={onOpenBookmark}>
{title && (
<Text className="line-clamp-2 text-xl font-bold text-foreground">
{title}
@@ -359,21 +341,43 @@ export default function BookmarkCard({
},
);
+ const viewBookmarkModal = useRef<BottomSheetModal>(null);
+
let comp;
switch (bookmark.content.type) {
case BookmarkTypes.LINK:
- comp = <LinkCard bookmark={bookmark} />;
+ comp = (
+ <LinkCard
+ bookmark={bookmark}
+ onOpenBookmark={() => viewBookmarkModal.current?.present()}
+ />
+ );
break;
case BookmarkTypes.TEXT:
- comp = <TextCard bookmark={bookmark} />;
+ comp = (
+ <TextCard
+ bookmark={bookmark}
+ onOpenBookmark={() => viewBookmarkModal.current?.present()}
+ />
+ );
break;
case BookmarkTypes.ASSET:
- comp = <AssetCard bookmark={bookmark} />;
+ comp = (
+ <AssetCard
+ bookmark={bookmark}
+ onOpenBookmark={() => viewBookmarkModal.current?.present()}
+ />
+ );
break;
}
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/NewBookmarkModal.tsx b/apps/mobile/components/bookmarks/NewBookmarkModal.tsx
index 8ac8bb39..6915c663 100644
--- a/apps/mobile/components/bookmarks/NewBookmarkModal.tsx
+++ b/apps/mobile/components/bookmarks/NewBookmarkModal.tsx
@@ -1,51 +1,31 @@
-import React, { useEffect, useState } from "react";
+import React, { useState } from "react";
import { Text, View } from "react-native";
import {
BottomSheetBackdrop,
BottomSheetModal,
BottomSheetModalProps,
- BottomSheetTextInput,
BottomSheetView,
useBottomSheetModal,
} from "@gorhom/bottom-sheet";
-import {
- useCreateBookmark,
- useUpdateBookmarkText,
-} from "@hoarder/shared-react/hooks/bookmarks";
-import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks";
+import { useCreateBookmark } from "@hoarder/shared-react/hooks/bookmarks";
+import { BookmarkTypes } from "@hoarder/shared/types/bookmarks";
import { Button } from "../ui/Button";
+import { Input } from "../ui/Input";
import PageTitle from "../ui/PageTitle";
const NoteEditorModal = React.forwardRef<
BottomSheetModal,
- Omit<
- BottomSheetModalProps,
- "children" | "backdropComponent" | "onDismiss"
- > & {
- bookmark?: ZBookmark;
- }
->(({ bookmark, ...props }, ref) => {
+ Omit<BottomSheetModalProps, "children" | "backdropComponent" | "onDismiss">
+>(({ ...props }, ref) => {
const { dismiss } = useBottomSheetModal();
- const isEditing = !!bookmark;
const [text, setText] = useState("");
const [error, setError] = useState<string | undefined>();
- const resetText = () => {
- if (bookmark) {
- if (bookmark.content.type !== BookmarkTypes.TEXT) {
- throw new Error("Wrong content type rendered");
- }
- setText(bookmark.content.text);
- }
- };
-
- useEffect(resetText, []);
-
const onSuccess = () => {
- resetText();
+ setText("");
dismiss();
};
@@ -63,31 +43,6 @@ const NoteEditorModal = React.forwardRef<
},
});
- const { mutate: updateBookmark } = useUpdateBookmarkText({
- onSuccess,
- 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 mutate = (text: string) => {
- if (isEditing) {
- updateBookmark({
- bookmarkId: bookmark.id,
- text,
- });
- } else {
- createBookmark({ type: BookmarkTypes.TEXT, text });
- }
- };
-
return (
<View>
<BottomSheetModal
@@ -101,23 +56,21 @@ const NoteEditorModal = React.forwardRef<
)}
{...props}
>
- <PageTitle title={isEditing ? "Edit Note" : "New Note"} />
- <BottomSheetView className="p-4">
+ <PageTitle title="New Note" />
+ <BottomSheetView className="gap-2 p-4">
{error && (
<Text className="w-full text-center text-red-500">{error}</Text>
)}
- <BottomSheetTextInput
- value={text}
+ <Input
onChangeText={setText}
multiline
- numberOfLines={8}
placeholder="What's on your mind?"
autoFocus
textAlignVertical="top"
/>
<Button
- onPress={() => mutate(text)}
- label={isEditing ? "Save" : "Add Note"}
+ onPress={() => createBookmark({ type: BookmarkTypes.TEXT, text })}
+ label="Add Note"
/>
</BottomSheetView>
</BottomSheetModal>
diff --git a/apps/mobile/components/bookmarks/TagPill.tsx b/apps/mobile/components/bookmarks/TagPill.tsx
new file mode 100644
index 00000000..04e01730
--- /dev/null
+++ b/apps/mobile/components/bookmarks/TagPill.tsx
@@ -0,0 +1,17 @@
+import { View } from "react-native";
+import { Link } from "expo-router";
+
+import { ZBookmarkTags } from "@hoarder/shared/types/tags";
+
+export default function TagPill({ tag }: { tag: ZBookmarkTags }) {
+ return (
+ <View
+ key={tag.id}
+ className="rounded-full border border-accent px-2.5 py-0.5 text-xs font-semibold"
+ >
+ <Link className="text-foreground" href={`dashboard/tags/${tag.id}`}>
+ {tag.name}
+ </Link>
+ </View>
+ );
+}
diff --git a/apps/mobile/components/bookmarks/ViewBookmarkModal.tsx b/apps/mobile/components/bookmarks/ViewBookmarkModal.tsx
new file mode 100644
index 00000000..318249eb
--- /dev/null
+++ b/apps/mobile/components/bookmarks/ViewBookmarkModal.tsx
@@ -0,0 +1,192 @@
+import React, { useState } from "react";
+import { Keyboard, Pressable, Text } from "react-native";
+import {
+ BottomSheetBackdrop,
+ BottomSheetModal,
+ BottomSheetModalProps,
+ BottomSheetScrollView,
+ BottomSheetView,
+ TouchableWithoutFeedback,
+} from "@gorhom/bottom-sheet";
+
+import {
+ useUpdateBookmark,
+ useUpdateBookmarkText,
+} from "@hoarder/shared-react/hooks/bookmarks";
+import { isBookmarkStillTagging } from "@hoarder/shared-react/utils/bookmarkUtils";
+import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks";
+
+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 }) {
+ return (
+ <BottomSheetView className="flex flex-row items-center gap-4">
+ <Text>Tags</Text>
+ {isBookmarkStillTagging(bookmark) ? (
+ <>
+ <Skeleton className="h-4 w-full" />
+ <Skeleton className="h-4 w-full" />
+ </>
+ ) : bookmark.tags.length > 0 ? (
+ <BottomSheetView className="flex flex-row flex-wrap gap-2">
+ {bookmark.tags.map((t) => (
+ <TagPill key={t.id} tag={t} />
+ ))}
+ </BottomSheetView>
+ ) : (
+ <Text>No tags</Text>
+ )}
+ </BottomSheetView>
+ );
+}
+
+function NotesEditor({ bookmark }: { bookmark: ZBookmark }) {
+ const { mutate, isPending } = useUpdateBookmark();
+ return (
+ <BottomSheetView className="flex flex-row items-center gap-4">
+ <Text>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 ?? ""}
+ />
+ </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 }) {
+ if (bookmark.content.type !== BookmarkTypes.ASSET) {
+ throw new Error("Wrong content type rendered");
+ }
+ return (
+ <BottomSheetView className="flex gap-2">
+ <BookmarkAssetImage
+ assetId={bookmark.content.assetId}
+ className="h-56 min-h-56 w-full object-cover"
+ />
+ </BottomSheetView>
+ );
+}
+
+const ViewBookmarkModal = React.forwardRef<
+ BottomSheetModal,
+ Omit<
+ BottomSheetModalProps,
+ "children" | "backdropComponent" | "onDismiss"
+ > & {
+ bookmark: ZBookmark;
+ }
+>(({ bookmark, ...props }, ref) => {
+ let comp;
+ let title = null;
+ switch (bookmark.content.type) {
+ case BookmarkTypes.LINK:
+ comp = null;
+ 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 (
+ <BottomSheetModal
+ ref={ref}
+ backdropComponent={(props) => (
+ <BottomSheetBackdrop
+ appearsOnIndex={0}
+ disappearsOnIndex={-1}
+ {...props}
+ />
+ )}
+ {...props}
+ >
+ <BottomSheetScrollView className="flex flex-1">
+ <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
+ <BottomSheetView className="flex flex-1">
+ <PageTitle title={title ?? "Untitled"} />
+ <BottomSheetView className="gap-4 px-4">
+ {comp}
+ <TagList bookmark={bookmark} />
+ <NotesEditor bookmark={bookmark} />
+ </BottomSheetView>
+ </BottomSheetView>
+ </TouchableWithoutFeedback>
+ </BottomSheetScrollView>
+ </BottomSheetModal>
+ );
+});
+
+ViewBookmarkModal.displayName = "ViewBookmarkModal";
+
+export default ViewBookmarkModal;