+2
actions/publishToPublication.ts
+2
actions/publishToPublication.ts
···
784
784
root_entity,
785
785
"theme/background-image-repeat",
786
786
)?.[0];
787
+
let pageWidth = scan.eav(root_entity, "theme/page-width")?.[0];
787
788
788
789
let theme: PubLeafletPublication.Theme = {
789
790
showPageBackground: showPageBackground ?? true,
790
791
};
791
792
793
+
if (pageWidth) theme.pageWidth = pageWidth.data.value;
792
794
if (pageBackground)
793
795
theme.backgroundColor = ColorToRGBA(parseColor(`hsba(${pageBackground})`));
794
796
if (cardBackground)
+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
+2
-2
app/[leaflet_id]/actions/PublishButton.tsx
+2
-2
app/[leaflet_id]/actions/PublishButton.tsx
+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 && (
+4
-1
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
+4
-1
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
···
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
+2
-1
app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx
+2
-1
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[];
···
38
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))]">
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 -ml-[1px] ${props.showPageBackground ? "rounded-l-none! rounded-r-lg!" : "rounded-lg! sm:mx-2"}`}
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
/>
+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
+
};
+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={() => {
+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
};
+1
app/lish/[did]/[publication]/page.tsx
+1
app/lish/[did]/[publication]/page.tsx
+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();
+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`}
+5
-3
components/Pages/PublicationMetadata.tsx
+5
-3
components/Pages/PublicationMetadata.tsx
···
121
121
<Separator classname="h-4!" />
122
122
</>
123
123
)}
124
-
<div className="flex gap-1 items-center">
125
-
<QuoteTiny />โ
126
-
</div>
124
+
{pubRecord?.preferences?.showMentions && (
125
+
<div className="flex gap-1 items-center">
126
+
<QuoteTiny />โ
127
+
</div>
128
+
)}
127
129
{pubRecord?.preferences?.showComments && (
128
130
<div className="flex gap-1 items-center">
129
131
<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
};
+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
}
+8
lexicons/api/lexicons.ts
+8
lexicons/api/lexicons.ts
+2
lexicons/api/types/pub/leaflet/publication.ts
+2
lexicons/api/types/pub/leaflet/publication.ts
+8
lexicons/pub/leaflet/publication.json
+8
lexicons/pub/leaflet/publication.json
+2
lexicons/src/publication.ts
+2
lexicons/src/publication.ts