import { useState, useEffect } from "react"; import { useAuth } from "../context/AuthContext"; import ReplyList from "./ReplyList"; import { Link } from "react-router-dom"; import RichText from "./RichText"; import { normalizeAnnotation, normalizeHighlight, likeAnnotation, unlikeAnnotation, getReplies, createReply, deleteReply, updateAnnotation, updateHighlight, getEditHistory, deleteAnnotation, } from "../api/client"; import { MessageSquare, Heart, Trash2, Folder, Edit2, Save, X, Clock, } from "lucide-react"; import { HighlightIcon, TrashIcon } from "./Icons"; import ShareMenu from "./ShareMenu"; import UserMeta from "./UserMeta"; function buildTextFragmentUrl(baseUrl, selector) { if (!selector || selector.type !== "TextQuoteSelector" || !selector.exact) { return baseUrl; } let fragment = ":~:text="; if (selector.prefix) { fragment += encodeURIComponent(selector.prefix) + "-,"; } fragment += encodeURIComponent(selector.exact); if (selector.suffix) { fragment += ",-" + encodeURIComponent(selector.suffix); } return baseUrl + "#" + fragment; } const truncateUrl = (url, maxLength = 50) => { if (!url) return ""; try { const parsed = new URL(url); const fullPath = parsed.host + parsed.pathname; if (fullPath.length > maxLength) return fullPath.substring(0, maxLength) + "..."; return fullPath; } catch { return url.length > maxLength ? url.substring(0, maxLength) + "..." : url; } }; function SembleBadge() { return (
via Semble Semble
); } export default function AnnotationCard({ annotation, onDelete, onAddToCollection, }) { const { user, login } = useAuth(); const data = normalizeAnnotation(annotation); const [likeCount, setLikeCount] = useState(data.likeCount || 0); const [isLiked, setIsLiked] = useState(data.viewerHasLiked || false); const [deleting, setDeleting] = useState(false); const [isEditing, setIsEditing] = useState(false); const [editText, setEditText] = useState(data.text || ""); const [editTags, setEditTags] = useState(data.tags?.join(", ") || ""); const [saving, setSaving] = useState(false); const [showHistory, setShowHistory] = useState(false); const [editHistory, setEditHistory] = useState([]); const [loadingHistory, setLoadingHistory] = useState(false); const [replies, setReplies] = useState([]); const [replyCount, setReplyCount] = useState(data.replyCount || 0); const [showReplies, setShowReplies] = useState(false); const [replyingTo, setReplyingTo] = useState(null); const [replyText, setReplyText] = useState(""); const [posting, setPosting] = useState(false); const [hasEditHistory, setHasEditHistory] = useState(false); const isOwner = user?.did && data.author?.did === user.did; const isSemble = data.uri?.includes("network.cosmik"); const highlightedText = data.selector?.type === "TextQuoteSelector" ? data.selector.exact : null; const fragmentUrl = buildTextFragmentUrl(data.url, data.selector); useEffect(() => { if (data.uri && !data.color && !data.description) { getEditHistory(data.uri) .then((history) => { if (history?.length > 0) setHasEditHistory(true); }) .catch(() => {}); } }, [data.uri, data.color, data.description]); const fetchHistory = async () => { if (showHistory) { setShowHistory(false); return; } try { setLoadingHistory(true); setShowHistory(true); const history = await getEditHistory(data.uri); setEditHistory(history); } catch (err) { console.error("Failed to fetch history:", err); } finally { setLoadingHistory(false); } }; const handlePostReply = async (parentReply) => { if (!replyText.trim()) return; try { setPosting(true); const parentUri = parentReply ? parentReply.id || parentReply.uri : data.uri; const parentCid = parentReply ? parentReply.cid : annotation.cid || data.cid; await createReply({ parentUri, parentCid: parentCid || "", rootUri: data.uri, rootCid: annotation.cid || data.cid || "", text: replyText, }); setReplyText(""); setReplyingTo(null); const res = await getReplies(data.uri); if (res.items) { setReplies(res.items); setReplyCount(res.items.length); } } catch (err) { alert("Failed to post reply: " + err.message); } finally { setPosting(false); } }; const handleSaveEdit = async () => { try { setSaving(true); const tagList = editTags .split(",") .map((t) => t.trim()) .filter(Boolean); await updateAnnotation(data.uri, editText, tagList); setIsEditing(false); if (annotation.body) annotation.body.value = editText; else if (annotation.text) annotation.text = editText; if (annotation.tags) annotation.tags = tagList; data.tags = tagList; } catch (err) { alert("Failed to update: " + err.message); } finally { setSaving(false); } }; const handleLike = async () => { if (!user) { login(); return; } try { if (isLiked) { setIsLiked(false); setLikeCount((prev) => Math.max(0, prev - 1)); await unlikeAnnotation(data.uri); } else { setIsLiked(true); setLikeCount((prev) => prev + 1); const cid = annotation.cid || data.cid || ""; if (data.uri && cid) await likeAnnotation(data.uri, cid); } } catch { setIsLiked(!isLiked); setLikeCount((prev) => (isLiked ? prev + 1 : prev - 1)); } }; const handleDelete = async () => { if (!confirm("Delete this annotation? This cannot be undone.")) return; try { setDeleting(true); const parts = data.uri.split("/"); const rkey = parts[parts.length - 1]; await deleteAnnotation(rkey); if (onDelete) onDelete(data.uri); else window.location.reload(); } catch (err) { alert("Failed to delete: " + err.message); } finally { setDeleting(false); } }; const loadReplies = async () => { if (!showReplies && replies.length === 0) { try { const res = await getReplies(data.uri); if (res.items) setReplies(res.items); } catch (err) { console.error("Failed to load replies:", err); } } setShowReplies(!showReplies); }; const handleCollect = () => { if (!user) { login(); return; } if (onAddToCollection) onAddToCollection(); }; return (
{isSemble && } {hasEditHistory && !data.color && !data.description && ( )} {isOwner && !isSemble && ( <> {!data.color && !data.description && ( )} )}
{showHistory && (

Edit History

{loadingHistory ? (
Loading history...
) : editHistory.length === 0 ? (
No edit history found.
) : ( )}
)}
{truncateUrl(data.url)} {data.title && ( · {data.title} )} {highlightedText && ( “{highlightedText}” )} {isEditing ? (