+67
-4
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/CommentBox.tsx
+67
-4
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/CommentBox.tsx
···
25
25
import { multi } from "linkifyjs";
26
26
import { Json } from "supabase/database.types";
27
27
import { isIOS } from "src/utils/isDevice";
28
+
import {
29
+
decodeQuotePosition,
30
+
QUOTE_PARAM,
31
+
QuotePosition,
32
+
} from "../../quotePosition";
33
+
import { QuoteContent } from "../Quotes";
34
+
import { create } from "zustand";
35
+
import { CloseTiny } from "components/Icons/CloseTiny";
36
+
import { CloseFillTiny } from "components/Icons/CloseFillTiny";
28
37
29
38
export function CommentBox(props: {
30
39
doc_uri: string;
···
32
41
onSubmit?: () => void;
33
42
}) {
34
43
let mountRef = useRef<HTMLPreElement | null>(null);
44
+
let quote = useInteractionState((s) => s.commentBox.quote);
35
45
let [editorState, setEditorState] = useState(() =>
36
46
EditorState.create({
37
47
schema: multiBlockSchema,
···
61
71
{ mount: mountRef.current },
62
72
{
63
73
state: editorState,
74
+
handlePaste: (view, e) => {
75
+
let text =
76
+
e.clipboardData?.getData("text") ||
77
+
e.clipboardData?.getData("text/html");
78
+
let html = e.clipboardData?.getData("text/html");
79
+
if (!text && html) {
80
+
let xml = new DOMParser().parseFromString(html, "text/html");
81
+
text = xml.textContent || "";
82
+
}
83
+
console.log("URL: " + window.location.toString());
84
+
console.log("TEXT: " + text, text?.includes(QUOTE_PARAM));
85
+
if (
86
+
text?.includes(QUOTE_PARAM) &&
87
+
text.includes(window.location.toString())
88
+
) {
89
+
const url = new URL(text);
90
+
const quoteParam = url.pathname.split("/l-quote/")[1];
91
+
if (!quoteParam) return;
92
+
const quotePosition = decodeQuotePosition(quoteParam);
93
+
if (!quotePosition) return;
94
+
useInteractionState.setState({
95
+
commentBox: { quote: quotePosition },
96
+
});
97
+
return true;
98
+
}
99
+
},
64
100
handleClickOn: (view, _pos, node, _nodePos, _event, direct) => {
65
101
if (!direct) return;
66
102
if (node.nodeSize - 2 <= _pos) return;
···
90
126
}, []);
91
127
let [loading, setLoading] = useState(false);
92
128
return (
93
-
<div className=" flex flex-col gap-1">
129
+
<div className=" flex flex-col">
130
+
{quote && (
131
+
<div className="relative mt-2 mb-2">
132
+
<QuoteContent position={quote} did="" index={-1} />
133
+
<button
134
+
className="text-border absolute -top-3 right-1 bg-bg-page p-1 rounded-full"
135
+
onClick={() =>
136
+
useInteractionState.setState({ commentBox: { quote: null } })
137
+
}
138
+
>
139
+
<CloseFillTiny />
140
+
</button>
141
+
</div>
142
+
)}
94
143
<div className="w-full relative group">
95
144
<pre
96
145
ref={mountRef}
97
-
className={`border whitespace-pre-wrap input-with-border min-h-32 h-fit`}
146
+
className={`border whitespace-pre-wrap input-with-border min-h-32 h-fit !px-2 !py-[6px]`}
98
147
/>
99
148
<IOSBS view={view} />
100
149
</div>
101
-
<div className="flex justify-between">
150
+
<div className="flex justify-between pt-1">
102
151
<div className="flex gap-1">
103
152
<TextDecorationButton
104
153
mark={multiBlockSchema.marks.strong}
···
126
175
let [plaintext, facets] = docToFacetedText(editorState.doc);
127
176
let comment = await publishComment({
128
177
document: props.doc_uri,
129
-
comment: { plaintext, facets, replyTo: props.replyTo },
178
+
comment: {
179
+
plaintext,
180
+
facets,
181
+
replyTo: props.replyTo,
182
+
attachment: quote
183
+
? {
184
+
$type: "pub.leaflet.comment#linearDocumentQuote",
185
+
document: props.doc_uri,
186
+
quote,
187
+
}
188
+
: undefined,
189
+
},
130
190
});
131
191
132
192
setLoading(false);
133
193
props.onSubmit?.();
134
194
useInteractionState.setState((s) => ({
195
+
commentBox: {
196
+
quote: null,
197
+
},
135
198
localComments: [
136
199
...s.localComments,
137
200
{
+2
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/commentAction.ts
+2
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/commentAction.ts
···
15
15
plaintext: string;
16
16
facets: PubLeafletRichtextFacet.Main[];
17
17
replyTo?: string;
18
+
attachment: PubLeafletComment.Record["attachment"];
18
19
};
19
20
}) {
20
21
const oauthClient = await createOauthClient();
···
31
32
plaintext: args.comment.plaintext,
32
33
facets: args.comment.facets,
33
34
reply: args.comment.replyTo ? { parent: args.comment.replyTo } : undefined,
35
+
attachment: args.comment.attachment,
34
36
};
35
37
let rkey = TID.nextStr();
36
38
let uri = AtUri.make(credentialSession.did!, "pub.leaflet.comment", rkey);
+14
-3
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
+14
-3
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
···
16
16
import { timeAgo } from "app/discover/PubListing";
17
17
import { BlueskyLogin } from "app/login/LoginForm";
18
18
import { usePathname } from "next/navigation";
19
+
import { QuoteContent } from "../Quotes";
19
20
20
21
export type Comment = {
21
22
record: Json;
···
57
58
</div>
58
59
)}
59
60
<hr className="border-border-light" />
60
-
<div className="flex flex-col gap-6">
61
+
<div className="flex flex-col gap-6 py-2">
61
62
{comments
62
63
.sort((a, b) => {
63
64
let aRecord = a.record as PubLeafletComment.Record;
···
99
100
}) => {
100
101
return (
101
102
<div className="comment">
102
-
<div className="flex gap-2 ">
103
+
<div className="flex gap-2">
103
104
{props.profile && (
104
105
<ProfilePopover profile={props.profile} comment={props.comment.uri} />
105
106
)}
106
107
<DatePopover date={props.record.createdAt} />
107
108
</div>
109
+
{props.record.attachment &&
110
+
PubLeafletComment.isLinearDocumentQuote(props.record.attachment) && (
111
+
<div className="mt-1 mb-2">
112
+
<QuoteContent
113
+
index={-1}
114
+
position={props.record.attachment.quote}
115
+
did={new AtUri(props.record.attachment.document).host}
116
+
/>
117
+
</div>
118
+
)}
108
119
<pre
109
120
key={props.comment.uri}
110
121
style={{ wordBreak: "break-word" }}
111
-
className="whitespace-pre-wrap text-secondary pb-[4px]"
122
+
className="whitespace-pre-wrap text-secondary pb-[4px] "
112
123
>
113
124
<BaseTextBlock
114
125
index={[]}
+2
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
+2
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
···
5
5
import type { Json } from "supabase/database.types";
6
6
import { create } from "zustand";
7
7
import type { Comment } from "./Comments";
8
+
import { QuotePosition } from "../quotePosition";
8
9
9
10
export let useInteractionState = create(() => ({
10
11
drawerOpen: undefined as boolean | undefined,
11
12
drawer: undefined as undefined | "comments" | "quotes",
12
13
localComments: [] as Comment[],
14
+
commentBox: { quote: null as QuotePosition | null },
13
15
}));
14
16
export function openInteractionDrawer(drawer: "comments" | "quotes") {
15
17
flushSync(() => {
+77
-68
app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx
+77
-68
app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx
···
15
15
PubLeafletPagesLinearDocument,
16
16
PubLeafletBlocksCode,
17
17
} from "lexicons/api";
18
-
import {
19
-
decodeQuotePosition,
20
-
QuotePosition,
21
-
useActiveHighlightState,
22
-
} from "../useHighlight";
18
+
import { decodeQuotePosition, QuotePosition } from "../quotePosition";
19
+
import { useActiveHighlightState } from "../useHighlight";
23
20
import { PostContent } from "../PostContent";
24
21
import { ProfileViewBasic } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
25
22
···
27
24
quotes: { link: string; bsky_posts: { post_view: Json } | null }[];
28
25
did: string;
29
26
}) => {
30
-
let isMobile = useIsMobile();
31
27
let data = useContext(PostPageContext);
32
28
33
29
return (
···
50
46
<div className="quotes flex flex-col gap-8">
51
47
{props.quotes.map((q, index) => {
52
48
let pv = q.bsky_posts?.post_view as unknown as PostView;
53
-
let record = data?.data as PubLeafletDocument.Record;
54
49
const url = new URL(q.link);
55
50
const quoteParam = url.pathname.split("/l-quote/")[1];
56
51
if (!quoteParam) return null;
57
52
const quotePosition = decodeQuotePosition(quoteParam);
58
53
if (!quotePosition) return null;
59
-
60
-
let page = record.pages[0] as PubLeafletPagesLinearDocument.Main;
61
-
// Extract blocks within the quote range
62
-
const content = extractQuotedBlocks(
63
-
page.blocks || [],
64
-
quotePosition,
65
-
[],
66
-
);
67
54
return (
68
-
<div
69
-
className="quoteSection flex flex-col gap-2"
70
-
key={index}
71
-
onMouseLeave={() => {
72
-
useActiveHighlightState.setState({ activeHighlight: null });
73
-
}}
74
-
onMouseEnter={() => {
75
-
useActiveHighlightState.setState({ activeHighlight: index });
76
-
}}
77
-
>
78
-
<div
79
-
className="quoteSectionQuote text-secondary text-sm text-left pb-1 hover:cursor-pointer"
80
-
onClick={(e) => {
81
-
let scrollMargin = isMobile
82
-
? 16
83
-
: e.currentTarget.getBoundingClientRect().top;
84
-
let scrollContainer =
85
-
window.document.getElementById("post-page");
86
-
let el = window.document.getElementById(
87
-
quotePosition.start.block.join("."),
88
-
);
89
-
if (!el || !scrollContainer) return;
90
-
let blockRect = el.getBoundingClientRect();
91
-
let quoteScrollTop =
92
-
(scrollContainer &&
93
-
blockRect.top + scrollContainer.scrollTop) ||
94
-
0;
55
+
<div key={index} className="flex flex-col ">
56
+
<QuoteContent
57
+
index={index}
58
+
did={props.did}
59
+
position={quotePosition}
60
+
/>
95
61
96
-
if (blockRect.left < 0)
97
-
scrollContainer.scrollIntoView({ behavior: "smooth" });
98
-
scrollContainer?.scrollTo({
99
-
top: quoteScrollTop - scrollMargin,
100
-
behavior: "smooth",
101
-
});
102
-
}}
103
-
>
104
-
<div className="italic">
105
-
<PostContent
106
-
bskyPostData={[]}
107
-
blocks={content || []}
108
-
did={props.did}
109
-
preview
110
-
/>
111
-
</div>
112
-
<BskyPost
113
-
rkey={new AtUri(pv.uri).rkey}
114
-
content={pv.record.text as string}
115
-
user={pv.author.displayName || pv.author.handle}
116
-
profile={pv.author}
117
-
handle={pv.author.handle}
118
-
/>
119
-
</div>
62
+
<div className="h-5 w-1 ml-5 border-l border-border-light" />
63
+
<BskyPost
64
+
rkey={new AtUri(pv.uri).rkey}
65
+
content={pv.record.text as string}
66
+
user={pv.author.displayName || pv.author.handle}
67
+
profile={pv.author}
68
+
handle={pv.author.handle}
69
+
/>
120
70
</div>
121
71
);
122
72
})}
···
126
76
);
127
77
};
128
78
79
+
export const QuoteContent = (props: {
80
+
position: QuotePosition;
81
+
index: number;
82
+
did: string;
83
+
}) => {
84
+
let isMobile = useIsMobile();
85
+
const data = useContext(PostPageContext);
86
+
87
+
let record = data?.data as PubLeafletDocument.Record;
88
+
let page = record.pages[0] as PubLeafletPagesLinearDocument.Main;
89
+
// Extract blocks within the quote range
90
+
const content = extractQuotedBlocks(page.blocks || [], props.position, []);
91
+
return (
92
+
<div
93
+
className="quoteSection"
94
+
onMouseLeave={() => {
95
+
useActiveHighlightState.setState({ activeHighlight: null });
96
+
}}
97
+
onMouseEnter={() => {
98
+
useActiveHighlightState.setState({ activeHighlight: props.index });
99
+
}}
100
+
>
101
+
<div
102
+
className="quoteSectionQuote text-secondary text-sm text-left hover:cursor-pointer"
103
+
onClick={(e) => {
104
+
let scrollMargin = isMobile
105
+
? 16
106
+
: e.currentTarget.getBoundingClientRect().top;
107
+
let scrollContainer = window.document.getElementById("post-page");
108
+
let el = window.document.getElementById(
109
+
props.position.start.block.join("."),
110
+
);
111
+
if (!el || !scrollContainer) return;
112
+
let blockRect = el.getBoundingClientRect();
113
+
let quoteScrollTop =
114
+
(scrollContainer && blockRect.top + scrollContainer.scrollTop) || 0;
115
+
116
+
if (blockRect.left < 0)
117
+
scrollContainer.scrollIntoView({ behavior: "smooth" });
118
+
scrollContainer?.scrollTo({
119
+
top: quoteScrollTop - scrollMargin,
120
+
behavior: "smooth",
121
+
});
122
+
}}
123
+
>
124
+
<div className="italic border border-border-light rounded-md px-2 pt-1">
125
+
<PostContent
126
+
bskyPostData={[]}
127
+
blocks={content}
128
+
did={props.did}
129
+
preview
130
+
className="!py-0"
131
+
/>
132
+
</div>
133
+
</div>
134
+
</div>
135
+
);
136
+
};
137
+
129
138
const BskyPost = (props: {
130
139
rkey: string;
131
140
content: string;
···
137
146
<a
138
147
target="_blank"
139
148
href={`https://bsky.app/profile/${props.handle}/post/${props.rkey}`}
140
-
className="quoteSectionBskyItem opaque-container py-2 px-2 text-sm flex gap-[6px] hover:no-underline font-normal"
149
+
className="quoteSectionBskyItem px-2 flex gap-[6px] hover:no-underline font-normal"
141
150
>
142
151
{props.profile.avatar && (
143
152
<img
···
151
160
<div className="font-bold">{props.user}</div>
152
161
<div className="text-tertiary">@{props.handle}</div>
153
162
</div>
154
-
<div className="text-secondary">{props.content}</div>
163
+
<div className="text-primary">{props.content}</div>
155
164
</div>
156
165
</a>
157
166
);
+8
-3
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
+8
-3
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
···
29
29
blocks,
30
30
did,
31
31
preview,
32
+
className,
32
33
prerenderedCodeBlocks,
33
34
bskyPostData,
34
35
}: {
35
36
blocks: PubLeafletPagesLinearDocument.Block[];
36
37
did: string;
37
38
preview?: boolean;
39
+
className?: string;
38
40
prerenderedCodeBlocks?: Map<string, string>;
39
41
bskyPostData: AppBskyFeedDefs.PostView[];
40
42
}) {
41
43
return (
42
-
<div id="post-content" className="postContent flex flex-col">
44
+
<div
45
+
id="post-content"
46
+
className={`postContent flex flex-col pb-1 sm:pb-2 pt-1 sm:pt-2 ${className}`}
47
+
>
43
48
{blocks.map((b, index) => {
44
49
return (
45
50
<Block
···
99
104
// non text blocks, they need this padding, pt-3 sm:pt-4, which is applied in each case
100
105
let className = `
101
106
postBlockWrapper
102
-
pt-1
103
107
min-h-7
104
-
${isList ? "isListItem pb-0 " : "pb-2 last:pb-3 last:sm:pb-4 first:pt-2 sm:first:pt-3"}
108
+
pt-1 pb-2
109
+
${isList && "isListItem !pb-0 "}
105
110
${alignment}
106
111
`;
107
112
+32
-10
app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx
+32
-10
app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx
···
4
4
import { Separator } from "components/Layout";
5
5
import { useSmoker } from "components/Toast";
6
6
import { useEffect, useMemo, useState } from "react";
7
+
import {
8
+
encodeQuotePosition,
9
+
decodeQuotePosition,
10
+
QuotePosition,
11
+
} from "./quotePosition";
12
+
import { useIdentityData } from "components/IdentityProvider";
13
+
import { CommentTiny } from "components/Icons/CommentTiny";
7
14
import { useInteractionState } from "./Interactions/Interactions";
8
-
import { encodeQuotePosition } from "./useHighlight";
9
-
import { useParams } from "next/navigation";
10
-
import { decodeQuotePosition, QuotePosition } from "./quotePosition";
11
15
12
16
export function QuoteHandler() {
13
17
let [position, setPosition] = useState<{
···
108
112
return (
109
113
<div
110
114
id="quote-trigger"
111
-
className={`accent-container border border-border-light text-accent-contrast px-2 flex gap-1 text-sm justify-center text-center items-center`}
115
+
className={`accent-container border border-border-light text-accent-contrast px-1 flex gap-1 text-sm justify-center text-center items-center`}
112
116
style={{
113
117
position: "absolute",
114
118
top: position.top,
···
123
127
124
128
export const QuoteOptionButtons = (props: { position: string }) => {
125
129
let smoker = useSmoker();
126
-
let url = useMemo(() => {
130
+
let { identity } = useIdentityData();
131
+
let [url, position] = useMemo(() => {
127
132
let currentUrl = new URL(window.location.href);
128
133
let pos = decodeQuotePosition(props.position);
129
134
if (currentUrl.pathname.includes("/l-quote/")) {
···
132
137
currentUrl.pathname = currentUrl.pathname + `/l-quote/${props.position}`;
133
138
134
139
currentUrl.hash = `#${pos?.start.block.join(".")}_${pos?.start.offset}`;
135
-
return currentUrl.toString();
140
+
return [currentUrl.toString(), pos];
136
141
}, [props.position]);
137
142
138
143
return (
139
144
<>
140
-
<div className="">Share Quote via</div>
145
+
<div className="">Share via</div>
141
146
142
147
<a
143
-
className="flex gap-1 items-center hover:font-bold p-1"
148
+
className="flex gap-1 items-center hover:font-bold px-1 hover:!no-underline"
144
149
role="link"
145
150
href={`https://bsky.app/intent/compose?text=${encodeURIComponent(url)}`}
146
151
target="_blank"
···
148
153
<BlueskyLinkTiny className="shrink-0" />
149
154
Bluesky
150
155
</a>
151
-
<Separator classname="h-3" />
156
+
<Separator classname="h-4" />
152
157
<button
153
158
id="copy-quote-link"
154
-
className="flex gap-1 items-center hover:font-bold p-1"
159
+
className="flex gap-1 items-center hover:font-bold px-1"
155
160
onClick={() => {
156
161
let rect = document
157
162
.getElementById("copy-quote-link")
···
171
176
<CopyTiny className="shrink-0" />
172
177
Link
173
178
</button>
179
+
<Separator classname="h-4" />
180
+
181
+
{identity?.atp_did && (
182
+
<button
183
+
className="flex gap-1 items-center hover:font-bold px-1"
184
+
onClick={() => {
185
+
if (!position) return;
186
+
useInteractionState.setState({
187
+
drawer: "comments",
188
+
drawerOpen: true,
189
+
commentBox: { quote: position },
190
+
});
191
+
}}
192
+
>
193
+
<CommentTiny /> Comment
194
+
</button>
195
+
)}
174
196
</>
175
197
);
176
198
};
+1
-1
app/lish/[did]/[publication]/[rkey]/quotePosition.ts
+1
-1
app/lish/[did]/[publication]/[rkey]/quotePosition.ts
+1
-55
app/lish/[did]/[publication]/[rkey]/useHighlight.tsx
+1
-55
app/lish/[did]/[publication]/[rkey]/useHighlight.tsx
···
5
5
import { useContext } from "react";
6
6
import { PostPageContext } from "./PostPageContext";
7
7
import { create } from "zustand";
8
-
9
-
export interface QuotePosition {
10
-
start: {
11
-
block: number[];
12
-
offset: number;
13
-
};
14
-
end: {
15
-
block: number[];
16
-
offset: number;
17
-
};
18
-
}
8
+
import { decodeQuotePosition } from "./quotePosition";
19
9
20
10
export const useActiveHighlightState = create(() => ({
21
11
activeHighlight: null as null | number,
···
77
67
})
78
68
.filter((highlight) => highlight !== null);
79
69
};
80
-
81
-
/**
82
-
* Encodes quote position into a URL-friendly string
83
-
* Format: startBlock_startOffset-endBlock_endOffset
84
-
* Block paths are joined with dots: 1.2.0_45-1.2.3_67
85
-
* Simple blocks: 0:12-2:45
86
-
*/
87
-
export function encodeQuotePosition(position: QuotePosition): string {
88
-
const { start, end } = position;
89
-
return `${start.block.join(".")}_${start.offset}-${end.block.join(".")}_${end.offset}`;
90
-
}
91
-
92
-
/**
93
-
* Decodes quote position from URL parameter
94
-
* Returns null if the format is invalid
95
-
*/
96
-
export function decodeQuotePosition(encoded: string): QuotePosition | null {
97
-
try {
98
-
// Match format: blockPath:number-blockPath:number
99
-
// Block paths can be: 5, 1.2, 0.1.3, etc.
100
-
const match = encoded.match(/^([\d.]+)_(\d+)-([\d.]+)_(\d+)$/);
101
-
102
-
if (!match) {
103
-
return null;
104
-
}
105
-
106
-
const [, startBlockPath, startOffset, endBlockPath, endOffset] = match;
107
-
108
-
const position: QuotePosition = {
109
-
start: {
110
-
block: startBlockPath.split(".").map((i) => parseInt(i)),
111
-
offset: parseInt(startOffset, 10),
112
-
},
113
-
end: {
114
-
block: endBlockPath.split(".").map((i) => parseInt(i)),
115
-
offset: parseInt(endOffset, 10),
116
-
},
117
-
};
118
-
119
-
return position;
120
-
} catch (error) {
121
-
return null;
122
-
}
123
-
}
+21
components/Icons/CloseFillTiny.tsx
+21
components/Icons/CloseFillTiny.tsx
···
1
+
import { Props } from "./Props";
2
+
3
+
export const CloseFillTiny = (props: Props) => {
4
+
return (
5
+
<svg
6
+
width="16"
7
+
height="16"
8
+
viewBox="0 0 16 16"
9
+
fill="none"
10
+
xmlns="http://www.w3.org/2000/svg"
11
+
{...props}
12
+
>
13
+
<path
14
+
fillRule="evenodd"
15
+
clipRule="evenodd"
16
+
d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM11.954 4.04603C12.282 4.37407 12.282 4.90593 11.954 5.23397L9.18794 8L11.954 10.766C12.282 11.0941 12.282 11.6259 11.954 11.954C11.6259 12.282 11.0941 12.282 10.766 11.954L8 9.18794L5.23397 11.954C4.90593 12.282 4.37407 12.282 4.04603 11.954C3.71799 11.6259 3.71799 11.0941 4.04603 10.766L6.81206 8L4.04603 5.23397C3.71799 4.90593 3.71799 4.37407 4.04603 4.04603C4.37407 3.71799 4.90593 3.71799 5.23397 4.04603L8 6.81206L10.766 4.04603C11.0941 3.71799 11.6259 3.71799 11.954 4.04603Z"
17
+
fill="currentColor"
18
+
/>
19
+
</svg>
20
+
);
21
+
};
+47
lexicons/api/lexicons.ts
+47
lexicons/api/lexicons.ts
···
46
46
ref: 'lex:pub.leaflet.richtext.facet',
47
47
},
48
48
},
49
+
attachment: {
50
+
type: 'union',
51
+
refs: ['lex:pub.leaflet.comment#linearDocumentQuote'],
52
+
},
53
+
},
54
+
},
55
+
},
56
+
linearDocumentQuote: {
57
+
type: 'object',
58
+
required: ['document', 'quote'],
59
+
properties: {
60
+
document: {
61
+
type: 'string',
62
+
format: 'at-uri',
63
+
},
64
+
quote: {
65
+
type: 'ref',
66
+
ref: 'lex:pub.leaflet.pages.linearDocument#quote',
49
67
},
50
68
},
51
69
},
···
551
569
},
552
570
textAlignRight: {
553
571
type: 'token',
572
+
},
573
+
quote: {
574
+
type: 'object',
575
+
required: ['start', 'end'],
576
+
properties: {
577
+
start: {
578
+
type: 'ref',
579
+
ref: 'lex:pub.leaflet.pages.linearDocument#position',
580
+
},
581
+
end: {
582
+
type: 'ref',
583
+
ref: 'lex:pub.leaflet.pages.linearDocument#position',
584
+
},
585
+
},
586
+
},
587
+
position: {
588
+
type: 'object',
589
+
required: ['block', 'offset'],
590
+
properties: {
591
+
block: {
592
+
type: 'array',
593
+
items: {
594
+
type: 'integer',
595
+
},
596
+
},
597
+
offset: {
598
+
type: 'integer',
599
+
},
600
+
},
554
601
},
555
602
},
556
603
},
+18
lexicons/api/types/pub/leaflet/comment.ts
+18
lexicons/api/types/pub/leaflet/comment.ts
···
6
6
import { validate as _validate } from '../../../lexicons'
7
7
import { $Typed, is$typed as _is$typed, OmitKey } from '../../../util'
8
8
import type * as PubLeafletRichtextFacet from './richtext/facet'
9
+
import type * as PubLeafletPagesLinearDocument from './pages/linearDocument'
9
10
10
11
const is$typed = _is$typed,
11
12
validate = _validate
···
18
19
reply?: ReplyRef
19
20
plaintext: string
20
21
facets?: PubLeafletRichtextFacet.Main[]
22
+
attachment?: $Typed<LinearDocumentQuote> | { $type: string }
21
23
[k: string]: unknown
22
24
}
23
25
···
29
31
30
32
export function validateRecord<V>(v: V) {
31
33
return validate<Record & V>(v, id, hashRecord, true)
34
+
}
35
+
36
+
export interface LinearDocumentQuote {
37
+
$type?: 'pub.leaflet.comment#linearDocumentQuote'
38
+
document: string
39
+
quote: PubLeafletPagesLinearDocument.Quote
40
+
}
41
+
42
+
const hashLinearDocumentQuote = 'linearDocumentQuote'
43
+
44
+
export function isLinearDocumentQuote<V>(v: V) {
45
+
return is$typed(v, id, hashLinearDocumentQuote)
46
+
}
47
+
48
+
export function validateLinearDocumentQuote<V>(v: V) {
49
+
return validate<LinearDocumentQuote & V>(v, id, hashLinearDocumentQuote)
32
50
}
33
51
34
52
export interface ReplyRef {
+32
lexicons/api/types/pub/leaflet/pages/linearDocument.ts
+32
lexicons/api/types/pub/leaflet/pages/linearDocument.ts
···
71
71
export const TEXTALIGNLEFT = `${id}#textAlignLeft`
72
72
export const TEXTALIGNCENTER = `${id}#textAlignCenter`
73
73
export const TEXTALIGNRIGHT = `${id}#textAlignRight`
74
+
75
+
export interface Quote {
76
+
$type?: 'pub.leaflet.pages.linearDocument#quote'
77
+
start: Position
78
+
end: Position
79
+
}
80
+
81
+
const hashQuote = 'quote'
82
+
83
+
export function isQuote<V>(v: V) {
84
+
return is$typed(v, id, hashQuote)
85
+
}
86
+
87
+
export function validateQuote<V>(v: V) {
88
+
return validate<Quote & V>(v, id, hashQuote)
89
+
}
90
+
91
+
export interface Position {
92
+
$type?: 'pub.leaflet.pages.linearDocument#position'
93
+
block: number[]
94
+
offset: number
95
+
}
96
+
97
+
const hashPosition = 'position'
98
+
99
+
export function isPosition<V>(v: V) {
100
+
return is$typed(v, id, hashPosition)
101
+
}
102
+
103
+
export function validatePosition<V>(v: V) {
104
+
return validate<Position & V>(v, id, hashPosition)
105
+
}
+23
lexicons/pub/leaflet/comment.json
+23
lexicons/pub/leaflet/comment.json
···
37
37
"type": "ref",
38
38
"ref": "pub.leaflet.richtext.facet"
39
39
}
40
+
},
41
+
"attachment": {
42
+
"type": "union",
43
+
"refs": [
44
+
"#linearDocumentQuote"
45
+
]
40
46
}
47
+
}
48
+
}
49
+
},
50
+
"linearDocumentQuote": {
51
+
"type": "object",
52
+
"required": [
53
+
"document",
54
+
"quote"
55
+
],
56
+
"properties": {
57
+
"document": {
58
+
"type": "string",
59
+
"format": "at-uri"
60
+
},
61
+
"quote": {
62
+
"type": "ref",
63
+
"ref": "pub.leaflet.pages.linearDocument#quote"
41
64
}
42
65
}
43
66
},
+35
lexicons/pub/leaflet/pages/linearDocument.json
+35
lexicons/pub/leaflet/pages/linearDocument.json
···
54
54
},
55
55
"textAlignRight": {
56
56
"type": "token"
57
+
},
58
+
"quote": {
59
+
"type": "object",
60
+
"required": [
61
+
"start",
62
+
"end"
63
+
],
64
+
"properties": {
65
+
"start": {
66
+
"type": "ref",
67
+
"ref": "#position"
68
+
},
69
+
"end": {
70
+
"type": "ref",
71
+
"ref": "#position"
72
+
}
73
+
}
74
+
},
75
+
"position": {
76
+
"type": "object",
77
+
"required": [
78
+
"block",
79
+
"offset"
80
+
],
81
+
"properties": {
82
+
"block": {
83
+
"type": "array",
84
+
"items": {
85
+
"type": "integer"
86
+
}
87
+
},
88
+
"offset": {
89
+
"type": "integer"
90
+
}
91
+
}
57
92
}
58
93
}
59
94
}
+9
lexicons/src/comment.ts
+9
lexicons/src/comment.ts
···
23
23
type: "array",
24
24
items: { type: "ref", ref: PubLeafletRichTextFacet.id },
25
25
},
26
+
attachment: { type: "union", refs: ["#linearDocumentQuote"] },
26
27
},
28
+
},
29
+
},
30
+
linearDocumentQuote: {
31
+
type: "object",
32
+
required: ["document", "quote"],
33
+
properties: {
34
+
document: { type: "string", format: "at-uri" },
35
+
quote: { type: "ref", ref: "pub.leaflet.pages.linearDocument#quote" },
27
36
},
28
37
},
29
38
replyRef: {
+16
lexicons/src/pages/LinearDocument.ts
+16
lexicons/src/pages/LinearDocument.ts
···
29
29
textAlignLeft: { type: "token" },
30
30
textAlignCenter: { type: "token" },
31
31
textAlignRight: { type: "token" },
32
+
quote: {
33
+
type: "object",
34
+
required: ["start", "end"],
35
+
properties: {
36
+
start: { type: "ref", ref: "#position" },
37
+
end: { type: "ref", ref: "#position" },
38
+
},
39
+
},
40
+
position: {
41
+
type: "object",
42
+
required: ["block", "offset"],
43
+
properties: {
44
+
block: { type: "array", items: { type: "integer" } },
45
+
offset: { type: "integer" },
46
+
},
47
+
},
32
48
},
33
49
};
+1
next-env.d.ts
+1
next-env.d.ts