a tool for shared writing and social publishing

Compare changes

Choose any two refs to compare.

+586 -499
+21
actions/publishToPublication.ts
··· 78 cover_image, 79 entitiesToDelete, 80 publishedAt, 81 }: { 82 root_entity: string; 83 publication_uri?: string; ··· 88 cover_image?: string | null; 89 entitiesToDelete?: string[]; 90 publishedAt?: string; 91 }): Promise<PublishResult> { 92 let identity = await getIdentityData(); 93 if (!identity || !identity.atp_did) { ··· 175 }; 176 } 177 178 // Extract theme for standalone documents (not for publications) 179 let theme: PubLeafletPublication.Theme | undefined; 180 if (!publication_uri) { ··· 245 ...(coverImageBlob && { coverImage: coverImageBlob }), 246 // Include theme for standalone documents (not for publication documents) 247 ...(!publication_uri && theme && { theme }), 248 content: { 249 $type: "pub.leaflet.content" as const, 250 pages: pagesArray, ··· 257 author: credentialSession.did!, 258 ...(publication_uri && { publication: publication_uri }), 259 ...(theme && { theme }), 260 title: title || "Untitled", 261 description: description || "", 262 ...(tags !== undefined && { tags }),
··· 78 cover_image, 79 entitiesToDelete, 80 publishedAt, 81 + postPreferences, 82 }: { 83 root_entity: string; 84 publication_uri?: string; ··· 89 cover_image?: string | null; 90 entitiesToDelete?: string[]; 91 publishedAt?: string; 92 + postPreferences?: { 93 + showComments?: boolean; 94 + showMentions?: boolean; 95 + showRecommends?: boolean; 96 + } | null; 97 }): Promise<PublishResult> { 98 let identity = await getIdentityData(); 99 if (!identity || !identity.atp_did) { ··· 181 }; 182 } 183 184 + // Resolve preferences: explicit param > draft DB value 185 + const preferences = postPreferences ?? draft?.preferences; 186 + 187 // Extract theme for standalone documents (not for publications) 188 let theme: PubLeafletPublication.Theme | undefined; 189 if (!publication_uri) { ··· 254 ...(coverImageBlob && { coverImage: coverImageBlob }), 255 // Include theme for standalone documents (not for publication documents) 256 ...(!publication_uri && theme && { theme }), 257 + ...(preferences && { 258 + preferences: { 259 + $type: "pub.leaflet.publication#preferences" as const, 260 + ...preferences, 261 + }, 262 + }), 263 content: { 264 $type: "pub.leaflet.content" as const, 265 pages: pagesArray, ··· 272 author: credentialSession.did!, 273 ...(publication_uri && { publication: publication_uri }), 274 ...(theme && { theme }), 275 + ...(preferences && { 276 + preferences: { 277 + $type: "pub.leaflet.publication#preferences" as const, 278 + ...preferences, 279 + }, 280 + }), 281 title: title || "Untitled", 282 description: description || "", 283 ...(tags !== undefined && { tags }),
+4
app/(home-pages)/notifications/NotificationList.tsx
··· 11 import { BskyPostEmbedNotification } from "./BskyPostEmbedNotification"; 12 import { MentionNotification } from "./MentionNotification"; 13 import { CommentMentionNotification } from "./CommentMentionNotification"; 14 15 export function NotificationList({ 16 notifications, ··· 57 } 58 if (n.type === "comment_mention") { 59 return <CommentMentionNotification key={n.id} {...n} />; 60 } 61 })} 62 </div>
··· 11 import { BskyPostEmbedNotification } from "./BskyPostEmbedNotification"; 12 import { MentionNotification } from "./MentionNotification"; 13 import { CommentMentionNotification } from "./CommentMentionNotification"; 14 + import { RecommendNotification } from "./RecommendNotification"; 15 16 export function NotificationList({ 17 notifications, ··· 58 } 59 if (n.type === "comment_mention") { 60 return <CommentMentionNotification key={n.id} {...n} />; 61 + } 62 + if (n.type === "recommend") { 63 + return <RecommendNotification key={n.id} {...n} />; 64 } 65 })} 66 </div>
+48
app/(home-pages)/notifications/RecommendNotification.tsx
···
··· 1 + import { ContentLayout, Notification } from "./Notification"; 2 + import { HydratedRecommendNotification } from "src/notifications"; 3 + import { blobRefToSrc } from "src/utils/blobRefToSrc"; 4 + import { AppBskyActorProfile } from "lexicons/api"; 5 + import { Avatar } from "components/Avatar"; 6 + import { AtUri } from "@atproto/api"; 7 + import { RecommendTinyFilled } from "components/Icons/RecommendTiny"; 8 + 9 + export const RecommendNotification = ( 10 + props: HydratedRecommendNotification, 11 + ) => { 12 + const profileRecord = props.recommendData?.identities?.bsky_profiles 13 + ?.record as AppBskyActorProfile.Record; 14 + const displayName = 15 + profileRecord?.displayName || 16 + props.recommendData?.identities?.bsky_profiles?.handle || 17 + "Someone"; 18 + const docRecord = props.normalizedDocument; 19 + const pubRecord = props.normalizedPublication; 20 + const avatarSrc = 21 + profileRecord?.avatar?.ref && 22 + blobRefToSrc( 23 + profileRecord.avatar.ref, 24 + props.recommendData?.recommender_did || "", 25 + ); 26 + 27 + if (!docRecord) return null; 28 + 29 + const docUri = new AtUri(props.document.uri); 30 + const rkey = docUri.rkey; 31 + const did = docUri.host; 32 + 33 + const href = pubRecord ? `${pubRecord.url}/${rkey}` : `/p/${did}/${rkey}`; 34 + 35 + return ( 36 + <Notification 37 + timestamp={props.created_at} 38 + href={href} 39 + icon={<RecommendTinyFilled />} 40 + actionText={<>{displayName} recommended your post</>} 41 + content={ 42 + <ContentLayout postTitle={docRecord.title} pubRecord={pubRecord}> 43 + {null} 44 + </ContentLayout> 45 + } 46 + /> 47 + ); 48 + };
+2
app/[leaflet_id]/Footer.tsx
··· 14 import { useIdentityData } from "components/IdentityProvider"; 15 import { useEntity } from "src/replicache"; 16 import { block } from "sharp"; 17 18 export function hasBlockToolbar(blockType: string | null | undefined) { 19 return ( ··· 64 65 <PublishButton entityID={props.entityID} /> 66 <ShareOptions /> 67 <ThemePopover entityID={props.entityID} /> 68 </ActionFooter> 69 ) : (
··· 14 import { useIdentityData } from "components/IdentityProvider"; 15 import { useEntity } from "src/replicache"; 16 import { block } from "sharp"; 17 + import { PostSettings } from "components/PostSettings"; 18 19 export function hasBlockToolbar(blockType: string | null | undefined) { 20 return ( ··· 65 66 <PublishButton entityID={props.entityID} /> 67 <ShareOptions /> 68 + <PostSettings /> 69 <ThemePopover entityID={props.entityID} /> 70 </ActionFooter> 71 ) : (
+2
app/[leaflet_id]/Sidebar.tsx
··· 8 import { ShareOptions } from "app/[leaflet_id]/actions/ShareOptions"; 9 import { ThemePopover } from "components/ThemeManager/ThemeSetter"; 10 import { PublishButton } from "./actions/PublishButton"; 11 import { Watermark } from "components/Watermark"; 12 import { BackToPubButton } from "./actions/BackToPubButton"; 13 import { useIdentityData } from "components/IdentityProvider"; ··· 30 <Sidebar> 31 <PublishButton entityID={rootEntity} /> 32 <ShareOptions /> 33 <ThemePopover entityID={rootEntity} /> 34 <HelpButton /> 35 <hr className="text-border" />
··· 8 import { ShareOptions } from "app/[leaflet_id]/actions/ShareOptions"; 9 import { ThemePopover } from "components/ThemeManager/ThemeSetter"; 10 import { PublishButton } from "./actions/PublishButton"; 11 + import { PostSettings } from "components/PostSettings"; 12 import { Watermark } from "components/Watermark"; 13 import { BackToPubButton } from "./actions/BackToPubButton"; 14 import { useIdentityData } from "components/IdentityProvider"; ··· 31 <Sidebar> 32 <PublishButton entityID={rootEntity} /> 33 <ShareOptions /> 34 + <PostSettings /> 35 <ThemePopover entityID={rootEntity} /> 36 <HelpButton /> 37 <hr className="text-border" />
+10
app/[leaflet_id]/actions/PublishButton.tsx
··· 96 tx.get<string | null>("publication_cover_image"), 97 ); 98 99 // Get local published at from Replicache (session-only state, not persisted to DB) 100 let publishedAt = useLocalPublishedAt((s) => 101 pub?.doc ? s[pub?.doc] : undefined, ··· 118 tags: currentTags, 119 cover_image: coverImage, 120 publishedAt: publishedAt?.toISOString(), 121 }); 122 setIsLoading(false); 123 mutate();
··· 96 tx.get<string | null>("publication_cover_image"), 97 ); 98 99 + // Get post preferences from Replicache state 100 + let postPreferences = useSubscribe(rep, (tx) => 101 + tx.get<{ 102 + showComments?: boolean; 103 + showMentions?: boolean; 104 + showRecommends?: boolean; 105 + } | null>("post_preferences"), 106 + ); 107 + 108 // Get local published at from Replicache (session-only state, not persisted to DB) 109 let publishedAt = useLocalPublishedAt((s) => 110 pub?.doc ? s[pub?.doc] : undefined, ··· 127 tags: currentTags, 128 cover_image: coverImage, 129 publishedAt: publishedAt?.toISOString(), 130 + postPreferences, 131 }); 132 setIsLoading(false); 133 mutate();
+10
app/[leaflet_id]/publish/PublishPost.tsx
··· 91 tx.get<string | null>("publication_cover_image"), 92 ); 93 94 // Use Replicache tags only when we have a draft 95 const currentTags = props.hasDraft 96 ? Array.isArray(replicacheTags) ··· 124 cover_image: replicacheCoverImage, 125 entitiesToDelete: props.entitiesToDelete, 126 publishedAt: localPublishedAt?.toISOString() || new Date().toISOString(), 127 }); 128 129 if (!result.success) {
··· 91 tx.get<string | null>("publication_cover_image"), 92 ); 93 94 + // Get post preferences from Replicache state 95 + let postPreferences = useSubscribe(rep, (tx) => 96 + tx.get<{ 97 + showComments?: boolean; 98 + showMentions?: boolean; 99 + showRecommends?: boolean; 100 + } | null>("post_preferences"), 101 + ); 102 + 103 // Use Replicache tags only when we have a draft 104 const currentTags = props.hasDraft 105 ? Array.isArray(replicacheTags) ··· 133 cover_image: replicacheCoverImage, 134 entitiesToDelete: props.entitiesToDelete, 135 publishedAt: localPublishedAt?.toISOString() || new Date().toISOString(), 136 + postPreferences, 137 }); 138 139 if (!result.success) {
+6
app/api/inngest/client.ts
··· 51 documentUris?: string[]; 52 }; 53 }; 54 "user/write-records-to-pds": { 55 data: { 56 did: string;
··· 51 documentUris?: string[]; 52 }; 53 }; 54 + "appview/sync-bsky-likes": { 55 + data: { 56 + document_uri: string; 57 + bsky_post_uri: string; 58 + }; 59 + }; 60 "user/write-records-to-pds": { 61 data: { 62 did: string;
+55
app/api/inngest/functions/sync_bsky_likes.ts
···
··· 1 + import { inngest } from "../client"; 2 + import { supabaseServerClient } from "supabase/serverClient"; 3 + import { AtpAgent, AtUri } from "@atproto/api"; 4 + import { idResolver } from "app/(home-pages)/reader/idResolver"; 5 + 6 + const TOTAL_ITERATIONS = 144; // 36 hours at 15-minute intervals 7 + 8 + export const sync_bsky_likes = inngest.createFunction( 9 + { 10 + id: "sync_bsky_likes", 11 + idempotency: "event.data.bsky_post_uri", 12 + }, 13 + { event: "appview/sync-bsky-likes" }, 14 + async ({ event, step }) => { 15 + const { document_uri, bsky_post_uri } = event.data; 16 + 17 + const isBridgy = await step.run("check-bridgy", async () => { 18 + const did = new AtUri(bsky_post_uri).host; 19 + const doc = await idResolver.did.resolve(did); 20 + const handle = doc?.alsoKnownAs 21 + ?.find((a) => a.startsWith("at://")) 22 + ?.replace("at://", ""); 23 + return handle?.includes("brid.gy") ?? false; 24 + }); 25 + 26 + if (isBridgy) { 27 + return { skipped: true, reason: "brid.gy post" }; 28 + } 29 + 30 + const agent = new AtpAgent({ service: "https://public.api.bsky.app" }); 31 + 32 + const fetchAndUpdate = async () => { 33 + const res = await agent.app.bsky.feed.getPosts({ 34 + uris: [bsky_post_uri], 35 + }); 36 + const post = res.data.posts[0]; 37 + if (!post) return 0; 38 + const likeCount = post.likeCount ?? 0; 39 + await supabaseServerClient 40 + .from("documents") 41 + .update({ bsky_like_count: likeCount }) 42 + .eq("uri", document_uri); 43 + return likeCount; 44 + }; 45 + 46 + let likeCount = await step.run("sync-0", fetchAndUpdate); 47 + 48 + for (let i = 1; i < TOTAL_ITERATIONS; i++) { 49 + await step.sleep(`wait-${i}`, "15m"); 50 + likeCount = await step.run(`sync-${i}`, fetchAndUpdate); 51 + } 52 + 53 + return { likeCount }; 54 + }, 55 + );
+2
app/api/inngest/route.tsx
··· 13 check_oauth_session, 14 } from "./functions/cleanup_expired_oauth_sessions"; 15 import { write_records_to_pds } from "./functions/write_records_to_pds"; 16 17 export const { GET, POST, PUT } = serve({ 18 client: inngest, ··· 28 cleanup_expired_oauth_sessions, 29 check_oauth_session, 30 write_records_to_pds, 31 ], 32 });
··· 13 check_oauth_session, 14 } from "./functions/cleanup_expired_oauth_sessions"; 15 import { write_records_to_pds } from "./functions/write_records_to_pds"; 16 + import { sync_bsky_likes } from "./functions/sync_bsky_likes"; 17 18 export const { GET, POST, PUT } = serve({ 19 client: inngest, ··· 29 cleanup_expired_oauth_sessions, 30 check_oauth_session, 31 write_records_to_pds, 32 + sync_bsky_likes, 33 ], 34 });
+2 -1
app/api/oauth/[route]/oauth-metadata.ts
··· 7 ? "http://localhost:3000" 8 : "https://leaflet.pub"; 9 10 - const scope = "atproto transition:generic transition:email"; 11 const localconfig: OAuthClientMetadataInput = { 12 client_id: `http://localhost/?redirect_uri=${encodeURI(`http://127.0.0.1:3000/api/oauth/callback`)}&scope=${encodeURIComponent(scope)}`, 13 client_name: `Leaflet`,
··· 7 ? "http://localhost:3000" 8 : "https://leaflet.pub"; 9 10 + const scope = 11 + "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview blob:*/*"; 12 const localconfig: OAuthClientMetadataInput = { 13 client_id: `http://localhost/?redirect_uri=${encodeURI(`http://127.0.0.1:3000/api/oauth/callback`)}&scope=${encodeURIComponent(scope)}`, 14 client_name: `Leaflet`,
+2 -1
app/api/oauth/[route]/route.ts
··· 42 const ac = new AbortController(); 43 44 const url = await client.authorize(handle || "https://bsky.social", { 45 - scope: "atproto transition:generic transition:email", 46 signal: ac.signal, 47 state: JSON.stringify(state), 48 });
··· 42 const ac = new AbortController(); 43 44 const url = await client.authorize(handle || "https://bsky.social", { 45 + scope: 46 + "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts include:app.bsky.authViewAll?aud=did:web:api.bsky.app%23bsky_appview blob:*/*", 47 signal: ac.signal, 48 state: JSON.stringify(state), 49 });
+1
app/api/rpc/[command]/get_publication_data.ts
··· 86 indexed_at: dip.documents.indexed_at, 87 sort_date: dip.documents.sort_date, 88 data: dip.documents.data, 89 commentsCount: dip.documents.comments_on_documents[0]?.count || 0, 90 mentionsCount: dip.documents.document_mentions_in_bsky[0]?.count || 0, 91 recommendsCount:
··· 86 indexed_at: dip.documents.indexed_at, 87 sort_date: dip.documents.sort_date, 88 data: dip.documents.data, 89 + bsky_like_count: dip.documents.bsky_like_count, 90 commentsCount: dip.documents.comments_on_documents[0]?.count || 0, 91 mentionsCount: dip.documents.document_mentions_in_bsky[0]?.count || 0, 92 recommendsCount:
+7
app/api/rpc/[command]/pull.ts
··· 9 import type { Attribute } from "src/replicache/attributes"; 10 import { makeRoute } from "../lib"; 11 import type { Env } from "./route"; 12 13 // First define the sub-types for V0 and V1 requests 14 const pullRequestV0 = z.object({ ··· 75 title: string; 76 tags: string[]; 77 cover_image: string | null; 78 }[]; 79 let pub_patch = publication_data?.[0] 80 ? [ ··· 97 op: "put", 98 key: "publication_cover_image", 99 value: publication_data[0].cover_image || null, 100 }, 101 ] 102 : [];
··· 9 import type { Attribute } from "src/replicache/attributes"; 10 import { makeRoute } from "../lib"; 11 import type { Env } from "./route"; 12 + import type { Json } from "supabase/database.types"; 13 14 // First define the sub-types for V0 and V1 requests 15 const pullRequestV0 = z.object({ ··· 76 title: string; 77 tags: string[]; 78 cover_image: string | null; 79 + preferences: Json | null; 80 }[]; 81 let pub_patch = publication_data?.[0] 82 ? [ ··· 99 op: "put", 100 key: "publication_cover_image", 101 value: publication_data[0].cover_image || null, 102 + }, 103 + { 104 + op: "put", 105 + key: "post_preferences", 106 + value: publication_data[0].preferences || null, 107 }, 108 ] 109 : [];
+2 -1
app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx
··· 21 } from "src/utils/normalizeRecords"; 22 import { DocumentProvider } from "contexts/DocumentContext"; 23 import { LeafletContentProvider } from "contexts/LeafletContentContext"; 24 25 export async function DocumentPageRenderer({ 26 did, ··· 133 <LeafletLayout> 134 <PostPages 135 document_uri={document.uri} 136 - preferences={pubRecord?.preferences || {}} 137 pubRecord={pubRecord} 138 profile={JSON.parse(JSON.stringify(profile.data))} 139 document={document}
··· 21 } from "src/utils/normalizeRecords"; 22 import { DocumentProvider } from "contexts/DocumentContext"; 23 import { LeafletContentProvider } from "contexts/LeafletContentContext"; 24 + import { mergePreferences } from "src/utils/mergePreferences"; 25 26 export async function DocumentPageRenderer({ 27 did, ··· 134 <LeafletLayout> 135 <PostPages 136 document_uri={document.uri} 137 + preferences={mergePreferences(record?.preferences, pubRecord?.preferences)} 138 pubRecord={pubRecord} 139 profile={JSON.parse(JSON.stringify(profile.data))} 140 document={document}
+21
app/lish/[did]/[publication]/[rkey]/Interactions/recommendAction.ts
··· 7 import { AtUri, Un$Typed } from "@atproto/api"; 8 import { supabaseServerClient } from "supabase/serverClient"; 9 import { Json } from "supabase/database.types"; 10 11 type RecommendResult = 12 | { success: true; uri: string } ··· 67 } as unknown as Json, 68 }); 69 console.log(res); 70 71 return { 72 success: true,
··· 7 import { AtUri, Un$Typed } from "@atproto/api"; 8 import { supabaseServerClient } from "supabase/serverClient"; 9 import { Json } from "supabase/database.types"; 10 + import { v7 } from "uuid"; 11 + import { 12 + Notification, 13 + pingIdentityToUpdateNotification, 14 + } from "src/notifications"; 15 16 type RecommendResult = 17 | { success: true; uri: string } ··· 72 } as unknown as Json, 73 }); 74 console.log(res); 75 + 76 + // Notify the document owner 77 + let documentOwner = new AtUri(args.document).host; 78 + if (documentOwner !== credentialSession.did) { 79 + let notification: Notification = { 80 + id: v7(), 81 + recipient: documentOwner, 82 + data: { 83 + type: "recommend", 84 + document_uri: args.document, 85 + recommend_uri: uri.toString(), 86 + }, 87 + }; 88 + await supabaseServerClient.from("notifications").insert(notification); 89 + await pingIdentityToUpdateNotification(documentOwner); 90 + } 91 92 return { 93 success: true,
+4 -4
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 295 showPageBackground={pubRecord?.theme?.showPageBackground} 296 document_uri={document.uri} 297 comments={ 298 - pubRecord?.preferences?.showComments === false 299 ? [] 300 : document.comments_on_documents 301 } 302 quotesAndMentions={ 303 - pubRecord?.preferences?.showMentions === false 304 ? [] 305 : quotesAndMentions 306 } ··· 387 pageId={page.id} 388 document_uri={document.uri} 389 comments={ 390 - pubRecord?.preferences?.showComments === false 391 ? [] 392 : document.comments_on_documents 393 } 394 quotesAndMentions={ 395 - pubRecord?.preferences?.showMentions === false 396 ? [] 397 : quotesAndMentions 398 }
··· 295 showPageBackground={pubRecord?.theme?.showPageBackground} 296 document_uri={document.uri} 297 comments={ 298 + preferences.showComments === false 299 ? [] 300 : document.comments_on_documents 301 } 302 quotesAndMentions={ 303 + preferences.showMentions === false 304 ? [] 305 : quotesAndMentions 306 } ··· 387 pageId={page.id} 388 document_uri={document.uri} 389 comments={ 390 + preferences.showComments === false 391 ? [] 392 : document.comments_on_documents 393 } 394 quotesAndMentions={ 395 + preferences.showMentions === false 396 ? [] 397 : quotesAndMentions 398 }
+1
app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx
··· 112 indexed_at: doc.indexed_at, 113 sort_date: doc.sort_date, 114 data: doc.data, 115 }, 116 }, 117 ],
··· 112 indexed_at: doc.indexed_at, 113 sort_date: doc.sort_date, 114 data: doc.data, 115 + bsky_like_count: doc.bsky_like_count ?? 0, 116 }, 117 }, 118 ],
+1 -1
app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx
··· 105 <div className="flex flex-col justify-start"> 106 <div className="font-bold">Show Mentions</div> 107 <div className="text-tertiary text-sm leading-tight"> 108 - Display a list Bluesky mentions about your post 109 </div> 110 </div> 111 </Toggle>
··· 105 <div className="flex flex-col justify-start"> 106 <div className="font-bold">Show Mentions</div> 107 <div className="text-tertiary text-sm leading-tight"> 108 + Display a list of Bluesky mentions about your post 109 </div> 110 </div> 111 </Toggle>
+18
appview/index.ts
··· 109 data: record.value as Json, 110 }); 111 if (docResult.error) console.log(docResult.error); 112 if (record.value.publication) { 113 let publicationURI = new AtUri(record.value.publication); 114 ··· 269 data: record.value as Json, 270 }); 271 if (docResult.error) console.log(docResult.error); 272 273 // site.standard.document uses "site" field to reference the publication 274 // For documents in publications, site is an AT-URI (at://did:plc:xxx/site.standard.publication/rkey)
··· 109 data: record.value as Json, 110 }); 111 if (docResult.error) console.log(docResult.error); 112 + if (record.value.postRef?.uri) { 113 + await inngest.send({ 114 + name: "appview/sync-bsky-likes", 115 + data: { 116 + document_uri: evt.uri.toString(), 117 + bsky_post_uri: record.value.postRef.uri, 118 + }, 119 + }); 120 + } 121 if (record.value.publication) { 122 let publicationURI = new AtUri(record.value.publication); 123 ··· 278 data: record.value as Json, 279 }); 280 if (docResult.error) console.log(docResult.error); 281 + if (record.value.bskyPostRef?.uri) { 282 + await inngest.send({ 283 + name: "appview/sync-bsky-likes", 284 + data: { 285 + document_uri: evt.uri.toString(), 286 + bsky_post_uri: record.value.bskyPostRef.uri, 287 + }, 288 + }); 289 + } 290 291 // site.standard.document uses "site" field to reference the publication 292 // For documents in publications, site is an AT-URI (at://did:plc:xxx/site.standard.publication/rkey)
+17 -4
components/Canvas.tsx
··· 24 import { useHandleCanvasDrop } from "./Blocks/useHandleCanvasDrop"; 25 import { useBlockMouseHandlers } from "./Blocks/useBlockMouseHandlers"; 26 import { RecommendTinyEmpty } from "./Icons/RecommendTiny"; 27 28 export function Canvas(props: { 29 entityID: string; ··· 164 165 const CanvasMetadata = (props: { isSubpage: boolean | undefined }) => { 166 let { data: pub, normalizedPublication } = useLeafletPublicationData(); 167 if (!pub || !pub.publications) return null; 168 169 if (!normalizedPublication) return null; 170 - let showComments = normalizedPublication.preferences?.showComments !== false; 171 - let showMentions = normalizedPublication.preferences?.showMentions !== false; 172 - let showRecommends = 173 - normalizedPublication.preferences?.showRecommends !== false; 174 175 return ( 176 <div className="flex flex-row gap-3 items-center absolute top-6 right-3 sm:top-4 sm:right-4 bg-bg-page border-border-light rounded-md px-2 py-1 h-fit z-20">
··· 24 import { useHandleCanvasDrop } from "./Blocks/useHandleCanvasDrop"; 25 import { useBlockMouseHandlers } from "./Blocks/useBlockMouseHandlers"; 26 import { RecommendTinyEmpty } from "./Icons/RecommendTiny"; 27 + import { useSubscribe } from "src/replicache/useSubscribe"; 28 + import { mergePreferences } from "src/utils/mergePreferences"; 29 30 export function Canvas(props: { 31 entityID: string; ··· 166 167 const CanvasMetadata = (props: { isSubpage: boolean | undefined }) => { 168 let { data: pub, normalizedPublication } = useLeafletPublicationData(); 169 + let { rep } = useReplicache(); 170 + let postPreferences = useSubscribe(rep, (tx) => 171 + tx.get<{ 172 + showComments?: boolean; 173 + showMentions?: boolean; 174 + showRecommends?: boolean; 175 + } | null>("post_preferences"), 176 + ); 177 if (!pub || !pub.publications) return null; 178 179 if (!normalizedPublication) return null; 180 + let merged = mergePreferences( 181 + postPreferences || undefined, 182 + normalizedPublication.preferences, 183 + ); 184 + let showComments = merged.showComments !== false; 185 + let showMentions = merged.showMentions !== false; 186 + let showRecommends = merged.showRecommends !== false; 187 188 return ( 189 <div className="flex flex-row gap-3 items-center absolute top-6 right-3 sm:top-4 sm:right-4 bg-bg-page border-border-light rounded-md px-2 py-1 h-fit z-20">
+18 -7
components/Pages/PublicationMetadata.tsx
··· 21 import { PostHeaderLayout } from "app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader"; 22 import { Backdater } from "./Backdater"; 23 import { RecommendTinyEmpty } from "components/Icons/RecommendTiny"; 24 25 export const PublicationMetadata = (props: { noInteractions?: boolean }) => { 26 let { rep } = useReplicache(); ··· 33 let title = useSubscribe(rep, (tx) => tx.get<string>("publication_title")); 34 let description = useSubscribe(rep, (tx) => 35 tx.get<string>("publication_description"), 36 ); 37 let publishedAt = normalizedDocument?.publishedAt; 38 ··· 121 )} 122 {!props.noInteractions && ( 123 <div className="flex gap-2 text-border items-center"> 124 - {normalizedPublication?.preferences?.showRecommends !== false && ( 125 <div className="flex gap-1 items-center"> 126 <RecommendTinyEmpty />โ€” 127 </div> 128 )} 129 130 - {normalizedPublication?.preferences?.showMentions !== false && ( 131 <div className="flex gap-1 items-center"> 132 <QuoteTiny />โ€” 133 </div> 134 )} 135 - {normalizedPublication?.preferences?.showComments !== false && ( 136 <div className="flex gap-1 items-center"> 137 <CommentTiny />โ€” 138 </div> 139 )} 140 {tags && ( 141 <> 142 - {normalizedPublication?.preferences?.showRecommends !== 143 - false || 144 - normalizedPublication?.preferences?.showMentions !== false || 145 - normalizedPublication?.preferences?.showComments !== false ? ( 146 <Separator classname="h-4!" /> 147 ) : null} 148 <AddTags />
··· 21 import { PostHeaderLayout } from "app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader"; 22 import { Backdater } from "./Backdater"; 23 import { RecommendTinyEmpty } from "components/Icons/RecommendTiny"; 24 + import { mergePreferences } from "src/utils/mergePreferences"; 25 26 export const PublicationMetadata = (props: { noInteractions?: boolean }) => { 27 let { rep } = useReplicache(); ··· 34 let title = useSubscribe(rep, (tx) => tx.get<string>("publication_title")); 35 let description = useSubscribe(rep, (tx) => 36 tx.get<string>("publication_description"), 37 + ); 38 + let postPreferences = useSubscribe(rep, (tx) => 39 + tx.get<{ 40 + showComments?: boolean; 41 + showMentions?: boolean; 42 + showRecommends?: boolean; 43 + } | null>("post_preferences"), 44 + ); 45 + let merged = mergePreferences( 46 + postPreferences || undefined, 47 + normalizedPublication?.preferences, 48 ); 49 let publishedAt = normalizedDocument?.publishedAt; 50 ··· 133 )} 134 {!props.noInteractions && ( 135 <div className="flex gap-2 text-border items-center"> 136 + {merged.showRecommends !== false && ( 137 <div className="flex gap-1 items-center"> 138 <RecommendTinyEmpty />โ€” 139 </div> 140 )} 141 142 + {merged.showMentions !== false && ( 143 <div className="flex gap-1 items-center"> 144 <QuoteTiny />โ€” 145 </div> 146 )} 147 + {merged.showComments !== false && ( 148 <div className="flex gap-1 items-center"> 149 <CommentTiny />โ€” 150 </div> 151 )} 152 {tags && ( 153 <> 154 + {merged.showRecommends !== false || 155 + merged.showMentions !== false || 156 + merged.showComments !== false ? ( 157 <Separator classname="h-4!" /> 158 ) : null} 159 <AddTags />
+7 -6
components/PostListing.tsx
··· 17 import Link from "next/link"; 18 import { InteractionPreview } from "./InteractionsPreview"; 19 import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 20 21 export const PostListing = (props: Post) => { 22 let pubRecord = props.publication?.pubRecord as ··· 48 ? pubRecord?.theme?.showPageBackground 49 : postRecord.theme?.showPageBackground ?? true; 50 51 let quotes = props.documents.document_mentions_in_bsky?.[0]?.count || 0; 52 let comments = 53 - pubRecord?.preferences?.showComments === false 54 ? 0 55 : props.documents.comments_on_documents?.[0]?.count || 0; 56 let recommends = props.documents.recommends_on_documents?.[0]?.count || 0; ··· 109 recommendsCount={recommends} 110 documentUri={props.documents.uri} 111 tags={tags} 112 - showComments={pubRecord?.preferences?.showComments !== false} 113 - showMentions={pubRecord?.preferences?.showMentions !== false} 114 - showRecommends={ 115 - pubRecord?.preferences?.showRecommends !== false 116 - } 117 share 118 /> 119 </div>
··· 17 import Link from "next/link"; 18 import { InteractionPreview } from "./InteractionsPreview"; 19 import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 20 + import { mergePreferences } from "src/utils/mergePreferences"; 21 22 export const PostListing = (props: Post) => { 23 let pubRecord = props.publication?.pubRecord as ··· 49 ? pubRecord?.theme?.showPageBackground 50 : postRecord.theme?.showPageBackground ?? true; 51 52 + let mergedPrefs = mergePreferences(postRecord?.preferences, pubRecord?.preferences); 53 + 54 let quotes = props.documents.document_mentions_in_bsky?.[0]?.count || 0; 55 let comments = 56 + mergedPrefs.showComments === false 57 ? 0 58 : props.documents.comments_on_documents?.[0]?.count || 0; 59 let recommends = props.documents.recommends_on_documents?.[0]?.count || 0; ··· 112 recommendsCount={recommends} 113 documentUri={props.documents.uri} 114 tags={tags} 115 + showComments={mergedPrefs.showComments !== false} 116 + showMentions={mergedPrefs.showMentions !== false} 117 + showRecommends={mergedPrefs.showRecommends !== false} 118 share 119 /> 120 </div>
+96
components/PostSettings.tsx
···
··· 1 + "use client"; 2 + 3 + import { ActionButton } from "components/ActionBar/ActionButton"; 4 + import { SettingsSmall } from "components/Icons/SettingsSmall"; 5 + import { Toggle } from "components/Toggle"; 6 + import { Popover } from "components/Popover"; 7 + import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 8 + import { useReplicache } from "src/replicache"; 9 + import { useSubscribe } from "src/replicache/useSubscribe"; 10 + 11 + type PostPreferences = { 12 + showComments?: boolean; 13 + showMentions?: boolean; 14 + showRecommends?: boolean; 15 + }; 16 + 17 + export function PostSettings() { 18 + let { data: pub, normalizedPublication } = useLeafletPublicationData(); 19 + let { rep } = useReplicache(); 20 + 21 + let postPreferences = useSubscribe(rep, (tx) => 22 + tx.get<PostPreferences | null>("post_preferences"), 23 + ); 24 + 25 + if (!pub || !pub.publications) return null; 26 + 27 + let pubPrefs = normalizedPublication?.preferences; 28 + 29 + let showComments = 30 + postPreferences?.showComments ?? pubPrefs?.showComments ?? true; 31 + let showMentions = 32 + postPreferences?.showMentions ?? pubPrefs?.showMentions ?? true; 33 + let showRecommends = 34 + postPreferences?.showRecommends ?? pubPrefs?.showRecommends ?? true; 35 + 36 + const updatePreference = ( 37 + field: keyof PostPreferences, 38 + value: boolean, 39 + ) => { 40 + let current: PostPreferences = postPreferences || {}; 41 + rep?.mutate.updatePublicationDraft({ 42 + preferences: { ...current, [field]: value }, 43 + }); 44 + }; 45 + 46 + return ( 47 + <Popover 48 + asChild 49 + side="right" 50 + align="start" 51 + className="max-w-xs w-[1000px]" 52 + trigger={ 53 + <ActionButton 54 + icon={<SettingsSmall />} 55 + label="Settings" 56 + /> 57 + } 58 + > 59 + <div className="text-primary flex flex-col"> 60 + <div className="flex justify-between font-bold text-secondary bg-border-light -mx-3 -mt-2 px-3 py-2 mb-1"> 61 + This Post Settings 62 + </div> 63 + <div className="flex flex-col gap-2"> 64 + <Toggle 65 + toggle={showComments} 66 + onToggle={() => updatePreference("showComments", !showComments)} 67 + > 68 + <div className="font-bold">Show Comments</div> 69 + </Toggle> 70 + <Toggle 71 + toggle={showMentions} 72 + onToggle={() => updatePreference("showMentions", !showMentions)} 73 + > 74 + <div className="flex flex-col justify-start"> 75 + <div className="font-bold">Show Mentions</div> 76 + <div className="text-tertiary text-sm leading-tight"> 77 + Display a list of Bluesky mentions about your post 78 + </div> 79 + </div> 80 + </Toggle> 81 + <Toggle 82 + toggle={showRecommends} 83 + onToggle={() => updatePreference("showRecommends", !showRecommends)} 84 + > 85 + <div className="flex flex-col justify-start"> 86 + <div className="font-bold">Show Recommends</div> 87 + <div className="text-tertiary text-sm leading-tight"> 88 + Allow readers to recommend/like your post 89 + </div> 90 + </div> 91 + </Toggle> 92 + </div> 93 + </div> 94 + </Popover> 95 + ); 96 + }
+2 -1
drizzle/schema.ts
··· 1 - import { pgTable, pgEnum, text, jsonb, foreignKey, timestamp, boolean, uuid, index, bigint, unique, uniqueIndex, smallint, primaryKey } from "drizzle-orm/pg-core" 2 import { sql } from "drizzle-orm" 3 4 export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) ··· 225 uri: text("uri").primaryKey().notNull(), 226 data: jsonb("data").notNull(), 227 indexed_at: timestamp("indexed_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 228 }); 229 230 export const atp_poll_votes = pgTable("atp_poll_votes", {
··· 1 + import { pgTable, pgEnum, text, jsonb, foreignKey, timestamp, boolean, uuid, index, bigint, unique, uniqueIndex, smallint, primaryKey, integer } from "drizzle-orm/pg-core" 2 import { sql } from "drizzle-orm" 3 4 export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) ··· 225 uri: text("uri").primaryKey().notNull(), 226 data: jsonb("data").notNull(), 227 indexed_at: timestamp("indexed_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 228 + bsky_like_count: integer("bsky_like_count").default(0).notNull(), 229 }); 230 231 export const atp_poll_votes = pgTable("atp_poll_votes", {
+17
lexicons/api/lexicons.ts
··· 1469 type: 'ref', 1470 ref: 'lex:pub.leaflet.publication#theme', 1471 }, 1472 tags: { 1473 type: 'array', 1474 items: { ··· 1868 type: 'boolean', 1869 default: true, 1870 }, 1871 }, 1872 }, 1873 theme: { ··· 2194 maxLength: 5000, 2195 type: 'string', 2196 }, 2197 updatedAt: { 2198 format: 'datetime', 2199 type: 'string', ··· 2288 }, 2289 showPrevNext: { 2290 default: false, 2291 type: 'boolean', 2292 }, 2293 },
··· 1469 type: 'ref', 1470 ref: 'lex:pub.leaflet.publication#theme', 1471 }, 1472 + preferences: { 1473 + type: 'ref', 1474 + ref: 'lex:pub.leaflet.publication#preferences', 1475 + }, 1476 tags: { 1477 type: 'array', 1478 items: { ··· 1872 type: 'boolean', 1873 default: true, 1874 }, 1875 + showRecommends: { 1876 + type: 'boolean', 1877 + default: true, 1878 + }, 1879 }, 1880 }, 1881 theme: { ··· 2202 maxLength: 5000, 2203 type: 'string', 2204 }, 2205 + preferences: { 2206 + type: 'union', 2207 + refs: ['lex:pub.leaflet.publication#preferences'], 2208 + closed: false, 2209 + }, 2210 updatedAt: { 2211 format: 'datetime', 2212 type: 'string', ··· 2301 }, 2302 showPrevNext: { 2303 default: false, 2304 + type: 'boolean', 2305 + }, 2306 + showRecommends: { 2307 + default: true, 2308 type: 'boolean', 2309 }, 2310 },
+1
lexicons/api/types/pub/leaflet/document.ts
··· 23 publication?: string 24 author: string 25 theme?: PubLeafletPublication.Theme 26 tags?: string[] 27 coverImage?: BlobRef 28 pages: (
··· 23 publication?: string 24 author: string 25 theme?: PubLeafletPublication.Theme 26 + preferences?: PubLeafletPublication.Preferences 27 tags?: string[] 28 coverImage?: BlobRef 29 pages: (
+40 -44
lexicons/api/types/pub/leaflet/publication.ts
··· 1 /** 2 * GENERATED CODE - DO NOT MODIFY 3 */ 4 - import { type ValidationResult, BlobRef } from "@atproto/lexicon"; 5 - import { CID } from "multiformats/cid"; 6 - import { validate as _validate } from "../../../lexicons"; 7 - import { 8 - type $Typed, 9 - is$typed as _is$typed, 10 - type OmitKey, 11 - } from "../../../util"; 12 - import type * as PubLeafletThemeColor from "./theme/color"; 13 - import type * as PubLeafletThemeBackgroundImage from "./theme/backgroundImage"; 14 15 const is$typed = _is$typed, 16 - validate = _validate; 17 - const id = "pub.leaflet.publication"; 18 19 export interface Record { 20 - $type: "pub.leaflet.publication"; 21 - name: string; 22 - base_path?: string; 23 - description?: string; 24 - icon?: BlobRef; 25 - theme?: Theme; 26 - preferences?: Preferences; 27 - [k: string]: unknown; 28 } 29 30 - const hashRecord = "main"; 31 32 export function isRecord<V>(v: V) { 33 - return is$typed(v, id, hashRecord); 34 } 35 36 export function validateRecord<V>(v: V) { 37 - return validate<Record & V>(v, id, hashRecord, true); 38 } 39 40 export interface Preferences { 41 - $type?: "pub.leaflet.publication#preferences"; 42 - showInDiscover: boolean; 43 - showComments: boolean; 44 - showMentions: boolean; 45 - showPrevNext: boolean; 46 - showRecommends: boolean; 47 } 48 49 - const hashPreferences = "preferences"; 50 51 export function isPreferences<V>(v: V) { 52 - return is$typed(v, id, hashPreferences); 53 } 54 55 export function validatePreferences<V>(v: V) { 56 - return validate<Preferences & V>(v, id, hashPreferences); 57 } 58 59 export interface Theme { 60 - $type?: "pub.leaflet.publication#theme"; 61 backgroundColor?: 62 | $Typed<PubLeafletThemeColor.Rgba> 63 | $Typed<PubLeafletThemeColor.Rgb> 64 - | { $type: string }; 65 - backgroundImage?: PubLeafletThemeBackgroundImage.Main; 66 - pageWidth?: number; 67 primary?: 68 | $Typed<PubLeafletThemeColor.Rgba> 69 | $Typed<PubLeafletThemeColor.Rgb> 70 - | { $type: string }; 71 pageBackground?: 72 | $Typed<PubLeafletThemeColor.Rgba> 73 | $Typed<PubLeafletThemeColor.Rgb> 74 - | { $type: string }; 75 - showPageBackground: boolean; 76 accentBackground?: 77 | $Typed<PubLeafletThemeColor.Rgba> 78 | $Typed<PubLeafletThemeColor.Rgb> 79 - | { $type: string }; 80 accentText?: 81 | $Typed<PubLeafletThemeColor.Rgba> 82 | $Typed<PubLeafletThemeColor.Rgb> 83 - | { $type: string }; 84 } 85 86 - const hashTheme = "theme"; 87 88 export function isTheme<V>(v: V) { 89 - return is$typed(v, id, hashTheme); 90 } 91 92 export function validateTheme<V>(v: V) { 93 - return validate<Theme & V>(v, id, hashTheme); 94 }
··· 1 /** 2 * GENERATED CODE - DO NOT MODIFY 3 */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../lexicons' 7 + import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 + import type * as PubLeafletThemeColor from './theme/color' 9 + import type * as PubLeafletThemeBackgroundImage from './theme/backgroundImage' 10 11 const is$typed = _is$typed, 12 + validate = _validate 13 + const id = 'pub.leaflet.publication' 14 15 export interface Record { 16 + $type: 'pub.leaflet.publication' 17 + name: string 18 + base_path?: string 19 + description?: string 20 + icon?: BlobRef 21 + theme?: Theme 22 + preferences?: Preferences 23 + [k: string]: unknown 24 } 25 26 + const hashRecord = 'main' 27 28 export function isRecord<V>(v: V) { 29 + return is$typed(v, id, hashRecord) 30 } 31 32 export function validateRecord<V>(v: V) { 33 + return validate<Record & V>(v, id, hashRecord, true) 34 } 35 36 export interface Preferences { 37 + $type?: 'pub.leaflet.publication#preferences' 38 + showInDiscover: boolean 39 + showComments: boolean 40 + showMentions: boolean 41 + showPrevNext: boolean 42 + showRecommends: boolean 43 } 44 45 + const hashPreferences = 'preferences' 46 47 export function isPreferences<V>(v: V) { 48 + return is$typed(v, id, hashPreferences) 49 } 50 51 export function validatePreferences<V>(v: V) { 52 + return validate<Preferences & V>(v, id, hashPreferences) 53 } 54 55 export interface Theme { 56 + $type?: 'pub.leaflet.publication#theme' 57 backgroundColor?: 58 | $Typed<PubLeafletThemeColor.Rgba> 59 | $Typed<PubLeafletThemeColor.Rgb> 60 + | { $type: string } 61 + backgroundImage?: PubLeafletThemeBackgroundImage.Main 62 + pageWidth?: number 63 primary?: 64 | $Typed<PubLeafletThemeColor.Rgba> 65 | $Typed<PubLeafletThemeColor.Rgb> 66 + | { $type: string } 67 pageBackground?: 68 | $Typed<PubLeafletThemeColor.Rgba> 69 | $Typed<PubLeafletThemeColor.Rgb> 70 + | { $type: string } 71 + showPageBackground: boolean 72 accentBackground?: 73 | $Typed<PubLeafletThemeColor.Rgba> 74 | $Typed<PubLeafletThemeColor.Rgb> 75 + | { $type: string } 76 accentText?: 77 | $Typed<PubLeafletThemeColor.Rgba> 78 | $Typed<PubLeafletThemeColor.Rgb> 79 + | { $type: string } 80 } 81 82 + const hashTheme = 'theme' 83 84 export function isTheme<V>(v: V) { 85 + return is$typed(v, id, hashTheme) 86 } 87 88 export function validateTheme<V>(v: V) { 89 + return validate<Theme & V>(v, id, hashTheme) 90 }
+1
lexicons/api/types/site/standard/document.ts
··· 28 textContent?: string 29 theme?: PubLeafletPublication.Theme 30 title: string 31 updatedAt?: string 32 [k: string]: unknown 33 }
··· 28 textContent?: string 29 theme?: PubLeafletPublication.Theme 30 title: string 31 + preferences?: $Typed<PubLeafletPublication.Preferences> | { $type: string } 32 updatedAt?: string 33 [k: string]: unknown 34 }
+29 -33
lexicons/api/types/site/standard/publication.ts
··· 1 /** 2 * GENERATED CODE - DO NOT MODIFY 3 */ 4 - import { type ValidationResult, BlobRef } from "@atproto/lexicon"; 5 - import { CID } from "multiformats/cid"; 6 - import { validate as _validate } from "../../../lexicons"; 7 - import { 8 - type $Typed, 9 - is$typed as _is$typed, 10 - type OmitKey, 11 - } from "../../../util"; 12 - import type * as SiteStandardThemeBasic from "./theme/basic"; 13 - import type * as PubLeafletPublication from "../../pub/leaflet/publication"; 14 15 const is$typed = _is$typed, 16 - validate = _validate; 17 - const id = "site.standard.publication"; 18 19 export interface Record { 20 - $type: "site.standard.publication"; 21 - basicTheme?: SiteStandardThemeBasic.Main; 22 - theme?: $Typed<PubLeafletPublication.Theme> | { $type: string }; 23 - description?: string; 24 - icon?: BlobRef; 25 - name: string; 26 - preferences?: Preferences; 27 - url: string; 28 - [k: string]: unknown; 29 } 30 31 - const hashRecord = "main"; 32 33 export function isRecord<V>(v: V) { 34 - return is$typed(v, id, hashRecord); 35 } 36 37 export function validateRecord<V>(v: V) { 38 - return validate<Record & V>(v, id, hashRecord, true); 39 } 40 41 export interface Preferences { 42 - $type?: "site.standard.publication#preferences"; 43 - showInDiscover: boolean; 44 - showComments: boolean; 45 - showMentions: boolean; 46 - showPrevNext: boolean; 47 - showRecommends: boolean; 48 } 49 50 - const hashPreferences = "preferences"; 51 52 export function isPreferences<V>(v: V) { 53 - return is$typed(v, id, hashPreferences); 54 } 55 56 export function validatePreferences<V>(v: V) { 57 - return validate<Preferences & V>(v, id, hashPreferences); 58 }
··· 1 /** 2 * GENERATED CODE - DO NOT MODIFY 3 */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../lexicons' 7 + import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 + import type * as SiteStandardThemeBasic from './theme/basic' 9 + import type * as PubLeafletPublication from '../../pub/leaflet/publication' 10 11 const is$typed = _is$typed, 12 + validate = _validate 13 + const id = 'site.standard.publication' 14 15 export interface Record { 16 + $type: 'site.standard.publication' 17 + basicTheme?: SiteStandardThemeBasic.Main 18 + theme?: $Typed<PubLeafletPublication.Theme> | { $type: string } 19 + description?: string 20 + icon?: BlobRef 21 + name: string 22 + preferences?: Preferences 23 + url: string 24 + [k: string]: unknown 25 } 26 27 + const hashRecord = 'main' 28 29 export function isRecord<V>(v: V) { 30 + return is$typed(v, id, hashRecord) 31 } 32 33 export function validateRecord<V>(v: V) { 34 + return validate<Record & V>(v, id, hashRecord, true) 35 } 36 37 export interface Preferences { 38 + $type?: 'site.standard.publication#preferences' 39 + showInDiscover: boolean 40 + showComments: boolean 41 + showMentions: boolean 42 + showPrevNext: boolean 43 + showRecommends: boolean 44 } 45 46 + const hashPreferences = 'preferences' 47 48 export function isPreferences<V>(v: V) { 49 + return is$typed(v, id, hashPreferences) 50 } 51 52 export function validatePreferences<V>(v: V) { 53 + return validate<Preferences & V>(v, id, hashPreferences) 54 }
+4
lexicons/pub/leaflet/document.json
··· 46 "type": "ref", 47 "ref": "pub.leaflet.publication#theme" 48 }, 49 "tags": { 50 "type": "array", 51 "items": {
··· 46 "type": "ref", 47 "ref": "pub.leaflet.publication#theme" 48 }, 49 + "preferences": { 50 + "type": "ref", 51 + "ref": "pub.leaflet.publication#preferences" 52 + }, 53 "tags": { 54 "type": "array", 55 "items": {
+5
lexicons/site/standard/document.json
··· 57 "maxLength": 5000, 58 "type": "string" 59 }, 60 "updatedAt": { 61 "format": "datetime", 62 "type": "string"
··· 57 "maxLength": 5000, 58 "type": "string" 59 }, 60 + "preferences": { 61 + "type": "union", 62 + "refs": ["pub.leaflet.publication#preferences"], 63 + "closed": false 64 + }, 65 "updatedAt": { 66 "format": "datetime", 67 "type": "string"
+4
lexicons/src/document.ts
··· 23 publication: { type: "string", format: "at-uri" }, 24 author: { type: "string", format: "at-identifier" }, 25 theme: { type: "ref", ref: "pub.leaflet.publication#theme" }, 26 tags: { type: "array", items: { type: "string", maxLength: 50 } }, 27 coverImage: { 28 type: "blob",
··· 23 publication: { type: "string", format: "at-uri" }, 24 author: { type: "string", format: "at-identifier" }, 25 theme: { type: "ref", ref: "pub.leaflet.publication#theme" }, 26 + preferences: { 27 + type: "ref", 28 + ref: "pub.leaflet.publication#preferences", 29 + }, 30 tags: { type: "array", items: { type: "string", maxLength: 50 } }, 31 coverImage: { 32 type: "blob",
+12
lexicons/src/normalize.ts
··· 28 export type NormalizedDocument = SiteStandardDocument.Record & { 29 // Keep the original theme for components that need leaflet-specific styling 30 theme?: PubLeafletPublication.Theme; 31 }; 32 33 // Normalized publication type - uses the generated site.standard.publication type ··· 169 170 // Pass through site.standard records directly (theme is already in correct format if present) 171 if (isStandardDocument(record)) { 172 return { 173 ...record, 174 theme: record.theme, 175 } as NormalizedDocument; 176 } 177 ··· 198 } 199 : undefined; 200 201 return { 202 $type: "site.standard.document", 203 title: record.title, ··· 210 bskyPostRef: record.postRef, 211 content, 212 theme: record.theme, 213 }; 214 } 215
··· 28 export type NormalizedDocument = SiteStandardDocument.Record & { 29 // Keep the original theme for components that need leaflet-specific styling 30 theme?: PubLeafletPublication.Theme; 31 + preferences?: SiteStandardPublication.Preferences; 32 }; 33 34 // Normalized publication type - uses the generated site.standard.publication type ··· 170 171 // Pass through site.standard records directly (theme is already in correct format if present) 172 if (isStandardDocument(record)) { 173 + const preferences = record.preferences as 174 + | SiteStandardPublication.Preferences 175 + | undefined; 176 return { 177 ...record, 178 theme: record.theme, 179 + preferences, 180 } as NormalizedDocument; 181 } 182 ··· 203 } 204 : undefined; 205 206 + // Extract preferences if present (available after lexicon rebuild) 207 + const leafletPrefs = (record as Record<string, unknown>) 208 + .preferences as SiteStandardPublication.Preferences | undefined; 209 + 210 return { 211 $type: "site.standard.document", 212 title: record.title, ··· 219 bskyPostRef: record.postRef, 220 content, 221 theme: record.theme, 222 + preferences: leafletPrefs 223 + ? { ...leafletPrefs, $type: "site.standard.publication#preferences" as const } 224 + : undefined, 225 }; 226 } 227
+6 -391
package-lock.json
··· 16 "@atproto/oauth-client-node": "^0.3.8", 17 "@atproto/sync": "^0.1.34", 18 "@atproto/syntax": "^0.3.3", 19 - "@atproto/tap": "^0.1.1", 20 "@atproto/xrpc": "^0.7.5", 21 "@atproto/xrpc-server": "^0.9.5", 22 "@hono/node-server": "^1.14.3", ··· 396 "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 397 "license": "(Apache-2.0 AND MIT)" 398 }, 399 - "node_modules/@atproto/lex": { 400 - "version": "0.0.9", 401 - "resolved": "https://registry.npmjs.org/@atproto/lex/-/lex-0.0.9.tgz", 402 - "integrity": "sha512-o6gauf1lz0iyzJR0rqSj4VHOrO+Nt8+/iPb0KPojw1ieXk13zOSTSxotAoDzO/dP6y8Ey5jxwuCQGuzab/4XnQ==", 403 - "license": "MIT", 404 - "dependencies": { 405 - "@atproto/lex-builder": "0.0.9", 406 - "@atproto/lex-client": "0.0.7", 407 - "@atproto/lex-data": "0.0.6", 408 - "@atproto/lex-installer": "0.0.9", 409 - "@atproto/lex-json": "0.0.6", 410 - "@atproto/lex-schema": "0.0.7", 411 - "tslib": "^2.8.1", 412 - "yargs": "^17.0.0" 413 - }, 414 - "bin": { 415 - "lex": "bin/lex", 416 - "ts-lex": "bin/lex" 417 - } 418 - }, 419 - "node_modules/@atproto/lex-builder": { 420 - "version": "0.0.9", 421 - "resolved": "https://registry.npmjs.org/@atproto/lex-builder/-/lex-builder-0.0.9.tgz", 422 - "integrity": "sha512-buOFk1JpuW3twI7To7f/67zQQ1NulLHf/oasH/kTOPUAd0dNyeAa13t9eRSVGbwi0BcZYxRxBm0QzPmdLKyuyw==", 423 - "license": "MIT", 424 - "dependencies": { 425 - "@atproto/lex-document": "0.0.8", 426 - "@atproto/lex-schema": "0.0.7", 427 - "prettier": "^3.2.5", 428 - "ts-morph": "^27.0.0", 429 - "tslib": "^2.8.1" 430 - } 431 - }, 432 - "node_modules/@atproto/lex-builder/node_modules/@ts-morph/common": { 433 - "version": "0.28.1", 434 - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.28.1.tgz", 435 - "integrity": "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==", 436 - "license": "MIT", 437 - "dependencies": { 438 - "minimatch": "^10.0.1", 439 - "path-browserify": "^1.0.1", 440 - "tinyglobby": "^0.2.14" 441 - } 442 - }, 443 - "node_modules/@atproto/lex-builder/node_modules/minimatch": { 444 - "version": "10.1.1", 445 - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", 446 - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", 447 - "license": "BlueOak-1.0.0", 448 - "dependencies": { 449 - "@isaacs/brace-expansion": "^5.0.0" 450 - }, 451 - "engines": { 452 - "node": "20 || >=22" 453 - }, 454 - "funding": { 455 - "url": "https://github.com/sponsors/isaacs" 456 - } 457 - }, 458 - "node_modules/@atproto/lex-builder/node_modules/ts-morph": { 459 - "version": "27.0.2", 460 - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-27.0.2.tgz", 461 - "integrity": "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==", 462 - "license": "MIT", 463 - "dependencies": { 464 - "@ts-morph/common": "~0.28.1", 465 - "code-block-writer": "^13.0.3" 466 - } 467 - }, 468 - "node_modules/@atproto/lex-cbor": { 469 - "version": "0.0.6", 470 - "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.6.tgz", 471 - "integrity": "sha512-lee2T00owDy3I1plRHuURT6f98NIpYZZr2wXa5pJZz5JzefZ+nv8gJ2V70C2f+jmSG+5S9NTIy4uJw94vaHf4A==", 472 - "license": "MIT", 473 - "dependencies": { 474 - "@atproto/lex-data": "0.0.6", 475 - "multiformats": "^9.9.0", 476 - "tslib": "^2.8.1" 477 - } 478 - }, 479 - "node_modules/@atproto/lex-cbor/node_modules/multiformats": { 480 - "version": "9.9.0", 481 - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 482 - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 483 - "license": "(Apache-2.0 AND MIT)" 484 - }, 485 "node_modules/@atproto/lex-cli": { 486 "version": "0.9.5", 487 "resolved": "https://registry.npmjs.org/@atproto/lex-cli/-/lex-cli-0.9.5.tgz", ··· 515 "tslib": "^2.8.1" 516 } 517 }, 518 - "node_modules/@atproto/lex-client": { 519 - "version": "0.0.7", 520 - "resolved": "https://registry.npmjs.org/@atproto/lex-client/-/lex-client-0.0.7.tgz", 521 - "integrity": "sha512-ofUz3yXJ0nN/M9aqqF2ZUL/4D1wWT1P4popCfV3OEDsDrtWofMflYPFz1IWuyPa2e83paaEHRhaw3bZEhgXH1w==", 522 - "license": "MIT", 523 - "dependencies": { 524 - "@atproto/lex-data": "0.0.6", 525 - "@atproto/lex-json": "0.0.6", 526 - "@atproto/lex-schema": "0.0.7", 527 - "tslib": "^2.8.1" 528 - } 529 - }, 530 - "node_modules/@atproto/lex-data": { 531 - "version": "0.0.6", 532 - "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.6.tgz", 533 - "integrity": "sha512-MBNB4ghRJQzuXK1zlUPljpPbQcF1LZ5dzxy274KqPt4p3uPuRw0mHjgcCoWzRUNBQC685WMQR4IN9DHtsnG57A==", 534 - "license": "MIT", 535 - "dependencies": { 536 - "@atproto/syntax": "0.4.2", 537 - "multiformats": "^9.9.0", 538 - "tslib": "^2.8.1", 539 - "uint8arrays": "3.0.0", 540 - "unicode-segmenter": "^0.14.0" 541 - } 542 - }, 543 - "node_modules/@atproto/lex-data/node_modules/@atproto/syntax": { 544 - "version": "0.4.2", 545 - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", 546 - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", 547 - "license": "MIT" 548 - }, 549 - "node_modules/@atproto/lex-data/node_modules/multiformats": { 550 - "version": "9.9.0", 551 - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 552 - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 553 - "license": "(Apache-2.0 AND MIT)" 554 - }, 555 - "node_modules/@atproto/lex-document": { 556 - "version": "0.0.8", 557 - "resolved": "https://registry.npmjs.org/@atproto/lex-document/-/lex-document-0.0.8.tgz", 558 - "integrity": "sha512-p3l5h96Hx0vxUwbO/eas6x5h2vU0JVN1a/ktX4k3PlK9YLXfWMFsv+RdVwVZom8o0irHwlcyh1D/cY0PyUojDA==", 559 - "license": "MIT", 560 - "dependencies": { 561 - "@atproto/lex-schema": "0.0.7", 562 - "core-js": "^3", 563 - "tslib": "^2.8.1" 564 - } 565 - }, 566 - "node_modules/@atproto/lex-installer": { 567 - "version": "0.0.9", 568 - "resolved": "https://registry.npmjs.org/@atproto/lex-installer/-/lex-installer-0.0.9.tgz", 569 - "integrity": "sha512-zEeIeSaSCb3j+zNsqqMY7+X5FO6fxy/MafaCEj42KsXQHNcobuygZsnG/0fxMj/kMvhjrNUCp/w9PyOMwx4hQg==", 570 - "license": "MIT", 571 - "dependencies": { 572 - "@atproto/lex-builder": "0.0.9", 573 - "@atproto/lex-cbor": "0.0.6", 574 - "@atproto/lex-data": "0.0.6", 575 - "@atproto/lex-document": "0.0.8", 576 - "@atproto/lex-resolver": "0.0.8", 577 - "@atproto/lex-schema": "0.0.7", 578 - "@atproto/syntax": "0.4.2", 579 - "tslib": "^2.8.1" 580 - } 581 - }, 582 - "node_modules/@atproto/lex-installer/node_modules/@atproto/syntax": { 583 - "version": "0.4.2", 584 - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", 585 - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", 586 - "license": "MIT" 587 - }, 588 - "node_modules/@atproto/lex-json": { 589 - "version": "0.0.6", 590 - "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.6.tgz", 591 - "integrity": "sha512-EILnN5cditPvf+PCNjXt7reMuzjugxAL1fpSzmzJbEMGMUwxOf5pPWxRsaA/M3Boip4NQZ+6DVrPOGUMlnqceg==", 592 - "license": "MIT", 593 - "dependencies": { 594 - "@atproto/lex-data": "0.0.6", 595 - "tslib": "^2.8.1" 596 - } 597 - }, 598 - "node_modules/@atproto/lex-resolver": { 599 - "version": "0.0.8", 600 - "resolved": "https://registry.npmjs.org/@atproto/lex-resolver/-/lex-resolver-0.0.8.tgz", 601 - "integrity": "sha512-4hXT560+k5BIttouuhXOr+UkhAuFvvkJaVdqYb8vx2Ez7eHPiZ+yWkUK6FKpyGsx2whHkJzgleEA6DNWtdDlWA==", 602 - "license": "MIT", 603 - "dependencies": { 604 - "@atproto-labs/did-resolver": "0.2.5", 605 - "@atproto/crypto": "0.4.5", 606 - "@atproto/lex-client": "0.0.7", 607 - "@atproto/lex-data": "0.0.6", 608 - "@atproto/lex-document": "0.0.8", 609 - "@atproto/lex-schema": "0.0.7", 610 - "@atproto/repo": "0.8.12", 611 - "@atproto/syntax": "0.4.2", 612 - "tslib": "^2.8.1" 613 - } 614 - }, 615 - "node_modules/@atproto/lex-resolver/node_modules/@atproto-labs/did-resolver": { 616 - "version": "0.2.5", 617 - "resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.2.5.tgz", 618 - "integrity": "sha512-he7EC6OMSifNs01a4RT9mta/yYitoKDzlK9ty2TFV5Uj/+HpB4vYMRdIDFrRW0Hcsehy90E2t/dw0t7361MEKQ==", 619 - "license": "MIT", 620 - "dependencies": { 621 - "@atproto-labs/fetch": "0.2.3", 622 - "@atproto-labs/pipe": "0.1.1", 623 - "@atproto-labs/simple-store": "0.3.0", 624 - "@atproto-labs/simple-store-memory": "0.1.4", 625 - "@atproto/did": "0.2.4", 626 - "zod": "^3.23.8" 627 - } 628 - }, 629 - "node_modules/@atproto/lex-resolver/node_modules/@atproto/did": { 630 - "version": "0.2.4", 631 - "resolved": "https://registry.npmjs.org/@atproto/did/-/did-0.2.4.tgz", 632 - "integrity": "sha512-nxNiCgXeo7pfjojq9fpfZxCO0X0xUipNVKW+AHNZwQKiUDt6zYL0VXEfm8HBUwQOCmKvj2pRRSM1Cur+tUWk3g==", 633 - "license": "MIT", 634 - "dependencies": { 635 - "zod": "^3.23.8" 636 - } 637 - }, 638 - "node_modules/@atproto/lex-resolver/node_modules/@atproto/syntax": { 639 - "version": "0.4.2", 640 - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", 641 - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", 642 - "license": "MIT" 643 - }, 644 - "node_modules/@atproto/lex-schema": { 645 - "version": "0.0.7", 646 - "resolved": "https://registry.npmjs.org/@atproto/lex-schema/-/lex-schema-0.0.7.tgz", 647 - "integrity": "sha512-/7HkTUsnP1rlzmVE6nnY0kl/hydL/W8V29V8BhFwdAvdDKpYcdRgzzsMe38LAt+ZOjHknRCZDIKGsbQMSbJErw==", 648 - "license": "MIT", 649 - "dependencies": { 650 - "@atproto/lex-data": "0.0.6", 651 - "@atproto/syntax": "0.4.2", 652 - "tslib": "^2.8.1" 653 - } 654 - }, 655 - "node_modules/@atproto/lex-schema/node_modules/@atproto/syntax": { 656 - "version": "0.4.2", 657 - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", 658 - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", 659 - "license": "MIT" 660 - }, 661 "node_modules/@atproto/lexicon": { 662 "version": "0.6.1", 663 "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.1.tgz", ··· 848 "integrity": "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==", 849 "license": "MIT" 850 }, 851 - "node_modules/@atproto/tap": { 852 - "version": "0.1.1", 853 - "resolved": "https://registry.npmjs.org/@atproto/tap/-/tap-0.1.1.tgz", 854 - "integrity": "sha512-gW4NzLOxj74TzaDOVzzzt5kl2PdC0r75XkIpYpI5xobwCfsc/DmVtwpuSw1fW9gr4Vzk2Q90S9UE4ifAFl2gyA==", 855 - "license": "MIT", 856 - "dependencies": { 857 - "@atproto/common": "^0.5.6", 858 - "@atproto/lex": "^0.0.9", 859 - "@atproto/syntax": "^0.4.2", 860 - "@atproto/ws-client": "^0.0.4", 861 - "ws": "^8.12.0", 862 - "zod": "^3.23.8" 863 - }, 864 - "engines": { 865 - "node": ">=18.7.0" 866 - } 867 - }, 868 - "node_modules/@atproto/tap/node_modules/@atproto/common": { 869 - "version": "0.5.10", 870 - "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.10.tgz", 871 - "integrity": "sha512-A1+4W3JmjZIgmtJFLJBAaoVruZhRL0ANtyjZ91aJR4rjHcZuaQ+v4IFR1UcE6yyTATacLdBk6ADy8OtxXzq14g==", 872 - "license": "MIT", 873 - "dependencies": { 874 - "@atproto/common-web": "^0.4.15", 875 - "@atproto/lex-cbor": "^0.0.10", 876 - "@atproto/lex-data": "^0.0.10", 877 - "iso-datestring-validator": "^2.2.2", 878 - "multiformats": "^9.9.0", 879 - "pino": "^8.21.0" 880 - }, 881 - "engines": { 882 - "node": ">=18.7.0" 883 - } 884 - }, 885 - "node_modules/@atproto/tap/node_modules/@atproto/lex-cbor": { 886 - "version": "0.0.10", 887 - "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.10.tgz", 888 - "integrity": "sha512-5RtV90iIhRNCXXvvETd3KlraV8XGAAAgOmiszUb+l8GySDU/sGk7AlVvArFfXnj/S/GXJq8DP6IaUxCw/sPASA==", 889 - "license": "MIT", 890 - "dependencies": { 891 - "@atproto/lex-data": "^0.0.10", 892 - "tslib": "^2.8.1" 893 - } 894 - }, 895 - "node_modules/@atproto/tap/node_modules/@atproto/lex-data": { 896 - "version": "0.0.10", 897 - "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.10.tgz", 898 - "integrity": "sha512-FDbcy8VIUVzS9Mi1F8SMxbkL/jOUmRRpqbeM1xB4A0fMxeZJTxf6naAbFt4gYF3quu/+TPJGmio6/7cav05FqQ==", 899 - "license": "MIT", 900 - "dependencies": { 901 - "multiformats": "^9.9.0", 902 - "tslib": "^2.8.1", 903 - "uint8arrays": "3.0.0", 904 - "unicode-segmenter": "^0.14.0" 905 - } 906 - }, 907 - "node_modules/@atproto/tap/node_modules/@atproto/syntax": { 908 - "version": "0.4.3", 909 - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.3.tgz", 910 - "integrity": "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA==", 911 - "license": "MIT", 912 - "dependencies": { 913 - "tslib": "^2.8.1" 914 - } 915 - }, 916 - "node_modules/@atproto/tap/node_modules/multiformats": { 917 - "version": "9.9.0", 918 - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 919 - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 920 - "license": "(Apache-2.0 AND MIT)" 921 - }, 922 - "node_modules/@atproto/ws-client": { 923 - "version": "0.0.4", 924 - "resolved": "https://registry.npmjs.org/@atproto/ws-client/-/ws-client-0.0.4.tgz", 925 - "integrity": "sha512-dox1XIymuC7/ZRhUqKezIGgooZS45C6vHCfu0PnWjfvsLCK2kAlnvX4IBkA/WpcoijDhQ9ejChnFbo/sLmgvAg==", 926 - "license": "MIT", 927 - "dependencies": { 928 - "@atproto/common": "^0.5.3", 929 - "ws": "^8.12.0" 930 - }, 931 - "engines": { 932 - "node": ">=18.7.0" 933 - } 934 - }, 935 - "node_modules/@atproto/ws-client/node_modules/@atproto/common": { 936 - "version": "0.5.10", 937 - "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.5.10.tgz", 938 - "integrity": "sha512-A1+4W3JmjZIgmtJFLJBAaoVruZhRL0ANtyjZ91aJR4rjHcZuaQ+v4IFR1UcE6yyTATacLdBk6ADy8OtxXzq14g==", 939 - "license": "MIT", 940 - "dependencies": { 941 - "@atproto/common-web": "^0.4.15", 942 - "@atproto/lex-cbor": "^0.0.10", 943 - "@atproto/lex-data": "^0.0.10", 944 - "iso-datestring-validator": "^2.2.2", 945 - "multiformats": "^9.9.0", 946 - "pino": "^8.21.0" 947 - }, 948 - "engines": { 949 - "node": ">=18.7.0" 950 - } 951 - }, 952 - "node_modules/@atproto/ws-client/node_modules/@atproto/lex-cbor": { 953 - "version": "0.0.10", 954 - "resolved": "https://registry.npmjs.org/@atproto/lex-cbor/-/lex-cbor-0.0.10.tgz", 955 - "integrity": "sha512-5RtV90iIhRNCXXvvETd3KlraV8XGAAAgOmiszUb+l8GySDU/sGk7AlVvArFfXnj/S/GXJq8DP6IaUxCw/sPASA==", 956 - "license": "MIT", 957 - "dependencies": { 958 - "@atproto/lex-data": "^0.0.10", 959 - "tslib": "^2.8.1" 960 - } 961 - }, 962 - "node_modules/@atproto/ws-client/node_modules/@atproto/lex-data": { 963 - "version": "0.0.10", 964 - "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.10.tgz", 965 - "integrity": "sha512-FDbcy8VIUVzS9Mi1F8SMxbkL/jOUmRRpqbeM1xB4A0fMxeZJTxf6naAbFt4gYF3quu/+TPJGmio6/7cav05FqQ==", 966 - "license": "MIT", 967 - "dependencies": { 968 - "multiformats": "^9.9.0", 969 - "tslib": "^2.8.1", 970 - "uint8arrays": "3.0.0", 971 - "unicode-segmenter": "^0.14.0" 972 - } 973 - }, 974 - "node_modules/@atproto/ws-client/node_modules/multiformats": { 975 - "version": "9.9.0", 976 - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 977 - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 978 - "license": "(Apache-2.0 AND MIT)" 979 - }, 980 "node_modules/@atproto/xrpc": { 981 "version": "0.7.5", 982 "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.5.tgz", ··· 3052 "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 3053 "license": "(Apache-2.0 AND MIT)" 3054 }, 3055 - "node_modules/@isaacs/balanced-match": { 3056 - "version": "4.0.1", 3057 - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", 3058 - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", 3059 - "license": "MIT", 3060 - "engines": { 3061 - "node": "20 || >=22" 3062 - } 3063 - }, 3064 - "node_modules/@isaacs/brace-expansion": { 3065 - "version": "5.0.0", 3066 - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", 3067 - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", 3068 - "license": "MIT", 3069 - "dependencies": { 3070 - "@isaacs/balanced-match": "^4.0.1" 3071 - }, 3072 - "engines": { 3073 - "node": "20 || >=22" 3074 - } 3075 - }, 3076 "node_modules/@isaacs/fs-minipass": { 3077 "version": "4.0.1", 3078 "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", ··· 9631 "version": "13.0.3", 9632 "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", 9633 "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", 9634 "license": "MIT" 9635 }, 9636 "node_modules/collapse-white-space": { ··· 9739 "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", 9740 "license": "MIT" 9741 }, 9742 - "node_modules/core-js": { 9743 - "version": "3.47.0", 9744 - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", 9745 - "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", 9746 - "hasInstallScript": true, 9747 - "license": "MIT", 9748 - "funding": { 9749 - "type": "opencollective", 9750 - "url": "https://opencollective.com/core-js" 9751 - } 9752 - }, 9753 "node_modules/crelt": { 9754 "version": "1.0.6", 9755 "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", ··· 16142 "version": "1.0.1", 16143 "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", 16144 "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", 16145 "license": "MIT" 16146 }, 16147 "node_modules/path-exists": { ··· 16424 "version": "3.2.5", 16425 "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", 16426 "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", 16427 "bin": { 16428 "prettier": "bin/prettier.cjs" 16429 }, ··· 18520 "version": "0.2.15", 18521 "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", 18522 "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", 18523 "license": "MIT", 18524 "dependencies": { 18525 "fdir": "^6.5.0", ··· 18536 "version": "6.5.0", 18537 "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", 18538 "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", 18539 "license": "MIT", 18540 "engines": { 18541 "node": ">=12.0.0" ··· 18553 "version": "4.0.3", 18554 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 18555 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 18556 "license": "MIT", 18557 "engines": { 18558 "node": ">=12"
··· 16 "@atproto/oauth-client-node": "^0.3.8", 17 "@atproto/sync": "^0.1.34", 18 "@atproto/syntax": "^0.3.3", 19 "@atproto/xrpc": "^0.7.5", 20 "@atproto/xrpc-server": "^0.9.5", 21 "@hono/node-server": "^1.14.3", ··· 395 "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 396 "license": "(Apache-2.0 AND MIT)" 397 }, 398 "node_modules/@atproto/lex-cli": { 399 "version": "0.9.5", 400 "resolved": "https://registry.npmjs.org/@atproto/lex-cli/-/lex-cli-0.9.5.tgz", ··· 428 "tslib": "^2.8.1" 429 } 430 }, 431 "node_modules/@atproto/lexicon": { 432 "version": "0.6.1", 433 "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.1.tgz", ··· 618 "integrity": "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==", 619 "license": "MIT" 620 }, 621 "node_modules/@atproto/xrpc": { 622 "version": "0.7.5", 623 "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.5.tgz", ··· 2693 "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 2694 "license": "(Apache-2.0 AND MIT)" 2695 }, 2696 "node_modules/@isaacs/fs-minipass": { 2697 "version": "4.0.1", 2698 "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", ··· 9251 "version": "13.0.3", 9252 "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", 9253 "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", 9254 + "dev": true, 9255 "license": "MIT" 9256 }, 9257 "node_modules/collapse-white-space": { ··· 9360 "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", 9361 "license": "MIT" 9362 }, 9363 "node_modules/crelt": { 9364 "version": "1.0.6", 9365 "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", ··· 15752 "version": "1.0.1", 15753 "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", 15754 "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", 15755 + "dev": true, 15756 "license": "MIT" 15757 }, 15758 "node_modules/path-exists": { ··· 16035 "version": "3.2.5", 16036 "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", 16037 "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", 16038 + "dev": true, 16039 "bin": { 16040 "prettier": "bin/prettier.cjs" 16041 }, ··· 18132 "version": "0.2.15", 18133 "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", 18134 "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", 18135 + "dev": true, 18136 "license": "MIT", 18137 "dependencies": { 18138 "fdir": "^6.5.0", ··· 18149 "version": "6.5.0", 18150 "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", 18151 "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", 18152 + "dev": true, 18153 "license": "MIT", 18154 "engines": { 18155 "node": ">=12.0.0" ··· 18167 "version": "4.0.3", 18168 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 18169 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 18170 + "dev": true, 18171 "license": "MIT", 18172 "engines": { 18173 "node": ">=12"
-1
package.json
··· 27 "@atproto/oauth-client-node": "^0.3.8", 28 "@atproto/sync": "^0.1.34", 29 "@atproto/syntax": "^0.3.3", 30 - "@atproto/tap": "^0.1.1", 31 "@atproto/xrpc": "^0.7.5", 32 "@atproto/xrpc-server": "^0.9.5", 33 "@hono/node-server": "^1.14.3",
··· 27 "@atproto/oauth-client-node": "^0.3.8", 28 "@atproto/sync": "^0.1.34", 29 "@atproto/syntax": "^0.3.3", 30 "@atproto/xrpc": "^0.7.5", 31 "@atproto/xrpc-server": "^0.9.5", 32 "@hono/node-server": "^1.14.3",
+59 -4
src/notifications.ts
··· 27 | { type: "mention"; document_uri: string; mention_type: "document"; mentioned_uri: string } 28 | { type: "comment_mention"; comment_uri: string; mention_type: "did" } 29 | { type: "comment_mention"; comment_uri: string; mention_type: "publication"; mentioned_uri: string } 30 - | { type: "comment_mention"; comment_uri: string; mention_type: "document"; mentioned_uri: string }; 31 32 export type HydratedNotification = 33 | HydratedCommentNotification ··· 35 | HydratedQuoteNotification 36 | HydratedBskyPostEmbedNotification 37 | HydratedMentionNotification 38 - | HydratedCommentMentionNotification; 39 export async function hydrateNotifications( 40 notifications: NotificationRow[], 41 ): Promise<Array<HydratedNotification>> { 42 // Call all hydrators in parallel 43 - const [commentNotifications, subscribeNotifications, quoteNotifications, bskyPostEmbedNotifications, mentionNotifications, commentMentionNotifications] = await Promise.all([ 44 hydrateCommentNotifications(notifications), 45 hydrateSubscribeNotifications(notifications), 46 hydrateQuoteNotifications(notifications), 47 hydrateBskyPostEmbedNotifications(notifications), 48 hydrateMentionNotifications(notifications), 49 hydrateCommentMentionNotifications(notifications), 50 ]); 51 52 // Combine all hydrated notifications 53 - const allHydrated = [...commentNotifications, ...subscribeNotifications, ...quoteNotifications, ...bskyPostEmbedNotifications, ...mentionNotifications, ...commentMentionNotifications]; 54 55 // Sort by created_at to maintain order 56 allHydrated.sort( ··· 514 ), 515 normalizedMentionedPublication: normalizePublicationRecord(mentionedPublication?.record), 516 normalizedMentionedDocument: normalizeDocumentRecord(mentionedDoc?.data, mentionedDoc?.uri), 517 }; 518 }) 519 .filter((n) => n !== null);
··· 27 | { type: "mention"; document_uri: string; mention_type: "document"; mentioned_uri: string } 28 | { type: "comment_mention"; comment_uri: string; mention_type: "did" } 29 | { type: "comment_mention"; comment_uri: string; mention_type: "publication"; mentioned_uri: string } 30 + | { type: "comment_mention"; comment_uri: string; mention_type: "document"; mentioned_uri: string } 31 + | { type: "recommend"; document_uri: string; recommend_uri: string }; 32 33 export type HydratedNotification = 34 | HydratedCommentNotification ··· 36 | HydratedQuoteNotification 37 | HydratedBskyPostEmbedNotification 38 | HydratedMentionNotification 39 + | HydratedCommentMentionNotification 40 + | HydratedRecommendNotification; 41 export async function hydrateNotifications( 42 notifications: NotificationRow[], 43 ): Promise<Array<HydratedNotification>> { 44 // Call all hydrators in parallel 45 + const [commentNotifications, subscribeNotifications, quoteNotifications, bskyPostEmbedNotifications, mentionNotifications, commentMentionNotifications, recommendNotifications] = await Promise.all([ 46 hydrateCommentNotifications(notifications), 47 hydrateSubscribeNotifications(notifications), 48 hydrateQuoteNotifications(notifications), 49 hydrateBskyPostEmbedNotifications(notifications), 50 hydrateMentionNotifications(notifications), 51 hydrateCommentMentionNotifications(notifications), 52 + hydrateRecommendNotifications(notifications), 53 ]); 54 55 // Combine all hydrated notifications 56 + const allHydrated = [...commentNotifications, ...subscribeNotifications, ...quoteNotifications, ...bskyPostEmbedNotifications, ...mentionNotifications, ...commentMentionNotifications, ...recommendNotifications]; 57 58 // Sort by created_at to maintain order 59 allHydrated.sort( ··· 517 ), 518 normalizedMentionedPublication: normalizePublicationRecord(mentionedPublication?.record), 519 normalizedMentionedDocument: normalizeDocumentRecord(mentionedDoc?.data, mentionedDoc?.uri), 520 + }; 521 + }) 522 + .filter((n) => n !== null); 523 + } 524 + 525 + export type HydratedRecommendNotification = Awaited< 526 + ReturnType<typeof hydrateRecommendNotifications> 527 + >[0]; 528 + 529 + async function hydrateRecommendNotifications(notifications: NotificationRow[]) { 530 + const recommendNotifications = notifications.filter( 531 + (n): n is NotificationRow & { data: ExtractNotificationType<"recommend"> } => 532 + (n.data as NotificationData)?.type === "recommend", 533 + ); 534 + 535 + if (recommendNotifications.length === 0) { 536 + return []; 537 + } 538 + 539 + // Fetch recommend data from the database 540 + const recommendUris = recommendNotifications.map((n) => n.data.recommend_uri); 541 + const documentUris = recommendNotifications.map((n) => n.data.document_uri); 542 + 543 + const [{ data: recommends }, { data: documents }] = await Promise.all([ 544 + supabaseServerClient 545 + .from("recommends_on_documents") 546 + .select("*, identities(bsky_profiles(*))") 547 + .in("uri", recommendUris), 548 + supabaseServerClient 549 + .from("documents") 550 + .select("*, documents_in_publications(publications(*))") 551 + .in("uri", documentUris), 552 + ]); 553 + 554 + return recommendNotifications 555 + .map((notification) => { 556 + const recommendData = recommends?.find((r) => r.uri === notification.data.recommend_uri); 557 + const document = documents?.find((d) => d.uri === notification.data.document_uri); 558 + if (!recommendData || !document) return null; 559 + return { 560 + id: notification.id, 561 + recipient: notification.recipient, 562 + created_at: notification.created_at, 563 + type: "recommend" as const, 564 + recommend_uri: notification.data.recommend_uri, 565 + document_uri: notification.data.document_uri, 566 + recommendData, 567 + document, 568 + normalizedDocument: normalizeDocumentRecord(document.data, document.uri), 569 + normalizedPublication: normalizePublicationRecord( 570 + document.documents_in_publications[0]?.publications?.record, 571 + ), 572 }; 573 }) 574 .filter((n) => n !== null);
+13
src/replicache/mutations.ts
··· 659 tags?: string[]; 660 cover_image?: string | null; 661 localPublishedAt?: string | null; 662 }> = async (args, ctx) => { 663 await ctx.runOnServer(async (serverCtx) => { 664 console.log("updating"); ··· 667 title?: string; 668 tags?: string[]; 669 cover_image?: string | null; 670 } = {}; 671 if (args.description !== undefined) updates.description = args.description; 672 if (args.title !== undefined) updates.title = args.title; 673 if (args.tags !== undefined) updates.tags = args.tags; 674 if (args.cover_image !== undefined) updates.cover_image = args.cover_image; 675 676 if (Object.keys(updates).length > 0) { 677 // First try to update leaflets_in_publications (for publications) ··· 700 await tx.set("publication_cover_image", args.cover_image); 701 if (args.localPublishedAt !== undefined) 702 await tx.set("publication_local_published_at", args.localPublishedAt); 703 }); 704 }; 705
··· 659 tags?: string[]; 660 cover_image?: string | null; 661 localPublishedAt?: string | null; 662 + preferences?: { 663 + showComments?: boolean; 664 + showMentions?: boolean; 665 + showRecommends?: boolean; 666 + } | null; 667 }> = async (args, ctx) => { 668 await ctx.runOnServer(async (serverCtx) => { 669 console.log("updating"); ··· 672 title?: string; 673 tags?: string[]; 674 cover_image?: string | null; 675 + preferences?: { 676 + showComments?: boolean; 677 + showMentions?: boolean; 678 + showRecommends?: boolean; 679 + } | null; 680 } = {}; 681 if (args.description !== undefined) updates.description = args.description; 682 if (args.title !== undefined) updates.title = args.title; 683 if (args.tags !== undefined) updates.tags = args.tags; 684 if (args.cover_image !== undefined) updates.cover_image = args.cover_image; 685 + if (args.preferences !== undefined) updates.preferences = args.preferences; 686 687 if (Object.keys(updates).length > 0) { 688 // First try to update leaflets_in_publications (for publications) ··· 711 await tx.set("publication_cover_image", args.cover_image); 712 if (args.localPublishedAt !== undefined) 713 await tx.set("publication_local_published_at", args.localPublishedAt); 714 + if (args.preferences !== undefined) 715 + await tx.set("post_preferences", args.preferences); 716 }); 717 }; 718
+24
src/utils/mergePreferences.ts
···
··· 1 + type PreferencesInput = { 2 + showComments?: boolean; 3 + showMentions?: boolean; 4 + showRecommends?: boolean; 5 + showPrevNext?: boolean; 6 + } | null; 7 + 8 + export function mergePreferences( 9 + documentPrefs?: PreferencesInput, 10 + publicationPrefs?: PreferencesInput, 11 + ): { 12 + showComments?: boolean; 13 + showMentions?: boolean; 14 + showRecommends?: boolean; 15 + showPrevNext?: boolean; 16 + } { 17 + return { 18 + showComments: documentPrefs?.showComments ?? publicationPrefs?.showComments, 19 + showMentions: documentPrefs?.showMentions ?? publicationPrefs?.showMentions, 20 + showRecommends: 21 + documentPrefs?.showRecommends ?? publicationPrefs?.showRecommends, 22 + showPrevNext: publicationPrefs?.showPrevNext, 23 + }; 24 + }
+9
supabase/database.types.ts
··· 335 } 336 documents: { 337 Row: { 338 data: Json 339 indexed_at: string 340 sort_date: string 341 uri: string 342 } 343 Insert: { 344 data: Json 345 indexed_at?: string 346 uri: string 347 } 348 Update: { 349 data?: Json 350 indexed_at?: string 351 uri?: string ··· 589 description: string 590 doc: string | null 591 leaflet: string 592 publication: string 593 tags: string[] | null 594 title: string ··· 599 description?: string 600 doc?: string | null 601 leaflet: string 602 publication: string 603 tags?: string[] | null 604 title?: string ··· 609 description?: string 610 doc?: string | null 611 leaflet?: string 612 publication?: string 613 tags?: string[] | null 614 title?: string ··· 645 description: string 646 document: string 647 leaflet: string 648 tags: string[] | null 649 title: string 650 } ··· 655 description?: string 656 document: string 657 leaflet: string 658 tags?: string[] | null 659 title?: string 660 } ··· 665 description?: string 666 document?: string 667 leaflet?: string 668 tags?: string[] | null 669 title?: string 670 }
··· 335 } 336 documents: { 337 Row: { 338 + bsky_like_count: number 339 data: Json 340 indexed_at: string 341 sort_date: string 342 uri: string 343 } 344 Insert: { 345 + bsky_like_count?: number 346 data: Json 347 indexed_at?: string 348 uri: string 349 } 350 Update: { 351 + bsky_like_count?: number 352 data?: Json 353 indexed_at?: string 354 uri?: string ··· 592 description: string 593 doc: string | null 594 leaflet: string 595 + preferences: Json | null 596 publication: string 597 tags: string[] | null 598 title: string ··· 603 description?: string 604 doc?: string | null 605 leaflet: string 606 + preferences?: Json | null 607 publication: string 608 tags?: string[] | null 609 title?: string ··· 614 description?: string 615 doc?: string | null 616 leaflet?: string 617 + preferences?: Json | null 618 publication?: string 619 tags?: string[] | null 620 title?: string ··· 651 description: string 652 document: string 653 leaflet: string 654 + preferences: Json | null 655 tags: string[] | null 656 title: string 657 } ··· 662 description?: string 663 document: string 664 leaflet: string 665 + preferences?: Json | null 666 tags?: string[] | null 667 title?: string 668 } ··· 673 description?: string 674 document?: string 675 leaflet?: string 676 + preferences?: Json | null 677 tags?: string[] | null 678 title?: string 679 }
+2
supabase/migrations/20260208000000_add_preferences_to_drafts.sql
···
··· 1 + ALTER TABLE leaflets_in_publications ADD COLUMN preferences jsonb; 2 + ALTER TABLE leaflets_to_documents ADD COLUMN preferences jsonb;
+1
supabase/migrations/20260209000000_add_bsky_like_count.sql
···
··· 1 + ALTER TABLE documents ADD COLUMN bsky_like_count integer NOT NULL DEFAULT 0;