a tool for shared writing and social publishing

render standalone published docs at /p/

+385 -225
+20
actions/publishToPublication.ts
··· 52 52 leaflet_id, 53 53 title, 54 54 description, 55 + entitiesToDelete, 55 56 }: { 56 57 root_entity: string; 57 58 publication_uri?: string; 58 59 leaflet_id: string; 59 60 title?: string; 60 61 description?: string; 62 + entitiesToDelete?: string[]; 61 63 }) { 62 64 const oauthClient = await createOauthClient(); 63 65 let identity = await getIdentityData(); ··· 179 181 .eq("leaflet", leaflet_id) 180 182 .eq("publication", publication_uri), 181 183 ]); 184 + 185 + // Heuristic: Remove title entities if this is the first time publishing 186 + // (when coming from a standalone leaflet with entitiesToDelete passed in) 187 + if (entitiesToDelete && entitiesToDelete.length > 0 && !existingDocUri) { 188 + await supabaseServerClient 189 + .from("entities") 190 + .delete() 191 + .in("id", entitiesToDelete); 192 + } 182 193 } else { 183 194 // Publishing standalone - update leaflets_to_documents 184 195 await supabaseServerClient.from("leaflets_to_documents").upsert({ ··· 187 198 title: title || "Untitled", 188 199 description: description || "", 189 200 }); 201 + 202 + // Heuristic: Remove title entities if this is the first time publishing standalone 203 + // (when entitiesToDelete is provided and there's no existing document) 204 + if (entitiesToDelete && entitiesToDelete.length > 0 && !existingDocUri) { 205 + await supabaseServerClient 206 + .from("entities") 207 + .delete() 208 + .in("id", entitiesToDelete); 209 + } 190 210 } 191 211 192 212 return { rkey, record: JSON.parse(JSON.stringify(record)) };
+15 -8
app/[leaflet_id]/actions/PublishButton.tsx
··· 190 190 // For looseleaf, navigate without publication_uri 191 191 if (selectedPub === "looseleaf") { 192 192 router.push( 193 - `${permission_token.id}/publish?title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}`, 193 + `${permission_token.id}/publish?title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}&entitiesToDelete=${encodeURIComponent(JSON.stringify(entitiesToDelete))}`, 194 194 ); 195 195 } else { 196 196 router.push( 197 - `${permission_token.id}/publish?publication_uri=${encodeURIComponent(selectedPub)}&title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}`, 197 + `${permission_token.id}/publish?publication_uri=${encodeURIComponent(selectedPub)}&title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}&entitiesToDelete=${encodeURIComponent(JSON.stringify(entitiesToDelete))}`, 198 198 ); 199 199 } 200 200 }} ··· 362 362 363 363 let useTitle = (entityID: string) => { 364 364 let rootPage = useEntity(entityID, "root/page")[0].data.value; 365 - let firstBlock = useBlocks(rootPage)[0]?.value; 365 + let blocks = useBlocks(rootPage); 366 + let firstBlock = blocks[0]; 366 367 367 - let firstBlockText = useEntity(firstBlock, "block/text")?.data.value; 368 + let firstBlockText = useEntity(firstBlock?.value, "block/text")?.data.value; 368 369 369 370 const leafletTitle = useMemo(() => { 370 371 if (!firstBlockText) return "Untitled"; ··· 375 376 return YJSFragmentToString(nodes[0]) || "Untitled"; 376 377 }, [firstBlockText]); 377 378 378 - let secondBlock = useBlocks(rootPage)[1]; 379 - let secondBlockTextValue = useEntity(secondBlock.value, "block/text")?.data 379 + let secondBlock = blocks[1]; 380 + let secondBlockTextValue = useEntity(secondBlock?.value, "block/text")?.data 380 381 .value; 381 382 const secondBlockText = useMemo(() => { 382 383 if (!secondBlockTextValue) return ""; ··· 388 389 }, [firstBlockText]); 389 390 390 391 let entitiesToDelete = useMemo(() => { 391 - let etod = [firstBlock]; 392 - if (secondBlockText.trim() === "" && secondBlock.type === "text") 392 + let etod: string[] = []; 393 + // Only delete first block if it's a heading type 394 + if (firstBlock?.type === "heading") { 395 + etod.push(firstBlock.value); 396 + } 397 + // Delete second block if it's empty text 398 + if (secondBlockText.trim() === "" && secondBlock?.type === "text") { 393 399 etod.push(secondBlock.value); 400 + } 394 401 return etod; 395 402 }, [firstBlock, secondBlockText, secondBlock]); 396 403
+2
app/[leaflet_id]/publish/PublishPost.tsx
··· 30 30 publication_uri?: string; 31 31 record?: PubLeafletPublication.Record; 32 32 posts_in_pub?: number; 33 + entitiesToDelete?: string[]; 33 34 }; 34 35 35 36 export function PublishPost(props: Props) { ··· 74 75 leaflet_id: props.leaflet_id, 75 76 title: props.title, 76 77 description: props.description, 78 + entitiesToDelete: props.entitiesToDelete, 77 79 }); 78 80 if (!doc) return; 79 81
+15
app/[leaflet_id]/publish/page.tsx
··· 17 17 publication_uri: string; 18 18 title: string; 19 19 description: string; 20 + entitiesToDelete: string; 20 21 }>; 21 22 }; 22 23 export default async function PublishLeafletPage(props: Props) { ··· 85 86 let agent = new AtpAgent({ service: "https://public.api.bsky.app" }); 86 87 let profile = await agent.getProfile({ actor: identity.atp_did }); 87 88 89 + // Parse entitiesToDelete from URL params 90 + let searchParams = await props.searchParams; 91 + let entitiesToDelete: string[] = []; 92 + try { 93 + if (searchParams.entitiesToDelete) { 94 + entitiesToDelete = JSON.parse( 95 + decodeURIComponent(searchParams.entitiesToDelete), 96 + ); 97 + } 98 + } catch (e) { 99 + // If parsing fails, just use empty array 100 + } 101 + 88 102 return ( 89 103 <ReplicacheProvider 90 104 rootEntity={rootEntity} ··· 101 115 publication_uri={publication?.uri} 102 116 record={publication?.record as PubLeafletPublication.Record | undefined} 103 117 posts_in_pub={publication?.documents_in_publications[0]?.count} 118 + entitiesToDelete={entitiesToDelete} 104 119 /> 105 120 </ReplicacheProvider> 106 121 );
+4 -2
app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx
··· 29 29 profile, 30 30 preferences, 31 31 pubRecord, 32 + theme, 32 33 prerenderedCodeBlocks, 33 34 bskyPostData, 34 35 pollData, ··· 42 43 document: PostPageData; 43 44 blocks: PubLeafletPagesCanvas.Block[]; 44 45 profile: ProfileViewDetailed; 45 - pubRecord: PubLeafletPublication.Record; 46 + pubRecord?: PubLeafletPublication.Record; 47 + theme?: PubLeafletPublication.Theme | null; 46 48 did: string; 47 49 prerenderedCodeBlocks?: Map<string, string>; 48 50 bskyPostData: AppBskyFeedDefs.PostView[]; ··· 55 57 }) { 56 58 if (!document) return null; 57 59 58 - let hasPageBackground = !!pubRecord.theme?.showPageBackground; 60 + let hasPageBackground = !!theme?.showPageBackground; 59 61 let isSubpage = !!pageId; 60 62 let drawer = useDrawerOpen(document_uri); 61 63
+139
app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx
··· 1 + import { AtpAgent } from "@atproto/api"; 2 + import { AtUri } from "@atproto/syntax"; 3 + import { ids } from "lexicons/api/lexicons"; 4 + import { 5 + PubLeafletBlocksBskyPost, 6 + PubLeafletDocument, 7 + PubLeafletPagesLinearDocument, 8 + PubLeafletPublication, 9 + } from "lexicons/api"; 10 + import { QuoteHandler } from "./QuoteHandler"; 11 + import { 12 + PublicationBackgroundProvider, 13 + PublicationThemeProvider, 14 + } from "components/ThemeManager/PublicationThemeProvider"; 15 + import { getPostPageData } from "./getPostPageData"; 16 + import { PostPageContextProvider } from "./PostPageContext"; 17 + import { PostPages } from "./PostPages"; 18 + import { extractCodeBlocks } from "./extractCodeBlocks"; 19 + import { LeafletLayout } from "components/LeafletLayout"; 20 + import { fetchPollData } from "./fetchPollData"; 21 + 22 + export async function DocumentPageRenderer({ did, rkey }: { did: string; rkey: string }) { 23 + let agent = new AtpAgent({ 24 + service: "https://public.api.bsky.app", 25 + fetch: (...args) => 26 + fetch(args[0], { 27 + ...args[1], 28 + next: { revalidate: 3600 }, 29 + }), 30 + }); 31 + 32 + let [document, profile] = await Promise.all([ 33 + getPostPageData( 34 + AtUri.make(did, ids.PubLeafletDocument, rkey).toString(), 35 + ), 36 + agent.getProfile({ actor: did }), 37 + ]); 38 + 39 + if (!document?.data) 40 + return ( 41 + <div className="bg-bg-leaflet h-full p-3 text-center relative"> 42 + <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 max-w-md w-full"> 43 + <div className=" px-3 py-4 opaque-container flex flex-col gap-1 mx-2 "> 44 + <h3>Sorry, post not found!</h3> 45 + <p> 46 + This may be a glitch on our end. If the issue persists please{" "} 47 + <a href="mailto:contact@leaflet.pub">send us a note</a>. 48 + </p> 49 + </div> 50 + </div> 51 + </div> 52 + ); 53 + 54 + let record = document.data as PubLeafletDocument.Record; 55 + let bskyPosts = 56 + record.pages.flatMap((p) => { 57 + let page = p as PubLeafletPagesLinearDocument.Main; 58 + return page.blocks?.filter( 59 + (b) => b.block.$type === ids.PubLeafletBlocksBskyPost, 60 + ); 61 + }) || []; 62 + 63 + // Batch bsky posts into groups of 25 and fetch in parallel 64 + let bskyPostBatches = []; 65 + for (let i = 0; i < bskyPosts.length; i += 25) { 66 + bskyPostBatches.push(bskyPosts.slice(i, i + 25)); 67 + } 68 + 69 + let bskyPostResponses = await Promise.all( 70 + bskyPostBatches.map((batch) => 71 + agent.getPosts( 72 + { 73 + uris: batch.map((p) => { 74 + let block = p?.block as PubLeafletBlocksBskyPost.Main; 75 + return block.postRef.uri; 76 + }), 77 + }, 78 + { headers: {} }, 79 + ), 80 + ), 81 + ); 82 + 83 + let bskyPostData = 84 + bskyPostResponses.length > 0 85 + ? bskyPostResponses.flatMap((response) => response.data.posts) 86 + : []; 87 + 88 + // Extract poll blocks and fetch vote data 89 + let pollBlocks = record.pages.flatMap((p) => { 90 + let page = p as PubLeafletPagesLinearDocument.Main; 91 + return ( 92 + page.blocks?.filter((b) => b.block.$type === ids.PubLeafletBlocksPoll) || 93 + [] 94 + ); 95 + }); 96 + let pollData = await fetchPollData( 97 + pollBlocks.map((b) => (b.block as any).pollRef.uri), 98 + ); 99 + 100 + // Get theme from publication or document (for standalone docs) 101 + let pubRecord = document.documents_in_publications[0]?.publications 102 + ?.record as PubLeafletPublication.Record | undefined; 103 + let theme = pubRecord?.theme || record.theme || null; 104 + let pub_creator = document.documents_in_publications[0]?.publications 105 + ?.identity_did || did; 106 + 107 + let firstPage = record.pages[0]; 108 + let blocks: PubLeafletPagesLinearDocument.Block[] = []; 109 + if (PubLeafletPagesLinearDocument.isMain(firstPage)) { 110 + blocks = firstPage.blocks || []; 111 + } 112 + 113 + let prerenderedCodeBlocks = await extractCodeBlocks(blocks); 114 + 115 + return ( 116 + <PostPageContextProvider value={document}> 117 + <PublicationThemeProvider theme={theme} pub_creator={pub_creator}> 118 + <PublicationBackgroundProvider theme={theme} pub_creator={pub_creator}> 119 + <LeafletLayout> 120 + <PostPages 121 + document_uri={document.uri} 122 + preferences={pubRecord?.preferences || {}} 123 + pubRecord={pubRecord} 124 + profile={JSON.parse(JSON.stringify(profile.data))} 125 + document={document} 126 + bskyPostData={bskyPostData} 127 + did={did} 128 + blocks={blocks} 129 + prerenderedCodeBlocks={prerenderedCodeBlocks} 130 + pollData={pollData} 131 + /> 132 + </LeafletLayout> 133 + 134 + <QuoteHandler /> 135 + </PublicationBackgroundProvider> 136 + </PublicationThemeProvider> 137 + </PostPageContextProvider> 138 + ); 139 + }
+23 -20
app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx
··· 31 31 profile, 32 32 preferences, 33 33 pubRecord, 34 + theme, 34 35 prerenderedCodeBlocks, 35 36 bskyPostData, 36 37 document_uri, ··· 43 44 document: PostPageData; 44 45 blocks: PubLeafletPagesLinearDocument.Block[]; 45 46 profile?: ProfileViewDetailed; 46 - pubRecord: PubLeafletPublication.Record; 47 + pubRecord?: PubLeafletPublication.Record; 48 + theme?: PubLeafletPublication.Theme | null; 47 49 did: string; 48 50 prerenderedCodeBlocks?: Map<string, string>; 49 51 bskyPostData: AppBskyFeedDefs.PostView[]; ··· 56 58 let { identity } = useIdentityData(); 57 59 let drawer = useDrawerOpen(document_uri); 58 60 59 - if (!document || !document.documents_in_publications[0].publications) 60 - return null; 61 + if (!document) return null; 61 62 62 - let hasPageBackground = !!pubRecord.theme?.showPageBackground; 63 + let hasPageBackground = !!theme?.showPageBackground; 63 64 let record = document.data as PubLeafletDocument.Record; 64 65 65 66 const isSubpage = !!pageId; ··· 114 115 <EditTiny /> Edit Post 115 116 </a> 116 117 ) : ( 117 - <SubscribeWithBluesky 118 - isPost 119 - base_url={getPublicationURL( 120 - document.documents_in_publications[0].publications, 121 - )} 122 - pub_uri={ 123 - document.documents_in_publications[0].publications.uri 124 - } 125 - subscribers={ 126 - document.documents_in_publications[0].publications 127 - .publication_subscriptions 128 - } 129 - pubName={ 130 - document.documents_in_publications[0].publications.name 131 - } 132 - /> 118 + document.documents_in_publications[0]?.publications && ( 119 + <SubscribeWithBluesky 120 + isPost 121 + base_url={getPublicationURL( 122 + document.documents_in_publications[0].publications, 123 + )} 124 + pub_uri={ 125 + document.documents_in_publications[0].publications.uri 126 + } 127 + subscribers={ 128 + document.documents_in_publications[0].publications 129 + .publication_subscriptions 130 + } 131 + pubName={ 132 + document.documents_in_publications[0].publications.name 133 + } 134 + /> 135 + ) 133 136 )} 134 137 </div> 135 138 </>
+14 -21
app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx
··· 27 27 28 28 let record = document?.data as PubLeafletDocument.Record; 29 29 let profile = props.profile; 30 - let pub = props.data?.documents_in_publications[0].publications; 31 - let pubRecord = pub?.record as PubLeafletPublication.Record; 30 + let pub = props.data?.documents_in_publications[0]?.publications; 32 31 33 32 const formattedDate = useLocalizedDate( 34 33 record.publishedAt || new Date().toISOString(), ··· 36 35 year: "numeric", 37 36 month: "long", 38 37 day: "2-digit", 39 - } 38 + }, 40 39 ); 41 40 42 - if (!document?.data || !document.documents_in_publications[0].publications) 43 - return; 41 + if (!document?.data) return; 44 42 return ( 45 43 <div 46 44 className="max-w-prose w-full mx-auto px-3 sm:px-4 sm:pt-3 pt-2" ··· 48 46 > 49 47 <div className="pubHeader flex flex-col pb-5"> 50 48 <div className="flex justify-between w-full"> 51 - <SpeedyLink 52 - className="font-bold hover:no-underline text-accent-contrast" 53 - href={ 54 - document && 55 - getPublicationURL( 56 - document.documents_in_publications[0].publications, 57 - ) 58 - } 59 - > 60 - {pub?.name} 61 - </SpeedyLink> 49 + {pub && ( 50 + <SpeedyLink 51 + className="font-bold hover:no-underline text-accent-contrast" 52 + href={document && getPublicationURL(pub)} 53 + > 54 + {pub?.name} 55 + </SpeedyLink> 56 + )} 62 57 {identity && 63 - identity.atp_did === 64 - document.documents_in_publications[0]?.publications 65 - .identity_did && 58 + pub && 59 + identity.atp_did === pub.identity_did && 66 60 document.leaflets_in_publications[0] && ( 67 61 <a 68 62 className=" rounded-full flex place-items-center" ··· 90 84 ) : null} 91 85 {record.publishedAt ? ( 92 86 <> 93 - | 94 - <p>{formattedDate}</p> 87 + |<p>{formattedDate}</p> 95 88 </> 96 89 ) : null} 97 90 |{" "}
+12 -6
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 114 114 document: PostPageData; 115 115 blocks: PubLeafletPagesLinearDocument.Block[]; 116 116 profile: ProfileViewDetailed; 117 - pubRecord: PubLeafletPublication.Record; 117 + pubRecord?: PubLeafletPublication.Record; 118 118 did: string; 119 119 prerenderedCodeBlocks?: Map<string, string>; 120 120 bskyPostData: AppBskyFeedDefs.PostView[]; ··· 124 124 let drawer = useDrawerOpen(document_uri); 125 125 useInitializeOpenPages(); 126 126 let pages = useOpenPages(); 127 - if (!document || !document.documents_in_publications[0].publications) 128 - return null; 127 + if (!document) return null; 129 128 130 - let hasPageBackground = !!pubRecord.theme?.showPageBackground; 131 129 let record = document.data as PubLeafletDocument.Record; 130 + 131 + // Get theme from publication or document (for standalone docs) 132 + let theme = pubRecord?.theme || record.theme || null; 133 + let hasPageBackground = !!theme?.showPageBackground; 134 + 132 135 let quotesAndMentions = document.quotesAndMentions; 133 136 134 137 let fullPageScroll = !hasPageBackground && !drawer && pages.length === 0; ··· 144 147 pollData={pollData} 145 148 preferences={preferences} 146 149 pubRecord={pubRecord} 150 + theme={theme} 147 151 prerenderedCodeBlocks={prerenderedCodeBlocks} 148 152 bskyPostData={bskyPostData} 149 153 document_uri={document_uri} ··· 153 157 <InteractionDrawer 154 158 document_uri={document.uri} 155 159 comments={ 156 - pubRecord.preferences?.showComments === false 160 + pubRecord?.preferences?.showComments === false 157 161 ? [] 158 162 : document.comments_on_documents 159 163 } ··· 190 194 preferences={preferences} 191 195 profile={profile} 192 196 pubRecord={pubRecord} 197 + theme={theme} 193 198 prerenderedCodeBlocks={prerenderedCodeBlocks} 194 199 pollData={pollData} 195 200 bskyPostData={bskyPostData} ··· 211 216 did={did} 212 217 preferences={preferences} 213 218 pubRecord={pubRecord} 219 + theme={theme} 214 220 pollData={pollData} 215 221 prerenderedCodeBlocks={prerenderedCodeBlocks} 216 222 bskyPostData={bskyPostData} ··· 229 235 pageId={page.id} 230 236 document_uri={document.uri} 231 237 comments={ 232 - pubRecord.preferences?.showComments === false 238 + pubRecord?.preferences?.showComments === false 233 239 ? [] 234 240 : document.comments_on_documents 235 241 }
+6 -156
app/lish/[did]/[publication]/[rkey]/page.tsx
··· 1 1 import { supabaseServerClient } from "supabase/serverClient"; 2 2 import { AtUri } from "@atproto/syntax"; 3 3 import { ids } from "lexicons/api/lexicons"; 4 - import { 5 - PubLeafletBlocksBskyPost, 6 - PubLeafletDocument, 7 - PubLeafletPagesLinearDocument, 8 - PubLeafletPublication, 9 - } from "lexicons/api"; 4 + import { PubLeafletDocument } from "lexicons/api"; 10 5 import { Metadata } from "next"; 11 - import { AtpAgent } from "@atproto/api"; 12 - import { QuoteHandler } from "./QuoteHandler"; 13 - import { InteractionDrawer } from "./Interactions/InteractionDrawer"; 14 - import { 15 - PublicationBackgroundProvider, 16 - PublicationThemeProvider, 17 - } from "components/ThemeManager/PublicationThemeProvider"; 18 - import { getPostPageData } from "./getPostPageData"; 19 - import { PostPageContextProvider } from "./PostPageContext"; 20 - import { PostPages } from "./PostPages"; 21 - import { extractCodeBlocks } from "./extractCodeBlocks"; 22 - import { LeafletLayout } from "components/LeafletLayout"; 23 - import { fetchPollData } from "./fetchPollData"; 6 + import { DocumentPageRenderer } from "./DocumentPageRenderer"; 24 7 25 8 export async function generateMetadata(props: { 26 9 params: Promise<{ publication: string; did: string; rkey: string }>; ··· 57 40 export default async function Post(props: { 58 41 params: Promise<{ publication: string; did: string; rkey: string }>; 59 42 }) { 60 - let did = decodeURIComponent((await props.params).did); 43 + let params = await props.params; 44 + let did = decodeURIComponent(params.did); 45 + 61 46 if (!did) 62 47 return ( 63 48 <div className="p-4 text-lg text-center flex flex-col gap-4"> ··· 68 53 </p> 69 54 </div> 70 55 ); 71 - let agent = new AtpAgent({ 72 - service: "https://public.api.bsky.app", 73 - fetch: (...args) => 74 - fetch(args[0], { 75 - ...args[1], 76 - next: { revalidate: 3600 }, 77 - }), 78 - }); 79 - let [document, profile] = await Promise.all([ 80 - getPostPageData( 81 - AtUri.make( 82 - did, 83 - ids.PubLeafletDocument, 84 - (await props.params).rkey, 85 - ).toString(), 86 - ), 87 - agent.getProfile({ actor: did }), 88 - ]); 89 - if (!document?.data || !document.documents_in_publications[0].publications) 90 - return ( 91 - <div className="bg-bg-leaflet h-full p-3 text-center relative"> 92 - <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 max-w-md w-full"> 93 - <div className=" px-3 py-4 opaque-container flex flex-col gap-1 mx-2 "> 94 - <h3>Sorry, post not found!</h3> 95 - <p> 96 - This may be a glitch on our end. If the issue persists please{" "} 97 - <a href="mailto:contact@leaflet.pub">send us a note</a>. 98 - </p> 99 - </div> 100 - </div> 101 - </div> 102 - ); 103 - let record = document.data as PubLeafletDocument.Record; 104 - let bskyPosts = 105 - record.pages.flatMap((p) => { 106 - let page = p as PubLeafletPagesLinearDocument.Main; 107 - return page.blocks?.filter( 108 - (b) => b.block.$type === ids.PubLeafletBlocksBskyPost, 109 - ); 110 - }) || []; 111 56 112 - // Batch bsky posts into groups of 25 and fetch in parallel 113 - let bskyPostBatches = []; 114 - for (let i = 0; i < bskyPosts.length; i += 25) { 115 - bskyPostBatches.push(bskyPosts.slice(i, i + 25)); 116 - } 117 - 118 - let bskyPostResponses = await Promise.all( 119 - bskyPostBatches.map((batch) => 120 - agent.getPosts( 121 - { 122 - uris: batch.map((p) => { 123 - let block = p?.block as PubLeafletBlocksBskyPost.Main; 124 - return block.postRef.uri; 125 - }), 126 - }, 127 - { headers: {} }, 128 - ), 129 - ), 130 - ); 131 - 132 - let bskyPostData = 133 - bskyPostResponses.length > 0 134 - ? bskyPostResponses.flatMap((response) => response.data.posts) 135 - : []; 136 - 137 - // Extract poll blocks and fetch vote data 138 - let pollBlocks = record.pages.flatMap((p) => { 139 - let page = p as PubLeafletPagesLinearDocument.Main; 140 - return ( 141 - page.blocks?.filter((b) => b.block.$type === ids.PubLeafletBlocksPoll) || 142 - [] 143 - ); 144 - }); 145 - let pollData = await fetchPollData( 146 - pollBlocks.map((b) => (b.block as any).pollRef.uri), 147 - ); 148 - 149 - let pubRecord = document.documents_in_publications[0]?.publications 150 - .record as PubLeafletPublication.Record; 151 - 152 - let firstPage = record.pages[0]; 153 - let blocks: PubLeafletPagesLinearDocument.Block[] = []; 154 - if (PubLeafletPagesLinearDocument.isMain(firstPage)) { 155 - blocks = firstPage.blocks || []; 156 - } 157 - 158 - let prerenderedCodeBlocks = await extractCodeBlocks(blocks); 159 - 160 - return ( 161 - <PostPageContextProvider value={document}> 162 - <PublicationThemeProvider 163 - theme={pubRecord?.theme || record.theme || null} 164 - pub_creator={ 165 - document.documents_in_publications[0].publications.identity_did 166 - } 167 - > 168 - <PublicationBackgroundProvider 169 - theme={pubRecord?.theme || record.theme || null} 170 - pub_creator={ 171 - document.documents_in_publications[0].publications.identity_did 172 - } 173 - > 174 - {/* 175 - TODO: SCROLL PAGE TO FIT DRAWER 176 - If the drawer fits without scrolling, dont scroll 177 - If both drawer and page fit if you scrolled it, scroll it all into the center 178 - If the drawer and pafe doesn't all fit, scroll to drawer 179 - 180 - TODO: SROLL BAR 181 - If there is no drawer && there is no page bg, scroll the entire page 182 - If there is either a drawer open OR a page background, scroll just the post content 183 - 184 - TODO: HIGHLIGHTING BORKED 185 - on chrome, if you scroll backward, things stop working 186 - seems like if you use an older browser, sel direction is not a thing yet 187 - */} 188 - <LeafletLayout> 189 - <PostPages 190 - document_uri={document.uri} 191 - preferences={pubRecord.preferences || {}} 192 - pubRecord={pubRecord} 193 - profile={JSON.parse(JSON.stringify(profile.data))} 194 - document={document} 195 - bskyPostData={bskyPostData} 196 - did={did} 197 - blocks={blocks} 198 - prerenderedCodeBlocks={prerenderedCodeBlocks} 199 - pollData={pollData} 200 - /> 201 - </LeafletLayout> 202 - 203 - <QuoteHandler /> 204 - </PublicationBackgroundProvider> 205 - </PublicationThemeProvider> 206 - </PostPageContextProvider> 207 - ); 57 + return <DocumentPageRenderer did={did} rkey={params.rkey} />; 208 58 }
+19
app/p/[didOrHandle]/[rkey]/l-quote/[quote]/opengraph-image.ts
··· 1 + import { getMicroLinkOgImage } from "src/utils/getMicroLinkOgImage"; 2 + import { decodeQuotePosition } from "app/lish/[did]/[publication]/[rkey]/quotePosition"; 3 + 4 + export const runtime = "edge"; 5 + export const revalidate = 60; 6 + 7 + export default async function OpenGraphImage(props: { 8 + params: { didOrHandle: string; rkey: string; quote: string }; 9 + }) { 10 + let quotePosition = decodeQuotePosition(props.params.quote); 11 + return getMicroLinkOgImage( 12 + `/p/${decodeURIComponent(props.params.didOrHandle)}/${props.params.rkey}/l-quote/${props.params.quote}#${quotePosition?.pageId ? `${quotePosition.pageId}~` : ""}${quotePosition?.start.block.join(".")}_${quotePosition?.start.offset}`, 13 + { 14 + width: 620, 15 + height: 324, 16 + deviceScaleFactor: 2, 17 + }, 18 + ); 19 + }
+8
app/p/[didOrHandle]/[rkey]/l-quote/[quote]/page.tsx
··· 1 + import PostPage from "app/p/[didOrHandle]/[rkey]/page"; 2 + 3 + export { generateMetadata } from "app/p/[didOrHandle]/[rkey]/page"; 4 + export default async function Post(props: { 5 + params: Promise<{ didOrHandle: string; rkey: string }>; 6 + }) { 7 + return <PostPage {...props} />; 8 + }
+90
app/p/[didOrHandle]/[rkey]/page.tsx
··· 1 + import { supabaseServerClient } from "supabase/serverClient"; 2 + import { AtUri } from "@atproto/syntax"; 3 + import { ids } from "lexicons/api/lexicons"; 4 + import { PubLeafletDocument } from "lexicons/api"; 5 + import { Metadata } from "next"; 6 + import { idResolver } from "app/(home-pages)/reader/idResolver"; 7 + import { DocumentPageRenderer } from "app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer"; 8 + 9 + export async function generateMetadata(props: { 10 + params: Promise<{ didOrHandle: string; rkey: string }>; 11 + }): Promise<Metadata> { 12 + let params = await props.params; 13 + let didOrHandle = decodeURIComponent(params.didOrHandle); 14 + 15 + // Resolve handle to DID if necessary 16 + let did = didOrHandle; 17 + if (!didOrHandle.startsWith("did:")) { 18 + try { 19 + let resolved = await idResolver.handle.resolve(didOrHandle); 20 + if (resolved) did = resolved; 21 + } catch (e) { 22 + return { title: "404" }; 23 + } 24 + } 25 + 26 + let { data: document } = await supabaseServerClient 27 + .from("documents") 28 + .select("*, documents_in_publications(publications(*))") 29 + .eq("uri", AtUri.make(did, ids.PubLeafletDocument, params.rkey)) 30 + .single(); 31 + 32 + if (!document) return { title: "404" }; 33 + 34 + let docRecord = document.data as PubLeafletDocument.Record; 35 + 36 + // For documents in publications, include publication name 37 + let publicationName = document.documents_in_publications[0]?.publications?.name; 38 + 39 + return { 40 + icons: { 41 + other: { 42 + rel: "alternate", 43 + url: document.uri, 44 + }, 45 + }, 46 + title: publicationName 47 + ? `${docRecord.title} - ${publicationName}` 48 + : docRecord.title, 49 + description: docRecord?.description || "", 50 + }; 51 + } 52 + 53 + export default async function StandaloneDocumentPage(props: { 54 + params: Promise<{ didOrHandle: string; rkey: string }>; 55 + }) { 56 + let params = await props.params; 57 + let didOrHandle = decodeURIComponent(params.didOrHandle); 58 + 59 + // Resolve handle to DID if necessary 60 + let did = didOrHandle; 61 + if (!didOrHandle.startsWith("did:")) { 62 + try { 63 + let resolved = await idResolver.handle.resolve(didOrHandle); 64 + if (!resolved) { 65 + return ( 66 + <div className="p-4 text-lg text-center flex flex-col gap-4"> 67 + <p>Sorry, can&apos;t resolve handle.</p> 68 + <p> 69 + This may be a glitch on our end. If the issue persists please{" "} 70 + <a href="mailto:contact@leaflet.pub">send us a note</a>. 71 + </p> 72 + </div> 73 + ); 74 + } 75 + did = resolved; 76 + } catch (e) { 77 + return ( 78 + <div className="p-4 text-lg text-center flex flex-col gap-4"> 79 + <p>Sorry, can&apos;t resolve handle.</p> 80 + <p> 81 + This may be a glitch on our end. If the issue persists please{" "} 82 + <a href="mailto:contact@leaflet.pub">send us a note</a>. 83 + </p> 84 + </div> 85 + ); 86 + } 87 + } 88 + 89 + return <DocumentPageRenderer did={did} rkey={params.rkey} />; 90 + }
+18 -12
components/Pages/PublicationMetadata.tsx
··· 25 25 let record = pub?.documents?.data as PubLeafletDocument.Record | null; 26 26 let publishedAt = record?.publishedAt; 27 27 28 - if (!pub || !pub.publications) return null; 28 + if (!pub) return null; 29 29 30 30 if (typeof title !== "string") { 31 31 title = pub?.title || ""; ··· 36 36 return ( 37 37 <div className={`flex flex-col px-3 sm:px-4 pb-5 sm:pt-3 pt-2`}> 38 38 <div className="flex gap-2"> 39 - <Link 40 - href={ 41 - identity?.atp_did === pub.publications?.identity_did 42 - ? `${getBasePublicationURL(pub.publications)}/dashboard` 43 - : getPublicationURL(pub.publications) 44 - } 45 - className="leafletMetadata text-accent-contrast font-bold hover:no-underline" 46 - > 47 - {pub.publications?.name} 48 - </Link> 39 + {pub.publications && ( 40 + <Link 41 + href={ 42 + identity?.atp_did === pub.publications?.identity_did 43 + ? `${getBasePublicationURL(pub.publications)}/dashboard` 44 + : getPublicationURL(pub.publications) 45 + } 46 + className="leafletMetadata text-accent-contrast font-bold hover:no-underline" 47 + > 48 + {pub.publications?.name} 49 + </Link> 50 + )} 49 51 <div className="font-bold text-tertiary px-1 text-sm flex place-items-center bg-border-light rounded-md "> 50 52 Editor 51 53 </div> ··· 81 83 <Link 82 84 target="_blank" 83 85 className="text-sm" 84 - href={`${getPublicationURL(pub.publications)}/${new AtUri(pub.doc).rkey}`} 86 + href={ 87 + pub.publications 88 + ? `${getPublicationURL(pub.publications)}/${new AtUri(pub.doc).rkey}` 89 + : `/p/${identity?.atp_did}/${new AtUri(pub.doc).rkey}` 90 + } 85 91 > 86 92 View Post 87 93 </Link>