diff --git a/actions/publishToPublication.ts b/actions/publishToPublication.ts index 710278df..e863c78e 100644 --- a/actions/publishToPublication.ts +++ b/actions/publishToPublication.ts @@ -52,12 +52,14 @@ export async function publishToPublication({ leaflet_id, title, description, + entitiesToDelete, }: { root_entity: string; publication_uri?: string; leaflet_id: string; title?: string; description?: string; + entitiesToDelete?: string[]; }) { const oauthClient = await createOauthClient(); let identity = await getIdentityData(); @@ -179,6 +181,15 @@ export async function publishToPublication({ .eq("leaflet", leaflet_id) .eq("publication", publication_uri), ]); + + // Heuristic: Remove title entities if this is the first time publishing + // (when coming from a standalone leaflet with entitiesToDelete passed in) + if (entitiesToDelete && entitiesToDelete.length > 0 && !existingDocUri) { + await supabaseServerClient + .from("entities") + .delete() + .in("id", entitiesToDelete); + } } else { // Publishing standalone - update leaflets_to_documents await supabaseServerClient.from("leaflets_to_documents").upsert({ @@ -187,6 +198,15 @@ export async function publishToPublication({ title: title || "Untitled", description: description || "", }); + + // Heuristic: Remove title entities if this is the first time publishing standalone + // (when entitiesToDelete is provided and there's no existing document) + if (entitiesToDelete && entitiesToDelete.length > 0 && !existingDocUri) { + await supabaseServerClient + .from("entities") + .delete() + .in("id", entitiesToDelete); + } } return { rkey, record: JSON.parse(JSON.stringify(record)) }; diff --git a/app/[leaflet_id]/actions/PublishButton.tsx b/app/[leaflet_id]/actions/PublishButton.tsx index 73044af0..accd4a9a 100644 --- a/app/[leaflet_id]/actions/PublishButton.tsx +++ b/app/[leaflet_id]/actions/PublishButton.tsx @@ -190,11 +190,11 @@ const PublishToPublicationButton = (props: { entityID: string }) => { // For looseleaf, navigate without publication_uri if (selectedPub === "looseleaf") { router.push( - `${permission_token.id}/publish?title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}`, + `${permission_token.id}/publish?title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}&entitiesToDelete=${encodeURIComponent(JSON.stringify(entitiesToDelete))}`, ); } else { router.push( - `${permission_token.id}/publish?publication_uri=${encodeURIComponent(selectedPub)}&title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}`, + `${permission_token.id}/publish?publication_uri=${encodeURIComponent(selectedPub)}&title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}&entitiesToDelete=${encodeURIComponent(JSON.stringify(entitiesToDelete))}`, ); } }} @@ -362,9 +362,10 @@ const PubOption = (props: { let useTitle = (entityID: string) => { let rootPage = useEntity(entityID, "root/page")[0].data.value; - let firstBlock = useBlocks(rootPage)[0]?.value; + let blocks = useBlocks(rootPage); + let firstBlock = blocks[0]; - let firstBlockText = useEntity(firstBlock, "block/text")?.data.value; + let firstBlockText = useEntity(firstBlock?.value, "block/text")?.data.value; const leafletTitle = useMemo(() => { if (!firstBlockText) return "Untitled"; @@ -375,8 +376,8 @@ let useTitle = (entityID: string) => { return YJSFragmentToString(nodes[0]) || "Untitled"; }, [firstBlockText]); - let secondBlock = useBlocks(rootPage)[1]; - let secondBlockTextValue = useEntity(secondBlock.value, "block/text")?.data + let secondBlock = blocks[1]; + let secondBlockTextValue = useEntity(secondBlock?.value, "block/text")?.data .value; const secondBlockText = useMemo(() => { if (!secondBlockTextValue) return ""; @@ -388,9 +389,15 @@ let useTitle = (entityID: string) => { }, [firstBlockText]); let entitiesToDelete = useMemo(() => { - let etod = [firstBlock]; - if (secondBlockText.trim() === "" && secondBlock.type === "text") + let etod: string[] = []; + // Only delete first block if it's a heading type + if (firstBlock?.type === "heading") { + etod.push(firstBlock.value); + } + // Delete second block if it's empty text + if (secondBlockText.trim() === "" && secondBlock?.type === "text") { etod.push(secondBlock.value); + } return etod; }, [firstBlock, secondBlockText, secondBlock]); diff --git a/app/[leaflet_id]/publish/PublishPost.tsx b/app/[leaflet_id]/publish/PublishPost.tsx index a7e46399..4d2b7bc6 100644 --- a/app/[leaflet_id]/publish/PublishPost.tsx +++ b/app/[leaflet_id]/publish/PublishPost.tsx @@ -30,6 +30,7 @@ type Props = { publication_uri?: string; record?: PubLeafletPublication.Record; posts_in_pub?: number; + entitiesToDelete?: string[]; }; export function PublishPost(props: Props) { @@ -74,6 +75,7 @@ const PublishPostForm = ( leaflet_id: props.leaflet_id, title: props.title, description: props.description, + entitiesToDelete: props.entitiesToDelete, }); if (!doc) return; diff --git a/app/[leaflet_id]/publish/page.tsx b/app/[leaflet_id]/publish/page.tsx index 68629120..9ee5917c 100644 --- a/app/[leaflet_id]/publish/page.tsx +++ b/app/[leaflet_id]/publish/page.tsx @@ -17,6 +17,7 @@ type Props = { publication_uri: string; title: string; description: string; + entitiesToDelete: string; }>; }; export default async function PublishLeafletPage(props: Props) { @@ -85,6 +86,19 @@ export default async function PublishLeafletPage(props: Props) { let agent = new AtpAgent({ service: "https://public.api.bsky.app" }); let profile = await agent.getProfile({ actor: identity.atp_did }); + // Parse entitiesToDelete from URL params + let searchParams = await props.searchParams; + let entitiesToDelete: string[] = []; + try { + if (searchParams.entitiesToDelete) { + entitiesToDelete = JSON.parse( + decodeURIComponent(searchParams.entitiesToDelete), + ); + } + } catch (e) { + // If parsing fails, just use empty array + } + return ( ); diff --git a/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx b/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx index 7602cdf3..c4e05e3a 100644 --- a/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx @@ -29,6 +29,7 @@ export function CanvasPage({ profile, preferences, pubRecord, + theme, prerenderedCodeBlocks, bskyPostData, pollData, @@ -42,7 +43,8 @@ export function CanvasPage({ document: PostPageData; blocks: PubLeafletPagesCanvas.Block[]; profile: ProfileViewDetailed; - pubRecord: PubLeafletPublication.Record; + pubRecord?: PubLeafletPublication.Record; + theme?: PubLeafletPublication.Theme | null; did: string; prerenderedCodeBlocks?: Map; bskyPostData: AppBskyFeedDefs.PostView[]; @@ -55,7 +57,7 @@ export function CanvasPage({ }) { if (!document) return null; - let hasPageBackground = !!pubRecord.theme?.showPageBackground; + let hasPageBackground = !!theme?.showPageBackground; let isSubpage = !!pageId; let drawer = useDrawerOpen(document_uri); diff --git a/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx b/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx new file mode 100644 index 00000000..ec13e593 --- /dev/null +++ b/app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx @@ -0,0 +1,139 @@ +import { AtpAgent } from "@atproto/api"; +import { AtUri } from "@atproto/syntax"; +import { ids } from "lexicons/api/lexicons"; +import { + PubLeafletBlocksBskyPost, + PubLeafletDocument, + PubLeafletPagesLinearDocument, + PubLeafletPublication, +} from "lexicons/api"; +import { QuoteHandler } from "./QuoteHandler"; +import { + PublicationBackgroundProvider, + PublicationThemeProvider, +} from "components/ThemeManager/PublicationThemeProvider"; +import { getPostPageData } from "./getPostPageData"; +import { PostPageContextProvider } from "./PostPageContext"; +import { PostPages } from "./PostPages"; +import { extractCodeBlocks } from "./extractCodeBlocks"; +import { LeafletLayout } from "components/LeafletLayout"; +import { fetchPollData } from "./fetchPollData"; + +export async function DocumentPageRenderer({ did, rkey }: { did: string; rkey: string }) { + let agent = new AtpAgent({ + service: "https://public.api.bsky.app", + fetch: (...args) => + fetch(args[0], { + ...args[1], + next: { revalidate: 3600 }, + }), + }); + + let [document, profile] = await Promise.all([ + getPostPageData( + AtUri.make(did, ids.PubLeafletDocument, rkey).toString(), + ), + agent.getProfile({ actor: did }), + ]); + + if (!document?.data) + return ( +
+
+
+

Sorry, post not found!

+

+ This may be a glitch on our end. If the issue persists please{" "} + send us a note. +

+
+
+
+ ); + + let record = document.data as PubLeafletDocument.Record; + let bskyPosts = + record.pages.flatMap((p) => { + let page = p as PubLeafletPagesLinearDocument.Main; + return page.blocks?.filter( + (b) => b.block.$type === ids.PubLeafletBlocksBskyPost, + ); + }) || []; + + // Batch bsky posts into groups of 25 and fetch in parallel + let bskyPostBatches = []; + for (let i = 0; i < bskyPosts.length; i += 25) { + bskyPostBatches.push(bskyPosts.slice(i, i + 25)); + } + + let bskyPostResponses = await Promise.all( + bskyPostBatches.map((batch) => + agent.getPosts( + { + uris: batch.map((p) => { + let block = p?.block as PubLeafletBlocksBskyPost.Main; + return block.postRef.uri; + }), + }, + { headers: {} }, + ), + ), + ); + + let bskyPostData = + bskyPostResponses.length > 0 + ? bskyPostResponses.flatMap((response) => response.data.posts) + : []; + + // Extract poll blocks and fetch vote data + let pollBlocks = record.pages.flatMap((p) => { + let page = p as PubLeafletPagesLinearDocument.Main; + return ( + page.blocks?.filter((b) => b.block.$type === ids.PubLeafletBlocksPoll) || + [] + ); + }); + let pollData = await fetchPollData( + pollBlocks.map((b) => (b.block as any).pollRef.uri), + ); + + // Get theme from publication or document (for standalone docs) + let pubRecord = document.documents_in_publications[0]?.publications + ?.record as PubLeafletPublication.Record | undefined; + let theme = pubRecord?.theme || record.theme || null; + let pub_creator = document.documents_in_publications[0]?.publications + ?.identity_did || did; + + let firstPage = record.pages[0]; + let blocks: PubLeafletPagesLinearDocument.Block[] = []; + if (PubLeafletPagesLinearDocument.isMain(firstPage)) { + blocks = firstPage.blocks || []; + } + + let prerenderedCodeBlocks = await extractCodeBlocks(blocks); + + return ( + + + + + + + + + + + + ); +} diff --git a/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx b/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx index ffa27a28..b252af36 100644 --- a/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx @@ -31,6 +31,7 @@ export function LinearDocumentPage({ profile, preferences, pubRecord, + theme, prerenderedCodeBlocks, bskyPostData, document_uri, @@ -43,7 +44,8 @@ export function LinearDocumentPage({ document: PostPageData; blocks: PubLeafletPagesLinearDocument.Block[]; profile?: ProfileViewDetailed; - pubRecord: PubLeafletPublication.Record; + pubRecord?: PubLeafletPublication.Record; + theme?: PubLeafletPublication.Theme | null; did: string; prerenderedCodeBlocks?: Map; bskyPostData: AppBskyFeedDefs.PostView[]; @@ -56,10 +58,9 @@ export function LinearDocumentPage({ let { identity } = useIdentityData(); let drawer = useDrawerOpen(document_uri); - if (!document || !document.documents_in_publications[0].publications) - return null; + if (!document) return null; - let hasPageBackground = !!pubRecord.theme?.showPageBackground; + let hasPageBackground = !!theme?.showPageBackground; let record = document.data as PubLeafletDocument.Record; const isSubpage = !!pageId; @@ -114,22 +115,24 @@ export function LinearDocumentPage({ Edit Post ) : ( - + document.documents_in_publications[0]?.publications && ( + + ) )} diff --git a/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx b/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx index ebea43ff..1ad7ed76 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx @@ -27,8 +27,7 @@ export function PostHeader(props: { let record = document?.data as PubLeafletDocument.Record; let profile = props.profile; - let pub = props.data?.documents_in_publications[0].publications; - let pubRecord = pub?.record as PubLeafletPublication.Record; + let pub = props.data?.documents_in_publications[0]?.publications; const formattedDate = useLocalizedDate( record.publishedAt || new Date().toISOString(), @@ -36,11 +35,10 @@ export function PostHeader(props: { year: "numeric", month: "long", day: "2-digit", - } + }, ); - if (!document?.data || !document.documents_in_publications[0].publications) - return; + if (!document?.data) return; return (
- - {pub?.name} - + {pub && ( + + {pub?.name} + + )} {identity && - identity.atp_did === - document.documents_in_publications[0]?.publications - .identity_did && + pub && + identity.atp_did === pub.identity_did && document.leaflets_in_publications[0] && ( - | -

{formattedDate}

+ |

{formattedDate}

) : null} |{" "} diff --git a/app/lish/[did]/[publication]/[rkey]/PostPages.tsx b/app/lish/[did]/[publication]/[rkey]/PostPages.tsx index 00b061b2..1bf3d8db 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostPages.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostPages.tsx @@ -114,7 +114,7 @@ export function PostPages({ document: PostPageData; blocks: PubLeafletPagesLinearDocument.Block[]; profile: ProfileViewDetailed; - pubRecord: PubLeafletPublication.Record; + pubRecord?: PubLeafletPublication.Record; did: string; prerenderedCodeBlocks?: Map; bskyPostData: AppBskyFeedDefs.PostView[]; @@ -124,11 +124,14 @@ export function PostPages({ let drawer = useDrawerOpen(document_uri); useInitializeOpenPages(); let pages = useOpenPages(); - if (!document || !document.documents_in_publications[0].publications) - return null; + if (!document) return null; - let hasPageBackground = !!pubRecord.theme?.showPageBackground; let record = document.data as PubLeafletDocument.Record; + + // Get theme from publication or document (for standalone docs) + let theme = pubRecord?.theme || record.theme || null; + let hasPageBackground = !!theme?.showPageBackground; + let quotesAndMentions = document.quotesAndMentions; let fullPageScroll = !hasPageBackground && !drawer && pages.length === 0; @@ -144,6 +147,7 @@ export function PostPages({ pollData={pollData} preferences={preferences} pubRecord={pubRecord} + theme={theme} prerenderedCodeBlocks={prerenderedCodeBlocks} bskyPostData={bskyPostData} document_uri={document_uri} @@ -153,7 +157,7 @@ export function PostPages({ ; @@ -57,7 +40,9 @@ export async function generateMetadata(props: { export default async function Post(props: { params: Promise<{ publication: string; did: string; rkey: string }>; }) { - let did = decodeURIComponent((await props.params).did); + let params = await props.params; + let did = decodeURIComponent(params.did); + if (!did) return (
@@ -68,141 +53,6 @@ export default async function Post(props: {

); - let agent = new AtpAgent({ - service: "https://public.api.bsky.app", - fetch: (...args) => - fetch(args[0], { - ...args[1], - next: { revalidate: 3600 }, - }), - }); - let [document, profile] = await Promise.all([ - getPostPageData( - AtUri.make( - did, - ids.PubLeafletDocument, - (await props.params).rkey, - ).toString(), - ), - agent.getProfile({ actor: did }), - ]); - if (!document?.data || !document.documents_in_publications[0].publications) - return ( -
- ); - let record = document.data as PubLeafletDocument.Record; - let bskyPosts = - record.pages.flatMap((p) => { - let page = p as PubLeafletPagesLinearDocument.Main; - return page.blocks?.filter( - (b) => b.block.$type === ids.PubLeafletBlocksBskyPost, - ); - }) || []; - - // Batch bsky posts into groups of 25 and fetch in parallel - let bskyPostBatches = []; - for (let i = 0; i < bskyPosts.length; i += 25) { - bskyPostBatches.push(bskyPosts.slice(i, i + 25)); - } - - let bskyPostResponses = await Promise.all( - bskyPostBatches.map((batch) => - agent.getPosts( - { - uris: batch.map((p) => { - let block = p?.block as PubLeafletBlocksBskyPost.Main; - return block.postRef.uri; - }), - }, - { headers: {} }, - ), - ), - ); - - let bskyPostData = - bskyPostResponses.length > 0 - ? bskyPostResponses.flatMap((response) => response.data.posts) - : []; - - // Extract poll blocks and fetch vote data - let pollBlocks = record.pages.flatMap((p) => { - let page = p as PubLeafletPagesLinearDocument.Main; - return ( - page.blocks?.filter((b) => b.block.$type === ids.PubLeafletBlocksPoll) || - [] - ); - }); - let pollData = await fetchPollData( - pollBlocks.map((b) => (b.block as any).pollRef.uri), - ); - - let pubRecord = document.documents_in_publications[0]?.publications - .record as PubLeafletPublication.Record; - - let firstPage = record.pages[0]; - let blocks: PubLeafletPagesLinearDocument.Block[] = []; - if (PubLeafletPagesLinearDocument.isMain(firstPage)) { - blocks = firstPage.blocks || []; - } - - let prerenderedCodeBlocks = await extractCodeBlocks(blocks); - - return ( - - - - {/* - TODO: SCROLL PAGE TO FIT DRAWER - If the drawer fits without scrolling, dont scroll - If both drawer and page fit if you scrolled it, scroll it all into the center - If the drawer and pafe doesn't all fit, scroll to drawer - - TODO: SROLL BAR - If there is no drawer && there is no page bg, scroll the entire page - If there is either a drawer open OR a page background, scroll just the post content - - TODO: HIGHLIGHTING BORKED - on chrome, if you scroll backward, things stop working - seems like if you use an older browser, sel direction is not a thing yet - */} - - - - - - - - ); + return ; } diff --git a/app/p/[didOrHandle]/[rkey]/l-quote/[quote]/opengraph-image.ts b/app/p/[didOrHandle]/[rkey]/l-quote/[quote]/opengraph-image.ts new file mode 100644 index 00000000..8d087ed2 --- /dev/null +++ b/app/p/[didOrHandle]/[rkey]/l-quote/[quote]/opengraph-image.ts @@ -0,0 +1,19 @@ +import { getMicroLinkOgImage } from "src/utils/getMicroLinkOgImage"; +import { decodeQuotePosition } from "app/lish/[did]/[publication]/[rkey]/quotePosition"; + +export const runtime = "edge"; +export const revalidate = 60; + +export default async function OpenGraphImage(props: { + params: { didOrHandle: string; rkey: string; quote: string }; +}) { + let quotePosition = decodeQuotePosition(props.params.quote); + return getMicroLinkOgImage( + `/p/${decodeURIComponent(props.params.didOrHandle)}/${props.params.rkey}/l-quote/${props.params.quote}#${quotePosition?.pageId ? `${quotePosition.pageId}~` : ""}${quotePosition?.start.block.join(".")}_${quotePosition?.start.offset}`, + { + width: 620, + height: 324, + deviceScaleFactor: 2, + }, + ); +} diff --git a/app/p/[didOrHandle]/[rkey]/l-quote/[quote]/page.tsx b/app/p/[didOrHandle]/[rkey]/l-quote/[quote]/page.tsx new file mode 100644 index 00000000..ccabe15e --- /dev/null +++ b/app/p/[didOrHandle]/[rkey]/l-quote/[quote]/page.tsx @@ -0,0 +1,8 @@ +import PostPage from "app/p/[didOrHandle]/[rkey]/page"; + +export { generateMetadata } from "app/p/[didOrHandle]/[rkey]/page"; +export default async function Post(props: { + params: Promise<{ didOrHandle: string; rkey: string }>; +}) { + return ; +} diff --git a/app/p/[didOrHandle]/[rkey]/page.tsx b/app/p/[didOrHandle]/[rkey]/page.tsx new file mode 100644 index 00000000..34d7a248 --- /dev/null +++ b/app/p/[didOrHandle]/[rkey]/page.tsx @@ -0,0 +1,90 @@ +import { supabaseServerClient } from "supabase/serverClient"; +import { AtUri } from "@atproto/syntax"; +import { ids } from "lexicons/api/lexicons"; +import { PubLeafletDocument } from "lexicons/api"; +import { Metadata } from "next"; +import { idResolver } from "app/(home-pages)/reader/idResolver"; +import { DocumentPageRenderer } from "app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer"; + +export async function generateMetadata(props: { + params: Promise<{ didOrHandle: string; rkey: string }>; +}): Promise { + let params = await props.params; + let didOrHandle = decodeURIComponent(params.didOrHandle); + + // Resolve handle to DID if necessary + let did = didOrHandle; + if (!didOrHandle.startsWith("did:")) { + try { + let resolved = await idResolver.handle.resolve(didOrHandle); + if (resolved) did = resolved; + } catch (e) { + return { title: "404" }; + } + } + + let { data: document } = await supabaseServerClient + .from("documents") + .select("*, documents_in_publications(publications(*))") + .eq("uri", AtUri.make(did, ids.PubLeafletDocument, params.rkey)) + .single(); + + if (!document) return { title: "404" }; + + let docRecord = document.data as PubLeafletDocument.Record; + + // For documents in publications, include publication name + let publicationName = document.documents_in_publications[0]?.publications?.name; + + return { + icons: { + other: { + rel: "alternate", + url: document.uri, + }, + }, + title: publicationName + ? `${docRecord.title} - ${publicationName}` + : docRecord.title, + description: docRecord?.description || "", + }; +} + +export default async function StandaloneDocumentPage(props: { + params: Promise<{ didOrHandle: string; rkey: string }>; +}) { + let params = await props.params; + let didOrHandle = decodeURIComponent(params.didOrHandle); + + // Resolve handle to DID if necessary + let did = didOrHandle; + if (!didOrHandle.startsWith("did:")) { + try { + let resolved = await idResolver.handle.resolve(didOrHandle); + if (!resolved) { + return ( +
+

Sorry, can't resolve handle.

+

+ This may be a glitch on our end. If the issue persists please{" "} + send us a note. +

+
+ ); + } + did = resolved; + } catch (e) { + return ( +
+

Sorry, can't resolve handle.

+

+ This may be a glitch on our end. If the issue persists please{" "} + send us a note. +

+
+ ); + } + } + + return ; +} diff --git a/components/Pages/PublicationMetadata.tsx b/components/Pages/PublicationMetadata.tsx index e8e6dc87..21d65873 100644 --- a/components/Pages/PublicationMetadata.tsx +++ b/components/Pages/PublicationMetadata.tsx @@ -25,7 +25,7 @@ export const PublicationMetadata = () => { let record = pub?.documents?.data as PubLeafletDocument.Record | null; let publishedAt = record?.publishedAt; - if (!pub || !pub.publications) return null; + if (!pub) return null; if (typeof title !== "string") { title = pub?.title || ""; @@ -36,16 +36,18 @@ export const PublicationMetadata = () => { return (
- - {pub.publications?.name} - + {pub.publications && ( + + {pub.publications?.name} + + )}
Editor
@@ -81,7 +83,11 @@ export const PublicationMetadata = () => { View Post