a tool for shared writing and social publishing

Compare changes

Choose any two refs to compare.

+66 -124
+3 -6
actions/publishToPublication.ts
··· 199 199 } 200 200 201 201 // Determine the collection to use - preserve existing schema if updating 202 - const existingCollection = existingDocUri 203 - ? new AtUri(existingDocUri).collection 204 - : undefined; 202 + const existingCollection = existingDocUri ? new AtUri(existingDocUri).collection : undefined; 205 203 const documentType = getDocumentType(existingCollection); 206 204 207 205 // Build the pages array (used by both formats) ··· 230 228 if (documentType === "site.standard.document") { 231 229 // site.standard.document format 232 230 // For standalone docs, use HTTPS URL; for publication docs, use the publication AT-URI 233 - const siteUri = 234 - publication_uri || `https://leaflet.pub/p/${credentialSession.did}`; 231 + const siteUri = publication_uri || `https://leaflet.pub/p/${credentialSession.did}`; 235 232 236 233 record = { 237 234 $type: "site.standard.document", 238 235 title: title || "Untitled", 239 236 site: siteUri, 240 - path: "/" + rkey, 237 + path: rkey, 241 238 publishedAt: 242 239 publishedAt || existingRecord.publishedAt || new Date().toISOString(), 243 240 ...(description && { description }),
+48 -30
app/api/inngest/functions/migrate_user_to_standard.ts
··· 109 109 }) 110 110 .filter((x) => x !== null); 111 111 112 - // Run PDS + DB writes together for each publication 113 - const pubResults = await Promise.all( 114 - publicationsToMigrate.map(({ pub, rkey, normalized, newRecord }) => 115 - step.run(`migrate-publication-${pub.uri}`, async () => { 116 - // PDS write 112 + // Run all PDS writes in parallel 113 + const pubPdsResults = await Promise.all( 114 + publicationsToMigrate.map(({ pub, rkey, newRecord }) => 115 + step.run(`pds-write-publication-${pub.uri}`, async () => { 117 116 const agent = await createAuthenticatedAgent(did); 118 117 const putResult = await agent.com.atproto.repo.putRecord({ 119 118 repo: did, ··· 122 121 record: newRecord, 123 122 validate: false, 124 123 }); 125 - const newUri = putResult.data.uri; 124 + return { oldUri: pub.uri, newUri: putResult.data.uri }; 125 + }), 126 + ), 127 + ); 126 128 127 - // DB write 129 + // Run all DB writes in parallel 130 + const pubDbResults = await Promise.all( 131 + publicationsToMigrate.map(({ pub, normalized, newRecord }, index) => { 132 + const newUri = pubPdsResults[index].newUri; 133 + return step.run(`db-write-publication-${pub.uri}`, async () => { 128 134 const { error: dbError } = await supabaseServerClient 129 135 .from("publications") 130 136 .upsert({ ··· 143 149 }; 144 150 } 145 151 return { success: true as const, oldUri: pub.uri, newUri }; 146 - }), 147 - ), 152 + }); 153 + }), 148 154 ); 149 155 150 156 // Process results 151 - for (const result of pubResults) { 157 + for (const result of pubDbResults) { 152 158 if (result.success) { 153 159 publicationUriMap[result.oldUri] = result.newUri; 154 160 stats.publicationsMigrated++; ··· 233 239 $type: "site.standard.document", 234 240 title: normalized.title || "Untitled", 235 241 site: siteValue, 236 - path: "/" + rkey, 242 + path: rkey, 237 243 publishedAt: normalized.publishedAt || new Date().toISOString(), 238 244 description: normalized.description, 239 245 content: normalized.content, ··· 246 252 }) 247 253 .filter((x) => x !== null); 248 254 249 - // Run PDS + DB writes together for each document 250 - const docResults = await Promise.all( 251 - documentsToMigrate.map(({ doc, rkey, newRecord, oldPubUri }) => 252 - step.run(`migrate-document-${doc.uri}`, async () => { 253 - // PDS write 255 + // Run all PDS writes in parallel 256 + const docPdsResults = await Promise.all( 257 + documentsToMigrate.map(({ doc, rkey, newRecord }) => 258 + step.run(`pds-write-document-${doc.uri}`, async () => { 254 259 const agent = await createAuthenticatedAgent(did); 255 260 const putResult = await agent.com.atproto.repo.putRecord({ 256 261 repo: did, ··· 259 264 record: newRecord, 260 265 validate: false, 261 266 }); 262 - const newUri = putResult.data.uri; 267 + return { oldUri: doc.uri, newUri: putResult.data.uri }; 268 + }), 269 + ), 270 + ); 263 271 264 - // DB write 272 + // Run all DB writes in parallel 273 + const docDbResults = await Promise.all( 274 + documentsToMigrate.map(({ doc, newRecord, oldPubUri }, index) => { 275 + const newUri = docPdsResults[index].newUri; 276 + return step.run(`db-write-document-${doc.uri}`, async () => { 265 277 const { error: dbError } = await supabaseServerClient 266 278 .from("documents") 267 279 .upsert({ ··· 290 302 } 291 303 292 304 return { success: true as const, oldUri: doc.uri, newUri }; 293 - }), 294 - ), 305 + }); 306 + }), 295 307 ); 296 308 297 309 // Process results 298 - for (const result of docResults) { 310 + for (const result of docDbResults) { 299 311 if (result.success) { 300 312 documentUriMap[result.oldUri] = result.newUri; 301 313 stats.documentsMigrated++; ··· 416 428 }) 417 429 .filter((x) => x !== null); 418 430 419 - // Run PDS + DB writes together for each subscription 420 - const subResults = await Promise.all( 431 + // Run all PDS writes in parallel 432 + const subPdsResults = await Promise.all( 421 433 subscriptionsToMigrate.map(({ sub, rkey, newRecord }) => 422 - step.run(`migrate-subscription-${sub.uri}`, async () => { 423 - // PDS write 434 + step.run(`pds-write-subscription-${sub.uri}`, async () => { 424 435 const agent = await createAuthenticatedAgent(did); 425 436 const putResult = await agent.com.atproto.repo.putRecord({ 426 437 repo: did, ··· 429 440 record: newRecord, 430 441 validate: false, 431 442 }); 432 - const newUri = putResult.data.uri; 443 + return { oldUri: sub.uri, newUri: putResult.data.uri }; 444 + }), 445 + ), 446 + ); 433 447 434 - // DB write 448 + // Run all DB writes in parallel 449 + const subDbResults = await Promise.all( 450 + subscriptionsToMigrate.map(({ sub, newRecord }, index) => { 451 + const newUri = subPdsResults[index].newUri; 452 + return step.run(`db-write-subscription-${sub.uri}`, async () => { 435 453 const { error: dbError } = await supabaseServerClient 436 454 .from("publication_subscriptions") 437 455 .update({ ··· 449 467 }; 450 468 } 451 469 return { success: true as const, oldUri: sub.uri, newUri }; 452 - }), 453 - ), 470 + }); 471 + }), 454 472 ); 455 473 456 474 // Process results 457 - for (const result of subResults) { 475 + for (const result of subDbResults) { 458 476 if (result.success) { 459 477 userSubscriptionUriMap[result.oldUri] = result.newUri; 460 478 stats.userSubscriptionsMigrated++;
-34
app/lish/[did]/[publication]/.well-known/site.standard.publication/route.ts
··· 1 - import { publicationNameOrUriFilter } from "src/utils/uriHelpers"; 2 - import { supabaseServerClient } from "supabase/serverClient"; 3 - 4 - export async function GET( 5 - req: Request, 6 - props: { 7 - params: Promise<{ publication: string; did: string }>; 8 - }, 9 - ) { 10 - let params = await props.params; 11 - let did = decodeURIComponent(params.did); 12 - let publication_name = decodeURIComponent(params.publication); 13 - let [{ data: publications }] = await Promise.all([ 14 - supabaseServerClient 15 - .from("publications") 16 - .select( 17 - `*, 18 - publication_subscriptions(*), 19 - documents_in_publications(documents( 20 - *, 21 - comments_on_documents(count), 22 - document_mentions_in_bsky(count) 23 - )) 24 - `, 25 - ) 26 - .eq("identity_did", did) 27 - .or(publicationNameOrUriFilter(did, publication_name)) 28 - .order("uri", { ascending: false }) 29 - .limit(1), 30 - ]); 31 - let publication = publications?.[0]; 32 - if (!did || !publication) return new Response(null, { status: 404 }); 33 - return new Response(publication.uri); 34 - }
+4 -7
app/p/[didOrHandle]/[rkey]/page.tsx
··· 38 38 39 39 return { 40 40 icons: { 41 - other: [ 42 - { 43 - rel: "alternate", 44 - url: document.uri, 45 - }, 46 - { rel: "site.standard.document", url: document.uri }, 47 - ], 41 + other: { 42 + rel: "alternate", 43 + url: document.uri, 44 + }, 48 45 }, 49 46 title: docRecord.title, 50 47 description: docRecord?.description || "",
+1 -2
components/Blocks/PublicationPollBlock.tsx
··· 27 27 setAreYouSure?: (value: boolean) => void; 28 28 }, 29 29 ) => { 30 - let { data: publicationData, normalizedDocument } = 31 - useLeafletPublicationData(); 30 + let { data: publicationData } = useLeafletPublicationData(); 32 31 let isSelected = useUIState((s) => 33 32 s.selectedBlocks.find((b) => b.value === props.entityID), 34 33 );
+2 -2
lexicons/api/lexicons.ts
··· 2215 2215 type: 'ref', 2216 2216 }, 2217 2217 theme: { 2218 - type: 'union', 2219 - refs: ['lex:pub.leaflet.publication#theme'], 2218 + type: 'ref', 2219 + ref: 'lex:pub.leaflet.publication#theme', 2220 2220 }, 2221 2221 description: { 2222 2222 maxGraphemes: 300,
+1 -1
lexicons/api/types/site/standard/publication.ts
··· 15 15 export interface Record { 16 16 $type: 'site.standard.publication' 17 17 basicTheme?: SiteStandardThemeBasic.Main 18 - theme?: $Typed<PubLeafletPublication.Theme> | { $type: string } 18 + theme?: PubLeafletPublication.Theme 19 19 description?: string 20 20 icon?: BlobRef 21 21 name: string
+2 -2
lexicons/site/standard/publication.json
··· 9 9 "type": "ref" 10 10 }, 11 11 "theme": { 12 - "type": "union", 13 - "refs": ["pub.leaflet.publication#theme"] 12 + "type": "ref", 13 + "ref": "pub.leaflet.publication#theme" 14 14 }, 15 15 "description": { 16 16 "maxGraphemes": 300,
+5 -40
lexicons/src/normalize.ts
··· 14 14 */ 15 15 16 16 import type * as PubLeafletDocument from "../api/types/pub/leaflet/document"; 17 - import * as PubLeafletPublication from "../api/types/pub/leaflet/publication"; 17 + import type * as PubLeafletPublication from "../api/types/pub/leaflet/publication"; 18 18 import type * as PubLeafletContent from "../api/types/pub/leaflet/content"; 19 19 import type * as SiteStandardDocument from "../api/types/site/standard/document"; 20 20 import type * as SiteStandardPublication from "../api/types/site/standard/publication"; ··· 31 31 }; 32 32 33 33 // Normalized publication type - uses the generated site.standard.publication type 34 - // with the theme narrowed to only the valid pub.leaflet.publication#theme type 35 - // (isTheme validates that $type is present, so we use $Typed) 36 - // Note: We explicitly list fields rather than using Omit because the generated Record type 37 - // has an index signature [k: string]: unknown that interferes with property typing 38 - export type NormalizedPublication = { 39 - $type: "site.standard.publication"; 40 - name: string; 41 - url: string; 42 - description?: string; 43 - icon?: SiteStandardPublication.Record["icon"]; 44 - basicTheme?: SiteStandardThemeBasic.Main; 45 - theme?: $Typed<PubLeafletPublication.Theme>; 46 - preferences?: SiteStandardPublication.Preferences; 47 - }; 34 + export type NormalizedPublication = SiteStandardPublication.Record; 48 35 49 36 /** 50 37 * Checks if the record is a pub.leaflet.document ··· 223 210 ): NormalizedPublication | null { 224 211 if (!record || typeof record !== "object") return null; 225 212 226 - // Pass through site.standard records directly, but validate the theme 213 + // Pass through site.standard records directly 227 214 if (isStandardPublication(record)) { 228 - // Validate theme - only keep if it's a valid pub.leaflet.publication#theme 229 - const theme = PubLeafletPublication.isTheme(record.theme) 230 - ? (record.theme as $Typed<PubLeafletPublication.Theme>) 231 - : undefined; 232 - return { 233 - ...record, 234 - theme, 235 - }; 215 + return record; 236 216 } 237 217 238 218 if (isLeafletPublication(record)) { ··· 245 225 246 226 const basicTheme = leafletThemeToBasicTheme(record.theme); 247 227 248 - // Validate theme - only keep if it's a valid pub.leaflet.publication#theme with $type set 249 - // For legacy records without $type, add it during normalization 250 - let theme: $Typed<PubLeafletPublication.Theme> | undefined; 251 - if (record.theme) { 252 - if (PubLeafletPublication.isTheme(record.theme)) { 253 - theme = record.theme as $Typed<PubLeafletPublication.Theme>; 254 - } else { 255 - // Legacy theme without $type - add it 256 - theme = { 257 - ...record.theme, 258 - $type: "pub.leaflet.publication#theme", 259 - }; 260 - } 261 - } 262 - 263 228 // Convert preferences to site.standard format (strip/replace $type) 264 229 const preferences: SiteStandardPublication.Preferences | undefined = 265 230 record.preferences ··· 278 243 description: record.description, 279 244 icon: record.icon, 280 245 basicTheme, 281 - theme, 246 + theme: record.theme, 282 247 preferences, 283 248 }; 284 249 }