From 56610a67d4cde251ddf2bd35b1a11c5d31ba8a8a Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Fri, 17 Oct 2025 15:45:20 -0400 Subject: [PATCH] make quotes have a page component --- .../[publication]/[rkey]/PostContent.tsx | 43 ++++++++++++++++--- .../[did]/[publication]/[rkey]/PostPages.tsx | 5 +-- .../[publication]/[rkey]/QuoteHandler.tsx | 27 ++++++++---- .../[did]/[publication]/[rkey]/TextBlock.tsx | 10 +++-- .../[publication]/[rkey]/quotePosition.ts | 25 ++++++++--- .../[publication]/[rkey]/useHighlight.tsx | 10 ++++- components/Blocks/ImageBlock.tsx | 4 +- 7 files changed, 96 insertions(+), 28 deletions(-) diff --git a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx index b05cf0e4..c28af348 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostContent.tsx @@ -50,7 +50,7 @@ export function PostContent({ }) { return (
{blocks.map((b, index) => { @@ -103,8 +103,13 @@ let Block = ({ scrollMarginBottom: "4rem", wordBreak: "break-word" as React.CSSProperties["wordBreak"], }, - id: preview ? undefined : index.join("."), + id: preview + ? undefined + : pageId + ? `${pageId}~${index.join(".")}` + : index.join("."), "data-index": index.join("."), + "data-page-id": pageId, }; let alignment = b.alignment === "lex:pub.leaflet.pages.linearDocument#textAlignRight" @@ -175,6 +180,7 @@ let Block = ({ did={did} key={i} className={className} + pageId={pageId} /> ))} @@ -277,6 +283,7 @@ let Block = ({ plaintext={b.block.plaintext} index={index} preview={preview} + pageId={pageId} /> ); @@ -289,6 +296,7 @@ let Block = ({ plaintext={b.block.plaintext} index={index} preview={preview} + pageId={pageId} />

); @@ -296,26 +304,46 @@ let Block = ({ if (b.block.level === 1) return (

- +

); if (b.block.level === 2) return (

- +

); if (b.block.level === 3) return (

- +

); // if (b.block.level === 4) return

{b.block.plaintext}

; // if (b.block.level === 5) return
{b.block.plaintext}
; return (
- +
); } @@ -331,6 +359,7 @@ function ListItem(props: { did: string; className?: string; bskyPostData: AppBskyFeedDefs.PostView[]; + pageId?: string; }) { let children = props.item.children?.length ? ( @@ -361,6 +391,7 @@ function ListItem(props: { did={props.did} isList index={props.index} + pageId={props.pageId} /> {children}{" "}
diff --git a/app/lish/[did]/[publication]/[rkey]/PostPages.tsx b/app/lish/[did]/[publication]/[rkey]/PostPages.tsx index 64c35486..c5f41da5 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostPages.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostPages.tsx @@ -58,7 +58,6 @@ export const closePage = (page: string) => export function PostPages({ document, blocks, - name, did, profile, preferences, @@ -70,7 +69,6 @@ export function PostPages({ document_uri: string; document: PostPageData; blocks: PubLeafletPagesLinearDocument.Block[]; - name: string; profile: ProfileViewDetailed; pubRecord: PubLeafletPublication.Record; did: string; @@ -99,7 +97,6 @@ export function PostPages({ )} diff --git a/app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx b/app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx index b551dbd7..e7d8561e 100644 --- a/app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx +++ b/app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx @@ -24,12 +24,18 @@ export function QuoteHandler() { useEffect(() => { const handleSelectionChange = (e: Event) => { const selection = document.getSelection(); - const postContent = document.getElementById("post-content"); + + // Check if selection is within any element with postContent class const isWithinPostContent = - postContent && selection?.rangeCount && selection.rangeCount > 0 - ? postContent.contains( - selection.getRangeAt(0).commonAncestorContainer, - ) + selection?.rangeCount && selection.rangeCount > 0 + ? (() => { + const range = selection.getRangeAt(0); + const ancestor = range.commonAncestorContainer; + const element = ancestor.nodeType === Node.ELEMENT_NODE + ? ancestor as Element + : ancestor.parentElement; + return element?.closest('.postContent') !== null; + })() : false; if (!selection || !isWithinPostContent || !selection?.toString()) @@ -88,6 +94,7 @@ export function QuoteHandler() { endIndex?.element, ); let position: QuotePosition = { + ...(startIndex.pageId && { pageId: startIndex.pageId }), start: { block: startIndex?.index.split(".").map((i) => parseInt(i)), offset: startOffset, @@ -145,7 +152,10 @@ export const QuoteOptionButtons = (props: { position: string }) => { // Clear existing query parameters currentUrl.search = ""; - currentUrl.hash = `#${pos?.start.block.join(".")}_${pos?.start.offset}`; + const fragmentId = pos?.pageId + ? `${pos.pageId}~${pos.start.block.join(".")}_${pos.start.offset}` + : `${pos?.start.block.join(".")}_${pos?.start.offset}`; + currentUrl.hash = `#${fragmentId}`; return [currentUrl.toString(), pos]; }, [props.position]); let pubRecord = data.documents_in_publications[0]?.publications?.record as @@ -210,13 +220,14 @@ export const QuoteOptionButtons = (props: { position: string }) => { ); }; -function findDataIndex(node: Node): { index: string; element: Element } | null { +function findDataIndex(node: Node): { index: string; element: Element; pageId?: string } | null { if (node.nodeType === Node.ELEMENT_NODE) { const element = node as Element; if (element.hasAttribute("data-index")) { const index = element.getAttribute("data-index"); if (index) { - return { index, element }; + const pageId = element.getAttribute("data-page-id") || undefined; + return { index, element, pageId }; } } } diff --git a/app/lish/[did]/[publication]/[rkey]/TextBlock.tsx b/app/lish/[did]/[publication]/[rkey]/TextBlock.tsx index 2f051d41..20c765a1 100644 --- a/app/lish/[did]/[publication]/[rkey]/TextBlock.tsx +++ b/app/lish/[did]/[publication]/[rkey]/TextBlock.tsx @@ -11,13 +11,17 @@ export function TextBlock(props: { facets?: Facet[]; index: number[]; preview?: boolean; + pageId?: string; }) { let children = []; - let highlights = useHighlight(props.index); + let highlights = useHighlight(props.index, props.pageId); let facets = useMemo(() => { if (props.preview) return props.facets; let facets = [...(props.facets || [])]; for (let highlight of highlights) { + const fragmentId = props.pageId + ? `${props.pageId}~${props.index.join(".")}_${highlight.startOffset || 0}` + : `${props.index.join(".")}_${highlight.startOffset || 0}`; facets = addFacet( facets, { @@ -35,7 +39,7 @@ export function TextBlock(props: { { $type: "pub.leaflet.richtext.facet#highlight" }, { $type: "pub.leaflet.richtext.facet#id", - id: `${props.index.join(".")}_${highlight.startOffset || 0}`, + id: fragmentId, }, ], }, @@ -43,7 +47,7 @@ export function TextBlock(props: { ); } return facets; - }, [props.plaintext, props.facets, highlights, props.preview]); + }, [props.plaintext, props.facets, highlights, props.preview, props.pageId]); return ; } diff --git a/app/lish/[did]/[publication]/[rkey]/quotePosition.ts b/app/lish/[did]/[publication]/[rkey]/quotePosition.ts index 4f2d1154..20182c8d 100644 --- a/app/lish/[did]/[publication]/[rkey]/quotePosition.ts +++ b/app/lish/[did]/[publication]/[rkey]/quotePosition.ts @@ -1,4 +1,5 @@ export interface QuotePosition { + pageId?: string; start: { block: number[]; offset: number; @@ -14,12 +15,15 @@ export const QUOTE_PARAM = "/l-quote/"; /** * Encodes quote position into a URL-friendly string * Format: startBlock_startOffset-endBlock_endOffset + * Format with page: pageId~startBlock_startOffset-endBlock_endOffset * Block paths are joined with dots: 1.2.0_45-1.2.3_67 - * Simple blocks: 0:12-2:45 + * Simple blocks: 0_12-2_45 + * With page: page1~0_12-2_45 */ export function encodeQuotePosition(position: QuotePosition): string { - const { start, end } = position; - return `${start.block.join(".")}_${start.offset}-${end.block.join(".")}_${end.offset}`; + const { pageId, start, end } = position; + const positionStr = `${start.block.join(".")}_${start.offset}-${end.block.join(".")}_${end.offset}`; + return pageId ? `${pageId}~${positionStr}` : positionStr; } /** @@ -28,9 +32,19 @@ export function encodeQuotePosition(position: QuotePosition): string { */ export function decodeQuotePosition(encoded: string): QuotePosition | null { try { - // Match format: blockPath:number-blockPath:number + // Check for pageId prefix (format: pageId~blockPath_number-blockPath_number) + let pageId: string | undefined; + let positionStr = encoded; + + const tildeIndex = encoded.indexOf("~"); + if (tildeIndex !== -1) { + pageId = encoded.substring(0, tildeIndex); + positionStr = encoded.substring(tildeIndex + 1); + } + + // Match format: blockPath_number-blockPath_number // Block paths can be: 5, 1.2, 0.1.3, etc. - const match = encoded.match(/^([\d.]+)_(\d+)-([\d.]+)_(\d+)$/); + const match = positionStr.match(/^([\d.]+)_(\d+)-([\d.]+)_(\d+)$/); if (!match) { return null; @@ -39,6 +53,7 @@ export function decodeQuotePosition(encoded: string): QuotePosition | null { const [, startBlockPath, startOffset, endBlockPath, endOffset] = match; const position: QuotePosition = { + ...(pageId && { pageId }), start: { block: startBlockPath.split(".").map((i) => parseInt(i)), offset: parseInt(startOffset, 10), diff --git a/app/lish/[did]/[publication]/[rkey]/useHighlight.tsx b/app/lish/[did]/[publication]/[rkey]/useHighlight.tsx index f3525ecc..ee045aa6 100644 --- a/app/lish/[did]/[publication]/[rkey]/useHighlight.tsx +++ b/app/lish/[did]/[publication]/[rkey]/useHighlight.tsx @@ -11,7 +11,7 @@ export const useActiveHighlightState = create(() => ({ activeHighlight: null as null | QuotePosition, })); -export const useHighlight = (pos: number[]) => { +export const useHighlight = (pos: number[], pageId?: string) => { let doc = useContext(PostPageContext); let { quote } = useParams(); let activeHighlight = useActiveHighlightState( @@ -23,6 +23,14 @@ export const useHighlight = (pos: number[]) => { return highlights .map((quotePosition) => { if (!quotePosition) return null; + // Filter by pageId if provided + if (pageId && quotePosition.pageId !== pageId) { + return null; + } + // If highlight has pageId but block doesn't, skip + if (quotePosition.pageId && !pageId) { + return null; + } let maxLength = Math.max( quotePosition.start.block.length, quotePosition.end.block.length, diff --git a/components/Blocks/ImageBlock.tsx b/components/Blocks/ImageBlock.tsx index 5a12ee03..45d3a4ed 100644 --- a/components/Blocks/ImageBlock.tsx +++ b/components/Blocks/ImageBlock.tsx @@ -140,7 +140,9 @@ export function ImageBlock(props: BlockProps & { preview?: boolean }) { ) : (