tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
a tool for shared writing and social publishing
284
fork
atom
overview
issues
27
pulls
pipelines
add a drawer for comments and mentions in reader
cozylittle.house
20 hours ago
f9fa02b9
55bed223
+187
-63
6 changed files
expand all
collapse all
unified
split
app
(home-pages)
reader
InboxContent.tsx
lish
[did]
[publication]
[rkey]
Interactions
Comments
index.tsx
InteractionDrawer.tsx
Quotes.tsx
getPostPageData.ts
components
PostListing.tsx
+82
-2
app/(home-pages)/reader/InboxContent.tsx
···
10
10
import { SortSmall } from "components/Icons/SortSmall";
11
11
import { Input } from "components/Input";
12
12
import { useHasBackgroundImage } from "components/Pages/useHasBackgroundImage";
13
13
-
import { InteractionDrawer } from "app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer";
13
13
+
import {
14
14
+
SelectedPostListing,
15
15
+
useSelectedPostListing,
16
16
+
} from "src/useSelectedPostState";
17
17
+
import { AtUri } from "@atproto/api";
18
18
+
import { MentionsDrawerContent } from "app/lish/[did]/[publication]/[rkey]/Interactions/Quotes";
19
19
+
import { CommentsDrawerContent } from "app/lish/[did]/[publication]/[rkey]/Interactions/Comments";
20
20
+
import { CloseTiny } from "components/Icons/CloseTiny";
21
21
+
import { SpeedyLink } from "components/SpeedyLink";
22
22
+
import { GoToArrow } from "components/Icons/GoToArrow";
14
23
15
24
export const InboxContent = (props: {
16
25
posts: Post[];
···
86
95
let hasBackgroundImage = useHasBackgroundImage();
87
96
88
97
return (
89
89
-
<div className="flex flex-row gap-6">
98
98
+
<div className="flex flex-row gap-6 ">
90
99
<div className="flex flex-col gap-6 relative">
91
100
<div className="flex justify-between gap-4 text-tertiary">
92
101
<Input
···
129
138
</div>
130
139
)}
131
140
</div>
141
141
+
<DesktopInteractionPreviewDrawer />
142
142
+
<MobileInteractionPreviewDrawer />
132
143
</div>
133
144
);
145
145
+
};
146
146
+
147
147
+
const MobileInteractionPreviewDrawer = () => {
148
148
+
let selectedPost = useSelectedPostListing((s) => s.selectedPostListing);
149
149
+
150
150
+
return (
151
151
+
<div
152
152
+
className={`z-20 fixed bottom-0 left-0 right-0 border border-border-light shrink-0 w-screen h-[90vh] px-3 bg-bg-leaflet rounded-t-lg overflow-auto ${selectedPost === null ? "hidden" : "block md:hidden "}`}
153
153
+
>
154
154
+
<PreviewDrawerContent selectedPost={selectedPost} />
155
155
+
</div>
156
156
+
);
157
157
+
};
158
158
+
const DesktopInteractionPreviewDrawer = () => {
159
159
+
let selectedPost = useSelectedPostListing((s) => s.selectedPostListing);
160
160
+
161
161
+
return (
162
162
+
<div
163
163
+
className={`hidden md:block border border-border-light shrink-0 w-96 mr-2 px-3 h-[calc(100vh-100px)] sticky top-11 bottom-4 right-0 rounded-lg overflow-auto ${selectedPost === null ? "shadow-none border-dashed bg-transparent" : "shadow-md border-border bg-bg-page "}`}
164
164
+
>
165
165
+
<PreviewDrawerContent selectedPost={selectedPost} />
166
166
+
</div>
167
167
+
);
168
168
+
};
169
169
+
170
170
+
const PreviewDrawerContent = (props: {
171
171
+
selectedPost: SelectedPostListing | null;
172
172
+
}) => {
173
173
+
if (!props.selectedPost || !props.selectedPost.document) return;
174
174
+
175
175
+
if (props.selectedPost.drawer === "quotes") {
176
176
+
return (
177
177
+
<>
178
178
+
{/*<MentionsDrawerContent
179
179
+
did={selectedPost.document_uri}
180
180
+
quotesAndMentions={[]}
181
181
+
/>*/}
182
182
+
</>
183
183
+
);
184
184
+
} else
185
185
+
return (
186
186
+
<>
187
187
+
<div className="w-full text-sm text-tertiary flex justify-between pt-3 gap-3">
188
188
+
<div className="truncate min-w-0 grow">
189
189
+
Comments for {props.selectedPost.document.title}
190
190
+
</div>
191
191
+
<button
192
192
+
className="text-tertiary"
193
193
+
onClick={() =>
194
194
+
useSelectedPostListing.getState().setSelectedPostListing(null)
195
195
+
}
196
196
+
>
197
197
+
<CloseTiny />
198
198
+
</button>
199
199
+
</div>
200
200
+
<SpeedyLink
201
201
+
className="shrink-0 flex gap-1 items-center "
202
202
+
href={"/"}
203
203
+
></SpeedyLink>
204
204
+
<ButtonPrimary fullWidth compact className="text-sm! mt-1">
205
205
+
See Full Post <GoToArrow />
206
206
+
</ButtonPrimary>
207
207
+
<CommentsDrawerContent
208
208
+
noCommentBox
209
209
+
document_uri={props.selectedPost.document_uri}
210
210
+
comments={[]}
211
211
+
/>
212
212
+
</>
213
213
+
);
134
214
};
135
215
136
216
export const ReaderEmpty = () => {
+13
-19
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
···
29
29
document_uri: string;
30
30
comments: Comment[];
31
31
pageId?: string;
32
32
+
noCommentBox?: boolean;
32
33
}) {
33
34
let { identity } = useIdentityData();
34
35
let { localComments } = useInteractionState(props.document_uri);
···
55
56
id={"commentsDrawer"}
56
57
className="flex flex-col gap-2 relative text-sm text-secondary"
57
58
>
58
58
-
<div className="w-full flex justify-between">
59
59
-
<h4> Comments</h4>
60
60
-
<button
61
61
-
className="text-tertiary"
62
62
-
onClick={() =>
63
63
-
setInteractionState(props.document_uri, { drawerOpen: false })
64
64
-
}
65
65
-
>
66
66
-
<CloseTiny />
67
67
-
</button>
68
68
-
</div>
69
69
-
{identity?.atp_did ? (
70
70
-
<CommentBox doc_uri={props.document_uri} pageId={props.pageId} />
71
71
-
) : (
72
72
-
<div className="w-full accent-container text-tertiary text-center italic p-3 flex flex-col gap-2">
73
73
-
Connect a Bluesky account to comment
74
74
-
<BlueskyLogin redirectRoute={redirectRoute} />
75
75
-
</div>
59
59
+
{!props.noCommentBox && (
60
60
+
<>
61
61
+
{identity?.atp_did ? (
62
62
+
<CommentBox doc_uri={props.document_uri} pageId={props.pageId} />
63
63
+
) : (
64
64
+
<div className="w-full accent-container text-tertiary text-center italic p-3 flex flex-col gap-2">
65
65
+
Connect a Bluesky account to comment
66
66
+
<BlueskyLogin redirectRoute={redirectRoute} />
67
67
+
</div>
68
68
+
)}
69
69
+
<hr className="border-border-light" />
70
70
+
</>
76
71
)}
77
77
-
<hr className="border-border-light" />
78
72
<div className="flex flex-col gap-4 py-2">
79
73
{comments
80
74
.sort((a, b) => {
+41
-11
app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx
···
1
1
"use client";
2
2
import { Media } from "components/Media";
3
3
import { MentionsDrawerContent } from "./Quotes";
4
4
-
import { InteractionState, useInteractionState } from "./Interactions";
4
4
+
import {
5
5
+
InteractionState,
6
6
+
setInteractionState,
7
7
+
useInteractionState,
8
8
+
} from "./Interactions";
5
9
import { Json } from "supabase/database.types";
6
10
import { Comment, CommentsDrawerContent } from "./Comments";
7
11
import { useSearchParams } from "next/navigation";
8
12
import { SandwichSpacer } from "components/LeafletLayout";
9
13
import { decodeQuotePosition } from "../quotePosition";
14
14
+
import { CloseTiny } from "components/Icons/CloseTiny";
10
15
11
16
export const InteractionDrawer = (props: {
12
17
showPageBackground: boolean | undefined;
···
39
44
<div className="snap-center h-full flex z-10 shrink-0 sm:max-w-prose sm:w-full w-[calc(100vw-12px)]">
40
45
<div
41
46
id="interaction-drawer"
42
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"}`}
47
47
+
className={`opaque-container relative h-full w-full px-3 sm:px-4 pt-2 sm:pt-3 pb-6 overflow-scroll flex flex-col ${props.showPageBackground ? "rounded-l-none! rounded-r-lg! -ml-[1px]" : "rounded-lg! sm:ml-4"}`}
43
48
>
44
49
{drawer.drawer === "quotes" ? (
45
45
-
<MentionsDrawerContent
46
46
-
{...props}
47
47
-
quotesAndMentions={filteredQuotesAndMentions}
48
48
-
/>
50
50
+
<>
51
51
+
<button
52
52
+
className="text-tertiary absolute top-0 right-0"
53
53
+
onClick={() =>
54
54
+
setInteractionState(props.document_uri, { drawerOpen: false })
55
55
+
}
56
56
+
>
57
57
+
<CloseTiny />
58
58
+
</button>
59
59
+
<MentionsDrawerContent
60
60
+
{...props}
61
61
+
quotesAndMentions={filteredQuotesAndMentions}
62
62
+
/>
63
63
+
</>
49
64
) : (
50
50
-
<CommentsDrawerContent
51
51
-
document_uri={props.document_uri}
52
52
-
comments={filteredComments}
53
53
-
pageId={props.pageId}
54
54
-
/>
65
65
+
<>
66
66
+
<div className="w-full flex justify-between">
67
67
+
<h4> Comments</h4>
68
68
+
<button
69
69
+
className="text-tertiary"
70
70
+
onClick={() =>
71
71
+
setInteractionState(props.document_uri, {
72
72
+
drawerOpen: false,
73
73
+
})
74
74
+
}
75
75
+
>
76
76
+
<CloseTiny />
77
77
+
</button>
78
78
+
</div>
79
79
+
<CommentsDrawerContent
80
80
+
document_uri={props.document_uri}
81
81
+
comments={filteredComments}
82
82
+
pageId={props.pageId}
83
83
+
/>
84
84
+
</>
55
85
)}
56
86
</div>
57
87
</div>
+2
-8
app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx
···
86
86
});
87
87
88
88
return (
89
89
-
<div className="relative w-full flex justify-between ">
90
90
-
<button
91
91
-
className="text-tertiary absolute top-0 right-0"
92
92
-
onClick={() => setInteractionState(document_uri, { drawerOpen: false })}
93
93
-
>
94
94
-
<CloseTiny />
95
95
-
</button>
89
89
+
<>
96
90
{props.quotesAndMentions.length === 0 ? (
97
91
<div className="opaque-container flex flex-col gap-0.5 p-[6px] text-tertiary italic text-sm text-center">
98
92
<div className="font-bold">no quotes yet!</div>
···
160
154
)}
161
155
</div>
162
156
)}
163
163
-
</div>
157
157
+
</>
164
158
);
165
159
};
166
160
+24
-13
app/lish/[did]/[publication]/[rkey]/getPostPageData.ts
···
3
3
import {
4
4
normalizeDocumentRecord,
5
5
normalizePublicationRecord,
6
6
-
type NormalizedDocument,
7
7
-
type NormalizedPublication,
8
6
} from "src/utils/normalizeRecords";
9
7
import { PubLeafletPublication, SiteStandardPublication } from "lexicons/api";
10
8
import { documentUriFilter } from "src/utils/uriHelpers";
···
33
31
if (!document) return null;
34
32
35
33
// Normalize the document record - this is the primary way consumers should access document data
36
36
-
const normalizedDocument = normalizeDocumentRecord(document.data, document.uri);
34
34
+
const normalizedDocument = normalizeDocumentRecord(
35
35
+
document.data,
36
36
+
document.uri,
37
37
+
);
37
38
if (!normalizedDocument) return null;
38
39
39
40
// Normalize the publication record - this is the primary way consumers should access publication data
40
41
const normalizedPublication = normalizePublicationRecord(
41
41
-
document.documents_in_publications[0]?.publications?.record
42
42
+
document.documents_in_publications[0]?.publications?.record,
42
43
);
43
44
44
45
// Fetch constellation backlinks for mentions
···
83
84
// Filter and sort documents by publishedAt
84
85
const sortedDocs = allDocs
85
86
.map((dip) => {
86
86
-
const normalizedData = normalizeDocumentRecord(dip?.documents?.data, dip?.documents?.uri);
87
87
+
const normalizedData = normalizeDocumentRecord(
88
88
+
dip?.documents?.data,
89
89
+
dip?.documents?.uri,
90
90
+
);
87
91
return {
88
92
uri: dip?.documents?.uri,
89
93
title: normalizedData?.title,
···
98
102
);
99
103
100
104
// Find current document index
101
101
-
const currentIndex = sortedDocs.findIndex((doc) => doc.uri === document.uri);
105
105
+
const currentIndex = sortedDocs.findIndex(
106
106
+
(doc) => doc.uri === document.uri,
107
107
+
);
102
108
103
109
if (currentIndex !== -1) {
104
110
prevNext = {
···
122
128
123
129
// Build explicit publication context for consumers
124
130
const rawPub = document.documents_in_publications[0]?.publications;
125
125
-
const publication = rawPub ? {
126
126
-
uri: rawPub.uri,
127
127
-
name: rawPub.name,
128
128
-
identity_did: rawPub.identity_did,
129
129
-
record: rawPub.record as PubLeafletPublication.Record | SiteStandardPublication.Record | null,
130
130
-
publication_subscriptions: rawPub.publication_subscriptions || [],
131
131
-
} : null;
131
131
+
const publication = rawPub
132
132
+
? {
133
133
+
uri: rawPub.uri,
134
134
+
name: rawPub.name,
135
135
+
identity_did: rawPub.identity_did,
136
136
+
record: rawPub.record as
137
137
+
| PubLeafletPublication.Record
138
138
+
| SiteStandardPublication.Record
139
139
+
| null,
140
140
+
publication_subscriptions: rawPub.publication_subscriptions || [],
141
141
+
}
142
142
+
: null;
132
143
133
144
return {
134
145
...document,
+25
-10
components/PostListing.tsx
···
15
15
import { useLocalizedDate } from "src/hooks/useLocalizedDate";
16
16
import { useSmoker } from "./Toast";
17
17
import { Separator } from "./Layout";
18
18
-
import { SpeedyLink } from "./SpeedyLink";
19
18
import { CommentTiny } from "./Icons/CommentTiny";
20
19
import { QuoteTiny } from "./Icons/QuoteTiny";
21
20
import { ShareTiny } from "./Icons/ShareTiny";
21
21
+
import { useSelectedPostListing } from "src/useSelectedPostState";
22
22
23
23
export const PostListing = (props: Post) => {
24
24
let pubRecord = props.publication?.pubRecord as
···
149
149
tags={tags}
150
150
showComments={pubRecord?.preferences?.showComments !== false}
151
151
showMentions={pubRecord?.preferences?.showMentions !== false}
152
152
-
/>{" "}
152
152
+
documentUri={props.documents.uri}
153
153
+
document={postRecord}
154
154
+
/>
153
155
<Share postUrl={postUrl} />
154
156
</div>
155
157
</div>
···
193
195
postUrl: string;
194
196
showComments: boolean;
195
197
showMentions: boolean;
198
198
+
documentUri: string;
199
199
+
document: NormalizedDocument;
196
200
}) => {
201
201
+
let setSelectedPostListing = useSelectedPostListing(
202
202
+
(s) => s.setSelectedPostListing,
203
203
+
);
204
204
+
let selectPostListing = (drawer: "quotes" | "comments") => {
205
205
+
setSelectedPostListing({
206
206
+
document_uri: props.documentUri,
207
207
+
document: props.document,
208
208
+
drawer,
209
209
+
});
210
210
+
};
211
211
+
197
212
return (
198
213
<div
199
214
className={`flex gap-2 text-tertiary text-sm items-center justify-between px-1`}
200
215
>
201
216
<div className="postListingsInteractions flex gap-3">
202
217
{!props.showMentions || props.quotesCount === 0 ? null : (
203
203
-
<SpeedyLink
218
218
+
<button
204
219
aria-label="Post quotes"
205
205
-
href={`${props.postUrl}?interactionDrawer=quotes`}
206
206
-
className="relative flex flex-row gap-1 text-sm items-center hover:text-accent-contrast hover:no-underline! text-tertiary"
220
220
+
onClick={() => selectPostListing("quotes")}
221
221
+
className="relative flex flex-row gap-1 text-sm items-center hover:text-accent-contrast text-tertiary"
207
222
>
208
223
<QuoteTiny /> {props.quotesCount}
209
209
-
</SpeedyLink>
224
224
+
</button>
210
225
)}
211
226
{!props.showComments || props.commentsCount === 0 ? null : (
212
212
-
<SpeedyLink
227
227
+
<button
213
228
aria-label="Post comments"
214
214
-
href={`${props.postUrl}?interactionDrawer=comments`}
215
215
-
className="relative flex flex-row gap-1 text-sm items-center hover:text-accent-contrast hover:no-underline! text-tertiary"
229
229
+
onClick={() => selectPostListing("comments")}
230
230
+
className="relative flex flex-row gap-1 text-sm items-center hover:text-accent-contrast text-tertiary"
216
231
>
217
232
<CommentTiny /> {props.commentsCount}
218
218
-
</SpeedyLink>
233
233
+
</button>
219
234
)}
220
235
</div>
221
236
</div>