tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
28
pulls
pipelines
implement subpage interaction drawers and buttons
awarm.space
4 months ago
fe62398c
5cc7518d
+107
-31
10 changed files
expand all
collapse all
unified
split
app
lish
[did]
[publication]
[rkey]
Interactions
Comments
index.tsx
InteractionDrawer.tsx
Interactions.tsx
PostPages.tsx
QuoteHandler.tsx
lexicons
api
lexicons.ts
types
pub
leaflet
comment.ts
pub
leaflet
comment.json
publication.json
src
comment.ts
+11
-2
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
···
23
23
uri: string;
24
24
bsky_profiles: { record: Json } | null;
25
25
};
26
26
-
export function Comments(props: { document_uri: string; comments: Comment[] }) {
26
26
+
export function Comments(props: {
27
27
+
document_uri: string;
28
28
+
comments: Comment[];
29
29
+
pageId?: string;
30
30
+
}) {
27
31
let { identity } = useIdentityData();
28
32
let { localComments } = useInteractionState(props.document_uri);
29
33
let comments = useMemo(() => {
30
30
-
return [...localComments, ...props.comments];
34
34
+
return [
35
35
+
...localComments.filter(
36
36
+
(c) => (c.record as any)?.onPage === props.pageId,
37
37
+
),
38
38
+
...props.comments,
39
39
+
];
31
40
}, [props.comments, localComments]);
32
41
let pathname = usePathname();
33
42
let redirectRoute = useMemo(() => {
+28
-7
app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx
···
1
1
"use client";
2
2
import { Media } from "components/Media";
3
3
import { Quotes } from "./Quotes";
4
4
-
import { useInteractionState } from "./Interactions";
4
4
+
import { InteractionState, useInteractionState } from "./Interactions";
5
5
import { Json } from "supabase/database.types";
6
6
import { Comment, Comments } from "./Comments";
7
7
import { useSearchParams } from "next/navigation";
8
8
import { SandwichSpacer } from "components/LeafletLayout";
9
9
+
import { decodeQuotePosition } from "../quotePosition";
9
10
10
11
export const InteractionDrawer = (props: {
11
12
document_uri: string;
12
13
quotes: { link: string; bsky_posts: { post_view: Json } | null }[];
13
14
comments: Comment[];
14
15
did: string;
16
16
+
pageId?: string;
15
17
}) => {
16
18
let drawer = useDrawerOpen(props.document_uri);
17
19
if (!drawer) return null;
20
20
+
21
21
+
// Filter comments and quotes based on pageId
22
22
+
const filteredComments = props.comments.filter(
23
23
+
(c) => (c.record as any)?.onPage === props.pageId,
24
24
+
);
25
25
+
26
26
+
const filteredQuotes = props.pageId
27
27
+
? props.quotes.filter((q) => q.link.includes(props.pageId!))
28
28
+
: props.quotes.filter((q) => {
29
29
+
const url = new URL(q.link);
30
30
+
const quoteParam = url.pathname.split("/l-quote/")[1];
31
31
+
if (!quoteParam) return null;
32
32
+
const quotePosition = decodeQuotePosition(quoteParam);
33
33
+
return !quotePosition?.pageId;
34
34
+
});
35
35
+
18
36
return (
19
37
<>
20
38
<SandwichSpacer noWidth />
21
21
-
<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)*.75)]">
39
39
+
<div className="snap-center h-full flex z-10 shrink-0 w-[calc(var(--page-width-units)-6px)] sm:w-[calc(var(--page-width-units))]">
22
40
<div
23
41
id="interaction-drawer"
24
42
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] "
25
43
>
26
26
-
{drawer === "quotes" ? (
27
27
-
<Quotes {...props} />
44
44
+
{drawer.drawer === "quotes" ? (
45
45
+
<Quotes {...props} quotes={filteredQuotes} />
28
46
) : (
29
47
<Comments
30
48
document_uri={props.document_uri}
31
31
-
comments={props.comments}
49
49
+
comments={filteredComments}
50
50
+
pageId={props.pageId}
32
51
/>
33
52
)}
34
53
</div>
···
40
59
export const useDrawerOpen = (uri: string) => {
41
60
let params = useSearchParams();
42
61
let interactionDrawerSearchParam = params.get("interactionDrawer");
43
43
-
let { drawerOpen: open, drawer } = useInteractionState(uri);
62
62
+
let { drawerOpen: open, drawer, pageId } = useInteractionState(uri);
44
63
if (open === false || (open === undefined && !interactionDrawerSearchParam))
45
64
return null;
46
46
-
return drawer || interactionDrawerSearchParam;
65
65
+
drawer =
66
66
+
drawer || (interactionDrawerSearchParam as InteractionState["drawer"]);
67
67
+
return { drawer, pageId };
47
68
};
+7
-4
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
···
10
10
import { PostPageContext } from "../PostPageContext";
11
11
import { scrollIntoView } from "src/utils/scrollIntoView";
12
12
13
13
-
type InteractionState = {
13
13
+
export type InteractionState = {
14
14
drawerOpen: undefined | boolean;
15
15
+
pageId?: string;
15
16
drawer: undefined | "comments" | "quotes";
16
17
localComments: Comment[];
17
18
commentBox: { quote: QuotePosition | null };
···
84
85
export function openInteractionDrawer(
85
86
drawer: "comments" | "quotes",
86
87
document_uri: string,
88
88
+
pageId?: string,
87
89
) {
88
90
flushSync(() => {
89
89
-
setInteractionState(document_uri, { drawerOpen: true, drawer });
91
91
+
setInteractionState(document_uri, { drawerOpen: true, drawer, pageId });
90
92
});
91
93
scrollIntoView("interaction-drawer");
92
94
}
···
97
99
compact?: boolean;
98
100
className?: string;
99
101
showComments?: boolean;
102
102
+
pageId?: string;
100
103
}) => {
101
104
const data = useContext(PostPageContext);
102
105
const document_uri = data?.uri;
···
113
116
className={`flex gap-1 items-center ${!props.compact && "px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline"}`}
114
117
onClick={() => {
115
118
if (!drawerOpen || drawer !== "quotes")
116
116
-
openInteractionDrawer("quotes", document_uri);
119
119
+
openInteractionDrawer("quotes", document_uri, props.pageId);
117
120
else setInteractionState(document_uri, { drawerOpen: false });
118
121
}}
119
122
>
···
130
133
className={`flex gap-1 items-center ${!props.compact && "px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline"}`}
131
134
onClick={() => {
132
135
if (!drawerOpen || drawer !== "comments")
133
133
-
openInteractionDrawer("comments", document_uri);
136
136
+
openInteractionDrawer("comments", document_uri, props.pageId);
134
137
else setInteractionState(document_uri, { drawerOpen: false });
135
138
}}
136
139
>
+51
-15
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
···
1
1
"use client";
2
2
import {
3
3
+
PubLeafletComment,
3
4
PubLeafletDocument,
4
5
PubLeafletPagesLinearDocument,
5
6
PubLeafletPublication,
···
119
120
preferences: { showComments?: boolean };
120
121
}) {
121
122
let { identity } = useIdentityData();
122
122
-
let drawerOpen = useDrawerOpen(document_uri);
123
123
+
let drawer = useDrawerOpen(document_uri);
123
124
useInitializeOpenPages();
124
125
let pages = useOpenPages();
125
126
if (!document || !document.documents_in_publications[0].publications)
126
127
return null;
127
128
128
129
let hasPageBackground = !!pubRecord.theme?.showPageBackground;
129
129
-
let fullPageScroll = !hasPageBackground && !drawerOpen && pages.length === 0;
130
130
+
let fullPageScroll = !hasPageBackground && !drawer && pages.length === 0;
130
131
let record = document.data as PubLeafletDocument.Record;
131
132
return (
132
133
<>
···
135
136
fullPageScroll={fullPageScroll}
136
137
cardBorderHidden={!hasPageBackground}
137
138
id={"post-page"}
138
138
-
drawerOpen={!!drawerOpen}
139
139
+
drawerOpen={!!drawer && !drawer.pageId}
139
140
>
140
141
<PostHeader
141
142
data={document}
···
152
153
<Interactions
153
154
showComments={preferences.showComments}
154
155
quotesCount={document.document_mentions_in_bsky.length}
155
155
-
commentsCount={document.comments_on_documents.length}
156
156
+
commentsCount={
157
157
+
document.comments_on_documents.filter(
158
158
+
(c) => !(c.record as PubLeafletComment.Record)?.onPage,
159
159
+
).length
160
160
+
}
156
161
/>
157
162
<hr className="border-border-light mb-4 mt-4 sm:mx-4 mx-3" />
158
163
<div className="pb-6 sm:px-4 px-3">
···
183
188
</div>
184
189
</PageWrapper>
185
190
186
186
-
<InteractionDrawer
187
187
-
document_uri={document.uri}
188
188
-
comments={
189
189
-
pubRecord.preferences?.showComments === false
190
190
-
? []
191
191
-
: document.comments_on_documents
192
192
-
}
193
193
-
quotes={document.document_mentions_in_bsky}
194
194
-
did={did}
195
195
-
/>
191
191
+
{drawer && !drawer.pageId && (
192
192
+
<InteractionDrawer
193
193
+
document_uri={document.uri}
194
194
+
comments={
195
195
+
pubRecord.preferences?.showComments === false
196
196
+
? []
197
197
+
: document.comments_on_documents
198
198
+
}
199
199
+
quotes={document.document_mentions_in_bsky}
200
200
+
did={did}
201
201
+
/>
202
202
+
)}
196
203
197
204
{pages.map((p) => {
198
205
let page = record.pages.find(
···
207
214
cardBorderHidden={!hasPageBackground}
208
215
id={`post-page-${p}`}
209
216
fullPageScroll={false}
210
210
-
drawerOpen={!!drawerOpen}
217
217
+
drawerOpen={!!drawer && drawer.pageId === page.id}
211
218
pageOptions={
212
219
<PageOptions
213
220
onClick={() => closePage(page?.id!)}
···
223
230
did={did}
224
231
prerenderedCodeBlocks={prerenderedCodeBlocks}
225
232
/>
233
233
+
<Interactions
234
234
+
pageId={page.id}
235
235
+
showComments={preferences.showComments}
236
236
+
quotesCount={
237
237
+
document.document_mentions_in_bsky.filter((q) =>
238
238
+
q.link.includes(page.id!),
239
239
+
).length
240
240
+
}
241
241
+
commentsCount={
242
242
+
document.comments_on_documents.filter(
243
243
+
(c) =>
244
244
+
(c.record as PubLeafletComment.Record)?.onPage ===
245
245
+
page.id,
246
246
+
).length
247
247
+
}
248
248
+
/>
226
249
</PageWrapper>
250
250
+
{drawer && drawer.pageId === page.id && (
251
251
+
<InteractionDrawer
252
252
+
pageId={page.id}
253
253
+
document_uri={document.uri}
254
254
+
comments={
255
255
+
pubRecord.preferences?.showComments === false
256
256
+
? []
257
257
+
: document.comments_on_documents
258
258
+
}
259
259
+
quotes={document.document_mentions_in_bsky}
260
260
+
did={did}
261
261
+
/>
262
262
+
)}
227
263
</Fragment>
228
264
);
229
265
})}
+1
app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx
···
212
212
setInteractionState(document_uri, {
213
213
drawer: "comments",
214
214
drawerOpen: true,
215
215
+
pageId: position.pageId,
215
216
commentBox: { quote: position },
216
217
}),
217
218
);
+3
-1
lexicons/api/lexicons.ts
···
1325
1325
ref: 'lex:pub.leaflet.richtext.facet',
1326
1326
},
1327
1327
},
1328
1328
+
onPage: {
1329
1329
+
type: 'string',
1330
1330
+
},
1328
1331
attachment: {
1329
1332
type: 'union',
1330
1333
refs: ['lex:pub.leaflet.comment#linearDocumentQuote'],
···
1541
1544
},
1542
1545
base_path: {
1543
1546
type: 'string',
1544
1544
-
format: 'uri',
1545
1547
},
1546
1548
description: {
1547
1549
type: 'string',
+1
lexicons/api/types/pub/leaflet/comment.ts
···
19
19
reply?: ReplyRef
20
20
plaintext: string
21
21
facets?: PubLeafletRichtextFacet.Main[]
22
22
+
onPage?: string
22
23
attachment?: $Typed<LinearDocumentQuote> | { $type: string }
23
24
[k: string]: unknown
24
25
}
+3
lexicons/pub/leaflet/comment.json
···
38
38
"ref": "pub.leaflet.richtext.facet"
39
39
}
40
40
},
41
41
+
"onPage": {
42
42
+
"type": "string"
43
43
+
},
41
44
"attachment": {
42
45
"type": "union",
43
46
"refs": [
+1
-2
lexicons/pub/leaflet/publication.json
···
17
17
"maxLength": 2000
18
18
},
19
19
"base_path": {
20
20
-
"type": "string",
21
21
-
"format": "uri"
20
20
+
"type": "string"
22
21
},
23
22
"description": {
24
23
"type": "string",
+1
lexicons/src/comment.ts
···
23
23
type: "array",
24
24
items: { type: "ref", ref: PubLeafletRichTextFacet.id },
25
25
},
26
26
+
onPage: { type: "string" },
26
27
attachment: { type: "union", refs: ["#linearDocumentQuote"] },
27
28
},
28
29
},