a tool for shared writing and social publishing

add user preference to show/hide recommends

+363 -206
+2 -1
.claude/settings.local.json
··· 8 8 "mcp__primitive__pending_delegations", 9 9 "mcp__primitive__claim_delegation", 10 10 "mcp__primitive__tasks_update", 11 - "mcp__primitive__contexts_update" 11 + "mcp__primitive__contexts_update", 12 + "mcp__primitive__contexts_list" 12 13 ] 13 14 } 14 15 }
+2
app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx
··· 206 206 preferences: { 207 207 showComments?: boolean; 208 208 showMentions?: boolean; 209 + showRecommends?: boolean; 209 210 showPrevNext?: boolean; 210 211 }; 211 212 quotesCount: number | undefined; ··· 221 222 recommendsCount={props.recommendsCount} 222 223 showComments={props.preferences.showComments !== false} 223 224 showMentions={props.preferences.showMentions !== false} 225 + showRecommends={props.preferences.showRecommends !== false} 224 226 pageId={props.pageId} 225 227 /> 226 228 {!props.isSubpage && (
+19 -11
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
··· 112 112 className?: string; 113 113 showComments: boolean; 114 114 showMentions: boolean; 115 + showRecommends: boolean; 115 116 pageId?: string; 116 117 }) => { 117 118 const { ··· 132 133 const tags = normalizedDocument.tags; 133 134 const tagCount = tags?.length || 0; 134 135 135 - let interactionsAvailable = props.showComments || props.showMentions; 136 + let interactionsAvailable = 137 + props.showComments || props.showMentions || props.showRecommends; 136 138 137 139 return ( 138 140 <div ··· 169 171 <QuoteTiny aria-hidden /> {props.quotesCount} 170 172 </button> 171 173 )} 172 - <RecommendButton 173 - documentUri={document_uri} 174 - recommendsCount={props.recommendsCount} 175 - /> 174 + {props.showRecommends === false ? null : ( 175 + <RecommendButton 176 + documentUri={document_uri} 177 + recommendsCount={props.recommendsCount} 178 + /> 179 + )} 176 180 <Separator classname="h-4!" /> 177 181 {tagCount > 0 && <TagPopover tags={tags} tagCount={tagCount} />} 178 182 </div> ··· 186 190 className?: string; 187 191 showComments: boolean; 188 192 showMentions: boolean; 193 + showRecommends: boolean; 189 194 pageId?: string; 190 195 }) => { 191 196 const { ··· 208 213 const tags = normalizedDocument.tags; 209 214 const tagCount = tags?.length || 0; 210 215 211 - let noInteractions = !props.showComments && !props.showMentions; 216 + let noInteractions = 217 + !props.showComments && !props.showMentions && !props.showRecommends; 212 218 213 219 let subscribed = 214 220 identity?.atp_did && ··· 237 243 ) : ( 238 244 <> 239 245 <div className="flex gap-2 sm:flex-row flex-col"> 240 - <RecommendButton 241 - documentUri={document_uri} 242 - recommendsCount={props.recommendsCount} 243 - expanded 244 - /> 246 + {props.showRecommends === false ? null : ( 247 + <RecommendButton 248 + documentUri={document_uri} 249 + recommendsCount={props.recommendsCount} 250 + expanded 251 + /> 252 + )} 245 253 {props.quotesCount === 0 || !props.showMentions ? null : ( 246 254 <ButtonSecondary 247 255 onClick={() => {
+1
app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx
··· 87 87 pageId={pageId} 88 88 showComments={preferences.showComments !== false} 89 89 showMentions={preferences.showMentions !== false} 90 + showRecommends={preferences.showRecommends !== false} 90 91 commentsCount={ 91 92 getCommentCount(document.comments_on_documents, pageId) || 0 92 93 }
+6 -1
app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx
··· 18 18 export function PostHeader(props: { 19 19 data: PostPageData; 20 20 profile: ProfileViewDetailed; 21 - preferences: { showComments?: boolean; showMentions?: boolean }; 21 + preferences: { 22 + showComments?: boolean; 23 + showMentions?: boolean; 24 + showRecommends?: boolean; 25 + }; 22 26 }) { 23 27 let { identity } = useIdentityData(); 24 28 let document = props.data; ··· 87 91 <Interactions 88 92 showComments={props.preferences.showComments !== false} 89 93 showMentions={props.preferences.showMentions !== false} 94 + showRecommends={props.preferences.showRecommends !== false} 90 95 quotesCount={getQuoteCount(document?.quotesAndMentions || []) || 0} 91 96 commentsCount={ 92 97 getCommentCount(document?.comments_on_documents || []) || 0
+2
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 170 170 preferences: { 171 171 showComments?: boolean; 172 172 showMentions?: boolean; 173 + showRecommends?: boolean; 173 174 showPrevNext?: boolean; 174 175 }; 175 176 pubRecord?: NormalizedPublication | null; ··· 233 234 preferences: { 234 235 showComments?: boolean; 235 236 showMentions?: boolean; 237 + showRecommends?: boolean; 236 238 showPrevNext?: boolean; 237 239 }; 238 240 pollData: PollData[];
+1
app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx
··· 142 142 tags={doc.record.tags || []} 143 143 showComments={pubRecord?.preferences?.showComments !== false} 144 144 showMentions={pubRecord?.preferences?.showMentions !== false} 145 + showRecommends={pubRecord?.preferences?.showRecommends !== false} 145 146 postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} 146 147 /> 147 148 </div>
+21 -1
app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx
··· 29 29 ? true 30 30 : record.preferences.showMentions, 31 31 ); 32 + let [showRecommends, setShowRecommends] = useState( 33 + record?.preferences?.showRecommends === undefined 34 + ? true 35 + : record.preferences.showRecommends, 36 + ); 32 37 let [showPrevNext, setShowPrevNext] = useState( 33 38 record?.preferences?.showPrevNext === undefined 34 39 ? true ··· 53 58 showComments: showComments, 54 59 showMentions: showMentions, 55 60 showPrevNext: showPrevNext, 61 + showRecommends: showRecommends, 56 62 }, 57 63 }); 58 64 toast({ type: "success", content: <strong>Posts Updated!</strong> }); ··· 99 105 <div className="flex flex-col justify-start"> 100 106 <div className="font-bold">Show Mentions</div> 101 107 <div className="text-tertiary text-sm leading-tight"> 102 - Display a list of posts on Bluesky that mention your post 108 + Display a list Bluesky mentions about your post 109 + </div> 110 + </div> 111 + </Toggle> 112 + 113 + <Toggle 114 + toggle={showRecommends} 115 + onToggle={() => { 116 + setShowRecommends(!showRecommends); 117 + }} 118 + > 119 + <div className="flex flex-col justify-start"> 120 + <div className="font-bold">Show Recommends</div> 121 + <div className="text-tertiary text-sm leading-tight"> 122 + Allow readers to recommend/like your post 103 123 </div> 104 124 </div> 105 125 </Toggle>
+3
app/lish/[did]/[publication]/page.tsx
··· 175 175 showMentions={ 176 176 record?.preferences?.showMentions !== false 177 177 } 178 + showRecommends={ 179 + record?.preferences?.showRecommends !== false 180 + } 178 181 /> 179 182 </div> 180 183 </div>
+1
app/lish/createPub/CreatePubForm.tsx
··· 58 58 showComments: true, 59 59 showMentions: true, 60 60 showPrevNext: true, 61 + showRecommends: true, 61 62 }, 62 63 }); 63 64
+1 -2
app/lish/createPub/UpdatePubForm.tsx
··· 88 88 showComments: showComments, 89 89 showMentions: showMentions, 90 90 showPrevNext: showPrevNext, 91 + showRecommends: record?.preferences?.showRecommends ?? true, 91 92 }, 92 93 }); 93 94 toast({ type: "success", content: "Updated!" }); ··· 194 195 </p> 195 196 </div> 196 197 </Toggle> 197 - 198 - 199 198 </div> 200 199 </form> 201 200 );
+23 -9
app/lish/createPub/createPublication.ts
··· 5 5 PubLeafletPublication, 6 6 SiteStandardPublication, 7 7 } from "lexicons/api"; 8 - import { 9 - restoreOAuthSession, 10 - OAuthSessionError, 11 - } from "src/atproto-oauth"; 8 + import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; 12 9 import { getIdentityData } from "actions/getIdentityData"; 13 10 import { supabaseServerClient } from "supabase/serverClient"; 14 11 import { Json } from "supabase/database.types"; ··· 76 73 77 74 // Build record based on publication type 78 75 let record: SiteStandardPublication.Record | PubLeafletPublication.Record; 79 - let iconBlob: Awaited<ReturnType<typeof agent.com.atproto.repo.uploadBlob>>["data"]["blob"] | undefined; 76 + let iconBlob: 77 + | Awaited< 78 + ReturnType<typeof agent.com.atproto.repo.uploadBlob> 79 + >["data"]["blob"] 80 + | undefined; 80 81 81 82 // Upload the icon if provided 82 83 if (iconFile && iconFile.size > 0) { ··· 97 98 ...(iconBlob && { icon: iconBlob }), 98 99 basicTheme: { 99 100 $type: "site.standard.theme.basic", 100 - background: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.background }, 101 - foreground: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.foreground }, 102 - accent: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.accent }, 103 - accentForeground: { $type: "site.standard.theme.color#rgb", ...PubThemeDefaultsRGB.accentForeground }, 101 + background: { 102 + $type: "site.standard.theme.color#rgb", 103 + ...PubThemeDefaultsRGB.background, 104 + }, 105 + foreground: { 106 + $type: "site.standard.theme.color#rgb", 107 + ...PubThemeDefaultsRGB.foreground, 108 + }, 109 + accent: { 110 + $type: "site.standard.theme.color#rgb", 111 + ...PubThemeDefaultsRGB.accent, 112 + }, 113 + accentForeground: { 114 + $type: "site.standard.theme.color#rgb", 115 + ...PubThemeDefaultsRGB.accentForeground, 116 + }, 104 117 }, 105 118 preferences: { 106 119 showInDiscover: preferences.showInDiscover, 107 120 showComments: preferences.showComments, 108 121 showMentions: preferences.showMentions, 109 122 showPrevNext: preferences.showPrevNext, 123 + showRecommends: preferences.showRecommends, 110 124 }, 111 125 } satisfies SiteStandardPublication.Record; 112 126 } else {
+167 -98
app/lish/createPub/updatePublication.ts
··· 77 77 } 78 78 79 79 const aturi = new AtUri(existingPub.uri); 80 - const publicationType = getPublicationType(aturi.collection) as PublicationType; 80 + const publicationType = getPublicationType( 81 + aturi.collection, 82 + ) as PublicationType; 81 83 82 84 // Normalize existing record 83 85 const normalizedPub = normalizePublicationRecord(existingPub.record); ··· 128 130 } 129 131 130 132 /** Merges override with existing value, respecting explicit undefined */ 131 - function resolveField<T>(override: T | undefined, existing: T | undefined, hasOverride: boolean): T | undefined { 133 + function resolveField<T>( 134 + override: T | undefined, 135 + existing: T | undefined, 136 + hasOverride: boolean, 137 + ): T | undefined { 132 138 return hasOverride ? override : existing; 133 139 } 134 140 ··· 146 152 return { 147 153 $type: "pub.leaflet.publication", 148 154 name: overrides.name ?? normalizedPub?.name ?? "", 149 - description: resolveField(overrides.description, normalizedPub?.description, "description" in overrides), 150 - icon: resolveField(overrides.icon, normalizedPub?.icon, "icon" in overrides), 151 - theme: resolveField(overrides.theme, normalizedPub?.theme, "theme" in overrides), 155 + description: resolveField( 156 + overrides.description, 157 + normalizedPub?.description, 158 + "description" in overrides, 159 + ), 160 + icon: resolveField( 161 + overrides.icon, 162 + normalizedPub?.icon, 163 + "icon" in overrides, 164 + ), 165 + theme: resolveField( 166 + overrides.theme, 167 + normalizedPub?.theme, 168 + "theme" in overrides, 169 + ), 152 170 base_path: overrides.basePath ?? existingBasePath, 153 - preferences: preferences ? { 154 - $type: "pub.leaflet.publication#preferences", 155 - showInDiscover: preferences.showInDiscover, 156 - showComments: preferences.showComments, 157 - showMentions: preferences.showMentions, 158 - showPrevNext: preferences.showPrevNext, 159 - } : undefined, 171 + preferences: preferences 172 + ? { 173 + $type: "pub.leaflet.publication#preferences", 174 + showInDiscover: preferences.showInDiscover, 175 + showComments: preferences.showComments, 176 + showMentions: preferences.showMentions, 177 + showPrevNext: preferences.showPrevNext, 178 + showRecommends: preferences.showRecommends, 179 + } 180 + : undefined, 160 181 }; 161 182 } 162 183 ··· 175 196 return { 176 197 $type: "site.standard.publication", 177 198 name: overrides.name ?? normalizedPub?.name ?? "", 178 - description: resolveField(overrides.description, normalizedPub?.description, "description" in overrides), 179 - icon: resolveField(overrides.icon, normalizedPub?.icon, "icon" in overrides), 180 - theme: resolveField(overrides.theme, normalizedPub?.theme, "theme" in overrides), 181 - basicTheme: resolveField(overrides.basicTheme, normalizedPub?.basicTheme, "basicTheme" in overrides), 199 + description: resolveField( 200 + overrides.description, 201 + normalizedPub?.description, 202 + "description" in overrides, 203 + ), 204 + icon: resolveField( 205 + overrides.icon, 206 + normalizedPub?.icon, 207 + "icon" in overrides, 208 + ), 209 + theme: resolveField( 210 + overrides.theme, 211 + normalizedPub?.theme, 212 + "theme" in overrides, 213 + ), 214 + basicTheme: resolveField( 215 + overrides.basicTheme, 216 + normalizedPub?.basicTheme, 217 + "basicTheme" in overrides, 218 + ), 182 219 url: basePath ? `https://${basePath}` : normalizedPub?.url || "", 183 - preferences: preferences ? { 184 - showInDiscover: preferences.showInDiscover, 185 - showComments: preferences.showComments, 186 - showMentions: preferences.showMentions, 187 - showPrevNext: preferences.showPrevNext, 188 - } : undefined, 220 + preferences: preferences 221 + ? { 222 + showInDiscover: preferences.showInDiscover, 223 + showComments: preferences.showComments, 224 + showMentions: preferences.showMentions, 225 + showPrevNext: preferences.showPrevNext, 226 + showRecommends: preferences.showRecommends, 227 + } 228 + : undefined, 189 229 }; 190 230 } 191 231 ··· 217 257 iconFile?: File | null; 218 258 preferences?: Omit<PubLeafletPublication.Preferences, "$type">; 219 259 }): Promise<UpdatePublicationResult> { 220 - return withPublicationUpdate(uri, async ({ normalizedPub, existingBasePath, publicationType, agent }) => { 221 - // Upload icon if provided 222 - let iconBlob = normalizedPub?.icon; 223 - if (iconFile && iconFile.size > 0) { 224 - const buffer = await iconFile.arrayBuffer(); 225 - const uploadResult = await agent.com.atproto.repo.uploadBlob( 226 - new Uint8Array(buffer), 227 - { encoding: iconFile.type }, 228 - ); 229 - if (uploadResult.data.blob) { 230 - iconBlob = uploadResult.data.blob; 260 + return withPublicationUpdate( 261 + uri, 262 + async ({ normalizedPub, existingBasePath, publicationType, agent }) => { 263 + // Upload icon if provided 264 + let iconBlob = normalizedPub?.icon; 265 + if (iconFile && iconFile.size > 0) { 266 + const buffer = await iconFile.arrayBuffer(); 267 + const uploadResult = await agent.com.atproto.repo.uploadBlob( 268 + new Uint8Array(buffer), 269 + { encoding: iconFile.type }, 270 + ); 271 + if (uploadResult.data.blob) { 272 + iconBlob = uploadResult.data.blob; 273 + } 231 274 } 232 - } 233 275 234 - return buildRecord(normalizedPub, existingBasePath, publicationType, { 235 - name, 236 - description, 237 - icon: iconBlob, 238 - preferences, 239 - }); 240 - }); 276 + return buildRecord(normalizedPub, existingBasePath, publicationType, { 277 + name, 278 + description, 279 + icon: iconBlob, 280 + preferences, 281 + }); 282 + }, 283 + ); 241 284 } 242 285 243 286 export async function updatePublicationBasePath({ ··· 247 290 uri: string; 248 291 base_path: string; 249 292 }): Promise<UpdatePublicationResult> { 250 - return withPublicationUpdate(uri, async ({ normalizedPub, existingBasePath, publicationType }) => { 251 - return buildRecord(normalizedPub, existingBasePath, publicationType, { 252 - basePath: base_path, 253 - }); 254 - }); 293 + return withPublicationUpdate( 294 + uri, 295 + async ({ normalizedPub, existingBasePath, publicationType }) => { 296 + return buildRecord(normalizedPub, existingBasePath, publicationType, { 297 + basePath: base_path, 298 + }); 299 + }, 300 + ); 255 301 } 256 302 257 303 type Color = ··· 275 321 accentText: Color; 276 322 }; 277 323 }): Promise<UpdatePublicationResult> { 278 - return withPublicationUpdate(uri, async ({ normalizedPub, existingBasePath, publicationType, agent }) => { 279 - // Build theme object 280 - const themeData = { 281 - $type: "pub.leaflet.publication#theme" as const, 282 - backgroundImage: theme.backgroundImage 283 - ? { 284 - $type: "pub.leaflet.theme.backgroundImage", 285 - image: ( 286 - await agent.com.atproto.repo.uploadBlob( 287 - new Uint8Array(await theme.backgroundImage.arrayBuffer()), 288 - { encoding: theme.backgroundImage.type }, 289 - ) 290 - )?.data.blob, 291 - width: theme.backgroundRepeat || undefined, 292 - repeat: !!theme.backgroundRepeat, 293 - } 294 - : theme.backgroundImage === null 295 - ? undefined 296 - : normalizedPub?.theme?.backgroundImage, 297 - backgroundColor: theme.backgroundColor 298 - ? { 299 - ...theme.backgroundColor, 300 - } 301 - : undefined, 302 - pageWidth: theme.pageWidth, 303 - primary: { 304 - ...theme.primary, 305 - }, 306 - pageBackground: { 307 - ...theme.pageBackground, 308 - }, 309 - showPageBackground: theme.showPageBackground, 310 - accentBackground: { 311 - ...theme.accentBackground, 312 - }, 313 - accentText: { 314 - ...theme.accentText, 315 - }, 316 - }; 324 + return withPublicationUpdate( 325 + uri, 326 + async ({ normalizedPub, existingBasePath, publicationType, agent }) => { 327 + // Build theme object 328 + const themeData = { 329 + $type: "pub.leaflet.publication#theme" as const, 330 + backgroundImage: theme.backgroundImage 331 + ? { 332 + $type: "pub.leaflet.theme.backgroundImage", 333 + image: ( 334 + await agent.com.atproto.repo.uploadBlob( 335 + new Uint8Array(await theme.backgroundImage.arrayBuffer()), 336 + { encoding: theme.backgroundImage.type }, 337 + ) 338 + )?.data.blob, 339 + width: theme.backgroundRepeat || undefined, 340 + repeat: !!theme.backgroundRepeat, 341 + } 342 + : theme.backgroundImage === null 343 + ? undefined 344 + : normalizedPub?.theme?.backgroundImage, 345 + backgroundColor: theme.backgroundColor 346 + ? { 347 + ...theme.backgroundColor, 348 + } 349 + : undefined, 350 + pageWidth: theme.pageWidth, 351 + primary: { 352 + ...theme.primary, 353 + }, 354 + pageBackground: { 355 + ...theme.pageBackground, 356 + }, 357 + showPageBackground: theme.showPageBackground, 358 + accentBackground: { 359 + ...theme.accentBackground, 360 + }, 361 + accentText: { 362 + ...theme.accentText, 363 + }, 364 + }; 317 365 318 - // Derive basicTheme from the theme colors for site.standard.publication 319 - const basicTheme: NormalizedPublication["basicTheme"] = { 320 - $type: "site.standard.theme.basic", 321 - background: { $type: "site.standard.theme.color#rgb", r: theme.backgroundColor.r, g: theme.backgroundColor.g, b: theme.backgroundColor.b }, 322 - foreground: { $type: "site.standard.theme.color#rgb", r: theme.primary.r, g: theme.primary.g, b: theme.primary.b }, 323 - accent: { $type: "site.standard.theme.color#rgb", r: theme.accentBackground.r, g: theme.accentBackground.g, b: theme.accentBackground.b }, 324 - accentForeground: { $type: "site.standard.theme.color#rgb", r: theme.accentText.r, g: theme.accentText.g, b: theme.accentText.b }, 325 - }; 366 + // Derive basicTheme from the theme colors for site.standard.publication 367 + const basicTheme: NormalizedPublication["basicTheme"] = { 368 + $type: "site.standard.theme.basic", 369 + background: { 370 + $type: "site.standard.theme.color#rgb", 371 + r: theme.backgroundColor.r, 372 + g: theme.backgroundColor.g, 373 + b: theme.backgroundColor.b, 374 + }, 375 + foreground: { 376 + $type: "site.standard.theme.color#rgb", 377 + r: theme.primary.r, 378 + g: theme.primary.g, 379 + b: theme.primary.b, 380 + }, 381 + accent: { 382 + $type: "site.standard.theme.color#rgb", 383 + r: theme.accentBackground.r, 384 + g: theme.accentBackground.g, 385 + b: theme.accentBackground.b, 386 + }, 387 + accentForeground: { 388 + $type: "site.standard.theme.color#rgb", 389 + r: theme.accentText.r, 390 + g: theme.accentText.g, 391 + b: theme.accentText.b, 392 + }, 393 + }; 326 394 327 - return buildRecord(normalizedPub, existingBasePath, publicationType, { 328 - theme: themeData, 329 - basicTheme, 330 - }); 331 - }); 395 + return buildRecord(normalizedPub, existingBasePath, publicationType, { 396 + theme: themeData, 397 + basicTheme, 398 + }); 399 + }, 400 + ); 332 401 }
+9 -5
components/InteractionsPreview.tsx
··· 18 18 postUrl: string; 19 19 showComments: boolean; 20 20 showMentions: boolean; 21 + showRecommends: boolean; 21 22 22 23 share?: boolean; 23 24 }) => { 24 25 let smoker = useSmoker(); 25 26 let interactionsAvailable = 26 27 (props.quotesCount > 0 && props.showMentions) || 27 - (props.showComments !== false && props.commentsCount > 0); 28 + (props.showComments !== false && props.commentsCount > 0) || 29 + (props.showRecommends !== false && props.recommendsCount > 0); 28 30 29 31 const tagsCount = props.tags?.length || 0; 30 32 31 33 return ( 32 34 <div className={`flex gap-2 text-tertiary text-sm items-center`}> 33 - <RecommendButton 34 - documentUri={props.documentUri} 35 - recommendsCount={props.recommendsCount} 36 - /> 35 + {props.showRecommends === false ? null : ( 36 + <RecommendButton 37 + documentUri={props.documentUri} 38 + recommendsCount={props.recommendsCount} 39 + /> 40 + )} 37 41 38 42 {!props.showMentions || props.quotesCount === 0 ? null : ( 39 43 <SpeedyLink
+3
components/PostListing.tsx
··· 111 111 tags={tags} 112 112 showComments={pubRecord?.preferences?.showComments !== false} 113 113 showMentions={pubRecord?.preferences?.showMentions !== false} 114 + showRecommends={ 115 + pubRecord?.preferences?.showRecommends !== false 116 + } 114 117 share 115 118 /> 116 119 </div>
+44 -39
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 { 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' 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"; 10 14 11 15 const is$typed = _is$typed, 12 - validate = _validate 13 - const id = 'pub.leaflet.publication' 16 + validate = _validate; 17 + const id = "pub.leaflet.publication"; 14 18 15 19 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 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; 24 28 } 25 29 26 - const hashRecord = 'main' 30 + const hashRecord = "main"; 27 31 28 32 export function isRecord<V>(v: V) { 29 - return is$typed(v, id, hashRecord) 33 + return is$typed(v, id, hashRecord); 30 34 } 31 35 32 36 export function validateRecord<V>(v: V) { 33 - return validate<Record & V>(v, id, hashRecord, true) 37 + return validate<Record & V>(v, id, hashRecord, true); 34 38 } 35 39 36 40 export interface Preferences { 37 - $type?: 'pub.leaflet.publication#preferences' 38 - showInDiscover: boolean 39 - showComments: boolean 40 - showMentions: boolean 41 - showPrevNext: boolean 41 + $type?: "pub.leaflet.publication#preferences"; 42 + showInDiscover: boolean; 43 + showComments: boolean; 44 + showMentions: boolean; 45 + showPrevNext: boolean; 46 + showRecommends: boolean; 42 47 } 43 48 44 - const hashPreferences = 'preferences' 49 + const hashPreferences = "preferences"; 45 50 46 51 export function isPreferences<V>(v: V) { 47 - return is$typed(v, id, hashPreferences) 52 + return is$typed(v, id, hashPreferences); 48 53 } 49 54 50 55 export function validatePreferences<V>(v: V) { 51 - return validate<Preferences & V>(v, id, hashPreferences) 56 + return validate<Preferences & V>(v, id, hashPreferences); 52 57 } 53 58 54 59 export interface Theme { 55 - $type?: 'pub.leaflet.publication#theme' 60 + $type?: "pub.leaflet.publication#theme"; 56 61 backgroundColor?: 57 62 | $Typed<PubLeafletThemeColor.Rgba> 58 63 | $Typed<PubLeafletThemeColor.Rgb> 59 - | { $type: string } 60 - backgroundImage?: PubLeafletThemeBackgroundImage.Main 61 - pageWidth?: number 64 + | { $type: string }; 65 + backgroundImage?: PubLeafletThemeBackgroundImage.Main; 66 + pageWidth?: number; 62 67 primary?: 63 68 | $Typed<PubLeafletThemeColor.Rgba> 64 69 | $Typed<PubLeafletThemeColor.Rgb> 65 - | { $type: string } 70 + | { $type: string }; 66 71 pageBackground?: 67 72 | $Typed<PubLeafletThemeColor.Rgba> 68 73 | $Typed<PubLeafletThemeColor.Rgb> 69 - | { $type: string } 70 - showPageBackground: boolean 74 + | { $type: string }; 75 + showPageBackground: boolean; 71 76 accentBackground?: 72 77 | $Typed<PubLeafletThemeColor.Rgba> 73 78 | $Typed<PubLeafletThemeColor.Rgb> 74 - | { $type: string } 79 + | { $type: string }; 75 80 accentText?: 76 81 | $Typed<PubLeafletThemeColor.Rgba> 77 82 | $Typed<PubLeafletThemeColor.Rgb> 78 - | { $type: string } 83 + | { $type: string }; 79 84 } 80 85 81 - const hashTheme = 'theme' 86 + const hashTheme = "theme"; 82 87 83 88 export function isTheme<V>(v: V) { 84 - return is$typed(v, id, hashTheme) 89 + return is$typed(v, id, hashTheme); 85 90 } 86 91 87 92 export function validateTheme<V>(v: V) { 88 - return validate<Theme & V>(v, id, hashTheme) 93 + return validate<Theme & V>(v, id, hashTheme); 89 94 }
+33 -28
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 { 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' 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"; 10 14 11 15 const is$typed = _is$typed, 12 - validate = _validate 13 - const id = 'site.standard.publication' 16 + validate = _validate; 17 + const id = "site.standard.publication"; 14 18 15 19 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 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; 25 29 } 26 30 27 - const hashRecord = 'main' 31 + const hashRecord = "main"; 28 32 29 33 export function isRecord<V>(v: V) { 30 - return is$typed(v, id, hashRecord) 34 + return is$typed(v, id, hashRecord); 31 35 } 32 36 33 37 export function validateRecord<V>(v: V) { 34 - return validate<Record & V>(v, id, hashRecord, true) 38 + return validate<Record & V>(v, id, hashRecord, true); 35 39 } 36 40 37 41 export interface Preferences { 38 - $type?: 'site.standard.publication#preferences' 39 - showInDiscover: boolean 40 - showComments: boolean 41 - showMentions: boolean 42 - showPrevNext: boolean 42 + $type?: "site.standard.publication#preferences"; 43 + showInDiscover: boolean; 44 + showComments: boolean; 45 + showMentions: boolean; 46 + showPrevNext: boolean; 47 + showRecommends: boolean; 43 48 } 44 49 45 - const hashPreferences = 'preferences' 50 + const hashPreferences = "preferences"; 46 51 47 52 export function isPreferences<V>(v: V) { 48 - return is$typed(v, id, hashPreferences) 53 + return is$typed(v, id, hashPreferences); 49 54 } 50 55 51 56 export function validatePreferences<V>(v: V) { 52 - return validate<Preferences & V>(v, id, hashPreferences) 57 + return validate<Preferences & V>(v, id, hashPreferences); 53 58 }
+4
lexicons/pub/leaflet/publication.json
··· 59 59 "showPrevNext": { 60 60 "type": "boolean", 61 61 "default": true 62 + }, 63 + "showRecommends": { 64 + "type": "boolean", 65 + "default": true 62 66 } 63 67 } 64 68 },
+4
lexicons/site/standard/publication.json
··· 58 58 "showPrevNext": { 59 59 "default": false, 60 60 "type": "boolean" 61 + }, 62 + "showRecommends": { 63 + "default": true, 64 + "type": "boolean" 61 65 } 62 66 }, 63 67 "type": "object"
+16 -11
lexicons/src/normalize.ts
··· 50 50 * Checks if the record is a pub.leaflet.document 51 51 */ 52 52 export function isLeafletDocument( 53 - record: unknown 53 + record: unknown, 54 54 ): record is PubLeafletDocument.Record { 55 55 if (!record || typeof record !== "object") return false; 56 56 const r = record as Record<string, unknown>; ··· 65 65 * Checks if the record is a site.standard.document 66 66 */ 67 67 export function isStandardDocument( 68 - record: unknown 68 + record: unknown, 69 69 ): record is SiteStandardDocument.Record { 70 70 if (!record || typeof record !== "object") return false; 71 71 const r = record as Record<string, unknown>; ··· 76 76 * Checks if the record is a pub.leaflet.publication 77 77 */ 78 78 export function isLeafletPublication( 79 - record: unknown 79 + record: unknown, 80 80 ): record is PubLeafletPublication.Record { 81 81 if (!record || typeof record !== "object") return false; 82 82 const r = record as Record<string, unknown>; ··· 91 91 * Checks if the record is a site.standard.publication 92 92 */ 93 93 export function isStandardPublication( 94 - record: unknown 94 + record: unknown, 95 95 ): record is SiteStandardPublication.Record { 96 96 if (!record || typeof record !== "object") return false; 97 97 const r = record as Record<string, unknown>; ··· 106 106 | $Typed<PubLeafletThemeColor.Rgba> 107 107 | $Typed<PubLeafletThemeColor.Rgb> 108 108 | { $type: string } 109 - | undefined 109 + | undefined, 110 110 ): { r: number; g: number; b: number } | undefined { 111 111 if (!color || typeof color !== "object") return undefined; 112 112 const c = color as Record<string, unknown>; ··· 124 124 * Converts a pub.leaflet theme to a site.standard.theme.basic format 125 125 */ 126 126 export function leafletThemeToBasicTheme( 127 - theme: PubLeafletPublication.Theme | undefined 127 + theme: PubLeafletPublication.Theme | undefined, 128 128 ): SiteStandardThemeBasic.Main | undefined { 129 129 if (!theme) return undefined; 130 130 131 131 const background = extractRgb(theme.backgroundColor); 132 - const accent = extractRgb(theme.accentBackground) || extractRgb(theme.primary); 132 + const accent = 133 + extractRgb(theme.accentBackground) || extractRgb(theme.primary); 133 134 const accentForeground = extractRgb(theme.accentText); 134 135 135 136 // If we don't have the required colors, return undefined ··· 160 161 * @param uri - Optional document URI, used to extract the rkey for the path field when normalizing pub.leaflet records 161 162 * @returns A normalized document in site.standard format, or null if invalid/unrecognized 162 163 */ 163 - export function normalizeDocument(record: unknown, uri?: string): NormalizedDocument | null { 164 + export function normalizeDocument( 165 + record: unknown, 166 + uri?: string, 167 + ): NormalizedDocument | null { 164 168 if (!record || typeof record !== "object") return null; 165 169 166 170 // Pass through site.standard records directly (theme is already in correct format if present) ··· 219 223 * @returns A normalized publication in site.standard format, or null if invalid/unrecognized 220 224 */ 221 225 export function normalizePublication( 222 - record: unknown 226 + record: unknown, 223 227 ): NormalizedPublication | null { 224 228 if (!record || typeof record !== "object") return null; 225 229 ··· 268 272 showComments: record.preferences.showComments, 269 273 showMentions: record.preferences.showMentions, 270 274 showPrevNext: record.preferences.showPrevNext, 275 + showRecommends: record.preferences.showRecommends, 271 276 } 272 277 : undefined; 273 278 ··· 290 295 * Type guard to check if a normalized document has leaflet content 291 296 */ 292 297 export function hasLeafletContent( 293 - doc: NormalizedDocument 298 + doc: NormalizedDocument, 294 299 ): doc is NormalizedDocument & { 295 300 content: $Typed<PubLeafletContent.Main>; 296 301 } { ··· 304 309 * Gets the pages array from a normalized document, handling both formats 305 310 */ 306 311 export function getDocumentPages( 307 - doc: NormalizedDocument 312 + doc: NormalizedDocument, 308 313 ): PubLeafletContent.Main["pages"] | undefined { 309 314 if (!doc.content) return undefined; 310 315
+1
lexicons/src/publication.ts
··· 29 29 showComments: { type: "boolean", default: true }, 30 30 showMentions: { type: "boolean", default: true }, 31 31 showPrevNext: { type: "boolean", default: true }, 32 + showRecommends: { type: "boolean", default: true }, 32 33 }, 33 34 }, 34 35 theme: {