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