import { ActivityIndicator, Alert, Image, Platform, Pressable, ScrollView, Share, View, } from "react-native"; import * as Clipboard from "expo-clipboard"; import * as FileSystem from "expo-file-system"; import * as Haptics from "expo-haptics"; import { router, useRouter } from "expo-router"; import * as Sharing from "expo-sharing"; import { Text } from "@/components/ui/Text"; import useAppSettings from "@/lib/settings"; import { api } from "@/lib/trpc"; import { MenuView } from "@react-native-menu/menu"; import { Ellipsis, ShareIcon, Star } from "lucide-react-native"; import type { ZBookmark } from "@karakeep/shared/types/bookmarks"; import { useDeleteBookmark, useUpdateBookmark, } from "@karakeep/shared-react/hooks/bookmarks"; import { useWhoAmI } from "@karakeep/shared-react/hooks/users"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; import { getBookmarkLinkImageUrl, getBookmarkRefreshInterval, isBookmarkStillTagging, } from "@karakeep/shared/utils/bookmarkUtils"; import { Divider } from "../ui/Divider"; import { Skeleton } from "../ui/Skeleton"; import { useToast } from "../ui/Toast"; import BookmarkAssetImage from "./BookmarkAssetImage"; import BookmarkTextMarkdown from "./BookmarkTextMarkdown"; import { NotePreview } from "./NotePreview"; import TagPill from "./TagPill"; function ActionBar({ bookmark }: { bookmark: ZBookmark }) { const { toast } = useToast(); const { settings } = useAppSettings(); const { data: currentUser } = useWhoAmI(); // Check if the current user owns this bookmark const isOwner = currentUser?.id === bookmark.userId; 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", }, ], ); const handleShare = async () => { try { switch (bookmark.content.type) { case BookmarkTypes.LINK: await Share.share({ url: bookmark.content.url, message: bookmark.content.url, }); break; case BookmarkTypes.TEXT: await Clipboard.setStringAsync(bookmark.content.text); toast({ message: "Text copied to clipboard", showProgress: false, }); break; case BookmarkTypes.ASSET: if (bookmark.content.assetType === "image") { if (await Sharing.isAvailableAsync()) { const assetUrl = `${settings.address}/api/assets/${bookmark.content.assetId}`; const fileUri = `${FileSystem.documentDirectory}temp_image.jpg`; const downloadResult = await FileSystem.downloadAsync( assetUrl, fileUri, { headers: { Authorization: `Bearer ${settings.apiKey}`, }, }, ); if (downloadResult.status === 200) { await Sharing.shareAsync(downloadResult.uri); // Clean up the temporary file await FileSystem.deleteAsync(downloadResult.uri, { idempotent: true, }); } else { throw new Error("Failed to download image"); } } } else { // For PDFs, share the URL const assetUrl = `${settings.address}/api/assets/${bookmark.content.assetId}`; await Share.share({ url: assetUrl, message: bookmark.title || bookmark.content.fileName || "PDF Document", }); } break; } } catch (error) { console.error("Share error:", error); toast({ message: "Failed to share", variant: "destructive", showProgress: false, }); } }; // Build actions array based on ownership const menuActions = []; if (isOwner) { menuActions.push( { id: "edit", title: "Edit", image: Platform.select({ ios: "pencil", }), }, { id: "manage_list", title: "Manage Lists", image: Platform.select({ ios: "list.bullet", }), }, { id: "manage_tags", title: "Manage Tags", image: Platform.select({ ios: "tag", }), }, { 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", }), }, ); } return ( {(isArchivePending || isDeletionPending) && } {isOwner && ( { Haptics.selectionAsync(); favouriteBookmark({ bookmarkId: bookmark.id, favourited: !bookmark.favourited, }); }} > {(variables ? variables.favourited : bookmark.favourited) ? ( ) : ( )} )} { Haptics.selectionAsync(); handleShare(); }} > {isOwner && menuActions.length > 0 && ( { 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`); } else if (nativeEvent.event === "edit") { router.push(`/dashboard/bookmarks/${bookmark.id}/info`); } }} actions={menuActions} shouldOpenOnLongPress={false} > Haptics.selectionAsync()} color="gray" /> )} ); } function TagList({ bookmark }: { bookmark: ZBookmark }) { const tags = bookmark.tags; const { data: currentUser } = useWhoAmI(); const isOwner = currentUser?.id === bookmark.userId; if (isBookmarkStillTagging(bookmark)) { return ( <> ); } return ( {tags.map((t) => ( ))} ); } function LinkCard({ bookmark, onOpenBookmark, }: { bookmark: ZBookmark; onOpenBookmark: () => void; }) { const { settings } = useAppSettings(); const { data: currentUser } = useWhoAmI(); const isOwner = currentUser?.id === bookmark.userId; if (bookmark.content.type !== BookmarkTypes.LINK) { throw new Error("Wrong content type rendered"); } const note = settings.showNotes ? bookmark.note?.trim() : undefined; 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} {note && ( )} {parsedUrl.host} ); } function TextCard({ bookmark, onOpenBookmark, }: { bookmark: ZBookmark; onOpenBookmark: () => void; }) { const { settings } = useAppSettings(); const { data: currentUser } = useWhoAmI(); const isOwner = currentUser?.id === bookmark.userId; if (bookmark.content.type !== BookmarkTypes.TEXT) { throw new Error("Wrong content type rendered"); } const note = settings.showNotes ? bookmark.note?.trim() : undefined; const content = bookmark.content.text; return ( {bookmark.title && ( {bookmark.title} )} {note && ( )} ); } function AssetCard({ bookmark, onOpenBookmark, }: { bookmark: ZBookmark; onOpenBookmark: () => void; }) { const { settings } = useAppSettings(); const { data: currentUser } = useWhoAmI(); const isOwner = currentUser?.id === bookmark.userId; if (bookmark.content.type !== BookmarkTypes.ASSET) { throw new Error("Wrong content type rendered"); } const note = settings.showNotes ? bookmark.note?.trim() : undefined; const title = bookmark.title ?? bookmark.content.fileName; const assetImage = bookmark.assets.find((r) => r.assetType == "assetScreenshot")?.id ?? bookmark.content.assetId; return ( {title && ( {title} )} {note && ( )} ); } 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; } return getBookmarkRefreshInterval(data); }, }, ); 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}; }