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