a tool for shared writing and social publishing

Compare changes

Choose any two refs to compare.

+398 -504
+21
actions/publishToPublication.ts
··· 78 78 cover_image, 79 79 entitiesToDelete, 80 80 publishedAt, 81 + postPreferences, 81 82 }: { 82 83 root_entity: string; 83 84 publication_uri?: string; ··· 88 89 cover_image?: string | null; 89 90 entitiesToDelete?: string[]; 90 91 publishedAt?: string; 92 + postPreferences?: { 93 + showComments?: boolean; 94 + showMentions?: boolean; 95 + showRecommends?: boolean; 96 + } | null; 91 97 }): Promise<PublishResult> { 92 98 let identity = await getIdentityData(); 93 99 if (!identity || !identity.atp_did) { ··· 175 181 }; 176 182 } 177 183 184 + // Resolve preferences: explicit param > draft DB value 185 + const preferences = postPreferences ?? draft?.preferences; 186 + 178 187 // Extract theme for standalone documents (not for publications) 179 188 let theme: PubLeafletPublication.Theme | undefined; 180 189 if (!publication_uri) { ··· 245 254 ...(coverImageBlob && { coverImage: coverImageBlob }), 246 255 // Include theme for standalone documents (not for publication documents) 247 256 ...(!publication_uri && theme && { theme }), 257 + ...(preferences && { 258 + preferences: { 259 + $type: "pub.leaflet.publication#preferences" as const, 260 + ...preferences, 261 + }, 262 + }), 248 263 content: { 249 264 $type: "pub.leaflet.content" as const, 250 265 pages: pagesArray, ··· 257 272 author: credentialSession.did!, 258 273 ...(publication_uri && { publication: publication_uri }), 259 274 ...(theme && { theme }), 275 + ...(preferences && { 276 + preferences: { 277 + $type: "pub.leaflet.publication#preferences" as const, 278 + ...preferences, 279 + }, 280 + }), 260 281 title: title || "Untitled", 261 282 description: description || "", 262 283 ...(tags !== undefined && { tags }),
+2
app/[leaflet_id]/Footer.tsx
··· 14 14 import { useIdentityData } from "components/IdentityProvider"; 15 15 import { useEntity } from "src/replicache"; 16 16 import { block } from "sharp"; 17 + import { PostSettings } from "components/PostSettings"; 17 18 18 19 export function hasBlockToolbar(blockType: string | null | undefined) { 19 20 return ( ··· 64 65 65 66 <PublishButton entityID={props.entityID} /> 66 67 <ShareOptions /> 68 + <PostSettings /> 67 69 <ThemePopover entityID={props.entityID} /> 68 70 </ActionFooter> 69 71 ) : (
+2
app/[leaflet_id]/Sidebar.tsx
··· 8 8 import { ShareOptions } from "app/[leaflet_id]/actions/ShareOptions"; 9 9 import { ThemePopover } from "components/ThemeManager/ThemeSetter"; 10 10 import { PublishButton } from "./actions/PublishButton"; 11 + import { PostSettings } from "components/PostSettings"; 11 12 import { Watermark } from "components/Watermark"; 12 13 import { BackToPubButton } from "./actions/BackToPubButton"; 13 14 import { useIdentityData } from "components/IdentityProvider"; ··· 30 31 <Sidebar> 31 32 <PublishButton entityID={rootEntity} /> 32 33 <ShareOptions /> 34 + <PostSettings /> 33 35 <ThemePopover entityID={rootEntity} /> 34 36 <HelpButton /> 35 37 <hr className="text-border" />
+10
app/[leaflet_id]/actions/PublishButton.tsx
··· 96 96 tx.get<string | null>("publication_cover_image"), 97 97 ); 98 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 + 99 108 // Get local published at from Replicache (session-only state, not persisted to DB) 100 109 let publishedAt = useLocalPublishedAt((s) => 101 110 pub?.doc ? s[pub?.doc] : undefined, ··· 118 127 tags: currentTags, 119 128 cover_image: coverImage, 120 129 publishedAt: publishedAt?.toISOString(), 130 + postPreferences, 121 131 }); 122 132 setIsLoading(false); 123 133 mutate();
+10
app/[leaflet_id]/publish/PublishPost.tsx
··· 91 91 tx.get<string | null>("publication_cover_image"), 92 92 ); 93 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 + 94 103 // Use Replicache tags only when we have a draft 95 104 const currentTags = props.hasDraft 96 105 ? Array.isArray(replicacheTags) ··· 124 133 cover_image: replicacheCoverImage, 125 134 entitiesToDelete: props.entitiesToDelete, 126 135 publishedAt: localPublishedAt?.toISOString() || new Date().toISOString(), 136 + postPreferences, 127 137 }); 128 138 129 139 if (!result.success) {
+2 -1
app/api/oauth/[route]/oauth-metadata.ts
··· 7 7 ? "http://localhost:3000" 8 8 : "https://leaflet.pub"; 9 9 10 - const scope = "atproto transition:generic transition:email"; 10 + const scope = 11 + "atproto transition:generic transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts blob:*/*"; 11 12 const localconfig: OAuthClientMetadataInput = { 12 13 client_id: `http://localhost/?redirect_uri=${encodeURI(`http://127.0.0.1:3000/api/oauth/callback`)}&scope=${encodeURIComponent(scope)}`, 13 14 client_name: `Leaflet`,
+2 -1
app/api/oauth/[route]/route.ts
··· 42 42 const ac = new AbortController(); 43 43 44 44 const url = await client.authorize(handle || "https://bsky.social", { 45 - scope: "atproto transition:generic transition:email", 45 + scope: 46 + "atproto transition:email include:pub.leaflet.authFullPermissions include:site.standard.authFull include:app.bsky.authCreatePosts blob:*/*", 46 47 signal: ac.signal, 47 48 state: JSON.stringify(state), 48 49 });
+7
app/api/rpc/[command]/pull.ts
··· 9 9 import type { Attribute } from "src/replicache/attributes"; 10 10 import { makeRoute } from "../lib"; 11 11 import type { Env } from "./route"; 12 + import type { Json } from "supabase/database.types"; 12 13 13 14 // First define the sub-types for V0 and V1 requests 14 15 const pullRequestV0 = z.object({ ··· 75 76 title: string; 76 77 tags: string[]; 77 78 cover_image: string | null; 79 + preferences: Json | null; 78 80 }[]; 79 81 let pub_patch = publication_data?.[0] 80 82 ? [ ··· 97 99 op: "put", 98 100 key: "publication_cover_image", 99 101 value: publication_data[0].cover_image || null, 102 + }, 103 + { 104 + op: "put", 105 + key: "post_preferences", 106 + value: publication_data[0].preferences || null, 100 107 }, 101 108 ] 102 109 : [];
+2 -1
app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx
··· 21 21 } from "src/utils/normalizeRecords"; 22 22 import { DocumentProvider } from "contexts/DocumentContext"; 23 23 import { LeafletContentProvider } from "contexts/LeafletContentContext"; 24 + import { mergePreferences } from "src/utils/mergePreferences"; 24 25 25 26 export async function DocumentPageRenderer({ 26 27 did, ··· 133 134 <LeafletLayout> 134 135 <PostPages 135 136 document_uri={document.uri} 136 - preferences={pubRecord?.preferences || {}} 137 + preferences={mergePreferences(record?.preferences, pubRecord?.preferences)} 137 138 pubRecord={pubRecord} 138 139 profile={JSON.parse(JSON.stringify(profile.data))} 139 140 document={document}
+4 -4
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 295 295 showPageBackground={pubRecord?.theme?.showPageBackground} 296 296 document_uri={document.uri} 297 297 comments={ 298 - pubRecord?.preferences?.showComments === false 298 + preferences.showComments === false 299 299 ? [] 300 300 : document.comments_on_documents 301 301 } 302 302 quotesAndMentions={ 303 - pubRecord?.preferences?.showMentions === false 303 + preferences.showMentions === false 304 304 ? [] 305 305 : quotesAndMentions 306 306 } ··· 387 387 pageId={page.id} 388 388 document_uri={document.uri} 389 389 comments={ 390 - pubRecord?.preferences?.showComments === false 390 + preferences.showComments === false 391 391 ? [] 392 392 : document.comments_on_documents 393 393 } 394 394 quotesAndMentions={ 395 - pubRecord?.preferences?.showMentions === false 395 + preferences.showMentions === false 396 396 ? [] 397 397 : quotesAndMentions 398 398 }
+17 -4
components/Canvas.tsx
··· 24 24 import { useHandleCanvasDrop } from "./Blocks/useHandleCanvasDrop"; 25 25 import { useBlockMouseHandlers } from "./Blocks/useBlockMouseHandlers"; 26 26 import { RecommendTinyEmpty } from "./Icons/RecommendTiny"; 27 + import { useSubscribe } from "src/replicache/useSubscribe"; 28 + import { mergePreferences } from "src/utils/mergePreferences"; 27 29 28 30 export function Canvas(props: { 29 31 entityID: string; ··· 164 166 165 167 const CanvasMetadata = (props: { isSubpage: boolean | undefined }) => { 166 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 + ); 167 177 if (!pub || !pub.publications) return null; 168 178 169 179 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; 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; 174 187 175 188 return ( 176 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">
+7 -2
components/OAuthError.tsx
··· 13 13 14 14 return ( 15 15 <div className={className}> 16 - <span>Your session has expired or is invalid. </span> 16 + <span> 17 + {error.type === "missing_scope" 18 + ? "Your session is missing required permissions. " 19 + : "Your session has expired or is invalid. "} 20 + </span> 17 21 <a href={signInUrl} className="underline font-bold whitespace-nowrap"> 18 22 Sign in again 19 23 </a> ··· 28 32 typeof error === "object" && 29 33 error !== null && 30 34 "type" in error && 31 - (error as OAuthSessionError).type === "oauth_session_expired" 35 + ((error as OAuthSessionError).type === "oauth_session_expired" || 36 + (error as OAuthSessionError).type === "missing_scope") 32 37 ); 33 38 }
+18 -7
components/Pages/PublicationMetadata.tsx
··· 21 21 import { PostHeaderLayout } from "app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader"; 22 22 import { Backdater } from "./Backdater"; 23 23 import { RecommendTinyEmpty } from "components/Icons/RecommendTiny"; 24 + import { mergePreferences } from "src/utils/mergePreferences"; 24 25 25 26 export const PublicationMetadata = (props: { noInteractions?: boolean }) => { 26 27 let { rep } = useReplicache(); ··· 33 34 let title = useSubscribe(rep, (tx) => tx.get<string>("publication_title")); 34 35 let description = useSubscribe(rep, (tx) => 35 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, 36 48 ); 37 49 let publishedAt = normalizedDocument?.publishedAt; 38 50 ··· 121 133 )} 122 134 {!props.noInteractions && ( 123 135 <div className="flex gap-2 text-border items-center"> 124 - {normalizedPublication?.preferences?.showRecommends !== false && ( 136 + {merged.showRecommends !== false && ( 125 137 <div className="flex gap-1 items-center"> 126 138 <RecommendTinyEmpty />โ€” 127 139 </div> 128 140 )} 129 141 130 - {normalizedPublication?.preferences?.showMentions !== false && ( 142 + {merged.showMentions !== false && ( 131 143 <div className="flex gap-1 items-center"> 132 144 <QuoteTiny />โ€” 133 145 </div> 134 146 )} 135 - {normalizedPublication?.preferences?.showComments !== false && ( 147 + {merged.showComments !== false && ( 136 148 <div className="flex gap-1 items-center"> 137 149 <CommentTiny />โ€” 138 150 </div> 139 151 )} 140 152 {tags && ( 141 153 <> 142 - {normalizedPublication?.preferences?.showRecommends !== 143 - false || 144 - normalizedPublication?.preferences?.showMentions !== false || 145 - normalizedPublication?.preferences?.showComments !== false ? ( 154 + {merged.showRecommends !== false || 155 + merged.showMentions !== false || 156 + merged.showComments !== false ? ( 146 157 <Separator classname="h-4!" /> 147 158 ) : null} 148 159 <AddTags />
+7 -6
components/PostListing.tsx
··· 17 17 import Link from "next/link"; 18 18 import { InteractionPreview } from "./InteractionsPreview"; 19 19 import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 20 + import { mergePreferences } from "src/utils/mergePreferences"; 20 21 21 22 export const PostListing = (props: Post) => { 22 23 let pubRecord = props.publication?.pubRecord as ··· 48 49 ? pubRecord?.theme?.showPageBackground 49 50 : postRecord.theme?.showPageBackground ?? true; 50 51 52 + let mergedPrefs = mergePreferences(postRecord?.preferences, pubRecord?.preferences); 53 + 51 54 let quotes = props.documents.document_mentions_in_bsky?.[0]?.count || 0; 52 55 let comments = 53 - pubRecord?.preferences?.showComments === false 56 + mergedPrefs.showComments === false 54 57 ? 0 55 58 : props.documents.comments_on_documents?.[0]?.count || 0; 56 59 let recommends = props.documents.recommends_on_documents?.[0]?.count || 0; ··· 109 112 recommendsCount={recommends} 110 113 documentUri={props.documents.uri} 111 114 tags={tags} 112 - showComments={pubRecord?.preferences?.showComments !== false} 113 - showMentions={pubRecord?.preferences?.showMentions !== false} 114 - showRecommends={ 115 - pubRecord?.preferences?.showRecommends !== false 116 - } 115 + showComments={mergedPrefs.showComments !== false} 116 + showMentions={mergedPrefs.showMentions !== false} 117 + showRecommends={mergedPrefs.showRecommends !== false} 117 118 share 118 119 /> 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 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 + }
+17
lexicons/api/lexicons.ts
··· 1469 1469 type: 'ref', 1470 1470 ref: 'lex:pub.leaflet.publication#theme', 1471 1471 }, 1472 + preferences: { 1473 + type: 'ref', 1474 + ref: 'lex:pub.leaflet.publication#preferences', 1475 + }, 1472 1476 tags: { 1473 1477 type: 'array', 1474 1478 items: { ··· 1868 1872 type: 'boolean', 1869 1873 default: true, 1870 1874 }, 1875 + showRecommends: { 1876 + type: 'boolean', 1877 + default: true, 1878 + }, 1871 1879 }, 1872 1880 }, 1873 1881 theme: { ··· 2194 2202 maxLength: 5000, 2195 2203 type: 'string', 2196 2204 }, 2205 + preferences: { 2206 + type: 'union', 2207 + refs: ['lex:pub.leaflet.publication#preferences'], 2208 + closed: false, 2209 + }, 2197 2210 updatedAt: { 2198 2211 format: 'datetime', 2199 2212 type: 'string', ··· 2288 2301 }, 2289 2302 showPrevNext: { 2290 2303 default: false, 2304 + type: 'boolean', 2305 + }, 2306 + showRecommends: { 2307 + default: true, 2291 2308 type: 'boolean', 2292 2309 }, 2293 2310 },
+1
lexicons/api/types/pub/leaflet/document.ts
··· 23 23 publication?: string 24 24 author: string 25 25 theme?: PubLeafletPublication.Theme 26 + preferences?: PubLeafletPublication.Preferences 26 27 tags?: string[] 27 28 coverImage?: BlobRef 28 29 pages: (
+40 -44
lexicons/api/types/pub/leaflet/publication.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 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"; 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' 14 10 15 11 const is$typed = _is$typed, 16 - validate = _validate; 17 - const id = "pub.leaflet.publication"; 12 + validate = _validate 13 + const id = 'pub.leaflet.publication' 18 14 19 15 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; 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 28 24 } 29 25 30 - const hashRecord = "main"; 26 + const hashRecord = 'main' 31 27 32 28 export function isRecord<V>(v: V) { 33 - return is$typed(v, id, hashRecord); 29 + return is$typed(v, id, hashRecord) 34 30 } 35 31 36 32 export function validateRecord<V>(v: V) { 37 - return validate<Record & V>(v, id, hashRecord, true); 33 + return validate<Record & V>(v, id, hashRecord, true) 38 34 } 39 35 40 36 export interface Preferences { 41 - $type?: "pub.leaflet.publication#preferences"; 42 - showInDiscover: boolean; 43 - showComments: boolean; 44 - showMentions: boolean; 45 - showPrevNext: boolean; 46 - showRecommends: boolean; 37 + $type?: 'pub.leaflet.publication#preferences' 38 + showInDiscover: boolean 39 + showComments: boolean 40 + showMentions: boolean 41 + showPrevNext: boolean 42 + showRecommends: boolean 47 43 } 48 44 49 - const hashPreferences = "preferences"; 45 + const hashPreferences = 'preferences' 50 46 51 47 export function isPreferences<V>(v: V) { 52 - return is$typed(v, id, hashPreferences); 48 + return is$typed(v, id, hashPreferences) 53 49 } 54 50 55 51 export function validatePreferences<V>(v: V) { 56 - return validate<Preferences & V>(v, id, hashPreferences); 52 + return validate<Preferences & V>(v, id, hashPreferences) 57 53 } 58 54 59 55 export interface Theme { 60 - $type?: "pub.leaflet.publication#theme"; 56 + $type?: 'pub.leaflet.publication#theme' 61 57 backgroundColor?: 62 58 | $Typed<PubLeafletThemeColor.Rgba> 63 59 | $Typed<PubLeafletThemeColor.Rgb> 64 - | { $type: string }; 65 - backgroundImage?: PubLeafletThemeBackgroundImage.Main; 66 - pageWidth?: number; 60 + | { $type: string } 61 + backgroundImage?: PubLeafletThemeBackgroundImage.Main 62 + pageWidth?: number 67 63 primary?: 68 64 | $Typed<PubLeafletThemeColor.Rgba> 69 65 | $Typed<PubLeafletThemeColor.Rgb> 70 - | { $type: string }; 66 + | { $type: string } 71 67 pageBackground?: 72 68 | $Typed<PubLeafletThemeColor.Rgba> 73 69 | $Typed<PubLeafletThemeColor.Rgb> 74 - | { $type: string }; 75 - showPageBackground: boolean; 70 + | { $type: string } 71 + showPageBackground: boolean 76 72 accentBackground?: 77 73 | $Typed<PubLeafletThemeColor.Rgba> 78 74 | $Typed<PubLeafletThemeColor.Rgb> 79 - | { $type: string }; 75 + | { $type: string } 80 76 accentText?: 81 77 | $Typed<PubLeafletThemeColor.Rgba> 82 78 | $Typed<PubLeafletThemeColor.Rgb> 83 - | { $type: string }; 79 + | { $type: string } 84 80 } 85 81 86 - const hashTheme = "theme"; 82 + const hashTheme = 'theme' 87 83 88 84 export function isTheme<V>(v: V) { 89 - return is$typed(v, id, hashTheme); 85 + return is$typed(v, id, hashTheme) 90 86 } 91 87 92 88 export function validateTheme<V>(v: V) { 93 - return validate<Theme & V>(v, id, hashTheme); 89 + return validate<Theme & V>(v, id, hashTheme) 94 90 }
+1
lexicons/api/types/site/standard/document.ts
··· 28 28 textContent?: string 29 29 theme?: PubLeafletPublication.Theme 30 30 title: string 31 + preferences?: $Typed<PubLeafletPublication.Preferences> | { $type: string } 31 32 updatedAt?: string 32 33 [k: string]: unknown 33 34 }
+29 -33
lexicons/api/types/site/standard/publication.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 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"; 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' 14 10 15 11 const is$typed = _is$typed, 16 - validate = _validate; 17 - const id = "site.standard.publication"; 12 + validate = _validate 13 + const id = 'site.standard.publication' 18 14 19 15 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; 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 29 25 } 30 26 31 - const hashRecord = "main"; 27 + const hashRecord = 'main' 32 28 33 29 export function isRecord<V>(v: V) { 34 - return is$typed(v, id, hashRecord); 30 + return is$typed(v, id, hashRecord) 35 31 } 36 32 37 33 export function validateRecord<V>(v: V) { 38 - return validate<Record & V>(v, id, hashRecord, true); 34 + return validate<Record & V>(v, id, hashRecord, true) 39 35 } 40 36 41 37 export interface Preferences { 42 - $type?: "site.standard.publication#preferences"; 43 - showInDiscover: boolean; 44 - showComments: boolean; 45 - showMentions: boolean; 46 - showPrevNext: boolean; 47 - showRecommends: boolean; 38 + $type?: 'site.standard.publication#preferences' 39 + showInDiscover: boolean 40 + showComments: boolean 41 + showMentions: boolean 42 + showPrevNext: boolean 43 + showRecommends: boolean 48 44 } 49 45 50 - const hashPreferences = "preferences"; 46 + const hashPreferences = 'preferences' 51 47 52 48 export function isPreferences<V>(v: V) { 53 - return is$typed(v, id, hashPreferences); 49 + return is$typed(v, id, hashPreferences) 54 50 } 55 51 56 52 export function validatePreferences<V>(v: V) { 57 - return validate<Preferences & V>(v, id, hashPreferences); 53 + return validate<Preferences & V>(v, id, hashPreferences) 58 54 }
+4
lexicons/pub/leaflet/document.json
··· 46 46 "type": "ref", 47 47 "ref": "pub.leaflet.publication#theme" 48 48 }, 49 + "preferences": { 50 + "type": "ref", 51 + "ref": "pub.leaflet.publication#preferences" 52 + }, 49 53 "tags": { 50 54 "type": "array", 51 55 "items": {
+5
lexicons/site/standard/document.json
··· 57 57 "maxLength": 5000, 58 58 "type": "string" 59 59 }, 60 + "preferences": { 61 + "type": "union", 62 + "refs": ["pub.leaflet.publication#preferences"], 63 + "closed": false 64 + }, 60 65 "updatedAt": { 61 66 "format": "datetime", 62 67 "type": "string"
+4
lexicons/src/document.ts
··· 23 23 publication: { type: "string", format: "at-uri" }, 24 24 author: { type: "string", format: "at-identifier" }, 25 25 theme: { type: "ref", ref: "pub.leaflet.publication#theme" }, 26 + preferences: { 27 + type: "ref", 28 + ref: "pub.leaflet.publication#preferences", 29 + }, 26 30 tags: { type: "array", items: { type: "string", maxLength: 50 } }, 27 31 coverImage: { 28 32 type: "blob",
+12
lexicons/src/normalize.ts
··· 28 28 export type NormalizedDocument = SiteStandardDocument.Record & { 29 29 // Keep the original theme for components that need leaflet-specific styling 30 30 theme?: PubLeafletPublication.Theme; 31 + preferences?: SiteStandardPublication.Preferences; 31 32 }; 32 33 33 34 // Normalized publication type - uses the generated site.standard.publication type ··· 169 170 170 171 // Pass through site.standard records directly (theme is already in correct format if present) 171 172 if (isStandardDocument(record)) { 173 + const preferences = record.preferences as 174 + | SiteStandardPublication.Preferences 175 + | undefined; 172 176 return { 173 177 ...record, 174 178 theme: record.theme, 179 + preferences, 175 180 } as NormalizedDocument; 176 181 } 177 182 ··· 198 203 } 199 204 : undefined; 200 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 + 201 210 return { 202 211 $type: "site.standard.document", 203 212 title: record.title, ··· 210 219 bskyPostRef: record.postRef, 211 220 content, 212 221 theme: record.theme, 222 + preferences: leafletPrefs 223 + ? { ...leafletPrefs, $type: "site.standard.publication#preferences" as const } 224 + : undefined, 213 225 }; 214 226 } 215 227
+6 -391
package-lock.json
··· 16 16 "@atproto/oauth-client-node": "^0.3.8", 17 17 "@atproto/sync": "^0.1.34", 18 18 "@atproto/syntax": "^0.3.3", 19 - "@atproto/tap": "^0.1.1", 20 19 "@atproto/xrpc": "^0.7.5", 21 20 "@atproto/xrpc-server": "^0.9.5", 22 21 "@hono/node-server": "^1.14.3", ··· 396 395 "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 397 396 "license": "(Apache-2.0 AND MIT)" 398 397 }, 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 398 "node_modules/@atproto/lex-cli": { 486 399 "version": "0.9.5", 487 400 "resolved": "https://registry.npmjs.org/@atproto/lex-cli/-/lex-cli-0.9.5.tgz", ··· 515 428 "tslib": "^2.8.1" 516 429 } 517 430 }, 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 431 "node_modules/@atproto/lexicon": { 662 432 "version": "0.6.1", 663 433 "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.1.tgz", ··· 848 618 "integrity": "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==", 849 619 "license": "MIT" 850 620 }, 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 621 "node_modules/@atproto/xrpc": { 981 622 "version": "0.7.5", 982 623 "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.5.tgz", ··· 3052 2693 "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 3053 2694 "license": "(Apache-2.0 AND MIT)" 3054 2695 }, 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 2696 "node_modules/@isaacs/fs-minipass": { 3077 2697 "version": "4.0.1", 3078 2698 "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", ··· 9631 9251 "version": "13.0.3", 9632 9252 "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", 9633 9253 "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", 9254 + "dev": true, 9634 9255 "license": "MIT" 9635 9256 }, 9636 9257 "node_modules/collapse-white-space": { ··· 9739 9360 "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", 9740 9361 "license": "MIT" 9741 9362 }, 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 9363 "node_modules/crelt": { 9754 9364 "version": "1.0.6", 9755 9365 "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", ··· 16142 15752 "version": "1.0.1", 16143 15753 "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", 16144 15754 "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", 15755 + "dev": true, 16145 15756 "license": "MIT" 16146 15757 }, 16147 15758 "node_modules/path-exists": { ··· 16424 16035 "version": "3.2.5", 16425 16036 "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", 16426 16037 "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", 16038 + "dev": true, 16427 16039 "bin": { 16428 16040 "prettier": "bin/prettier.cjs" 16429 16041 }, ··· 18520 18132 "version": "0.2.15", 18521 18133 "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", 18522 18134 "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", 18135 + "dev": true, 18523 18136 "license": "MIT", 18524 18137 "dependencies": { 18525 18138 "fdir": "^6.5.0", ··· 18536 18149 "version": "6.5.0", 18537 18150 "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", 18538 18151 "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", 18152 + "dev": true, 18539 18153 "license": "MIT", 18540 18154 "engines": { 18541 18155 "node": ">=12.0.0" ··· 18553 18167 "version": "4.0.3", 18554 18168 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 18555 18169 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 18170 + "dev": true, 18556 18171 "license": "MIT", 18557 18172 "engines": { 18558 18173 "node": ">=12"
-1
package.json
··· 27 27 "@atproto/oauth-client-node": "^0.3.8", 28 28 "@atproto/sync": "^0.1.34", 29 29 "@atproto/syntax": "^0.3.3", 30 - "@atproto/tap": "^0.1.1", 31 30 "@atproto/xrpc": "^0.7.5", 32 31 "@atproto/xrpc-server": "^0.9.5", 33 32 "@hono/node-server": "^1.14.3",
+27 -9
src/atproto-oauth.ts
··· 93 93 }, 94 94 }; 95 95 96 - export type OAuthSessionError = { 97 - type: "oauth_session_expired"; 98 - message: string; 99 - did: string; 100 - }; 96 + export type OAuthSessionError = 97 + | { 98 + type: "oauth_session_expired"; 99 + message: string; 100 + did: string; 101 + } 102 + | { 103 + type: "missing_scope"; 104 + message: string; 105 + did: string; 106 + scope: string; 107 + }; 101 108 102 109 export async function restoreOAuthSession( 103 110 did: string ··· 107 114 const session = await oauthClient.restore(did); 108 115 return Ok(session); 109 116 } catch (error) { 117 + const message = 118 + error instanceof Error 119 + ? error.message 120 + : "OAuth session expired or invalid"; 121 + if (message.includes("Missing required scope")) { 122 + const scope = message.replace("Missing required scope: ", "").trim(); 123 + console.error(`OAuth missing scope for ${did}: ${scope}`); 124 + return Err({ 125 + type: "missing_scope", 126 + message, 127 + did, 128 + scope, 129 + }); 130 + } 110 131 return Err({ 111 132 type: "oauth_session_expired", 112 - message: 113 - error instanceof Error 114 - ? error.message 115 - : "OAuth session expired or invalid", 133 + message, 116 134 did, 117 135 }); 118 136 }
+13
src/replicache/mutations.ts
··· 659 659 tags?: string[]; 660 660 cover_image?: string | null; 661 661 localPublishedAt?: string | null; 662 + preferences?: { 663 + showComments?: boolean; 664 + showMentions?: boolean; 665 + showRecommends?: boolean; 666 + } | null; 662 667 }> = async (args, ctx) => { 663 668 await ctx.runOnServer(async (serverCtx) => { 664 669 console.log("updating"); ··· 667 672 title?: string; 668 673 tags?: string[]; 669 674 cover_image?: string | null; 675 + preferences?: { 676 + showComments?: boolean; 677 + showMentions?: boolean; 678 + showRecommends?: boolean; 679 + } | null; 670 680 } = {}; 671 681 if (args.description !== undefined) updates.description = args.description; 672 682 if (args.title !== undefined) updates.title = args.title; 673 683 if (args.tags !== undefined) updates.tags = args.tags; 674 684 if (args.cover_image !== undefined) updates.cover_image = args.cover_image; 685 + if (args.preferences !== undefined) updates.preferences = args.preferences; 675 686 676 687 if (Object.keys(updates).length > 0) { 677 688 // First try to update leaflets_in_publications (for publications) ··· 700 711 await tx.set("publication_cover_image", args.cover_image); 701 712 if (args.localPublishedAt !== undefined) 702 713 await tx.set("publication_local_published_at", args.localPublishedAt); 714 + if (args.preferences !== undefined) 715 + await tx.set("post_preferences", args.preferences); 703 716 }); 704 717 }; 705 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 + }
+6
supabase/database.types.ts
··· 589 589 description: string 590 590 doc: string | null 591 591 leaflet: string 592 + preferences: Json | null 592 593 publication: string 593 594 tags: string[] | null 594 595 title: string ··· 599 600 description?: string 600 601 doc?: string | null 601 602 leaflet: string 603 + preferences?: Json | null 602 604 publication: string 603 605 tags?: string[] | null 604 606 title?: string ··· 609 611 description?: string 610 612 doc?: string | null 611 613 leaflet?: string 614 + preferences?: Json | null 612 615 publication?: string 613 616 tags?: string[] | null 614 617 title?: string ··· 645 648 description: string 646 649 document: string 647 650 leaflet: string 651 + preferences: Json | null 648 652 tags: string[] | null 649 653 title: string 650 654 } ··· 655 659 description?: string 656 660 document: string 657 661 leaflet: string 662 + preferences?: Json | null 658 663 tags?: string[] | null 659 664 title?: string 660 665 } ··· 665 670 description?: string 666 671 document?: string 667 672 leaflet?: string 673 + preferences?: Json | null 668 674 tags?: string[] | null 669 675 title?: string 670 676 }
+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;