import React from "react"; import { ActivityIndicator, Alert, Image, Platform, Pressable, ScrollView, Text, View, } from "react-native"; import * as Haptics from "expo-haptics"; import { router, useRouter } from "expo-router"; import useAppSettings from "@/lib/settings"; import { api } from "@/lib/trpc"; import { MenuView } from "@react-native-menu/menu"; import { Ellipsis, Star } from "lucide-react-native"; import type { ZBookmark } from "@hoarder/shared/types/bookmarks"; import { useDeleteBookmark, useUpdateBookmark, } from "@hoarder/shared-react/hooks/bookmarks"; import { getBookmarkLinkImageUrl, isBookmarkStillLoading, isBookmarkStillTagging, } from "@hoarder/shared-react/utils/bookmarkUtils"; import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; import { Divider } from "../ui/Divider"; import { Skeleton } from "../ui/Skeleton"; import { useToast } from "../ui/Toast"; import BookmarkAssetImage from "./BookmarkAssetImage"; import BookmarkTextMarkdown from "./BookmarkTextMarkdown"; import TagPill from "./TagPill"; function ActionBar({ bookmark }: { bookmark: ZBookmark }) { const { toast } = useToast(); const onError = () => { toast({ message: "Something went wrong", variant: "destructive", showProgress: false, }); }; const { mutate: deleteBookmark, isPending: isDeletionPending } = useDeleteBookmark({ onSuccess: () => { toast({ message: "The bookmark has been deleted!", showProgress: false, }); }, onError, }); const { mutate: favouriteBookmark, variables } = useUpdateBookmark({ onError, }); const { mutate: archiveBookmark, isPending: isArchivePending } = useUpdateBookmark({ onSuccess: (resp) => { toast({ message: `The bookmark has been ${resp.archived ? "archived" : "un-archived"}!`, showProgress: false, }); }, onError, }); 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", }, ], ); return ( {(isArchivePending || isDeletionPending) && } { Haptics.selectionAsync(); favouriteBookmark({ bookmarkId: bookmark.id, favourited: !bookmark.favourited, }); }} > {(variables ? variables.favourited : bookmark.favourited) ? ( ) : ( )} { Haptics.selectionAsync(); if (nativeEvent.event === "delete") { deleteBookmarkAlert(); } else if (nativeEvent.event === "archive") { archiveBookmark({ bookmarkId: bookmark.id, archived: !bookmark.archived, }); } else if (nativeEvent.event === "manage_list") { router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`); } else if (nativeEvent.event === "manage_tags") { router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`); } }} actions={[ { id: "archive", title: bookmark.archived ? "Un-archive" : "Archive", image: Platform.select({ ios: "folder", }), }, { id: "delete", title: "Delete", attributes: { destructive: true, }, image: Platform.select({ ios: "trash", }), }, { id: "manage_list", title: "Manage Lists", image: Platform.select({ ios: "list", }), }, { id: "manage_tags", title: "Manage Tags", image: Platform.select({ ios: "tag", }), }, ]} shouldOpenOnLongPress={false} > Haptics.selectionAsync()} color="gray" /> ); } function TagList({ bookmark }: { bookmark: ZBookmark }) { const tags = bookmark.tags; if (isBookmarkStillTagging(bookmark)) { return ( <> ); } return ( {tags.map((t) => ( ))} ); } function LinkCard({ bookmark, onOpenBookmark, }: { bookmark: ZBookmark; onOpenBookmark: () => void; }) { const { settings } = useAppSettings(); if (bookmark.content.type !== BookmarkTypes.LINK) { throw new Error("Wrong content type rendered"); } const url = bookmark.content.url; const parsedUrl = new URL(url); const imageUrl = getBookmarkLinkImageUrl(bookmark.content); let imageComp; if (imageUrl) { imageComp = ( ); } else { imageComp = ( ); } return ( {imageComp} {bookmark.title ?? bookmark.content.title ?? parsedUrl.host} {parsedUrl.host} ); } 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 ( {bookmark.title && ( {bookmark.title} )} ); } function AssetCard({ bookmark, onOpenBookmark, }: { bookmark: ZBookmark; onOpenBookmark: () => void; }) { if (bookmark.content.type !== BookmarkTypes.ASSET) { throw new Error("Wrong content type rendered"); } const title = bookmark.title ?? bookmark.content.fileName; const assetImage = bookmark.assets.find((r) => r.assetType == "assetScreenshot")?.id ?? bookmark.content.assetId; return ( {title && ( {title} )} ); } export default function BookmarkCard({ bookmark: initialData, }: { bookmark: ZBookmark; }) { const { data: bookmark } = api.bookmarks.getBookmark.useQuery( { bookmarkId: initialData.id, }, { initialData, refetchInterval: (query) => { const data = query.state.data; if (!data) { return false; } // If the link is not crawled or not tagged if (isBookmarkStillLoading(data)) { return 1000; } return false; }, }, ); const router = useRouter(); let comp; switch (bookmark.content.type) { case BookmarkTypes.LINK: comp = ( router.push(`/dashboard/bookmarks/${bookmark.id}`) } /> ); break; case BookmarkTypes.TEXT: comp = ( router.push(`/dashboard/bookmarks/${bookmark.id}`) } /> ); break; case BookmarkTypes.ASSET: comp = ( router.push(`/dashboard/bookmarks/${bookmark.id}`) } /> ); break; } return ( {comp} ); }