diff options
| author | MohamedBassem <me@mbassem.com> | 2024-08-26 13:41:13 +0300 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-08-26 16:17:35 +0300 |
| commit | b094b2cecb0da1bcdf4c63dd081638d87793c53c (patch) | |
| tree | 332db48228a83a43e3336aa8e818c70e58b4f85d /apps/mobile/components | |
| parent | 8410a6d3c125cf27daa4e3abeb4c4a4d228e2cfd (diff) | |
| download | karakeep-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')
| -rw-r--r-- | apps/mobile/components/bookmarks/BookmarkCard.tsx | 100 | ||||
| -rw-r--r-- | apps/mobile/components/bookmarks/NewBookmarkModal.tsx | 71 | ||||
| -rw-r--r-- | apps/mobile/components/bookmarks/TagPill.tsx | 17 | ||||
| -rw-r--r-- | apps/mobile/components/bookmarks/ViewBookmarkModal.tsx | 192 | ||||
| -rw-r--r-- | apps/mobile/components/ui/Input.tsx | 15 |
5 files changed, 285 insertions, 110 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; diff --git a/apps/mobile/components/ui/Input.tsx b/apps/mobile/components/ui/Input.tsx index 57d16f5d..dc84f54f 100644 --- a/apps/mobile/components/ui/Input.tsx +++ b/apps/mobile/components/ui/Input.tsx @@ -1,5 +1,5 @@ import { forwardRef } from "react"; -import { Text, TextInput, View } from "react-native"; +import { ActivityIndicator, Text, TextInput, View } from "react-native"; import { cn } from "@/lib/utils"; import { TailwindResolver } from "../TailwindResolver"; @@ -11,8 +11,14 @@ export interface InputProps inputClasses?: string; } -const Input = forwardRef<React.ElementRef<typeof TextInput>, InputProps>( - ({ className, label, labelClasses, inputClasses, ...props }, ref) => ( +const Input = forwardRef< + React.ElementRef<typeof TextInput>, + InputProps & { loading?: boolean } +>( + ( + { className, label, labelClasses, inputClasses, loading, ...props }, + ref, + ) => ( <View className={cn("flex flex-col gap-1.5", className)}> {label && <Text className={cn("text-base", labelClasses)}>{label}</Text>} <TailwindResolver @@ -30,6 +36,9 @@ const Input = forwardRef<React.ElementRef<typeof TextInput>, InputProps>( /> )} /> + {loading && ( + <ActivityIndicator className="absolute bottom-0 right-0 p-2" /> + )} </View> ), ); |
