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