import React, { useEffect, useState } from "react"; import { useParams, Link, useLocation, useNavigate } from "react-router-dom"; import { useStore } from "@nanostores/react"; import { $user } from "../../store/auth"; import { getAnnotation, getReplies, resolveHandle, createReply, deleteReply, } from "../../api/client"; import type { AnnotationItem } from "../../types"; import Card from "../../components/common/Card"; import ReplyList from "../../components/feed/ReplyList"; import { Loader2, MessageSquare, ArrowLeft, X, AlertTriangle, } from "lucide-react"; import { getAvatarUrl } from "../../api/client"; export default function AnnotationDetail() { const { uri, did, rkey, handle, type } = useParams(); const location = useLocation(); const navigate = useNavigate(); const user = useStore($user); const [annotation, setAnnotation] = useState(null); const [replies, setReplies] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [replyText, setReplyText] = useState(""); const [posting, setPosting] = useState(false); const [replyingTo, setReplyingTo] = useState(null); const [targetUri, setTargetUri] = useState(uri || null); useEffect(() => { async function resolve() { if (uri) { setTargetUri(decodeURIComponent(uri)); return; } if (handle && rkey) { let collection = "at.margin.annotation"; if (type === "highlight" || location.pathname.includes("/highlight/")) collection = "at.margin.highlight"; if (type === "bookmark" || location.pathname.includes("/bookmark/")) collection = "at.margin.bookmark"; try { const resolvedDid = await resolveHandle(handle); if (resolvedDid) { setTargetUri(`at://${resolvedDid}/${collection}/${rkey}`); } else { throw new Error("Could not resolve handle"); } } catch (e) { setError( "Failed to resolve handle: " + (e instanceof Error ? e.message : "Unknown error"), ); setLoading(false); } } else if (did && rkey) { setTargetUri(`at://${did}/at.margin.annotation/${rkey}`); } else { const pathParts = (location.pathname || "").split("/"); const atIndex = pathParts.indexOf("at"); if ( atIndex !== -1 && pathParts[atIndex + 1] && pathParts[atIndex + 2] ) { setTargetUri( `at://${pathParts[atIndex + 1]}/at.margin.annotation/${pathParts[atIndex + 2]}`, ); } } } resolve(); }, [uri, did, rkey, handle, type, location.pathname]); const refreshReplies = async () => { if (!targetUri) return; const repliesData = await getReplies(targetUri); setReplies(repliesData.items || []); }; useEffect(() => { async function fetchData() { if (!targetUri) return; try { setLoading(true); const [annData, repliesData] = await Promise.all([ getAnnotation(targetUri), getReplies(targetUri).catch(() => ({ items: [] as AnnotationItem[], })), ]); if (!annData) { setError("Annotation not found"); } else { setAnnotation(annData); setReplies(repliesData.items || []); } } catch (err) { setError(err instanceof Error ? err.message : "Unknown error"); } finally { setLoading(false); } } fetchData(); }, [targetUri]); const handleReply = async (e?: React.FormEvent) => { if (e) e.preventDefault(); if (!replyText.trim() || !annotation || !targetUri) return; try { setPosting(true); const parentUri = replyingTo ? replyingTo.uri || replyingTo.id : targetUri; const parentCid = replyingTo ? replyingTo.cid : annotation.cid; if (!parentUri || !parentCid || !annotation.cid) throw new Error("Missing parent info"); await createReply( parentUri, parentCid, targetUri, annotation.cid, replyText, ); setReplyText(""); setReplyingTo(null); await refreshReplies(); } catch (err) { alert( "Failed to post reply: " + (err instanceof Error ? err.message : "Unknown error"), ); } finally { setPosting(false); } }; const handleDeleteReply = async (reply: AnnotationItem) => { if (!window.confirm("Delete this reply?")) return; try { await deleteReply(reply.uri || reply.id!); await refreshReplies(); } catch (err) { alert( "Failed to delete: " + (err instanceof Error ? err.message : "Unknown error"), ); } }; if (loading) { return (
); } if (error || !annotation) { return (

Not found

{error || "This may have been deleted."}

Back to Feed
); } return (
Back
navigate("/home")} /> {annotation.type !== "Bookmark" && annotation.type !== "Highlight" && !annotation.motivation?.includes("bookmark") && !annotation.motivation?.includes("highlight") && (

Replies ({replies.length})

{user ? (
{replyingTo && (
Replying to{" "} @ {(replyingTo.author || replyingTo.creator)?.handle || "unknown"}
)}
{getAvatarUrl(user.did, user.avatar) ? ( ) : (
{user.handle?.[0]?.toUpperCase()}
)}