a tool for shared writing and social publishing
0

Configure Feed

Select the types of activity you want to include in your feed.

de-duplicate things

Jared Pereira (May 20, 2026, 1:38 AM EDT) 2a7a5928 1880ebb1

+294 -354
+2 -6
app/lish/[did]/[publication]/PublicationNav.tsx
··· 1 1 import { SpeedyLink } from "components/SpeedyLink"; 2 + import { sortPublicationPages } from "./sortPublicationPages"; 2 3 3 4 export type PublicationNavPage = { 4 5 id: number; ··· 13 14 }) { 14 15 if (props.pages.length === 0) return null; 15 16 16 - let sortedPages = [...props.pages].sort((a, b) => { 17 - let aHome = a.path === "/" ? 0 : 1; 18 - let bHome = b.path === "/" ? 0 : 1; 19 - if (aHome !== bHome) return aHome - bHome; 20 - return (a.path ?? "").localeCompare(b.path ?? ""); 21 - }); 17 + let sortedPages = sortPublicationPages(props.pages); 22 18 23 19 return ( 24 20 <nav className="publicationNav border-t border-b border-border-light w-full sm:max-w-[calc(var(--page-width-units)*1.25)] mx-auto">
+13 -137
app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx
··· 1 1 import { AtpAgent } from "@atproto/api"; 2 - import { ids } from "lexicons/api/lexicons"; 3 2 import { 4 - PubLeafletBlocksBskyPost, 5 - PubLeafletBlocksStandardSitePost, 6 - PubLeafletBlocksOrderedList, 7 - PubLeafletBlocksUnorderedList, 8 3 PubLeafletPagesLinearDocument, 9 4 PubLeafletPagesCanvas, 10 - PubLeafletBlocksPoll, 11 5 } from "lexicons/api"; 12 - import { type $Typed } from "lexicons/api/util"; 13 - import { get_standard_site_posts } from "app/api/rpc/[command]/get_standard_site_posts"; 14 - import { supabaseServerClient } from "supabase/serverClient"; 15 6 import { QuoteHandler } from "./QuoteHandler"; 16 7 import { 17 8 PublicationBackgroundProvider, ··· 19 10 } from "components/ThemeManager/PublicationThemeProvider"; 20 11 import { getPostPageData } from "./getPostPageData"; 21 12 import { PostPages } from "./PostPages"; 22 - import { extractCodeBlocks } from "./extractCodeBlocks"; 13 + import { collectAndFetchBlockResources } from "./collectAndFetchBlockResources"; 23 14 import { LeafletLayout } from "components/LeafletLayout"; 24 - import { fetchPollData } from "./fetchPollData"; 25 - import { 26 - getDocumentPages, 27 - hasLeafletContent, 28 - } from "src/utils/normalizeRecords"; 15 + import { getDocumentPages } from "src/utils/normalizeRecords"; 29 16 import { DocumentProvider } from "contexts/DocumentContext"; 30 17 import { LeafletContentProvider } from "contexts/LeafletContentContext"; 31 18 import { FontLoader } from "components/FontLoader"; ··· 74 61 </div> 75 62 </div> 76 63 ); 77 - let bskyPosts = 78 - pages.flatMap((p) => { 79 - let page = p as PubLeafletPagesLinearDocument.Main; 80 - return extractBlocksByType<$Typed<PubLeafletBlocksBskyPost.Main>>( 81 - page.blocks || [], 82 - ids.PubLeafletBlocksBskyPost, 83 - ); 84 - }) || []; 85 64 86 - // Batch bsky posts into groups of 25 and fetch in parallel 87 - let bskyPostBatches = []; 88 - for (let i = 0; i < bskyPosts.length; i += 25) { 89 - bskyPostBatches.push(bskyPosts.slice(i, i + 25)); 90 - } 91 - 92 - let bskyPostResponses = await Promise.all( 93 - bskyPostBatches.map((batch) => 94 - agent.getPosts( 95 - { 96 - uris: batch.map((p) => { 97 - let block = p?.block as unknown as PubLeafletBlocksBskyPost.Main; 98 - return block.postRef.uri; 99 - }), 100 - }, 101 - { headers: {} }, 102 - ), 103 - ), 104 - ); 105 - 106 - let bskyPostData = 107 - bskyPostResponses.length > 0 108 - ? bskyPostResponses.flatMap((response) => response.data.posts) 109 - : []; 110 - 111 - let standardSitePostBlocks = pages.flatMap((p) => { 112 - let page = p as PubLeafletPagesLinearDocument.Main; 113 - return extractBlocksByType<$Typed<PubLeafletBlocksStandardSitePost.Main>>( 114 - page.blocks || [], 115 - ids.PubLeafletBlocksStandardSitePost, 116 - ); 117 - }); 118 - let standardSitePostUris = Array.from( 119 - new Set(standardSitePostBlocks.map((b) => b.block.uri)), 120 - ); 121 - let standardSitePostsResult = 122 - standardSitePostUris.length > 0 123 - ? await get_standard_site_posts.handler( 124 - { uris: standardSitePostUris }, 125 - { supabase: supabaseServerClient }, 126 - ) 127 - : { result: { posts: [] } }; 128 - let standardSitePosts = standardSitePostsResult.result.posts; 129 - 130 - // Extract poll blocks and fetch vote data 131 - let pollBlocks = pages.flatMap((p) => { 132 - let page = p as PubLeafletPagesLinearDocument.Main; 133 - return extractBlocksByType<$Typed<PubLeafletBlocksPoll.Main>>( 134 - page.blocks || [], 135 - ids.PubLeafletBlocksPoll, 136 - ); 65 + const { 66 + bskyPostData, 67 + standardSitePostData: standardSitePosts, 68 + pollData, 69 + prerenderedCodeBlocks, 70 + } = await collectAndFetchBlockResources({ 71 + agent, 72 + pages: pages as ( 73 + | PubLeafletPagesLinearDocument.Main 74 + | PubLeafletPagesCanvas.Main 75 + )[], 137 76 }); 138 - let pollData = await fetchPollData( 139 - pollBlocks.map((b) => b.block.pollRef.uri), 140 - ); 141 77 142 78 const pubRecord = document.normalizedPublication; 143 79 let pub_creator = document.publication?.identity_did || did; 144 80 let isStandalone = !pubRecord; 145 - 146 - let firstPage = pages[0]; 147 - let firstPageBlocks = 148 - ( 149 - firstPage as 150 - | PubLeafletPagesLinearDocument.Main 151 - | PubLeafletPagesCanvas.Main 152 - ).blocks || []; 153 - let prerenderedCodeBlocks = await extractCodeBlocks(firstPageBlocks); 154 81 155 82 return ( 156 83 <DocumentProvider value={document}> ··· 204 131 </DocumentProvider> 205 132 ); 206 133 } 207 - 208 - function extractBlocksByType<T extends { $type: string }>( 209 - blocks: PubLeafletPagesLinearDocument.Block[], 210 - type: string, 211 - ): { block: T }[] { 212 - let results: { block: T }[] = []; 213 - for (let b of blocks) { 214 - if (b.block.$type === type) { 215 - results.push(b as unknown as { block: T }); 216 - } 217 - if ( 218 - b.block.$type === ids.PubLeafletBlocksOrderedList || 219 - b.block.$type === ids.PubLeafletBlocksUnorderedList 220 - ) { 221 - let list = b.block as 222 - | PubLeafletBlocksOrderedList.Main 223 - | PubLeafletBlocksUnorderedList.Main; 224 - extractFromListItems(list.children, type, results); 225 - } 226 - } 227 - return results; 228 - } 229 - 230 - function extractFromListItems<T extends { $type: string }>( 231 - items: 232 - | PubLeafletBlocksOrderedList.ListItem[] 233 - | PubLeafletBlocksUnorderedList.ListItem[], 234 - type: string, 235 - results: { block: T }[], 236 - ) { 237 - for (let item of items) { 238 - if ((item.content as { $type?: string })?.$type === type) { 239 - results.push({ 240 - block: item.content as unknown as T, 241 - }); 242 - } 243 - if (item.children) { 244 - extractFromListItems(item.children, type, results); 245 - } 246 - let orderedChildren = (item as PubLeafletBlocksUnorderedList.ListItem) 247 - .orderedListChildren; 248 - if (orderedChildren) { 249 - extractFromListItems(orderedChildren.children, type, results); 250 - } 251 - let unorderedChildren = (item as PubLeafletBlocksOrderedList.ListItem) 252 - .unorderedListChildren; 253 - if (unorderedChildren) { 254 - extractFromListItems(unorderedChildren.children, type, results); 255 - } 256 - } 257 - }
+11 -102
app/lish/[did]/[publication]/[rkey]/PublicationPageRenderer.tsx
··· 1 - import { ids } from "lexicons/api/lexicons"; 2 1 import { 3 - PubLeafletBlocksBskyPost, 4 - PubLeafletBlocksPoll, 5 2 PubLeafletBlocksPostsList, 6 - PubLeafletBlocksStandardSitePost, 7 - PubLeafletBlocksOrderedList, 8 - PubLeafletBlocksUnorderedList, 9 3 PubLeafletPagesLinearDocument, 10 4 PubLeafletPublicationPage, 11 5 } from "lexicons/api"; 12 - import { type $Typed } from "lexicons/api/util"; 13 6 import { AtpAgent } from "@atproto/api"; 14 7 15 - import { supabaseServerClient } from "supabase/serverClient"; 16 8 import { 17 9 normalizeDocumentRecord, 18 10 normalizePublicationRecord, 19 11 type NormalizedPublication, 20 12 } from "src/utils/normalizeRecords"; 21 - import { get_standard_site_posts } from "app/api/rpc/[command]/get_standard_site_posts"; 22 13 23 14 import { 24 15 PublicationBackgroundProvider, ··· 38 29 import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 39 30 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 40 31 41 - import { extractCodeBlocks } from "./extractCodeBlocks"; 42 - import { fetchPollData } from "./fetchPollData"; 32 + import { collectAndFetchBlockResources } from "./collectAndFetchBlockResources"; 43 33 import { PostContent } from "./PostContent"; 44 34 45 35 export type PublicationPageRecord = PubLeafletPublicationPage.Record; ··· 93 83 }), 94 84 }); 95 85 96 - const bskyPostBlocks = extractBlocksByType< 97 - $Typed<PubLeafletBlocksBskyPost.Main> 98 - >(allBlocks, ids.PubLeafletBlocksBskyPost); 99 - const bskyPostBatches: typeof bskyPostBlocks[] = []; 100 - for (let i = 0; i < bskyPostBlocks.length; i += 25) { 101 - bskyPostBatches.push(bskyPostBlocks.slice(i, i + 25)); 102 - } 103 - const bskyPostResponses = await Promise.all( 104 - bskyPostBatches.map((batch) => 105 - agent.getPosts( 106 - { uris: batch.map((p) => p.block.postRef.uri) }, 107 - { headers: {} }, 108 - ), 109 - ), 110 - ); 111 - const bskyPostData = bskyPostResponses.flatMap((r) => r.data.posts); 86 + const resourcePages = 87 + firstPage && firstPage.$type === "pub.leaflet.pages.linearDocument" 88 + ? [firstPage as PubLeafletPagesLinearDocument.Main] 89 + : []; 112 90 113 - const standardSitePostUris = Array.from( 114 - new Set( 115 - extractBlocksByType<$Typed<PubLeafletBlocksStandardSitePost.Main>>( 116 - allBlocks, 117 - ids.PubLeafletBlocksStandardSitePost, 118 - ).map((b) => b.block.uri), 119 - ), 120 - ); 121 - const standardSitePostsResult = 122 - standardSitePostUris.length > 0 123 - ? await get_standard_site_posts.handler( 124 - { uris: standardSitePostUris }, 125 - { supabase: supabaseServerClient }, 126 - ) 127 - : { result: { posts: [] } }; 128 - const standardSitePosts = standardSitePostsResult.result.posts; 129 - 130 - const pollBlocks = extractBlocksByType<$Typed<PubLeafletBlocksPoll.Main>>( 131 - allBlocks, 132 - ids.PubLeafletBlocksPoll, 133 - ); 134 - const pollData = await fetchPollData( 135 - pollBlocks.map((b) => b.block.pollRef.uri), 136 - ); 91 + const { 92 + bskyPostData, 93 + standardSitePostData: standardSitePosts, 94 + pollData, 95 + prerenderedCodeBlocks, 96 + } = await collectAndFetchBlockResources({ agent, pages: resourcePages }); 137 97 138 98 const hasPostsList = allBlocks.some((b) => 139 99 PubLeafletBlocksPostsList.isMain(b.block), ··· 167 127 posts: postsListPosts, 168 128 } 169 129 : undefined; 170 - 171 - const prerenderedCodeBlocks = await extractCodeBlocks(allBlocks); 172 130 173 131 const theme = normalizedPublication?.theme; 174 132 const showPageBackground = !!theme?.showPageBackground; ··· 260 218 </DocumentProvider> 261 219 ); 262 220 } 263 - 264 - function extractBlocksByType<T extends { $type: string }>( 265 - blocks: PubLeafletPagesLinearDocument.Block[], 266 - type: string, 267 - ): { block: T }[] { 268 - const results: { block: T }[] = []; 269 - for (const b of blocks) { 270 - if (b.block.$type === type) { 271 - results.push(b as unknown as { block: T }); 272 - } 273 - if ( 274 - b.block.$type === ids.PubLeafletBlocksOrderedList || 275 - b.block.$type === ids.PubLeafletBlocksUnorderedList 276 - ) { 277 - const list = b.block as 278 - | PubLeafletBlocksOrderedList.Main 279 - | PubLeafletBlocksUnorderedList.Main; 280 - extractFromListItems(list.children, type, results); 281 - } 282 - } 283 - return results; 284 - } 285 - 286 - function extractFromListItems<T extends { $type: string }>( 287 - items: 288 - | PubLeafletBlocksOrderedList.ListItem[] 289 - | PubLeafletBlocksUnorderedList.ListItem[], 290 - type: string, 291 - results: { block: T }[], 292 - ) { 293 - for (const item of items) { 294 - if ((item.content as { $type?: string })?.$type === type) { 295 - results.push({ block: item.content as unknown as T }); 296 - } 297 - if (item.children) { 298 - extractFromListItems(item.children, type, results); 299 - } 300 - const orderedChildren = (item as PubLeafletBlocksUnorderedList.ListItem) 301 - .orderedListChildren; 302 - if (orderedChildren) { 303 - extractFromListItems(orderedChildren.children, type, results); 304 - } 305 - const unorderedChildren = (item as PubLeafletBlocksOrderedList.ListItem) 306 - .unorderedListChildren; 307 - if (unorderedChildren) { 308 - extractFromListItems(unorderedChildren.children, type, results); 309 - } 310 - } 311 - }
+91
app/lish/[did]/[publication]/[rkey]/collectAndFetchBlockResources.ts
··· 1 + import { ids } from "lexicons/api/lexicons"; 2 + import { 3 + PubLeafletBlocksBskyPost, 4 + PubLeafletBlocksPoll, 5 + PubLeafletBlocksStandardSitePost, 6 + PubLeafletPagesLinearDocument, 7 + PubLeafletPagesCanvas, 8 + } from "lexicons/api"; 9 + import { type $Typed } from "lexicons/api/util"; 10 + import { AtpAgent, AppBskyFeedDefs } from "@atproto/api"; 11 + import { supabaseServerClient } from "supabase/serverClient"; 12 + import { 13 + get_standard_site_posts, 14 + type StandardSitePostData, 15 + } from "app/api/rpc/[command]/get_standard_site_posts"; 16 + import { extractBlocksByType } from "./extractBlocksByType"; 17 + import { extractCodeBlocks } from "./extractCodeBlocks"; 18 + import { fetchPollData, type PollData } from "./fetchPollData"; 19 + 20 + type Page = 21 + | PubLeafletPagesLinearDocument.Main 22 + | PubLeafletPagesCanvas.Main; 23 + 24 + export async function collectAndFetchBlockResources({ 25 + agent, 26 + pages, 27 + }: { 28 + agent: AtpAgent; 29 + pages: Page[]; 30 + }): Promise<{ 31 + bskyPostData: AppBskyFeedDefs.PostView[]; 32 + standardSitePostData: StandardSitePostData[]; 33 + pollData: PollData[]; 34 + prerenderedCodeBlocks: Map<string, string>; 35 + }> { 36 + const allBlocks: PubLeafletPagesLinearDocument.Block[] = pages.flatMap( 37 + (p) => (p as PubLeafletPagesLinearDocument.Main).blocks ?? [], 38 + ); 39 + 40 + const bskyPostBlocks = extractBlocksByType< 41 + $Typed<PubLeafletBlocksBskyPost.Main> 42 + >(allBlocks, ids.PubLeafletBlocksBskyPost); 43 + const bskyPostBatches: typeof bskyPostBlocks[] = []; 44 + for (let i = 0; i < bskyPostBlocks.length; i += 25) { 45 + bskyPostBatches.push(bskyPostBlocks.slice(i, i + 25)); 46 + } 47 + const bskyPostResponses = await Promise.all( 48 + bskyPostBatches.map((batch) => 49 + agent.getPosts( 50 + { uris: batch.map((p) => p.block.postRef.uri) }, 51 + { headers: {} }, 52 + ), 53 + ), 54 + ); 55 + const bskyPostData = bskyPostResponses.flatMap((r) => r.data.posts); 56 + 57 + const standardSitePostUris = Array.from( 58 + new Set( 59 + extractBlocksByType<$Typed<PubLeafletBlocksStandardSitePost.Main>>( 60 + allBlocks, 61 + ids.PubLeafletBlocksStandardSitePost, 62 + ).map((b) => b.block.uri), 63 + ), 64 + ); 65 + const standardSitePostsResult = 66 + standardSitePostUris.length > 0 67 + ? await get_standard_site_posts.handler( 68 + { uris: standardSitePostUris }, 69 + { supabase: supabaseServerClient }, 70 + ) 71 + : { result: { posts: [] } }; 72 + const standardSitePostData = standardSitePostsResult.result.posts; 73 + 74 + const pollBlocks = extractBlocksByType<$Typed<PubLeafletBlocksPoll.Main>>( 75 + allBlocks, 76 + ids.PubLeafletBlocksPoll, 77 + ); 78 + const pollData = await fetchPollData( 79 + pollBlocks.map((b) => b.block.pollRef.uri), 80 + ); 81 + 82 + const firstPageBlocks = (pages[0] as Page | undefined)?.blocks ?? []; 83 + const prerenderedCodeBlocks = await extractCodeBlocks(firstPageBlocks); 84 + 85 + return { 86 + bskyPostData, 87 + standardSitePostData, 88 + pollData, 89 + prerenderedCodeBlocks, 90 + }; 91 + }
+55
app/lish/[did]/[publication]/[rkey]/extractBlocksByType.ts
··· 1 + import { ids } from "lexicons/api/lexicons"; 2 + import { 3 + PubLeafletBlocksOrderedList, 4 + PubLeafletBlocksUnorderedList, 5 + PubLeafletPagesLinearDocument, 6 + } from "lexicons/api"; 7 + 8 + export function extractBlocksByType<T extends { $type: string }>( 9 + blocks: PubLeafletPagesLinearDocument.Block[], 10 + type: string, 11 + ): { block: T }[] { 12 + const results: { block: T }[] = []; 13 + for (const b of blocks) { 14 + if (b.block.$type === type) { 15 + results.push(b as unknown as { block: T }); 16 + } 17 + if ( 18 + b.block.$type === ids.PubLeafletBlocksOrderedList || 19 + b.block.$type === ids.PubLeafletBlocksUnorderedList 20 + ) { 21 + const list = b.block as 22 + | PubLeafletBlocksOrderedList.Main 23 + | PubLeafletBlocksUnorderedList.Main; 24 + extractFromListItems(list.children, type, results); 25 + } 26 + } 27 + return results; 28 + } 29 + 30 + function extractFromListItems<T extends { $type: string }>( 31 + items: 32 + | PubLeafletBlocksOrderedList.ListItem[] 33 + | PubLeafletBlocksUnorderedList.ListItem[], 34 + type: string, 35 + results: { block: T }[], 36 + ) { 37 + for (const item of items) { 38 + if ((item.content as { $type?: string })?.$type === type) { 39 + results.push({ block: item.content as unknown as T }); 40 + } 41 + if (item.children) { 42 + extractFromListItems(item.children, type, results); 43 + } 44 + const orderedChildren = (item as PubLeafletBlocksUnorderedList.ListItem) 45 + .orderedListChildren; 46 + if (orderedChildren) { 47 + extractFromListItems(orderedChildren.children, type, results); 48 + } 49 + const unorderedChildren = (item as PubLeafletBlocksOrderedList.ListItem) 50 + .unorderedListChildren; 51 + if (unorderedChildren) { 52 + extractFromListItems(unorderedChildren.children, type, results); 53 + } 54 + } 55 + }
+8 -19
app/lish/[did]/[publication]/[rkey]/page.tsx
··· 1 1 import { supabaseServerClient } from "supabase/serverClient"; 2 2 import { Metadata } from "next"; 3 3 import { DocumentPageRenderer } from "./DocumentPageRenderer"; 4 - import { PublicationPageRenderer } from "./PublicationPageRenderer"; 4 + import { tryRenderPublicationPage } from "../tryRenderPublicationPage"; 5 5 import { normalizeDocumentRecord } from "src/utils/normalizeRecords"; 6 6 import { 7 7 documentUriFilter, 8 8 publicationNameOrUriFilter, 9 9 } from "src/utils/uriHelpers"; 10 - import { PubLeafletPublicationPage } from "lexicons/api"; 11 10 12 11 export async function generateMetadata(props: { 13 12 params: Promise<{ publication: string; did: string; rkey: string }>; ··· 108 107 .limit(1); 109 108 let publication = publications?.[0]; 110 109 111 - let matchingPage = publication?.publication_pages?.find( 112 - (p) => p.path === "/" + rkey && p.record_uri && p.record, 113 - ); 114 - if (publication && matchingPage && matchingPage.record) { 115 - let pageRecord = matchingPage.record as unknown as PubLeafletPublicationPage.Record; 116 - return ( 117 - <PublicationPageRenderer 118 - did={did} 119 - page={{ 120 - id: matchingPage.id, 121 - path: matchingPage.path ?? "/", 122 - title: matchingPage.title, 123 - record: pageRecord, 124 - }} 125 - publication={publication} 126 - /> 127 - ); 110 + if (publication) { 111 + const pageRender = tryRenderPublicationPage({ 112 + did, 113 + publication, 114 + path: "/" + rkey, 115 + }); 116 + if (pageRender) return pageRender; 128 117 } 129 118 130 119 return <DocumentPageRenderer did={did} rkey={rkey} />;
+2 -6
app/lish/[did]/[publication]/edit/[[...route]]/PublicationPagesNav.tsx
··· 6 6 import { AddTiny } from "components/Icons/AddTiny"; 7 7 import { createPublicationPage } from "actions/createPublicationPage"; 8 8 import { usePublicationData } from "../../dashboard/PublicationSWRProvider"; 9 + import { sortPublicationPages } from "../../sortPublicationPages"; 9 10 10 11 export function PublicationPagesNav(props: { 11 12 did: string; ··· 42 43 router.push(hrefForPath(created.path)); 43 44 } 44 45 45 - let sortedPages = [...pages].sort((a, b) => { 46 - let ap = a.path === "/" ? 0 : 1; 47 - let bp = b.path === "/" ? 0 : 1; 48 - if (ap !== bp) return ap - bp; 49 - return (a.path ?? "").localeCompare(b.path ?? ""); 50 - }); 46 + let sortedPages = sortPublicationPages(pages); 51 47 52 48 return ( 53 49 <nav className="publicationPagesNav border-t border-b border-border-light shrink-0 w-full sm:max-w-[calc(var(--page-width-units)*1.25)] mx-auto">
+7 -22
app/lish/[did]/[publication]/page.tsx
··· 9 9 import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout"; 10 10 import { normalizePublicationRecord } from "src/utils/normalizeRecords"; 11 11 import { PublicationContent } from "./PublicationContent"; 12 - import { PublicationPageRenderer } from "./[rkey]/PublicationPageRenderer"; 13 - import { PubLeafletPublicationPage } from "lexicons/api"; 12 + import { tryRenderPublicationPage } from "./tryRenderPublicationPage"; 14 13 import { 15 14 PublicationThemeProvider, 16 15 PublicationBackgroundProvider, ··· 54 53 55 54 if (!publication) return <PubNotFound />; 56 55 57 - let homePage = publication.publication_pages?.find( 58 - (p) => p.path === "/" && p.record_uri && p.record, 59 - ); 60 - 61 56 try { 62 - if (homePage && homePage.record) { 63 - let pageRecord = 64 - homePage.record as unknown as PubLeafletPublicationPage.Record; 65 - return ( 66 - <PublicationPageRenderer 67 - did={did} 68 - page={{ 69 - id: homePage.id, 70 - path: "/", 71 - title: homePage.title, 72 - record: pageRecord, 73 - }} 74 - publication={publication} 75 - /> 76 - ); 77 - } 57 + const homePageRender = tryRenderPublicationPage({ 58 + did, 59 + publication, 60 + path: "/", 61 + }); 62 + if (homePageRender) return homePageRender; 78 63 return ( 79 64 <PublicationThemeProvider 80 65 theme={record?.theme}
+10
app/lish/[did]/[publication]/sortPublicationPages.ts
··· 1 + export function sortPublicationPages<T extends { path: string | null }>( 2 + pages: T[], 3 + ): T[] { 4 + return [...pages].sort((a, b) => { 5 + let aHome = a.path === "/" ? 0 : 1; 6 + let bHome = b.path === "/" ? 0 : 1; 7 + if (aHome !== bHome) return aHome - bHome; 8 + return (a.path ?? "").localeCompare(b.path ?? ""); 9 + }); 10 + }
+48
app/lish/[did]/[publication]/tryRenderPublicationPage.tsx
··· 1 + import { 2 + PublicationPageRenderer, 3 + type PublicationPageRecord, 4 + } from "./[rkey]/PublicationPageRenderer"; 5 + 6 + type RendererProps = Parameters<typeof PublicationPageRenderer>[0]; 7 + 8 + type PublicationForRenderer = Omit< 9 + RendererProps["publication"], 10 + "publication_pages" 11 + > & { 12 + publication_pages?: { 13 + id: number; 14 + path: string | null; 15 + title: string; 16 + record: unknown; 17 + record_uri: string | null; 18 + }[]; 19 + }; 20 + 21 + export function tryRenderPublicationPage({ 22 + did, 23 + publication, 24 + path, 25 + }: { 26 + did: string; 27 + publication: PublicationForRenderer; 28 + path: string; 29 + }) { 30 + const matchingPage = publication.publication_pages?.find( 31 + (p) => p.path === path && p.record_uri && p.record, 32 + ); 33 + if (!matchingPage || !matchingPage.record) return null; 34 + const pageRecord = 35 + matchingPage.record as unknown as PublicationPageRecord; 36 + return ( 37 + <PublicationPageRenderer 38 + did={did} 39 + page={{ 40 + id: matchingPage.id, 41 + path: matchingPage.path ?? "/", 42 + title: matchingPage.title, 43 + record: pageRecord, 44 + }} 45 + publication={publication} 46 + /> 47 + ); 48 + }
+2 -12
components/Blocks/EmbedBlock.tsx
··· 24 24 import { scrollIntoView } from "src/utils/scrollIntoView"; 25 25 import { EmbedBlockData } from "src/partsPageChannel"; 26 26 import { useColorAttribute } from "components/ThemeManager/useColorAttribute"; 27 + import { assertStandardSitePostFacts } from "src/utils/addLinkBlock"; 27 28 28 29 export const EmbedBlock = (props: BlockProps & { preview?: boolean }) => { 29 30 let entity_set = useEntitySetContext(); ··· 269 270 if (res.status === 200) { 270 271 let data = await (res.json() as LinkPreviewMetadataResult); 271 272 if (data.leafletPost) { 272 - await rep.mutate.assertFact([ 273 - { 274 - entity, 275 - attribute: "block/type", 276 - data: { type: "block-type-union", value: "standard-site-post" }, 277 - }, 278 - { 279 - entity, 280 - attribute: "block/standard-site-post", 281 - data: { type: "string", value: data.leafletPost.uri }, 282 - }, 283 - ]); 273 + await assertStandardSitePostFacts(rep, entity, data.leafletPost.uri); 284 274 setLoading(false); 285 275 return; 286 276 }
+3 -19
components/Blocks/PostsListBlock.tsx
··· 1 - import { forwardRef, useMemo } from "react"; 1 + import { useMemo } from "react"; 2 2 import { useUIState } from "src/useUIState"; 3 3 import { useEntity, useReplicache } from "src/replicache"; 4 4 import { BlockProps, BlockLayout } from "./Block"; ··· 10 10 import { Popover } from "components/Popover"; 11 11 import { Toggle } from "components/Toggle"; 12 12 import { ToggleGroup } from "components/ToggleGroup"; 13 - import { SettingsTiny } from "components/Icons/SettingsTiny"; 13 + import { SettingsTriggerButton } from "./SettingsTriggerButton"; 14 14 15 15 type PostsListView = "compact" | "full"; 16 16 ··· 93 93 ); 94 94 } 95 95 96 - const SettingsTriggerButton = forwardRef< 97 - HTMLButtonElement, 98 - React.ButtonHTMLAttributes<HTMLButtonElement> 99 - >((props, ref) => ( 100 - <button 101 - {...props} 102 - ref={ref} 103 - onMouseDown={(e) => e.preventDefault()} 104 - aria-label="Posts List Settings" 105 - className="flex items-center" 106 - > 107 - <SettingsTiny /> 108 - </button> 109 - )); 110 - SettingsTriggerButton.displayName = "SettingsTriggerButton"; 111 - 112 96 function PostsListSettingsButton(props: { entityID: string }) { 113 97 let { rep } = useReplicache(); 114 98 let { data } = usePublicationData(); ··· 141 125 side="top" 142 126 align="end" 143 127 sideOffset={6} 144 - trigger={<SettingsTriggerButton />} 128 + trigger={<SettingsTriggerButton aria-label="Posts List Settings" />} 145 129 > 146 130 <div className="flex flex-col gap-3 text-primary py-1 min-w-[220px]"> 147 131 <div className="flex flex-col gap-1">
+18
components/Blocks/SettingsTriggerButton.tsx
··· 1 + import { forwardRef } from "react"; 2 + import { SettingsTiny } from "components/Icons/SettingsTiny"; 3 + 4 + export const SettingsTriggerButton = forwardRef< 5 + HTMLButtonElement, 6 + React.ButtonHTMLAttributes<HTMLButtonElement> 7 + >(({ "aria-label": ariaLabel = "Settings", ...props }, ref) => ( 8 + <button 9 + {...props} 10 + ref={ref} 11 + onMouseDown={(e) => e.preventDefault()} 12 + aria-label={ariaLabel} 13 + className="flex items-center" 14 + > 15 + <SettingsTiny /> 16 + </button> 17 + )); 18 + SettingsTriggerButton.displayName = "SettingsTriggerButton";
+4 -19
components/Blocks/StandardSitePostBlock/index.tsx
··· 1 - import { forwardRef } from "react"; 2 1 import { BlockProps, BlockLayout } from "../Block"; 3 2 import { useEntity, useReplicache } from "src/replicache"; 4 3 import { useUIState } from "src/useUIState"; 5 4 import { Popover } from "components/Popover"; 6 5 import { ToggleGroup } from "components/ToggleGroup"; 7 - import { SettingsTiny } from "components/Icons/SettingsTiny"; 6 + import { SettingsTriggerButton } from "../SettingsTriggerButton"; 8 7 import { 9 8 StandardSitePostItem, 10 9 type StandardSitePostSize, ··· 37 36 ); 38 37 }; 39 38 40 - const SettingsTriggerButton = forwardRef< 41 - HTMLButtonElement, 42 - React.ButtonHTMLAttributes<HTMLButtonElement> 43 - >((props, ref) => ( 44 - <button 45 - {...props} 46 - ref={ref} 47 - onMouseDown={(e) => e.preventDefault()} 48 - aria-label="Standard Site Post Settings" 49 - className="flex items-center" 50 - > 51 - <SettingsTiny /> 52 - </button> 53 - )); 54 - SettingsTriggerButton.displayName = "SettingsTriggerButton"; 55 - 56 39 function StandardSitePostSettingsButton(props: { entityID: string }) { 57 40 let { rep } = useReplicache(); 58 41 let sizeFact = useEntity(props.entityID, "standard-site-post/size"); ··· 64 47 side="top" 65 48 align="end" 66 49 sideOffset={6} 67 - trigger={<SettingsTriggerButton />} 50 + trigger={ 51 + <SettingsTriggerButton aria-label="Standard Site Post Settings" /> 52 + } 68 53 > 69 54 <div className="flex flex-col gap-3 text-primary py-1 min-w-[220px]"> 70 55 <div className="flex flex-col gap-1">
+20 -12
src/utils/addLinkBlock.ts
··· 9 9 import { v7 } from "uuid"; 10 10 import { getAspectRatio } from "src/utils/aspectRatio"; 11 11 12 + export async function assertStandardSitePostFacts( 13 + rep: Replicache<ReplicacheMutators>, 14 + entityID: string, 15 + uri: string, 16 + ) { 17 + await rep.mutate.assertFact([ 18 + { 19 + entity: entityID, 20 + attribute: "block/type", 21 + data: { type: "block-type-union", value: "standard-site-post" }, 22 + }, 23 + { 24 + entity: entityID, 25 + attribute: "block/standard-site-post", 26 + data: { type: "string", value: uri }, 27 + }, 28 + ]); 29 + } 30 + 12 31 export async function addLinkBlock( 13 32 url: string, 14 33 entityID: string, ··· 42 61 let data = await (res.json() as LinkPreviewMetadataResult); 43 62 44 63 if (data.leafletPost) { 45 - await rep.mutate.assertFact([ 46 - { 47 - entity: entityID, 48 - attribute: "block/type", 49 - data: { type: "block-type-union", value: "standard-site-post" }, 50 - }, 51 - { 52 - entity: entityID, 53 - attribute: "block/standard-site-post", 54 - data: { type: "string", value: data.leafletPost.uri }, 55 - }, 56 - ]); 64 + await assertStandardSitePostFacts(rep, entityID, data.leafletPost.uri); 57 65 return; 58 66 } 59 67