import { ActivityIndicator, Image, Platform, Pressable, ScrollView, Text, View, } from "react-native"; import Markdown from "react-native-markdown-display"; import * as Haptics from "expo-haptics"; import { Link } from "expo-router"; import * as WebBrowser from "expo-web-browser"; 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/trpc/types/bookmarks"; import { Divider } from "../ui/Divider"; import { Skeleton } from "../ui/Skeleton"; import { useToast } from "../ui/Toast"; const MAX_LOADING_MSEC = 30 * 1000; export function isBookmarkStillCrawling(bookmark: ZBookmark) { return ( bookmark.content.type === "link" && !bookmark.content.crawledAt && Date.now().valueOf() - bookmark.createdAt.valueOf() < MAX_LOADING_MSEC ); } export function isBookmarkStillTagging(bookmark: ZBookmark) { return ( bookmark.taggingStatus === "pending" && Date.now().valueOf() - bookmark.createdAt.valueOf() < MAX_LOADING_MSEC ); } export function isBookmarkStillLoading(bookmark: ZBookmark) { return isBookmarkStillTagging(bookmark) || isBookmarkStillCrawling(bookmark); } function ActionBar({ bookmark }: { bookmark: ZBookmark }) { const { toast } = useToast(); const apiUtils = api.useUtils(); const onError = () => { toast({ message: "Something went wrong", variant: "destructive", showProgress: false, }); }; const { mutate: deleteBookmark, isPending: isDeletionPending } = api.bookmarks.deleteBookmark.useMutation({ onSuccess: () => { toast({ message: "The bookmark has been deleted!", showProgress: false, }); apiUtils.bookmarks.getBookmarks.invalidate(); apiUtils.bookmarks.searchBookmarks.invalidate(); }, onError, }); const { mutate: favouriteBookmark, variables } = api.bookmarks.updateBookmark.useMutation({ onSuccess: () => { apiUtils.bookmarks.getBookmarks.invalidate(); apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: bookmark.id }); }, onError, }); const { mutate: archiveBookmark, isPending: isArchivePending } = api.bookmarks.updateBookmark.useMutation({ onSuccess: (resp) => { toast({ message: `The bookmark has been ${resp.archived ? "archived" : "un-archived"}!`, showProgress: false, }); apiUtils.bookmarks.getBookmarks.invalidate(); apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: bookmark.id }); apiUtils.bookmarks.searchBookmarks.invalidate(); }, onError, }); return ( {(isArchivePending || isDeletionPending) && } { Haptics.selectionAsync(); favouriteBookmark({ bookmarkId: bookmark.id, favourited: !bookmark.favourited, }); }} > {(variables ? variables.favourited : bookmark.favourited) ? ( ) : ( )} { Haptics.selectionAsync(); if (nativeEvent.event === "delete") { deleteBookmark({ bookmarkId: bookmark.id, }); } else if (nativeEvent.event === "archive") { archiveBookmark({ bookmarkId: bookmark.id, archived: !bookmark.archived, }); } }} 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", }), }, ]} shouldOpenOnLongPress={false} > Haptics.selectionAsync()} color="gray" /> ); } function TagList({ bookmark }: { bookmark: ZBookmark }) { const tags = bookmark.tags; if (isBookmarkStillTagging(bookmark)) { return ( <> ); } return ( {tags.map((t) => ( {t.name} ))} ); } function LinkCard({ bookmark }: { bookmark: ZBookmark }) { if (bookmark.content.type !== "link") { throw new Error("Wrong content type rendered"); } const url = bookmark.content.url; const parsedUrl = new URL(url); const imageComp = bookmark.content.imageUrl ? ( ) : ( ); return ( {imageComp} WebBrowser.openBrowserAsync(url)} > {bookmark.content.title ?? parsedUrl.host} {parsedUrl.host} ); } function TextCard({ bookmark }: { bookmark: ZBookmark }) { if (bookmark.content.type !== "text") { throw new Error("Wrong content type rendered"); } return ( {bookmark.content.text} ); } function AssetCard({ bookmark }: { bookmark: ZBookmark }) { const { settings } = useAppSettings(); if (bookmark.content.type !== "asset") { throw new Error("Wrong content type rendered"); } return ( ); } 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; }, }, ); let comp; switch (bookmark.content.type) { case "link": comp = ; break; case "text": comp = ; break; case "asset": comp = ; break; } return {comp}; }