a tool for shared writing and social publishing

Compare changes

Choose any two refs to compare.

Changed files
+3015 -2429
actions
app
components
lexicons
api
types
pub
leaflet
pub
leaflet
src
src
+1 -1
.prettierrc
··· 1 - {}
··· 1 + {}
+20 -8
actions/publishToPublication.ts
··· 2 3 import * as Y from "yjs"; 4 import * as base64 from "base64-js"; 5 - import { 6 - restoreOAuthSession, 7 - OAuthSessionError, 8 - } from "src/atproto-oauth"; 9 import { getIdentityData } from "actions/getIdentityData"; 10 import { 11 AtpBaseClient, ··· 50 ColorToRGBA, 51 } from "components/ThemeManager/colorToLexicons"; 52 import { parseColor } from "@react-stately/color"; 53 - import { Notification, pingIdentityToUpdateNotification } from "src/notifications"; 54 import { v7 } from "uuid"; 55 56 type PublishResult = ··· 253 254 // Create notifications for mentions (only on first publish) 255 if (!existingDocUri) { 256 - await createMentionNotifications(result.uri, record, credentialSession.did!); 257 } 258 259 return { success: true, rkey, record: JSON.parse(JSON.stringify(record)) }; ··· 463 464 if (b.type == "text") { 465 let [stringValue, facets] = getBlockContent(b.value); 466 let block: $Typed<PubLeafletBlocksText.Main> = { 467 $type: ids.PubLeafletBlocksText, 468 plaintext: stringValue, 469 facets, 470 }; 471 return block; 472 } ··· 778 root_entity, 779 "theme/background-image-repeat", 780 )?.[0]; 781 782 let theme: PubLeafletPublication.Theme = { 783 showPageBackground: showPageBackground ?? true, 784 }; 785 786 if (pageBackground) 787 theme.backgroundColor = ColorToRGBA(parseColor(`hsba(${pageBackground})`)); 788 if (cardBackground) ··· 865 .single(); 866 867 if (publication && publication.identity_did !== authorDid) { 868 - mentionedPublications.set(publication.identity_did, feature.atURI); 869 } 870 } else if (uri.collection === "pub.leaflet.document") { 871 // Get the document owner's DID ··· 876 .single(); 877 878 if (document) { 879 - const docRecord = document.data as PubLeafletDocument.Record; 880 if (docRecord.author !== authorDid) { 881 mentionedDocuments.set(docRecord.author, feature.atURI); 882 }
··· 2 3 import * as Y from "yjs"; 4 import * as base64 from "base64-js"; 5 + import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; 6 import { getIdentityData } from "actions/getIdentityData"; 7 import { 8 AtpBaseClient, ··· 47 ColorToRGBA, 48 } from "components/ThemeManager/colorToLexicons"; 49 import { parseColor } from "@react-stately/color"; 50 + import { 51 + Notification, 52 + pingIdentityToUpdateNotification, 53 + } from "src/notifications"; 54 import { v7 } from "uuid"; 55 56 type PublishResult = ··· 253 254 // Create notifications for mentions (only on first publish) 255 if (!existingDocUri) { 256 + await createMentionNotifications( 257 + result.uri, 258 + record, 259 + credentialSession.did!, 260 + ); 261 } 262 263 return { success: true, rkey, record: JSON.parse(JSON.stringify(record)) }; ··· 467 468 if (b.type == "text") { 469 let [stringValue, facets] = getBlockContent(b.value); 470 + let [textSize] = scan.eav(b.value, "block/text-size"); 471 let block: $Typed<PubLeafletBlocksText.Main> = { 472 $type: ids.PubLeafletBlocksText, 473 plaintext: stringValue, 474 facets, 475 + ...(textSize && { textSize: textSize.data.value }), 476 }; 477 return block; 478 } ··· 784 root_entity, 785 "theme/background-image-repeat", 786 )?.[0]; 787 + let pageWidth = scan.eav(root_entity, "theme/page-width")?.[0]; 788 789 let theme: PubLeafletPublication.Theme = { 790 showPageBackground: showPageBackground ?? true, 791 }; 792 793 + if (pageWidth) theme.pageWidth = pageWidth.data.value; 794 if (pageBackground) 795 theme.backgroundColor = ColorToRGBA(parseColor(`hsba(${pageBackground})`)); 796 if (cardBackground) ··· 873 .single(); 874 875 if (publication && publication.identity_did !== authorDid) { 876 + mentionedPublications.set( 877 + publication.identity_did, 878 + feature.atURI, 879 + ); 880 } 881 } else if (uri.collection === "pub.leaflet.document") { 882 // Get the document owner's DID ··· 887 .single(); 888 889 if (document) { 890 + const docRecord = 891 + document.data as PubLeafletDocument.Record; 892 if (docRecord.author !== authorDid) { 893 mentionedDocuments.set(docRecord.author, feature.atURI); 894 }
+1 -1
app/(home-pages)/notifications/CommentNotication.tsx
··· 1 - import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock"; 2 import { 3 AppBskyActorProfile, 4 PubLeafletComment,
··· 1 + import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/Blocks/BaseTextBlock"; 2 import { 3 AppBskyActorProfile, 4 PubLeafletComment,
+1 -1
app/(home-pages)/notifications/Notification.tsx
··· 1 "use client"; 2 import { Avatar } from "components/Avatar"; 3 - import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock"; 4 import { PubLeafletPublication, PubLeafletRichtextFacet } from "lexicons/api"; 5 import { timeAgo } from "src/utils/timeAgo"; 6 import { useReplicache, useEntity } from "src/replicache";
··· 1 "use client"; 2 import { Avatar } from "components/Avatar"; 3 + import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/Blocks/BaseTextBlock"; 4 import { PubLeafletPublication, PubLeafletRichtextFacet } from "lexicons/api"; 5 import { timeAgo } from "src/utils/timeAgo"; 6 import { useReplicache, useEntity } from "src/replicache";
+1 -1
app/(home-pages)/notifications/ReplyNotification.tsx
··· 1 import { Avatar } from "components/Avatar"; 2 - import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock"; 3 import { ReplyTiny } from "components/Icons/ReplyTiny"; 4 import { 5 CommentInNotification,
··· 1 import { Avatar } from "components/Avatar"; 2 + import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/Blocks/BaseTextBlock"; 3 import { ReplyTiny } from "components/Icons/ReplyTiny"; 4 import { 5 CommentInNotification,
+56 -36
app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx
··· 1 "use client"; 2 import { Avatar } from "components/Avatar"; 3 - import { AppBskyActorProfile, PubLeafletPublication } from "lexicons/api"; 4 - import { blobRefToSrc } from "src/utils/blobRefToSrc"; 5 - import type { ProfileData } from "./layout"; 6 import { usePubTheme } from "components/ThemeManager/PublicationThemeProvider"; 7 import { colorToString } from "components/ThemeManager/useColorAttribute"; 8 import { PubIcon } from "components/ActionBar/Publications"; ··· 25 <Avatar 26 src={profileRecord.avatar} 27 displayName={profileRecord.displayName} 28 - className="mx-auto mt-3 sm:mt-4" 29 giant 30 /> 31 ); 32 33 const displayNameElement = ( 34 - <h3 className=" px-3 sm:px-4 pt-2 leading-tight"> 35 {profileRecord.displayName 36 ? profileRecord.displayName 37 : `@${props.profile.handle}`} ··· 40 41 const handleElement = profileRecord.displayName && ( 42 <div 43 - className={`text-tertiary ${props.popover ? "text-xs" : "text-sm"} pb-1 italic px-3 sm:px-4 truncate`} 44 > 45 @{props.profile.handle} 46 </div> 47 ); 48 49 return ( 50 <div 51 - className={`flex flex-col relative ${props.popover && "text-sm"}`} 52 id="profile-header" 53 > 54 - <ProfileLinks handle={props.profile.handle || ""} /> 55 - <div className="flex flex-col"> 56 - <div className="flex flex-col group"> 57 {props.popover ? ( 58 <SpeedyLink className={"hover:no-underline!"} href={profileUrl}> 59 {avatarElement} ··· 61 ) : ( 62 avatarElement 63 )} 64 - {props.popover ? ( 65 - <SpeedyLink 66 - className={" text-primary group-hover:underline"} 67 - href={profileUrl} 68 - > 69 - {displayNameElement} 70 - </SpeedyLink> 71 - ) : ( 72 - displayNameElement 73 - )} 74 - {props.popover && handleElement ? ( 75 - <SpeedyLink className={"group-hover:underline"} href={profileUrl}> 76 - {handleElement} 77 - </SpeedyLink> 78 - ) : ( 79 - handleElement 80 - )} 81 </div> 82 - <pre className="text-secondary px-3 sm:px-4 whitespace-pre-wrap"> 83 - {profileRecord.description 84 - ? parseDescription(profileRecord.description) 85 - : null} 86 - </pre> 87 - <div className=" w-full overflow-x-scroll py-3 mb-3 "> 88 <div 89 className={`grid grid-flow-col gap-2 mx-auto w-fit px-3 sm:px-4 ${props.popover ? "auto-cols-[164px]" : "auto-cols-[164px] sm:auto-cols-[240px]"}`} 90 > ··· 104 105 const ProfileLinks = (props: { handle: string }) => { 106 return ( 107 - <div className="absolute sm:top-4 top-3 sm:right-4 right-3 flex flex-row gap-2"> 108 <a 109 className="text-tertiary hover:text-accent-contrast hover:no-underline!" 110 href={`https://bsky.app/profile/${props.handle}`} ··· 124 return ( 125 <a 126 href={`https://${record.base_path}`} 127 - className="border border-border p-2 rounded-lg hover:no-underline! text-primary basis-1/2" 128 style={{ backgroundColor: `rgb(${colorToString(bgLeaflet, "rgb")})` }} 129 > 130 <div ··· 225 ? urlWithoutProtocol.slice(0, 50) + "โ€ฆ" 226 : urlWithoutProtocol; 227 parts.push( 228 - <a key={key++} href={match.href} target="_blank" rel="noopener noreferrer"> 229 {displayText} 230 </a>, 231 ); ··· 241 242 return parts; 243 }
··· 1 "use client"; 2 import { Avatar } from "components/Avatar"; 3 + import { PubLeafletPublication } from "lexicons/api"; 4 import { usePubTheme } from "components/ThemeManager/PublicationThemeProvider"; 5 import { colorToString } from "components/ThemeManager/useColorAttribute"; 6 import { PubIcon } from "components/ActionBar/Publications"; ··· 23 <Avatar 24 src={profileRecord.avatar} 25 displayName={profileRecord.displayName} 26 + className="profileAvatar mx-auto mt-3 sm:mt-4" 27 giant 28 /> 29 ); 30 31 const displayNameElement = ( 32 + <h3 className="profileName px-3 sm:px-4 pt-2 leading-tight"> 33 {profileRecord.displayName 34 ? profileRecord.displayName 35 : `@${props.profile.handle}`} ··· 38 39 const handleElement = profileRecord.displayName && ( 40 <div 41 + className={`profileHandle text-secondary ${props.popover ? "text-sm" : "text-sm"} px-3 sm:px-4 truncate`} 42 > 43 @{props.profile.handle} 44 </div> 45 ); 46 + console.log(props.profile); 47 48 return ( 49 <div 50 + className={`profileHeader flex flex-col relative `} 51 id="profile-header" 52 > 53 + {!props.popover && <ProfileLinks handle={props.profile.handle || ""} />} 54 + <div className="profileInfo flex flex-col gap-3"> 55 + <div className="profileNameAndHandle flex flex-col "> 56 {props.popover ? ( 57 <SpeedyLink className={"hover:no-underline!"} href={profileUrl}> 58 {avatarElement} ··· 60 ) : ( 61 avatarElement 62 )} 63 + {displayNameElement} 64 + 65 + {handleElement} 66 + <KnownFollowers 67 + viewer={props.profile.viewer} 68 + did={props.profile.did} 69 + /> 70 + 71 + <pre className="profileDescription pt-1 px-3 sm:px-4 whitespace-pre-wrap"> 72 + {profileRecord.description 73 + ? parseDescription(profileRecord.description) 74 + : null} 75 + </pre> 76 </div> 77 + 78 + <div className="profilePublicationCards w-full overflow-x-scroll"> 79 <div 80 className={`grid grid-flow-col gap-2 mx-auto w-fit px-3 sm:px-4 ${props.popover ? "auto-cols-[164px]" : "auto-cols-[164px] sm:auto-cols-[240px]"}`} 81 > ··· 95 96 const ProfileLinks = (props: { handle: string }) => { 97 return ( 98 + <div className="profileLinks absolute sm:top-4 top-3 sm:right-4 right-3 flex flex-row gap-2"> 99 <a 100 className="text-tertiary hover:text-accent-contrast hover:no-underline!" 101 href={`https://bsky.app/profile/${props.handle}`} ··· 115 return ( 116 <a 117 href={`https://${record.base_path}`} 118 + className="profilePublicationCard border border-border p-2 rounded-lg hover:no-underline! text-primary basis-1/2 " 119 style={{ backgroundColor: `rgb(${colorToString(bgLeaflet, "rgb")})` }} 120 > 121 <div ··· 216 ? urlWithoutProtocol.slice(0, 50) + "โ€ฆ" 217 : urlWithoutProtocol; 218 parts.push( 219 + <a 220 + key={key++} 221 + href={match.href} 222 + target="_blank" 223 + rel="noopener noreferrer" 224 + > 225 {displayText} 226 </a>, 227 ); ··· 237 238 return parts; 239 } 240 + 241 + const KnownFollowers = (props: { 242 + viewer: ProfileViewDetailed["viewer"]; 243 + did: string; 244 + }) => { 245 + if (!props.viewer?.knownFollowers) return null; 246 + let count = props.viewer.knownFollowers.count; 247 + 248 + return ( 249 + <> 250 + <div className="profileKnownFollowers sm:px-4 px-3 text-xs text-tertiary italic"> 251 + Followed by{" "} 252 + <a 253 + className="hover:underline" 254 + href={`https://bsky.app/profile/${props.did}/known-followers`} 255 + target="_blank" 256 + > 257 + {props.viewer?.knownFollowers?.followers[0]?.displayName}{" "} 258 + {count > 1 ? `and ${count - 1} other${count > 2 ? "s" : ""}` : ""} 259 + </a> 260 + </div> 261 + </> 262 + ); 263 + };
+1 -1
app/(home-pages)/p/[didOrHandle]/ProfileTabs.tsx
··· 41 const bgColor = cardBorderHidden ? "var(--bg-leaflet)" : "var(--bg-page)"; 42 43 return ( 44 - <div className="flex flex-col w-full sticky top-3 sm:top-4 z-20 sm:px-4 px-3"> 45 <div 46 style={ 47 scrollPosWithinTabContent < 20
··· 41 const bgColor = cardBorderHidden ? "var(--bg-leaflet)" : "var(--bg-page)"; 42 43 return ( 44 + <div className="flex flex-col w-full sticky top-3 sm:top-4 z-20 sm:px-4 px-3 pt-6"> 45 <div 46 style={ 47 scrollPosWithinTabContent < 20
+1 -1
app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx
··· 6 import { PubLeafletComment, PubLeafletDocument } from "lexicons/api"; 7 import { ReplyTiny } from "components/Icons/ReplyTiny"; 8 import { Avatar } from "components/Avatar"; 9 - import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/BaseTextBlock"; 10 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 11 import { 12 getProfileComments,
··· 6 import { PubLeafletComment, PubLeafletDocument } from "lexicons/api"; 7 import { ReplyTiny } from "components/Icons/ReplyTiny"; 8 import { Avatar } from "components/Avatar"; 9 + import { BaseTextBlock } from "app/lish/[did]/[publication]/[rkey]/Blocks/BaseTextBlock"; 10 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 11 import { 12 getProfileComments,
+24
app/[leaflet_id]/actions/HelpButton.tsx
··· 58 keys={[metaKey(), isMac() ? "Ctrl" : "Meta", "X"]} 59 /> 60 <KeyboardShortcut name="Inline Link" keys={[metaKey(), "K"]} /> 61 62 <Label>Block Shortcuts</Label> 63 {/* shift + up/down arrows (or click + drag): select multiple blocks */}
··· 58 keys={[metaKey(), isMac() ? "Ctrl" : "Meta", "X"]} 59 /> 60 <KeyboardShortcut name="Inline Link" keys={[metaKey(), "K"]} /> 61 + <KeyboardShortcut 62 + name="Make Title" 63 + keys={[metaKey(), isMac() ? "Opt" : "Alt", "1"]} 64 + /> 65 + <KeyboardShortcut 66 + name="Make Heading" 67 + keys={[metaKey(), isMac() ? "Opt" : "Alt", "2"]} 68 + /> 69 + <KeyboardShortcut 70 + name="Make Subheading" 71 + keys={[metaKey(), isMac() ? "Opt" : "Alt", "3"]} 72 + /> 73 + <KeyboardShortcut 74 + name="Regular Text" 75 + keys={[metaKey(), isMac() ? "Opt" : "Alt", "0"]} 76 + /> 77 + <KeyboardShortcut 78 + name="Large Text" 79 + keys={[metaKey(), isMac() ? "Opt" : "Alt", "+"]} 80 + /> 81 + <KeyboardShortcut 82 + name="Small Text" 83 + keys={[metaKey(), isMac() ? "Opt" : "Alt", "-"]} 84 + /> 85 86 <Label>Block Shortcuts</Label> 87 {/* shift + up/down arrows (or click + drag): select multiple blocks */}
+3 -1
app/[leaflet_id]/actions/PublishButton.tsx
··· 136 content: ( 137 <div> 138 {pub.doc ? "Updated! " : "Published! "} 139 - <SpeedyLink href={docUrl}>link</SpeedyLink> 140 </div> 141 ), 142 type: "success",
··· 136 content: ( 137 <div> 138 {pub.doc ? "Updated! " : "Published! "} 139 + <SpeedyLink className="underline" href={docUrl}> 140 + See Published Post 141 + </SpeedyLink> 142 </div> 143 ), 144 type: "success",
+5 -1
app/[leaflet_id]/publish/PublishPost.tsx
··· 199 className="place-self-end h-[30px]" 200 disabled={charCount > 300} 201 > 202 - {isLoading ? <DotLoader /> : "Publish this Post!"} 203 </ButtonPrimary> 204 </div> 205 {oauthError && (
··· 199 className="place-self-end h-[30px]" 200 disabled={charCount > 300} 201 > 202 + {isLoading ? ( 203 + <DotLoader className="h-[23px]" /> 204 + ) : ( 205 + "Publish this Post!" 206 + )} 207 </ButtonPrimary> 208 </div> 209 {oauthError && (
-26
app/lish/[did]/[publication]/[rkey]/BaseTextBlock.tsx
··· 1 - import { ProfilePopover } from "components/ProfilePopover"; 2 - import { TextBlockCore, TextBlockCoreProps, RichText } from "./TextBlockCore"; 3 - import { ReactNode } from "react"; 4 - 5 - // Re-export RichText for backwards compatibility 6 - export { RichText }; 7 - 8 - function DidMentionWithPopover(props: { did: string; children: ReactNode }) { 9 - return ( 10 - <ProfilePopover 11 - didOrHandle={props.did} 12 - trigger={props.children} 13 - /> 14 - ); 15 - } 16 - 17 - export function BaseTextBlock(props: Omit<TextBlockCoreProps, "renderers">) { 18 - return ( 19 - <TextBlockCore 20 - {...props} 21 - renderers={{ 22 - DidMention: DidMentionWithPopover, 23 - }} 24 - /> 25 - ); 26 - }
···
+25
app/lish/[did]/[publication]/[rkey]/Blocks/BaseTextBlock.tsx
···
··· 1 + import { ProfilePopover } from "components/ProfilePopover"; 2 + import { 3 + TextBlockCore, 4 + TextBlockCoreProps, 5 + RichText, 6 + } from "../Blocks/TextBlockCore"; 7 + import { ReactNode } from "react"; 8 + 9 + // Re-export RichText for backwards compatibility 10 + export { RichText }; 11 + 12 + function DidMentionWithPopover(props: { did: string; children: ReactNode }) { 13 + return <ProfilePopover didOrHandle={props.did} trigger={props.children} />; 14 + } 15 + 16 + export function BaseTextBlock(props: Omit<TextBlockCoreProps, "renderers">) { 17 + return ( 18 + <TextBlockCore 19 + {...props} 20 + renderers={{ 21 + DidMention: DidMentionWithPopover, 22 + }} 23 + /> 24 + ); 25 + }
+28
app/lish/[did]/[publication]/[rkey]/Blocks/PubCodeBlock.tsx
···
··· 1 + "use client"; 2 + 3 + import { PubLeafletBlocksCode } from "lexicons/api"; 4 + import { useLayoutEffect, useState } from "react"; 5 + import { codeToHtml, bundledLanguagesInfo, bundledThemesInfo } from "shiki"; 6 + 7 + export function PubCodeBlock({ 8 + block, 9 + prerenderedCode, 10 + }: { 11 + block: PubLeafletBlocksCode.Main; 12 + prerenderedCode?: string; 13 + }) { 14 + const [html, setHTML] = useState<string | null>(prerenderedCode || null); 15 + 16 + useLayoutEffect(() => { 17 + const lang = bundledLanguagesInfo.find((l) => l.id === block.language)?.id || "plaintext"; 18 + const theme = bundledThemesInfo.find((t) => t.id === block.syntaxHighlightingTheme)?.id || "github-light"; 19 + 20 + codeToHtml(block.plaintext, { lang, theme }).then(setHTML); 21 + }, [block]); 22 + return ( 23 + <div 24 + className="w-full min-h-[42px] my-2 rounded-md border-border-light outline-border-light selected-outline" 25 + dangerouslySetInnerHTML={{ __html: html || "" }} 26 + /> 27 + ); 28 + }
+174
app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx
···
··· 1 + import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 2 + import { AppBskyFeedDefs, AppBskyFeedPost } from "@atproto/api"; 3 + import { Separator } from "components/Layout"; 4 + import { useHasPageLoaded } from "components/InitialPageLoadProvider"; 5 + import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 6 + import { CommentTiny } from "components/Icons/CommentTiny"; 7 + import { QuoteTiny } from "components/Icons/QuoteTiny"; 8 + import { ThreadLink, QuotesLink } from "../PostLinks"; 9 + import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 10 + import { 11 + BlueskyEmbed, 12 + PostNotAvailable, 13 + } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; 14 + import { BlueskyRichText } from "components/Blocks/BlueskyPostBlock/BlueskyRichText"; 15 + import { openPage } from "../PostPages"; 16 + 17 + export const PubBlueskyPostBlock = (props: { 18 + post: PostView; 19 + className: string; 20 + pageId?: string; 21 + }) => { 22 + let post = props.post; 23 + 24 + const handleOpenThread = () => { 25 + openPage(props.pageId ? { type: "doc", id: props.pageId } : undefined, { 26 + type: "thread", 27 + uri: post.uri, 28 + }); 29 + }; 30 + 31 + switch (true) { 32 + case AppBskyFeedDefs.isBlockedPost(post) || 33 + AppBskyFeedDefs.isBlockedAuthor(post) || 34 + AppBskyFeedDefs.isNotFoundPost(post): 35 + return ( 36 + <div className={`w-full`}> 37 + <PostNotAvailable /> 38 + </div> 39 + ); 40 + 41 + case AppBskyFeedDefs.validatePostView(post).success: 42 + let record = post.record as AppBskyFeedDefs.PostView["record"]; 43 + 44 + // silliness to get the text and timestamp from the record with proper types 45 + let timestamp: string | undefined = undefined; 46 + if (AppBskyFeedPost.isRecord(record)) { 47 + timestamp = (record as AppBskyFeedPost.Record).createdAt; 48 + } 49 + 50 + //getting the url to the post 51 + let postId = post.uri.split("/")[4]; 52 + let url = `https://bsky.app/profile/${post.author.handle}/post/${postId}`; 53 + 54 + const parent = props.pageId 55 + ? { type: "doc" as const, id: props.pageId } 56 + : undefined; 57 + 58 + return ( 59 + <div 60 + onClick={handleOpenThread} 61 + className={` 62 + ${props.className} 63 + block-border 64 + mb-2 65 + flex flex-col gap-2 relative w-full overflow-hidden group/blueskyPostBlock sm:p-3 p-2 text-sm text-secondary bg-bg-page 66 + cursor-pointer hover:border-accent-contrast 67 + `} 68 + > 69 + {post.author && record && ( 70 + <> 71 + <div className="bskyAuthor w-full flex items-center gap-2"> 72 + {post.author.avatar && ( 73 + <img 74 + src={post.author?.avatar} 75 + alt={`${post.author?.displayName}'s avatar`} 76 + className="shink-0 w-8 h-8 rounded-full border border-border-light" 77 + /> 78 + )} 79 + <div className="grow flex flex-col gap-0.5 leading-tight"> 80 + <div className=" font-bold text-secondary"> 81 + {post.author?.displayName} 82 + </div> 83 + <a 84 + className="text-xs text-tertiary hover:underline" 85 + target="_blank" 86 + href={`https://bsky.app/profile/${post.author?.handle}`} 87 + onClick={(e) => e.stopPropagation()} 88 + > 89 + @{post.author?.handle} 90 + </a> 91 + </div> 92 + </div> 93 + 94 + <div className="flex flex-col gap-2 "> 95 + <div> 96 + <pre className="whitespace-pre-wrap"> 97 + {BlueskyRichText({ 98 + record: record as AppBskyFeedPost.Record | null, 99 + })} 100 + </pre> 101 + </div> 102 + {post.embed && ( 103 + <div onClick={(e) => e.stopPropagation()}> 104 + <BlueskyEmbed embed={post.embed} postUrl={url} /> 105 + </div> 106 + )} 107 + </div> 108 + </> 109 + )} 110 + <div className="w-full flex gap-2 items-center justify-between"> 111 + <ClientDate date={timestamp} /> 112 + <div className="flex gap-2 items-center"> 113 + {post.replyCount != null && post.replyCount > 0 && ( 114 + <> 115 + <ThreadLink 116 + threadUri={post.uri} 117 + parent={parent} 118 + className="flex items-center gap-1 hover:text-accent-contrast" 119 + onClick={(e) => e.stopPropagation()} 120 + > 121 + {post.replyCount} 122 + <CommentTiny /> 123 + </ThreadLink> 124 + <Separator classname="h-4" /> 125 + </> 126 + )} 127 + {post.quoteCount != null && post.quoteCount > 0 && ( 128 + <> 129 + <QuotesLink 130 + postUri={post.uri} 131 + parent={parent} 132 + className="flex items-center gap-1 hover:text-accent-contrast" 133 + onClick={(e) => e.stopPropagation()} 134 + > 135 + {post.quoteCount} 136 + <QuoteTiny /> 137 + </QuotesLink> 138 + <Separator classname="h-4" /> 139 + </> 140 + )} 141 + 142 + <a 143 + className="" 144 + target="_blank" 145 + href={url} 146 + onClick={(e) => e.stopPropagation()} 147 + > 148 + <BlueskyTiny /> 149 + </a> 150 + </div> 151 + </div> 152 + </div> 153 + ); 154 + } 155 + }; 156 + 157 + const ClientDate = (props: { date?: string }) => { 158 + let pageLoaded = useHasPageLoaded(); 159 + const formattedDate = useLocalizedDate( 160 + props.date || new Date().toISOString(), 161 + { 162 + month: "short", 163 + day: "numeric", 164 + year: "numeric", 165 + hour: "numeric", 166 + minute: "numeric", 167 + hour12: true, 168 + }, 169 + ); 170 + 171 + if (!pageLoaded) return null; 172 + 173 + return <div className="text-xs text-tertiary">{formattedDate}</div>; 174 + };
+344
app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPageBlock.tsx
···
··· 1 + "use client"; 2 + 3 + import { useEntity, useReplicache } from "src/replicache"; 4 + import { useUIState } from "src/useUIState"; 5 + import { CSSProperties, useContext, useRef } from "react"; 6 + import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 7 + import { PostContent, Block } from "../PostContent"; 8 + import { 9 + PubLeafletBlocksHeader, 10 + PubLeafletBlocksText, 11 + PubLeafletComment, 12 + PubLeafletPagesLinearDocument, 13 + PubLeafletPagesCanvas, 14 + PubLeafletPublication, 15 + } from "lexicons/api"; 16 + import { AppBskyFeedDefs } from "@atproto/api"; 17 + import { TextBlock } from "./TextBlock"; 18 + import { PostPageContext } from "../PostPageContext"; 19 + import { openPage, useOpenPages } from "../PostPages"; 20 + import { 21 + openInteractionDrawer, 22 + setInteractionState, 23 + useInteractionState, 24 + } from "../Interactions/Interactions"; 25 + import { CommentTiny } from "components/Icons/CommentTiny"; 26 + import { QuoteTiny } from "components/Icons/QuoteTiny"; 27 + import { CanvasBackgroundPattern } from "components/Canvas"; 28 + 29 + export function PublishedPageLinkBlock(props: { 30 + blocks: PubLeafletPagesLinearDocument.Block[] | PubLeafletPagesCanvas.Block[]; 31 + parentPageId: string | undefined; 32 + pageId: string; 33 + did: string; 34 + preview?: boolean; 35 + className?: string; 36 + prerenderedCodeBlocks?: Map<string, string>; 37 + bskyPostData: AppBskyFeedDefs.PostView[]; 38 + isCanvas?: boolean; 39 + pages?: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 40 + }) { 41 + //switch to use actually state 42 + let openPages = useOpenPages(); 43 + let isOpen = openPages.some((p) => p.type === "doc" && p.id === props.pageId); 44 + return ( 45 + <div 46 + className={`w-full cursor-pointer 47 + pageLinkBlockWrapper relative group/pageLinkBlock 48 + bg-bg-page shadow-sm 49 + flex overflow-clip 50 + block-border 51 + ${isOpen && "!border-tertiary"} 52 + ${props.className} 53 + `} 54 + onClick={(e) => { 55 + if (e.isDefaultPrevented()) return; 56 + if (e.shiftKey) return; 57 + e.preventDefault(); 58 + e.stopPropagation(); 59 + 60 + openPage( 61 + props.parentPageId 62 + ? { type: "doc", id: props.parentPageId } 63 + : undefined, 64 + { type: "doc", id: props.pageId }, 65 + ); 66 + }} 67 + > 68 + {props.isCanvas ? ( 69 + <CanvasLinkBlock 70 + blocks={props.blocks as PubLeafletPagesCanvas.Block[]} 71 + did={props.did} 72 + pageId={props.pageId} 73 + bskyPostData={props.bskyPostData} 74 + pages={props.pages || []} 75 + /> 76 + ) : ( 77 + <DocLinkBlock 78 + {...props} 79 + blocks={props.blocks as PubLeafletPagesLinearDocument.Block[]} 80 + /> 81 + )} 82 + </div> 83 + ); 84 + } 85 + export function DocLinkBlock(props: { 86 + blocks: PubLeafletPagesLinearDocument.Block[]; 87 + pageId: string; 88 + parentPageId?: string; 89 + did: string; 90 + preview?: boolean; 91 + className?: string; 92 + prerenderedCodeBlocks?: Map<string, string>; 93 + bskyPostData: AppBskyFeedDefs.PostView[]; 94 + }) { 95 + let [title, description] = props.blocks 96 + .map((b) => b.block) 97 + .filter( 98 + (b) => PubLeafletBlocksText.isMain(b) || PubLeafletBlocksHeader.isMain(b), 99 + ); 100 + 101 + return ( 102 + <div 103 + style={{ "--list-marker-width": "20px" } as CSSProperties} 104 + className={` 105 + w-full h-[104px] 106 + `} 107 + > 108 + <> 109 + <div className="pageLinkBlockContent w-full flex overflow-clip cursor-pointer h-full"> 110 + <div className="my-2 ml-3 grow min-w-0 text-sm bg-transparent overflow-clip flex flex-col "> 111 + <div className="grow"> 112 + {title && ( 113 + <div 114 + className={`pageBlockOne outline-none resize-none align-top gap-2 ${title.$type === "pub.leaflet.blocks.header" ? "font-bold text-base" : ""}`} 115 + > 116 + <TextBlock 117 + facets={title.facets} 118 + plaintext={title.plaintext} 119 + index={[]} 120 + preview 121 + /> 122 + </div> 123 + )} 124 + {description && ( 125 + <div 126 + className={`pageBlockLineTwo outline-none resize-none align-top gap-2 ${description.$type === "pub.leaflet.blocks.header" ? "font-bold" : ""}`} 127 + > 128 + <TextBlock 129 + facets={description.facets} 130 + plaintext={description.plaintext} 131 + index={[]} 132 + preview 133 + /> 134 + </div> 135 + )} 136 + </div> 137 + 138 + <Interactions 139 + pageId={props.pageId} 140 + parentPageId={props.parentPageId} 141 + /> 142 + </div> 143 + {!props.preview && ( 144 + <PagePreview blocks={props.blocks} did={props.did} /> 145 + )} 146 + </div> 147 + </> 148 + </div> 149 + ); 150 + } 151 + 152 + export function PagePreview(props: { 153 + did: string; 154 + blocks: PubLeafletPagesLinearDocument.Block[]; 155 + }) { 156 + let previewRef = useRef<HTMLDivElement | null>(null); 157 + let { rootEntity } = useReplicache(); 158 + let data = useContext(PostPageContext); 159 + let theme = data?.theme; 160 + let pageWidth = `var(--page-width-unitless)`; 161 + let cardBorderHidden = !theme?.showPageBackground; 162 + return ( 163 + <div 164 + ref={previewRef} 165 + className={`pageLinkBlockPreview w-[120px] overflow-clip mx-3 mt-3 -mb-2 border rounded-md shrink-0 border-border-light flex flex-col gap-0.5 rotate-[4deg] origin-center ${cardBorderHidden ? "" : "bg-bg-page"}`} 166 + > 167 + <div 168 + className="absolute top-0 left-0 origin-top-left pointer-events-none " 169 + style={{ 170 + width: `calc(1px * ${pageWidth})`, 171 + height: `calc(100vh - 64px)`, 172 + transform: `scale(calc((120 / ${pageWidth} )))`, 173 + backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))", 174 + }} 175 + > 176 + {!cardBorderHidden && ( 177 + <div 178 + className={`pageLinkBlockBackground 179 + absolute top-0 left-0 right-0 bottom-0 180 + pointer-events-none 181 + `} 182 + /> 183 + )} 184 + <PostContent 185 + pollData={[]} 186 + pages={[]} 187 + did={props.did} 188 + blocks={props.blocks} 189 + preview 190 + bskyPostData={[]} 191 + /> 192 + </div> 193 + </div> 194 + ); 195 + } 196 + 197 + const Interactions = (props: { pageId: string; parentPageId?: string }) => { 198 + const data = useContext(PostPageContext); 199 + const document_uri = data?.uri; 200 + if (!document_uri) 201 + throw new Error("document_uri not available in PostPageContext"); 202 + let comments = data.comments_on_documents.filter( 203 + (c) => (c.record as PubLeafletComment.Record)?.onPage === props.pageId, 204 + ).length; 205 + let quotes = data.document_mentions_in_bsky.filter((q) => 206 + q.link.includes(props.pageId), 207 + ).length; 208 + 209 + let { drawerOpen, drawer, pageId } = useInteractionState(document_uri); 210 + 211 + return ( 212 + <div 213 + className={`flex gap-2 text-tertiary text-sm absolute bottom-2 bg-bg-page`} 214 + > 215 + {quotes > 0 && ( 216 + <button 217 + className={`flex gap-1 items-center`} 218 + onClick={(e) => { 219 + e.preventDefault(); 220 + e.stopPropagation(); 221 + openPage( 222 + props.parentPageId 223 + ? { type: "doc", id: props.parentPageId } 224 + : undefined, 225 + { type: "doc", id: props.pageId }, 226 + { scrollIntoView: false }, 227 + ); 228 + if (!drawerOpen || drawer !== "quotes") 229 + openInteractionDrawer("quotes", document_uri, props.pageId); 230 + else setInteractionState(document_uri, { drawerOpen: false }); 231 + }} 232 + > 233 + <span className="sr-only">Page quotes</span> 234 + <QuoteTiny aria-hidden /> {quotes}{" "} 235 + </button> 236 + )} 237 + {comments > 0 && ( 238 + <button 239 + className={`flex gap-1 items-center`} 240 + onClick={(e) => { 241 + e.preventDefault(); 242 + e.stopPropagation(); 243 + openPage( 244 + props.parentPageId 245 + ? { type: "doc", id: props.parentPageId } 246 + : undefined, 247 + { type: "doc", id: props.pageId }, 248 + { scrollIntoView: false }, 249 + ); 250 + if (!drawerOpen || drawer !== "comments" || pageId !== props.pageId) 251 + openInteractionDrawer("comments", document_uri, props.pageId); 252 + else setInteractionState(document_uri, { drawerOpen: false }); 253 + }} 254 + > 255 + <span className="sr-only">Page comments</span> 256 + <CommentTiny aria-hidden /> {comments}{" "} 257 + </button> 258 + )} 259 + </div> 260 + ); 261 + }; 262 + 263 + const CanvasLinkBlock = (props: { 264 + blocks: PubLeafletPagesCanvas.Block[]; 265 + did: string; 266 + pageId: string; 267 + bskyPostData: AppBskyFeedDefs.PostView[]; 268 + pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 269 + }) => { 270 + let pageWidth = `var(--page-width-unitless)`; 271 + let height = 272 + props.blocks.length > 0 ? Math.max(...props.blocks.map((b) => b.y), 0) : 0; 273 + 274 + return ( 275 + <div 276 + style={{ contain: "size layout paint" }} 277 + className={`pageLinkBlockPreview shrink-0 h-[200px] w-full overflow-clip relative`} 278 + > 279 + <div 280 + className={`absolute top-0 left-0 origin-top-left pointer-events-none w-full`} 281 + style={{ 282 + width: `calc(1px * ${pageWidth})`, 283 + height: "calc(1150px * 2)", 284 + transform: `scale(calc(((${pageWidth} - 36) / 1272 )))`, 285 + }} 286 + > 287 + <div 288 + style={{ 289 + minHeight: height + 512, 290 + contain: "size layout paint", 291 + }} 292 + className="relative h-full w-[1272px]" 293 + > 294 + <div className="w-full h-full pointer-events-none"> 295 + <CanvasBackgroundPattern pattern="grid" /> 296 + </div> 297 + {props.blocks 298 + .sort((a, b) => { 299 + if (a.y === b.y) { 300 + return a.x - b.x; 301 + } 302 + return a.y - b.y; 303 + }) 304 + .map((canvasBlock, index) => { 305 + let { x, y, width, rotation } = canvasBlock; 306 + let transform = `translate(${x}px, ${y}px)${rotation ? ` rotate(${rotation}deg)` : ""}`; 307 + 308 + // Wrap the block in a LinearDocument.Block structure for compatibility 309 + let linearBlock: PubLeafletPagesLinearDocument.Block = { 310 + $type: "pub.leaflet.pages.linearDocument#block", 311 + block: canvasBlock.block, 312 + }; 313 + 314 + return ( 315 + <div 316 + key={index} 317 + className="absolute rounded-lg flex items-stretch origin-center p-3" 318 + style={{ 319 + top: 0, 320 + left: 0, 321 + width, 322 + transform, 323 + }} 324 + > 325 + <div className="contents"> 326 + <Block 327 + pollData={[]} 328 + pageId={props.pageId} 329 + pages={props.pages} 330 + bskyPostData={props.bskyPostData} 331 + block={linearBlock} 332 + did={props.did} 333 + index={[index]} 334 + preview={true} 335 + /> 336 + </div> 337 + </div> 338 + ); 339 + })} 340 + </div> 341 + </div> 342 + </div> 343 + ); 344 + };
+346
app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPollBlock.tsx
···
··· 1 + "use client"; 2 + 3 + import { 4 + PubLeafletBlocksPoll, 5 + PubLeafletPollDefinition, 6 + PubLeafletPollVote, 7 + } from "lexicons/api"; 8 + import { useState, useEffect } from "react"; 9 + import { ButtonPrimary, ButtonSecondary } from "components/Buttons"; 10 + import { useIdentityData } from "components/IdentityProvider"; 11 + import { AtpAgent } from "@atproto/api"; 12 + import { voteOnPublishedPoll } from "../voteOnPublishedPoll"; 13 + import { PollData } from "../fetchPollData"; 14 + import { Popover } from "components/Popover"; 15 + import LoginForm from "app/login/LoginForm"; 16 + import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 17 + import { getVoterIdentities, VoterIdentity } from "../getVoterIdentities"; 18 + import { Json } from "supabase/database.types"; 19 + import { InfoSmall } from "components/Icons/InfoSmall"; 20 + 21 + // Helper function to extract the first option from a vote record 22 + const getVoteOption = (voteRecord: any): string | null => { 23 + try { 24 + const record = voteRecord as PubLeafletPollVote.Record; 25 + return record.option && record.option.length > 0 ? record.option[0] : null; 26 + } catch { 27 + return null; 28 + } 29 + }; 30 + 31 + export const PublishedPollBlock = (props: { 32 + block: PubLeafletBlocksPoll.Main; 33 + pollData: PollData; 34 + className?: string; 35 + }) => { 36 + const { identity } = useIdentityData(); 37 + const [selectedOption, setSelectedOption] = useState<string | null>(null); 38 + const [isVoting, setIsVoting] = useState(false); 39 + const [showResults, setShowResults] = useState(false); 40 + const [optimisticVote, setOptimisticVote] = useState<{ 41 + option: string; 42 + voter_did: string; 43 + } | null>(null); 44 + let pollRecord = props.pollData.record as PubLeafletPollDefinition.Record; 45 + let [isClient, setIsClient] = useState(false); 46 + useEffect(() => { 47 + setIsClient(true); 48 + }, []); 49 + 50 + const handleVote = async () => { 51 + if (!selectedOption || !identity?.atp_did) return; 52 + 53 + setIsVoting(true); 54 + 55 + // Optimistically add the vote 56 + setOptimisticVote({ 57 + option: selectedOption, 58 + voter_did: identity.atp_did, 59 + }); 60 + setShowResults(true); 61 + 62 + try { 63 + const result = await voteOnPublishedPoll( 64 + props.block.pollRef.uri, 65 + props.block.pollRef.cid, 66 + selectedOption, 67 + ); 68 + 69 + if (!result.success) { 70 + console.error("Failed to vote:", result.error); 71 + // Revert optimistic update on failure 72 + setOptimisticVote(null); 73 + setShowResults(false); 74 + } 75 + } catch (error) { 76 + console.error("Failed to vote:", error); 77 + // Revert optimistic update on failure 78 + setOptimisticVote(null); 79 + setShowResults(false); 80 + } finally { 81 + setIsVoting(false); 82 + } 83 + }; 84 + 85 + const hasVoted = 86 + !!identity?.atp_did && 87 + (!!props.pollData?.atp_poll_votes.find( 88 + (v) => v.voter_did === identity?.atp_did, 89 + ) || 90 + !!optimisticVote); 91 + let isCreator = 92 + identity?.atp_did && props.pollData.uri.includes(identity?.atp_did); 93 + const displayResults = showResults || hasVoted; 94 + 95 + return ( 96 + <div 97 + className={`poll flex flex-col gap-2 p-3 w-full ${props.className} block-border`} 98 + style={{ 99 + backgroundColor: 100 + "color-mix(in oklab, rgb(var(--accent-1)), rgb(var(--bg-page)) 85%)", 101 + }} 102 + > 103 + {displayResults ? ( 104 + <> 105 + <PollResults 106 + pollData={props.pollData} 107 + hasVoted={hasVoted} 108 + setShowResults={setShowResults} 109 + optimisticVote={optimisticVote} 110 + /> 111 + {!hasVoted && ( 112 + <div className="flex justify-start"> 113 + <button 114 + className="w-fit flex gap-2 items-center justify-start text-sm text-accent-contrast" 115 + onClick={() => setShowResults(false)} 116 + > 117 + Back to Voting 118 + </button> 119 + </div> 120 + )} 121 + </> 122 + ) : ( 123 + <> 124 + {pollRecord.options.map((option, index) => ( 125 + <PollOptionButton 126 + key={index} 127 + option={option} 128 + optionIndex={index.toString()} 129 + selected={selectedOption === index.toString()} 130 + onSelect={() => setSelectedOption(index.toString())} 131 + disabled={!identity?.atp_did} 132 + /> 133 + ))} 134 + <div className="flex flex-col-reverse sm:flex-row sm:justify-between gap-2 items-center pt-2"> 135 + <div className="text-sm text-tertiary">All votes are public</div> 136 + <div className="flex sm:gap-3 sm:flex-row flex-col-reverse sm:justify-end justify-center gap-1 items-center"> 137 + <button 138 + className="w-fit font-bold text-accent-contrast" 139 + onClick={() => setShowResults(!showResults)} 140 + > 141 + See Results 142 + </button> 143 + {identity?.atp_did ? ( 144 + <ButtonPrimary 145 + className="place-self-end" 146 + onClick={handleVote} 147 + disabled={!selectedOption || isVoting} 148 + > 149 + {isVoting ? "Voting..." : "Vote!"} 150 + </ButtonPrimary> 151 + ) : ( 152 + <Popover 153 + asChild 154 + trigger={ 155 + <ButtonPrimary className="place-self-center"> 156 + <BlueskyTiny /> Login to vote 157 + </ButtonPrimary> 158 + } 159 + > 160 + {isClient && ( 161 + <LoginForm 162 + text="Log in to vote on this poll!" 163 + noEmail 164 + redirectRoute={window?.location.href + "?refreshAuth"} 165 + /> 166 + )} 167 + </Popover> 168 + )} 169 + </div> 170 + </div> 171 + </> 172 + )} 173 + </div> 174 + ); 175 + }; 176 + 177 + const PollOptionButton = (props: { 178 + option: PubLeafletPollDefinition.Option; 179 + optionIndex: string; 180 + selected: boolean; 181 + onSelect: () => void; 182 + disabled?: boolean; 183 + }) => { 184 + const ButtonComponent = props.selected ? ButtonPrimary : ButtonSecondary; 185 + 186 + return ( 187 + <div className="flex gap-2 items-center"> 188 + <ButtonComponent 189 + className="pollOption grow max-w-full flex" 190 + onClick={props.onSelect} 191 + disabled={props.disabled} 192 + > 193 + {props.option.text} 194 + </ButtonComponent> 195 + </div> 196 + ); 197 + }; 198 + 199 + const PollResults = (props: { 200 + pollData: PollData; 201 + hasVoted: boolean; 202 + setShowResults: (show: boolean) => void; 203 + optimisticVote: { option: string; voter_did: string } | null; 204 + }) => { 205 + // Merge optimistic vote with actual votes 206 + const allVotes = props.optimisticVote 207 + ? [ 208 + ...props.pollData.atp_poll_votes, 209 + { 210 + voter_did: props.optimisticVote.voter_did, 211 + record: { 212 + $type: "pub.leaflet.poll.vote", 213 + option: [props.optimisticVote.option], 214 + }, 215 + }, 216 + ] 217 + : props.pollData.atp_poll_votes; 218 + 219 + const totalVotes = allVotes.length || 0; 220 + let pollRecord = props.pollData.record as PubLeafletPollDefinition.Record; 221 + let optionsWithCount = pollRecord.options.map((o, index) => ({ 222 + ...o, 223 + votes: allVotes.filter((v) => getVoteOption(v.record) == index.toString()), 224 + })); 225 + 226 + const highestVotes = Math.max(...optionsWithCount.map((o) => o.votes.length)); 227 + return ( 228 + <> 229 + {pollRecord.options.map((option, index) => { 230 + const voteRecords = allVotes.filter( 231 + (v) => getVoteOption(v.record) === index.toString(), 232 + ); 233 + const isWinner = totalVotes > 0 && voteRecords.length === highestVotes; 234 + 235 + return ( 236 + <PollResult 237 + key={index} 238 + option={option} 239 + votes={voteRecords.length} 240 + voteRecords={voteRecords} 241 + totalVotes={totalVotes} 242 + winner={isWinner} 243 + /> 244 + ); 245 + })} 246 + </> 247 + ); 248 + }; 249 + 250 + const VoterListPopover = (props: { 251 + votes: number; 252 + voteRecords: { voter_did: string; record: Json }[]; 253 + }) => { 254 + const [voterIdentities, setVoterIdentities] = useState<VoterIdentity[]>([]); 255 + const [isLoading, setIsLoading] = useState(false); 256 + const [hasFetched, setHasFetched] = useState(false); 257 + 258 + const handleOpenChange = async () => { 259 + if (!hasFetched && props.voteRecords.length > 0) { 260 + setIsLoading(true); 261 + setHasFetched(true); 262 + try { 263 + const dids = props.voteRecords.map((v) => v.voter_did); 264 + const identities = await getVoterIdentities(dids); 265 + setVoterIdentities(identities); 266 + } catch (error) { 267 + console.error("Failed to fetch voter identities:", error); 268 + } finally { 269 + setIsLoading(false); 270 + } 271 + } 272 + }; 273 + 274 + return ( 275 + <Popover 276 + trigger={ 277 + <button 278 + className="hover:underline cursor-pointer" 279 + disabled={props.votes === 0} 280 + > 281 + {props.votes} 282 + </button> 283 + } 284 + onOpenChange={handleOpenChange} 285 + className="w-64 max-h-80" 286 + > 287 + {isLoading ? ( 288 + <div className="flex justify-center py-4"> 289 + <div className="text-sm text-secondary">Loading...</div> 290 + </div> 291 + ) : ( 292 + <div className="flex flex-col gap-1 text-sm py-0.5"> 293 + {voterIdentities.map((voter) => ( 294 + <a 295 + key={voter.did} 296 + href={`https://bsky.app/profile/${voter.handle || voter.did}`} 297 + target="_blank" 298 + rel="noopener noreferrer" 299 + className="" 300 + > 301 + @{voter.handle || voter.did} 302 + </a> 303 + ))} 304 + </div> 305 + )} 306 + </Popover> 307 + ); 308 + }; 309 + 310 + const PollResult = (props: { 311 + option: PubLeafletPollDefinition.Option; 312 + votes: number; 313 + voteRecords: { voter_did: string; record: Json }[]; 314 + totalVotes: number; 315 + winner: boolean; 316 + }) => { 317 + return ( 318 + <div 319 + className={`pollResult relative grow py-0.5 px-2 border-accent-contrast rounded-md overflow-hidden ${props.winner ? "font-bold border-2" : "border"}`} 320 + > 321 + <div 322 + style={{ 323 + WebkitTextStroke: `${props.winner ? "6px" : "6px"} rgb(var(--bg-page))`, 324 + paintOrder: "stroke fill", 325 + }} 326 + className="pollResultContent text-accent-contrast relative flex gap-2 justify-between z-10" 327 + > 328 + <div className="grow max-w-full truncate">{props.option.text}</div> 329 + <VoterListPopover votes={props.votes} voteRecords={props.voteRecords} /> 330 + </div> 331 + <div className="pollResultBG absolute bg-bg-page w-full top-0 bottom-0 left-0 right-0 flex flex-row z-0"> 332 + <div 333 + className="bg-accent-contrast rounded-[2px] m-0.5" 334 + style={{ 335 + maskImage: "var(--hatchSVG)", 336 + maskRepeat: "repeat repeat", 337 + ...(props.votes === 0 338 + ? { width: "4px" } 339 + : { flexBasis: `${(props.votes / props.totalVotes) * 100}%` }), 340 + }} 341 + /> 342 + <div /> 343 + </div> 344 + </div> 345 + ); 346 + };
+20
app/lish/[did]/[publication]/[rkey]/Blocks/StaticMathBlock.tsx
···
··· 1 + import { PubLeafletBlocksMath } from "lexicons/api"; 2 + import Katex from "katex"; 3 + import "katex/dist/katex.min.css"; 4 + 5 + export const StaticMathBlock = ({ 6 + block, 7 + }: { 8 + block: PubLeafletBlocksMath.Main; 9 + }) => { 10 + const html = Katex.renderToString(block.tex, { 11 + displayMode: true, 12 + output: "html", 13 + throwOnError: false, 14 + }); 15 + return ( 16 + <div className="math-block my-2"> 17 + <div dangerouslySetInnerHTML={{ __html: html }} /> 18 + </div> 19 + ); 20 + };
+95
app/lish/[did]/[publication]/[rkey]/Blocks/TextBlock.tsx
···
··· 1 + "use client"; 2 + import { UnicodeString } from "@atproto/api"; 3 + import { PubLeafletRichtextFacet } from "lexicons/api"; 4 + import { useMemo } from "react"; 5 + import { useHighlight } from "../useHighlight"; 6 + import { BaseTextBlock } from "./BaseTextBlock"; 7 + 8 + type Facet = PubLeafletRichtextFacet.Main; 9 + export function TextBlock(props: { 10 + plaintext: string; 11 + facets?: Facet[]; 12 + index: number[]; 13 + preview?: boolean; 14 + pageId?: string; 15 + }) { 16 + let children = []; 17 + let highlights = useHighlight(props.index, props.pageId); 18 + let facets = useMemo(() => { 19 + if (props.preview) return props.facets; 20 + let facets = [...(props.facets || [])]; 21 + for (let highlight of highlights) { 22 + const fragmentId = props.pageId 23 + ? `${props.pageId}~${props.index.join(".")}_${highlight.startOffset || 0}` 24 + : `${props.index.join(".")}_${highlight.startOffset || 0}`; 25 + facets = addFacet( 26 + facets, 27 + { 28 + index: { 29 + byteStart: highlight.startOffset 30 + ? new UnicodeString( 31 + props.plaintext.slice(0, highlight.startOffset), 32 + ).length 33 + : 0, 34 + byteEnd: new UnicodeString( 35 + props.plaintext.slice(0, highlight.endOffset ?? undefined), 36 + ).length, 37 + }, 38 + features: [ 39 + { $type: "pub.leaflet.richtext.facet#highlight" }, 40 + { 41 + $type: "pub.leaflet.richtext.facet#id", 42 + id: fragmentId, 43 + }, 44 + ], 45 + }, 46 + new UnicodeString(props.plaintext).length, 47 + ); 48 + } 49 + return facets; 50 + }, [props.plaintext, props.facets, highlights, props.preview, props.pageId]); 51 + return <BaseTextBlock {...props} facets={facets} />; 52 + } 53 + 54 + function addFacet(facets: Facet[], newFacet: Facet, length: number) { 55 + if (facets.length === 0) { 56 + return [newFacet]; 57 + } 58 + 59 + const allFacets = [...facets, newFacet]; 60 + 61 + // Collect all boundary positions 62 + const boundaries = new Set<number>(); 63 + boundaries.add(0); 64 + boundaries.add(length); 65 + 66 + for (const facet of allFacets) { 67 + boundaries.add(facet.index.byteStart); 68 + boundaries.add(facet.index.byteEnd); 69 + } 70 + 71 + const sortedBoundaries = Array.from(boundaries).sort((a, b) => a - b); 72 + const result: Facet[] = []; 73 + 74 + // Process segments between consecutive boundaries 75 + for (let i = 0; i < sortedBoundaries.length - 1; i++) { 76 + const start = sortedBoundaries[i]; 77 + const end = sortedBoundaries[i + 1]; 78 + 79 + // Find facets that are active at the start position 80 + const activeFacets = allFacets.filter( 81 + (facet) => facet.index.byteStart <= start && facet.index.byteEnd > start, 82 + ); 83 + 84 + // Only create facet if there are active facets (features present) 85 + if (activeFacets.length > 0) { 86 + const features = activeFacets.flatMap((f) => f.features); 87 + result.push({ 88 + index: { byteStart: start, byteEnd: end }, 89 + features, 90 + }); 91 + } 92 + } 93 + 94 + return result; 95 + }
+181
app/lish/[did]/[publication]/[rkey]/Blocks/TextBlockCore.tsx
···
··· 1 + import { UnicodeString } from "@atproto/api"; 2 + import { PubLeafletRichtextFacet } from "lexicons/api"; 3 + import { AtMentionLink } from "components/AtMentionLink"; 4 + import { ReactNode } from "react"; 5 + 6 + type Facet = PubLeafletRichtextFacet.Main; 7 + 8 + export type FacetRenderers = { 9 + DidMention?: (props: { did: string; children: ReactNode }) => ReactNode; 10 + }; 11 + 12 + export type TextBlockCoreProps = { 13 + plaintext: string; 14 + facets?: Facet[]; 15 + index: number[]; 16 + preview?: boolean; 17 + renderers?: FacetRenderers; 18 + }; 19 + 20 + export function TextBlockCore(props: TextBlockCoreProps) { 21 + let children = []; 22 + let richText = new RichText({ 23 + text: props.plaintext, 24 + facets: props.facets || [], 25 + }); 26 + let counter = 0; 27 + for (const segment of richText.segments()) { 28 + let id = segment.facet?.find(PubLeafletRichtextFacet.isId); 29 + let link = segment.facet?.find(PubLeafletRichtextFacet.isLink); 30 + let isBold = segment.facet?.find(PubLeafletRichtextFacet.isBold); 31 + let isCode = segment.facet?.find(PubLeafletRichtextFacet.isCode); 32 + let isStrikethrough = segment.facet?.find( 33 + PubLeafletRichtextFacet.isStrikethrough, 34 + ); 35 + let isDidMention = segment.facet?.find( 36 + PubLeafletRichtextFacet.isDidMention, 37 + ); 38 + let isAtMention = segment.facet?.find(PubLeafletRichtextFacet.isAtMention); 39 + let isUnderline = segment.facet?.find(PubLeafletRichtextFacet.isUnderline); 40 + let isItalic = segment.facet?.find(PubLeafletRichtextFacet.isItalic); 41 + let isHighlighted = segment.facet?.find( 42 + PubLeafletRichtextFacet.isHighlight, 43 + ); 44 + let className = ` 45 + ${isCode ? "inline-code" : ""} 46 + ${id ? "scroll-mt-12 scroll-mb-10" : ""} 47 + ${isBold ? "font-bold" : ""} 48 + ${isItalic ? "italic" : ""} 49 + ${isUnderline ? "underline" : ""} 50 + ${isStrikethrough ? "line-through decoration-tertiary" : ""} 51 + ${isHighlighted ? "highlight bg-highlight-1" : ""}`.replaceAll("\n", " "); 52 + 53 + // Split text by newlines and insert <br> tags 54 + const textParts = segment.text.split("\n"); 55 + const renderedText = textParts.flatMap((part, i) => 56 + i < textParts.length - 1 57 + ? [part, <br key={`br-${counter}-${i}`} />] 58 + : [part], 59 + ); 60 + 61 + if (isCode) { 62 + children.push( 63 + <code key={counter} className={className} id={id?.id}> 64 + {renderedText} 65 + </code>, 66 + ); 67 + } else if (isDidMention) { 68 + const DidMentionRenderer = props.renderers?.DidMention; 69 + if (DidMentionRenderer) { 70 + children.push( 71 + <DidMentionRenderer key={counter} did={isDidMention.did}> 72 + <span className="mention">{renderedText}</span> 73 + </DidMentionRenderer>, 74 + ); 75 + } else { 76 + // Default: render as a simple link 77 + children.push( 78 + <a 79 + key={counter} 80 + href={`https://leaflet.pub/p/${isDidMention.did}`} 81 + target="_blank" 82 + className="no-underline" 83 + > 84 + <span className="mention">{renderedText}</span> 85 + </a>, 86 + ); 87 + } 88 + } else if (isAtMention) { 89 + children.push( 90 + <AtMentionLink 91 + key={counter} 92 + atURI={isAtMention.atURI} 93 + className={className} 94 + > 95 + {renderedText} 96 + </AtMentionLink>, 97 + ); 98 + } else if (link) { 99 + children.push( 100 + <a 101 + key={counter} 102 + href={link.uri.trim()} 103 + className={`text-accent-contrast hover:underline ${className}`} 104 + target="_blank" 105 + > 106 + {renderedText} 107 + </a>, 108 + ); 109 + } else { 110 + children.push( 111 + <span key={counter} className={className} id={id?.id}> 112 + {renderedText} 113 + </span>, 114 + ); 115 + } 116 + 117 + counter++; 118 + } 119 + return <>{children}</>; 120 + } 121 + 122 + type RichTextSegment = { 123 + text: string; 124 + facet?: Exclude<Facet["features"], { $type: string }>; 125 + }; 126 + 127 + export class RichText { 128 + unicodeText: UnicodeString; 129 + facets?: Facet[]; 130 + 131 + constructor(props: { text: string; facets: Facet[] }) { 132 + this.unicodeText = new UnicodeString(props.text); 133 + this.facets = props.facets; 134 + if (this.facets) { 135 + this.facets = this.facets 136 + .filter((facet) => facet.index.byteStart <= facet.index.byteEnd) 137 + .sort((a, b) => a.index.byteStart - b.index.byteStart); 138 + } 139 + } 140 + 141 + *segments(): Generator<RichTextSegment, void, void> { 142 + const facets = this.facets || []; 143 + if (!facets.length) { 144 + yield { text: this.unicodeText.utf16 }; 145 + return; 146 + } 147 + 148 + let textCursor = 0; 149 + let facetCursor = 0; 150 + do { 151 + const currFacet = facets[facetCursor]; 152 + if (textCursor < currFacet.index.byteStart) { 153 + yield { 154 + text: this.unicodeText.slice(textCursor, currFacet.index.byteStart), 155 + }; 156 + } else if (textCursor > currFacet.index.byteStart) { 157 + facetCursor++; 158 + continue; 159 + } 160 + if (currFacet.index.byteStart < currFacet.index.byteEnd) { 161 + const subtext = this.unicodeText.slice( 162 + currFacet.index.byteStart, 163 + currFacet.index.byteEnd, 164 + ); 165 + if (!subtext.trim()) { 166 + // dont empty string entities 167 + yield { text: subtext }; 168 + } else { 169 + yield { text: subtext, facet: currFacet.features }; 170 + } 171 + } 172 + textCursor = currFacet.index.byteEnd; 173 + facetCursor++; 174 + } while (facetCursor < facets.length); 175 + if (textCursor < this.unicodeText.length) { 176 + yield { 177 + text: this.unicodeText.slice(textCursor, this.unicodeText.length), 178 + }; 179 + } 180 + } 181 + }
+6 -1
app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx
··· 202 isSubpage: boolean | undefined; 203 data: PostPageData; 204 profile: ProfileViewDetailed; 205 - preferences: { showComments?: boolean }; 206 quotesCount: number | undefined; 207 commentsCount: number | undefined; 208 }) => { ··· 213 quotesCount={props.quotesCount || 0} 214 commentsCount={props.commentsCount || 0} 215 showComments={props.preferences.showComments} 216 pageId={props.pageId} 217 /> 218 {!props.isSubpage && (
··· 202 isSubpage: boolean | undefined; 203 data: PostPageData; 204 profile: ProfileViewDetailed; 205 + preferences: { 206 + showComments?: boolean; 207 + showMentions?: boolean; 208 + showPrevNext?: boolean; 209 + }; 210 quotesCount: number | undefined; 211 commentsCount: number | undefined; 212 }) => { ··· 217 quotesCount={props.quotesCount || 0} 218 commentsCount={props.commentsCount || 0} 219 showComments={props.preferences.showComments} 220 + showMentions={props.preferences.showMentions} 221 pageId={props.pageId} 222 /> 223 {!props.isSubpage && (
+5 -2
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
··· 5 import { CommentBox } from "./CommentBox"; 6 import { Json } from "supabase/database.types"; 7 import { PubLeafletComment } from "lexicons/api"; 8 - import { BaseTextBlock } from "../../BaseTextBlock"; 9 import { useMemo, useState } from "react"; 10 import { CommentTiny } from "components/Icons/CommentTiny"; 11 import { Separator } from "components/Layout"; ··· 51 }, []); 52 53 return ( 54 - <div id={"commentsDrawer"} className="flex flex-col gap-2 relative"> 55 <div className="w-full flex justify-between text-secondary font-bold"> 56 Comments 57 <button
··· 5 import { CommentBox } from "./CommentBox"; 6 import { Json } from "supabase/database.types"; 7 import { PubLeafletComment } from "lexicons/api"; 8 + import { BaseTextBlock } from "../../Blocks/BaseTextBlock"; 9 import { useMemo, useState } from "react"; 10 import { CommentTiny } from "components/Icons/CommentTiny"; 11 import { Separator } from "components/Layout"; ··· 51 }, []); 52 53 return ( 54 + <div 55 + id={"commentsDrawer"} 56 + className="flex flex-col gap-2 relative text-sm text-secondary" 57 + > 58 <div className="w-full flex justify-between text-secondary font-bold"> 59 Comments 60 <button
+3 -2
app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx
··· 9 import { decodeQuotePosition } from "../quotePosition"; 10 11 export const InteractionDrawer = (props: { 12 document_uri: string; 13 quotesAndMentions: { uri: string; link?: string }[]; 14 comments: Comment[]; ··· 35 return ( 36 <> 37 <SandwichSpacer noWidth /> 38 - <div className="snap-center h-full flex z-10 shrink-0 w-[calc(var(--page-width-units)-6px)] sm:w-[calc(var(--page-width-units))]"> 39 <div 40 id="interaction-drawer" 41 - className="opaque-container rounded-l-none! rounded-r-lg! h-full w-full px-3 sm:px-4 pt-2 sm:pt-3 pb-6 overflow-scroll -ml-[1px] " 42 > 43 {drawer.drawer === "quotes" ? ( 44 <Quotes {...props} quotesAndMentions={filteredQuotesAndMentions} />
··· 9 import { decodeQuotePosition } from "../quotePosition"; 10 11 export const InteractionDrawer = (props: { 12 + showPageBackground: boolean | undefined; 13 document_uri: string; 14 quotesAndMentions: { uri: string; link?: string }[]; 15 comments: Comment[]; ··· 36 return ( 37 <> 38 <SandwichSpacer noWidth /> 39 + <div className="snap-center h-full flex z-10 shrink-0 sm:max-w-prose sm:w-full w-[calc(100vw-12px)]"> 40 <div 41 id="interaction-drawer" 42 + className={`opaque-container h-full w-full px-3 sm:px-4 pt-2 sm:pt-3 pb-6 overflow-scroll ${props.showPageBackground ? "rounded-l-none! rounded-r-lg! -ml-[1px]" : "rounded-lg! sm:ml-4"}`} 43 > 44 {drawer.drawer === "quotes" ? ( 45 <Quotes {...props} quotesAndMentions={filteredQuotesAndMentions} />
+68 -44
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
··· 108 commentsCount: number; 109 className?: string; 110 showComments?: boolean; 111 pageId?: string; 112 }) => { 113 const data = useContext(PostPageContext); ··· 131 <div className={`flex gap-2 text-tertiary text-sm ${props.className}`}> 132 {tagCount > 0 && <TagPopover tags={tags} tagCount={tagCount} />} 133 134 - {props.quotesCount > 0 && ( 135 <button 136 className="flex w-fit gap-2 items-center" 137 onClick={() => { ··· 168 commentsCount: number; 169 className?: string; 170 showComments?: boolean; 171 pageId?: string; 172 }) => { 173 const data = useContext(PostPageContext); ··· 189 const tags = (data?.data as any)?.tags as string[] | undefined; 190 const tagCount = tags?.length || 0; 191 192 let subscribed = 193 identity?.atp_did && 194 publication?.publication_subscriptions && ··· 229 <TagList tags={tags} className="mb-3" /> 230 </> 231 )} 232 <hr className="border-border-light mb-3 " /> 233 <div className="flex gap-2 justify-between"> 234 - <div className="flex gap-2"> 235 - {props.quotesCount > 0 && ( 236 - <button 237 - className="flex w-fit gap-2 items-center px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 238 - onClick={() => { 239 - if (!drawerOpen || drawer !== "quotes") 240 - openInteractionDrawer("quotes", document_uri, props.pageId); 241 - else setInteractionState(document_uri, { drawerOpen: false }); 242 - }} 243 - onMouseEnter={handleQuotePrefetch} 244 - onTouchStart={handleQuotePrefetch} 245 - aria-label="Post quotes" 246 - > 247 - <QuoteTiny aria-hidden /> {props.quotesCount}{" "} 248 - <span 249 - aria-hidden 250 - >{`Mention${props.quotesCount === 1 ? "" : "s"}`}</span> 251 - </button> 252 - )} 253 - {props.showComments === false ? null : ( 254 - <button 255 - className="flex gap-2 items-center w-fit px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 256 - onClick={() => { 257 - if ( 258 - !drawerOpen || 259 - drawer !== "comments" || 260 - pageId !== props.pageId 261 - ) 262 - openInteractionDrawer("comments", document_uri, props.pageId); 263 - else setInteractionState(document_uri, { drawerOpen: false }); 264 - }} 265 - aria-label="Post comments" 266 - > 267 - <CommentTiny aria-hidden />{" "} 268 - {props.commentsCount > 0 ? ( 269 - <span aria-hidden> 270 - {`${props.commentsCount} Comment${props.commentsCount === 1 ? "" : "s"}`} 271 - </span> 272 - ) : ( 273 - "Comment" 274 )} 275 - </button> 276 - )} 277 - </div> 278 <EditButton document={data} /> 279 {subscribed && publication && ( 280 <ManageSubscription
··· 108 commentsCount: number; 109 className?: string; 110 showComments?: boolean; 111 + showMentions?: boolean; 112 pageId?: string; 113 }) => { 114 const data = useContext(PostPageContext); ··· 132 <div className={`flex gap-2 text-tertiary text-sm ${props.className}`}> 133 {tagCount > 0 && <TagPopover tags={tags} tagCount={tagCount} />} 134 135 + {props.quotesCount === 0 || props.showMentions === false ? null : ( 136 <button 137 className="flex w-fit gap-2 items-center" 138 onClick={() => { ··· 169 commentsCount: number; 170 className?: string; 171 showComments?: boolean; 172 + showMentions?: boolean; 173 pageId?: string; 174 }) => { 175 const data = useContext(PostPageContext); ··· 191 const tags = (data?.data as any)?.tags as string[] | undefined; 192 const tagCount = tags?.length || 0; 193 194 + let noInteractions = !props.showComments && !props.showMentions; 195 + 196 let subscribed = 197 identity?.atp_did && 198 publication?.publication_subscriptions && ··· 233 <TagList tags={tags} className="mb-3" /> 234 </> 235 )} 236 + 237 <hr className="border-border-light mb-3 " /> 238 + 239 <div className="flex gap-2 justify-between"> 240 + {noInteractions ? ( 241 + <div /> 242 + ) : ( 243 + <> 244 + <div className="flex gap-2"> 245 + {props.quotesCount === 0 || 246 + props.showMentions === false ? null : ( 247 + <button 248 + className="flex w-fit gap-2 items-center px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 249 + onClick={() => { 250 + if (!drawerOpen || drawer !== "quotes") 251 + openInteractionDrawer( 252 + "quotes", 253 + document_uri, 254 + props.pageId, 255 + ); 256 + else 257 + setInteractionState(document_uri, { drawerOpen: false }); 258 + }} 259 + onMouseEnter={handleQuotePrefetch} 260 + onTouchStart={handleQuotePrefetch} 261 + aria-label="Post quotes" 262 + > 263 + <QuoteTiny aria-hidden /> {props.quotesCount}{" "} 264 + <span 265 + aria-hidden 266 + >{`Mention${props.quotesCount === 1 ? "" : "s"}`}</span> 267 + </button> 268 )} 269 + {props.showComments === false ? null : ( 270 + <button 271 + className="flex gap-2 items-center w-fit px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 272 + onClick={() => { 273 + if ( 274 + !drawerOpen || 275 + drawer !== "comments" || 276 + pageId !== props.pageId 277 + ) 278 + openInteractionDrawer( 279 + "comments", 280 + document_uri, 281 + props.pageId, 282 + ); 283 + else 284 + setInteractionState(document_uri, { drawerOpen: false }); 285 + }} 286 + aria-label="Post comments" 287 + > 288 + <CommentTiny aria-hidden />{" "} 289 + {props.commentsCount > 0 ? ( 290 + <span aria-hidden> 291 + {`${props.commentsCount} Comment${props.commentsCount === 1 ? "" : "s"}`} 292 + </span> 293 + ) : ( 294 + "Comment" 295 + )} 296 + </button> 297 + )} 298 + </div> 299 + </> 300 + )} 301 + 302 <EditButton document={data} /> 303 {subscribed && publication && ( 304 <ManageSubscription
+7 -2
app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx
··· 14 ExpandedInteractions, 15 getCommentCount, 16 getQuoteCount, 17 - Interactions, 18 } from "./Interactions/Interactions"; 19 import { PostContent } from "./PostContent"; 20 import { PostHeader } from "./PostHeader/PostHeader"; ··· 25 import { decodeQuotePosition } from "./quotePosition"; 26 import { PollData } from "./fetchPollData"; 27 import { SharedPageProps } from "./PostPages"; 28 29 export function LinearDocumentPage({ 30 blocks, ··· 56 57 const isSubpage = !!pageId; 58 59 return ( 60 <> 61 <PageWrapper ··· 83 did={did} 84 prerenderedCodeBlocks={prerenderedCodeBlocks} 85 /> 86 - 87 <ExpandedInteractions 88 pageId={pageId} 89 showComments={preferences.showComments} 90 commentsCount={getCommentCount(document, pageId) || 0} 91 quotesCount={getQuoteCount(document, pageId) || 0} 92 />
··· 14 ExpandedInteractions, 15 getCommentCount, 16 getQuoteCount, 17 } from "./Interactions/Interactions"; 18 import { PostContent } from "./PostContent"; 19 import { PostHeader } from "./PostHeader/PostHeader"; ··· 24 import { decodeQuotePosition } from "./quotePosition"; 25 import { PollData } from "./fetchPollData"; 26 import { SharedPageProps } from "./PostPages"; 27 + import { PostPrevNextButtons } from "./PostPrevNextButtons"; 28 29 export function LinearDocumentPage({ 30 blocks, ··· 56 57 const isSubpage = !!pageId; 58 59 + console.log("prev/next?: " + preferences.showPrevNext); 60 + 61 return ( 62 <> 63 <PageWrapper ··· 85 did={did} 86 prerenderedCodeBlocks={prerenderedCodeBlocks} 87 /> 88 + <PostPrevNextButtons 89 + showPrevNext={preferences.showPrevNext && !isSubpage} 90 + /> 91 <ExpandedInteractions 92 pageId={pageId} 93 showComments={preferences.showComments} 94 + showMentions={preferences.showMentions} 95 commentsCount={getCommentCount(document, pageId) || 0} 96 quotesCount={getQuoteCount(document, pageId) || 0} 97 />
+18 -8
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 20 } from "lexicons/api"; 21 22 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 23 - import { TextBlock } from "./TextBlock"; 24 import { Popover } from "components/Popover"; 25 import { theme } from "tailwind.config"; 26 import { ImageAltSmall } from "components/Icons/ImageAlt"; 27 - import { StaticMathBlock } from "./StaticMathBlock"; 28 - import { PubCodeBlock } from "./PubCodeBlock"; 29 import { AppBskyFeedDefs } from "@atproto/api"; 30 - import { PubBlueskyPostBlock } from "./PublishBskyPostBlock"; 31 import { openPage } from "./PostPages"; 32 import { PageLinkBlock } from "components/Blocks/PageLinkBlock"; 33 - import { PublishedPageLinkBlock } from "./PublishedPageBlock"; 34 - import { PublishedPollBlock } from "./PublishedPollBlock"; 35 import { PollData } from "./fetchPollData"; 36 import { ButtonPrimary } from "components/Buttons"; 37 ··· 173 let uri = b.block.postRef.uri; 174 let post = bskyPostData.find((p) => p.uri === uri); 175 if (!post) return <div>no prefetched post rip</div>; 176 - return <PubBlueskyPostBlock post={post} className={className} pageId={pageId} />; 177 } 178 case PubLeafletBlocksIframe.isMain(b.block): { 179 return ( ··· 339 } 340 case PubLeafletBlocksText.isMain(b.block): 341 return ( 342 - <p className={`textBlock ${className}`} {...blockProps}> 343 <TextBlock 344 facets={b.block.facets} 345 plaintext={b.block.plaintext} ··· 349 /> 350 </p> 351 ); 352 case PubLeafletBlocksHeader.isMain(b.block): { 353 if (b.block.level === 1) 354 return (
··· 20 } from "lexicons/api"; 21 22 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 23 + import { TextBlock } from "./Blocks/TextBlock"; 24 import { Popover } from "components/Popover"; 25 import { theme } from "tailwind.config"; 26 import { ImageAltSmall } from "components/Icons/ImageAlt"; 27 + import { StaticMathBlock } from "./Blocks/StaticMathBlock"; 28 + import { PubCodeBlock } from "./Blocks/PubCodeBlock"; 29 import { AppBskyFeedDefs } from "@atproto/api"; 30 + import { PubBlueskyPostBlock } from "./Blocks/PublishBskyPostBlock"; 31 import { openPage } from "./PostPages"; 32 import { PageLinkBlock } from "components/Blocks/PageLinkBlock"; 33 + import { PublishedPageLinkBlock } from "./Blocks/PublishedPageBlock"; 34 + import { PublishedPollBlock } from "./Blocks/PublishedPollBlock"; 35 import { PollData } from "./fetchPollData"; 36 import { ButtonPrimary } from "components/Buttons"; 37 ··· 173 let uri = b.block.postRef.uri; 174 let post = bskyPostData.find((p) => p.uri === uri); 175 if (!post) return <div>no prefetched post rip</div>; 176 + return ( 177 + <PubBlueskyPostBlock 178 + post={post} 179 + className={className} 180 + pageId={pageId} 181 + /> 182 + ); 183 } 184 case PubLeafletBlocksIframe.isMain(b.block): { 185 return ( ··· 345 } 346 case PubLeafletBlocksText.isMain(b.block): 347 return ( 348 + <p 349 + className={`textBlock ${className} ${b.block.textSize === "small" ? "text-sm text-secondary" : b.block.textSize === "large" ? "text-lg" : ""}`} 350 + {...blockProps} 351 + > 352 <TextBlock 353 facets={b.block.facets} 354 plaintext={b.block.plaintext} ··· 358 /> 359 </p> 360 ); 361 + 362 case PubLeafletBlocksHeader.isMain(b.block): { 363 if (b.block.level === 1) 364 return (
+2 -1
app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx
··· 23 export function PostHeader(props: { 24 data: PostPageData; 25 profile: ProfileViewDetailed; 26 - preferences: { showComments?: boolean }; 27 }) { 28 let { identity } = useIdentityData(); 29 let document = props.data; ··· 91 </div> 92 <Interactions 93 showComments={props.preferences.showComments} 94 quotesCount={getQuoteCount(document) || 0} 95 commentsCount={getCommentCount(document) || 0} 96 />
··· 23 export function PostHeader(props: { 24 data: PostPageData; 25 profile: ProfileViewDetailed; 26 + preferences: { showComments?: boolean; showMentions?: boolean }; 27 }) { 28 let { identity } = useIdentityData(); 29 let document = props.data; ··· 91 </div> 92 <Interactions 93 showComments={props.preferences.showComments} 94 + showMentions={props.preferences.showMentions} 95 quotesCount={getQuoteCount(document) || 0} 96 commentsCount={getCommentCount(document) || 0} 97 />
+22 -4
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 147 document: PostPageData; 148 did: string; 149 profile: ProfileViewDetailed; 150 - preferences: { showComments?: boolean }; 151 pubRecord?: PubLeafletPublication.Record; 152 theme?: PubLeafletPublication.Theme | null; 153 prerenderedCodeBlocks?: Map<string, string>; ··· 206 did: string; 207 prerenderedCodeBlocks?: Map<string, string>; 208 bskyPostData: AppBskyFeedDefs.PostView[]; 209 - preferences: { showComments?: boolean }; 210 pollData: PollData[]; 211 }) { 212 let drawer = useDrawerOpen(document_uri); ··· 261 262 {drawer && !drawer.pageId && ( 263 <InteractionDrawer 264 document_uri={document.uri} 265 comments={ 266 pubRecord?.preferences?.showComments === false 267 ? [] 268 : document.comments_on_documents 269 } 270 - quotesAndMentions={quotesAndMentions} 271 did={did} 272 /> 273 )} ··· 347 /> 348 {drawer && drawer.pageId === page.id && ( 349 <InteractionDrawer 350 pageId={page.id} 351 document_uri={document.uri} 352 comments={ ··· 354 ? [] 355 : document.comments_on_documents 356 } 357 - quotesAndMentions={quotesAndMentions} 358 did={did} 359 /> 360 )}
··· 147 document: PostPageData; 148 did: string; 149 profile: ProfileViewDetailed; 150 + preferences: { 151 + showComments?: boolean; 152 + showMentions?: boolean; 153 + showPrevNext?: boolean; 154 + }; 155 pubRecord?: PubLeafletPublication.Record; 156 theme?: PubLeafletPublication.Theme | null; 157 prerenderedCodeBlocks?: Map<string, string>; ··· 210 did: string; 211 prerenderedCodeBlocks?: Map<string, string>; 212 bskyPostData: AppBskyFeedDefs.PostView[]; 213 + preferences: { 214 + showComments?: boolean; 215 + showMentions?: boolean; 216 + showPrevNext?: boolean; 217 + }; 218 pollData: PollData[]; 219 }) { 220 let drawer = useDrawerOpen(document_uri); ··· 269 270 {drawer && !drawer.pageId && ( 271 <InteractionDrawer 272 + showPageBackground={pubRecord?.theme?.showPageBackground} 273 document_uri={document.uri} 274 comments={ 275 pubRecord?.preferences?.showComments === false 276 ? [] 277 : document.comments_on_documents 278 } 279 + quotesAndMentions={ 280 + pubRecord?.preferences?.showMentions === false 281 + ? [] 282 + : quotesAndMentions 283 + } 284 did={did} 285 /> 286 )} ··· 360 /> 361 {drawer && drawer.pageId === page.id && ( 362 <InteractionDrawer 363 + showPageBackground={pubRecord?.theme?.showPageBackground} 364 pageId={page.id} 365 document_uri={document.uri} 366 comments={ ··· 368 ? [] 369 : document.comments_on_documents 370 } 371 + quotesAndMentions={ 372 + pubRecord?.preferences?.showMentions === false 373 + ? [] 374 + : quotesAndMentions 375 + } 376 did={did} 377 /> 378 )}
+58
app/lish/[did]/[publication]/[rkey]/PostPrevNextButtons.tsx
···
··· 1 + "use client"; 2 + import { PubLeafletDocument } from "lexicons/api"; 3 + import { usePublicationData } from "../dashboard/PublicationSWRProvider"; 4 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 5 + import { AtUri } from "@atproto/api"; 6 + import { useParams } from "next/navigation"; 7 + import { getPostPageData } from "./getPostPageData"; 8 + import { PostPageContext } from "./PostPageContext"; 9 + import { useContext } from "react"; 10 + import { SpeedyLink } from "components/SpeedyLink"; 11 + import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; 12 + 13 + export const PostPrevNextButtons = (props: { 14 + showPrevNext: boolean | undefined; 15 + }) => { 16 + let postData = useContext(PostPageContext); 17 + let pub = postData?.documents_in_publications[0]?.publications; 18 + 19 + if (!props.showPrevNext || !pub || !postData) return; 20 + 21 + function getPostLink(uri: string) { 22 + return pub && uri 23 + ? `${getPublicationURL(pub)}/${new AtUri(uri).rkey}` 24 + : "leaflet.pub/not-found"; 25 + } 26 + let prevPost = postData?.prevNext?.prev; 27 + let nextPost = postData?.prevNext?.next; 28 + 29 + return ( 30 + <div className="flex flex-col gap-1 w-full px-3 sm:px-4 pb-2 pt-2"> 31 + {/*<hr className="border-border-light" />*/} 32 + <div className="flex justify-between w-full gap-8 "> 33 + {nextPost ? ( 34 + <SpeedyLink 35 + href={getPostLink(nextPost.uri)} 36 + className="flex gap-1 items-center truncate min-w-0 basis-1/2" 37 + > 38 + <ArrowRightTiny className="rotate-180 shrink-0" /> 39 + <div className="min-w-0 truncate">{nextPost.title}</div> 40 + </SpeedyLink> 41 + ) : ( 42 + <div /> 43 + )} 44 + {prevPost ? ( 45 + <SpeedyLink 46 + href={getPostLink(prevPost.uri)} 47 + className="flex gap-1 items-center truncate min-w-0 basis-1/2 justify-end" 48 + > 49 + <div className="min-w-0 truncate">{prevPost.title}</div> 50 + <ArrowRightTiny className="shrink-0" /> 51 + </SpeedyLink> 52 + ) : ( 53 + <div /> 54 + )} 55 + </div> 56 + </div> 57 + ); 58 + };
-28
app/lish/[did]/[publication]/[rkey]/PubCodeBlock.tsx
··· 1 - "use client"; 2 - 3 - import { PubLeafletBlocksCode } from "lexicons/api"; 4 - import { useLayoutEffect, useState } from "react"; 5 - import { codeToHtml, bundledLanguagesInfo, bundledThemesInfo } from "shiki"; 6 - 7 - export function PubCodeBlock({ 8 - block, 9 - prerenderedCode, 10 - }: { 11 - block: PubLeafletBlocksCode.Main; 12 - prerenderedCode?: string; 13 - }) { 14 - const [html, setHTML] = useState<string | null>(prerenderedCode || null); 15 - 16 - useLayoutEffect(() => { 17 - const lang = bundledLanguagesInfo.find((l) => l.id === block.language)?.id || "plaintext"; 18 - const theme = bundledThemesInfo.find((t) => t.id === block.syntaxHighlightingTheme)?.id || "github-light"; 19 - 20 - codeToHtml(block.plaintext, { lang, theme }).then(setHTML); 21 - }, [block]); 22 - return ( 23 - <div 24 - className="w-full min-h-[42px] my-2 rounded-md border-border-light outline-border-light selected-outline" 25 - dangerouslySetInnerHTML={{ __html: html || "" }} 26 - /> 27 - ); 28 - }
···
-172
app/lish/[did]/[publication]/[rkey]/PublishBskyPostBlock.tsx
··· 1 - import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 2 - import { AppBskyFeedDefs, AppBskyFeedPost } from "@atproto/api"; 3 - import { Separator } from "components/Layout"; 4 - import { useHasPageLoaded } from "components/InitialPageLoadProvider"; 5 - import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 6 - import { CommentTiny } from "components/Icons/CommentTiny"; 7 - import { QuoteTiny } from "components/Icons/QuoteTiny"; 8 - import { ThreadLink, QuotesLink } from "./PostLinks"; 9 - import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 10 - import { 11 - BlueskyEmbed, 12 - PostNotAvailable, 13 - } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; 14 - import { BlueskyRichText } from "components/Blocks/BlueskyPostBlock/BlueskyRichText"; 15 - import { openPage } from "./PostPages"; 16 - 17 - export const PubBlueskyPostBlock = (props: { 18 - post: PostView; 19 - className: string; 20 - pageId?: string; 21 - }) => { 22 - let post = props.post; 23 - 24 - const handleOpenThread = () => { 25 - openPage( 26 - props.pageId ? { type: "doc", id: props.pageId } : undefined, 27 - { type: "thread", uri: post.uri }, 28 - ); 29 - }; 30 - 31 - switch (true) { 32 - case AppBskyFeedDefs.isBlockedPost(post) || 33 - AppBskyFeedDefs.isBlockedAuthor(post) || 34 - AppBskyFeedDefs.isNotFoundPost(post): 35 - return ( 36 - <div className={`w-full`}> 37 - <PostNotAvailable /> 38 - </div> 39 - ); 40 - 41 - case AppBskyFeedDefs.validatePostView(post).success: 42 - let record = post.record as AppBskyFeedDefs.PostView["record"]; 43 - 44 - // silliness to get the text and timestamp from the record with proper types 45 - let timestamp: string | undefined = undefined; 46 - if (AppBskyFeedPost.isRecord(record)) { 47 - timestamp = (record as AppBskyFeedPost.Record).createdAt; 48 - } 49 - 50 - //getting the url to the post 51 - let postId = post.uri.split("/")[4]; 52 - let url = `https://bsky.app/profile/${post.author.handle}/post/${postId}`; 53 - 54 - const parent = props.pageId ? { type: "doc" as const, id: props.pageId } : undefined; 55 - 56 - return ( 57 - <div 58 - onClick={handleOpenThread} 59 - className={` 60 - ${props.className} 61 - block-border 62 - mb-2 63 - flex flex-col gap-2 relative w-full overflow-hidden group/blueskyPostBlock sm:p-3 p-2 text-sm text-secondary bg-bg-page 64 - cursor-pointer hover:border-accent-contrast 65 - `} 66 - > 67 - {post.author && record && ( 68 - <> 69 - <div className="bskyAuthor w-full flex items-center gap-2"> 70 - {post.author.avatar && ( 71 - <img 72 - src={post.author?.avatar} 73 - alt={`${post.author?.displayName}'s avatar`} 74 - className="shink-0 w-8 h-8 rounded-full border border-border-light" 75 - /> 76 - )} 77 - <div className="grow flex flex-col gap-0.5 leading-tight"> 78 - <div className=" font-bold text-secondary"> 79 - {post.author?.displayName} 80 - </div> 81 - <a 82 - className="text-xs text-tertiary hover:underline" 83 - target="_blank" 84 - href={`https://bsky.app/profile/${post.author?.handle}`} 85 - onClick={(e) => e.stopPropagation()} 86 - > 87 - @{post.author?.handle} 88 - </a> 89 - </div> 90 - </div> 91 - 92 - <div className="flex flex-col gap-2 "> 93 - <div> 94 - <pre className="whitespace-pre-wrap"> 95 - {BlueskyRichText({ 96 - record: record as AppBskyFeedPost.Record | null, 97 - })} 98 - </pre> 99 - </div> 100 - {post.embed && ( 101 - <div onClick={(e) => e.stopPropagation()}> 102 - <BlueskyEmbed embed={post.embed} postUrl={url} /> 103 - </div> 104 - )} 105 - </div> 106 - </> 107 - )} 108 - <div className="w-full flex gap-2 items-center justify-between"> 109 - <ClientDate date={timestamp} /> 110 - <div className="flex gap-2 items-center"> 111 - {post.replyCount != null && post.replyCount > 0 && ( 112 - <> 113 - <ThreadLink 114 - threadUri={post.uri} 115 - parent={parent} 116 - className="flex items-center gap-1 hover:text-accent-contrast" 117 - onClick={(e) => e.stopPropagation()} 118 - > 119 - {post.replyCount} 120 - <CommentTiny /> 121 - </ThreadLink> 122 - <Separator classname="h-4" /> 123 - </> 124 - )} 125 - {post.quoteCount != null && post.quoteCount > 0 && ( 126 - <> 127 - <QuotesLink 128 - postUri={post.uri} 129 - parent={parent} 130 - className="flex items-center gap-1 hover:text-accent-contrast" 131 - onClick={(e) => e.stopPropagation()} 132 - > 133 - {post.quoteCount} 134 - <QuoteTiny /> 135 - </QuotesLink> 136 - <Separator classname="h-4" /> 137 - </> 138 - )} 139 - 140 - <a 141 - className="" 142 - target="_blank" 143 - href={url} 144 - onClick={(e) => e.stopPropagation()} 145 - > 146 - <BlueskyTiny /> 147 - </a> 148 - </div> 149 - </div> 150 - </div> 151 - ); 152 - } 153 - }; 154 - 155 - const ClientDate = (props: { date?: string }) => { 156 - let pageLoaded = useHasPageLoaded(); 157 - const formattedDate = useLocalizedDate( 158 - props.date || new Date().toISOString(), 159 - { 160 - month: "short", 161 - day: "numeric", 162 - year: "numeric", 163 - hour: "numeric", 164 - minute: "numeric", 165 - hour12: true, 166 - }, 167 - ); 168 - 169 - if (!pageLoaded) return null; 170 - 171 - return <div className="text-xs text-tertiary">{formattedDate}</div>; 172 - };
···
-340
app/lish/[did]/[publication]/[rkey]/PublishedPageBlock.tsx
··· 1 - "use client"; 2 - 3 - import { useEntity, useReplicache } from "src/replicache"; 4 - import { useUIState } from "src/useUIState"; 5 - import { CSSProperties, useContext, useRef } from "react"; 6 - import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 7 - import { PostContent, Block } from "./PostContent"; 8 - import { 9 - PubLeafletBlocksHeader, 10 - PubLeafletBlocksText, 11 - PubLeafletComment, 12 - PubLeafletPagesLinearDocument, 13 - PubLeafletPagesCanvas, 14 - PubLeafletPublication, 15 - } from "lexicons/api"; 16 - import { AppBskyFeedDefs } from "@atproto/api"; 17 - import { TextBlock } from "./TextBlock"; 18 - import { PostPageContext } from "./PostPageContext"; 19 - import { openPage, useOpenPages } from "./PostPages"; 20 - import { 21 - openInteractionDrawer, 22 - setInteractionState, 23 - useInteractionState, 24 - } from "./Interactions/Interactions"; 25 - import { CommentTiny } from "components/Icons/CommentTiny"; 26 - import { QuoteTiny } from "components/Icons/QuoteTiny"; 27 - import { CanvasBackgroundPattern } from "components/Canvas"; 28 - 29 - export function PublishedPageLinkBlock(props: { 30 - blocks: PubLeafletPagesLinearDocument.Block[] | PubLeafletPagesCanvas.Block[]; 31 - parentPageId: string | undefined; 32 - pageId: string; 33 - did: string; 34 - preview?: boolean; 35 - className?: string; 36 - prerenderedCodeBlocks?: Map<string, string>; 37 - bskyPostData: AppBskyFeedDefs.PostView[]; 38 - isCanvas?: boolean; 39 - pages?: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 40 - }) { 41 - //switch to use actually state 42 - let openPages = useOpenPages(); 43 - let isOpen = openPages.some( 44 - (p) => p.type === "doc" && p.id === props.pageId, 45 - ); 46 - return ( 47 - <div 48 - className={`w-full cursor-pointer 49 - pageLinkBlockWrapper relative group/pageLinkBlock 50 - bg-bg-page shadow-sm 51 - flex overflow-clip 52 - block-border 53 - ${isOpen && "!border-tertiary"} 54 - ${props.className} 55 - `} 56 - onClick={(e) => { 57 - if (e.isDefaultPrevented()) return; 58 - if (e.shiftKey) return; 59 - e.preventDefault(); 60 - e.stopPropagation(); 61 - 62 - openPage( 63 - props.parentPageId ? { type: "doc", id: props.parentPageId } : undefined, 64 - { type: "doc", id: props.pageId }, 65 - ); 66 - }} 67 - > 68 - {props.isCanvas ? ( 69 - <CanvasLinkBlock 70 - blocks={props.blocks as PubLeafletPagesCanvas.Block[]} 71 - did={props.did} 72 - pageId={props.pageId} 73 - bskyPostData={props.bskyPostData} 74 - pages={props.pages || []} 75 - /> 76 - ) : ( 77 - <DocLinkBlock 78 - {...props} 79 - blocks={props.blocks as PubLeafletPagesLinearDocument.Block[]} 80 - /> 81 - )} 82 - </div> 83 - ); 84 - } 85 - export function DocLinkBlock(props: { 86 - blocks: PubLeafletPagesLinearDocument.Block[]; 87 - pageId: string; 88 - parentPageId?: string; 89 - did: string; 90 - preview?: boolean; 91 - className?: string; 92 - prerenderedCodeBlocks?: Map<string, string>; 93 - bskyPostData: AppBskyFeedDefs.PostView[]; 94 - }) { 95 - let [title, description] = props.blocks 96 - .map((b) => b.block) 97 - .filter( 98 - (b) => PubLeafletBlocksText.isMain(b) || PubLeafletBlocksHeader.isMain(b), 99 - ); 100 - 101 - return ( 102 - <div 103 - style={{ "--list-marker-width": "20px" } as CSSProperties} 104 - className={` 105 - w-full h-[104px] 106 - `} 107 - > 108 - <> 109 - <div className="pageLinkBlockContent w-full flex overflow-clip cursor-pointer h-full"> 110 - <div className="my-2 ml-3 grow min-w-0 text-sm bg-transparent overflow-clip flex flex-col "> 111 - <div className="grow"> 112 - {title && ( 113 - <div 114 - className={`pageBlockOne outline-none resize-none align-top gap-2 ${title.$type === "pub.leaflet.blocks.header" ? "font-bold text-base" : ""}`} 115 - > 116 - <TextBlock 117 - facets={title.facets} 118 - plaintext={title.plaintext} 119 - index={[]} 120 - preview 121 - /> 122 - </div> 123 - )} 124 - {description && ( 125 - <div 126 - className={`pageBlockLineTwo outline-none resize-none align-top gap-2 ${description.$type === "pub.leaflet.blocks.header" ? "font-bold" : ""}`} 127 - > 128 - <TextBlock 129 - facets={description.facets} 130 - plaintext={description.plaintext} 131 - index={[]} 132 - preview 133 - /> 134 - </div> 135 - )} 136 - </div> 137 - 138 - <Interactions 139 - pageId={props.pageId} 140 - parentPageId={props.parentPageId} 141 - /> 142 - </div> 143 - {!props.preview && ( 144 - <PagePreview blocks={props.blocks} did={props.did} /> 145 - )} 146 - </div> 147 - </> 148 - </div> 149 - ); 150 - } 151 - 152 - export function PagePreview(props: { 153 - did: string; 154 - blocks: PubLeafletPagesLinearDocument.Block[]; 155 - }) { 156 - let previewRef = useRef<HTMLDivElement | null>(null); 157 - let { rootEntity } = useReplicache(); 158 - let data = useContext(PostPageContext); 159 - let theme = data?.theme; 160 - let pageWidth = `var(--page-width-unitless)`; 161 - let cardBorderHidden = !theme?.showPageBackground; 162 - return ( 163 - <div 164 - ref={previewRef} 165 - className={`pageLinkBlockPreview w-[120px] overflow-clip mx-3 mt-3 -mb-2 border rounded-md shrink-0 border-border-light flex flex-col gap-0.5 rotate-[4deg] origin-center ${cardBorderHidden ? "" : "bg-bg-page"}`} 166 - > 167 - <div 168 - className="absolute top-0 left-0 origin-top-left pointer-events-none " 169 - style={{ 170 - width: `calc(1px * ${pageWidth})`, 171 - height: `calc(100vh - 64px)`, 172 - transform: `scale(calc((120 / ${pageWidth} )))`, 173 - backgroundColor: "rgba(var(--bg-page), var(--bg-page-alpha))", 174 - }} 175 - > 176 - {!cardBorderHidden && ( 177 - <div 178 - className={`pageLinkBlockBackground 179 - absolute top-0 left-0 right-0 bottom-0 180 - pointer-events-none 181 - `} 182 - /> 183 - )} 184 - <PostContent 185 - pollData={[]} 186 - pages={[]} 187 - did={props.did} 188 - blocks={props.blocks} 189 - preview 190 - bskyPostData={[]} 191 - /> 192 - </div> 193 - </div> 194 - ); 195 - } 196 - 197 - const Interactions = (props: { pageId: string; parentPageId?: string }) => { 198 - const data = useContext(PostPageContext); 199 - const document_uri = data?.uri; 200 - if (!document_uri) 201 - throw new Error("document_uri not available in PostPageContext"); 202 - let comments = data.comments_on_documents.filter( 203 - (c) => (c.record as PubLeafletComment.Record)?.onPage === props.pageId, 204 - ).length; 205 - let quotes = data.document_mentions_in_bsky.filter((q) => 206 - q.link.includes(props.pageId), 207 - ).length; 208 - 209 - let { drawerOpen, drawer, pageId } = useInteractionState(document_uri); 210 - 211 - return ( 212 - <div 213 - className={`flex gap-2 text-tertiary text-sm absolute bottom-2 bg-bg-page`} 214 - > 215 - {quotes > 0 && ( 216 - <button 217 - className={`flex gap-1 items-center`} 218 - onClick={(e) => { 219 - e.preventDefault(); 220 - e.stopPropagation(); 221 - openPage( 222 - props.parentPageId ? { type: "doc", id: props.parentPageId } : undefined, 223 - { type: "doc", id: props.pageId }, 224 - { scrollIntoView: false }, 225 - ); 226 - if (!drawerOpen || drawer !== "quotes") 227 - openInteractionDrawer("quotes", document_uri, props.pageId); 228 - else setInteractionState(document_uri, { drawerOpen: false }); 229 - }} 230 - > 231 - <span className="sr-only">Page quotes</span> 232 - <QuoteTiny aria-hidden /> {quotes}{" "} 233 - </button> 234 - )} 235 - {comments > 0 && ( 236 - <button 237 - className={`flex gap-1 items-center`} 238 - onClick={(e) => { 239 - e.preventDefault(); 240 - e.stopPropagation(); 241 - openPage( 242 - props.parentPageId ? { type: "doc", id: props.parentPageId } : undefined, 243 - { type: "doc", id: props.pageId }, 244 - { scrollIntoView: false }, 245 - ); 246 - if (!drawerOpen || drawer !== "comments" || pageId !== props.pageId) 247 - openInteractionDrawer("comments", document_uri, props.pageId); 248 - else setInteractionState(document_uri, { drawerOpen: false }); 249 - }} 250 - > 251 - <span className="sr-only">Page comments</span> 252 - <CommentTiny aria-hidden /> {comments}{" "} 253 - </button> 254 - )} 255 - </div> 256 - ); 257 - }; 258 - 259 - const CanvasLinkBlock = (props: { 260 - blocks: PubLeafletPagesCanvas.Block[]; 261 - did: string; 262 - pageId: string; 263 - bskyPostData: AppBskyFeedDefs.PostView[]; 264 - pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 265 - }) => { 266 - let pageWidth = `var(--page-width-unitless)`; 267 - let height = 268 - props.blocks.length > 0 ? Math.max(...props.blocks.map((b) => b.y), 0) : 0; 269 - 270 - return ( 271 - <div 272 - style={{ contain: "size layout paint" }} 273 - className={`pageLinkBlockPreview shrink-0 h-[200px] w-full overflow-clip relative`} 274 - > 275 - <div 276 - className={`absolute top-0 left-0 origin-top-left pointer-events-none w-full`} 277 - style={{ 278 - width: `calc(1px * ${pageWidth})`, 279 - height: "calc(1150px * 2)", 280 - transform: `scale(calc(((${pageWidth} - 36) / 1272 )))`, 281 - }} 282 - > 283 - <div 284 - style={{ 285 - minHeight: height + 512, 286 - contain: "size layout paint", 287 - }} 288 - className="relative h-full w-[1272px]" 289 - > 290 - <div className="w-full h-full pointer-events-none"> 291 - <CanvasBackgroundPattern pattern="grid" /> 292 - </div> 293 - {props.blocks 294 - .sort((a, b) => { 295 - if (a.y === b.y) { 296 - return a.x - b.x; 297 - } 298 - return a.y - b.y; 299 - }) 300 - .map((canvasBlock, index) => { 301 - let { x, y, width, rotation } = canvasBlock; 302 - let transform = `translate(${x}px, ${y}px)${rotation ? ` rotate(${rotation}deg)` : ""}`; 303 - 304 - // Wrap the block in a LinearDocument.Block structure for compatibility 305 - let linearBlock: PubLeafletPagesLinearDocument.Block = { 306 - $type: "pub.leaflet.pages.linearDocument#block", 307 - block: canvasBlock.block, 308 - }; 309 - 310 - return ( 311 - <div 312 - key={index} 313 - className="absolute rounded-lg flex items-stretch origin-center p-3" 314 - style={{ 315 - top: 0, 316 - left: 0, 317 - width, 318 - transform, 319 - }} 320 - > 321 - <div className="contents"> 322 - <Block 323 - pollData={[]} 324 - pageId={props.pageId} 325 - pages={props.pages} 326 - bskyPostData={props.bskyPostData} 327 - block={linearBlock} 328 - did={props.did} 329 - index={[index]} 330 - preview={true} 331 - /> 332 - </div> 333 - </div> 334 - ); 335 - })} 336 - </div> 337 - </div> 338 - </div> 339 - ); 340 - };
···
-346
app/lish/[did]/[publication]/[rkey]/PublishedPollBlock.tsx
··· 1 - "use client"; 2 - 3 - import { 4 - PubLeafletBlocksPoll, 5 - PubLeafletPollDefinition, 6 - PubLeafletPollVote, 7 - } from "lexicons/api"; 8 - import { useState, useEffect } from "react"; 9 - import { ButtonPrimary, ButtonSecondary } from "components/Buttons"; 10 - import { useIdentityData } from "components/IdentityProvider"; 11 - import { AtpAgent } from "@atproto/api"; 12 - import { voteOnPublishedPoll } from "./voteOnPublishedPoll"; 13 - import { PollData } from "./fetchPollData"; 14 - import { Popover } from "components/Popover"; 15 - import LoginForm from "app/login/LoginForm"; 16 - import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 17 - import { getVoterIdentities, VoterIdentity } from "./getVoterIdentities"; 18 - import { Json } from "supabase/database.types"; 19 - import { InfoSmall } from "components/Icons/InfoSmall"; 20 - 21 - // Helper function to extract the first option from a vote record 22 - const getVoteOption = (voteRecord: any): string | null => { 23 - try { 24 - const record = voteRecord as PubLeafletPollVote.Record; 25 - return record.option && record.option.length > 0 ? record.option[0] : null; 26 - } catch { 27 - return null; 28 - } 29 - }; 30 - 31 - export const PublishedPollBlock = (props: { 32 - block: PubLeafletBlocksPoll.Main; 33 - pollData: PollData; 34 - className?: string; 35 - }) => { 36 - const { identity } = useIdentityData(); 37 - const [selectedOption, setSelectedOption] = useState<string | null>(null); 38 - const [isVoting, setIsVoting] = useState(false); 39 - const [showResults, setShowResults] = useState(false); 40 - const [optimisticVote, setOptimisticVote] = useState<{ 41 - option: string; 42 - voter_did: string; 43 - } | null>(null); 44 - let pollRecord = props.pollData.record as PubLeafletPollDefinition.Record; 45 - let [isClient, setIsClient] = useState(false); 46 - useEffect(() => { 47 - setIsClient(true); 48 - }, []); 49 - 50 - const handleVote = async () => { 51 - if (!selectedOption || !identity?.atp_did) return; 52 - 53 - setIsVoting(true); 54 - 55 - // Optimistically add the vote 56 - setOptimisticVote({ 57 - option: selectedOption, 58 - voter_did: identity.atp_did, 59 - }); 60 - setShowResults(true); 61 - 62 - try { 63 - const result = await voteOnPublishedPoll( 64 - props.block.pollRef.uri, 65 - props.block.pollRef.cid, 66 - selectedOption, 67 - ); 68 - 69 - if (!result.success) { 70 - console.error("Failed to vote:", result.error); 71 - // Revert optimistic update on failure 72 - setOptimisticVote(null); 73 - setShowResults(false); 74 - } 75 - } catch (error) { 76 - console.error("Failed to vote:", error); 77 - // Revert optimistic update on failure 78 - setOptimisticVote(null); 79 - setShowResults(false); 80 - } finally { 81 - setIsVoting(false); 82 - } 83 - }; 84 - 85 - const hasVoted = 86 - !!identity?.atp_did && 87 - (!!props.pollData?.atp_poll_votes.find( 88 - (v) => v.voter_did === identity?.atp_did, 89 - ) || 90 - !!optimisticVote); 91 - let isCreator = 92 - identity?.atp_did && props.pollData.uri.includes(identity?.atp_did); 93 - const displayResults = showResults || hasVoted; 94 - 95 - return ( 96 - <div 97 - className={`poll flex flex-col gap-2 p-3 w-full ${props.className} block-border`} 98 - style={{ 99 - backgroundColor: 100 - "color-mix(in oklab, rgb(var(--accent-1)), rgb(var(--bg-page)) 85%)", 101 - }} 102 - > 103 - {displayResults ? ( 104 - <> 105 - <PollResults 106 - pollData={props.pollData} 107 - hasVoted={hasVoted} 108 - setShowResults={setShowResults} 109 - optimisticVote={optimisticVote} 110 - /> 111 - {!hasVoted && ( 112 - <div className="flex justify-start"> 113 - <button 114 - className="w-fit flex gap-2 items-center justify-start text-sm text-accent-contrast" 115 - onClick={() => setShowResults(false)} 116 - > 117 - Back to Voting 118 - </button> 119 - </div> 120 - )} 121 - </> 122 - ) : ( 123 - <> 124 - {pollRecord.options.map((option, index) => ( 125 - <PollOptionButton 126 - key={index} 127 - option={option} 128 - optionIndex={index.toString()} 129 - selected={selectedOption === index.toString()} 130 - onSelect={() => setSelectedOption(index.toString())} 131 - disabled={!identity?.atp_did} 132 - /> 133 - ))} 134 - <div className="flex flex-col-reverse sm:flex-row sm:justify-between gap-2 items-center pt-2"> 135 - <div className="text-sm text-tertiary">All votes are public</div> 136 - <div className="flex sm:gap-3 sm:flex-row flex-col-reverse sm:justify-end justify-center gap-1 items-center"> 137 - <button 138 - className="w-fit font-bold text-accent-contrast" 139 - onClick={() => setShowResults(!showResults)} 140 - > 141 - See Results 142 - </button> 143 - {identity?.atp_did ? ( 144 - <ButtonPrimary 145 - className="place-self-end" 146 - onClick={handleVote} 147 - disabled={!selectedOption || isVoting} 148 - > 149 - {isVoting ? "Voting..." : "Vote!"} 150 - </ButtonPrimary> 151 - ) : ( 152 - <Popover 153 - asChild 154 - trigger={ 155 - <ButtonPrimary className="place-self-center"> 156 - <BlueskyTiny /> Login to vote 157 - </ButtonPrimary> 158 - } 159 - > 160 - {isClient && ( 161 - <LoginForm 162 - text="Log in to vote on this poll!" 163 - noEmail 164 - redirectRoute={window?.location.href + "?refreshAuth"} 165 - /> 166 - )} 167 - </Popover> 168 - )} 169 - </div> 170 - </div> 171 - </> 172 - )} 173 - </div> 174 - ); 175 - }; 176 - 177 - const PollOptionButton = (props: { 178 - option: PubLeafletPollDefinition.Option; 179 - optionIndex: string; 180 - selected: boolean; 181 - onSelect: () => void; 182 - disabled?: boolean; 183 - }) => { 184 - const ButtonComponent = props.selected ? ButtonPrimary : ButtonSecondary; 185 - 186 - return ( 187 - <div className="flex gap-2 items-center"> 188 - <ButtonComponent 189 - className="pollOption grow max-w-full flex" 190 - onClick={props.onSelect} 191 - disabled={props.disabled} 192 - > 193 - {props.option.text} 194 - </ButtonComponent> 195 - </div> 196 - ); 197 - }; 198 - 199 - const PollResults = (props: { 200 - pollData: PollData; 201 - hasVoted: boolean; 202 - setShowResults: (show: boolean) => void; 203 - optimisticVote: { option: string; voter_did: string } | null; 204 - }) => { 205 - // Merge optimistic vote with actual votes 206 - const allVotes = props.optimisticVote 207 - ? [ 208 - ...props.pollData.atp_poll_votes, 209 - { 210 - voter_did: props.optimisticVote.voter_did, 211 - record: { 212 - $type: "pub.leaflet.poll.vote", 213 - option: [props.optimisticVote.option], 214 - }, 215 - }, 216 - ] 217 - : props.pollData.atp_poll_votes; 218 - 219 - const totalVotes = allVotes.length || 0; 220 - let pollRecord = props.pollData.record as PubLeafletPollDefinition.Record; 221 - let optionsWithCount = pollRecord.options.map((o, index) => ({ 222 - ...o, 223 - votes: allVotes.filter((v) => getVoteOption(v.record) == index.toString()), 224 - })); 225 - 226 - const highestVotes = Math.max(...optionsWithCount.map((o) => o.votes.length)); 227 - return ( 228 - <> 229 - {pollRecord.options.map((option, index) => { 230 - const voteRecords = allVotes.filter( 231 - (v) => getVoteOption(v.record) === index.toString(), 232 - ); 233 - const isWinner = totalVotes > 0 && voteRecords.length === highestVotes; 234 - 235 - return ( 236 - <PollResult 237 - key={index} 238 - option={option} 239 - votes={voteRecords.length} 240 - voteRecords={voteRecords} 241 - totalVotes={totalVotes} 242 - winner={isWinner} 243 - /> 244 - ); 245 - })} 246 - </> 247 - ); 248 - }; 249 - 250 - const VoterListPopover = (props: { 251 - votes: number; 252 - voteRecords: { voter_did: string; record: Json }[]; 253 - }) => { 254 - const [voterIdentities, setVoterIdentities] = useState<VoterIdentity[]>([]); 255 - const [isLoading, setIsLoading] = useState(false); 256 - const [hasFetched, setHasFetched] = useState(false); 257 - 258 - const handleOpenChange = async () => { 259 - if (!hasFetched && props.voteRecords.length > 0) { 260 - setIsLoading(true); 261 - setHasFetched(true); 262 - try { 263 - const dids = props.voteRecords.map((v) => v.voter_did); 264 - const identities = await getVoterIdentities(dids); 265 - setVoterIdentities(identities); 266 - } catch (error) { 267 - console.error("Failed to fetch voter identities:", error); 268 - } finally { 269 - setIsLoading(false); 270 - } 271 - } 272 - }; 273 - 274 - return ( 275 - <Popover 276 - trigger={ 277 - <button 278 - className="hover:underline cursor-pointer" 279 - disabled={props.votes === 0} 280 - > 281 - {props.votes} 282 - </button> 283 - } 284 - onOpenChange={handleOpenChange} 285 - className="w-64 max-h-80" 286 - > 287 - {isLoading ? ( 288 - <div className="flex justify-center py-4"> 289 - <div className="text-sm text-secondary">Loading...</div> 290 - </div> 291 - ) : ( 292 - <div className="flex flex-col gap-1 text-sm py-0.5"> 293 - {voterIdentities.map((voter) => ( 294 - <a 295 - key={voter.did} 296 - href={`https://bsky.app/profile/${voter.handle || voter.did}`} 297 - target="_blank" 298 - rel="noopener noreferrer" 299 - className="" 300 - > 301 - @{voter.handle || voter.did} 302 - </a> 303 - ))} 304 - </div> 305 - )} 306 - </Popover> 307 - ); 308 - }; 309 - 310 - const PollResult = (props: { 311 - option: PubLeafletPollDefinition.Option; 312 - votes: number; 313 - voteRecords: { voter_did: string; record: Json }[]; 314 - totalVotes: number; 315 - winner: boolean; 316 - }) => { 317 - return ( 318 - <div 319 - className={`pollResult relative grow py-0.5 px-2 border-accent-contrast rounded-md overflow-hidden ${props.winner ? "font-bold border-2" : "border"}`} 320 - > 321 - <div 322 - style={{ 323 - WebkitTextStroke: `${props.winner ? "6px" : "6px"} rgb(var(--bg-page))`, 324 - paintOrder: "stroke fill", 325 - }} 326 - className="pollResultContent text-accent-contrast relative flex gap-2 justify-between z-10" 327 - > 328 - <div className="grow max-w-full truncate">{props.option.text}</div> 329 - <VoterListPopover votes={props.votes} voteRecords={props.voteRecords} /> 330 - </div> 331 - <div className="pollResultBG absolute bg-bg-page w-full top-0 bottom-0 left-0 right-0 flex flex-row z-0"> 332 - <div 333 - className="bg-accent-contrast rounded-[2px] m-0.5" 334 - style={{ 335 - maskImage: "var(--hatchSVG)", 336 - maskRepeat: "repeat repeat", 337 - ...(props.votes === 0 338 - ? { width: "4px" } 339 - : { flexBasis: `${(props.votes / props.totalVotes) * 100}%` }), 340 - }} 341 - /> 342 - <div /> 343 - </div> 344 - </div> 345 - ); 346 - };
···
+3 -2
app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx
··· 186 <BlueskyLinkTiny className="shrink-0" /> 187 Bluesky 188 </a> 189 - <Separator classname="h-4" /> 190 <button 191 id="copy-quote-link" 192 className="flex gap-1 items-center hover:font-bold px-1" ··· 211 </button> 212 {pubRecord?.preferences?.showComments !== false && identity?.atp_did && ( 213 <> 214 - <Separator classname="h-4" /> 215 <button 216 className="flex gap-1 items-center hover:font-bold px-1" 217 onClick={() => {
··· 186 <BlueskyLinkTiny className="shrink-0" /> 187 Bluesky 188 </a> 189 + <Separator classname="h-4!" /> 190 <button 191 id="copy-quote-link" 192 className="flex gap-1 items-center hover:font-bold px-1" ··· 211 </button> 212 {pubRecord?.preferences?.showComments !== false && identity?.atp_did && ( 213 <> 214 + <Separator classname="h-4! " /> 215 + 216 <button 217 className="flex gap-1 items-center hover:font-bold px-1" 218 onClick={() => {
-20
app/lish/[did]/[publication]/[rkey]/StaticMathBlock.tsx
··· 1 - import { PubLeafletBlocksMath } from "lexicons/api"; 2 - import Katex from "katex"; 3 - import "katex/dist/katex.min.css"; 4 - 5 - export const StaticMathBlock = ({ 6 - block, 7 - }: { 8 - block: PubLeafletBlocksMath.Main; 9 - }) => { 10 - const html = Katex.renderToString(block.tex, { 11 - displayMode: true, 12 - output: "html", 13 - throwOnError: false, 14 - }); 15 - return ( 16 - <div className="math-block my-2"> 17 - <div dangerouslySetInnerHTML={{ __html: html }} /> 18 - </div> 19 - ); 20 - };
···
+2 -2
app/lish/[did]/[publication]/[rkey]/StaticPostContent.tsx
··· 12 PubLeafletPagesLinearDocument, 13 } from "lexicons/api"; 14 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 15 - import { TextBlockCore, TextBlockCoreProps } from "./TextBlockCore"; 16 - import { StaticMathBlock } from "./StaticMathBlock"; 17 import { codeToHtml, bundledLanguagesInfo, bundledThemesInfo } from "shiki"; 18 19 function StaticBaseTextBlock(props: Omit<TextBlockCoreProps, "renderers">) {
··· 12 PubLeafletPagesLinearDocument, 13 } from "lexicons/api"; 14 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 15 + import { TextBlockCore, TextBlockCoreProps } from "./Blocks/TextBlockCore"; 16 + import { StaticMathBlock } from "./Blocks/StaticMathBlock"; 17 import { codeToHtml, bundledLanguagesInfo, bundledThemesInfo } from "shiki"; 18 19 function StaticBaseTextBlock(props: Omit<TextBlockCoreProps, "renderers">) {
-95
app/lish/[did]/[publication]/[rkey]/TextBlock.tsx
··· 1 - "use client"; 2 - import { UnicodeString } from "@atproto/api"; 3 - import { PubLeafletRichtextFacet } from "lexicons/api"; 4 - import { useMemo } from "react"; 5 - import { useHighlight } from "./useHighlight"; 6 - import { BaseTextBlock } from "./BaseTextBlock"; 7 - 8 - type Facet = PubLeafletRichtextFacet.Main; 9 - export function TextBlock(props: { 10 - plaintext: string; 11 - facets?: Facet[]; 12 - index: number[]; 13 - preview?: boolean; 14 - pageId?: string; 15 - }) { 16 - let children = []; 17 - let highlights = useHighlight(props.index, props.pageId); 18 - let facets = useMemo(() => { 19 - if (props.preview) return props.facets; 20 - let facets = [...(props.facets || [])]; 21 - for (let highlight of highlights) { 22 - const fragmentId = props.pageId 23 - ? `${props.pageId}~${props.index.join(".")}_${highlight.startOffset || 0}` 24 - : `${props.index.join(".")}_${highlight.startOffset || 0}`; 25 - facets = addFacet( 26 - facets, 27 - { 28 - index: { 29 - byteStart: highlight.startOffset 30 - ? new UnicodeString( 31 - props.plaintext.slice(0, highlight.startOffset), 32 - ).length 33 - : 0, 34 - byteEnd: new UnicodeString( 35 - props.plaintext.slice(0, highlight.endOffset ?? undefined), 36 - ).length, 37 - }, 38 - features: [ 39 - { $type: "pub.leaflet.richtext.facet#highlight" }, 40 - { 41 - $type: "pub.leaflet.richtext.facet#id", 42 - id: fragmentId, 43 - }, 44 - ], 45 - }, 46 - new UnicodeString(props.plaintext).length, 47 - ); 48 - } 49 - return facets; 50 - }, [props.plaintext, props.facets, highlights, props.preview, props.pageId]); 51 - return <BaseTextBlock {...props} facets={facets} />; 52 - } 53 - 54 - function addFacet(facets: Facet[], newFacet: Facet, length: number) { 55 - if (facets.length === 0) { 56 - return [newFacet]; 57 - } 58 - 59 - const allFacets = [...facets, newFacet]; 60 - 61 - // Collect all boundary positions 62 - const boundaries = new Set<number>(); 63 - boundaries.add(0); 64 - boundaries.add(length); 65 - 66 - for (const facet of allFacets) { 67 - boundaries.add(facet.index.byteStart); 68 - boundaries.add(facet.index.byteEnd); 69 - } 70 - 71 - const sortedBoundaries = Array.from(boundaries).sort((a, b) => a - b); 72 - const result: Facet[] = []; 73 - 74 - // Process segments between consecutive boundaries 75 - for (let i = 0; i < sortedBoundaries.length - 1; i++) { 76 - const start = sortedBoundaries[i]; 77 - const end = sortedBoundaries[i + 1]; 78 - 79 - // Find facets that are active at the start position 80 - const activeFacets = allFacets.filter( 81 - (facet) => facet.index.byteStart <= start && facet.index.byteEnd > start, 82 - ); 83 - 84 - // Only create facet if there are active facets (features present) 85 - if (activeFacets.length > 0) { 86 - const features = activeFacets.flatMap((f) => f.features); 87 - result.push({ 88 - index: { byteStart: start, byteEnd: end }, 89 - features, 90 - }); 91 - } 92 - } 93 - 94 - return result; 95 - }
···
-181
app/lish/[did]/[publication]/[rkey]/TextBlockCore.tsx
··· 1 - import { UnicodeString } from "@atproto/api"; 2 - import { PubLeafletRichtextFacet } from "lexicons/api"; 3 - import { AtMentionLink } from "components/AtMentionLink"; 4 - import { ReactNode } from "react"; 5 - 6 - type Facet = PubLeafletRichtextFacet.Main; 7 - 8 - export type FacetRenderers = { 9 - DidMention?: (props: { did: string; children: ReactNode }) => ReactNode; 10 - }; 11 - 12 - export type TextBlockCoreProps = { 13 - plaintext: string; 14 - facets?: Facet[]; 15 - index: number[]; 16 - preview?: boolean; 17 - renderers?: FacetRenderers; 18 - }; 19 - 20 - export function TextBlockCore(props: TextBlockCoreProps) { 21 - let children = []; 22 - let richText = new RichText({ 23 - text: props.plaintext, 24 - facets: props.facets || [], 25 - }); 26 - let counter = 0; 27 - for (const segment of richText.segments()) { 28 - let id = segment.facet?.find(PubLeafletRichtextFacet.isId); 29 - let link = segment.facet?.find(PubLeafletRichtextFacet.isLink); 30 - let isBold = segment.facet?.find(PubLeafletRichtextFacet.isBold); 31 - let isCode = segment.facet?.find(PubLeafletRichtextFacet.isCode); 32 - let isStrikethrough = segment.facet?.find( 33 - PubLeafletRichtextFacet.isStrikethrough, 34 - ); 35 - let isDidMention = segment.facet?.find( 36 - PubLeafletRichtextFacet.isDidMention, 37 - ); 38 - let isAtMention = segment.facet?.find(PubLeafletRichtextFacet.isAtMention); 39 - let isUnderline = segment.facet?.find(PubLeafletRichtextFacet.isUnderline); 40 - let isItalic = segment.facet?.find(PubLeafletRichtextFacet.isItalic); 41 - let isHighlighted = segment.facet?.find( 42 - PubLeafletRichtextFacet.isHighlight, 43 - ); 44 - let className = ` 45 - ${isCode ? "inline-code" : ""} 46 - ${id ? "scroll-mt-12 scroll-mb-10" : ""} 47 - ${isBold ? "font-bold" : ""} 48 - ${isItalic ? "italic" : ""} 49 - ${isUnderline ? "underline" : ""} 50 - ${isStrikethrough ? "line-through decoration-tertiary" : ""} 51 - ${isHighlighted ? "highlight bg-highlight-1" : ""}`.replaceAll("\n", " "); 52 - 53 - // Split text by newlines and insert <br> tags 54 - const textParts = segment.text.split("\n"); 55 - const renderedText = textParts.flatMap((part, i) => 56 - i < textParts.length - 1 57 - ? [part, <br key={`br-${counter}-${i}`} />] 58 - : [part], 59 - ); 60 - 61 - if (isCode) { 62 - children.push( 63 - <code key={counter} className={className} id={id?.id}> 64 - {renderedText} 65 - </code>, 66 - ); 67 - } else if (isDidMention) { 68 - const DidMentionRenderer = props.renderers?.DidMention; 69 - if (DidMentionRenderer) { 70 - children.push( 71 - <DidMentionRenderer key={counter} did={isDidMention.did}> 72 - <span className="mention">{renderedText}</span> 73 - </DidMentionRenderer>, 74 - ); 75 - } else { 76 - // Default: render as a simple link 77 - children.push( 78 - <a 79 - key={counter} 80 - href={`https://leaflet.pub/p/${isDidMention.did}`} 81 - target="_blank" 82 - className="no-underline" 83 - > 84 - <span className="mention">{renderedText}</span> 85 - </a>, 86 - ); 87 - } 88 - } else if (isAtMention) { 89 - children.push( 90 - <AtMentionLink 91 - key={counter} 92 - atURI={isAtMention.atURI} 93 - className={className} 94 - > 95 - {renderedText} 96 - </AtMentionLink>, 97 - ); 98 - } else if (link) { 99 - children.push( 100 - <a 101 - key={counter} 102 - href={link.uri.trim()} 103 - className={`text-accent-contrast hover:underline ${className}`} 104 - target="_blank" 105 - > 106 - {renderedText} 107 - </a>, 108 - ); 109 - } else { 110 - children.push( 111 - <span key={counter} className={className} id={id?.id}> 112 - {renderedText} 113 - </span>, 114 - ); 115 - } 116 - 117 - counter++; 118 - } 119 - return <>{children}</>; 120 - } 121 - 122 - type RichTextSegment = { 123 - text: string; 124 - facet?: Exclude<Facet["features"], { $type: string }>; 125 - }; 126 - 127 - export class RichText { 128 - unicodeText: UnicodeString; 129 - facets?: Facet[]; 130 - 131 - constructor(props: { text: string; facets: Facet[] }) { 132 - this.unicodeText = new UnicodeString(props.text); 133 - this.facets = props.facets; 134 - if (this.facets) { 135 - this.facets = this.facets 136 - .filter((facet) => facet.index.byteStart <= facet.index.byteEnd) 137 - .sort((a, b) => a.index.byteStart - b.index.byteStart); 138 - } 139 - } 140 - 141 - *segments(): Generator<RichTextSegment, void, void> { 142 - const facets = this.facets || []; 143 - if (!facets.length) { 144 - yield { text: this.unicodeText.utf16 }; 145 - return; 146 - } 147 - 148 - let textCursor = 0; 149 - let facetCursor = 0; 150 - do { 151 - const currFacet = facets[facetCursor]; 152 - if (textCursor < currFacet.index.byteStart) { 153 - yield { 154 - text: this.unicodeText.slice(textCursor, currFacet.index.byteStart), 155 - }; 156 - } else if (textCursor > currFacet.index.byteStart) { 157 - facetCursor++; 158 - continue; 159 - } 160 - if (currFacet.index.byteStart < currFacet.index.byteEnd) { 161 - const subtext = this.unicodeText.slice( 162 - currFacet.index.byteStart, 163 - currFacet.index.byteEnd, 164 - ); 165 - if (!subtext.trim()) { 166 - // dont empty string entities 167 - yield { text: subtext }; 168 - } else { 169 - yield { text: subtext, facet: currFacet.features }; 170 - } 171 - } 172 - textCursor = currFacet.index.byteEnd; 173 - facetCursor++; 174 - } while (facetCursor < facets.length); 175 - if (textCursor < this.unicodeText.length) { 176 - yield { 177 - text: this.unicodeText.slice(textCursor, this.unicodeText.length), 178 - }; 179 - } 180 - } 181 - }
···
+58 -1
app/lish/[did]/[publication]/[rkey]/getPostPageData.ts
··· 10 data, 11 uri, 12 comments_on_documents(*, bsky_profiles(*)), 13 - documents_in_publications(publications(*, publication_subscriptions(*))), 14 document_mentions_in_bsky(*), 15 leaflets_in_publications(*) 16 `, ··· 51 ?.record as PubLeafletPublication.Record 52 )?.theme || (document?.data as PubLeafletDocument.Record)?.theme; 53 54 return { 55 ...document, 56 quotesAndMentions, 57 theme, 58 }; 59 } 60
··· 10 data, 11 uri, 12 comments_on_documents(*, bsky_profiles(*)), 13 + documents_in_publications(publications(*, 14 + documents_in_publications(documents(uri, data)), 15 + publication_subscriptions(*)) 16 + ), 17 document_mentions_in_bsky(*), 18 leaflets_in_publications(*) 19 `, ··· 54 ?.record as PubLeafletPublication.Record 55 )?.theme || (document?.data as PubLeafletDocument.Record)?.theme; 56 57 + // Calculate prev/next documents from the fetched publication documents 58 + let prevNext: 59 + | { 60 + prev?: { uri: string; title: string }; 61 + next?: { uri: string; title: string }; 62 + } 63 + | undefined; 64 + 65 + const currentPublishedAt = (document.data as PubLeafletDocument.Record) 66 + ?.publishedAt; 67 + const allDocs = 68 + document.documents_in_publications[0]?.publications 69 + ?.documents_in_publications; 70 + 71 + if (currentPublishedAt && allDocs) { 72 + // Filter and sort documents by publishedAt 73 + const sortedDocs = allDocs 74 + .map((dip) => ({ 75 + uri: dip?.documents?.uri, 76 + title: (dip?.documents?.data as PubLeafletDocument.Record).title, 77 + publishedAt: (dip?.documents?.data as PubLeafletDocument.Record) 78 + .publishedAt, 79 + })) 80 + .filter((doc) => doc.publishedAt) // Only include docs with publishedAt 81 + .sort( 82 + (a, b) => 83 + new Date(a.publishedAt!).getTime() - 84 + new Date(b.publishedAt!).getTime(), 85 + ); 86 + 87 + // Find current document index 88 + const currentIndex = sortedDocs.findIndex((doc) => doc.uri === uri); 89 + 90 + if (currentIndex !== -1) { 91 + prevNext = { 92 + prev: 93 + currentIndex > 0 94 + ? { 95 + uri: sortedDocs[currentIndex - 1].uri || "", 96 + title: sortedDocs[currentIndex - 1].title, 97 + } 98 + : undefined, 99 + next: 100 + currentIndex < sortedDocs.length - 1 101 + ? { 102 + uri: sortedDocs[currentIndex + 1].uri || "", 103 + title: sortedDocs[currentIndex + 1].title, 104 + } 105 + : undefined, 106 + }; 107 + } 108 + } 109 + 110 return { 111 ...document, 112 quotesAndMentions, 113 theme, 114 + prevNext, 115 }; 116 } 117
+1
app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx
··· 140 commentsCount={comments} 141 tags={tags} 142 showComments={pubRecord?.preferences?.showComments} 143 postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} 144 /> 145 </div>
··· 140 commentsCount={comments} 141 tags={tags} 142 showComments={pubRecord?.preferences?.showComments} 143 + showMentions={pubRecord?.preferences?.showMentions} 144 postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} 145 /> 146 </div>
+31 -25
app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx
··· 22 ? true 23 : record.preferences.showComments, 24 ); 25 - let [showMentions, setShowMentions] = useState(true); 26 - let [showPrevNext, setShowPrevNext] = useState(true); 27 28 let toast = useToaster(); 29 return ( 30 <form 31 onSubmit={async (e) => { 32 - // if (!pubData) return; 33 - // e.preventDefault(); 34 - // props.setLoading(true); 35 - // let data = await updatePublication({ 36 - // uri: pubData.uri, 37 - // name: nameValue, 38 - // description: descriptionValue, 39 - // iconFile: iconFile, 40 - // preferences: { 41 - // showInDiscover: showInDiscover, 42 - // showComments: showComments, 43 - // }, 44 - // }); 45 - // toast({ type: "success", content: "Posts Updated!" }); 46 - // props.setLoading(false); 47 - // mutate("publication-data"); 48 }} 49 className="text-primary flex flex-col" 50 > ··· 57 Post Options 58 </PubSettingsHeader> 59 <h4 className="mb-1">Layout</h4> 60 - {/*<div>Max Post Width</div>*/} 61 <Toggle 62 toggle={showPrevNext} 63 onToggle={() => { 64 setShowPrevNext(!showPrevNext); 65 }} 66 > 67 - <div className="flex flex-col justify-start"> 68 - <div className="font-bold">Show Prev/Next Buttons</div> 69 - <div className="text-tertiary text-sm leading-tight"> 70 - Show buttons that navigate to the previous and next posts 71 - </div> 72 - </div> 73 </Toggle> 74 <hr className="my-2 border-border-light" /> 75 <h4 className="mb-1">Interactions</h4>
··· 22 ? true 23 : record.preferences.showComments, 24 ); 25 + let [showMentions, setShowMentions] = useState( 26 + record?.preferences?.showMentions === undefined 27 + ? true 28 + : record.preferences.showMentions, 29 + ); 30 + let [showPrevNext, setShowPrevNext] = useState( 31 + record?.preferences?.showPrevNext === undefined 32 + ? true 33 + : record.preferences.showPrevNext, 34 + ); 35 36 let toast = useToaster(); 37 return ( 38 <form 39 onSubmit={async (e) => { 40 + if (!pubData) return; 41 + e.preventDefault(); 42 + props.setLoading(true); 43 + let data = await updatePublication({ 44 + name: record.name, 45 + uri: pubData.uri, 46 + preferences: { 47 + showInDiscover: 48 + record?.preferences?.showInDiscover === undefined 49 + ? true 50 + : record.preferences.showInDiscover, 51 + showComments: showComments, 52 + showMentions: showMentions, 53 + showPrevNext: showPrevNext, 54 + }, 55 + }); 56 + toast({ type: "success", content: <strong>Posts Updated!</strong> }); 57 + console.log(record.preferences?.showPrevNext); 58 + props.setLoading(false); 59 + mutate("publication-data"); 60 }} 61 className="text-primary flex flex-col" 62 > ··· 69 Post Options 70 </PubSettingsHeader> 71 <h4 className="mb-1">Layout</h4> 72 <Toggle 73 toggle={showPrevNext} 74 onToggle={() => { 75 setShowPrevNext(!showPrevNext); 76 }} 77 > 78 + <div className="font-bold">Show Prev/Next Buttons</div> 79 </Toggle> 80 <hr className="my-2 border-border-light" /> 81 <h4 className="mb-1">Interactions</h4>
+2 -2
app/lish/[did]/[publication]/dashboard/settings/PublicationSettings.tsx
··· 103 Theme and Layout 104 <ArrowRightTiny /> 105 </button> 106 - {/*<button 107 className={menuItemClassName} 108 type="button" 109 onClick={() => props.setState("post-options")} 110 > 111 Post Options 112 <ArrowRightTiny /> 113 - </button>*/} 114 </div> 115 ); 116 };
··· 103 Theme and Layout 104 <ArrowRightTiny /> 105 </button> 106 + <button 107 className={menuItemClassName} 108 type="button" 109 onClick={() => props.setState("post-options")} 110 > 111 Post Options 112 <ArrowRightTiny /> 113 + </button> 114 </div> 115 ); 116 };
+8 -2
app/lish/[did]/[publication]/page.tsx
··· 18 import { LocalizedDate } from "./LocalizedDate"; 19 import { PublicationHomeLayout } from "./PublicationHomeLayout"; 20 import { PublicationAuthor } from "./PublicationAuthor"; 21 22 export default async function Publication(props: { 23 params: Promise<{ publication: string; did: string }>; ··· 147 </p> 148 </SpeedyLink> 149 150 - <div className="text-sm text-tertiary flex gap-1 flex-wrap pt-2"> 151 <p className="text-sm text-tertiary "> 152 {doc_record.publishedAt && ( 153 <LocalizedDate ··· 160 /> 161 )}{" "} 162 </p> 163 - {comments > 0 || quotes > 0 ? "| " : ""} 164 <InteractionPreview 165 quotesCount={quotes} 166 commentsCount={comments} 167 tags={tags} 168 postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} 169 showComments={record?.preferences?.showComments} 170 /> 171 </div> 172 </div>
··· 18 import { LocalizedDate } from "./LocalizedDate"; 19 import { PublicationHomeLayout } from "./PublicationHomeLayout"; 20 import { PublicationAuthor } from "./PublicationAuthor"; 21 + import { Separator } from "components/Layout"; 22 23 export default async function Publication(props: { 24 params: Promise<{ publication: string; did: string }>; ··· 148 </p> 149 </SpeedyLink> 150 151 + <div className="text-sm text-tertiary flex gap-1 flex-wrap pt-2 items-center"> 152 <p className="text-sm text-tertiary "> 153 {doc_record.publishedAt && ( 154 <LocalizedDate ··· 161 /> 162 )}{" "} 163 </p> 164 + {comments > 0 || quotes > 0 || tags.length > 0 ? ( 165 + <Separator classname="h-4! mx-1" /> 166 + ) : ( 167 + "" 168 + )} 169 <InteractionPreview 170 quotesCount={quotes} 171 commentsCount={comments} 172 tags={tags} 173 postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} 174 showComments={record?.preferences?.showComments} 175 + showMentions={record?.preferences?.showMentions} 176 /> 177 </div> 178 </div>
+9 -2
app/lish/createPub/CreatePubForm.tsx
··· 53 description: descriptionValue, 54 iconFile: logoFile, 55 subdomain: domainValue, 56 - preferences: { showInDiscover, showComments: true }, 57 }); 58 59 if (!result.success) { ··· 68 setTimeout(() => { 69 setFormState("normal"); 70 if (result.publication) 71 - router.push(`${getBasePublicationURL(result.publication)}/dashboard`); 72 }, 500); 73 }} 74 >
··· 53 description: descriptionValue, 54 iconFile: logoFile, 55 subdomain: domainValue, 56 + preferences: { 57 + showInDiscover, 58 + showComments: true, 59 + showMentions: true, 60 + showPrevNext: false, 61 + }, 62 }); 63 64 if (!result.success) { ··· 73 setTimeout(() => { 74 setFormState("normal"); 75 if (result.publication) 76 + router.push( 77 + `${getBasePublicationURL(result.publication)}/dashboard`, 78 + ); 79 }, 500); 80 }} 81 >
+19 -14
app/lish/createPub/UpdatePubForm.tsx
··· 21 import { Checkbox } from "components/Checkbox"; 22 import type { GetDomainConfigResponseBody } from "@vercel/sdk/esm/models/getdomainconfigop"; 23 import { PubSettingsHeader } from "../[did]/[publication]/dashboard/settings/PublicationSettings"; 24 25 export const EditPubForm = (props: { 26 backToMenuAction: () => void; ··· 43 ? true 44 : record.preferences.showComments, 45 ); 46 let [descriptionValue, setDescriptionValue] = useState( 47 record?.description || "", 48 ); ··· 74 preferences: { 75 showInDiscover: showInDiscover, 76 showComments: showComments, 77 }, 78 }); 79 toast({ type: "success", content: "Updated!" }); ··· 90 General Settings 91 </PubSettingsHeader> 92 <div className="flex flex-col gap-3 w-[1000px] max-w-full pb-2"> 93 - <div className="flex items-center justify-between gap-2 "> 94 <p className="pl-0.5 pb-0.5 text-tertiary italic text-sm font-bold"> 95 Logo <span className="font-normal">(optional)</span> 96 </p> ··· 160 <CustomDomainForm /> 161 <hr className="border-border-light" /> 162 163 - <Checkbox 164 - checked={showInDiscover} 165 - onChange={(e) => setShowInDiscover(e.target.checked)} 166 > 167 - <div className=" pt-0.5 flex flex-col text-sm italic text-tertiary "> 168 <p className="font-bold"> 169 Show In{" "} 170 <a href="/discover" target="_blank"> ··· 179 page. You can change this at any time! 180 </p> 181 </div> 182 - </Checkbox> 183 184 - <Checkbox 185 - checked={showComments} 186 - onChange={(e) => setShowComments(e.target.checked)} 187 - > 188 - <div className=" pt-0.5 flex flex-col text-sm italic text-tertiary "> 189 - <p className="font-bold">Show comments on posts</p> 190 - </div> 191 - </Checkbox> 192 </div> 193 </form> 194 );
··· 21 import { Checkbox } from "components/Checkbox"; 22 import type { GetDomainConfigResponseBody } from "@vercel/sdk/esm/models/getdomainconfigop"; 23 import { PubSettingsHeader } from "../[did]/[publication]/dashboard/settings/PublicationSettings"; 24 + import { Toggle } from "components/Toggle"; 25 26 export const EditPubForm = (props: { 27 backToMenuAction: () => void; ··· 44 ? true 45 : record.preferences.showComments, 46 ); 47 + let showMentions = 48 + record?.preferences?.showMentions === undefined 49 + ? true 50 + : record.preferences.showMentions; 51 + let showPrevNext = 52 + record?.preferences?.showPrevNext === undefined 53 + ? true 54 + : record.preferences.showPrevNext; 55 + 56 let [descriptionValue, setDescriptionValue] = useState( 57 record?.description || "", 58 ); ··· 84 preferences: { 85 showInDiscover: showInDiscover, 86 showComments: showComments, 87 + showMentions: showMentions, 88 + showPrevNext: showPrevNext, 89 }, 90 }); 91 toast({ type: "success", content: "Updated!" }); ··· 102 General Settings 103 </PubSettingsHeader> 104 <div className="flex flex-col gap-3 w-[1000px] max-w-full pb-2"> 105 + <div className="flex items-center justify-between gap-2 mt-2 "> 106 <p className="pl-0.5 pb-0.5 text-tertiary italic text-sm font-bold"> 107 Logo <span className="font-normal">(optional)</span> 108 </p> ··· 172 <CustomDomainForm /> 173 <hr className="border-border-light" /> 174 175 + <Toggle 176 + toggle={showInDiscover} 177 + onToggle={() => setShowInDiscover(!showInDiscover)} 178 > 179 + <div className=" pt-0.5 flex flex-col text-sm text-tertiary "> 180 <p className="font-bold"> 181 Show In{" "} 182 <a href="/discover" target="_blank"> ··· 191 page. You can change this at any time! 192 </p> 193 </div> 194 + </Toggle> 195 196 + 197 </div> 198 </form> 199 );
+2 -2
app/lish/createPub/updatePublication.ts
··· 25 }: { 26 uri: string; 27 name: string; 28 - description: string; 29 - iconFile: File | null; 30 preferences?: Omit<PubLeafletPublication.Preferences, "$type">; 31 }): Promise<UpdatePublicationResult> { 32 let identity = await getIdentityData();
··· 25 }: { 26 uri: string; 27 name: string; 28 + description?: string; 29 + iconFile?: File | null; 30 preferences?: Omit<PubLeafletPublication.Preferences, "$type">; 31 }): Promise<UpdatePublicationResult> { 32 let identity = await getIdentityData();
+2 -2
components/ActionBar/ActionButton.tsx
··· 70 > 71 <div className="shrink-0">{icon}</div> 72 <div 73 - className={`flex flex-col pr-1 leading-snug max-w-full min-w-0 ${sidebar.open ? "block" : showLabelOnMobile ? "sm:hidden block" : "hidden"}`} 74 > 75 - <div className="truncate text-left pt-[1px]">{label}</div> 76 {subtext && ( 77 <div className="text-xs text-tertiary font-normal text-left"> 78 {subtext}
··· 70 > 71 <div className="shrink-0">{icon}</div> 72 <div 73 + className={`flex flex-col pr-1 ${subtext && "leading-snug"} max-w-full min-w-0 ${sidebar.open ? "block" : showLabelOnMobile ? "sm:hidden block" : "hidden"}`} 74 > 75 + <div className="truncate text-left">{label}</div> 76 {subtext && ( 77 <div className="text-xs text-tertiary font-normal text-left"> 78 {subtext}
+1 -1
components/Blocks/Block.tsx
··· 10 import { useHandleDrop } from "./useHandleDrop"; 11 import { useEntitySetContext } from "components/EntitySetProvider"; 12 13 - import { TextBlock } from "components/Blocks/TextBlock"; 14 import { ImageBlock } from "./ImageBlock"; 15 import { PageLinkBlock } from "./PageLinkBlock"; 16 import { ExternalLinkBlock } from "./ExternalLinkBlock";
··· 10 import { useHandleDrop } from "./useHandleDrop"; 11 import { useEntitySetContext } from "components/EntitySetProvider"; 12 13 + import { TextBlock } from "./TextBlock/index"; 14 import { ImageBlock } from "./ImageBlock"; 15 import { PageLinkBlock } from "./PageLinkBlock"; 16 import { ExternalLinkBlock } from "./ExternalLinkBlock";
+1 -1
components/Blocks/TextBlock/RenderYJSFragment.tsx
··· 8 import { Delta } from "src/utils/yjsFragmentToString"; 9 import { ProfilePopover } from "components/ProfilePopover"; 10 11 - type BlockElements = "h1" | "h2" | "h3" | null | "blockquote" | "p"; 12 export function RenderYJSFragment({ 13 value, 14 wrapper,
··· 8 import { Delta } from "src/utils/yjsFragmentToString"; 9 import { ProfilePopover } from "components/ProfilePopover"; 10 11 + type BlockElements = "h1" | "h2" | "h3" | null | "blockquote" | "p" | "small"; 12 export function RenderYJSFragment({ 13 value, 14 wrapper,
+18 -4
components/Blocks/TextBlock/index.tsx
··· 120 }) { 121 let initialFact = useEntity(props.entityID, "block/text"); 122 let headingLevel = useEntity(props.entityID, "block/heading-level"); 123 let alignment = 124 useEntity(props.entityID, "block/text-alignment")?.data.value || "left"; 125 let alignmentClass = { ··· 128 center: "text-center", 129 justify: "text-justify", 130 }[alignment]; 131 let { permissions } = useEntitySetContext(); 132 133 let content = <br />; ··· 159 className={` 160 ${alignmentClass} 161 ${props.type === "blockquote" ? (props.previousBlock?.type === "blockquote" ? `blockquote pt-3 ` : "blockquote") : ""} 162 - ${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : ""} 163 w-full whitespace-pre-wrap outline-hidden ${props.className} `} 164 > 165 {content} ··· 169 170 export function BaseTextBlock(props: BlockProps & { className?: string }) { 171 let headingLevel = useEntity(props.entityID, "block/heading-level"); 172 let alignment = 173 useEntity(props.entityID, "block/text-alignment")?.data.value || "left"; 174 ··· 184 center: "text-center", 185 justify: "text-justify", 186 }[alignment]; 187 188 let editorState = useEditorStates( 189 (s) => s.editorStates[props.entityID], ··· 258 grow resize-none align-top whitespace-pre-wrap bg-transparent 259 outline-hidden 260 261 - ${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : ""} 262 ${props.className}`} 263 ref={mountRef} 264 /> ··· 277 // if this is the only block on the page and is empty or is a canvas, show placeholder 278 <div 279 className={`${props.className} ${alignmentClass} w-full pointer-events-none absolute top-0 left-0 italic text-tertiary flex flex-col 280 - ${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : ""} 281 `} 282 > 283 {props.type === "text" ··· 496 497 // Find the relative positioned parent container 498 const editorEl = view.dom; 499 - const container = editorEl.closest('.relative') as HTMLElement | null; 500 501 if (container) { 502 const containerRect = container.getBoundingClientRect();
··· 120 }) { 121 let initialFact = useEntity(props.entityID, "block/text"); 122 let headingLevel = useEntity(props.entityID, "block/heading-level"); 123 + let textSize = useEntity(props.entityID, "block/text-size"); 124 let alignment = 125 useEntity(props.entityID, "block/text-alignment")?.data.value || "left"; 126 let alignmentClass = { ··· 129 center: "text-center", 130 justify: "text-justify", 131 }[alignment]; 132 + let textStyle = 133 + textSize?.data.value === "small" 134 + ? "text-sm" 135 + : textSize?.data.value === "large" 136 + ? "text-lg" 137 + : ""; 138 let { permissions } = useEntitySetContext(); 139 140 let content = <br />; ··· 166 className={` 167 ${alignmentClass} 168 ${props.type === "blockquote" ? (props.previousBlock?.type === "blockquote" ? `blockquote pt-3 ` : "blockquote") : ""} 169 + ${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : textStyle} 170 w-full whitespace-pre-wrap outline-hidden ${props.className} `} 171 > 172 {content} ··· 176 177 export function BaseTextBlock(props: BlockProps & { className?: string }) { 178 let headingLevel = useEntity(props.entityID, "block/heading-level"); 179 + let textSize = useEntity(props.entityID, "block/text-size"); 180 let alignment = 181 useEntity(props.entityID, "block/text-alignment")?.data.value || "left"; 182 ··· 192 center: "text-center", 193 justify: "text-justify", 194 }[alignment]; 195 + let textStyle = 196 + textSize?.data.value === "small" 197 + ? "text-sm text-secondary" 198 + : textSize?.data.value === "large" 199 + ? "text-lg text-primary" 200 + : "text-base text-primary"; 201 202 let editorState = useEditorStates( 203 (s) => s.editorStates[props.entityID], ··· 272 grow resize-none align-top whitespace-pre-wrap bg-transparent 273 outline-hidden 274 275 + ${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : textStyle} 276 ${props.className}`} 277 ref={mountRef} 278 /> ··· 291 // if this is the only block on the page and is empty or is a canvas, show placeholder 292 <div 293 className={`${props.className} ${alignmentClass} w-full pointer-events-none absolute top-0 left-0 italic text-tertiary flex flex-col 294 + ${props.type === "heading" ? HeadingStyle[headingLevel?.data.value || 1] : textStyle} 295 `} 296 > 297 {props.type === "text" ··· 510 511 // Find the relative positioned parent container 512 const editorEl = view.dom; 513 + const container = editorEl.closest(".relative") as HTMLElement | null; 514 515 if (container) { 516 const containerRect = container.getBoundingClientRect();
+14
components/Blocks/TextBlock/keymap.ts
··· 555 }, 556 }); 557 } 558 }; 559 asyncRun().then(() => { 560 useUIState.getState().setSelectedBlock({
··· 555 }, 556 }); 557 } 558 + let [textSize] = 559 + (await repRef.current?.query((tx) => 560 + scanIndex(tx).eav(propsRef.current.entityID, "block/text-size"), 561 + )) || []; 562 + if (textSize) { 563 + await repRef.current?.mutate.assertFact({ 564 + entity: newEntityID, 565 + attribute: "block/text-size", 566 + data: { 567 + type: "text-size-union", 568 + value: textSize.data.value, 569 + }, 570 + }); 571 + } 572 }; 573 asyncRun().then(() => { 574 useUIState.getState().setSelectedBlock({
+11
components/Blocks/TextBlock/useHandlePaste.ts
··· 299 }, 300 }); 301 } 302 if (child.tagName === "A") { 303 let href = child.getAttribute("href"); 304 let dataType = child.getAttribute("data-type");
··· 299 }, 300 }); 301 } 302 + let textSize = child.getAttribute("data-text-size"); 303 + if (textSize && ["default", "small", "large"].includes(textSize)) { 304 + rep.mutate.assertFact({ 305 + entity: entityID, 306 + attribute: "block/text-size", 307 + data: { 308 + type: "text-size-union", 309 + value: textSize as "default" | "small" | "large", 310 + }, 311 + }); 312 + } 313 if (child.tagName === "A") { 314 let href = child.getAttribute("href"); 315 let dataType = child.getAttribute("data-type");
+6 -3
components/Canvas.tsx
··· 170 171 let pubRecord = pub.publications.record as PubLeafletPublication.Record; 172 let showComments = pubRecord.preferences?.showComments; 173 174 return ( 175 <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"> ··· 178 <CommentTiny className="text-border" /> โ€” 179 </div> 180 )} 181 - <div className="flex gap-1 text-tertiary items-center"> 182 - <QuoteTiny className="text-border" /> โ€” 183 - </div> 184 185 {!props.isSubpage && ( 186 <>
··· 170 171 let pubRecord = pub.publications.record as PubLeafletPublication.Record; 172 let showComments = pubRecord.preferences?.showComments; 173 + let showMentions = pubRecord.preferences?.showMentions; 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"> ··· 179 <CommentTiny className="text-border" /> โ€” 180 </div> 181 )} 182 + {showComments && ( 183 + <div className="flex gap-1 text-tertiary items-center"> 184 + <QuoteTiny className="text-border" /> โ€” 185 + </div> 186 + )} 187 188 {!props.isSubpage && ( 189 <>
+4 -2
components/InteractionsPreview.tsx
··· 14 tags?: string[]; 15 postUrl: string; 16 showComments: boolean | undefined; 17 share?: boolean; 18 }) => { 19 let smoker = useSmoker(); 20 let interactionsAvailable = 21 - props.quotesCount > 0 || 22 (props.showComments !== false && props.commentsCount > 0); 23 24 const tagsCount = props.tags?.length || 0; ··· 36 </> 37 )} 38 39 - {props.quotesCount === 0 ? null : ( 40 <SpeedyLink 41 aria-label="Post quotes" 42 href={`${props.postUrl}?interactionDrawer=quotes`}
··· 14 tags?: string[]; 15 postUrl: string; 16 showComments: boolean | undefined; 17 + showMentions: boolean | undefined; 18 + 19 share?: boolean; 20 }) => { 21 let smoker = useSmoker(); 22 let interactionsAvailable = 23 + (props.quotesCount > 0 && props.showMentions !== false) || 24 (props.showComments !== false && props.commentsCount > 0); 25 26 const tagsCount = props.tags?.length || 0; ··· 38 </> 39 )} 40 41 + {props.showMentions === false || props.quotesCount === 0 ? null : ( 42 <SpeedyLink 43 aria-label="Post quotes" 44 href={`${props.postUrl}?interactionDrawer=quotes`}
+9 -4
components/Pages/PublicationMetadata.tsx
··· 118 {tags && ( 119 <> 120 <AddTags /> 121 - <Separator classname="h-4!" /> 122 </> 123 )} 124 - <div className="flex gap-1 items-center"> 125 - <QuoteTiny />โ€” 126 - </div> 127 {pubRecord?.preferences?.showComments && ( 128 <div className="flex gap-1 items-center"> 129 <CommentTiny />โ€”
··· 118 {tags && ( 119 <> 120 <AddTags /> 121 + {pubRecord?.preferences?.showMentions || 122 + pubRecord?.preferences?.showComments ? ( 123 + <Separator classname="h-4!" /> 124 + ) : null} 125 </> 126 )} 127 + {pubRecord?.preferences?.showMentions && ( 128 + <div className="flex gap-1 items-center"> 129 + <QuoteTiny />โ€” 130 + </div> 131 + )} 132 {pubRecord?.preferences?.showComments && ( 133 <div className="flex gap-1 items-center"> 134 <CommentTiny />โ€”
+1
components/PostListing.tsx
··· 97 commentsCount={comments} 98 tags={tags} 99 showComments={pubRecord?.preferences?.showComments} 100 share 101 /> 102 </div>
··· 97 commentsCount={comments} 98 tags={tags} 99 showComments={pubRecord?.preferences?.showComments} 100 + showMentions={pubRecord?.preferences?.showMentions} 101 share 102 /> 103 </div>
+33 -27
components/ProfilePopover.tsx
··· 7 import { SpeedyLink } from "./SpeedyLink"; 8 import { Tooltip } from "./Tooltip"; 9 import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 10 11 export const ProfilePopover = (props: { 12 trigger: React.ReactNode; ··· 27 ); 28 29 return ( 30 - <Tooltip 31 className="max-w-sm p-0! text-center" 32 - asChild 33 trigger={ 34 - <a 35 className="no-underline" 36 - href={`https://leaflet.pub/p/${props.didOrHandle}`} 37 - target="_blank" 38 onPointerEnter={(e) => { 39 if (hoverTimeout.current) { 40 window.clearTimeout(hoverTimeout.current); ··· 53 }} 54 > 55 {props.trigger} 56 - </a> 57 } 58 onOpenChange={setIsOpen} 59 > ··· 66 publications={data.publications} 67 popover 68 /> 69 - <KnownFollowers viewer={data.profile.viewer} did={data.profile.did} /> 70 </div> 71 ) : ( 72 - <div className="text-secondary py-2 px-4">Profile not found</div> 73 )} 74 - </Tooltip> 75 ); 76 }; 77 78 - let KnownFollowers = (props: { 79 - viewer: ProfileViewDetailed["viewer"]; 80 - did: string; 81 - }) => { 82 - if (!props.viewer?.knownFollowers) return null; 83 - let count = props.viewer.knownFollowers.count; 84 return ( 85 - <> 86 - <hr className="border-border" /> 87 - Followed by{" "} 88 - <a 89 - className="hover:underline" 90 - href={`https://bsky.social/profile/${props.did}/known-followers`} 91 - target="_blank" 92 - > 93 - {props.viewer?.knownFollowers?.followers[0]?.displayName}{" "} 94 - {count > 1 ? `and ${count - 1} other${count > 2 ? "s" : ""}` : ""} 95 - </a> 96 - </> 97 ); 98 };
··· 7 import { SpeedyLink } from "./SpeedyLink"; 8 import { Tooltip } from "./Tooltip"; 9 import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 10 + import { BlueskyTiny } from "./Icons/BlueskyTiny"; 11 + import { ArrowRightTiny } from "./Icons/ArrowRightTiny"; 12 13 export const ProfilePopover = (props: { 14 trigger: React.ReactNode; ··· 29 ); 30 31 return ( 32 + <Popover 33 className="max-w-sm p-0! text-center" 34 trigger={ 35 + <div 36 className="no-underline" 37 onPointerEnter={(e) => { 38 if (hoverTimeout.current) { 39 window.clearTimeout(hoverTimeout.current); ··· 52 }} 53 > 54 {props.trigger} 55 + </div> 56 } 57 onOpenChange={setIsOpen} 58 > ··· 65 publications={data.publications} 66 popover 67 /> 68 + 69 + <ProfileLinks handle={data.profile.handle} /> 70 </div> 71 ) : ( 72 + <div className="text-secondary py-2 px-4">No profile found...</div> 73 )} 74 + </Popover> 75 ); 76 }; 77 78 + const ProfileLinks = (props: { handle: string }) => { 79 + let linkClassName = 80 + "flex gap-1.5 text-tertiary items-center border border-transparent px-1 rounded-md hover:bg-[var(--accent-light)] hover:border-accent-contrast hover:text-accent-contrast no-underline hover:no-underline"; 81 return ( 82 + <div className="w-full flex-col"> 83 + <hr className="border-border-light mt-3" /> 84 + <div className="flex gap-2 justify-between sm:px-4 px-3 py-2"> 85 + <div className="flex gap-2"> 86 + <a 87 + href={`https://bsky.app/profile/${props.handle}`} 88 + target="_blank" 89 + className={linkClassName} 90 + > 91 + <BlueskyTiny /> 92 + Bluesky 93 + </a> 94 + </div> 95 + <SpeedyLink 96 + href={`https://leaflet.pub/p/${props.handle}`} 97 + className={linkClassName} 98 + > 99 + Full profile <ArrowRightTiny /> 100 + </SpeedyLink> 101 + </div> 102 + </div> 103 ); 104 };
+148 -1
components/SelectionManager/index.tsx
··· 89 }, 90 { 91 metaKey: true, 92 shift: true, 93 key: ["ArrowDown", "J"], 94 handler: async () => { ··· 684 } 685 return null; 686 } 687 - 688 689 function toggleMarkInBlocks(blocks: string[], mark: MarkType, attrs?: any) { 690 let everyBlockHasMark = blocks.reduce((acc, block) => {
··· 89 }, 90 { 91 metaKey: true, 92 + altKey: true, 93 + key: ["1", "ยก"], 94 + handler: async () => { 95 + let [sortedBlocks] = await getSortedSelectionBound(); 96 + for (let block of sortedBlocks) { 97 + await rep?.mutate.assertFact({ 98 + entity: block.value, 99 + attribute: "block/heading-level", 100 + data: { type: "number", value: 1 }, 101 + }); 102 + await rep?.mutate.assertFact({ 103 + entity: block.value, 104 + attribute: "block/type", 105 + data: { type: "block-type-union", value: "heading" }, 106 + }); 107 + } 108 + }, 109 + }, 110 + { 111 + metaKey: true, 112 + altKey: true, 113 + key: ["2", "โ„ข"], 114 + handler: async () => { 115 + let [sortedBlocks] = await getSortedSelectionBound(); 116 + for (let block of sortedBlocks) { 117 + await rep?.mutate.assertFact({ 118 + entity: block.value, 119 + attribute: "block/heading-level", 120 + data: { type: "number", value: 2 }, 121 + }); 122 + await rep?.mutate.assertFact({ 123 + entity: block.value, 124 + attribute: "block/type", 125 + data: { type: "block-type-union", value: "heading" }, 126 + }); 127 + } 128 + }, 129 + }, 130 + { 131 + metaKey: true, 132 + altKey: true, 133 + key: ["3", "ยฃ"], 134 + handler: async () => { 135 + let [sortedBlocks] = await getSortedSelectionBound(); 136 + for (let block of sortedBlocks) { 137 + await rep?.mutate.assertFact({ 138 + entity: block.value, 139 + attribute: "block/heading-level", 140 + data: { type: "number", value: 3 }, 141 + }); 142 + await rep?.mutate.assertFact({ 143 + entity: block.value, 144 + attribute: "block/type", 145 + data: { type: "block-type-union", value: "heading" }, 146 + }); 147 + } 148 + }, 149 + }, 150 + { 151 + metaKey: true, 152 + altKey: true, 153 + key: ["0", "ยบ"], 154 + handler: async () => { 155 + let [sortedBlocks] = await getSortedSelectionBound(); 156 + for (let block of sortedBlocks) { 157 + // Convert to text block 158 + await rep?.mutate.assertFact({ 159 + entity: block.value, 160 + attribute: "block/type", 161 + data: { type: "block-type-union", value: "text" }, 162 + }); 163 + // Remove heading level if exists 164 + let headingLevel = await rep?.query((tx) => 165 + scanIndex(tx).eav(block.value, "block/heading-level"), 166 + ); 167 + if (headingLevel?.[0]) { 168 + await rep?.mutate.retractFact({ factID: headingLevel[0].id }); 169 + } 170 + // Remove text-size to make it default 171 + let textSizeFact = await rep?.query((tx) => 172 + scanIndex(tx).eav(block.value, "block/text-size"), 173 + ); 174 + if (textSizeFact?.[0]) { 175 + await rep?.mutate.retractFact({ factID: textSizeFact[0].id }); 176 + } 177 + } 178 + }, 179 + }, 180 + { 181 + metaKey: true, 182 + altKey: true, 183 + key: ["+", "โ‰ "], 184 + handler: async () => { 185 + let [sortedBlocks] = await getSortedSelectionBound(); 186 + for (let block of sortedBlocks) { 187 + // Convert to text block 188 + await rep?.mutate.assertFact({ 189 + entity: block.value, 190 + attribute: "block/type", 191 + data: { type: "block-type-union", value: "text" }, 192 + }); 193 + // Remove heading level if exists 194 + let headingLevel = await rep?.query((tx) => 195 + scanIndex(tx).eav(block.value, "block/heading-level"), 196 + ); 197 + if (headingLevel?.[0]) { 198 + await rep?.mutate.retractFact({ factID: headingLevel[0].id }); 199 + } 200 + // Set text size to large 201 + await rep?.mutate.assertFact({ 202 + entity: block.value, 203 + attribute: "block/text-size", 204 + data: { type: "text-size-union", value: "large" }, 205 + }); 206 + } 207 + }, 208 + }, 209 + { 210 + metaKey: true, 211 + altKey: true, 212 + key: ["-", "โ€“"], 213 + handler: async () => { 214 + let [sortedBlocks] = await getSortedSelectionBound(); 215 + for (let block of sortedBlocks) { 216 + // Convert to text block 217 + await rep?.mutate.assertFact({ 218 + entity: block.value, 219 + attribute: "block/type", 220 + data: { type: "block-type-union", value: "text" }, 221 + }); 222 + // Remove heading level if exists 223 + let headingLevel = await rep?.query((tx) => 224 + scanIndex(tx).eav(block.value, "block/heading-level"), 225 + ); 226 + if (headingLevel?.[0]) { 227 + await rep?.mutate.retractFact({ factID: headingLevel[0].id }); 228 + } 229 + // Set text size to small 230 + await rep?.mutate.assertFact({ 231 + entity: block.value, 232 + attribute: "block/text-size", 233 + data: { type: "text-size-union", value: "small" }, 234 + }); 235 + } 236 + }, 237 + }, 238 + { 239 + metaKey: true, 240 shift: true, 241 key: ["ArrowDown", "J"], 242 handler: async () => { ··· 832 } 833 return null; 834 } 835 836 function toggleMarkInBlocks(blocks: string[], mark: MarkType, attrs?: any) { 837 let everyBlockHasMark = blocks.reduce((acc, block) => {
+2 -2
components/ThemeManager/Pickers/PageWidthSetter.tsx
··· 89 <div 90 className={`w-full cursor-pointer ${selectedPreset === "default" ? "text-[#595959]" : "text-[#969696]"}`} 91 > 92 - default (624px) 93 </div> 94 </Radio> 95 </label> ··· 111 <div 112 className={`w-full cursor-pointer ${selectedPreset === "wide" ? "text-[#595959]" : "text-[#969696]"}`} 113 > 114 - wide (756px) 115 </div> 116 </Radio> 117 </label>
··· 89 <div 90 className={`w-full cursor-pointer ${selectedPreset === "default" ? "text-[#595959]" : "text-[#969696]"}`} 91 > 92 + default ({defaultPreset}px) 93 </div> 94 </Radio> 95 </label> ··· 111 <div 112 className={`w-full cursor-pointer ${selectedPreset === "wide" ? "text-[#595959]" : "text-[#969696]"}`} 113 > 114 + wide ({widePreset}px) 115 </div> 116 </Radio> 117 </label>
+2 -2
components/ThemeManager/PubPickers/PubPageWidthSetter.tsx
··· 76 <div 77 className={`w-full cursor-pointer ${selectedPreset === "default" ? "text-[#595959]" : "text-[#969696]"}`} 78 > 79 - default (624px) 80 </div> 81 </Radio> 82 </label> ··· 98 <div 99 className={`w-full cursor-pointer ${selectedPreset === "wide" ? "text-[#595959]" : "text-[#969696]"}`} 100 > 101 - wide (756px) 102 </div> 103 </Radio> 104 </label>
··· 76 <div 77 className={`w-full cursor-pointer ${selectedPreset === "default" ? "text-[#595959]" : "text-[#969696]"}`} 78 > 79 + default ({defaultPreset}px) 80 </div> 81 </Radio> 82 </label> ··· 98 <div 99 className={`w-full cursor-pointer ${selectedPreset === "wide" ? "text-[#595959]" : "text-[#969696]"}`} 100 > 101 + wide ({widePreset}px) 102 </div> 103 </Radio> 104 </label>
+7 -7
components/ThemeManager/PublicationThemeProvider.tsx
··· 2 import { useMemo, useState } from "react"; 3 import { parseColor } from "react-aria-components"; 4 import { useEntity } from "src/replicache"; 5 - import { getColorContrast } from "./themeUtils"; 6 import { useColorAttribute, colorToString } from "./useColorAttribute"; 7 import { BaseThemeProvider, CardBorderHiddenContext } from "./ThemeProvider"; 8 import { PubLeafletPublication, PubLeafletThemeColor } from "lexicons/api"; ··· 174 let newAccentContrast; 175 let sortedAccents = [newTheme.accent1, newTheme.accent2].sort((a, b) => { 176 return ( 177 - getColorContrast( 178 colorToString(b, "rgb"), 179 colorToString( 180 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, 181 "rgb", 182 ), 183 ) - 184 - getColorContrast( 185 colorToString(a, "rgb"), 186 colorToString( 187 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, ··· 191 ); 192 }); 193 if ( 194 - getColorContrast( 195 colorToString(sortedAccents[0], "rgb"), 196 colorToString(newTheme.primary, "rgb"), 197 - ) < 30 && 198 - getColorContrast( 199 colorToString(sortedAccents[1], "rgb"), 200 colorToString( 201 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, 202 "rgb", 203 ), 204 - ) > 12 205 ) { 206 newAccentContrast = sortedAccents[1]; 207 } else newAccentContrast = sortedAccents[0];
··· 2 import { useMemo, useState } from "react"; 3 import { parseColor } from "react-aria-components"; 4 import { useEntity } from "src/replicache"; 5 + import { getColorDifference } from "./themeUtils"; 6 import { useColorAttribute, colorToString } from "./useColorAttribute"; 7 import { BaseThemeProvider, CardBorderHiddenContext } from "./ThemeProvider"; 8 import { PubLeafletPublication, PubLeafletThemeColor } from "lexicons/api"; ··· 174 let newAccentContrast; 175 let sortedAccents = [newTheme.accent1, newTheme.accent2].sort((a, b) => { 176 return ( 177 + getColorDifference( 178 colorToString(b, "rgb"), 179 colorToString( 180 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, 181 "rgb", 182 ), 183 ) - 184 + getColorDifference( 185 colorToString(a, "rgb"), 186 colorToString( 187 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, ··· 191 ); 192 }); 193 if ( 194 + getColorDifference( 195 colorToString(sortedAccents[0], "rgb"), 196 colorToString(newTheme.primary, "rgb"), 197 + ) < 0.15 && 198 + getColorDifference( 199 colorToString(sortedAccents[1], "rgb"), 200 colorToString( 201 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, 202 "rgb", 203 ), 204 + ) > 0.08 205 ) { 206 newAccentContrast = sortedAccents[1]; 207 } else newAccentContrast = sortedAccents[0];
+9 -9
components/ThemeManager/ThemeProvider.tsx
··· 22 PublicationThemeProvider, 23 } from "./PublicationThemeProvider"; 24 import { PubLeafletPublication } from "lexicons/api"; 25 - import { getColorContrast } from "./themeUtils"; 26 27 // define a function to set an Aria Color to a CSS Variable in RGB 28 function setCSSVariableToColor( ··· 140 //sorting the accents by contrast on background 141 let sortedAccents = [accent1, accent2].sort((a, b) => { 142 return ( 143 - getColorContrast( 144 colorToString(b, "rgb"), 145 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 146 ) - 147 - getColorContrast( 148 colorToString(a, "rgb"), 149 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 150 ) ··· 156 // then use the not contrasty option 157 158 if ( 159 - getColorContrast( 160 colorToString(sortedAccents[0], "rgb"), 161 colorToString(primary, "rgb"), 162 - ) < 30 && 163 - getColorContrast( 164 colorToString(sortedAccents[1], "rgb"), 165 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 166 - ) > 12 167 ) { 168 accentContrast = sortedAccents[1]; 169 } else accentContrast = sortedAccents[0]; ··· 286 bgPage && accent1 && accent2 287 ? [accent1, accent2].sort((a, b) => { 288 return ( 289 - getColorContrast( 290 colorToString(b, "rgb"), 291 colorToString(bgPage, "rgb"), 292 ) - 293 - getColorContrast( 294 colorToString(a, "rgb"), 295 colorToString(bgPage, "rgb"), 296 )
··· 22 PublicationThemeProvider, 23 } from "./PublicationThemeProvider"; 24 import { PubLeafletPublication } from "lexicons/api"; 25 + import { getColorDifference } from "./themeUtils"; 26 27 // define a function to set an Aria Color to a CSS Variable in RGB 28 function setCSSVariableToColor( ··· 140 //sorting the accents by contrast on background 141 let sortedAccents = [accent1, accent2].sort((a, b) => { 142 return ( 143 + getColorDifference( 144 colorToString(b, "rgb"), 145 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 146 ) - 147 + getColorDifference( 148 colorToString(a, "rgb"), 149 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 150 ) ··· 156 // then use the not contrasty option 157 158 if ( 159 + getColorDifference( 160 colorToString(sortedAccents[0], "rgb"), 161 colorToString(primary, "rgb"), 162 + ) < 0.15 && 163 + getColorDifference( 164 colorToString(sortedAccents[1], "rgb"), 165 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 166 + ) > 0.08 167 ) { 168 accentContrast = sortedAccents[1]; 169 } else accentContrast = sortedAccents[0]; ··· 286 bgPage && accent1 && accent2 287 ? [accent1, accent2].sort((a, b) => { 288 return ( 289 + getColorDifference( 290 colorToString(b, "rgb"), 291 colorToString(bgPage, "rgb"), 292 ) - 293 + getColorDifference( 294 colorToString(a, "rgb"), 295 colorToString(bgPage, "rgb"), 296 )
+2 -3
components/ThemeManager/ThemeSetter.tsx
··· 1 "use client"; 2 import { Popover } from "components/Popover"; 3 - import { theme } from "../../tailwind.config"; 4 5 import { Color } from "react-aria-components"; 6 ··· 166 setOpenPicker={(pickers) => setOpenPicker(pickers)} 167 /> 168 <SectionArrow 169 - fill={theme.colors["accent-2"]} 170 - stroke={theme.colors["accent-1"]} 171 className="ml-2" 172 /> 173 </div>
··· 1 "use client"; 2 import { Popover } from "components/Popover"; 3 4 import { Color } from "react-aria-components"; 5 ··· 165 setOpenPicker={(pickers) => setOpenPicker(pickers)} 166 /> 167 <SectionArrow 168 + fill="rgb(var(--accent-2))" 169 + stroke="rgb(var(--accent-1))" 170 className="ml-2" 171 /> 172 </div>
+4 -3
components/ThemeManager/themeUtils.ts
··· 1 - import { parse, contrastLstar, ColorSpace, sRGB } from "colorjs.io/fn"; 2 3 // define the color defaults for everything 4 export const ThemeDefaults = { ··· 17 }; 18 19 // used to calculate the contrast between page and accent1, accent2, and determin which is higher contrast 20 - export function getColorContrast(color1: string, color2: string) { 21 ColorSpace.register(sRGB); 22 23 let parsedColor1 = parse(`rgb(${color1})`); 24 let parsedColor2 = parse(`rgb(${color2})`); 25 26 - return contrastLstar(parsedColor1, parsedColor2); 27 }
··· 1 + import { parse, ColorSpace, sRGB, distance, OKLab } from "colorjs.io/fn"; 2 3 // define the color defaults for everything 4 export const ThemeDefaults = { ··· 17 }; 18 19 // used to calculate the contrast between page and accent1, accent2, and determin which is higher contrast 20 + export function getColorDifference(color1: string, color2: string) { 21 ColorSpace.register(sRGB); 22 + ColorSpace.register(OKLab); 23 24 let parsedColor1 = parse(`rgb(${color1})`); 25 let parsedColor2 = parse(`rgb(${color2})`); 26 27 + return distance(parsedColor1, parsedColor2, "oklab"); 28 }
+9 -5
components/Toolbar/BlockToolbar.tsx
··· 5 import { useUIState } from "src/useUIState"; 6 import { LockBlockButton } from "./LockBlockButton"; 7 import { TextAlignmentButton } from "./TextAlignmentToolbar"; 8 - import { ImageFullBleedButton, ImageAltTextButton, ImageCoverButton } from "./ImageToolbar"; 9 import { DeleteSmall } from "components/Icons/DeleteSmall"; 10 import { getSortedSelection } from "components/SelectionManager/selectionState"; 11 ··· 37 > 38 <DeleteSmall /> 39 </ToolbarButton> 40 - <Separator classname="h-6" /> 41 <MoveBlockButtons /> 42 {blockType === "image" && ( 43 <> ··· 46 <ImageAltTextButton setToolbarState={props.setToolbarState} /> 47 <ImageCoverButton /> 48 {focusedEntityType?.data.value !== "canvas" && ( 49 - <Separator classname="h-6" /> 50 )} 51 </> 52 )} ··· 54 <> 55 <TextAlignmentButton setToolbarState={props.setToolbarState} /> 56 {focusedEntityType?.data.value !== "canvas" && ( 57 - <Separator classname="h-6" /> 58 )} 59 </> 60 )} ··· 175 > 176 <MoveBlockDown /> 177 </ToolbarButton> 178 - <Separator classname="h-6" /> 179 </> 180 ); 181 };
··· 5 import { useUIState } from "src/useUIState"; 6 import { LockBlockButton } from "./LockBlockButton"; 7 import { TextAlignmentButton } from "./TextAlignmentToolbar"; 8 + import { 9 + ImageFullBleedButton, 10 + ImageAltTextButton, 11 + ImageCoverButton, 12 + } from "./ImageToolbar"; 13 import { DeleteSmall } from "components/Icons/DeleteSmall"; 14 import { getSortedSelection } from "components/SelectionManager/selectionState"; 15 ··· 41 > 42 <DeleteSmall /> 43 </ToolbarButton> 44 + <Separator classname="h-6!" /> 45 <MoveBlockButtons /> 46 {blockType === "image" && ( 47 <> ··· 50 <ImageAltTextButton setToolbarState={props.setToolbarState} /> 51 <ImageCoverButton /> 52 {focusedEntityType?.data.value !== "canvas" && ( 53 + <Separator classname="h-6!" /> 54 )} 55 </> 56 )} ··· 58 <> 59 <TextAlignmentButton setToolbarState={props.setToolbarState} /> 60 {focusedEntityType?.data.value !== "canvas" && ( 61 + <Separator classname="h-6!" /> 62 )} 63 </> 64 )} ··· 179 > 180 <MoveBlockDown /> 181 </ToolbarButton> 182 + <Separator classname="h-6!" /> 183 </> 184 ); 185 };
+1 -1
components/Toolbar/HighlightToolbar.tsx
··· 126 setLastUsedHightlight={props.setLastUsedHighlight} 127 /> 128 129 - <Separator classname="h-6" /> 130 <HighlightColorSettings pageID={props.pageID} /> 131 </div> 132 </div>
··· 126 setLastUsedHightlight={props.setLastUsedHighlight} 127 /> 128 129 + <Separator classname="h-6!" /> 130 <HighlightColorSettings pageID={props.pageID} /> 131 </div> 132 </div>
+1 -1
components/Toolbar/InlineLinkToolbar.tsx
··· 132 return ( 133 <div className="w-full flex items-center gap-[6px] grow"> 134 <LinkSmall /> 135 - <Separator classname="h-6" /> 136 <Input 137 autoFocus 138 className="w-full grow bg-transparent border-none outline-hidden "
··· 132 return ( 133 <div className="w-full flex items-center gap-[6px] grow"> 134 <LinkSmall /> 135 + <Separator classname="h-6!" /> 136 <Input 137 autoFocus 138 className="w-full grow bg-transparent border-none outline-hidden "
+2 -2
components/Toolbar/ListToolbar.tsx
··· 131 > 132 <ListIndentIncreaseSmall /> 133 </ToolbarButton> 134 - <Separator classname="h-6" /> 135 <ToolbarButton 136 disabled={!isList?.data.value} 137 tooltipContent=<div className="flex flex-col gap-1 justify-center"> 138 <div className="text-center">Add a Checkbox</div> 139 <div className="flex gap-1 font-normal"> 140 - start line with <ShortcutKey>[</ShortcutKey> 141 <ShortcutKey>]</ShortcutKey> 142 </div> 143 </div>
··· 131 > 132 <ListIndentIncreaseSmall /> 133 </ToolbarButton> 134 + <Separator classname="h-6!" /> 135 <ToolbarButton 136 disabled={!isList?.data.value} 137 tooltipContent=<div className="flex flex-col gap-1 justify-center"> 138 <div className="text-center">Add a Checkbox</div> 139 <div className="flex gap-1 font-normal"> 140 + <ShortcutKey>[</ShortcutKey> 141 <ShortcutKey>]</ShortcutKey> 142 </div> 143 </div>
+154 -95
components/Toolbar/TextBlockTypeToolbar.tsx
··· 4 Header3Small, 5 } from "components/Icons/BlockTextSmall"; 6 import { Props } from "components/Icons/Props"; 7 - import { ShortcutKey } from "components/Layout"; 8 import { ToolbarButton } from "components/Toolbar"; 9 import { TextSelection } from "prosemirror-state"; 10 import { useCallback } from "react"; ··· 22 focusedBlock?.entityID || null, 23 "block/heading-level", 24 ); 25 let { rep } = useReplicache(); 26 27 let setLevel = useCallback( ··· 51 ); 52 return ( 53 // This Toolbar should close once the user starts typing again 54 - <div className="flex w-full justify-between items-center gap-4"> 55 - <div className="flex items-center gap-[6px]"> 56 - <ToolbarButton 57 - className={props.className} 58 - onClick={() => { 59 - setLevel(1); 60 - }} 61 - active={ 62 - blockType?.data.value === "heading" && 63 - headingLevel?.data.value === 1 64 - } 65 - tooltipContent={ 66 - <div className="flex flex-col justify-center"> 67 - <div className="font-bold text-center">Title</div> 68 - <div className="flex gap-1 font-normal"> 69 - start line with 70 - <ShortcutKey>#</ShortcutKey> 71 - </div> 72 </div> 73 - } 74 - > 75 - <Header1Small /> 76 - </ToolbarButton> 77 - <ToolbarButton 78 - className={props.className} 79 - onClick={() => { 80 - setLevel(2); 81 - }} 82 - active={ 83 - blockType?.data.value === "heading" && 84 - headingLevel?.data.value === 2 85 - } 86 - tooltipContent={ 87 - <div className="flex flex-col justify-center"> 88 - <div className="font-bold text-center">Heading</div> 89 - <div className="flex gap-1 font-normal"> 90 - start line with 91 - <ShortcutKey>##</ShortcutKey> 92 - </div> 93 </div> 94 } 95 - > 96 - <Header2Small /> 97 - </ToolbarButton> 98 - <ToolbarButton 99 - className={props.className} 100 - onClick={() => { 101 - setLevel(3); 102 - }} 103 - active={ 104 - blockType?.data.value === "heading" && 105 - headingLevel?.data.value === 3 106 - } 107 - tooltipContent={ 108 - <div className="flex flex-col justify-center"> 109 - <div className="font-bold text-center">Subheading</div> 110 - <div className="flex gap-1 font-normal"> 111 - start line with 112 - <ShortcutKey>###</ShortcutKey> 113 - </div> 114 - </div> 115 } 116 - > 117 - <Header3Small /> 118 - </ToolbarButton> 119 - <ToolbarButton 120 - className={`px-[6px] ${props.className}`} 121 - onClick={async () => { 122 if (headingLevel) 123 await rep?.mutate.retractFact({ factID: headingLevel.id }); 124 - if (!focusedBlock || !blockType) return; 125 - if (blockType.data.value !== "text") { 126 - let existingEditor = 127 - useEditorStates.getState().editorStates[focusedBlock.entityID]; 128 - let selection = existingEditor?.editor.selection; 129 - await rep?.mutate.assertFact({ 130 - entity: focusedBlock?.entityID, 131 - attribute: "block/type", 132 - data: { type: "block-type-union", value: "text" }, 133 - }); 134 - 135 - let newEditor = 136 - useEditorStates.getState().editorStates[focusedBlock.entityID]; 137 - if (!newEditor || !selection) return; 138 - newEditor.view?.dispatch( 139 - newEditor.editor.tr.setSelection( 140 - TextSelection.create(newEditor.editor.doc, selection.anchor), 141 - ), 142 - ); 143 - 144 - newEditor.view?.focus(); 145 - } 146 - }} 147 - active={blockType?.data.value === "text"} 148 - tooltipContent={<div>Paragraph</div>} 149 - > 150 - Paragraph 151 - </ToolbarButton> 152 - </div> 153 - </div> 154 ); 155 }; 156
··· 4 Header3Small, 5 } from "components/Icons/BlockTextSmall"; 6 import { Props } from "components/Icons/Props"; 7 + import { ShortcutKey, Separator } from "components/Layout"; 8 import { ToolbarButton } from "components/Toolbar"; 9 import { TextSelection } from "prosemirror-state"; 10 import { useCallback } from "react"; ··· 22 focusedBlock?.entityID || null, 23 "block/heading-level", 24 ); 25 + 26 + let textSize = useEntity(focusedBlock?.entityID || null, "block/text-size"); 27 let { rep } = useReplicache(); 28 29 let setLevel = useCallback( ··· 53 ); 54 return ( 55 // This Toolbar should close once the user starts typing again 56 + <> 57 + <ToolbarButton 58 + className={props.className} 59 + onClick={() => { 60 + setLevel(1); 61 + }} 62 + active={ 63 + blockType?.data.value === "heading" && headingLevel?.data.value === 1 64 + } 65 + tooltipContent={ 66 + <div className="flex flex-col justify-center"> 67 + <div className="font-bold text-center">Title</div> 68 + <div className="flex gap-1 font-normal"> 69 + start line with 70 + <ShortcutKey>#</ShortcutKey> 71 + </div> 72 + </div> 73 + } 74 + > 75 + <Header1Small /> 76 + </ToolbarButton> 77 + <ToolbarButton 78 + className={props.className} 79 + onClick={() => { 80 + setLevel(2); 81 + }} 82 + active={ 83 + blockType?.data.value === "heading" && headingLevel?.data.value === 2 84 + } 85 + tooltipContent={ 86 + <div className="flex flex-col justify-center"> 87 + <div className="font-bold text-center">Heading</div> 88 + <div className="flex gap-1 font-normal"> 89 + start line with 90 + <ShortcutKey>##</ShortcutKey> 91 </div> 92 + </div> 93 + } 94 + > 95 + <Header2Small /> 96 + </ToolbarButton> 97 + <ToolbarButton 98 + className={props.className} 99 + onClick={() => { 100 + setLevel(3); 101 + }} 102 + active={ 103 + blockType?.data.value === "heading" && headingLevel?.data.value === 3 104 + } 105 + tooltipContent={ 106 + <div className="flex flex-col justify-center"> 107 + <div className="font-bold text-center">Subheading</div> 108 + <div className="flex gap-1 font-normal"> 109 + start line with 110 + <ShortcutKey>###</ShortcutKey> 111 </div> 112 + </div> 113 + } 114 + > 115 + <Header3Small /> 116 + </ToolbarButton> 117 + <Separator classname="h-6!" /> 118 + <ToolbarButton 119 + className={`px-[6px] ${props.className}`} 120 + onClick={async () => { 121 + if (headingLevel) 122 + await rep?.mutate.retractFact({ factID: headingLevel.id }); 123 + if (textSize) await rep?.mutate.retractFact({ factID: textSize.id }); 124 + if (!focusedBlock || !blockType) return; 125 + if (blockType.data.value !== "text") { 126 + let existingEditor = 127 + useEditorStates.getState().editorStates[focusedBlock.entityID]; 128 + let selection = existingEditor?.editor.selection; 129 + await rep?.mutate.assertFact({ 130 + entity: focusedBlock?.entityID, 131 + attribute: "block/type", 132 + data: { type: "block-type-union", value: "text" }, 133 + }); 134 + 135 + let newEditor = 136 + useEditorStates.getState().editorStates[focusedBlock.entityID]; 137 + if (!newEditor || !selection) return; 138 + newEditor.view?.dispatch( 139 + newEditor.editor.tr.setSelection( 140 + TextSelection.create(newEditor.editor.doc, selection.anchor), 141 + ), 142 + ); 143 + 144 + newEditor.view?.focus(); 145 } 146 + }} 147 + active={ 148 + blockType?.data.value === "text" && 149 + textSize?.data.value !== "small" && 150 + textSize?.data.value !== "large" 151 + } 152 + tooltipContent={<div>Normal Text</div>} 153 + > 154 + Text 155 + </ToolbarButton> 156 + <ToolbarButton 157 + className={`px-[6px] text-lg ${props.className}`} 158 + onClick={async () => { 159 + if (!focusedBlock || !blockType) return; 160 + if (blockType.data.value !== "text") { 161 + // Convert to text block first if it's a heading 162 + if (headingLevel) 163 + await rep?.mutate.retractFact({ factID: headingLevel.id }); 164 + await rep?.mutate.assertFact({ 165 + entity: focusedBlock.entityID, 166 + attribute: "block/type", 167 + data: { type: "block-type-union", value: "text" }, 168 + }); 169 } 170 + // Set text size to large 171 + await rep?.mutate.assertFact({ 172 + entity: focusedBlock.entityID, 173 + attribute: "block/text-size", 174 + data: { type: "text-size-union", value: "large" }, 175 + }); 176 + }} 177 + active={ 178 + blockType?.data.value === "text" && textSize?.data.value === "large" 179 + } 180 + tooltipContent={<div>Large Text</div>} 181 + > 182 + <div className="leading-[1.625rem]">Large</div> 183 + </ToolbarButton> 184 + <ToolbarButton 185 + className={`px-[6px] text-sm text-secondary ${props.className}`} 186 + onClick={async () => { 187 + if (!focusedBlock || !blockType) return; 188 + if (blockType.data.value !== "text") { 189 + // Convert to text block first if it's a heading 190 if (headingLevel) 191 await rep?.mutate.retractFact({ factID: headingLevel.id }); 192 + await rep?.mutate.assertFact({ 193 + entity: focusedBlock.entityID, 194 + attribute: "block/type", 195 + data: { type: "block-type-union", value: "text" }, 196 + }); 197 + } 198 + // Set text size to small 199 + await rep?.mutate.assertFact({ 200 + entity: focusedBlock.entityID, 201 + attribute: "block/text-size", 202 + data: { type: "text-size-union", value: "small" }, 203 + }); 204 + }} 205 + active={ 206 + blockType?.data.value === "text" && textSize?.data.value === "small" 207 + } 208 + tooltipContent={<div>Small Text</div>} 209 + > 210 + <div className="leading-[1.625rem]">Small</div> 211 + </ToolbarButton> 212 + </> 213 ); 214 }; 215
+3 -3
components/Toolbar/TextToolbar.tsx
··· 74 lastUsedHighlight={props.lastUsedHighlight} 75 setToolbarState={props.setToolbarState} 76 /> 77 - <Separator classname="h-6" /> 78 <LinkButton setToolbarState={props.setToolbarState} /> 79 - <Separator classname="h-6" /> 80 <TextBlockTypeButton setToolbarState={props.setToolbarState} /> 81 <TextAlignmentButton setToolbarState={props.setToolbarState} /> 82 <ListButton setToolbarState={props.setToolbarState} /> 83 - <Separator classname="h-6" /> 84 85 <LockBlockButton /> 86 </>
··· 74 lastUsedHighlight={props.lastUsedHighlight} 75 setToolbarState={props.setToolbarState} 76 /> 77 + <Separator classname="h-6!" /> 78 <LinkButton setToolbarState={props.setToolbarState} /> 79 + <Separator classname="h-6!" /> 80 <TextBlockTypeButton setToolbarState={props.setToolbarState} /> 81 <TextAlignmentButton setToolbarState={props.setToolbarState} /> 82 <ListButton setToolbarState={props.setToolbarState} /> 83 + <Separator classname="h-6!" /> 84 85 <LockBlockButton /> 86 </>
+2 -2
components/utils/DotLoader.tsx
··· 1 import { useEffect, useState } from "react"; 2 3 - export function DotLoader() { 4 let [dots, setDots] = useState(1); 5 useEffect(() => { 6 let id = setInterval(() => { ··· 11 }; 12 }, []); 13 return ( 14 - <div className="w-[26px] h-[24px] text-center text-sm"> 15 {".".repeat(dots) + "\u00a0".repeat(3 - dots)} 16 </div> 17 );
··· 1 import { useEffect, useState } from "react"; 2 3 + export function DotLoader(props: { className?: string }) { 4 let [dots, setDots] = useState(1); 5 useEffect(() => { 6 let id = setInterval(() => { ··· 11 }; 12 }, []); 13 return ( 14 + <div className={`w-[26px] h-[24px] text-center text-sm ${props.className}`}> 15 {".".repeat(dots) + "\u00a0".repeat(3 - dots)} 16 </div> 17 );
+841 -829
lexicons/api/lexicons.ts
··· 6 Lexicons, 7 ValidationError, 8 type ValidationResult, 9 - } from "@atproto/lexicon"; 10 - import { type $Typed, is$typed, maybe$typed } from "./util"; 11 12 export const schemaDict = { 13 AppBskyActorProfile: { 14 lexicon: 1, 15 - id: "app.bsky.actor.profile", 16 defs: { 17 main: { 18 - type: "record", 19 - description: "A declaration of a Bluesky account profile.", 20 - key: "literal:self", 21 record: { 22 - type: "object", 23 properties: { 24 displayName: { 25 - type: "string", 26 maxGraphemes: 64, 27 maxLength: 640, 28 }, 29 description: { 30 - type: "string", 31 - description: "Free-form profile description text.", 32 maxGraphemes: 256, 33 maxLength: 2560, 34 }, 35 avatar: { 36 - type: "blob", 37 description: 38 "Small image to be displayed next to posts from account. AKA, 'profile picture'", 39 - accept: ["image/png", "image/jpeg"], 40 maxSize: 1000000, 41 }, 42 banner: { 43 - type: "blob", 44 description: 45 - "Larger horizontal image to display behind profile view.", 46 - accept: ["image/png", "image/jpeg"], 47 maxSize: 1000000, 48 }, 49 labels: { 50 - type: "union", 51 description: 52 - "Self-label values, specific to the Bluesky application, on the overall account.", 53 - refs: ["lex:com.atproto.label.defs#selfLabels"], 54 }, 55 joinedViaStarterPack: { 56 - type: "ref", 57 - ref: "lex:com.atproto.repo.strongRef", 58 }, 59 pinnedPost: { 60 - type: "ref", 61 - ref: "lex:com.atproto.repo.strongRef", 62 }, 63 createdAt: { 64 - type: "string", 65 - format: "datetime", 66 }, 67 }, 68 }, ··· 71 }, 72 ComAtprotoLabelDefs: { 73 lexicon: 1, 74 - id: "com.atproto.label.defs", 75 defs: { 76 label: { 77 - type: "object", 78 description: 79 - "Metadata tag on an atproto resource (eg, repo or record).", 80 - required: ["src", "uri", "val", "cts"], 81 properties: { 82 ver: { 83 - type: "integer", 84 - description: "The AT Protocol version of the label object.", 85 }, 86 src: { 87 - type: "string", 88 - format: "did", 89 - description: "DID of the actor who created this label.", 90 }, 91 uri: { 92 - type: "string", 93 - format: "uri", 94 description: 95 - "AT URI of the record, repository (account), or other resource that this label applies to.", 96 }, 97 cid: { 98 - type: "string", 99 - format: "cid", 100 description: 101 "Optionally, CID specifying the specific version of 'uri' resource this label applies to.", 102 }, 103 val: { 104 - type: "string", 105 maxLength: 128, 106 description: 107 - "The short string name of the value or type of this label.", 108 }, 109 neg: { 110 - type: "boolean", 111 description: 112 - "If true, this is a negation label, overwriting a previous label.", 113 }, 114 cts: { 115 - type: "string", 116 - format: "datetime", 117 - description: "Timestamp when this label was created.", 118 }, 119 exp: { 120 - type: "string", 121 - format: "datetime", 122 description: 123 - "Timestamp at which this label expires (no longer applies).", 124 }, 125 sig: { 126 - type: "bytes", 127 - description: "Signature of dag-cbor encoded label.", 128 }, 129 }, 130 }, 131 selfLabels: { 132 - type: "object", 133 description: 134 - "Metadata tags on an atproto record, published by the author within the record.", 135 - required: ["values"], 136 properties: { 137 values: { 138 - type: "array", 139 items: { 140 - type: "ref", 141 - ref: "lex:com.atproto.label.defs#selfLabel", 142 }, 143 maxLength: 10, 144 }, 145 }, 146 }, 147 selfLabel: { 148 - type: "object", 149 description: 150 - "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.", 151 - required: ["val"], 152 properties: { 153 val: { 154 - type: "string", 155 maxLength: 128, 156 description: 157 - "The short string name of the value or type of this label.", 158 }, 159 }, 160 }, 161 labelValueDefinition: { 162 - type: "object", 163 description: 164 - "Declares a label value and its expected interpretations and behaviors.", 165 - required: ["identifier", "severity", "blurs", "locales"], 166 properties: { 167 identifier: { 168 - type: "string", 169 description: 170 "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", 171 maxLength: 100, 172 maxGraphemes: 100, 173 }, 174 severity: { 175 - type: "string", 176 description: 177 "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", 178 - knownValues: ["inform", "alert", "none"], 179 }, 180 blurs: { 181 - type: "string", 182 description: 183 "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.", 184 - knownValues: ["content", "media", "none"], 185 }, 186 defaultSetting: { 187 - type: "string", 188 - description: "The default setting for this label.", 189 - knownValues: ["ignore", "warn", "hide"], 190 - default: "warn", 191 }, 192 adultOnly: { 193 - type: "boolean", 194 description: 195 - "Does the user need to have adult content enabled in order to configure this label?", 196 }, 197 locales: { 198 - type: "array", 199 items: { 200 - type: "ref", 201 - ref: "lex:com.atproto.label.defs#labelValueDefinitionStrings", 202 }, 203 }, 204 }, 205 }, 206 labelValueDefinitionStrings: { 207 - type: "object", 208 description: 209 - "Strings which describe the label in the UI, localized into a specific language.", 210 - required: ["lang", "name", "description"], 211 properties: { 212 lang: { 213 - type: "string", 214 description: 215 - "The code of the language these strings are written in.", 216 - format: "language", 217 }, 218 name: { 219 - type: "string", 220 - description: "A short human-readable name for the label.", 221 maxGraphemes: 64, 222 maxLength: 640, 223 }, 224 description: { 225 - type: "string", 226 description: 227 - "A longer description of what the label means and why it might be applied.", 228 maxGraphemes: 10000, 229 maxLength: 100000, 230 }, 231 }, 232 }, 233 labelValue: { 234 - type: "string", 235 knownValues: [ 236 - "!hide", 237 - "!no-promote", 238 - "!warn", 239 - "!no-unauthenticated", 240 - "dmca-violation", 241 - "doxxing", 242 - "porn", 243 - "sexual", 244 - "nudity", 245 - "nsfl", 246 - "gore", 247 ], 248 }, 249 }, 250 }, 251 ComAtprotoRepoApplyWrites: { 252 lexicon: 1, 253 - id: "com.atproto.repo.applyWrites", 254 defs: { 255 main: { 256 - type: "procedure", 257 description: 258 - "Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.", 259 input: { 260 - encoding: "application/json", 261 schema: { 262 - type: "object", 263 - required: ["repo", "writes"], 264 properties: { 265 repo: { 266 - type: "string", 267 - format: "at-identifier", 268 description: 269 - "The handle or DID of the repo (aka, current account).", 270 }, 271 validate: { 272 - type: "boolean", 273 description: 274 "Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons.", 275 }, 276 writes: { 277 - type: "array", 278 items: { 279 - type: "union", 280 refs: [ 281 - "lex:com.atproto.repo.applyWrites#create", 282 - "lex:com.atproto.repo.applyWrites#update", 283 - "lex:com.atproto.repo.applyWrites#delete", 284 ], 285 closed: true, 286 }, 287 }, 288 swapCommit: { 289 - type: "string", 290 description: 291 - "If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.", 292 - format: "cid", 293 }, 294 }, 295 }, 296 }, 297 output: { 298 - encoding: "application/json", 299 schema: { 300 - type: "object", 301 required: [], 302 properties: { 303 commit: { 304 - type: "ref", 305 - ref: "lex:com.atproto.repo.defs#commitMeta", 306 }, 307 results: { 308 - type: "array", 309 items: { 310 - type: "union", 311 refs: [ 312 - "lex:com.atproto.repo.applyWrites#createResult", 313 - "lex:com.atproto.repo.applyWrites#updateResult", 314 - "lex:com.atproto.repo.applyWrites#deleteResult", 315 ], 316 closed: true, 317 }, ··· 321 }, 322 errors: [ 323 { 324 - name: "InvalidSwap", 325 description: 326 "Indicates that the 'swapCommit' parameter did not match current commit.", 327 }, 328 ], 329 }, 330 create: { 331 - type: "object", 332 - description: "Operation which creates a new record.", 333 - required: ["collection", "value"], 334 properties: { 335 collection: { 336 - type: "string", 337 - format: "nsid", 338 }, 339 rkey: { 340 - type: "string", 341 maxLength: 512, 342 - format: "record-key", 343 description: 344 - "NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility.", 345 }, 346 value: { 347 - type: "unknown", 348 }, 349 }, 350 }, 351 update: { 352 - type: "object", 353 - description: "Operation which updates an existing record.", 354 - required: ["collection", "rkey", "value"], 355 properties: { 356 collection: { 357 - type: "string", 358 - format: "nsid", 359 }, 360 rkey: { 361 - type: "string", 362 - format: "record-key", 363 }, 364 value: { 365 - type: "unknown", 366 }, 367 }, 368 }, 369 delete: { 370 - type: "object", 371 - description: "Operation which deletes an existing record.", 372 - required: ["collection", "rkey"], 373 properties: { 374 collection: { 375 - type: "string", 376 - format: "nsid", 377 }, 378 rkey: { 379 - type: "string", 380 - format: "record-key", 381 }, 382 }, 383 }, 384 createResult: { 385 - type: "object", 386 - required: ["uri", "cid"], 387 properties: { 388 uri: { 389 - type: "string", 390 - format: "at-uri", 391 }, 392 cid: { 393 - type: "string", 394 - format: "cid", 395 }, 396 validationStatus: { 397 - type: "string", 398 - knownValues: ["valid", "unknown"], 399 }, 400 }, 401 }, 402 updateResult: { 403 - type: "object", 404 - required: ["uri", "cid"], 405 properties: { 406 uri: { 407 - type: "string", 408 - format: "at-uri", 409 }, 410 cid: { 411 - type: "string", 412 - format: "cid", 413 }, 414 validationStatus: { 415 - type: "string", 416 - knownValues: ["valid", "unknown"], 417 }, 418 }, 419 }, 420 deleteResult: { 421 - type: "object", 422 required: [], 423 properties: {}, 424 }, ··· 426 }, 427 ComAtprotoRepoCreateRecord: { 428 lexicon: 1, 429 - id: "com.atproto.repo.createRecord", 430 defs: { 431 main: { 432 - type: "procedure", 433 description: 434 - "Create a single new repository record. Requires auth, implemented by PDS.", 435 input: { 436 - encoding: "application/json", 437 schema: { 438 - type: "object", 439 - required: ["repo", "collection", "record"], 440 properties: { 441 repo: { 442 - type: "string", 443 - format: "at-identifier", 444 description: 445 - "The handle or DID of the repo (aka, current account).", 446 }, 447 collection: { 448 - type: "string", 449 - format: "nsid", 450 - description: "The NSID of the record collection.", 451 }, 452 rkey: { 453 - type: "string", 454 - format: "record-key", 455 - description: "The Record Key.", 456 maxLength: 512, 457 }, 458 validate: { 459 - type: "boolean", 460 description: 461 "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.", 462 }, 463 record: { 464 - type: "unknown", 465 - description: "The record itself. Must contain a $type field.", 466 }, 467 swapCommit: { 468 - type: "string", 469 - format: "cid", 470 description: 471 - "Compare and swap with the previous commit by CID.", 472 }, 473 }, 474 }, 475 }, 476 output: { 477 - encoding: "application/json", 478 schema: { 479 - type: "object", 480 - required: ["uri", "cid"], 481 properties: { 482 uri: { 483 - type: "string", 484 - format: "at-uri", 485 }, 486 cid: { 487 - type: "string", 488 - format: "cid", 489 }, 490 commit: { 491 - type: "ref", 492 - ref: "lex:com.atproto.repo.defs#commitMeta", 493 }, 494 validationStatus: { 495 - type: "string", 496 - knownValues: ["valid", "unknown"], 497 }, 498 }, 499 }, 500 }, 501 errors: [ 502 { 503 - name: "InvalidSwap", 504 description: 505 "Indicates that 'swapCommit' didn't match current repo commit.", 506 }, ··· 510 }, 511 ComAtprotoRepoDefs: { 512 lexicon: 1, 513 - id: "com.atproto.repo.defs", 514 defs: { 515 commitMeta: { 516 - type: "object", 517 - required: ["cid", "rev"], 518 properties: { 519 cid: { 520 - type: "string", 521 - format: "cid", 522 }, 523 rev: { 524 - type: "string", 525 - format: "tid", 526 }, 527 }, 528 }, ··· 530 }, 531 ComAtprotoRepoDeleteRecord: { 532 lexicon: 1, 533 - id: "com.atproto.repo.deleteRecord", 534 defs: { 535 main: { 536 - type: "procedure", 537 description: 538 "Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.", 539 input: { 540 - encoding: "application/json", 541 schema: { 542 - type: "object", 543 - required: ["repo", "collection", "rkey"], 544 properties: { 545 repo: { 546 - type: "string", 547 - format: "at-identifier", 548 description: 549 - "The handle or DID of the repo (aka, current account).", 550 }, 551 collection: { 552 - type: "string", 553 - format: "nsid", 554 - description: "The NSID of the record collection.", 555 }, 556 rkey: { 557 - type: "string", 558 - format: "record-key", 559 - description: "The Record Key.", 560 }, 561 swapRecord: { 562 - type: "string", 563 - format: "cid", 564 description: 565 - "Compare and swap with the previous record by CID.", 566 }, 567 swapCommit: { 568 - type: "string", 569 - format: "cid", 570 description: 571 - "Compare and swap with the previous commit by CID.", 572 }, 573 }, 574 }, 575 }, 576 output: { 577 - encoding: "application/json", 578 schema: { 579 - type: "object", 580 properties: { 581 commit: { 582 - type: "ref", 583 - ref: "lex:com.atproto.repo.defs#commitMeta", 584 }, 585 }, 586 }, 587 }, 588 errors: [ 589 { 590 - name: "InvalidSwap", 591 }, 592 ], 593 }, ··· 595 }, 596 ComAtprotoRepoDescribeRepo: { 597 lexicon: 1, 598 - id: "com.atproto.repo.describeRepo", 599 defs: { 600 main: { 601 - type: "query", 602 description: 603 - "Get information about an account and repository, including the list of collections. Does not require auth.", 604 parameters: { 605 - type: "params", 606 - required: ["repo"], 607 properties: { 608 repo: { 609 - type: "string", 610 - format: "at-identifier", 611 - description: "The handle or DID of the repo.", 612 }, 613 }, 614 }, 615 output: { 616 - encoding: "application/json", 617 schema: { 618 - type: "object", 619 required: [ 620 - "handle", 621 - "did", 622 - "didDoc", 623 - "collections", 624 - "handleIsCorrect", 625 ], 626 properties: { 627 handle: { 628 - type: "string", 629 - format: "handle", 630 }, 631 did: { 632 - type: "string", 633 - format: "did", 634 }, 635 didDoc: { 636 - type: "unknown", 637 - description: "The complete DID document for this account.", 638 }, 639 collections: { 640 - type: "array", 641 description: 642 - "List of all the collections (NSIDs) for which this repo contains at least one record.", 643 items: { 644 - type: "string", 645 - format: "nsid", 646 }, 647 }, 648 handleIsCorrect: { 649 - type: "boolean", 650 description: 651 - "Indicates if handle is currently valid (resolves bi-directionally)", 652 }, 653 }, 654 }, ··· 658 }, 659 ComAtprotoRepoGetRecord: { 660 lexicon: 1, 661 - id: "com.atproto.repo.getRecord", 662 defs: { 663 main: { 664 - type: "query", 665 description: 666 - "Get a single record from a repository. Does not require auth.", 667 parameters: { 668 - type: "params", 669 - required: ["repo", "collection", "rkey"], 670 properties: { 671 repo: { 672 - type: "string", 673 - format: "at-identifier", 674 - description: "The handle or DID of the repo.", 675 }, 676 collection: { 677 - type: "string", 678 - format: "nsid", 679 - description: "The NSID of the record collection.", 680 }, 681 rkey: { 682 - type: "string", 683 - description: "The Record Key.", 684 - format: "record-key", 685 }, 686 cid: { 687 - type: "string", 688 - format: "cid", 689 description: 690 - "The CID of the version of the record. If not specified, then return the most recent version.", 691 }, 692 }, 693 }, 694 output: { 695 - encoding: "application/json", 696 schema: { 697 - type: "object", 698 - required: ["uri", "value"], 699 properties: { 700 uri: { 701 - type: "string", 702 - format: "at-uri", 703 }, 704 cid: { 705 - type: "string", 706 - format: "cid", 707 }, 708 value: { 709 - type: "unknown", 710 }, 711 }, 712 }, 713 }, 714 errors: [ 715 { 716 - name: "RecordNotFound", 717 }, 718 ], 719 }, ··· 721 }, 722 ComAtprotoRepoImportRepo: { 723 lexicon: 1, 724 - id: "com.atproto.repo.importRepo", 725 defs: { 726 main: { 727 - type: "procedure", 728 description: 729 - "Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.", 730 input: { 731 - encoding: "application/vnd.ipld.car", 732 }, 733 }, 734 }, 735 }, 736 ComAtprotoRepoListMissingBlobs: { 737 lexicon: 1, 738 - id: "com.atproto.repo.listMissingBlobs", 739 defs: { 740 main: { 741 - type: "query", 742 description: 743 - "Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.", 744 parameters: { 745 - type: "params", 746 properties: { 747 limit: { 748 - type: "integer", 749 minimum: 1, 750 maximum: 1000, 751 default: 500, 752 }, 753 cursor: { 754 - type: "string", 755 }, 756 }, 757 }, 758 output: { 759 - encoding: "application/json", 760 schema: { 761 - type: "object", 762 - required: ["blobs"], 763 properties: { 764 cursor: { 765 - type: "string", 766 }, 767 blobs: { 768 - type: "array", 769 items: { 770 - type: "ref", 771 - ref: "lex:com.atproto.repo.listMissingBlobs#recordBlob", 772 }, 773 }, 774 }, ··· 776 }, 777 }, 778 recordBlob: { 779 - type: "object", 780 - required: ["cid", "recordUri"], 781 properties: { 782 cid: { 783 - type: "string", 784 - format: "cid", 785 }, 786 recordUri: { 787 - type: "string", 788 - format: "at-uri", 789 }, 790 }, 791 }, ··· 793 }, 794 ComAtprotoRepoListRecords: { 795 lexicon: 1, 796 - id: "com.atproto.repo.listRecords", 797 defs: { 798 main: { 799 - type: "query", 800 description: 801 - "List a range of records in a repository, matching a specific collection. Does not require auth.", 802 parameters: { 803 - type: "params", 804 - required: ["repo", "collection"], 805 properties: { 806 repo: { 807 - type: "string", 808 - format: "at-identifier", 809 - description: "The handle or DID of the repo.", 810 }, 811 collection: { 812 - type: "string", 813 - format: "nsid", 814 - description: "The NSID of the record type.", 815 }, 816 limit: { 817 - type: "integer", 818 minimum: 1, 819 maximum: 100, 820 default: 50, 821 - description: "The number of records to return.", 822 }, 823 cursor: { 824 - type: "string", 825 }, 826 rkeyStart: { 827 - type: "string", 828 description: 829 - "DEPRECATED: The lowest sort-ordered rkey to start from (exclusive)", 830 }, 831 rkeyEnd: { 832 - type: "string", 833 description: 834 - "DEPRECATED: The highest sort-ordered rkey to stop at (exclusive)", 835 }, 836 reverse: { 837 - type: "boolean", 838 - description: "Flag to reverse the order of the returned records.", 839 }, 840 }, 841 }, 842 output: { 843 - encoding: "application/json", 844 schema: { 845 - type: "object", 846 - required: ["records"], 847 properties: { 848 cursor: { 849 - type: "string", 850 }, 851 records: { 852 - type: "array", 853 items: { 854 - type: "ref", 855 - ref: "lex:com.atproto.repo.listRecords#record", 856 }, 857 }, 858 }, ··· 860 }, 861 }, 862 record: { 863 - type: "object", 864 - required: ["uri", "cid", "value"], 865 properties: { 866 uri: { 867 - type: "string", 868 - format: "at-uri", 869 }, 870 cid: { 871 - type: "string", 872 - format: "cid", 873 }, 874 value: { 875 - type: "unknown", 876 }, 877 }, 878 }, ··· 880 }, 881 ComAtprotoRepoPutRecord: { 882 lexicon: 1, 883 - id: "com.atproto.repo.putRecord", 884 defs: { 885 main: { 886 - type: "procedure", 887 description: 888 - "Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.", 889 input: { 890 - encoding: "application/json", 891 schema: { 892 - type: "object", 893 - required: ["repo", "collection", "rkey", "record"], 894 - nullable: ["swapRecord"], 895 properties: { 896 repo: { 897 - type: "string", 898 - format: "at-identifier", 899 description: 900 - "The handle or DID of the repo (aka, current account).", 901 }, 902 collection: { 903 - type: "string", 904 - format: "nsid", 905 - description: "The NSID of the record collection.", 906 }, 907 rkey: { 908 - type: "string", 909 - format: "record-key", 910 - description: "The Record Key.", 911 maxLength: 512, 912 }, 913 validate: { 914 - type: "boolean", 915 description: 916 "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.", 917 }, 918 record: { 919 - type: "unknown", 920 - description: "The record to write.", 921 }, 922 swapRecord: { 923 - type: "string", 924 - format: "cid", 925 description: 926 - "Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation", 927 }, 928 swapCommit: { 929 - type: "string", 930 - format: "cid", 931 description: 932 - "Compare and swap with the previous commit by CID.", 933 }, 934 }, 935 }, 936 }, 937 output: { 938 - encoding: "application/json", 939 schema: { 940 - type: "object", 941 - required: ["uri", "cid"], 942 properties: { 943 uri: { 944 - type: "string", 945 - format: "at-uri", 946 }, 947 cid: { 948 - type: "string", 949 - format: "cid", 950 }, 951 commit: { 952 - type: "ref", 953 - ref: "lex:com.atproto.repo.defs#commitMeta", 954 }, 955 validationStatus: { 956 - type: "string", 957 - knownValues: ["valid", "unknown"], 958 }, 959 }, 960 }, 961 }, 962 errors: [ 963 { 964 - name: "InvalidSwap", 965 }, 966 ], 967 }, ··· 969 }, 970 ComAtprotoRepoStrongRef: { 971 lexicon: 1, 972 - id: "com.atproto.repo.strongRef", 973 - description: "A URI with a content-hash fingerprint.", 974 defs: { 975 main: { 976 - type: "object", 977 - required: ["uri", "cid"], 978 properties: { 979 uri: { 980 - type: "string", 981 - format: "at-uri", 982 }, 983 cid: { 984 - type: "string", 985 - format: "cid", 986 }, 987 }, 988 }, ··· 990 }, 991 ComAtprotoRepoUploadBlob: { 992 lexicon: 1, 993 - id: "com.atproto.repo.uploadBlob", 994 defs: { 995 main: { 996 - type: "procedure", 997 description: 998 - "Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.", 999 input: { 1000 - encoding: "*/*", 1001 }, 1002 output: { 1003 - encoding: "application/json", 1004 schema: { 1005 - type: "object", 1006 - required: ["blob"], 1007 properties: { 1008 blob: { 1009 - type: "blob", 1010 }, 1011 }, 1012 }, ··· 1016 }, 1017 PubLeafletBlocksBlockquote: { 1018 lexicon: 1, 1019 - id: "pub.leaflet.blocks.blockquote", 1020 defs: { 1021 main: { 1022 - type: "object", 1023 - required: ["plaintext"], 1024 properties: { 1025 plaintext: { 1026 - type: "string", 1027 }, 1028 facets: { 1029 - type: "array", 1030 items: { 1031 - type: "ref", 1032 - ref: "lex:pub.leaflet.richtext.facet", 1033 }, 1034 }, 1035 }, ··· 1038 }, 1039 PubLeafletBlocksBskyPost: { 1040 lexicon: 1, 1041 - id: "pub.leaflet.blocks.bskyPost", 1042 defs: { 1043 main: { 1044 - type: "object", 1045 - required: ["postRef"], 1046 properties: { 1047 postRef: { 1048 - type: "ref", 1049 - ref: "lex:com.atproto.repo.strongRef", 1050 }, 1051 }, 1052 }, ··· 1054 }, 1055 PubLeafletBlocksButton: { 1056 lexicon: 1, 1057 - id: "pub.leaflet.blocks.button", 1058 defs: { 1059 main: { 1060 - type: "object", 1061 - required: ["text", "url"], 1062 properties: { 1063 text: { 1064 - type: "string", 1065 }, 1066 url: { 1067 - type: "string", 1068 - format: "uri", 1069 }, 1070 }, 1071 }, ··· 1073 }, 1074 PubLeafletBlocksCode: { 1075 lexicon: 1, 1076 - id: "pub.leaflet.blocks.code", 1077 defs: { 1078 main: { 1079 - type: "object", 1080 - required: ["plaintext"], 1081 properties: { 1082 plaintext: { 1083 - type: "string", 1084 }, 1085 language: { 1086 - type: "string", 1087 }, 1088 syntaxHighlightingTheme: { 1089 - type: "string", 1090 }, 1091 }, 1092 }, ··· 1094 }, 1095 PubLeafletBlocksHeader: { 1096 lexicon: 1, 1097 - id: "pub.leaflet.blocks.header", 1098 defs: { 1099 main: { 1100 - type: "object", 1101 - required: ["plaintext"], 1102 properties: { 1103 level: { 1104 - type: "integer", 1105 minimum: 1, 1106 maximum: 6, 1107 }, 1108 plaintext: { 1109 - type: "string", 1110 }, 1111 facets: { 1112 - type: "array", 1113 items: { 1114 - type: "ref", 1115 - ref: "lex:pub.leaflet.richtext.facet", 1116 }, 1117 }, 1118 }, ··· 1121 }, 1122 PubLeafletBlocksHorizontalRule: { 1123 lexicon: 1, 1124 - id: "pub.leaflet.blocks.horizontalRule", 1125 defs: { 1126 main: { 1127 - type: "object", 1128 required: [], 1129 properties: {}, 1130 }, ··· 1132 }, 1133 PubLeafletBlocksIframe: { 1134 lexicon: 1, 1135 - id: "pub.leaflet.blocks.iframe", 1136 defs: { 1137 main: { 1138 - type: "object", 1139 - required: ["url"], 1140 properties: { 1141 url: { 1142 - type: "string", 1143 - format: "uri", 1144 }, 1145 height: { 1146 - type: "integer", 1147 minimum: 16, 1148 maximum: 1600, 1149 }, ··· 1153 }, 1154 PubLeafletBlocksImage: { 1155 lexicon: 1, 1156 - id: "pub.leaflet.blocks.image", 1157 defs: { 1158 main: { 1159 - type: "object", 1160 - required: ["image", "aspectRatio"], 1161 properties: { 1162 image: { 1163 - type: "blob", 1164 - accept: ["image/*"], 1165 maxSize: 1000000, 1166 }, 1167 alt: { 1168 - type: "string", 1169 description: 1170 - "Alt text description of the image, for accessibility.", 1171 }, 1172 aspectRatio: { 1173 - type: "ref", 1174 - ref: "lex:pub.leaflet.blocks.image#aspectRatio", 1175 }, 1176 }, 1177 }, 1178 aspectRatio: { 1179 - type: "object", 1180 - required: ["width", "height"], 1181 properties: { 1182 width: { 1183 - type: "integer", 1184 }, 1185 height: { 1186 - type: "integer", 1187 }, 1188 }, 1189 }, ··· 1191 }, 1192 PubLeafletBlocksMath: { 1193 lexicon: 1, 1194 - id: "pub.leaflet.blocks.math", 1195 defs: { 1196 main: { 1197 - type: "object", 1198 - required: ["tex"], 1199 properties: { 1200 tex: { 1201 - type: "string", 1202 }, 1203 }, 1204 }, ··· 1206 }, 1207 PubLeafletBlocksPage: { 1208 lexicon: 1, 1209 - id: "pub.leaflet.blocks.page", 1210 defs: { 1211 main: { 1212 - type: "object", 1213 - required: ["id"], 1214 properties: { 1215 id: { 1216 - type: "string", 1217 }, 1218 }, 1219 }, ··· 1221 }, 1222 PubLeafletBlocksPoll: { 1223 lexicon: 1, 1224 - id: "pub.leaflet.blocks.poll", 1225 defs: { 1226 main: { 1227 - type: "object", 1228 - required: ["pollRef"], 1229 properties: { 1230 pollRef: { 1231 - type: "ref", 1232 - ref: "lex:com.atproto.repo.strongRef", 1233 }, 1234 }, 1235 }, ··· 1237 }, 1238 PubLeafletBlocksText: { 1239 lexicon: 1, 1240 - id: "pub.leaflet.blocks.text", 1241 defs: { 1242 main: { 1243 - type: "object", 1244 - required: ["plaintext"], 1245 properties: { 1246 plaintext: { 1247 - type: "string", 1248 }, 1249 facets: { 1250 - type: "array", 1251 items: { 1252 - type: "ref", 1253 - ref: "lex:pub.leaflet.richtext.facet", 1254 }, 1255 }, 1256 }, ··· 1259 }, 1260 PubLeafletBlocksUnorderedList: { 1261 lexicon: 1, 1262 - id: "pub.leaflet.blocks.unorderedList", 1263 defs: { 1264 main: { 1265 - type: "object", 1266 - required: ["children"], 1267 properties: { 1268 children: { 1269 - type: "array", 1270 items: { 1271 - type: "ref", 1272 - ref: "lex:pub.leaflet.blocks.unorderedList#listItem", 1273 }, 1274 }, 1275 }, 1276 }, 1277 listItem: { 1278 - type: "object", 1279 - required: ["content"], 1280 properties: { 1281 content: { 1282 - type: "union", 1283 refs: [ 1284 - "lex:pub.leaflet.blocks.text", 1285 - "lex:pub.leaflet.blocks.header", 1286 - "lex:pub.leaflet.blocks.image", 1287 ], 1288 }, 1289 children: { 1290 - type: "array", 1291 items: { 1292 - type: "ref", 1293 - ref: "lex:pub.leaflet.blocks.unorderedList#listItem", 1294 }, 1295 }, 1296 }, ··· 1299 }, 1300 PubLeafletBlocksWebsite: { 1301 lexicon: 1, 1302 - id: "pub.leaflet.blocks.website", 1303 defs: { 1304 main: { 1305 - type: "object", 1306 - required: ["src"], 1307 properties: { 1308 previewImage: { 1309 - type: "blob", 1310 - accept: ["image/*"], 1311 maxSize: 1000000, 1312 }, 1313 title: { 1314 - type: "string", 1315 }, 1316 description: { 1317 - type: "string", 1318 }, 1319 src: { 1320 - type: "string", 1321 - format: "uri", 1322 }, 1323 }, 1324 }, ··· 1326 }, 1327 PubLeafletComment: { 1328 lexicon: 1, 1329 - id: "pub.leaflet.comment", 1330 revision: 1, 1331 - description: "A lexicon for comments on documents", 1332 defs: { 1333 main: { 1334 - type: "record", 1335 - key: "tid", 1336 - description: "Record containing a comment", 1337 record: { 1338 - type: "object", 1339 - required: ["subject", "plaintext", "createdAt"], 1340 properties: { 1341 subject: { 1342 - type: "string", 1343 - format: "at-uri", 1344 }, 1345 createdAt: { 1346 - type: "string", 1347 - format: "datetime", 1348 }, 1349 reply: { 1350 - type: "ref", 1351 - ref: "lex:pub.leaflet.comment#replyRef", 1352 }, 1353 plaintext: { 1354 - type: "string", 1355 }, 1356 facets: { 1357 - type: "array", 1358 items: { 1359 - type: "ref", 1360 - ref: "lex:pub.leaflet.richtext.facet", 1361 }, 1362 }, 1363 onPage: { 1364 - type: "string", 1365 }, 1366 attachment: { 1367 - type: "union", 1368 - refs: ["lex:pub.leaflet.comment#linearDocumentQuote"], 1369 }, 1370 }, 1371 }, 1372 }, 1373 linearDocumentQuote: { 1374 - type: "object", 1375 - required: ["document", "quote"], 1376 properties: { 1377 document: { 1378 - type: "string", 1379 - format: "at-uri", 1380 }, 1381 quote: { 1382 - type: "ref", 1383 - ref: "lex:pub.leaflet.pages.linearDocument#quote", 1384 }, 1385 }, 1386 }, 1387 replyRef: { 1388 - type: "object", 1389 - required: ["parent"], 1390 properties: { 1391 parent: { 1392 - type: "string", 1393 - format: "at-uri", 1394 }, 1395 }, 1396 }, ··· 1398 }, 1399 PubLeafletDocument: { 1400 lexicon: 1, 1401 - id: "pub.leaflet.document", 1402 revision: 1, 1403 - description: "A lexicon for long form rich media documents", 1404 defs: { 1405 main: { 1406 - type: "record", 1407 - key: "tid", 1408 - description: "Record containing a document", 1409 record: { 1410 - type: "object", 1411 - required: ["pages", "author", "title"], 1412 properties: { 1413 title: { 1414 - type: "string", 1415 maxLength: 1280, 1416 maxGraphemes: 128, 1417 }, 1418 postRef: { 1419 - type: "ref", 1420 - ref: "lex:com.atproto.repo.strongRef", 1421 }, 1422 description: { 1423 - type: "string", 1424 maxLength: 3000, 1425 maxGraphemes: 300, 1426 }, 1427 publishedAt: { 1428 - type: "string", 1429 - format: "datetime", 1430 }, 1431 publication: { 1432 - type: "string", 1433 - format: "at-uri", 1434 }, 1435 author: { 1436 - type: "string", 1437 - format: "at-identifier", 1438 }, 1439 theme: { 1440 - type: "ref", 1441 - ref: "lex:pub.leaflet.publication#theme", 1442 }, 1443 tags: { 1444 - type: "array", 1445 items: { 1446 - type: "string", 1447 maxLength: 50, 1448 }, 1449 }, 1450 coverImage: { 1451 - type: "blob", 1452 - accept: ["image/png", "image/jpeg", "image/webp"], 1453 maxSize: 1000000, 1454 }, 1455 pages: { 1456 - type: "array", 1457 items: { 1458 - type: "union", 1459 refs: [ 1460 - "lex:pub.leaflet.pages.linearDocument", 1461 - "lex:pub.leaflet.pages.canvas", 1462 ], 1463 }, 1464 }, ··· 1469 }, 1470 PubLeafletGraphSubscription: { 1471 lexicon: 1, 1472 - id: "pub.leaflet.graph.subscription", 1473 defs: { 1474 main: { 1475 - type: "record", 1476 - key: "tid", 1477 - description: "Record declaring a subscription to a publication", 1478 record: { 1479 - type: "object", 1480 - required: ["publication"], 1481 properties: { 1482 publication: { 1483 - type: "string", 1484 - format: "at-uri", 1485 }, 1486 }, 1487 }, ··· 1490 }, 1491 PubLeafletPagesCanvas: { 1492 lexicon: 1, 1493 - id: "pub.leaflet.pages.canvas", 1494 defs: { 1495 main: { 1496 - type: "object", 1497 - required: ["blocks"], 1498 properties: { 1499 id: { 1500 - type: "string", 1501 }, 1502 blocks: { 1503 - type: "array", 1504 items: { 1505 - type: "ref", 1506 - ref: "lex:pub.leaflet.pages.canvas#block", 1507 }, 1508 }, 1509 }, 1510 }, 1511 block: { 1512 - type: "object", 1513 - required: ["block", "x", "y", "width"], 1514 properties: { 1515 block: { 1516 - type: "union", 1517 refs: [ 1518 - "lex:pub.leaflet.blocks.iframe", 1519 - "lex:pub.leaflet.blocks.text", 1520 - "lex:pub.leaflet.blocks.blockquote", 1521 - "lex:pub.leaflet.blocks.header", 1522 - "lex:pub.leaflet.blocks.image", 1523 - "lex:pub.leaflet.blocks.unorderedList", 1524 - "lex:pub.leaflet.blocks.website", 1525 - "lex:pub.leaflet.blocks.math", 1526 - "lex:pub.leaflet.blocks.code", 1527 - "lex:pub.leaflet.blocks.horizontalRule", 1528 - "lex:pub.leaflet.blocks.bskyPost", 1529 - "lex:pub.leaflet.blocks.page", 1530 - "lex:pub.leaflet.blocks.poll", 1531 - "lex:pub.leaflet.blocks.button", 1532 ], 1533 }, 1534 x: { 1535 - type: "integer", 1536 }, 1537 y: { 1538 - type: "integer", 1539 }, 1540 width: { 1541 - type: "integer", 1542 }, 1543 height: { 1544 - type: "integer", 1545 }, 1546 rotation: { 1547 - type: "integer", 1548 - description: "The rotation of the block in degrees", 1549 }, 1550 }, 1551 }, 1552 textAlignLeft: { 1553 - type: "token", 1554 }, 1555 textAlignCenter: { 1556 - type: "token", 1557 }, 1558 textAlignRight: { 1559 - type: "token", 1560 }, 1561 quote: { 1562 - type: "object", 1563 - required: ["start", "end"], 1564 properties: { 1565 start: { 1566 - type: "ref", 1567 - ref: "lex:pub.leaflet.pages.canvas#position", 1568 }, 1569 end: { 1570 - type: "ref", 1571 - ref: "lex:pub.leaflet.pages.canvas#position", 1572 }, 1573 }, 1574 }, 1575 position: { 1576 - type: "object", 1577 - required: ["block", "offset"], 1578 properties: { 1579 block: { 1580 - type: "array", 1581 items: { 1582 - type: "integer", 1583 }, 1584 }, 1585 offset: { 1586 - type: "integer", 1587 }, 1588 }, 1589 }, ··· 1591 }, 1592 PubLeafletPagesLinearDocument: { 1593 lexicon: 1, 1594 - id: "pub.leaflet.pages.linearDocument", 1595 defs: { 1596 main: { 1597 - type: "object", 1598 - required: ["blocks"], 1599 properties: { 1600 id: { 1601 - type: "string", 1602 }, 1603 blocks: { 1604 - type: "array", 1605 items: { 1606 - type: "ref", 1607 - ref: "lex:pub.leaflet.pages.linearDocument#block", 1608 }, 1609 }, 1610 }, 1611 }, 1612 block: { 1613 - type: "object", 1614 - required: ["block"], 1615 properties: { 1616 block: { 1617 - type: "union", 1618 refs: [ 1619 - "lex:pub.leaflet.blocks.iframe", 1620 - "lex:pub.leaflet.blocks.text", 1621 - "lex:pub.leaflet.blocks.blockquote", 1622 - "lex:pub.leaflet.blocks.header", 1623 - "lex:pub.leaflet.blocks.image", 1624 - "lex:pub.leaflet.blocks.unorderedList", 1625 - "lex:pub.leaflet.blocks.website", 1626 - "lex:pub.leaflet.blocks.math", 1627 - "lex:pub.leaflet.blocks.code", 1628 - "lex:pub.leaflet.blocks.horizontalRule", 1629 - "lex:pub.leaflet.blocks.bskyPost", 1630 - "lex:pub.leaflet.blocks.page", 1631 - "lex:pub.leaflet.blocks.poll", 1632 - "lex:pub.leaflet.blocks.button", 1633 ], 1634 }, 1635 alignment: { 1636 - type: "string", 1637 knownValues: [ 1638 - "lex:pub.leaflet.pages.linearDocument#textAlignLeft", 1639 - "lex:pub.leaflet.pages.linearDocument#textAlignCenter", 1640 - "lex:pub.leaflet.pages.linearDocument#textAlignRight", 1641 - "lex:pub.leaflet.pages.linearDocument#textAlignJustify", 1642 ], 1643 }, 1644 }, 1645 }, 1646 textAlignLeft: { 1647 - type: "token", 1648 }, 1649 textAlignCenter: { 1650 - type: "token", 1651 }, 1652 textAlignRight: { 1653 - type: "token", 1654 }, 1655 textAlignJustify: { 1656 - type: "token", 1657 }, 1658 quote: { 1659 - type: "object", 1660 - required: ["start", "end"], 1661 properties: { 1662 start: { 1663 - type: "ref", 1664 - ref: "lex:pub.leaflet.pages.linearDocument#position", 1665 }, 1666 end: { 1667 - type: "ref", 1668 - ref: "lex:pub.leaflet.pages.linearDocument#position", 1669 }, 1670 }, 1671 }, 1672 position: { 1673 - type: "object", 1674 - required: ["block", "offset"], 1675 properties: { 1676 block: { 1677 - type: "array", 1678 items: { 1679 - type: "integer", 1680 }, 1681 }, 1682 offset: { 1683 - type: "integer", 1684 }, 1685 }, 1686 }, ··· 1688 }, 1689 PubLeafletPollDefinition: { 1690 lexicon: 1, 1691 - id: "pub.leaflet.poll.definition", 1692 defs: { 1693 main: { 1694 - type: "record", 1695 - key: "tid", 1696 - description: "Record declaring a poll", 1697 record: { 1698 - type: "object", 1699 - required: ["name", "options"], 1700 properties: { 1701 name: { 1702 - type: "string", 1703 maxLength: 500, 1704 maxGraphemes: 100, 1705 }, 1706 options: { 1707 - type: "array", 1708 items: { 1709 - type: "ref", 1710 - ref: "lex:pub.leaflet.poll.definition#option", 1711 }, 1712 }, 1713 endDate: { 1714 - type: "string", 1715 - format: "datetime", 1716 }, 1717 }, 1718 }, 1719 }, 1720 option: { 1721 - type: "object", 1722 properties: { 1723 text: { 1724 - type: "string", 1725 maxLength: 500, 1726 maxGraphemes: 50, 1727 }, ··· 1731 }, 1732 PubLeafletPollVote: { 1733 lexicon: 1, 1734 - id: "pub.leaflet.poll.vote", 1735 defs: { 1736 main: { 1737 - type: "record", 1738 - key: "tid", 1739 - description: "Record declaring a vote on a poll", 1740 record: { 1741 - type: "object", 1742 - required: ["poll", "option"], 1743 properties: { 1744 poll: { 1745 - type: "ref", 1746 - ref: "lex:com.atproto.repo.strongRef", 1747 }, 1748 option: { 1749 - type: "array", 1750 items: { 1751 - type: "string", 1752 }, 1753 }, 1754 }, ··· 1758 }, 1759 PubLeafletPublication: { 1760 lexicon: 1, 1761 - id: "pub.leaflet.publication", 1762 defs: { 1763 main: { 1764 - type: "record", 1765 - key: "tid", 1766 - description: "Record declaring a publication", 1767 record: { 1768 - type: "object", 1769 - required: ["name"], 1770 properties: { 1771 name: { 1772 - type: "string", 1773 maxLength: 2000, 1774 }, 1775 base_path: { 1776 - type: "string", 1777 }, 1778 description: { 1779 - type: "string", 1780 maxLength: 2000, 1781 }, 1782 icon: { 1783 - type: "blob", 1784 - accept: ["image/*"], 1785 maxSize: 1000000, 1786 }, 1787 theme: { 1788 - type: "ref", 1789 - ref: "lex:pub.leaflet.publication#theme", 1790 }, 1791 preferences: { 1792 - type: "ref", 1793 - ref: "lex:pub.leaflet.publication#preferences", 1794 }, 1795 }, 1796 }, 1797 }, 1798 preferences: { 1799 - type: "object", 1800 properties: { 1801 showInDiscover: { 1802 - type: "boolean", 1803 default: true, 1804 }, 1805 showComments: { 1806 - type: "boolean", 1807 default: true, 1808 }, 1809 }, 1810 }, 1811 theme: { 1812 - type: "object", 1813 properties: { 1814 backgroundColor: { 1815 - type: "union", 1816 refs: [ 1817 - "lex:pub.leaflet.theme.color#rgba", 1818 - "lex:pub.leaflet.theme.color#rgb", 1819 ], 1820 }, 1821 backgroundImage: { 1822 - type: "ref", 1823 - ref: "lex:pub.leaflet.theme.backgroundImage", 1824 }, 1825 pageWidth: { 1826 - type: "integer", 1827 - minimum: 320, 1828 - maximum: 1200, 1829 }, 1830 primary: { 1831 - type: "union", 1832 refs: [ 1833 - "lex:pub.leaflet.theme.color#rgba", 1834 - "lex:pub.leaflet.theme.color#rgb", 1835 ], 1836 }, 1837 pageBackground: { 1838 - type: "union", 1839 refs: [ 1840 - "lex:pub.leaflet.theme.color#rgba", 1841 - "lex:pub.leaflet.theme.color#rgb", 1842 ], 1843 }, 1844 showPageBackground: { 1845 - type: "boolean", 1846 default: false, 1847 }, 1848 accentBackground: { 1849 - type: "union", 1850 refs: [ 1851 - "lex:pub.leaflet.theme.color#rgba", 1852 - "lex:pub.leaflet.theme.color#rgb", 1853 ], 1854 }, 1855 accentText: { 1856 - type: "union", 1857 refs: [ 1858 - "lex:pub.leaflet.theme.color#rgba", 1859 - "lex:pub.leaflet.theme.color#rgb", 1860 ], 1861 }, 1862 }, ··· 1865 }, 1866 PubLeafletRichtextFacet: { 1867 lexicon: 1, 1868 - id: "pub.leaflet.richtext.facet", 1869 defs: { 1870 main: { 1871 - type: "object", 1872 - description: "Annotation of a sub-string within rich text.", 1873 - required: ["index", "features"], 1874 properties: { 1875 index: { 1876 - type: "ref", 1877 - ref: "lex:pub.leaflet.richtext.facet#byteSlice", 1878 }, 1879 features: { 1880 - type: "array", 1881 items: { 1882 - type: "union", 1883 refs: [ 1884 - "lex:pub.leaflet.richtext.facet#link", 1885 - "lex:pub.leaflet.richtext.facet#didMention", 1886 - "lex:pub.leaflet.richtext.facet#atMention", 1887 - "lex:pub.leaflet.richtext.facet#code", 1888 - "lex:pub.leaflet.richtext.facet#highlight", 1889 - "lex:pub.leaflet.richtext.facet#underline", 1890 - "lex:pub.leaflet.richtext.facet#strikethrough", 1891 - "lex:pub.leaflet.richtext.facet#id", 1892 - "lex:pub.leaflet.richtext.facet#bold", 1893 - "lex:pub.leaflet.richtext.facet#italic", 1894 ], 1895 }, 1896 }, 1897 }, 1898 }, 1899 byteSlice: { 1900 - type: "object", 1901 description: 1902 - "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.", 1903 - required: ["byteStart", "byteEnd"], 1904 properties: { 1905 byteStart: { 1906 - type: "integer", 1907 minimum: 0, 1908 }, 1909 byteEnd: { 1910 - type: "integer", 1911 minimum: 0, 1912 }, 1913 }, 1914 }, 1915 link: { 1916 - type: "object", 1917 description: 1918 - "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.", 1919 - required: ["uri"], 1920 properties: { 1921 uri: { 1922 - type: "string", 1923 }, 1924 }, 1925 }, 1926 didMention: { 1927 - type: "object", 1928 - description: "Facet feature for mentioning a did.", 1929 - required: ["did"], 1930 properties: { 1931 did: { 1932 - type: "string", 1933 - format: "did", 1934 }, 1935 }, 1936 }, 1937 atMention: { 1938 - type: "object", 1939 - description: "Facet feature for mentioning an AT URI.", 1940 - required: ["atURI"], 1941 properties: { 1942 atURI: { 1943 - type: "string", 1944 - format: "uri", 1945 }, 1946 }, 1947 }, 1948 code: { 1949 - type: "object", 1950 - description: "Facet feature for inline code.", 1951 required: [], 1952 properties: {}, 1953 }, 1954 highlight: { 1955 - type: "object", 1956 - description: "Facet feature for highlighted text.", 1957 required: [], 1958 properties: {}, 1959 }, 1960 underline: { 1961 - type: "object", 1962 - description: "Facet feature for underline markup", 1963 required: [], 1964 properties: {}, 1965 }, 1966 strikethrough: { 1967 - type: "object", 1968 - description: "Facet feature for strikethrough markup", 1969 required: [], 1970 properties: {}, 1971 }, 1972 id: { 1973 - type: "object", 1974 description: 1975 - "Facet feature for an identifier. Used for linking to a segment", 1976 required: [], 1977 properties: { 1978 id: { 1979 - type: "string", 1980 }, 1981 }, 1982 }, 1983 bold: { 1984 - type: "object", 1985 - description: "Facet feature for bold text", 1986 required: [], 1987 properties: {}, 1988 }, 1989 italic: { 1990 - type: "object", 1991 - description: "Facet feature for italic text", 1992 required: [], 1993 properties: {}, 1994 }, ··· 1996 }, 1997 PubLeafletThemeBackgroundImage: { 1998 lexicon: 1, 1999 - id: "pub.leaflet.theme.backgroundImage", 2000 defs: { 2001 main: { 2002 - type: "object", 2003 - required: ["image"], 2004 properties: { 2005 image: { 2006 - type: "blob", 2007 - accept: ["image/*"], 2008 maxSize: 1000000, 2009 }, 2010 width: { 2011 - type: "integer", 2012 }, 2013 repeat: { 2014 - type: "boolean", 2015 }, 2016 }, 2017 }, ··· 2019 }, 2020 PubLeafletThemeColor: { 2021 lexicon: 1, 2022 - id: "pub.leaflet.theme.color", 2023 defs: { 2024 rgba: { 2025 - type: "object", 2026 - required: ["r", "g", "b", "a"], 2027 properties: { 2028 r: { 2029 - type: "integer", 2030 maximum: 255, 2031 minimum: 0, 2032 }, 2033 g: { 2034 - type: "integer", 2035 maximum: 255, 2036 minimum: 0, 2037 }, 2038 b: { 2039 - type: "integer", 2040 maximum: 255, 2041 minimum: 0, 2042 }, 2043 a: { 2044 - type: "integer", 2045 maximum: 100, 2046 minimum: 0, 2047 }, 2048 }, 2049 }, 2050 rgb: { 2051 - type: "object", 2052 - required: ["r", "g", "b"], 2053 properties: { 2054 r: { 2055 - type: "integer", 2056 maximum: 255, 2057 minimum: 0, 2058 }, 2059 g: { 2060 - type: "integer", 2061 maximum: 255, 2062 minimum: 0, 2063 }, 2064 b: { 2065 - type: "integer", 2066 maximum: 255, 2067 minimum: 0, 2068 }, ··· 2070 }, 2071 }, 2072 }, 2073 - } as const satisfies Record<string, LexiconDoc>; 2074 - export const schemas = Object.values(schemaDict) satisfies LexiconDoc[]; 2075 - export const lexicons: Lexicons = new Lexicons(schemas); 2076 2077 export function validate<T extends { $type: string }>( 2078 v: unknown, 2079 id: string, 2080 hash: string, 2081 requiredType: true, 2082 - ): ValidationResult<T>; 2083 export function validate<T extends { $type?: string }>( 2084 v: unknown, 2085 id: string, 2086 hash: string, 2087 requiredType?: false, 2088 - ): ValidationResult<T>; 2089 export function validate( 2090 v: unknown, 2091 id: string, ··· 2097 : { 2098 success: false, 2099 error: new ValidationError( 2100 - `Must be an object with "${hash === "main" ? id : `${id}#${hash}`}" $type property`, 2101 ), 2102 - }; 2103 } 2104 2105 export const ids = { 2106 - AppBskyActorProfile: "app.bsky.actor.profile", 2107 - ComAtprotoLabelDefs: "com.atproto.label.defs", 2108 - ComAtprotoRepoApplyWrites: "com.atproto.repo.applyWrites", 2109 - ComAtprotoRepoCreateRecord: "com.atproto.repo.createRecord", 2110 - ComAtprotoRepoDefs: "com.atproto.repo.defs", 2111 - ComAtprotoRepoDeleteRecord: "com.atproto.repo.deleteRecord", 2112 - ComAtprotoRepoDescribeRepo: "com.atproto.repo.describeRepo", 2113 - ComAtprotoRepoGetRecord: "com.atproto.repo.getRecord", 2114 - ComAtprotoRepoImportRepo: "com.atproto.repo.importRepo", 2115 - ComAtprotoRepoListMissingBlobs: "com.atproto.repo.listMissingBlobs", 2116 - ComAtprotoRepoListRecords: "com.atproto.repo.listRecords", 2117 - ComAtprotoRepoPutRecord: "com.atproto.repo.putRecord", 2118 - ComAtprotoRepoStrongRef: "com.atproto.repo.strongRef", 2119 - ComAtprotoRepoUploadBlob: "com.atproto.repo.uploadBlob", 2120 - PubLeafletBlocksBlockquote: "pub.leaflet.blocks.blockquote", 2121 - PubLeafletBlocksBskyPost: "pub.leaflet.blocks.bskyPost", 2122 - PubLeafletBlocksButton: "pub.leaflet.blocks.button", 2123 - PubLeafletBlocksCode: "pub.leaflet.blocks.code", 2124 - PubLeafletBlocksHeader: "pub.leaflet.blocks.header", 2125 - PubLeafletBlocksHorizontalRule: "pub.leaflet.blocks.horizontalRule", 2126 - PubLeafletBlocksIframe: "pub.leaflet.blocks.iframe", 2127 - PubLeafletBlocksImage: "pub.leaflet.blocks.image", 2128 - PubLeafletBlocksMath: "pub.leaflet.blocks.math", 2129 - PubLeafletBlocksPage: "pub.leaflet.blocks.page", 2130 - PubLeafletBlocksPoll: "pub.leaflet.blocks.poll", 2131 - PubLeafletBlocksText: "pub.leaflet.blocks.text", 2132 - PubLeafletBlocksUnorderedList: "pub.leaflet.blocks.unorderedList", 2133 - PubLeafletBlocksWebsite: "pub.leaflet.blocks.website", 2134 - PubLeafletComment: "pub.leaflet.comment", 2135 - PubLeafletDocument: "pub.leaflet.document", 2136 - PubLeafletGraphSubscription: "pub.leaflet.graph.subscription", 2137 - PubLeafletPagesCanvas: "pub.leaflet.pages.canvas", 2138 - PubLeafletPagesLinearDocument: "pub.leaflet.pages.linearDocument", 2139 - PubLeafletPollDefinition: "pub.leaflet.poll.definition", 2140 - PubLeafletPollVote: "pub.leaflet.poll.vote", 2141 - PubLeafletPublication: "pub.leaflet.publication", 2142 - PubLeafletRichtextFacet: "pub.leaflet.richtext.facet", 2143 - PubLeafletThemeBackgroundImage: "pub.leaflet.theme.backgroundImage", 2144 - PubLeafletThemeColor: "pub.leaflet.theme.color", 2145 - } as const;
··· 6 Lexicons, 7 ValidationError, 8 type ValidationResult, 9 + } from '@atproto/lexicon' 10 + import { type $Typed, is$typed, maybe$typed } from './util' 11 12 export const schemaDict = { 13 AppBskyActorProfile: { 14 lexicon: 1, 15 + id: 'app.bsky.actor.profile', 16 defs: { 17 main: { 18 + type: 'record', 19 + description: 'A declaration of a Bluesky account profile.', 20 + key: 'literal:self', 21 record: { 22 + type: 'object', 23 properties: { 24 displayName: { 25 + type: 'string', 26 maxGraphemes: 64, 27 maxLength: 640, 28 }, 29 description: { 30 + type: 'string', 31 + description: 'Free-form profile description text.', 32 maxGraphemes: 256, 33 maxLength: 2560, 34 }, 35 avatar: { 36 + type: 'blob', 37 description: 38 "Small image to be displayed next to posts from account. AKA, 'profile picture'", 39 + accept: ['image/png', 'image/jpeg'], 40 maxSize: 1000000, 41 }, 42 banner: { 43 + type: 'blob', 44 description: 45 + 'Larger horizontal image to display behind profile view.', 46 + accept: ['image/png', 'image/jpeg'], 47 maxSize: 1000000, 48 }, 49 labels: { 50 + type: 'union', 51 description: 52 + 'Self-label values, specific to the Bluesky application, on the overall account.', 53 + refs: ['lex:com.atproto.label.defs#selfLabels'], 54 }, 55 joinedViaStarterPack: { 56 + type: 'ref', 57 + ref: 'lex:com.atproto.repo.strongRef', 58 }, 59 pinnedPost: { 60 + type: 'ref', 61 + ref: 'lex:com.atproto.repo.strongRef', 62 }, 63 createdAt: { 64 + type: 'string', 65 + format: 'datetime', 66 }, 67 }, 68 }, ··· 71 }, 72 ComAtprotoLabelDefs: { 73 lexicon: 1, 74 + id: 'com.atproto.label.defs', 75 defs: { 76 label: { 77 + type: 'object', 78 description: 79 + 'Metadata tag on an atproto resource (eg, repo or record).', 80 + required: ['src', 'uri', 'val', 'cts'], 81 properties: { 82 ver: { 83 + type: 'integer', 84 + description: 'The AT Protocol version of the label object.', 85 }, 86 src: { 87 + type: 'string', 88 + format: 'did', 89 + description: 'DID of the actor who created this label.', 90 }, 91 uri: { 92 + type: 'string', 93 + format: 'uri', 94 description: 95 + 'AT URI of the record, repository (account), or other resource that this label applies to.', 96 }, 97 cid: { 98 + type: 'string', 99 + format: 'cid', 100 description: 101 "Optionally, CID specifying the specific version of 'uri' resource this label applies to.", 102 }, 103 val: { 104 + type: 'string', 105 maxLength: 128, 106 description: 107 + 'The short string name of the value or type of this label.', 108 }, 109 neg: { 110 + type: 'boolean', 111 description: 112 + 'If true, this is a negation label, overwriting a previous label.', 113 }, 114 cts: { 115 + type: 'string', 116 + format: 'datetime', 117 + description: 'Timestamp when this label was created.', 118 }, 119 exp: { 120 + type: 'string', 121 + format: 'datetime', 122 description: 123 + 'Timestamp at which this label expires (no longer applies).', 124 }, 125 sig: { 126 + type: 'bytes', 127 + description: 'Signature of dag-cbor encoded label.', 128 }, 129 }, 130 }, 131 selfLabels: { 132 + type: 'object', 133 description: 134 + 'Metadata tags on an atproto record, published by the author within the record.', 135 + required: ['values'], 136 properties: { 137 values: { 138 + type: 'array', 139 items: { 140 + type: 'ref', 141 + ref: 'lex:com.atproto.label.defs#selfLabel', 142 }, 143 maxLength: 10, 144 }, 145 }, 146 }, 147 selfLabel: { 148 + type: 'object', 149 description: 150 + 'Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.', 151 + required: ['val'], 152 properties: { 153 val: { 154 + type: 'string', 155 maxLength: 128, 156 description: 157 + 'The short string name of the value or type of this label.', 158 }, 159 }, 160 }, 161 labelValueDefinition: { 162 + type: 'object', 163 description: 164 + 'Declares a label value and its expected interpretations and behaviors.', 165 + required: ['identifier', 'severity', 'blurs', 'locales'], 166 properties: { 167 identifier: { 168 + type: 'string', 169 description: 170 "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", 171 maxLength: 100, 172 maxGraphemes: 100, 173 }, 174 severity: { 175 + type: 'string', 176 description: 177 "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", 178 + knownValues: ['inform', 'alert', 'none'], 179 }, 180 blurs: { 181 + type: 'string', 182 description: 183 "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.", 184 + knownValues: ['content', 'media', 'none'], 185 }, 186 defaultSetting: { 187 + type: 'string', 188 + description: 'The default setting for this label.', 189 + knownValues: ['ignore', 'warn', 'hide'], 190 + default: 'warn', 191 }, 192 adultOnly: { 193 + type: 'boolean', 194 description: 195 + 'Does the user need to have adult content enabled in order to configure this label?', 196 }, 197 locales: { 198 + type: 'array', 199 items: { 200 + type: 'ref', 201 + ref: 'lex:com.atproto.label.defs#labelValueDefinitionStrings', 202 }, 203 }, 204 }, 205 }, 206 labelValueDefinitionStrings: { 207 + type: 'object', 208 description: 209 + 'Strings which describe the label in the UI, localized into a specific language.', 210 + required: ['lang', 'name', 'description'], 211 properties: { 212 lang: { 213 + type: 'string', 214 description: 215 + 'The code of the language these strings are written in.', 216 + format: 'language', 217 }, 218 name: { 219 + type: 'string', 220 + description: 'A short human-readable name for the label.', 221 maxGraphemes: 64, 222 maxLength: 640, 223 }, 224 description: { 225 + type: 'string', 226 description: 227 + 'A longer description of what the label means and why it might be applied.', 228 maxGraphemes: 10000, 229 maxLength: 100000, 230 }, 231 }, 232 }, 233 labelValue: { 234 + type: 'string', 235 knownValues: [ 236 + '!hide', 237 + '!no-promote', 238 + '!warn', 239 + '!no-unauthenticated', 240 + 'dmca-violation', 241 + 'doxxing', 242 + 'porn', 243 + 'sexual', 244 + 'nudity', 245 + 'nsfl', 246 + 'gore', 247 ], 248 }, 249 }, 250 }, 251 ComAtprotoRepoApplyWrites: { 252 lexicon: 1, 253 + id: 'com.atproto.repo.applyWrites', 254 defs: { 255 main: { 256 + type: 'procedure', 257 description: 258 + 'Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.', 259 input: { 260 + encoding: 'application/json', 261 schema: { 262 + type: 'object', 263 + required: ['repo', 'writes'], 264 properties: { 265 repo: { 266 + type: 'string', 267 + format: 'at-identifier', 268 description: 269 + 'The handle or DID of the repo (aka, current account).', 270 }, 271 validate: { 272 + type: 'boolean', 273 description: 274 "Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons.", 275 }, 276 writes: { 277 + type: 'array', 278 items: { 279 + type: 'union', 280 refs: [ 281 + 'lex:com.atproto.repo.applyWrites#create', 282 + 'lex:com.atproto.repo.applyWrites#update', 283 + 'lex:com.atproto.repo.applyWrites#delete', 284 ], 285 closed: true, 286 }, 287 }, 288 swapCommit: { 289 + type: 'string', 290 description: 291 + 'If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.', 292 + format: 'cid', 293 }, 294 }, 295 }, 296 }, 297 output: { 298 + encoding: 'application/json', 299 schema: { 300 + type: 'object', 301 required: [], 302 properties: { 303 commit: { 304 + type: 'ref', 305 + ref: 'lex:com.atproto.repo.defs#commitMeta', 306 }, 307 results: { 308 + type: 'array', 309 items: { 310 + type: 'union', 311 refs: [ 312 + 'lex:com.atproto.repo.applyWrites#createResult', 313 + 'lex:com.atproto.repo.applyWrites#updateResult', 314 + 'lex:com.atproto.repo.applyWrites#deleteResult', 315 ], 316 closed: true, 317 }, ··· 321 }, 322 errors: [ 323 { 324 + name: 'InvalidSwap', 325 description: 326 "Indicates that the 'swapCommit' parameter did not match current commit.", 327 }, 328 ], 329 }, 330 create: { 331 + type: 'object', 332 + description: 'Operation which creates a new record.', 333 + required: ['collection', 'value'], 334 properties: { 335 collection: { 336 + type: 'string', 337 + format: 'nsid', 338 }, 339 rkey: { 340 + type: 'string', 341 maxLength: 512, 342 + format: 'record-key', 343 description: 344 + 'NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility.', 345 }, 346 value: { 347 + type: 'unknown', 348 }, 349 }, 350 }, 351 update: { 352 + type: 'object', 353 + description: 'Operation which updates an existing record.', 354 + required: ['collection', 'rkey', 'value'], 355 properties: { 356 collection: { 357 + type: 'string', 358 + format: 'nsid', 359 }, 360 rkey: { 361 + type: 'string', 362 + format: 'record-key', 363 }, 364 value: { 365 + type: 'unknown', 366 }, 367 }, 368 }, 369 delete: { 370 + type: 'object', 371 + description: 'Operation which deletes an existing record.', 372 + required: ['collection', 'rkey'], 373 properties: { 374 collection: { 375 + type: 'string', 376 + format: 'nsid', 377 }, 378 rkey: { 379 + type: 'string', 380 + format: 'record-key', 381 }, 382 }, 383 }, 384 createResult: { 385 + type: 'object', 386 + required: ['uri', 'cid'], 387 properties: { 388 uri: { 389 + type: 'string', 390 + format: 'at-uri', 391 }, 392 cid: { 393 + type: 'string', 394 + format: 'cid', 395 }, 396 validationStatus: { 397 + type: 'string', 398 + knownValues: ['valid', 'unknown'], 399 }, 400 }, 401 }, 402 updateResult: { 403 + type: 'object', 404 + required: ['uri', 'cid'], 405 properties: { 406 uri: { 407 + type: 'string', 408 + format: 'at-uri', 409 }, 410 cid: { 411 + type: 'string', 412 + format: 'cid', 413 }, 414 validationStatus: { 415 + type: 'string', 416 + knownValues: ['valid', 'unknown'], 417 }, 418 }, 419 }, 420 deleteResult: { 421 + type: 'object', 422 required: [], 423 properties: {}, 424 }, ··· 426 }, 427 ComAtprotoRepoCreateRecord: { 428 lexicon: 1, 429 + id: 'com.atproto.repo.createRecord', 430 defs: { 431 main: { 432 + type: 'procedure', 433 description: 434 + 'Create a single new repository record. Requires auth, implemented by PDS.', 435 input: { 436 + encoding: 'application/json', 437 schema: { 438 + type: 'object', 439 + required: ['repo', 'collection', 'record'], 440 properties: { 441 repo: { 442 + type: 'string', 443 + format: 'at-identifier', 444 description: 445 + 'The handle or DID of the repo (aka, current account).', 446 }, 447 collection: { 448 + type: 'string', 449 + format: 'nsid', 450 + description: 'The NSID of the record collection.', 451 }, 452 rkey: { 453 + type: 'string', 454 + format: 'record-key', 455 + description: 'The Record Key.', 456 maxLength: 512, 457 }, 458 validate: { 459 + type: 'boolean', 460 description: 461 "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.", 462 }, 463 record: { 464 + type: 'unknown', 465 + description: 'The record itself. Must contain a $type field.', 466 }, 467 swapCommit: { 468 + type: 'string', 469 + format: 'cid', 470 description: 471 + 'Compare and swap with the previous commit by CID.', 472 }, 473 }, 474 }, 475 }, 476 output: { 477 + encoding: 'application/json', 478 schema: { 479 + type: 'object', 480 + required: ['uri', 'cid'], 481 properties: { 482 uri: { 483 + type: 'string', 484 + format: 'at-uri', 485 }, 486 cid: { 487 + type: 'string', 488 + format: 'cid', 489 }, 490 commit: { 491 + type: 'ref', 492 + ref: 'lex:com.atproto.repo.defs#commitMeta', 493 }, 494 validationStatus: { 495 + type: 'string', 496 + knownValues: ['valid', 'unknown'], 497 }, 498 }, 499 }, 500 }, 501 errors: [ 502 { 503 + name: 'InvalidSwap', 504 description: 505 "Indicates that 'swapCommit' didn't match current repo commit.", 506 }, ··· 510 }, 511 ComAtprotoRepoDefs: { 512 lexicon: 1, 513 + id: 'com.atproto.repo.defs', 514 defs: { 515 commitMeta: { 516 + type: 'object', 517 + required: ['cid', 'rev'], 518 properties: { 519 cid: { 520 + type: 'string', 521 + format: 'cid', 522 }, 523 rev: { 524 + type: 'string', 525 + format: 'tid', 526 }, 527 }, 528 }, ··· 530 }, 531 ComAtprotoRepoDeleteRecord: { 532 lexicon: 1, 533 + id: 'com.atproto.repo.deleteRecord', 534 defs: { 535 main: { 536 + type: 'procedure', 537 description: 538 "Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.", 539 input: { 540 + encoding: 'application/json', 541 schema: { 542 + type: 'object', 543 + required: ['repo', 'collection', 'rkey'], 544 properties: { 545 repo: { 546 + type: 'string', 547 + format: 'at-identifier', 548 description: 549 + 'The handle or DID of the repo (aka, current account).', 550 }, 551 collection: { 552 + type: 'string', 553 + format: 'nsid', 554 + description: 'The NSID of the record collection.', 555 }, 556 rkey: { 557 + type: 'string', 558 + format: 'record-key', 559 + description: 'The Record Key.', 560 }, 561 swapRecord: { 562 + type: 'string', 563 + format: 'cid', 564 description: 565 + 'Compare and swap with the previous record by CID.', 566 }, 567 swapCommit: { 568 + type: 'string', 569 + format: 'cid', 570 description: 571 + 'Compare and swap with the previous commit by CID.', 572 }, 573 }, 574 }, 575 }, 576 output: { 577 + encoding: 'application/json', 578 schema: { 579 + type: 'object', 580 properties: { 581 commit: { 582 + type: 'ref', 583 + ref: 'lex:com.atproto.repo.defs#commitMeta', 584 }, 585 }, 586 }, 587 }, 588 errors: [ 589 { 590 + name: 'InvalidSwap', 591 }, 592 ], 593 }, ··· 595 }, 596 ComAtprotoRepoDescribeRepo: { 597 lexicon: 1, 598 + id: 'com.atproto.repo.describeRepo', 599 defs: { 600 main: { 601 + type: 'query', 602 description: 603 + 'Get information about an account and repository, including the list of collections. Does not require auth.', 604 parameters: { 605 + type: 'params', 606 + required: ['repo'], 607 properties: { 608 repo: { 609 + type: 'string', 610 + format: 'at-identifier', 611 + description: 'The handle or DID of the repo.', 612 }, 613 }, 614 }, 615 output: { 616 + encoding: 'application/json', 617 schema: { 618 + type: 'object', 619 required: [ 620 + 'handle', 621 + 'did', 622 + 'didDoc', 623 + 'collections', 624 + 'handleIsCorrect', 625 ], 626 properties: { 627 handle: { 628 + type: 'string', 629 + format: 'handle', 630 }, 631 did: { 632 + type: 'string', 633 + format: 'did', 634 }, 635 didDoc: { 636 + type: 'unknown', 637 + description: 'The complete DID document for this account.', 638 }, 639 collections: { 640 + type: 'array', 641 description: 642 + 'List of all the collections (NSIDs) for which this repo contains at least one record.', 643 items: { 644 + type: 'string', 645 + format: 'nsid', 646 }, 647 }, 648 handleIsCorrect: { 649 + type: 'boolean', 650 description: 651 + 'Indicates if handle is currently valid (resolves bi-directionally)', 652 }, 653 }, 654 }, ··· 658 }, 659 ComAtprotoRepoGetRecord: { 660 lexicon: 1, 661 + id: 'com.atproto.repo.getRecord', 662 defs: { 663 main: { 664 + type: 'query', 665 description: 666 + 'Get a single record from a repository. Does not require auth.', 667 parameters: { 668 + type: 'params', 669 + required: ['repo', 'collection', 'rkey'], 670 properties: { 671 repo: { 672 + type: 'string', 673 + format: 'at-identifier', 674 + description: 'The handle or DID of the repo.', 675 }, 676 collection: { 677 + type: 'string', 678 + format: 'nsid', 679 + description: 'The NSID of the record collection.', 680 }, 681 rkey: { 682 + type: 'string', 683 + description: 'The Record Key.', 684 + format: 'record-key', 685 }, 686 cid: { 687 + type: 'string', 688 + format: 'cid', 689 description: 690 + 'The CID of the version of the record. If not specified, then return the most recent version.', 691 }, 692 }, 693 }, 694 output: { 695 + encoding: 'application/json', 696 schema: { 697 + type: 'object', 698 + required: ['uri', 'value'], 699 properties: { 700 uri: { 701 + type: 'string', 702 + format: 'at-uri', 703 }, 704 cid: { 705 + type: 'string', 706 + format: 'cid', 707 }, 708 value: { 709 + type: 'unknown', 710 }, 711 }, 712 }, 713 }, 714 errors: [ 715 { 716 + name: 'RecordNotFound', 717 }, 718 ], 719 }, ··· 721 }, 722 ComAtprotoRepoImportRepo: { 723 lexicon: 1, 724 + id: 'com.atproto.repo.importRepo', 725 defs: { 726 main: { 727 + type: 'procedure', 728 description: 729 + 'Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.', 730 input: { 731 + encoding: 'application/vnd.ipld.car', 732 }, 733 }, 734 }, 735 }, 736 ComAtprotoRepoListMissingBlobs: { 737 lexicon: 1, 738 + id: 'com.atproto.repo.listMissingBlobs', 739 defs: { 740 main: { 741 + type: 'query', 742 description: 743 + 'Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.', 744 parameters: { 745 + type: 'params', 746 properties: { 747 limit: { 748 + type: 'integer', 749 minimum: 1, 750 maximum: 1000, 751 default: 500, 752 }, 753 cursor: { 754 + type: 'string', 755 }, 756 }, 757 }, 758 output: { 759 + encoding: 'application/json', 760 schema: { 761 + type: 'object', 762 + required: ['blobs'], 763 properties: { 764 cursor: { 765 + type: 'string', 766 }, 767 blobs: { 768 + type: 'array', 769 items: { 770 + type: 'ref', 771 + ref: 'lex:com.atproto.repo.listMissingBlobs#recordBlob', 772 }, 773 }, 774 }, ··· 776 }, 777 }, 778 recordBlob: { 779 + type: 'object', 780 + required: ['cid', 'recordUri'], 781 properties: { 782 cid: { 783 + type: 'string', 784 + format: 'cid', 785 }, 786 recordUri: { 787 + type: 'string', 788 + format: 'at-uri', 789 }, 790 }, 791 }, ··· 793 }, 794 ComAtprotoRepoListRecords: { 795 lexicon: 1, 796 + id: 'com.atproto.repo.listRecords', 797 defs: { 798 main: { 799 + type: 'query', 800 description: 801 + 'List a range of records in a repository, matching a specific collection. Does not require auth.', 802 parameters: { 803 + type: 'params', 804 + required: ['repo', 'collection'], 805 properties: { 806 repo: { 807 + type: 'string', 808 + format: 'at-identifier', 809 + description: 'The handle or DID of the repo.', 810 }, 811 collection: { 812 + type: 'string', 813 + format: 'nsid', 814 + description: 'The NSID of the record type.', 815 }, 816 limit: { 817 + type: 'integer', 818 minimum: 1, 819 maximum: 100, 820 default: 50, 821 + description: 'The number of records to return.', 822 }, 823 cursor: { 824 + type: 'string', 825 }, 826 rkeyStart: { 827 + type: 'string', 828 description: 829 + 'DEPRECATED: The lowest sort-ordered rkey to start from (exclusive)', 830 }, 831 rkeyEnd: { 832 + type: 'string', 833 description: 834 + 'DEPRECATED: The highest sort-ordered rkey to stop at (exclusive)', 835 }, 836 reverse: { 837 + type: 'boolean', 838 + description: 'Flag to reverse the order of the returned records.', 839 }, 840 }, 841 }, 842 output: { 843 + encoding: 'application/json', 844 schema: { 845 + type: 'object', 846 + required: ['records'], 847 properties: { 848 cursor: { 849 + type: 'string', 850 }, 851 records: { 852 + type: 'array', 853 items: { 854 + type: 'ref', 855 + ref: 'lex:com.atproto.repo.listRecords#record', 856 }, 857 }, 858 }, ··· 860 }, 861 }, 862 record: { 863 + type: 'object', 864 + required: ['uri', 'cid', 'value'], 865 properties: { 866 uri: { 867 + type: 'string', 868 + format: 'at-uri', 869 }, 870 cid: { 871 + type: 'string', 872 + format: 'cid', 873 }, 874 value: { 875 + type: 'unknown', 876 }, 877 }, 878 }, ··· 880 }, 881 ComAtprotoRepoPutRecord: { 882 lexicon: 1, 883 + id: 'com.atproto.repo.putRecord', 884 defs: { 885 main: { 886 + type: 'procedure', 887 description: 888 + 'Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.', 889 input: { 890 + encoding: 'application/json', 891 schema: { 892 + type: 'object', 893 + required: ['repo', 'collection', 'rkey', 'record'], 894 + nullable: ['swapRecord'], 895 properties: { 896 repo: { 897 + type: 'string', 898 + format: 'at-identifier', 899 description: 900 + 'The handle or DID of the repo (aka, current account).', 901 }, 902 collection: { 903 + type: 'string', 904 + format: 'nsid', 905 + description: 'The NSID of the record collection.', 906 }, 907 rkey: { 908 + type: 'string', 909 + format: 'record-key', 910 + description: 'The Record Key.', 911 maxLength: 512, 912 }, 913 validate: { 914 + type: 'boolean', 915 description: 916 "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.", 917 }, 918 record: { 919 + type: 'unknown', 920 + description: 'The record to write.', 921 }, 922 swapRecord: { 923 + type: 'string', 924 + format: 'cid', 925 description: 926 + 'Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation', 927 }, 928 swapCommit: { 929 + type: 'string', 930 + format: 'cid', 931 description: 932 + 'Compare and swap with the previous commit by CID.', 933 }, 934 }, 935 }, 936 }, 937 output: { 938 + encoding: 'application/json', 939 schema: { 940 + type: 'object', 941 + required: ['uri', 'cid'], 942 properties: { 943 uri: { 944 + type: 'string', 945 + format: 'at-uri', 946 }, 947 cid: { 948 + type: 'string', 949 + format: 'cid', 950 }, 951 commit: { 952 + type: 'ref', 953 + ref: 'lex:com.atproto.repo.defs#commitMeta', 954 }, 955 validationStatus: { 956 + type: 'string', 957 + knownValues: ['valid', 'unknown'], 958 }, 959 }, 960 }, 961 }, 962 errors: [ 963 { 964 + name: 'InvalidSwap', 965 }, 966 ], 967 }, ··· 969 }, 970 ComAtprotoRepoStrongRef: { 971 lexicon: 1, 972 + id: 'com.atproto.repo.strongRef', 973 + description: 'A URI with a content-hash fingerprint.', 974 defs: { 975 main: { 976 + type: 'object', 977 + required: ['uri', 'cid'], 978 properties: { 979 uri: { 980 + type: 'string', 981 + format: 'at-uri', 982 }, 983 cid: { 984 + type: 'string', 985 + format: 'cid', 986 }, 987 }, 988 }, ··· 990 }, 991 ComAtprotoRepoUploadBlob: { 992 lexicon: 1, 993 + id: 'com.atproto.repo.uploadBlob', 994 defs: { 995 main: { 996 + type: 'procedure', 997 description: 998 + 'Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.', 999 input: { 1000 + encoding: '*/*', 1001 }, 1002 output: { 1003 + encoding: 'application/json', 1004 schema: { 1005 + type: 'object', 1006 + required: ['blob'], 1007 properties: { 1008 blob: { 1009 + type: 'blob', 1010 }, 1011 }, 1012 }, ··· 1016 }, 1017 PubLeafletBlocksBlockquote: { 1018 lexicon: 1, 1019 + id: 'pub.leaflet.blocks.blockquote', 1020 defs: { 1021 main: { 1022 + type: 'object', 1023 + required: ['plaintext'], 1024 properties: { 1025 plaintext: { 1026 + type: 'string', 1027 }, 1028 facets: { 1029 + type: 'array', 1030 items: { 1031 + type: 'ref', 1032 + ref: 'lex:pub.leaflet.richtext.facet', 1033 }, 1034 }, 1035 }, ··· 1038 }, 1039 PubLeafletBlocksBskyPost: { 1040 lexicon: 1, 1041 + id: 'pub.leaflet.blocks.bskyPost', 1042 defs: { 1043 main: { 1044 + type: 'object', 1045 + required: ['postRef'], 1046 properties: { 1047 postRef: { 1048 + type: 'ref', 1049 + ref: 'lex:com.atproto.repo.strongRef', 1050 }, 1051 }, 1052 }, ··· 1054 }, 1055 PubLeafletBlocksButton: { 1056 lexicon: 1, 1057 + id: 'pub.leaflet.blocks.button', 1058 defs: { 1059 main: { 1060 + type: 'object', 1061 + required: ['text', 'url'], 1062 properties: { 1063 text: { 1064 + type: 'string', 1065 }, 1066 url: { 1067 + type: 'string', 1068 + format: 'uri', 1069 }, 1070 }, 1071 }, ··· 1073 }, 1074 PubLeafletBlocksCode: { 1075 lexicon: 1, 1076 + id: 'pub.leaflet.blocks.code', 1077 defs: { 1078 main: { 1079 + type: 'object', 1080 + required: ['plaintext'], 1081 properties: { 1082 plaintext: { 1083 + type: 'string', 1084 }, 1085 language: { 1086 + type: 'string', 1087 }, 1088 syntaxHighlightingTheme: { 1089 + type: 'string', 1090 }, 1091 }, 1092 }, ··· 1094 }, 1095 PubLeafletBlocksHeader: { 1096 lexicon: 1, 1097 + id: 'pub.leaflet.blocks.header', 1098 defs: { 1099 main: { 1100 + type: 'object', 1101 + required: ['plaintext'], 1102 properties: { 1103 level: { 1104 + type: 'integer', 1105 minimum: 1, 1106 maximum: 6, 1107 }, 1108 plaintext: { 1109 + type: 'string', 1110 }, 1111 facets: { 1112 + type: 'array', 1113 items: { 1114 + type: 'ref', 1115 + ref: 'lex:pub.leaflet.richtext.facet', 1116 }, 1117 }, 1118 }, ··· 1121 }, 1122 PubLeafletBlocksHorizontalRule: { 1123 lexicon: 1, 1124 + id: 'pub.leaflet.blocks.horizontalRule', 1125 defs: { 1126 main: { 1127 + type: 'object', 1128 required: [], 1129 properties: {}, 1130 }, ··· 1132 }, 1133 PubLeafletBlocksIframe: { 1134 lexicon: 1, 1135 + id: 'pub.leaflet.blocks.iframe', 1136 defs: { 1137 main: { 1138 + type: 'object', 1139 + required: ['url'], 1140 properties: { 1141 url: { 1142 + type: 'string', 1143 + format: 'uri', 1144 }, 1145 height: { 1146 + type: 'integer', 1147 minimum: 16, 1148 maximum: 1600, 1149 }, ··· 1153 }, 1154 PubLeafletBlocksImage: { 1155 lexicon: 1, 1156 + id: 'pub.leaflet.blocks.image', 1157 defs: { 1158 main: { 1159 + type: 'object', 1160 + required: ['image', 'aspectRatio'], 1161 properties: { 1162 image: { 1163 + type: 'blob', 1164 + accept: ['image/*'], 1165 maxSize: 1000000, 1166 }, 1167 alt: { 1168 + type: 'string', 1169 description: 1170 + 'Alt text description of the image, for accessibility.', 1171 }, 1172 aspectRatio: { 1173 + type: 'ref', 1174 + ref: 'lex:pub.leaflet.blocks.image#aspectRatio', 1175 }, 1176 }, 1177 }, 1178 aspectRatio: { 1179 + type: 'object', 1180 + required: ['width', 'height'], 1181 properties: { 1182 width: { 1183 + type: 'integer', 1184 }, 1185 height: { 1186 + type: 'integer', 1187 }, 1188 }, 1189 }, ··· 1191 }, 1192 PubLeafletBlocksMath: { 1193 lexicon: 1, 1194 + id: 'pub.leaflet.blocks.math', 1195 defs: { 1196 main: { 1197 + type: 'object', 1198 + required: ['tex'], 1199 properties: { 1200 tex: { 1201 + type: 'string', 1202 }, 1203 }, 1204 }, ··· 1206 }, 1207 PubLeafletBlocksPage: { 1208 lexicon: 1, 1209 + id: 'pub.leaflet.blocks.page', 1210 defs: { 1211 main: { 1212 + type: 'object', 1213 + required: ['id'], 1214 properties: { 1215 id: { 1216 + type: 'string', 1217 }, 1218 }, 1219 }, ··· 1221 }, 1222 PubLeafletBlocksPoll: { 1223 lexicon: 1, 1224 + id: 'pub.leaflet.blocks.poll', 1225 defs: { 1226 main: { 1227 + type: 'object', 1228 + required: ['pollRef'], 1229 properties: { 1230 pollRef: { 1231 + type: 'ref', 1232 + ref: 'lex:com.atproto.repo.strongRef', 1233 }, 1234 }, 1235 }, ··· 1237 }, 1238 PubLeafletBlocksText: { 1239 lexicon: 1, 1240 + id: 'pub.leaflet.blocks.text', 1241 defs: { 1242 main: { 1243 + type: 'object', 1244 + required: ['plaintext'], 1245 properties: { 1246 plaintext: { 1247 + type: 'string', 1248 + }, 1249 + textSize: { 1250 + type: 'string', 1251 + enum: ['default', 'small', 'large'], 1252 }, 1253 facets: { 1254 + type: 'array', 1255 items: { 1256 + type: 'ref', 1257 + ref: 'lex:pub.leaflet.richtext.facet', 1258 }, 1259 }, 1260 }, ··· 1263 }, 1264 PubLeafletBlocksUnorderedList: { 1265 lexicon: 1, 1266 + id: 'pub.leaflet.blocks.unorderedList', 1267 defs: { 1268 main: { 1269 + type: 'object', 1270 + required: ['children'], 1271 properties: { 1272 children: { 1273 + type: 'array', 1274 items: { 1275 + type: 'ref', 1276 + ref: 'lex:pub.leaflet.blocks.unorderedList#listItem', 1277 }, 1278 }, 1279 }, 1280 }, 1281 listItem: { 1282 + type: 'object', 1283 + required: ['content'], 1284 properties: { 1285 content: { 1286 + type: 'union', 1287 refs: [ 1288 + 'lex:pub.leaflet.blocks.text', 1289 + 'lex:pub.leaflet.blocks.header', 1290 + 'lex:pub.leaflet.blocks.image', 1291 ], 1292 }, 1293 children: { 1294 + type: 'array', 1295 items: { 1296 + type: 'ref', 1297 + ref: 'lex:pub.leaflet.blocks.unorderedList#listItem', 1298 }, 1299 }, 1300 }, ··· 1303 }, 1304 PubLeafletBlocksWebsite: { 1305 lexicon: 1, 1306 + id: 'pub.leaflet.blocks.website', 1307 defs: { 1308 main: { 1309 + type: 'object', 1310 + required: ['src'], 1311 properties: { 1312 previewImage: { 1313 + type: 'blob', 1314 + accept: ['image/*'], 1315 maxSize: 1000000, 1316 }, 1317 title: { 1318 + type: 'string', 1319 }, 1320 description: { 1321 + type: 'string', 1322 }, 1323 src: { 1324 + type: 'string', 1325 + format: 'uri', 1326 }, 1327 }, 1328 }, ··· 1330 }, 1331 PubLeafletComment: { 1332 lexicon: 1, 1333 + id: 'pub.leaflet.comment', 1334 revision: 1, 1335 + description: 'A lexicon for comments on documents', 1336 defs: { 1337 main: { 1338 + type: 'record', 1339 + key: 'tid', 1340 + description: 'Record containing a comment', 1341 record: { 1342 + type: 'object', 1343 + required: ['subject', 'plaintext', 'createdAt'], 1344 properties: { 1345 subject: { 1346 + type: 'string', 1347 + format: 'at-uri', 1348 }, 1349 createdAt: { 1350 + type: 'string', 1351 + format: 'datetime', 1352 }, 1353 reply: { 1354 + type: 'ref', 1355 + ref: 'lex:pub.leaflet.comment#replyRef', 1356 }, 1357 plaintext: { 1358 + type: 'string', 1359 }, 1360 facets: { 1361 + type: 'array', 1362 items: { 1363 + type: 'ref', 1364 + ref: 'lex:pub.leaflet.richtext.facet', 1365 }, 1366 }, 1367 onPage: { 1368 + type: 'string', 1369 }, 1370 attachment: { 1371 + type: 'union', 1372 + refs: ['lex:pub.leaflet.comment#linearDocumentQuote'], 1373 }, 1374 }, 1375 }, 1376 }, 1377 linearDocumentQuote: { 1378 + type: 'object', 1379 + required: ['document', 'quote'], 1380 properties: { 1381 document: { 1382 + type: 'string', 1383 + format: 'at-uri', 1384 }, 1385 quote: { 1386 + type: 'ref', 1387 + ref: 'lex:pub.leaflet.pages.linearDocument#quote', 1388 }, 1389 }, 1390 }, 1391 replyRef: { 1392 + type: 'object', 1393 + required: ['parent'], 1394 properties: { 1395 parent: { 1396 + type: 'string', 1397 + format: 'at-uri', 1398 }, 1399 }, 1400 }, ··· 1402 }, 1403 PubLeafletDocument: { 1404 lexicon: 1, 1405 + id: 'pub.leaflet.document', 1406 revision: 1, 1407 + description: 'A lexicon for long form rich media documents', 1408 defs: { 1409 main: { 1410 + type: 'record', 1411 + key: 'tid', 1412 + description: 'Record containing a document', 1413 record: { 1414 + type: 'object', 1415 + required: ['pages', 'author', 'title'], 1416 properties: { 1417 title: { 1418 + type: 'string', 1419 maxLength: 1280, 1420 maxGraphemes: 128, 1421 }, 1422 postRef: { 1423 + type: 'ref', 1424 + ref: 'lex:com.atproto.repo.strongRef', 1425 }, 1426 description: { 1427 + type: 'string', 1428 maxLength: 3000, 1429 maxGraphemes: 300, 1430 }, 1431 publishedAt: { 1432 + type: 'string', 1433 + format: 'datetime', 1434 }, 1435 publication: { 1436 + type: 'string', 1437 + format: 'at-uri', 1438 }, 1439 author: { 1440 + type: 'string', 1441 + format: 'at-identifier', 1442 }, 1443 theme: { 1444 + type: 'ref', 1445 + ref: 'lex:pub.leaflet.publication#theme', 1446 }, 1447 tags: { 1448 + type: 'array', 1449 items: { 1450 + type: 'string', 1451 maxLength: 50, 1452 }, 1453 }, 1454 coverImage: { 1455 + type: 'blob', 1456 + accept: ['image/png', 'image/jpeg', 'image/webp'], 1457 maxSize: 1000000, 1458 }, 1459 pages: { 1460 + type: 'array', 1461 items: { 1462 + type: 'union', 1463 refs: [ 1464 + 'lex:pub.leaflet.pages.linearDocument', 1465 + 'lex:pub.leaflet.pages.canvas', 1466 ], 1467 }, 1468 }, ··· 1473 }, 1474 PubLeafletGraphSubscription: { 1475 lexicon: 1, 1476 + id: 'pub.leaflet.graph.subscription', 1477 defs: { 1478 main: { 1479 + type: 'record', 1480 + key: 'tid', 1481 + description: 'Record declaring a subscription to a publication', 1482 record: { 1483 + type: 'object', 1484 + required: ['publication'], 1485 properties: { 1486 publication: { 1487 + type: 'string', 1488 + format: 'at-uri', 1489 }, 1490 }, 1491 }, ··· 1494 }, 1495 PubLeafletPagesCanvas: { 1496 lexicon: 1, 1497 + id: 'pub.leaflet.pages.canvas', 1498 defs: { 1499 main: { 1500 + type: 'object', 1501 + required: ['blocks'], 1502 properties: { 1503 id: { 1504 + type: 'string', 1505 }, 1506 blocks: { 1507 + type: 'array', 1508 items: { 1509 + type: 'ref', 1510 + ref: 'lex:pub.leaflet.pages.canvas#block', 1511 }, 1512 }, 1513 }, 1514 }, 1515 block: { 1516 + type: 'object', 1517 + required: ['block', 'x', 'y', 'width'], 1518 properties: { 1519 block: { 1520 + type: 'union', 1521 refs: [ 1522 + 'lex:pub.leaflet.blocks.iframe', 1523 + 'lex:pub.leaflet.blocks.text', 1524 + 'lex:pub.leaflet.blocks.blockquote', 1525 + 'lex:pub.leaflet.blocks.header', 1526 + 'lex:pub.leaflet.blocks.image', 1527 + 'lex:pub.leaflet.blocks.unorderedList', 1528 + 'lex:pub.leaflet.blocks.website', 1529 + 'lex:pub.leaflet.blocks.math', 1530 + 'lex:pub.leaflet.blocks.code', 1531 + 'lex:pub.leaflet.blocks.horizontalRule', 1532 + 'lex:pub.leaflet.blocks.bskyPost', 1533 + 'lex:pub.leaflet.blocks.page', 1534 + 'lex:pub.leaflet.blocks.poll', 1535 + 'lex:pub.leaflet.blocks.button', 1536 ], 1537 }, 1538 x: { 1539 + type: 'integer', 1540 }, 1541 y: { 1542 + type: 'integer', 1543 }, 1544 width: { 1545 + type: 'integer', 1546 }, 1547 height: { 1548 + type: 'integer', 1549 }, 1550 rotation: { 1551 + type: 'integer', 1552 + description: 'The rotation of the block in degrees', 1553 }, 1554 }, 1555 }, 1556 textAlignLeft: { 1557 + type: 'token', 1558 }, 1559 textAlignCenter: { 1560 + type: 'token', 1561 }, 1562 textAlignRight: { 1563 + type: 'token', 1564 }, 1565 quote: { 1566 + type: 'object', 1567 + required: ['start', 'end'], 1568 properties: { 1569 start: { 1570 + type: 'ref', 1571 + ref: 'lex:pub.leaflet.pages.canvas#position', 1572 }, 1573 end: { 1574 + type: 'ref', 1575 + ref: 'lex:pub.leaflet.pages.canvas#position', 1576 }, 1577 }, 1578 }, 1579 position: { 1580 + type: 'object', 1581 + required: ['block', 'offset'], 1582 properties: { 1583 block: { 1584 + type: 'array', 1585 items: { 1586 + type: 'integer', 1587 }, 1588 }, 1589 offset: { 1590 + type: 'integer', 1591 }, 1592 }, 1593 }, ··· 1595 }, 1596 PubLeafletPagesLinearDocument: { 1597 lexicon: 1, 1598 + id: 'pub.leaflet.pages.linearDocument', 1599 defs: { 1600 main: { 1601 + type: 'object', 1602 + required: ['blocks'], 1603 properties: { 1604 id: { 1605 + type: 'string', 1606 }, 1607 blocks: { 1608 + type: 'array', 1609 items: { 1610 + type: 'ref', 1611 + ref: 'lex:pub.leaflet.pages.linearDocument#block', 1612 }, 1613 }, 1614 }, 1615 }, 1616 block: { 1617 + type: 'object', 1618 + required: ['block'], 1619 properties: { 1620 block: { 1621 + type: 'union', 1622 refs: [ 1623 + 'lex:pub.leaflet.blocks.iframe', 1624 + 'lex:pub.leaflet.blocks.text', 1625 + 'lex:pub.leaflet.blocks.blockquote', 1626 + 'lex:pub.leaflet.blocks.header', 1627 + 'lex:pub.leaflet.blocks.image', 1628 + 'lex:pub.leaflet.blocks.unorderedList', 1629 + 'lex:pub.leaflet.blocks.website', 1630 + 'lex:pub.leaflet.blocks.math', 1631 + 'lex:pub.leaflet.blocks.code', 1632 + 'lex:pub.leaflet.blocks.horizontalRule', 1633 + 'lex:pub.leaflet.blocks.bskyPost', 1634 + 'lex:pub.leaflet.blocks.page', 1635 + 'lex:pub.leaflet.blocks.poll', 1636 + 'lex:pub.leaflet.blocks.button', 1637 ], 1638 }, 1639 alignment: { 1640 + type: 'string', 1641 knownValues: [ 1642 + 'lex:pub.leaflet.pages.linearDocument#textAlignLeft', 1643 + 'lex:pub.leaflet.pages.linearDocument#textAlignCenter', 1644 + 'lex:pub.leaflet.pages.linearDocument#textAlignRight', 1645 + 'lex:pub.leaflet.pages.linearDocument#textAlignJustify', 1646 ], 1647 }, 1648 }, 1649 }, 1650 textAlignLeft: { 1651 + type: 'token', 1652 }, 1653 textAlignCenter: { 1654 + type: 'token', 1655 }, 1656 textAlignRight: { 1657 + type: 'token', 1658 }, 1659 textAlignJustify: { 1660 + type: 'token', 1661 }, 1662 quote: { 1663 + type: 'object', 1664 + required: ['start', 'end'], 1665 properties: { 1666 start: { 1667 + type: 'ref', 1668 + ref: 'lex:pub.leaflet.pages.linearDocument#position', 1669 }, 1670 end: { 1671 + type: 'ref', 1672 + ref: 'lex:pub.leaflet.pages.linearDocument#position', 1673 }, 1674 }, 1675 }, 1676 position: { 1677 + type: 'object', 1678 + required: ['block', 'offset'], 1679 properties: { 1680 block: { 1681 + type: 'array', 1682 items: { 1683 + type: 'integer', 1684 }, 1685 }, 1686 offset: { 1687 + type: 'integer', 1688 }, 1689 }, 1690 }, ··· 1692 }, 1693 PubLeafletPollDefinition: { 1694 lexicon: 1, 1695 + id: 'pub.leaflet.poll.definition', 1696 defs: { 1697 main: { 1698 + type: 'record', 1699 + key: 'tid', 1700 + description: 'Record declaring a poll', 1701 record: { 1702 + type: 'object', 1703 + required: ['name', 'options'], 1704 properties: { 1705 name: { 1706 + type: 'string', 1707 maxLength: 500, 1708 maxGraphemes: 100, 1709 }, 1710 options: { 1711 + type: 'array', 1712 items: { 1713 + type: 'ref', 1714 + ref: 'lex:pub.leaflet.poll.definition#option', 1715 }, 1716 }, 1717 endDate: { 1718 + type: 'string', 1719 + format: 'datetime', 1720 }, 1721 }, 1722 }, 1723 }, 1724 option: { 1725 + type: 'object', 1726 properties: { 1727 text: { 1728 + type: 'string', 1729 maxLength: 500, 1730 maxGraphemes: 50, 1731 }, ··· 1735 }, 1736 PubLeafletPollVote: { 1737 lexicon: 1, 1738 + id: 'pub.leaflet.poll.vote', 1739 defs: { 1740 main: { 1741 + type: 'record', 1742 + key: 'tid', 1743 + description: 'Record declaring a vote on a poll', 1744 record: { 1745 + type: 'object', 1746 + required: ['poll', 'option'], 1747 properties: { 1748 poll: { 1749 + type: 'ref', 1750 + ref: 'lex:com.atproto.repo.strongRef', 1751 }, 1752 option: { 1753 + type: 'array', 1754 items: { 1755 + type: 'string', 1756 }, 1757 }, 1758 }, ··· 1762 }, 1763 PubLeafletPublication: { 1764 lexicon: 1, 1765 + id: 'pub.leaflet.publication', 1766 defs: { 1767 main: { 1768 + type: 'record', 1769 + key: 'tid', 1770 + description: 'Record declaring a publication', 1771 record: { 1772 + type: 'object', 1773 + required: ['name'], 1774 properties: { 1775 name: { 1776 + type: 'string', 1777 maxLength: 2000, 1778 }, 1779 base_path: { 1780 + type: 'string', 1781 }, 1782 description: { 1783 + type: 'string', 1784 maxLength: 2000, 1785 }, 1786 icon: { 1787 + type: 'blob', 1788 + accept: ['image/*'], 1789 maxSize: 1000000, 1790 }, 1791 theme: { 1792 + type: 'ref', 1793 + ref: 'lex:pub.leaflet.publication#theme', 1794 }, 1795 preferences: { 1796 + type: 'ref', 1797 + ref: 'lex:pub.leaflet.publication#preferences', 1798 }, 1799 }, 1800 }, 1801 }, 1802 preferences: { 1803 + type: 'object', 1804 properties: { 1805 showInDiscover: { 1806 + type: 'boolean', 1807 default: true, 1808 }, 1809 showComments: { 1810 + type: 'boolean', 1811 default: true, 1812 }, 1813 + showMentions: { 1814 + type: 'boolean', 1815 + default: true, 1816 + }, 1817 + showPrevNext: { 1818 + type: 'boolean', 1819 + default: false, 1820 + }, 1821 }, 1822 }, 1823 theme: { 1824 + type: 'object', 1825 properties: { 1826 backgroundColor: { 1827 + type: 'union', 1828 refs: [ 1829 + 'lex:pub.leaflet.theme.color#rgba', 1830 + 'lex:pub.leaflet.theme.color#rgb', 1831 ], 1832 }, 1833 backgroundImage: { 1834 + type: 'ref', 1835 + ref: 'lex:pub.leaflet.theme.backgroundImage', 1836 }, 1837 pageWidth: { 1838 + type: 'integer', 1839 + minimum: 0, 1840 + maximum: 1600, 1841 }, 1842 primary: { 1843 + type: 'union', 1844 refs: [ 1845 + 'lex:pub.leaflet.theme.color#rgba', 1846 + 'lex:pub.leaflet.theme.color#rgb', 1847 ], 1848 }, 1849 pageBackground: { 1850 + type: 'union', 1851 refs: [ 1852 + 'lex:pub.leaflet.theme.color#rgba', 1853 + 'lex:pub.leaflet.theme.color#rgb', 1854 ], 1855 }, 1856 showPageBackground: { 1857 + type: 'boolean', 1858 default: false, 1859 }, 1860 accentBackground: { 1861 + type: 'union', 1862 refs: [ 1863 + 'lex:pub.leaflet.theme.color#rgba', 1864 + 'lex:pub.leaflet.theme.color#rgb', 1865 ], 1866 }, 1867 accentText: { 1868 + type: 'union', 1869 refs: [ 1870 + 'lex:pub.leaflet.theme.color#rgba', 1871 + 'lex:pub.leaflet.theme.color#rgb', 1872 ], 1873 }, 1874 }, ··· 1877 }, 1878 PubLeafletRichtextFacet: { 1879 lexicon: 1, 1880 + id: 'pub.leaflet.richtext.facet', 1881 defs: { 1882 main: { 1883 + type: 'object', 1884 + description: 'Annotation of a sub-string within rich text.', 1885 + required: ['index', 'features'], 1886 properties: { 1887 index: { 1888 + type: 'ref', 1889 + ref: 'lex:pub.leaflet.richtext.facet#byteSlice', 1890 }, 1891 features: { 1892 + type: 'array', 1893 items: { 1894 + type: 'union', 1895 refs: [ 1896 + 'lex:pub.leaflet.richtext.facet#link', 1897 + 'lex:pub.leaflet.richtext.facet#didMention', 1898 + 'lex:pub.leaflet.richtext.facet#atMention', 1899 + 'lex:pub.leaflet.richtext.facet#code', 1900 + 'lex:pub.leaflet.richtext.facet#highlight', 1901 + 'lex:pub.leaflet.richtext.facet#underline', 1902 + 'lex:pub.leaflet.richtext.facet#strikethrough', 1903 + 'lex:pub.leaflet.richtext.facet#id', 1904 + 'lex:pub.leaflet.richtext.facet#bold', 1905 + 'lex:pub.leaflet.richtext.facet#italic', 1906 ], 1907 }, 1908 }, 1909 }, 1910 }, 1911 byteSlice: { 1912 + type: 'object', 1913 description: 1914 + 'Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.', 1915 + required: ['byteStart', 'byteEnd'], 1916 properties: { 1917 byteStart: { 1918 + type: 'integer', 1919 minimum: 0, 1920 }, 1921 byteEnd: { 1922 + type: 'integer', 1923 minimum: 0, 1924 }, 1925 }, 1926 }, 1927 link: { 1928 + type: 'object', 1929 description: 1930 + 'Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.', 1931 + required: ['uri'], 1932 properties: { 1933 uri: { 1934 + type: 'string', 1935 }, 1936 }, 1937 }, 1938 didMention: { 1939 + type: 'object', 1940 + description: 'Facet feature for mentioning a did.', 1941 + required: ['did'], 1942 properties: { 1943 did: { 1944 + type: 'string', 1945 + format: 'did', 1946 }, 1947 }, 1948 }, 1949 atMention: { 1950 + type: 'object', 1951 + description: 'Facet feature for mentioning an AT URI.', 1952 + required: ['atURI'], 1953 properties: { 1954 atURI: { 1955 + type: 'string', 1956 + format: 'uri', 1957 }, 1958 }, 1959 }, 1960 code: { 1961 + type: 'object', 1962 + description: 'Facet feature for inline code.', 1963 required: [], 1964 properties: {}, 1965 }, 1966 highlight: { 1967 + type: 'object', 1968 + description: 'Facet feature for highlighted text.', 1969 required: [], 1970 properties: {}, 1971 }, 1972 underline: { 1973 + type: 'object', 1974 + description: 'Facet feature for underline markup', 1975 required: [], 1976 properties: {}, 1977 }, 1978 strikethrough: { 1979 + type: 'object', 1980 + description: 'Facet feature for strikethrough markup', 1981 required: [], 1982 properties: {}, 1983 }, 1984 id: { 1985 + type: 'object', 1986 description: 1987 + 'Facet feature for an identifier. Used for linking to a segment', 1988 required: [], 1989 properties: { 1990 id: { 1991 + type: 'string', 1992 }, 1993 }, 1994 }, 1995 bold: { 1996 + type: 'object', 1997 + description: 'Facet feature for bold text', 1998 required: [], 1999 properties: {}, 2000 }, 2001 italic: { 2002 + type: 'object', 2003 + description: 'Facet feature for italic text', 2004 required: [], 2005 properties: {}, 2006 }, ··· 2008 }, 2009 PubLeafletThemeBackgroundImage: { 2010 lexicon: 1, 2011 + id: 'pub.leaflet.theme.backgroundImage', 2012 defs: { 2013 main: { 2014 + type: 'object', 2015 + required: ['image'], 2016 properties: { 2017 image: { 2018 + type: 'blob', 2019 + accept: ['image/*'], 2020 maxSize: 1000000, 2021 }, 2022 width: { 2023 + type: 'integer', 2024 }, 2025 repeat: { 2026 + type: 'boolean', 2027 }, 2028 }, 2029 }, ··· 2031 }, 2032 PubLeafletThemeColor: { 2033 lexicon: 1, 2034 + id: 'pub.leaflet.theme.color', 2035 defs: { 2036 rgba: { 2037 + type: 'object', 2038 + required: ['r', 'g', 'b', 'a'], 2039 properties: { 2040 r: { 2041 + type: 'integer', 2042 maximum: 255, 2043 minimum: 0, 2044 }, 2045 g: { 2046 + type: 'integer', 2047 maximum: 255, 2048 minimum: 0, 2049 }, 2050 b: { 2051 + type: 'integer', 2052 maximum: 255, 2053 minimum: 0, 2054 }, 2055 a: { 2056 + type: 'integer', 2057 maximum: 100, 2058 minimum: 0, 2059 }, 2060 }, 2061 }, 2062 rgb: { 2063 + type: 'object', 2064 + required: ['r', 'g', 'b'], 2065 properties: { 2066 r: { 2067 + type: 'integer', 2068 maximum: 255, 2069 minimum: 0, 2070 }, 2071 g: { 2072 + type: 'integer', 2073 maximum: 255, 2074 minimum: 0, 2075 }, 2076 b: { 2077 + type: 'integer', 2078 maximum: 255, 2079 minimum: 0, 2080 }, ··· 2082 }, 2083 }, 2084 }, 2085 + } as const satisfies Record<string, LexiconDoc> 2086 + export const schemas = Object.values(schemaDict) satisfies LexiconDoc[] 2087 + export const lexicons: Lexicons = new Lexicons(schemas) 2088 2089 export function validate<T extends { $type: string }>( 2090 v: unknown, 2091 id: string, 2092 hash: string, 2093 requiredType: true, 2094 + ): ValidationResult<T> 2095 export function validate<T extends { $type?: string }>( 2096 v: unknown, 2097 id: string, 2098 hash: string, 2099 requiredType?: false, 2100 + ): ValidationResult<T> 2101 export function validate( 2102 v: unknown, 2103 id: string, ··· 2109 : { 2110 success: false, 2111 error: new ValidationError( 2112 + `Must be an object with "${hash === 'main' ? id : `${id}#${hash}`}" $type property`, 2113 ), 2114 + } 2115 } 2116 2117 export const ids = { 2118 + AppBskyActorProfile: 'app.bsky.actor.profile', 2119 + ComAtprotoLabelDefs: 'com.atproto.label.defs', 2120 + ComAtprotoRepoApplyWrites: 'com.atproto.repo.applyWrites', 2121 + ComAtprotoRepoCreateRecord: 'com.atproto.repo.createRecord', 2122 + ComAtprotoRepoDefs: 'com.atproto.repo.defs', 2123 + ComAtprotoRepoDeleteRecord: 'com.atproto.repo.deleteRecord', 2124 + ComAtprotoRepoDescribeRepo: 'com.atproto.repo.describeRepo', 2125 + ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord', 2126 + ComAtprotoRepoImportRepo: 'com.atproto.repo.importRepo', 2127 + ComAtprotoRepoListMissingBlobs: 'com.atproto.repo.listMissingBlobs', 2128 + ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords', 2129 + ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord', 2130 + ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', 2131 + ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', 2132 + PubLeafletBlocksBlockquote: 'pub.leaflet.blocks.blockquote', 2133 + PubLeafletBlocksBskyPost: 'pub.leaflet.blocks.bskyPost', 2134 + PubLeafletBlocksButton: 'pub.leaflet.blocks.button', 2135 + PubLeafletBlocksCode: 'pub.leaflet.blocks.code', 2136 + PubLeafletBlocksHeader: 'pub.leaflet.blocks.header', 2137 + PubLeafletBlocksHorizontalRule: 'pub.leaflet.blocks.horizontalRule', 2138 + PubLeafletBlocksIframe: 'pub.leaflet.blocks.iframe', 2139 + PubLeafletBlocksImage: 'pub.leaflet.blocks.image', 2140 + PubLeafletBlocksMath: 'pub.leaflet.blocks.math', 2141 + PubLeafletBlocksPage: 'pub.leaflet.blocks.page', 2142 + PubLeafletBlocksPoll: 'pub.leaflet.blocks.poll', 2143 + PubLeafletBlocksText: 'pub.leaflet.blocks.text', 2144 + PubLeafletBlocksUnorderedList: 'pub.leaflet.blocks.unorderedList', 2145 + PubLeafletBlocksWebsite: 'pub.leaflet.blocks.website', 2146 + PubLeafletComment: 'pub.leaflet.comment', 2147 + PubLeafletDocument: 'pub.leaflet.document', 2148 + PubLeafletGraphSubscription: 'pub.leaflet.graph.subscription', 2149 + PubLeafletPagesCanvas: 'pub.leaflet.pages.canvas', 2150 + PubLeafletPagesLinearDocument: 'pub.leaflet.pages.linearDocument', 2151 + PubLeafletPollDefinition: 'pub.leaflet.poll.definition', 2152 + PubLeafletPollVote: 'pub.leaflet.poll.vote', 2153 + PubLeafletPublication: 'pub.leaflet.publication', 2154 + PubLeafletRichtextFacet: 'pub.leaflet.richtext.facet', 2155 + PubLeafletThemeBackgroundImage: 'pub.leaflet.theme.backgroundImage', 2156 + PubLeafletThemeColor: 'pub.leaflet.theme.color', 2157 + } as const
+1
lexicons/api/types/pub/leaflet/blocks/text.ts
··· 18 export interface Main { 19 $type?: 'pub.leaflet.blocks.text' 20 plaintext: string 21 facets?: PubLeafletRichtextFacet.Main[] 22 } 23
··· 18 export interface Main { 19 $type?: 'pub.leaflet.blocks.text' 20 plaintext: string 21 + textSize?: 'default' | 'small' | 'large' 22 facets?: PubLeafletRichtextFacet.Main[] 23 } 24
+39 -41
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 } 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 } 55 56 export interface Theme { 57 - $type?: "pub.leaflet.publication#theme"; 58 backgroundColor?: 59 | $Typed<PubLeafletThemeColor.Rgba> 60 | $Typed<PubLeafletThemeColor.Rgb> 61 - | { $type: string }; 62 - backgroundImage?: PubLeafletThemeBackgroundImage.Main; 63 - pageWidth?: number; 64 primary?: 65 | $Typed<PubLeafletThemeColor.Rgba> 66 | $Typed<PubLeafletThemeColor.Rgb> 67 - | { $type: string }; 68 pageBackground?: 69 | $Typed<PubLeafletThemeColor.Rgba> 70 | $Typed<PubLeafletThemeColor.Rgb> 71 - | { $type: string }; 72 - showPageBackground: boolean; 73 accentBackground?: 74 | $Typed<PubLeafletThemeColor.Rgba> 75 | $Typed<PubLeafletThemeColor.Rgb> 76 - | { $type: string }; 77 accentText?: 78 | $Typed<PubLeafletThemeColor.Rgba> 79 | $Typed<PubLeafletThemeColor.Rgb> 80 - | { $type: string }; 81 } 82 83 - const hashTheme = "theme"; 84 85 export function isTheme<V>(v: V) { 86 - return is$typed(v, id, hashTheme); 87 } 88 89 export function validateTheme<V>(v: V) { 90 - return validate<Theme & V>(v, id, hashTheme); 91 }
··· 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 } 43 44 + const hashPreferences = 'preferences' 45 46 export function isPreferences<V>(v: V) { 47 + return is$typed(v, id, hashPreferences) 48 } 49 50 export function validatePreferences<V>(v: V) { 51 + return validate<Preferences & V>(v, id, hashPreferences) 52 } 53 54 export interface Theme { 55 + $type?: 'pub.leaflet.publication#theme' 56 backgroundColor?: 57 | $Typed<PubLeafletThemeColor.Rgba> 58 | $Typed<PubLeafletThemeColor.Rgb> 59 + | { $type: string } 60 + backgroundImage?: PubLeafletThemeBackgroundImage.Main 61 + pageWidth?: number 62 primary?: 63 | $Typed<PubLeafletThemeColor.Rgba> 64 | $Typed<PubLeafletThemeColor.Rgb> 65 + | { $type: string } 66 pageBackground?: 67 | $Typed<PubLeafletThemeColor.Rgba> 68 | $Typed<PubLeafletThemeColor.Rgb> 69 + | { $type: string } 70 + showPageBackground: boolean 71 accentBackground?: 72 | $Typed<PubLeafletThemeColor.Rgba> 73 | $Typed<PubLeafletThemeColor.Rgb> 74 + | { $type: string } 75 accentText?: 76 | $Typed<PubLeafletThemeColor.Rgba> 77 | $Typed<PubLeafletThemeColor.Rgb> 78 + | { $type: string } 79 } 80 81 + const hashTheme = 'theme' 82 83 export function isTheme<V>(v: V) { 84 + return is$typed(v, id, hashTheme) 85 } 86 87 export function validateTheme<V>(v: V) { 88 + return validate<Theme & V>(v, id, hashTheme) 89 }
+8
lexicons/pub/leaflet/blocks/text.json
··· 11 "plaintext": { 12 "type": "string" 13 }, 14 "facets": { 15 "type": "array", 16 "items": {
··· 11 "plaintext": { 12 "type": "string" 13 }, 14 + "textSize": { 15 + "type": "string", 16 + "enum": [ 17 + "default", 18 + "small", 19 + "large" 20 + ] 21 + }, 22 "facets": { 23 "type": "array", 24 "items": {
+15 -3
lexicons/pub/leaflet/publication.json
··· 8 "description": "Record declaring a publication", 9 "record": { 10 "type": "object", 11 - "required": ["name"], 12 "properties": { 13 "name": { 14 "type": "string", ··· 23 }, 24 "icon": { 25 "type": "blob", 26 - "accept": ["image/*"], 27 "maxSize": 1000000 28 }, 29 "theme": { ··· 47 "showComments": { 48 "type": "boolean", 49 "default": true 50 } 51 } 52 }, ··· 104 } 105 } 106 } 107 - }
··· 8 "description": "Record declaring a publication", 9 "record": { 10 "type": "object", 11 + "required": [ 12 + "name" 13 + ], 14 "properties": { 15 "name": { 16 "type": "string", ··· 25 }, 26 "icon": { 27 "type": "blob", 28 + "accept": [ 29 + "image/*" 30 + ], 31 "maxSize": 1000000 32 }, 33 "theme": { ··· 51 "showComments": { 52 "type": "boolean", 53 "default": true 54 + }, 55 + "showMentions": { 56 + "type": "boolean", 57 + "default": true 58 + }, 59 + "showPrevNext": { 60 + "type": "boolean", 61 + "default": false 62 } 63 } 64 }, ··· 116 } 117 } 118 } 119 + }
+1
lexicons/src/blocks.ts
··· 10 required: ["plaintext"], 11 properties: { 12 plaintext: { type: "string" }, 13 facets: { 14 type: "array", 15 items: { type: "ref", ref: PubLeafletRichTextFacet.id },
··· 10 required: ["plaintext"], 11 properties: { 12 plaintext: { type: "string" }, 13 + textSize: { type: "string", enum: ["default", "small", "large"] }, 14 facets: { 15 type: "array", 16 items: { type: "ref", ref: PubLeafletRichTextFacet.id },
+2
lexicons/src/publication.ts
··· 27 properties: { 28 showInDiscover: { type: "boolean", default: true }, 29 showComments: { type: "boolean", default: true }, 30 }, 31 }, 32 theme: {
··· 27 properties: { 28 showInDiscover: { type: "boolean", default: true }, 29 showComments: { type: "boolean", default: true }, 30 + showMentions: { type: "boolean", default: true }, 31 + showPrevNext: { type: "boolean", default: false }, 32 }, 33 }, 34 theme: {
+8
src/replicache/attributes.ts
··· 71 type: "number", 72 cardinality: "one", 73 }, 74 "block/image": { 75 type: "image", 76 cardinality: "one", ··· 321 "text-alignment-type-union": { 322 type: "text-alignment-type-union"; 323 value: "right" | "left" | "center" | "justify"; 324 }; 325 "page-type-union": { type: "page-type-union"; value: "doc" | "canvas" }; 326 "block-type-union": {
··· 71 type: "number", 72 cardinality: "one", 73 }, 74 + "block/text-size": { 75 + type: "text-size-union", 76 + cardinality: "one", 77 + }, 78 "block/image": { 79 type: "image", 80 cardinality: "one", ··· 325 "text-alignment-type-union": { 326 type: "text-alignment-type-union"; 327 value: "right" | "left" | "center" | "justify"; 328 + }; 329 + "text-size-union": { 330 + type: "text-size-union"; 331 + value: "default" | "small" | "large"; 332 }; 333 "page-type-union": { type: "page-type-union"; value: "doc" | "canvas" }; 334 "block-type-union": {
+3
src/utils/getBlocksAsHTML.tsx
··· 171 }, 172 text: async (b, tx, a) => { 173 let [value] = await scanIndex(tx).eav(b.value, "block/text"); 174 return ( 175 <RenderYJSFragment 176 value={value?.data.value} 177 attrs={{ 178 "data-alignment": a, 179 }} 180 wrapper="p" 181 />
··· 171 }, 172 text: async (b, tx, a) => { 173 let [value] = await scanIndex(tx).eav(b.value, "block/text"); 174 + let [textSize] = await scanIndex(tx).eav(b.value, "block/text-size"); 175 + 176 return ( 177 <RenderYJSFragment 178 value={value?.data.value} 179 attrs={{ 180 "data-alignment": a, 181 + "data-text-size": textSize?.data.value, 182 }} 183 wrapper="p" 184 />