a tool for shared writing and social publishing

fix a few bugs

+620 -90
+5
app/api/inngest/client.ts
··· 21 21 }; 22 22 }; 23 23 "appview/come-online": { data: {} }; 24 + "user/migrate-to-standard": { 25 + data: { 26 + did: string; 27 + }; 28 + }; 24 29 }; 25 30 26 31 // Create a client to send and receive events
+3 -1
app/api/inngest/functions/index_post_mention.ts
··· 55 55 authorDid = did; 56 56 } else { 57 57 // Publication post: look up by custom domain 58 + // Support both old format (pub.leaflet.publication with base_path) and 59 + // new format (site.standard.publication with url as https://domain) 58 60 let { data: pub, error } = await supabaseServerClient 59 61 .from("publications") 60 62 .select("*") 61 - .eq("record->>base_path", url.host) 63 + .or(`record->>base_path.eq.${url.host},record->>url.eq.https://${url.host}`) 62 64 .single(); 63 65 64 66 if (!pub) {
+427
app/api/inngest/functions/migrate_user_to_standard.ts
··· 1 + import { supabaseServerClient } from "supabase/serverClient"; 2 + import { inngest } from "../client"; 3 + import { restoreOAuthSession } from "src/atproto-oauth"; 4 + import { AtpBaseClient, SiteStandardPublication, SiteStandardDocument, SiteStandardGraphSubscription } from "lexicons/api"; 5 + import { AtUri } from "@atproto/syntax"; 6 + import { Json } from "supabase/database.types"; 7 + import { normalizePublicationRecord, normalizeDocumentRecord } from "src/utils/normalizeRecords"; 8 + 9 + type MigrationResult = 10 + | { success: true; oldUri: string; newUri: string; skipped?: boolean } 11 + | { success: false; error: string }; 12 + 13 + async function createAuthenticatedAgent(did: string): Promise<AtpBaseClient> { 14 + const result = await restoreOAuthSession(did); 15 + if (!result.ok) { 16 + throw new Error(`Failed to restore OAuth session: ${result.error.message}`); 17 + } 18 + const credentialSession = result.value; 19 + return new AtpBaseClient( 20 + credentialSession.fetchHandler.bind(credentialSession) 21 + ); 22 + } 23 + 24 + export const migrate_user_to_standard = inngest.createFunction( 25 + { id: "migrate_user_to_standard" }, 26 + { event: "user/migrate-to-standard" }, 27 + async ({ event, step }) => { 28 + const { did } = event.data; 29 + 30 + const stats = { 31 + publicationsMigrated: 0, 32 + documentsMigrated: 0, 33 + userSubscriptionsMigrated: 0, 34 + referencesUpdated: 0, 35 + errors: [] as string[], 36 + }; 37 + 38 + // Step 1: Verify OAuth session is valid 39 + await step.run("verify-oauth-session", async () => { 40 + const result = await restoreOAuthSession(did); 41 + if (!result.ok) { 42 + throw new Error(`Failed to restore OAuth session: ${result.error.message}`); 43 + } 44 + return { success: true }; 45 + }); 46 + 47 + // Step 2: Get user's pub.leaflet.publication records 48 + const oldPublications = await step.run("fetch-old-publications", async () => { 49 + const { data, error } = await supabaseServerClient 50 + .from("publications") 51 + .select("*") 52 + .eq("identity_did", did) 53 + .like("uri", `at://${did}/pub.leaflet.publication/%`); 54 + 55 + if (error) throw new Error(`Failed to fetch publications: ${error.message}`); 56 + return data || []; 57 + }); 58 + 59 + // Step 3: Migrate each publication 60 + const publicationUriMap: Record<string, string> = {}; // old URI -> new URI 61 + 62 + for (const pub of oldPublications) { 63 + const aturi = new AtUri(pub.uri); 64 + 65 + // Skip if already a site.standard.publication 66 + if (aturi.collection === "site.standard.publication") { 67 + publicationUriMap[pub.uri] = pub.uri; 68 + continue; 69 + } 70 + 71 + const rkey = aturi.rkey; 72 + const normalized = normalizePublicationRecord(pub.record); 73 + 74 + if (!normalized) { 75 + stats.errors.push(`Publication ${pub.uri}: Failed to normalize publication record`); 76 + continue; 77 + } 78 + 79 + // Build site.standard.publication record 80 + const newRecord: SiteStandardPublication.Record = { 81 + $type: "site.standard.publication", 82 + name: normalized.name, 83 + url: normalized.url, 84 + description: normalized.description, 85 + icon: normalized.icon, 86 + theme: normalized.theme, 87 + basicTheme: normalized.basicTheme, 88 + preferences: normalized.preferences, 89 + }; 90 + 91 + // Step: Write to PDS 92 + const pdsResult = await step.run(`pds-write-publication-${pub.uri}`, async () => { 93 + const agent = await createAuthenticatedAgent(did); 94 + const putResult = await agent.com.atproto.repo.putRecord({ 95 + repo: did, 96 + collection: "site.standard.publication", 97 + rkey, 98 + record: newRecord, 99 + validate: false, 100 + }); 101 + return { newUri: putResult.data.uri }; 102 + }); 103 + 104 + const newUri = pdsResult.newUri; 105 + 106 + // Step: Write to database 107 + const dbResult = await step.run(`db-write-publication-${pub.uri}`, async () => { 108 + const { error: dbError } = await supabaseServerClient 109 + .from("publications") 110 + .upsert({ 111 + uri: newUri, 112 + identity_did: did, 113 + name: normalized.name, 114 + record: newRecord as Json, 115 + }); 116 + 117 + if (dbError) { 118 + return { success: false as const, error: dbError.message }; 119 + } 120 + return { success: true as const }; 121 + }); 122 + 123 + if (dbResult.success) { 124 + publicationUriMap[pub.uri] = newUri; 125 + stats.publicationsMigrated++; 126 + } else { 127 + stats.errors.push(`Publication ${pub.uri}: Database error: ${dbResult.error}`); 128 + } 129 + } 130 + 131 + // Step 4: Get and migrate documents for these publications 132 + const oldDocuments = await step.run("fetch-old-documents", async () => { 133 + const oldPubUris = Object.keys(publicationUriMap); 134 + if (oldPubUris.length === 0) return []; 135 + 136 + const { data, error } = await supabaseServerClient 137 + .from("documents_in_publications") 138 + .select("document, publication, documents(uri, data)") 139 + .in("publication", oldPubUris); 140 + 141 + if (error) throw new Error(`Failed to fetch documents: ${error.message}`); 142 + return data || []; 143 + }); 144 + 145 + const documentUriMap: Record<string, string> = {}; // old URI -> new URI 146 + 147 + for (const docRow of oldDocuments) { 148 + if (!docRow.documents) continue; 149 + const doc = docRow.documents as { uri: string; data: Json }; 150 + const aturi = new AtUri(doc.uri); 151 + 152 + // Skip if already a site.standard.document 153 + if (aturi.collection === "site.standard.document") { 154 + documentUriMap[doc.uri] = doc.uri; 155 + continue; 156 + } 157 + 158 + const rkey = aturi.rkey; 159 + const normalized = normalizeDocumentRecord(doc.data); 160 + 161 + if (!normalized) { 162 + stats.errors.push(`Document ${doc.uri}: Failed to normalize document record`); 163 + continue; 164 + } 165 + 166 + // Get the new publication URI 167 + const newPubUri = publicationUriMap[docRow.publication]; 168 + if (!newPubUri) { 169 + stats.errors.push(`Document ${doc.uri}: No migrated publication found`); 170 + continue; 171 + } 172 + 173 + // Build site.standard.document record 174 + const newRecord: SiteStandardDocument.Record = { 175 + $type: "site.standard.document", 176 + title: normalized.title || "Untitled", 177 + site: newPubUri, 178 + publishedAt: normalized.publishedAt || new Date().toISOString(), 179 + description: normalized.description, 180 + content: normalized.content, 181 + path: normalized.path, 182 + tags: normalized.tags, 183 + coverImage: normalized.coverImage, 184 + bskyPostRef: normalized.bskyPostRef, 185 + }; 186 + 187 + // Step: Write to PDS 188 + const pdsResult = await step.run(`pds-write-document-${doc.uri}`, async () => { 189 + const agent = await createAuthenticatedAgent(did); 190 + const putResult = await agent.com.atproto.repo.putRecord({ 191 + repo: did, 192 + collection: "site.standard.document", 193 + rkey, 194 + record: newRecord, 195 + validate: false, 196 + }); 197 + return { newUri: putResult.data.uri }; 198 + }); 199 + 200 + const newUri = pdsResult.newUri; 201 + 202 + // Step: Write to database 203 + const dbResult = await step.run(`db-write-document-${doc.uri}`, async () => { 204 + const { error: dbError } = await supabaseServerClient 205 + .from("documents") 206 + .upsert({ 207 + uri: newUri, 208 + data: newRecord as Json, 209 + }); 210 + 211 + if (dbError) { 212 + return { success: false as const, error: dbError.message }; 213 + } 214 + 215 + // Add to documents_in_publications with new URIs 216 + await supabaseServerClient 217 + .from("documents_in_publications") 218 + .upsert({ 219 + publication: newPubUri, 220 + document: newUri, 221 + }); 222 + 223 + return { success: true as const }; 224 + }); 225 + 226 + if (dbResult.success) { 227 + documentUriMap[doc.uri] = newUri; 228 + stats.documentsMigrated++; 229 + } else { 230 + stats.errors.push(`Document ${doc.uri}: Database error: ${dbResult.error}`); 231 + } 232 + } 233 + 234 + // Step 5: Update references in database tables 235 + await step.run("update-references", async () => { 236 + // Update leaflets_in_publications - update publication and doc references 237 + for (const [oldUri, newUri] of Object.entries(publicationUriMap)) { 238 + const { error } = await supabaseServerClient 239 + .from("leaflets_in_publications") 240 + .update({ publication: newUri }) 241 + .eq("publication", oldUri); 242 + 243 + if (!error) stats.referencesUpdated++; 244 + } 245 + 246 + for (const [oldUri, newUri] of Object.entries(documentUriMap)) { 247 + const { error } = await supabaseServerClient 248 + .from("leaflets_in_publications") 249 + .update({ doc: newUri }) 250 + .eq("doc", oldUri); 251 + 252 + if (!error) stats.referencesUpdated++; 253 + } 254 + 255 + // Update leaflets_to_documents - update document references 256 + for (const [oldUri, newUri] of Object.entries(documentUriMap)) { 257 + const { error } = await supabaseServerClient 258 + .from("leaflets_to_documents") 259 + .update({ document: newUri }) 260 + .eq("document", oldUri); 261 + 262 + if (!error) stats.referencesUpdated++; 263 + } 264 + 265 + // Update publication_domains - update publication references 266 + for (const [oldUri, newUri] of Object.entries(publicationUriMap)) { 267 + const { error } = await supabaseServerClient 268 + .from("publication_domains") 269 + .update({ publication: newUri }) 270 + .eq("publication", oldUri); 271 + 272 + if (!error) stats.referencesUpdated++; 273 + } 274 + 275 + // Update comments_on_documents - update document references 276 + for (const [oldUri, newUri] of Object.entries(documentUriMap)) { 277 + const { error } = await supabaseServerClient 278 + .from("comments_on_documents") 279 + .update({ document: newUri }) 280 + .eq("document", oldUri); 281 + 282 + if (!error) stats.referencesUpdated++; 283 + } 284 + 285 + // Update document_mentions_in_bsky - update document references 286 + for (const [oldUri, newUri] of Object.entries(documentUriMap)) { 287 + const { error } = await supabaseServerClient 288 + .from("document_mentions_in_bsky") 289 + .update({ document: newUri }) 290 + .eq("document", oldUri); 291 + 292 + if (!error) stats.referencesUpdated++; 293 + } 294 + 295 + // Update subscribers_to_publications - update publication references 296 + for (const [oldUri, newUri] of Object.entries(publicationUriMap)) { 297 + const { error } = await supabaseServerClient 298 + .from("subscribers_to_publications") 299 + .update({ publication: newUri }) 300 + .eq("publication", oldUri); 301 + 302 + if (!error) stats.referencesUpdated++; 303 + } 304 + 305 + // Update publication_subscriptions - update publication references for incoming subscriptions 306 + for (const [oldUri, newUri] of Object.entries(publicationUriMap)) { 307 + const { error } = await supabaseServerClient 308 + .from("publication_subscriptions") 309 + .update({ publication: newUri }) 310 + .eq("publication", oldUri); 311 + 312 + if (!error) stats.referencesUpdated++; 313 + } 314 + 315 + return stats.referencesUpdated; 316 + }); 317 + 318 + // Step 6: Migrate user's own subscriptions - subscriptions BY this user to other publications 319 + const userSubscriptions = await step.run("fetch-user-subscriptions", async () => { 320 + const { data, error } = await supabaseServerClient 321 + .from("publication_subscriptions") 322 + .select("*") 323 + .eq("identity", did) 324 + .like("uri", `at://${did}/pub.leaflet.graph.subscription/%`); 325 + 326 + if (error) throw new Error(`Failed to fetch user subscriptions: ${error.message}`); 327 + return data || []; 328 + }); 329 + 330 + const userSubscriptionUriMap: Record<string, string> = {}; // old URI -> new URI 331 + 332 + for (const sub of userSubscriptions) { 333 + const aturi = new AtUri(sub.uri); 334 + 335 + // Skip if already a site.standard.graph.subscription 336 + if (aturi.collection === "site.standard.graph.subscription") { 337 + userSubscriptionUriMap[sub.uri] = sub.uri; 338 + continue; 339 + } 340 + 341 + const rkey = aturi.rkey; 342 + 343 + // Build site.standard.graph.subscription record 344 + const newRecord: SiteStandardGraphSubscription.Record = { 345 + $type: "site.standard.graph.subscription", 346 + publication: sub.publication, 347 + }; 348 + 349 + // Step: Write to PDS 350 + const pdsResult = await step.run(`pds-write-subscription-${sub.uri}`, async () => { 351 + const agent = await createAuthenticatedAgent(did); 352 + const putResult = await agent.com.atproto.repo.putRecord({ 353 + repo: did, 354 + collection: "site.standard.graph.subscription", 355 + rkey, 356 + record: newRecord, 357 + validate: false, 358 + }); 359 + return { newUri: putResult.data.uri }; 360 + }); 361 + 362 + const newUri = pdsResult.newUri; 363 + 364 + // Step: Write to database 365 + const dbResult = await step.run(`db-write-subscription-${sub.uri}`, async () => { 366 + const { error: dbError } = await supabaseServerClient 367 + .from("publication_subscriptions") 368 + .update({ 369 + uri: newUri, 370 + record: newRecord as Json, 371 + }) 372 + .eq("uri", sub.uri); 373 + 374 + if (dbError) { 375 + return { success: false as const, error: dbError.message }; 376 + } 377 + return { success: true as const }; 378 + }); 379 + 380 + if (dbResult.success) { 381 + userSubscriptionUriMap[sub.uri] = newUri; 382 + stats.userSubscriptionsMigrated++; 383 + } else { 384 + stats.errors.push(`User subscription ${sub.uri}: Database error: ${dbResult.error}`); 385 + } 386 + } 387 + 388 + // Step 7: Delete old records from our database tables 389 + await step.run("delete-old-db-records", async () => { 390 + const oldPubUris = Object.keys(publicationUriMap).filter(uri => 391 + new AtUri(uri).collection === "pub.leaflet.publication" 392 + ); 393 + const oldDocUris = Object.keys(documentUriMap).filter(uri => 394 + new AtUri(uri).collection === "pub.leaflet.document" 395 + ); 396 + 397 + // NOTE: We intentionally keep old documents_in_publications entries. 398 + // New entries are created in Step 4 with the new URIs, but the old entries 399 + // should remain so that notifications and other references that point to 400 + // old document/publication URIs can still look up the relationship. 401 + 402 + // Delete from documents (old document URIs) 403 + if (oldDocUris.length > 0) { 404 + await supabaseServerClient 405 + .from("documents") 406 + .delete() 407 + .in("uri", oldDocUris); 408 + } 409 + 410 + // Delete from publications (old publication URIs) 411 + if (oldPubUris.length > 0) { 412 + await supabaseServerClient 413 + .from("publications") 414 + .delete() 415 + .in("uri", oldPubUris); 416 + } 417 + }); 418 + 419 + return { 420 + success: stats.errors.length === 0, 421 + stats, 422 + publicationUriMap, 423 + documentUriMap, 424 + userSubscriptionUriMap, 425 + }; 426 + } 427 + );
+2
app/api/inngest/route.tsx
··· 4 4 import { come_online } from "./functions/come_online"; 5 5 import { batched_update_profiles } from "./functions/batched_update_profiles"; 6 6 import { index_follows } from "./functions/index_follows"; 7 + import { migrate_user_to_standard } from "./functions/migrate_user_to_standard"; 7 8 8 9 export const { GET, POST, PUT } = serve({ 9 10 client: inngest, ··· 12 13 come_online, 13 14 batched_update_profiles, 14 15 index_follows, 16 + migrate_user_to_standard, 15 17 ], 16 18 });
+11 -4
app/api/rpc/[command]/get_publication_data.ts
··· 4 4 import { AtUri } from "@atproto/syntax"; 5 5 import { getFactsFromHomeLeaflets } from "./getFactsFromHomeLeaflets"; 6 6 import { normalizeDocumentRecord } from "src/utils/normalizeRecords"; 7 + import { ids } from "lexicons/api/lexicons"; 7 8 8 9 export type GetPublicationDataReturnType = Awaited< 9 10 ReturnType<(typeof get_publication_data)["handler"]> ··· 18 19 { did, publication_name }, 19 20 { supabase }: Pick<Env, "supabase">, 20 21 ) => { 21 - let uri; 22 + let pubLeafletUri; 23 + let siteStandardUri; 22 24 if (/^(?!\.$|\.\.S)[A-Za-z0-9._:~-]{1,512}$/.test(publication_name)) { 23 - uri = AtUri.make( 25 + pubLeafletUri = AtUri.make( 26 + did, 27 + ids.PubLeafletPublication, 28 + publication_name, 29 + ).toString(); 30 + siteStandardUri = AtUri.make( 24 31 did, 25 - "pub.leaflet.publication", 32 + ids.SiteStandardPublication, 26 33 publication_name, 27 34 ).toString(); 28 35 } ··· 45 52 ) 46 53 )`, 47 54 ) 48 - .or(`name.eq."${publication_name}", uri.eq."${uri}"`) 55 + .or(`name.eq."${publication_name}", uri.eq."${pubLeafletUri}", uri.eq."${siteStandardUri}"`) 49 56 .eq("identity_did", did) 50 57 .single(); 51 58
+159 -79
app/lish/createPub/updatePublication.ts
··· 4 4 AtpBaseClient, 5 5 PubLeafletPublication, 6 6 PubLeafletThemeColor, 7 + SiteStandardPublication, 7 8 } from "lexicons/api"; 8 9 import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; 9 10 import { getIdentityData } from "actions/getIdentityData"; ··· 66 67 // Preserve existing schema when updating 67 68 const publicationType = getPublicationType(aturi.collection); 68 69 69 - let record = { 70 - $type: publicationType, 71 - ...(existingPub.record as object), 72 - name, 73 - } as PubLeafletPublication.Record; 74 - if (preferences) { 75 - record.preferences = preferences; 76 - } 77 - 78 - if (description !== undefined) { 79 - record.description = description; 80 - } 70 + // Normalize the existing record to read its properties 71 + const normalizedPub = normalizePublicationRecord(existingPub.record); 72 + // Extract base_path from url if it exists (url format is https://domain, base_path is just domain) 73 + const existingBasePath = normalizedPub?.url 74 + ? normalizedPub.url.replace(/^https?:\/\//, "") 75 + : undefined; 81 76 82 - // Upload the icon if provided How do I tell if there isn't a new one? 77 + // Upload the icon if provided 78 + let iconBlob = normalizedPub?.icon; 83 79 if (iconFile && iconFile.size > 0) { 84 80 const buffer = await iconFile.arrayBuffer(); 85 81 const uploadResult = await agent.com.atproto.repo.uploadBlob( ··· 88 84 ); 89 85 90 86 if (uploadResult.data.blob) { 91 - record.icon = uploadResult.data.blob; 87 + iconBlob = uploadResult.data.blob; 92 88 } 93 89 } 94 90 91 + // Build preferences based on input or existing normalized preferences 92 + const preferencesData = preferences || normalizedPub?.preferences; 93 + 94 + // Build the record with the correct field based on publication type 95 + const record = 96 + publicationType === "site.standard.publication" 97 + ? ({ 98 + $type: publicationType, 99 + name, 100 + description: description !== undefined ? description : normalizedPub?.description, 101 + icon: iconBlob, 102 + theme: normalizedPub?.theme, 103 + preferences: preferencesData 104 + ? { 105 + $type: "site.standard.publication#preferences" as const, 106 + showInDiscover: preferencesData.showInDiscover, 107 + showComments: preferencesData.showComments, 108 + showMentions: preferencesData.showMentions, 109 + showPrevNext: preferencesData.showPrevNext, 110 + } 111 + : undefined, 112 + url: normalizedPub?.url || "", 113 + } as SiteStandardPublication.Record) 114 + : ({ 115 + $type: publicationType, 116 + name, 117 + description: description !== undefined ? description : normalizedPub?.description, 118 + icon: iconBlob, 119 + theme: normalizedPub?.theme, 120 + preferences: preferencesData 121 + ? { 122 + $type: "pub.leaflet.publication#preferences" as const, 123 + showInDiscover: preferencesData.showInDiscover, 124 + showComments: preferencesData.showComments, 125 + showMentions: preferencesData.showMentions, 126 + showPrevNext: preferencesData.showPrevNext, 127 + } 128 + : undefined, 129 + base_path: existingBasePath, 130 + } as PubLeafletPublication.Record); 131 + 95 132 let result = await agent.com.atproto.repo.putRecord({ 96 133 repo: credentialSession.did!, 97 134 rkey: aturi.rkey, ··· 159 196 ? normalizedPub.url.replace(/^https?:\/\//, "") 160 197 : undefined; 161 198 162 - let record = { 163 - $type: publicationType, 164 - name: normalizedPub?.name || "", 165 - description: normalizedPub?.description, 166 - icon: normalizedPub?.icon, 167 - theme: normalizedPub?.theme, 168 - preferences: normalizedPub?.preferences 169 - ? { 170 - $type: "pub.leaflet.publication#preferences" as const, 171 - showInDiscover: normalizedPub.preferences.showInDiscover, 172 - showComments: normalizedPub.preferences.showComments, 173 - showMentions: normalizedPub.preferences.showMentions, 174 - showPrevNext: normalizedPub.preferences.showPrevNext, 175 - } 176 - : undefined, 177 - base_path, 178 - } as PubLeafletPublication.Record; 199 + // Build the record with the correct field based on publication type 200 + const record = 201 + publicationType === "site.standard.publication" 202 + ? ({ 203 + $type: publicationType, 204 + name: normalizedPub?.name || "", 205 + description: normalizedPub?.description, 206 + icon: normalizedPub?.icon, 207 + theme: normalizedPub?.theme, 208 + preferences: normalizedPub?.preferences 209 + ? { 210 + $type: "site.standard.publication#preferences" as const, 211 + showInDiscover: normalizedPub.preferences.showInDiscover, 212 + showComments: normalizedPub.preferences.showComments, 213 + showMentions: normalizedPub.preferences.showMentions, 214 + showPrevNext: normalizedPub.preferences.showPrevNext, 215 + } 216 + : undefined, 217 + url: `https://${base_path}`, 218 + } as SiteStandardPublication.Record) 219 + : ({ 220 + $type: publicationType, 221 + name: normalizedPub?.name || "", 222 + description: normalizedPub?.description, 223 + icon: normalizedPub?.icon, 224 + theme: normalizedPub?.theme, 225 + preferences: normalizedPub?.preferences 226 + ? { 227 + $type: "pub.leaflet.publication#preferences" as const, 228 + showInDiscover: normalizedPub.preferences.showInDiscover, 229 + showComments: normalizedPub.preferences.showComments, 230 + showMentions: normalizedPub.preferences.showMentions, 231 + showPrevNext: normalizedPub.preferences.showPrevNext, 232 + } 233 + : undefined, 234 + base_path, 235 + } as PubLeafletPublication.Record); 179 236 180 237 let result = await agent.com.atproto.repo.putRecord({ 181 238 repo: credentialSession.did!, ··· 257 314 ? normalizedPub.url.replace(/^https?:\/\//, "") 258 315 : undefined; 259 316 260 - let record = { 261 - $type: publicationType, 262 - name: normalizedPub?.name || "", 263 - description: normalizedPub?.description, 264 - icon: normalizedPub?.icon, 265 - base_path: existingBasePath, 266 - preferences: normalizedPub?.preferences 317 + // Build theme object (shared between both publication types) 318 + const themeData = { 319 + backgroundImage: theme.backgroundImage 267 320 ? { 268 - $type: "pub.leaflet.publication#preferences" as const, 269 - showInDiscover: normalizedPub.preferences.showInDiscover, 270 - showComments: normalizedPub.preferences.showComments, 271 - showMentions: normalizedPub.preferences.showMentions, 272 - showPrevNext: normalizedPub.preferences.showPrevNext, 321 + $type: "pub.leaflet.theme.backgroundImage", 322 + image: ( 323 + await agent.com.atproto.repo.uploadBlob( 324 + new Uint8Array(await theme.backgroundImage.arrayBuffer()), 325 + { encoding: theme.backgroundImage.type }, 326 + ) 327 + )?.data.blob, 328 + width: theme.backgroundRepeat || undefined, 329 + repeat: !!theme.backgroundRepeat, 330 + } 331 + : theme.backgroundImage === null 332 + ? undefined 333 + : normalizedPub?.theme?.backgroundImage, 334 + backgroundColor: theme.backgroundColor 335 + ? { 336 + ...theme.backgroundColor, 273 337 } 274 338 : undefined, 275 - theme: { 276 - backgroundImage: theme.backgroundImage 277 - ? { 278 - $type: "pub.leaflet.theme.backgroundImage", 279 - image: ( 280 - await agent.com.atproto.repo.uploadBlob( 281 - new Uint8Array(await theme.backgroundImage.arrayBuffer()), 282 - { encoding: theme.backgroundImage.type }, 283 - ) 284 - )?.data.blob, 285 - width: theme.backgroundRepeat || undefined, 286 - repeat: !!theme.backgroundRepeat, 287 - } 288 - : theme.backgroundImage === null 289 - ? undefined 290 - : normalizedPub?.theme?.backgroundImage, 291 - backgroundColor: theme.backgroundColor 292 - ? { 293 - ...theme.backgroundColor, 294 - } 295 - : undefined, 296 - pageWidth: theme.pageWidth, 297 - primary: { 298 - ...theme.primary, 299 - }, 300 - pageBackground: { 301 - ...theme.pageBackground, 302 - }, 303 - showPageBackground: theme.showPageBackground, 304 - accentBackground: { 305 - ...theme.accentBackground, 306 - }, 307 - accentText: { 308 - ...theme.accentText, 309 - }, 339 + pageWidth: theme.pageWidth, 340 + primary: { 341 + ...theme.primary, 342 + }, 343 + pageBackground: { 344 + ...theme.pageBackground, 345 + }, 346 + showPageBackground: theme.showPageBackground, 347 + accentBackground: { 348 + ...theme.accentBackground, 349 + }, 350 + accentText: { 351 + ...theme.accentText, 310 352 }, 311 - } as PubLeafletPublication.Record; 353 + }; 354 + 355 + // Build the record with the correct field based on publication type 356 + const record = 357 + publicationType === "site.standard.publication" 358 + ? ({ 359 + $type: publicationType, 360 + name: normalizedPub?.name || "", 361 + description: normalizedPub?.description, 362 + icon: normalizedPub?.icon, 363 + url: normalizedPub?.url || "", 364 + preferences: normalizedPub?.preferences 365 + ? { 366 + $type: "site.standard.publication#preferences" as const, 367 + showInDiscover: normalizedPub.preferences.showInDiscover, 368 + showComments: normalizedPub.preferences.showComments, 369 + showMentions: normalizedPub.preferences.showMentions, 370 + showPrevNext: normalizedPub.preferences.showPrevNext, 371 + } 372 + : undefined, 373 + theme: themeData, 374 + } as SiteStandardPublication.Record) 375 + : ({ 376 + $type: publicationType, 377 + name: normalizedPub?.name || "", 378 + description: normalizedPub?.description, 379 + icon: normalizedPub?.icon, 380 + base_path: existingBasePath, 381 + preferences: normalizedPub?.preferences 382 + ? { 383 + $type: "pub.leaflet.publication#preferences" as const, 384 + showInDiscover: normalizedPub.preferences.showInDiscover, 385 + showComments: normalizedPub.preferences.showComments, 386 + showMentions: normalizedPub.preferences.showMentions, 387 + showPrevNext: normalizedPub.preferences.showPrevNext, 388 + } 389 + : undefined, 390 + theme: themeData, 391 + } as PubLeafletPublication.Record); 312 392 313 393 let result = await agent.com.atproto.repo.putRecord({ 314 394 repo: credentialSession.did!,
+9 -5
app/lish/subscribeToPublication.ts
··· 48 48 let agent = new AtpBaseClient( 49 49 credentialSession.fetchHandler.bind(credentialSession), 50 50 ); 51 - let record = await agent.pub.leaflet.graph.subscription.create( 51 + let record = await agent.site.standard.graph.subscription.create( 52 52 { repo: credentialSession.did!, rkey: TID.nextStr() }, 53 53 { 54 54 publication, ··· 140 140 .eq("publication", publication) 141 141 .single(); 142 142 if (!existingSubscription) return { success: true }; 143 - await agent.pub.leaflet.graph.subscription.delete({ 144 - repo: credentialSession.did!, 145 - rkey: new AtUri(existingSubscription.uri).rkey, 146 - }); 143 + 144 + // Delete from both collections (old and new schema) - one or both may exist 145 + let rkey = new AtUri(existingSubscription.uri).rkey; 146 + await Promise.all([ 147 + agent.pub.leaflet.graph.subscription.delete({ repo: credentialSession.did!, rkey }).catch(() => {}), 148 + agent.site.standard.graph.subscription.delete({ repo: credentialSession.did!, rkey }).catch(() => {}), 149 + ]); 150 + 147 151 await supabaseServerClient 148 152 .from("publication_subscriptions") 149 153 .delete()
+4 -1
appview/index.ts
··· 247 247 if (docResult.error) console.log(docResult.error); 248 248 249 249 // site.standard.document uses "site" field to reference the publication 250 - if (record.value.site) { 250 + // For documents in publications, site is an AT-URI (at://did:plc:xxx/site.standard.publication/rkey) 251 + // For standalone documents, site is an HTTPS URL (https://leaflet.pub/p/did:plc:xxx) 252 + // Only link to publications table for AT-URI sites 253 + if (record.value.site && record.value.site.startsWith("at://")) { 251 254 let siteURI = new AtUri(record.value.site); 252 255 253 256 if (siteURI.host !== evt.uri.host) {