a tool for shared writing and social publishing

Compare changes

Choose any two refs to compare.

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